Compare commits
5 Commits
2c38cc8874
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
7b4d96845d
|
|||
|
937c6a1ce0
|
|||
|
6270d8c956
|
|||
|
1ae9c33f00
|
|||
|
b91b29d516
|
@@ -28,8 +28,8 @@ jobs:
|
|||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
browser-cli-client-version:
|
browser-cli-client-version:
|
||||||
- "0.9.3"
|
- "0.15.0"
|
||||||
- "0.9.5"
|
- "0.16.0"
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
|
|||||||
@@ -196,8 +196,9 @@ browser-cli search so click choices
|
|||||||
```sh
|
```sh
|
||||||
browser-cli tabs list # list all open tabs (all windows)
|
browser-cli tabs list # list all open tabs (all windows)
|
||||||
browser-cli tabs count # count all tabs
|
browser-cli tabs count # count all tabs
|
||||||
browser-cli tabs count youtube # count tabs matching URL pattern
|
browser-cli tabs count youtube # count tabs whose URL contains "youtube"
|
||||||
browser-cli tabs filter youtube # list tabs matching URL pattern
|
browser-cli tabs filter youtube # list tabs whose URL contains "youtube"
|
||||||
|
browser-cli tabs filter 'twitch.tv/*' # glob: list every twitch.tv tab
|
||||||
browser-cli tabs query "pull request" # search tabs by URL or title
|
browser-cli tabs query "pull request" # search tabs by URL or title
|
||||||
|
|
||||||
browser-cli tabs active 1234 # switch browser focus to tab
|
browser-cli tabs active 1234 # switch browser focus to tab
|
||||||
@@ -219,6 +220,10 @@ browser-cli tabs sort --by time
|
|||||||
browser-cli tabs merge-windows # pull all tabs into the current window
|
browser-cli tabs merge-windows # pull all tabs into the current window
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> URL patterns for `tabs filter` / `tabs count` match against the full tab URL.
|
||||||
|
> A plain string is a case-sensitive substring (`youtube`); a pattern containing
|
||||||
|
> `*` or `?` is treated as a glob (`twitch.tv/*`, `*.twitch.tv`).
|
||||||
|
|
||||||
### Tab groups
|
### Tab groups
|
||||||
```sh
|
```sh
|
||||||
browser-cli groups list # list all tab groups
|
browser-cli groups list # list all tab groups
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
"""Persistent server identity keys for SSH-style remote host pinning."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from cryptography.exceptions import InvalidSignature
|
||||||
|
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey, Ed25519PublicKey
|
||||||
|
from cryptography.hazmat.primitives.serialization import Encoding, NoEncryption, PrivateFormat, PublicFormat, load_pem_private_key
|
||||||
|
|
||||||
|
from browser_cli.constants import CONFIG_DIR
|
||||||
|
|
||||||
|
SERVER_IDENTITY_PATH = CONFIG_DIR / "server_identity.pem"
|
||||||
|
|
||||||
|
def load_or_create_server_identity(path: Path = SERVER_IDENTITY_PATH) -> Ed25519PrivateKey:
|
||||||
|
"""Load the persistent serve identity key, creating it on first start."""
|
||||||
|
if path.exists():
|
||||||
|
key = load_pem_private_key(path.read_bytes(), password=None)
|
||||||
|
if not isinstance(key, Ed25519PrivateKey):
|
||||||
|
raise ValueError(f"server identity key is not Ed25519: {path}")
|
||||||
|
return key
|
||||||
|
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
key = Ed25519PrivateKey.generate()
|
||||||
|
pem = key.private_bytes(Encoding.PEM, PrivateFormat.PKCS8, NoEncryption())
|
||||||
|
fd = path.open("xb")
|
||||||
|
try:
|
||||||
|
fd.write(pem)
|
||||||
|
finally:
|
||||||
|
fd.close()
|
||||||
|
path.chmod(0o600)
|
||||||
|
return key
|
||||||
|
|
||||||
|
def public_key_hex(key: Ed25519PrivateKey) -> str:
|
||||||
|
return key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw).hex()
|
||||||
|
|
||||||
|
def _signed_challenge_fields(challenge: dict) -> dict:
|
||||||
|
return {key: value for key, value in challenge.items() if key != "server_sig"}
|
||||||
|
|
||||||
|
def challenge_payload(challenge: dict) -> bytes:
|
||||||
|
"""Canonical bytes signed by the server identity key."""
|
||||||
|
return json.dumps(_signed_challenge_fields(challenge), sort_keys=True, separators=(",", ":")).encode("utf-8")
|
||||||
|
|
||||||
|
def sign_challenge(challenge: dict, key: Ed25519PrivateKey) -> str:
|
||||||
|
return key.sign(challenge_payload(challenge)).hex()
|
||||||
|
|
||||||
|
def verify_challenge_signature(challenge: dict) -> bool:
|
||||||
|
pub_hex = challenge.get("server_pubkey")
|
||||||
|
sig_hex = challenge.get("server_sig")
|
||||||
|
if not isinstance(pub_hex, str) or not isinstance(sig_hex, str):
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
pub = Ed25519PublicKey.from_public_bytes(bytes.fromhex(pub_hex))
|
||||||
|
pub.verify(bytes.fromhex(sig_hex), challenge_payload(challenge))
|
||||||
|
return True
|
||||||
|
except (InvalidSignature, ValueError):
|
||||||
|
return False
|
||||||
@@ -151,21 +151,25 @@ def active_browser_targets(*, include_remotes: bool = True, key=None, suppress_p
|
|||||||
targets.extend(_remote_browser_targets(key=key, suppress_pq_warning=suppress_pq_warning))
|
targets.extend(_remote_browser_targets(key=key, suppress_pq_warning=suppress_pq_warning))
|
||||||
return targets
|
return targets
|
||||||
|
|
||||||
def _cached_client_row(target: BrowserTarget) -> dict | None:
|
def _cached_client_row(target: BrowserTarget, *, scoped: bool = False) -> dict | None:
|
||||||
"""Build a clients row from a target's discovery data, skipping a roundtrip.
|
"""Build a clients row from a target's discovery data, skipping a roundtrip.
|
||||||
|
|
||||||
Returns None when the remote didn't advertise its version (older serve), so
|
Returns None when the remote didn't advertise its version (older serve), so
|
||||||
callers fall back to an explicit ``clients.list`` query.
|
callers fall back to an explicit ``clients.list`` query. When *scoped* is
|
||||||
|
true, the caller already selected one remote host, so render profile-only
|
||||||
|
labels instead of adding a host group header.
|
||||||
"""
|
"""
|
||||||
if target.version is None and target.extension_version is None:
|
if target.version is None and target.extension_version is None:
|
||||||
return None
|
return None
|
||||||
return {
|
row = {
|
||||||
"profile": target.display_name,
|
"profile": target.profile if scoped else target.display_name,
|
||||||
"profileGroup": target.display_group,
|
|
||||||
"name": target.browser_name or "",
|
"name": target.browser_name or "",
|
||||||
"version": target.version or "",
|
"version": target.version or "",
|
||||||
"extensionVersion": target.extension_version or "",
|
"extensionVersion": target.extension_version or "",
|
||||||
}
|
}
|
||||||
|
if target.display_group and not scoped:
|
||||||
|
row["profileGroup"] = target.display_group
|
||||||
|
return row
|
||||||
|
|
||||||
def _rows_from_result(result, label: str, profile_group: str | None) -> list[dict]:
|
def _rows_from_result(result, label: str, profile_group: str | None) -> list[dict]:
|
||||||
rows = []
|
rows = []
|
||||||
@@ -243,6 +247,34 @@ def collect_browser_clients(
|
|||||||
return rows
|
return rows
|
||||||
|
|
||||||
if remote:
|
if remote:
|
||||||
|
targets = remote_browser_targets(remote, key=key)
|
||||||
|
if browser_alias:
|
||||||
|
targets = [target for target in targets if target.profile == browser_alias or target.display_name == browser_alias]
|
||||||
|
if targets:
|
||||||
|
uncached = []
|
||||||
|
for target in targets:
|
||||||
|
cached = _cached_client_row(target, scoped=True)
|
||||||
|
if cached is not None:
|
||||||
|
rows.append(cached)
|
||||||
|
else:
|
||||||
|
uncached.append(target)
|
||||||
|
results = _run_concurrent([
|
||||||
|
(lambda t=t: _client_rows_async(
|
||||||
|
t.profile,
|
||||||
|
profile=t.profile,
|
||||||
|
remote=remote,
|
||||||
|
key=key,
|
||||||
|
))
|
||||||
|
for t in uncached
|
||||||
|
])
|
||||||
|
for result in results:
|
||||||
|
if isinstance(result, (BrowserNotConnected, RuntimeError)):
|
||||||
|
continue
|
||||||
|
if isinstance(result, BaseException):
|
||||||
|
raise result
|
||||||
|
rows.extend(result)
|
||||||
|
return rows
|
||||||
|
|
||||||
result = send_command("clients.list", profile=browser_alias, remote=remote, key=key)
|
result = send_command("clients.list", profile=browser_alias, remote=remote, key=key)
|
||||||
for item in result or []:
|
for item in result or []:
|
||||||
row = dict(item)
|
row = dict(item)
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import uuid
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from browser_cli import transport
|
from browser_cli import transport
|
||||||
from browser_cli.endpoints import _normalize_endpoint
|
|
||||||
from browser_cli.errors import BrowserNotConnected
|
from browser_cli.errors import BrowserNotConnected
|
||||||
|
from browser_cli.remote.registry import resolve_remote_endpoint
|
||||||
|
|
||||||
def base_message(command: str, args: dict | None) -> dict:
|
def base_message(command: str, args: dict | None) -> dict:
|
||||||
return {"id": str(uuid.uuid4()), "command": command, "args": args or {}}
|
return {"id": str(uuid.uuid4()), "command": command, "args": args or {}}
|
||||||
@@ -14,7 +14,7 @@ def base_message(command: str, args: dict | None) -> dict:
|
|||||||
def requested_target(profile: str | None, remote: str | None) -> tuple[str | None, str | None]:
|
def requested_target(profile: str | None, remote: str | None) -> tuple[str | None, str | None]:
|
||||||
requested_profile = profile or os.environ.get("BROWSER_CLI_PROFILE")
|
requested_profile = profile or os.environ.get("BROWSER_CLI_PROFILE")
|
||||||
remote_endpoint = remote or os.environ.get("BROWSER_CLI_REMOTE")
|
remote_endpoint = remote or os.environ.get("BROWSER_CLI_REMOTE")
|
||||||
return requested_profile, _normalize_endpoint(remote_endpoint) if remote_endpoint else None
|
return requested_profile, resolve_remote_endpoint(remote_endpoint) if remote_endpoint else None
|
||||||
|
|
||||||
def encode_payload(msg: dict) -> bytes:
|
def encode_payload(msg: dict) -> bytes:
|
||||||
return json.dumps(msg).encode("utf-8")
|
return json.dumps(msg).encode("utf-8")
|
||||||
|
|||||||
@@ -1,18 +1,53 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
|
||||||
|
|
||||||
import click
|
import click
|
||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
from rich.table import Table
|
from rich.table import Table
|
||||||
|
|
||||||
|
from browser_cli.errors import BrowserNotConnected
|
||||||
|
|
||||||
from browser_cli import BrowserCLI
|
from browser_cli import BrowserCLI
|
||||||
from browser_cli.commands import handle_errors
|
from browser_cli.commands import handle_errors
|
||||||
from browser_cli.commands.rendering import print_browser_grouped_table_rows
|
from browser_cli.commands.rendering import print_browser_grouped_table_rows
|
||||||
from browser_cli.remote.registry import REMOTE_REGISTRY_PATH, load_remotes, save_remote_key
|
from browser_cli.remote.known_hosts import fingerprint, load_known_hosts, remove_known_host, save_known_host
|
||||||
|
from browser_cli.remote.registry import load_remotes, remove_remote, save_remote, save_remote_key
|
||||||
|
|
||||||
console = Console()
|
console = Console()
|
||||||
|
|
||||||
|
def _print_remotes() -> None:
|
||||||
|
remotes = load_remotes()
|
||||||
|
if not remotes:
|
||||||
|
console.print("[yellow]No remembered remotes[/yellow]")
|
||||||
|
return
|
||||||
|
table = Table(show_header=True, header_style="bold cyan")
|
||||||
|
table.add_column("Endpoint")
|
||||||
|
table.add_column("Key")
|
||||||
|
for endpoint, cfg in sorted(remotes.items()):
|
||||||
|
table.add_row(endpoint, str(cfg.get("key", "")))
|
||||||
|
console.print(table)
|
||||||
|
|
||||||
|
def _remove_remote(endpoint: str, *, verb: str) -> None:
|
||||||
|
if not remove_remote(endpoint):
|
||||||
|
console.print(f"[yellow]Remote {endpoint} not remembered[/yellow]")
|
||||||
|
return
|
||||||
|
console.print(f"[green]{verb} {endpoint}[/green]")
|
||||||
|
|
||||||
|
def _fetch_server_pubkey(endpoint: str) -> str:
|
||||||
|
from browser_cli.auth.server_identity import verify_challenge_signature
|
||||||
|
from browser_cli.remote.auth import parse_challenge
|
||||||
|
from browser_cli.remote.socket import connect_socket, recv_all
|
||||||
|
|
||||||
|
sock = connect_socket(endpoint)
|
||||||
|
try:
|
||||||
|
challenge, _nonce = parse_challenge(recv_all(sock) or b"")
|
||||||
|
finally:
|
||||||
|
sock.close()
|
||||||
|
if not isinstance(challenge, dict) or not isinstance(challenge.get("server_pubkey"), str):
|
||||||
|
raise BrowserNotConnected("remote server did not advertise a server identity key")
|
||||||
|
if not verify_challenge_signature(challenge):
|
||||||
|
raise BrowserNotConnected("remote server identity signature is invalid")
|
||||||
|
return str(challenge["server_pubkey"])
|
||||||
|
|
||||||
@click.group("remote")
|
@click.group("remote")
|
||||||
def remote_group():
|
def remote_group():
|
||||||
"""Manage remembered browser-cli remote endpoints."""
|
"""Manage remembered browser-cli remote endpoints."""
|
||||||
@@ -42,6 +77,17 @@ def remote_status(endpoint, key):
|
|||||||
browser_header="Profile",
|
browser_header="Profile",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@remote_group.command("add")
|
||||||
|
@click.argument("endpoint")
|
||||||
|
@click.option("--key", "key_spec", default=None, help="Key spec/path to remember for this endpoint")
|
||||||
|
def remote_add(endpoint, key_spec):
|
||||||
|
"""Remember a remote endpoint for global multi-browser commands."""
|
||||||
|
save_remote(endpoint, key_spec)
|
||||||
|
if key_spec:
|
||||||
|
console.print(f"[green]Added remote {endpoint} with key {key_spec}[/green]")
|
||||||
|
else:
|
||||||
|
console.print(f"[green]Added remote {endpoint}[/green]")
|
||||||
|
|
||||||
@remote_group.command("trust")
|
@remote_group.command("trust")
|
||||||
@click.argument("endpoint")
|
@click.argument("endpoint")
|
||||||
@click.argument("key_spec")
|
@click.argument("key_spec")
|
||||||
@@ -50,29 +96,58 @@ def remote_trust(endpoint, key_spec):
|
|||||||
save_remote_key(endpoint, key_spec)
|
save_remote_key(endpoint, key_spec)
|
||||||
console.print(f"[green]Trusted remote {endpoint} with key {key_spec}[/green]")
|
console.print(f"[green]Trusted remote {endpoint} with key {key_spec}[/green]")
|
||||||
|
|
||||||
@remote_group.command("keys")
|
@remote_group.command("list")
|
||||||
def remote_keys():
|
def remote_list():
|
||||||
"""List remembered remote key specs."""
|
"""List remembered remote endpoints."""
|
||||||
remotes = load_remotes()
|
_print_remotes()
|
||||||
if not remotes:
|
|
||||||
console.print("[yellow]No remembered remotes[/yellow]")
|
@remote_group.command("trust-host")
|
||||||
|
@click.argument("endpoint")
|
||||||
|
@click.option("--pubkey", default=None, help="Pin this server public key instead of probing the endpoint")
|
||||||
|
@handle_errors
|
||||||
|
def remote_trust_host(endpoint, pubkey):
|
||||||
|
"""Pin a remote server identity key, SSH known_hosts style."""
|
||||||
|
server_pubkey = pubkey or _fetch_server_pubkey(endpoint)
|
||||||
|
save_known_host(endpoint, server_pubkey)
|
||||||
|
console.print(f"[green]Trusted server {endpoint}[/green] [dim]{fingerprint(server_pubkey)}[/dim]")
|
||||||
|
|
||||||
|
@remote_group.command("known-hosts")
|
||||||
|
def remote_known_hosts():
|
||||||
|
"""List pinned remote server identity keys."""
|
||||||
|
known = load_known_hosts()
|
||||||
|
if not known:
|
||||||
|
console.print("[yellow]No known remote server identities[/yellow]")
|
||||||
return
|
return
|
||||||
table = Table(show_header=True, header_style="bold cyan")
|
table = Table(show_header=True, header_style="bold cyan")
|
||||||
table.add_column("Endpoint")
|
table.add_column("Endpoint")
|
||||||
table.add_column("Key")
|
table.add_column("Fingerprint")
|
||||||
for endpoint, cfg in sorted(remotes.items()):
|
table.add_column("Public Key")
|
||||||
table.add_row(endpoint, str(cfg.get("key", "")))
|
for endpoint, pubkey in sorted(known.items()):
|
||||||
|
table.add_row(endpoint, fingerprint(pubkey), pubkey)
|
||||||
console.print(table)
|
console.print(table)
|
||||||
|
|
||||||
|
@remote_group.command("untrust-host")
|
||||||
|
@click.argument("endpoint")
|
||||||
|
def remote_untrust_host(endpoint):
|
||||||
|
"""Remove a pinned remote server identity key."""
|
||||||
|
if not remove_known_host(endpoint):
|
||||||
|
console.print(f"[yellow]Remote server {endpoint} is not in known hosts[/yellow]")
|
||||||
|
return
|
||||||
|
console.print(f"[green]Removed server identity for {endpoint}[/green]")
|
||||||
|
|
||||||
|
@remote_group.command("keys")
|
||||||
|
def remote_keys():
|
||||||
|
"""List remembered remote key specs."""
|
||||||
|
_print_remotes()
|
||||||
|
|
||||||
|
@remote_group.command("remove")
|
||||||
|
@click.argument("endpoint")
|
||||||
|
def remote_remove(endpoint):
|
||||||
|
"""Remove a remembered remote endpoint."""
|
||||||
|
_remove_remote(endpoint, verb="Removed")
|
||||||
|
|
||||||
@remote_group.command("revoke")
|
@remote_group.command("revoke")
|
||||||
@click.argument("endpoint")
|
@click.argument("endpoint")
|
||||||
def remote_revoke(endpoint):
|
def remote_revoke(endpoint):
|
||||||
"""Remove remembered key/config for ENDPOINT."""
|
"""Remove remembered key/config for ENDPOINT."""
|
||||||
remotes = load_remotes()
|
_remove_remote(endpoint, verb="Revoked")
|
||||||
if endpoint not in remotes:
|
|
||||||
console.print(f"[yellow]Remote {endpoint} not remembered[/yellow]")
|
|
||||||
return
|
|
||||||
del remotes[endpoint]
|
|
||||||
REMOTE_REGISTRY_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
||||||
REMOTE_REGISTRY_PATH.write_text(json.dumps(remotes, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
|
||||||
console.print(f"[green]Revoked {endpoint}[/green]")
|
|
||||||
|
|||||||
@@ -6,43 +6,23 @@ Add one entry per breaking auth-field change:
|
|||||||
("X.Y.Z", transformer_fn)
|
("X.Y.Z", transformer_fn)
|
||||||
|
|
||||||
Entries must stay in ascending version order.
|
Entries must stay in ascending version order.
|
||||||
|
|
||||||
|
The registry is intentionally empty: the first public release was 0.14.1, so no
|
||||||
|
legacy-client shim has ever been needed. This module is the seam — add a tuple
|
||||||
|
here (and a unit test for it) the day a breaking auth-field change ships.
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
from browser_cli.version_manager import parse_version
|
from browser_cli.version_manager import parse_version
|
||||||
|
|
||||||
|
|
||||||
# ── v0.9.3 ────────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
def _auth_0_9_3(msg: dict) -> dict:
|
|
||||||
"""pubkey validation tightened to lowercase hex; normalize for older clients."""
|
|
||||||
changed: dict = {}
|
|
||||||
pk = msg.get("pubkey")
|
|
||||||
if isinstance(pk, str) and pk:
|
|
||||||
changed["pubkey"] = pk.lower()
|
|
||||||
if msg.get("command") in {"browser-cli.auth.trust", "browser-cli.auth.policy"}:
|
|
||||||
args = msg.get("args") or {}
|
|
||||||
trust_pk = args.get("pubkey")
|
|
||||||
identifier = args.get("identifier")
|
|
||||||
patched = dict(args)
|
|
||||||
if isinstance(trust_pk, str) and trust_pk:
|
|
||||||
patched["pubkey"] = trust_pk.lower()
|
|
||||||
if isinstance(identifier, str) and identifier and len(identifier) == 64:
|
|
||||||
patched["identifier"] = identifier.lower()
|
|
||||||
if patched != args:
|
|
||||||
changed["args"] = patched
|
|
||||||
return {**msg, **changed} if changed else msg
|
|
||||||
|
|
||||||
|
|
||||||
# ── registry ──────────────────────────────────────────────────────────────────
|
# ── registry ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
_AUTH_COMPAT: list[tuple[str, Callable[[dict], dict]]] = [
|
_AUTH_COMPAT: list[tuple[str, Callable[[dict], dict]]] = []
|
||||||
("0.9.3", _auth_0_9_3),
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def adapt_auth(msg: dict, client_version: str) -> dict:
|
def adapt_auth(msg: dict, client_version: str) -> dict:
|
||||||
"""Apply all auth normalizers needed to bring msg up to the current format."""
|
"""Apply all auth normalizers needed to bring msg up to the current format."""
|
||||||
|
if not _AUTH_COMPAT:
|
||||||
|
return msg
|
||||||
cv = parse_version(client_version)
|
cv = parse_version(client_version)
|
||||||
for version, fn in _AUTH_COMPAT:
|
for version, fn in _AUTH_COMPAT:
|
||||||
if cv < parse_version(version):
|
if cv < parse_version(version):
|
||||||
|
|||||||
@@ -11,31 +11,34 @@ Add one entry per breaking command-format change:
|
|||||||
Entries must stay in ascending version order.
|
Entries must stay in ascending version order.
|
||||||
adapt_request walks forward (oldest first); adapt_response walks backward.
|
adapt_request walks forward (oldest first); adapt_response walks backward.
|
||||||
|
|
||||||
Current baseline: 0.9.3 — no command-format shims needed yet.
|
The registry is intentionally empty: no command-format shim has been needed
|
||||||
|
since the first public release (0.14.1). This module is the seam — add a tuple
|
||||||
|
here (and a unit test for it) the day a breaking command-format change ships.
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
from browser_cli.version_manager import parse_version
|
from browser_cli.version_manager import parse_version
|
||||||
|
|
||||||
|
|
||||||
# ── registry ──────────────────────────────────────────────────────────────────
|
# ── registry ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
_COMPAT: list[tuple[str, Callable[[dict], dict] | None, Callable[[bytes, str], bytes] | None]] = [
|
_COMPAT: list[tuple[str, Callable[[dict], dict] | None, Callable[[bytes, str], bytes] | None]] = [
|
||||||
# ("1.0.0", _req_1_0_0, _resp_1_0_0),
|
# ("1.0.0", _req_1_0_0, _resp_1_0_0),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def adapt_request(msg: dict, client_version: str) -> dict:
|
def adapt_request(msg: dict, client_version: str) -> dict:
|
||||||
"""Upgrade a client message to the current browser command format."""
|
"""Upgrade a client message to the current browser command format."""
|
||||||
|
if not _COMPAT:
|
||||||
|
return msg
|
||||||
cv = parse_version(client_version)
|
cv = parse_version(client_version)
|
||||||
for version, req_fn, _ in _COMPAT:
|
for version, req_fn, _ in _COMPAT:
|
||||||
if cv < parse_version(version) and req_fn is not None:
|
if cv < parse_version(version) and req_fn is not None:
|
||||||
msg = req_fn(msg)
|
msg = req_fn(msg)
|
||||||
return msg
|
return msg
|
||||||
|
|
||||||
|
|
||||||
def adapt_response(resp: bytes, command: str, client_version: str) -> bytes:
|
def adapt_response(resp: bytes, command: str, client_version: str) -> bytes:
|
||||||
"""Downgrade a native-host response to the format the client expects."""
|
"""Downgrade a native-host response to the format the client expects."""
|
||||||
|
if not _COMPAT:
|
||||||
|
return resp
|
||||||
cv = parse_version(client_version)
|
cv = parse_version(client_version)
|
||||||
for version, _, resp_fn in reversed(_COMPAT):
|
for version, _, resp_fn in reversed(_COMPAT):
|
||||||
if cv < parse_version(version) and resp_fn is not None:
|
if cv < parse_version(version) and resp_fn is not None:
|
||||||
|
|||||||
@@ -0,0 +1,108 @@
|
|||||||
|
"""SSH-style known-hosts pinning for browser-cli remote servers."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from browser_cli.constants import CONFIG_DIR
|
||||||
|
from browser_cli.endpoints import _normalize_endpoint
|
||||||
|
from browser_cli.errors import BrowserNotConnected
|
||||||
|
|
||||||
|
KNOWN_HOSTS_PATH = CONFIG_DIR / "known_hosts.json"
|
||||||
|
|
||||||
|
def fingerprint(pubkey_hex: str) -> str:
|
||||||
|
"""Return a compact SHA256 fingerprint for display."""
|
||||||
|
import base64
|
||||||
|
import hashlib
|
||||||
|
|
||||||
|
digest = hashlib.sha256(bytes.fromhex(pubkey_hex)).digest()
|
||||||
|
return "SHA256:" + base64.b64encode(digest).decode("ascii").rstrip("=")
|
||||||
|
|
||||||
|
def load_known_hosts(path: Path | None = None) -> dict[str, str]:
|
||||||
|
path = path or KNOWN_HOSTS_PATH
|
||||||
|
if not path.exists():
|
||||||
|
return {}
|
||||||
|
try:
|
||||||
|
data = json.loads(path.read_text(encoding="utf-8"))
|
||||||
|
except Exception:
|
||||||
|
return {}
|
||||||
|
if not isinstance(data, dict):
|
||||||
|
return {}
|
||||||
|
return {_normalize_endpoint(str(endpoint)): str(pubkey) for endpoint, pubkey in data.items() if isinstance(pubkey, str)}
|
||||||
|
|
||||||
|
def save_known_host(endpoint: str, pubkey_hex: str, path: Path | None = None) -> None:
|
||||||
|
path = path or KNOWN_HOSTS_PATH
|
||||||
|
known = load_known_hosts(path)
|
||||||
|
known[_normalize_endpoint(endpoint)] = pubkey_hex
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
fd = os.open(str(path), os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
|
||||||
|
with os.fdopen(fd, "w", encoding="utf-8") as file:
|
||||||
|
file.write(json.dumps(known, indent=2, sort_keys=True) + "\n")
|
||||||
|
|
||||||
|
def remove_known_host(endpoint: str, path: Path | None = None) -> bool:
|
||||||
|
path = path or KNOWN_HOSTS_PATH
|
||||||
|
known = load_known_hosts(path)
|
||||||
|
normalized = _normalize_endpoint(endpoint)
|
||||||
|
if normalized not in known:
|
||||||
|
return False
|
||||||
|
del known[normalized]
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
fd = os.open(str(path), os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
|
||||||
|
with os.fdopen(fd, "w", encoding="utf-8") as file:
|
||||||
|
file.write(json.dumps(known, indent=2, sort_keys=True) + "\n")
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _is_loopback_endpoint(endpoint: str) -> bool:
|
||||||
|
host, sep, _port = endpoint.rpartition(":")
|
||||||
|
check = host if sep else endpoint
|
||||||
|
return check in {"127.0.0.1", "localhost", "::1"}
|
||||||
|
|
||||||
|
def verify_known_host(endpoint: str, challenge: dict | None) -> None:
|
||||||
|
"""Verify and pin the server identity from a challenge frame.
|
||||||
|
|
||||||
|
First contact auto-adds the host when the process is interactive, mirroring
|
||||||
|
SSH's trust-on-first-use flow. Non-interactive clients must pin explicitly via
|
||||||
|
`browser-cli remote trust-host ENDPOINT`.
|
||||||
|
"""
|
||||||
|
if not isinstance(challenge, dict):
|
||||||
|
return
|
||||||
|
pubkey = challenge.get("server_pubkey")
|
||||||
|
if not isinstance(pubkey, str) or not pubkey:
|
||||||
|
return
|
||||||
|
|
||||||
|
from browser_cli.auth.server_identity import verify_challenge_signature
|
||||||
|
if not verify_challenge_signature(challenge):
|
||||||
|
raise BrowserNotConnected("Remote server identity signature is invalid")
|
||||||
|
|
||||||
|
normalized = _normalize_endpoint(endpoint)
|
||||||
|
known = load_known_hosts()
|
||||||
|
expected = known.get(normalized)
|
||||||
|
if expected is None and _is_loopback_endpoint(endpoint):
|
||||||
|
return
|
||||||
|
if expected is None:
|
||||||
|
if not sys.stdin.isatty():
|
||||||
|
raise BrowserNotConnected(
|
||||||
|
f"Unknown remote server identity for {normalized} ({fingerprint(pubkey)}).\n"
|
||||||
|
f"Run: browser-cli remote trust-host {normalized}"
|
||||||
|
)
|
||||||
|
sys.stderr.write(
|
||||||
|
f"The authenticity of remote '{normalized}' can't be established.\n"
|
||||||
|
f"Server key fingerprint is {fingerprint(pubkey)}.\n"
|
||||||
|
"Trust this server and add it to known hosts? [y/N] "
|
||||||
|
)
|
||||||
|
answer = sys.stdin.readline().strip().lower()
|
||||||
|
if answer not in {"y", "yes"}:
|
||||||
|
raise BrowserNotConnected("Remote server identity was not trusted")
|
||||||
|
save_known_host(normalized, pubkey)
|
||||||
|
sys.stderr.write(f"Added {normalized} to browser-cli known hosts.\n")
|
||||||
|
return
|
||||||
|
|
||||||
|
if expected != pubkey:
|
||||||
|
raise BrowserNotConnected(
|
||||||
|
f"REMOTE SERVER IDENTITY CHANGED for {normalized}!\n"
|
||||||
|
f"Known: {fingerprint(expected)}\n"
|
||||||
|
f"Seen: {fingerprint(pubkey)}\n"
|
||||||
|
f"If this is expected, run: browser-cli remote untrust-host {normalized} && browser-cli remote trust-host {normalized}"
|
||||||
|
)
|
||||||
@@ -22,24 +22,71 @@ def load_remotes() -> dict[str, dict[str, str]]:
|
|||||||
# Normalize keys so old entries stored as "domain:443" match current lookups.
|
# Normalize keys so old entries stored as "domain:443" match current lookups.
|
||||||
return {_normalize_endpoint(str(endpoint)): cfg for endpoint, cfg in data.items() if isinstance(cfg, dict)}
|
return {_normalize_endpoint(str(endpoint)): cfg for endpoint, cfg in data.items() if isinstance(cfg, dict)}
|
||||||
|
|
||||||
|
def resolve_remote_endpoint(endpoint: str | None) -> str | None:
|
||||||
|
"""Resolve a user-supplied remote alias to a remembered endpoint.
|
||||||
|
|
||||||
|
Domain-like remotes without an explicit port still default to :443 when no
|
||||||
|
matching remembered remote exists. If the user remembered exactly one
|
||||||
|
explicit-port remote for the same host (for example
|
||||||
|
``browser-host.example:8765``), use that endpoint so ``--remote
|
||||||
|
browser-host.example`` targets the stored service instead of assuming HTTPS.
|
||||||
|
"""
|
||||||
|
if not endpoint:
|
||||||
|
return None
|
||||||
|
normalized = _normalize_endpoint(endpoint)
|
||||||
|
host, sep, _port = normalized.rpartition(":")
|
||||||
|
if sep:
|
||||||
|
return normalized
|
||||||
|
|
||||||
|
remotes = load_remotes()
|
||||||
|
explicit_matches = []
|
||||||
|
for remote_endpoint in remotes:
|
||||||
|
remote_host, remote_sep, remote_port = remote_endpoint.rpartition(":")
|
||||||
|
if remote_sep and remote_host == normalized and remote_port != "443":
|
||||||
|
explicit_matches.append(remote_endpoint)
|
||||||
|
if len(explicit_matches) == 1:
|
||||||
|
return explicit_matches[0]
|
||||||
|
return normalized
|
||||||
|
|
||||||
def is_valid_key_spec(value: str) -> bool:
|
def is_valid_key_spec(value: str) -> bool:
|
||||||
"""Return True for 'agent', 'agent:<selector>', or a plausible key file path."""
|
"""Return True for 'agent', 'agent:<selector>', or a plausible key file path."""
|
||||||
return value == "agent" or value.startswith("agent:") or (
|
return value == "agent" or value.startswith("agent:") or (
|
||||||
not value.startswith("<") and ("/" in value or Path(value).suffix in {".pem", ".key"})
|
not value.startswith("<") and ("/" in value or Path(value).suffix in {".pem", ".key"})
|
||||||
)
|
)
|
||||||
|
|
||||||
def save_remote_key(endpoint: str, key_spec: str) -> None:
|
def save_remote(endpoint: str, key_spec: str | None = None) -> None:
|
||||||
"""Persist the key spec (e.g. 'agent' or a file path) for a remote endpoint."""
|
"""Persist a remote endpoint, optionally with a key spec."""
|
||||||
if not endpoint or not key_spec or not is_valid_key_spec(key_spec):
|
if not endpoint:
|
||||||
return
|
return
|
||||||
|
normalized = _normalize_endpoint(endpoint)
|
||||||
remotes = load_remotes()
|
remotes = load_remotes()
|
||||||
current = remotes.get(endpoint, {})
|
current = remotes.get(normalized, {})
|
||||||
|
if key_spec:
|
||||||
|
if not is_valid_key_spec(key_spec):
|
||||||
|
return
|
||||||
current["key"] = key_spec
|
current["key"] = key_spec
|
||||||
remotes[endpoint] = current
|
remotes[normalized] = current
|
||||||
REMOTE_REGISTRY_PATH.parent.mkdir(parents=True, exist_ok=True)
|
REMOTE_REGISTRY_PATH.parent.mkdir(parents=True, exist_ok=True)
|
||||||
fd = os.open(str(REMOTE_REGISTRY_PATH), os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
|
fd = os.open(str(REMOTE_REGISTRY_PATH), os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
|
||||||
with os.fdopen(fd, "w", encoding="utf-8") as f:
|
with os.fdopen(fd, "w", encoding="utf-8") as f:
|
||||||
f.write(json.dumps(remotes, indent=2, sort_keys=True))
|
f.write(json.dumps(remotes, indent=2, sort_keys=True) + "\n")
|
||||||
|
|
||||||
|
def remove_remote(endpoint: str) -> bool:
|
||||||
|
"""Remove a remembered remote endpoint. Returns True when it existed."""
|
||||||
|
normalized = _normalize_endpoint(endpoint)
|
||||||
|
remotes = load_remotes()
|
||||||
|
if normalized not in remotes:
|
||||||
|
return False
|
||||||
|
del remotes[normalized]
|
||||||
|
REMOTE_REGISTRY_PATH.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
fd = os.open(str(REMOTE_REGISTRY_PATH), os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
|
||||||
|
with os.fdopen(fd, "w", encoding="utf-8") as f:
|
||||||
|
f.write(json.dumps(remotes, indent=2, sort_keys=True) + "\n")
|
||||||
|
return True
|
||||||
|
|
||||||
|
def save_remote_key(endpoint: str, key_spec: str) -> None:
|
||||||
|
"""Persist the key spec (e.g. 'agent' or a file path) for a remote endpoint."""
|
||||||
|
save_remote(endpoint, key_spec)
|
||||||
|
|
||||||
def key_for_remote(endpoint: str | None) -> str | None:
|
def key_for_remote(endpoint: str | None) -> str | None:
|
||||||
if not endpoint:
|
if not endpoint:
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ from browser_cli.remote.socket import (
|
|||||||
split_endpoint as _split_endpoint,
|
split_endpoint as _split_endpoint,
|
||||||
)
|
)
|
||||||
from browser_cli.remote import pool as _pool
|
from browser_cli.remote import pool as _pool
|
||||||
|
from browser_cli.remote.known_hosts import verify_known_host
|
||||||
|
|
||||||
def _send_remote(endpoint: str, msg: dict, private_key=None, *, warn_no_pq: bool | None = None) -> bytes | None:
|
def _send_remote(endpoint: str, msg: dict, private_key=None, *, warn_no_pq: bool | None = None) -> bytes | None:
|
||||||
# Reuse an already-authenticated connection when one is idle for this endpoint.
|
# Reuse an already-authenticated connection when one is idle for this endpoint.
|
||||||
@@ -51,7 +52,10 @@ def _send_remote_handshake(endpoint: str, msg: dict, private_key=None, *, warn_n
|
|||||||
|
|
||||||
sock = _connect_socket(endpoint)
|
sock = _connect_socket(endpoint)
|
||||||
try:
|
try:
|
||||||
payload_msg, pq_shared_secret = _with_challenge(_recv_all(sock), msg, private_key, build_auth)
|
challenge_raw = _recv_all(sock)
|
||||||
|
challenge, _nonce_hex = _parse_challenge(challenge_raw)
|
||||||
|
verify_known_host(endpoint, challenge)
|
||||||
|
payload_msg, pq_shared_secret = _with_challenge(challenge_raw, msg, private_key, build_auth)
|
||||||
sock.sendall(frame(json.dumps(payload_msg).encode("utf-8")))
|
sock.sendall(frame(json.dumps(payload_msg).encode("utf-8")))
|
||||||
response = _decode_pq_response(_recv_all(sock), pq_shared_secret)
|
response = _decode_pq_response(_recv_all(sock), pq_shared_secret)
|
||||||
except BaseException:
|
except BaseException:
|
||||||
@@ -69,6 +73,8 @@ async def _send_remote_async(endpoint: str, msg: dict, private_key=None, *, warn
|
|||||||
reader, writer = await _open_async_connection(endpoint)
|
reader, writer = await _open_async_connection(endpoint)
|
||||||
try:
|
try:
|
||||||
challenge_raw = await _async_recv_all(reader)
|
challenge_raw = await _async_recv_all(reader)
|
||||||
|
challenge, _nonce_hex = _parse_challenge(challenge_raw)
|
||||||
|
verify_known_host(endpoint, challenge)
|
||||||
warn = _should_warn_no_pq(msg) if warn_no_pq is None else warn_no_pq
|
warn = _should_warn_no_pq(msg) if warn_no_pq is None else warn_no_pq
|
||||||
|
|
||||||
async def build_auth(sync_msg: dict, challenge: dict | None, nonce_hex: str | None, key):
|
async def build_auth(sync_msg: dict, challenge: dict | None, nonce_hex: str | None, key):
|
||||||
|
|||||||
@@ -28,4 +28,8 @@ async def build_challenge(auth_keys_path: Path | None) -> tuple[str, object | No
|
|||||||
if pq_keypair is not None:
|
if pq_keypair is not None:
|
||||||
pq_private_key, pq_public_key = pq_keypair
|
pq_private_key, pq_public_key = pq_keypair
|
||||||
challenge_msg["pq_kex"] = {"alg": PQ_KEX_ALG, "public_key": pq_public_key.hex()}
|
challenge_msg["pq_kex"] = {"alg": PQ_KEX_ALG, "public_key": pq_public_key.hex()}
|
||||||
|
from browser_cli.auth.server_identity import load_or_create_server_identity, public_key_hex, sign_challenge
|
||||||
|
server_key = await asyncio.to_thread(load_or_create_server_identity)
|
||||||
|
challenge_msg["server_pubkey"] = public_key_hex(server_key)
|
||||||
|
challenge_msg["server_sig"] = sign_challenge(challenge_msg, server_key)
|
||||||
return nonce, pq_private_key, challenge_msg
|
return nonce, pq_private_key, challenge_msg
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"name": "browser-cli",
|
"name": "browser-cli",
|
||||||
"version": "0.16.3",
|
"version": "0.16.6",
|
||||||
"description": "Control your browser from the terminal or Python SDK",
|
"description": "Control your browser from the terminal or Python SDK",
|
||||||
"browser_specific_settings": {
|
"browser_specific_settings": {
|
||||||
"gecko": {
|
"gecko": {
|
||||||
|
|||||||
@@ -4,6 +4,26 @@ import { CommandGroup } from '../classes/CommandGroup';
|
|||||||
import type { CommandEntry } from '../classes/CommandGroup';
|
import type { CommandEntry } from '../classes/CommandGroup';
|
||||||
import type { TabIdArgs, TabsActiveInWindowArgs, TabsPatternArgs, TabsQueryArgs, TabsWatchUrlArgs } from '../types';
|
import type { TabIdArgs, TabsActiveInWindowArgs, TabsPatternArgs, TabsQueryArgs, TabsWatchUrlArgs } from '../types';
|
||||||
|
|
||||||
|
/** Convert a shell-style glob (`*` = any run, `?` = any single char) to an
|
||||||
|
* unanchored RegExp. Every other character is matched literally. Unanchored so
|
||||||
|
* `twitch.tv/*` matches anywhere inside `https://www.twitch.tv/foo`. */
|
||||||
|
function globToRegExp(glob: string): RegExp {
|
||||||
|
const escaped = glob.replace(/[.+^${}()|[\]\\]/g, '\\$&');
|
||||||
|
return new RegExp(escaped.replace(/\*/g, '.*').replace(/\?/g, '.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Match a tab URL against a pattern. Backward-compatible: a pattern with no
|
||||||
|
* glob metacharacters is a plain case-sensitive substring match (the historic
|
||||||
|
* behavior); a pattern containing `*` or `?` is treated as a glob, so
|
||||||
|
* `twitch.tv/*` matches every Twitch tab.
|
||||||
|
*/
|
||||||
|
export function urlMatchesPattern(url: string | undefined, pattern: string): boolean {
|
||||||
|
if (!url || !pattern) return false;
|
||||||
|
if (/[*?]/.test(pattern)) return globToRegExp(pattern).test(url);
|
||||||
|
return url.includes(pattern);
|
||||||
|
}
|
||||||
|
|
||||||
export class TabsQueryCommands extends CommandGroup {
|
export class TabsQueryCommands extends CommandGroup {
|
||||||
readonly namespace = "tabs";
|
readonly namespace = "tabs";
|
||||||
readonly commands: Record<string, CommandEntry> = {
|
readonly commands: Record<string, CommandEntry> = {
|
||||||
@@ -50,12 +70,12 @@ export class TabsQueryCommands extends CommandGroup {
|
|||||||
|
|
||||||
private async tabsFilter({ pattern }: TabsPatternArgs) {
|
private async tabsFilter({ pattern }: TabsPatternArgs) {
|
||||||
const all = await api.tabs.query({});
|
const all = await api.tabs.query({});
|
||||||
return all.filter(t => t.url && t.url.includes(pattern)).map(tabInfo);
|
return all.filter(t => urlMatchesPattern(t.url, pattern)).map(tabInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async tabsCount({ pattern }: TabsPatternArgs) {
|
private async tabsCount({ pattern }: TabsPatternArgs) {
|
||||||
const all = await api.tabs.query({});
|
const all = await api.tabs.query({});
|
||||||
if (pattern) return all.filter(t => t.url && t.url.includes(pattern)).length;
|
if (pattern) return all.filter(t => urlMatchesPattern(t.url, pattern)).length;
|
||||||
return all.length;
|
return all.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
import test from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { urlMatchesPattern } from '../src/commands/tabs-query';
|
||||||
|
|
||||||
|
const TWITCH = 'https://www.twitch.tv/somechannel';
|
||||||
|
|
||||||
|
test('plain pattern is a case-sensitive substring match (historic behavior)', () => {
|
||||||
|
assert.equal(urlMatchesPattern(TWITCH, 'twitch.tv'), true);
|
||||||
|
assert.equal(urlMatchesPattern(TWITCH, 'somechannel'), true);
|
||||||
|
assert.equal(urlMatchesPattern(TWITCH, 'Twitch.tv'), false, 'case-sensitive');
|
||||||
|
assert.equal(urlMatchesPattern(TWITCH, 'youtube.com'), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('glob with /* matches anywhere in the URL', () => {
|
||||||
|
// The reported case: a glob, not a literal substring.
|
||||||
|
assert.equal(urlMatchesPattern(TWITCH, 'twitch.tv/*'), true);
|
||||||
|
assert.equal(urlMatchesPattern('https://www.twitch.tv/', 'twitch.tv/*'), true);
|
||||||
|
assert.equal(urlMatchesPattern('https://twitch.tv', 'twitch.tv/*'), false, 'no slash → no match');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('leading wildcard and ? wildcard work', () => {
|
||||||
|
assert.equal(urlMatchesPattern(TWITCH, '*.twitch.tv/*'), true);
|
||||||
|
assert.equal(urlMatchesPattern('https://a.twitch.tv/x', 'https://?.twitch.tv/*'), true);
|
||||||
|
assert.equal(urlMatchesPattern('https://ab.twitch.tv/x', 'https://?.twitch.tv/*'), false, '? is one char');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('regex metacharacters in a non-glob pattern stay literal', () => {
|
||||||
|
assert.equal(urlMatchesPattern('https://x.dev/a.b', 'a.b'), true);
|
||||||
|
assert.equal(urlMatchesPattern('https://x.dev/axb', 'a.b'), false, 'plain substring is literal — "." is not a regex wildcard');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('regex metacharacters next to a glob are escaped', () => {
|
||||||
|
// The "." must stay literal even when "*" promotes the pattern to a glob.
|
||||||
|
assert.equal(urlMatchesPattern('https://x.dev/foo', 'x.dev/*'), true);
|
||||||
|
assert.equal(urlMatchesPattern('https://xydev/foo', 'x.dev/*'), false, '. does not match y');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('empty url or pattern never matches', () => {
|
||||||
|
assert.equal(urlMatchesPattern('', 'twitch.tv'), false);
|
||||||
|
assert.equal(urlMatchesPattern(undefined, 'twitch.tv'), false);
|
||||||
|
assert.equal(urlMatchesPattern(TWITCH, ''), false);
|
||||||
|
});
|
||||||
@@ -44,27 +44,56 @@ Paste the contents of `n8n_key.pem` into the n8n credential.
|
|||||||
| Port | `serve` TCP port (default `8765`) |
|
| Port | `serve` TCP port (default `8765`) |
|
||||||
| Ed25519 Private Key | PKCS8 PEM from `browser-cli auth keygen` (empty only for `--no-auth` loopback) |
|
| Ed25519 Private Key | PKCS8 PEM from `browser-cli auth keygen` (empty only for `--no-auth` loopback) |
|
||||||
| Browser Alias | optional `_route` target — required if the endpoint serves multiple browsers |
|
| Browser Alias | optional `_route` target — required if the endpoint serves multiple browsers |
|
||||||
|
| Server Public Key/Fingerprint | pinned `browser-cli serve` identity (`SHA256:...` fingerprint or 64-char server public key hex) |
|
||||||
|
| Allow Unknown Server Identity | disables SSH-style server pinning; use only for loopback/dev |
|
||||||
| Use TLS | wrap the connection in TLS (only for a TLS-terminating proxy; the protocol is already encrypted) |
|
| Use TLS | wrap the connection in TLS (only for a TLS-terminating proxy; the protocol is already encrypted) |
|
||||||
| Ignore SSL Issues | when TLS is on, accept a self-signed proxy cert |
|
| Ignore SSL Issues | when TLS is on, accept a self-signed proxy cert |
|
||||||
|
|
||||||
|
### Server identity pinning
|
||||||
|
Recent `browser-cli serve` versions advertise a persistent Ed25519 server
|
||||||
|
identity in the challenge frame. The n8n node verifies the challenge signature
|
||||||
|
and compares the key against the credential's **Server Public Key/Fingerprint**
|
||||||
|
field, similar to SSH `known_hosts`.
|
||||||
|
|
||||||
|
On a trusted machine, pin the server once with the Python CLI and copy the
|
||||||
|
fingerprint into the n8n credential:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
browser-cli remote trust-host browser-host.example:8765
|
||||||
|
browser-cli remote known-hosts
|
||||||
|
```
|
||||||
|
|
||||||
|
If the server key changes, the node fails with `REMOTE SERVER IDENTITY CHANGED`.
|
||||||
|
Only enable **Allow Unknown Server Identity** for local/dev endpoints where you
|
||||||
|
explicitly do not want pinning.
|
||||||
|
|
||||||
## Operations
|
## Operations
|
||||||
Every operation maps to one raw browser-cli command, each subject to the server
|
Every operation maps to one raw browser-cli command, each subject to the server
|
||||||
policy tier noted below.
|
policy tier noted below.
|
||||||
|
|
||||||
| Resource | Operation | Command | Server flag needed |
|
| Resource | Operation | Command | Server flag needed |
|
||||||
|----------|-----------|---------|--------------------|
|
|----------|-----------|---------|--------------------|
|
||||||
| Tab | List | `tabs.list` | safe (default) |
|
| Tab | List / Query / Get / Count / Filter / Active in Window | `tabs.list` / `tabs.query` / `tabs.status` / `tabs.count` / `tabs.filter` / `tabs.active_in_window` | safe (default) |
|
||||||
| Tab | Open | `navigate.open` | `--allow-control` |
|
|
||||||
| Tab | Close | `tabs.close` (ids / inactive / duplicates) | `--allow-control` |
|
|
||||||
| Tab | Get HTML | `tabs.html` | `--allow-read-page` |
|
| Tab | Get HTML | `tabs.html` | `--allow-read-page` |
|
||||||
|
| Tab | Open / Close / Activate / Move / Navigate To / Reload / Hard Reload / Back / Forward | `navigate.open` / `tabs.close` / `tabs.active` / `tabs.move` / `navigate.to` / `navigate.reload` / `navigate.hard_reload` / `navigate.back` / `navigate.forward` | `--allow-control` |
|
||||||
|
| Tab | Mute / Unmute / Pin / Unpin / Dedupe / Sort / Merge Windows | `tabs.mute` / `tabs.unmute` / `tabs.pin` / `tabs.unpin` / `tabs.dedupe` / `tabs.sort` / `tabs.merge_windows` | `--allow-control` |
|
||||||
|
| Tab | Screenshot | `tabs.screenshot` | `--allow-dangerous` |
|
||||||
| Page | Get Info | `page.info` | safe (default) |
|
| Page | Get Info | `page.info` | safe (default) |
|
||||||
| Page | Extract Text / Links / Images / HTML / Markdown | `extract.*` | `--allow-read-page` |
|
| Page | Extract Text / Links / Images / HTML / Markdown / JSON | `extract.*` | `--allow-read-page` |
|
||||||
| DOM | Query | `dom.query` | `--allow-read-page` |
|
| DOM | Query / Text / Attribute / Exists | `dom.query` / `dom.text` / `dom.attr` / `dom.exists` | `--allow-read-page` |
|
||||||
| DOM | Click / Type | `dom.click` / `dom.type` | `--allow-control` |
|
| DOM | Click / Type / Select / Hover / Focus / Check / Uncheck / Clear / Submit / Scroll / Key | `dom.*` | `--allow-control` |
|
||||||
| DOM | Eval | `dom.eval` | `--allow-dangerous` |
|
| DOM | Eval | `dom.eval` | `--allow-dangerous` |
|
||||||
|
| Group | List / Query / Tabs | `group.list` / `group.query` / `group.tabs` | safe (default) |
|
||||||
|
| Group | Count / Create / Add Tab / Move / Close | `group.count` / `group.open` / `group.add_tab` / `group.move` / `group.close` | `--allow-control` |
|
||||||
|
| Window | List | `windows.list` | safe (default) |
|
||||||
|
| Window | Open / Close / Rename | `windows.open` / `windows.close` / `windows.rename` | `--allow-control` |
|
||||||
|
| Session | List / Save / Load / Remove / Export / Diff / Auto Save | `session.*` | `--allow-control` |
|
||||||
|
| Storage | Get / Set | `storage.get` / `storage.set` | `--allow-dangerous` |
|
||||||
|
| Performance | Status | `perf.status` | safe (default) |
|
||||||
|
| Extension | Info / Capabilities | `extension.info` / `extension.capabilities` | safe (default) |
|
||||||
|
| Extension | Reload | `extension.reload` | `--allow-control` |
|
||||||
| Client | List | `clients.list` | safe (default) |
|
| Client | List | `clients.list` | safe (default) |
|
||||||
| Command | Execute | any command name + JSON args | per command |
|
| Command | Execute | any command name + JSON args | per command |
|
||||||
| Gateway | Health | pings with `tabs.list` | safe (default) |
|
|
||||||
|
|
||||||
**Command → Execute** is the escape hatch: any command string the server policy
|
**Command → Execute** is the escape hatch: any command string the server policy
|
||||||
allows (`tabs.query`, `session.save`, `windows.list`, …) with a JSON args object.
|
allows (`tabs.query`, `session.save`, `windows.list`, …) with a JSON args object.
|
||||||
@@ -74,6 +103,12 @@ Use it for anything the typed operations don't cover.
|
|||||||
> `extract.markdown` therefore returns the page payload as the extension hands it
|
> `extract.markdown` therefore returns the page payload as the extension hands it
|
||||||
> back, not the CLI's rendered Markdown. For clean text use **Extract Text**.
|
> back, not the CLI's rendered Markdown. For clean text use **Extract Text**.
|
||||||
|
|
||||||
|
> **Tab → Filter / Count URL Pattern:** matched against the full tab URL. A plain
|
||||||
|
> string is a case-sensitive substring (`twitch.tv`); a pattern containing `*` or
|
||||||
|
> `?` is a glob (`twitch.tv/*`, `*.twitch.tv`). Glob needs the serve-side extension
|
||||||
|
> at **0.16.4+**; older extensions treat the whole pattern as a literal substring,
|
||||||
|
> so `twitch.tv/*` matches nothing there — use `twitch.tv` instead.
|
||||||
|
|
||||||
## Develop / build
|
## Develop / build
|
||||||
```bash
|
```bash
|
||||||
cd n8n-nodes-browser-cli
|
cd n8n-nodes-browser-cli
|
||||||
|
|||||||
@@ -65,6 +65,23 @@ export class BrowserCliApi implements ICredentialType {
|
|||||||
description:
|
description:
|
||||||
'Optional browser alias to route to (the serve `_route` target). Required when the serve endpoint exposes multiple browser instances; leave empty for a single-browser serve.',
|
'Optional browser alias to route to (the serve `_route` target). Required when the serve endpoint exposes multiple browser instances; leave empty for a single-browser serve.',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Server Public Key/Fingerprint',
|
||||||
|
name: 'serverIdentity',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
placeholder: 'SHA256:... or 64-char Ed25519 public key hex',
|
||||||
|
description:
|
||||||
|
'Pinned browser-cli serve identity. Get it with `browser-cli remote trust-host ENDPOINT` / `browser-cli remote known-hosts`, then paste the SHA256 fingerprint or raw server public key here. Required for non-loopback endpoints.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Allow Unknown Server Identity',
|
||||||
|
name: 'allowUnknownServerIdentity',
|
||||||
|
type: 'boolean',
|
||||||
|
default: false,
|
||||||
|
description:
|
||||||
|
'Whether to connect without a pinned server identity. Only use for local development/loopback; disabling pinning weakens SSH-style host verification.',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
displayName: 'Use TLS',
|
displayName: 'Use TLS',
|
||||||
name: 'tls',
|
name: 'tls',
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export class BrowserCli implements INodeType {
|
|||||||
icon: 'file:browserCli.svg',
|
icon: 'file:browserCli.svg',
|
||||||
group: ['transform'],
|
group: ['transform'],
|
||||||
version: 1,
|
version: 1,
|
||||||
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
|
subtitle: '={{({ tab: "Tab", page: "Page", dom: "DOM", group: "Group", window: "Window", session: "Session", storage: "Storage", perf: "Perf", extension: "Extension", client: "Client", command: "Command" }[$parameter["resource"]] || $parameter["resource"]) + ": " + $parameter["operation"]}}',
|
||||||
description: 'Control a remote browser by talking directly to a browser-cli serve endpoint',
|
description: 'Control a remote browser by talking directly to a browser-cli serve endpoint',
|
||||||
defaults: { name: 'Browser CLI' },
|
defaults: { name: 'Browser CLI' },
|
||||||
inputs: [NodeConnectionTypes.Main],
|
inputs: [NodeConnectionTypes.Main],
|
||||||
@@ -43,9 +43,14 @@ export class BrowserCli implements INodeType {
|
|||||||
{ name: 'Tab', value: 'tab' },
|
{ name: 'Tab', value: 'tab' },
|
||||||
{ name: 'Page', value: 'page' },
|
{ name: 'Page', value: 'page' },
|
||||||
{ name: 'DOM', value: 'dom' },
|
{ name: 'DOM', value: 'dom' },
|
||||||
|
{ name: 'Group', value: 'group' },
|
||||||
|
{ name: 'Window', value: 'window' },
|
||||||
|
{ name: 'Session', value: 'session' },
|
||||||
|
{ name: 'Storage', value: 'storage' },
|
||||||
|
{ name: 'Performance', value: 'perf' },
|
||||||
|
{ name: 'Extension', value: 'extension' },
|
||||||
{ name: 'Client', value: 'client' },
|
{ name: 'Client', value: 'client' },
|
||||||
{ name: 'Command', value: 'command' },
|
{ name: 'Command', value: 'command' },
|
||||||
{ name: 'Gateway', value: 'gateway' },
|
|
||||||
],
|
],
|
||||||
default: 'tab',
|
default: 'tab',
|
||||||
},
|
},
|
||||||
@@ -59,9 +64,29 @@ export class BrowserCli implements INodeType {
|
|||||||
displayOptions: { show: { resource: ['tab'] } },
|
displayOptions: { show: { resource: ['tab'] } },
|
||||||
options: [
|
options: [
|
||||||
{ name: 'List', value: 'list', action: 'List open tabs', description: 'tabs.list (safe)' },
|
{ name: 'List', value: 'list', action: 'List open tabs', description: 'tabs.list (safe)' },
|
||||||
|
{ name: 'Query', value: 'query', action: 'Search tabs by text', description: 'tabs.query (safe)' },
|
||||||
|
{ name: 'Get', value: 'get', action: 'Get a tab status', description: 'tabs.status (safe)' },
|
||||||
|
{ name: 'Count', value: 'count', action: 'Count open tabs', description: 'tabs.count (safe)' },
|
||||||
|
{ name: 'Filter', value: 'filter', action: 'Filter tabs by URL pattern', description: 'tabs.filter (safe)' },
|
||||||
|
{ name: 'Active in Window', value: 'activeInWindow', action: 'Get active tab in a window', description: 'tabs.active_in_window (safe)' },
|
||||||
{ name: 'Open', value: 'open', action: 'Open a URL in a new tab', description: 'navigate.open (needs --allow-control)' },
|
{ name: 'Open', value: 'open', action: 'Open a URL in a new tab', description: 'navigate.open (needs --allow-control)' },
|
||||||
{ name: 'Close', value: 'close', action: 'Close tabs', description: 'tabs.close (needs --allow-control)' },
|
{ name: 'Close', value: 'close', action: 'Close tabs', description: 'tabs.close (needs --allow-control)' },
|
||||||
{ name: 'Get HTML', value: 'getHtml', action: 'Get a tab raw HTML', description: 'tabs.html (needs --allow-read-page)' },
|
{ name: 'Get HTML', value: 'getHtml', action: 'Get a tab raw HTML', description: 'tabs.html (needs --allow-read-page)' },
|
||||||
|
{ name: 'Activate', value: 'activate', action: 'Switch focus to a tab', description: 'tabs.active (needs --allow-control)' },
|
||||||
|
{ name: 'Move', value: 'move', action: 'Move a tab', description: 'tabs.move (needs --allow-control)' },
|
||||||
|
{ name: 'Navigate To', value: 'navigateTo', action: 'Navigate a tab to a URL', description: 'navigate.to (needs --allow-control)' },
|
||||||
|
{ name: 'Reload', value: 'reload', action: 'Reload a tab', description: 'navigate.reload (needs --allow-control)' },
|
||||||
|
{ name: 'Hard Reload', value: 'hardReload', action: 'Hard reload a tab', description: 'navigate.hard_reload (needs --allow-control)' },
|
||||||
|
{ name: 'Back', value: 'back', action: 'Go back in history', description: 'navigate.back (needs --allow-control)' },
|
||||||
|
{ name: 'Forward', value: 'forward', action: 'Go forward in history', description: 'navigate.forward (needs --allow-control)' },
|
||||||
|
{ name: 'Mute', value: 'mute', action: 'Mute a tab', description: 'tabs.mute (needs --allow-control)' },
|
||||||
|
{ name: 'Unmute', value: 'unmute', action: 'Unmute a tab', description: 'tabs.unmute (needs --allow-control)' },
|
||||||
|
{ name: 'Pin', value: 'pin', action: 'Pin a tab', description: 'tabs.pin (needs --allow-control)' },
|
||||||
|
{ name: 'Unpin', value: 'unpin', action: 'Unpin a tab', description: 'tabs.unpin (needs --allow-control)' },
|
||||||
|
{ name: 'Dedupe', value: 'dedupe', action: 'Close duplicate tabs', description: 'tabs.dedupe (needs --allow-control)' },
|
||||||
|
{ name: 'Sort', value: 'sort', action: 'Sort tabs within windows', description: 'tabs.sort (needs --allow-control)' },
|
||||||
|
{ name: 'Merge Windows', value: 'mergeWindows', action: 'Merge all tabs into one window', description: 'tabs.merge_windows (needs --allow-control)' },
|
||||||
|
{ name: 'Screenshot', value: 'screenshot', action: 'Capture a tab screenshot', description: 'tabs.screenshot (needs --allow-dangerous)' },
|
||||||
],
|
],
|
||||||
default: 'list',
|
default: 'list',
|
||||||
},
|
},
|
||||||
@@ -80,6 +105,7 @@ export class BrowserCli implements INodeType {
|
|||||||
{ name: 'Extract Images', value: 'extractImages', action: 'Extract images', description: 'extract.images (needs --allow-read-page)' },
|
{ name: 'Extract Images', value: 'extractImages', action: 'Extract images', description: 'extract.images (needs --allow-read-page)' },
|
||||||
{ name: 'Extract HTML', value: 'extractHtml', action: 'Extract HTML', description: 'extract.html (needs --allow-read-page)' },
|
{ name: 'Extract HTML', value: 'extractHtml', action: 'Extract HTML', description: 'extract.html (needs --allow-read-page)' },
|
||||||
{ name: 'Extract Markdown', value: 'extractMarkdown', action: 'Extract Markdown payload', description: 'extract.markdown — returns the raw page payload (not SDK-rendered) (needs --allow-read-page)' },
|
{ name: 'Extract Markdown', value: 'extractMarkdown', action: 'Extract Markdown payload', description: 'extract.markdown — returns the raw page payload (not SDK-rendered) (needs --allow-read-page)' },
|
||||||
|
{ name: 'Extract JSON', value: 'extractJson', action: 'Extract JSON-LD / structured data', description: 'extract.json (needs --allow-read-page)' },
|
||||||
],
|
],
|
||||||
default: 'extractText',
|
default: 'extractText',
|
||||||
},
|
},
|
||||||
@@ -93,13 +119,122 @@ export class BrowserCli implements INodeType {
|
|||||||
displayOptions: { show: { resource: ['dom'] } },
|
displayOptions: { show: { resource: ['dom'] } },
|
||||||
options: [
|
options: [
|
||||||
{ name: 'Query', value: 'query', action: 'Query elements by selector', description: 'dom.query (needs --allow-read-page)' },
|
{ name: 'Query', value: 'query', action: 'Query elements by selector', description: 'dom.query (needs --allow-read-page)' },
|
||||||
|
{ name: 'Text', value: 'text', action: 'Get element text', description: 'dom.text (needs --allow-read-page)' },
|
||||||
|
{ name: 'Attribute', value: 'attr', action: 'Get an element attribute', description: 'dom.attr (needs --allow-read-page)' },
|
||||||
|
{ name: 'Exists', value: 'exists', action: 'Check if an element exists', description: 'dom.exists (needs --allow-read-page)' },
|
||||||
{ name: 'Click', value: 'click', action: 'Click an element', description: 'dom.click (needs --allow-control)' },
|
{ name: 'Click', value: 'click', action: 'Click an element', description: 'dom.click (needs --allow-control)' },
|
||||||
{ name: 'Type', value: 'type', action: 'Type into an element', description: 'dom.type (needs --allow-control)' },
|
{ name: 'Type', value: 'type', action: 'Type into an element', description: 'dom.type (needs --allow-control)' },
|
||||||
|
{ name: 'Select', value: 'select', action: 'Select a dropdown option', description: 'dom.select (needs --allow-control)' },
|
||||||
|
{ name: 'Hover', value: 'hover', action: 'Hover over an element', description: 'dom.hover (needs --allow-control)' },
|
||||||
|
{ name: 'Focus', value: 'focus', action: 'Focus an element', description: 'dom.focus (needs --allow-control)' },
|
||||||
|
{ name: 'Check', value: 'check', action: 'Check a checkbox', description: 'dom.check (needs --allow-control)' },
|
||||||
|
{ name: 'Uncheck', value: 'uncheck', action: 'Uncheck a checkbox', description: 'dom.uncheck (needs --allow-control)' },
|
||||||
|
{ name: 'Clear', value: 'clear', action: 'Clear an input', description: 'dom.clear (needs --allow-control)' },
|
||||||
|
{ name: 'Submit', value: 'submit', action: 'Submit a form', description: 'dom.submit (needs --allow-control)' },
|
||||||
|
{ name: 'Scroll', value: 'scroll', action: 'Scroll to an element or position', description: 'dom.scroll (needs --allow-control)' },
|
||||||
|
{ name: 'Key', value: 'key', action: 'Send a keyboard key', description: 'dom.key (needs --allow-control)' },
|
||||||
{ name: 'Eval', value: 'eval', action: 'Evaluate JavaScript', description: 'dom.eval (needs --allow-dangerous)' },
|
{ name: 'Eval', value: 'eval', action: 'Evaluate JavaScript', description: 'dom.eval (needs --allow-dangerous)' },
|
||||||
],
|
],
|
||||||
default: 'query',
|
default: 'query',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// --- Group operations -------------------------------------------------
|
||||||
|
{
|
||||||
|
displayName: 'Operation',
|
||||||
|
name: 'operation',
|
||||||
|
type: 'options',
|
||||||
|
noDataExpression: true,
|
||||||
|
displayOptions: { show: { resource: ['group'] } },
|
||||||
|
options: [
|
||||||
|
{ name: 'List', value: 'list', action: 'List tab groups', description: 'group.list (safe)' },
|
||||||
|
{ name: 'Query', value: 'query', action: 'Search groups by name', description: 'group.query (safe)' },
|
||||||
|
{ name: 'Tabs', value: 'tabs', action: 'List tabs in a group', description: 'group.tabs (safe)' },
|
||||||
|
{ name: 'Count', value: 'count', action: 'Count tab groups', description: 'group.count (needs --allow-control)' },
|
||||||
|
{ name: 'Create', value: 'create', action: 'Create a tab group', description: 'group.open (needs --allow-control)' },
|
||||||
|
{ name: 'Add Tab', value: 'addTab', action: 'Add a tab to a group', description: 'group.add_tab (needs --allow-control)' },
|
||||||
|
{ name: 'Move', value: 'move', action: 'Move a group forward/backward', description: 'group.move (needs --allow-control)' },
|
||||||
|
{ name: 'Close', value: 'close', action: 'Close a tab group', description: 'group.close (needs --allow-control)' },
|
||||||
|
],
|
||||||
|
default: 'list',
|
||||||
|
},
|
||||||
|
|
||||||
|
// --- Window operations ------------------------------------------------
|
||||||
|
{
|
||||||
|
displayName: 'Operation',
|
||||||
|
name: 'operation',
|
||||||
|
type: 'options',
|
||||||
|
noDataExpression: true,
|
||||||
|
displayOptions: { show: { resource: ['window'] } },
|
||||||
|
options: [
|
||||||
|
{ name: 'List', value: 'list', action: 'List browser windows', description: 'windows.list (safe)' },
|
||||||
|
{ name: 'Open', value: 'open', action: 'Open a new window', description: 'windows.open (needs --allow-control)' },
|
||||||
|
{ name: 'Close', value: 'close', action: 'Close a window', description: 'windows.close (needs --allow-control)' },
|
||||||
|
{ name: 'Rename', value: 'rename', action: 'Rename a window', description: 'windows.rename (needs --allow-control)' },
|
||||||
|
],
|
||||||
|
default: 'list',
|
||||||
|
},
|
||||||
|
|
||||||
|
// --- Session operations -----------------------------------------------
|
||||||
|
{
|
||||||
|
displayName: 'Operation',
|
||||||
|
name: 'operation',
|
||||||
|
type: 'options',
|
||||||
|
noDataExpression: true,
|
||||||
|
displayOptions: { show: { resource: ['session'] } },
|
||||||
|
options: [
|
||||||
|
{ name: 'List', value: 'list', action: 'List saved sessions', description: 'session.list (needs --allow-control)' },
|
||||||
|
{ name: 'Save', value: 'save', action: 'Save the current session', description: 'session.save (needs --allow-control)' },
|
||||||
|
{ name: 'Load', value: 'load', action: 'Load a saved session', description: 'session.load (needs --allow-control)' },
|
||||||
|
{ name: 'Remove', value: 'remove', action: 'Delete a saved session', description: 'session.remove (needs --allow-control)' },
|
||||||
|
{ name: 'Export', value: 'export', action: 'Export a session as JSON', description: 'session.export (needs --allow-control)' },
|
||||||
|
{ name: 'Diff', value: 'diff', action: 'Diff two sessions', description: 'session.diff (needs --allow-control)' },
|
||||||
|
{ name: 'Auto Save', value: 'autoSave', action: 'Toggle session auto-save', description: 'session.auto_save (needs --allow-control)' },
|
||||||
|
],
|
||||||
|
default: 'list',
|
||||||
|
},
|
||||||
|
|
||||||
|
// --- Storage operations -----------------------------------------------
|
||||||
|
{
|
||||||
|
displayName: 'Operation',
|
||||||
|
name: 'operation',
|
||||||
|
type: 'options',
|
||||||
|
noDataExpression: true,
|
||||||
|
displayOptions: { show: { resource: ['storage'] } },
|
||||||
|
options: [
|
||||||
|
{ name: 'Get', value: 'get', action: 'Read localStorage / sessionStorage', description: 'storage.get (needs --allow-dangerous)' },
|
||||||
|
{ name: 'Set', value: 'set', action: 'Write localStorage / sessionStorage', description: 'storage.set (needs --allow-dangerous)' },
|
||||||
|
],
|
||||||
|
default: 'get',
|
||||||
|
},
|
||||||
|
|
||||||
|
// --- Performance operations -------------------------------------------
|
||||||
|
{
|
||||||
|
displayName: 'Operation',
|
||||||
|
name: 'operation',
|
||||||
|
type: 'options',
|
||||||
|
noDataExpression: true,
|
||||||
|
displayOptions: { show: { resource: ['perf'] } },
|
||||||
|
options: [
|
||||||
|
{ name: 'Status', value: 'status', action: 'Get performance status', description: 'perf.status (safe)' },
|
||||||
|
],
|
||||||
|
default: 'status',
|
||||||
|
},
|
||||||
|
|
||||||
|
// --- Extension operations ---------------------------------------------
|
||||||
|
{
|
||||||
|
displayName: 'Operation',
|
||||||
|
name: 'operation',
|
||||||
|
type: 'options',
|
||||||
|
noDataExpression: true,
|
||||||
|
displayOptions: { show: { resource: ['extension'] } },
|
||||||
|
options: [
|
||||||
|
{ name: 'Info', value: 'info', action: 'Get extension info', description: 'extension.info (safe)' },
|
||||||
|
{ name: 'Capabilities', value: 'capabilities', action: 'List extension capabilities', description: 'extension.capabilities (safe)' },
|
||||||
|
{ name: 'Reload', value: 'reload', action: 'Reload the extension', description: 'extension.reload (needs --allow-control)' },
|
||||||
|
],
|
||||||
|
default: 'info',
|
||||||
|
},
|
||||||
|
|
||||||
// --- Client operations ------------------------------------------------
|
// --- Client operations ------------------------------------------------
|
||||||
{
|
{
|
||||||
displayName: 'Operation',
|
displayName: 'Operation',
|
||||||
@@ -126,18 +261,6 @@ export class BrowserCli implements INodeType {
|
|||||||
default: 'execute',
|
default: 'execute',
|
||||||
},
|
},
|
||||||
|
|
||||||
// --- Gateway operations -----------------------------------------------
|
|
||||||
{
|
|
||||||
displayName: 'Operation',
|
|
||||||
name: 'operation',
|
|
||||||
type: 'options',
|
|
||||||
noDataExpression: true,
|
|
||||||
displayOptions: { show: { resource: ['gateway'] } },
|
|
||||||
options: [
|
|
||||||
{ name: 'Health', value: 'health', action: 'Check serve connectivity', description: 'Pings the endpoint with tabs.list (safe)' },
|
|
||||||
],
|
|
||||||
default: 'health',
|
|
||||||
},
|
|
||||||
|
|
||||||
// --- Shared parameter fields -----------------------------------------
|
// --- Shared parameter fields -----------------------------------------
|
||||||
{
|
{
|
||||||
@@ -147,7 +270,16 @@ export class BrowserCli implements INodeType {
|
|||||||
default: '',
|
default: '',
|
||||||
required: true,
|
required: true,
|
||||||
placeholder: 'https://example.com',
|
placeholder: 'https://example.com',
|
||||||
displayOptions: { show: showFor('tab', ['open']) },
|
displayOptions: { show: showFor('tab', ['open', 'navigateTo']) },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'URL',
|
||||||
|
name: 'url',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
placeholder: 'https://example.com',
|
||||||
|
description: 'Optional URL to open. Leave empty for a blank window/tab.',
|
||||||
|
displayOptions: { show: { resource: ['window', 'group'], operation: ['open', 'addTab'] } },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
displayName: 'Focus Tab',
|
displayName: 'Focus Tab',
|
||||||
@@ -184,7 +316,136 @@ export class BrowserCli implements INodeType {
|
|||||||
type: 'number',
|
type: 'number',
|
||||||
default: 0,
|
default: 0,
|
||||||
description: 'Target tab ID. Leave 0 for the active tab.',
|
description: 'Target tab ID. Leave 0 for the active tab.',
|
||||||
displayOptions: { show: { resource: ['tab'], operation: ['getHtml'] } },
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
resource: ['tab', 'dom'],
|
||||||
|
operation: ['getHtml', 'get', 'reload', 'hardReload', 'back', 'forward', 'mute', 'unmute', 'pin', 'unpin', 'screenshot', 'eval'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Tab ID',
|
||||||
|
name: 'tabId',
|
||||||
|
type: 'number',
|
||||||
|
default: 0,
|
||||||
|
required: true,
|
||||||
|
description: 'Target tab ID (required — no active-tab fallback)',
|
||||||
|
displayOptions: { show: showFor('tab', ['activate', 'move', 'navigateTo']) },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Tab ID',
|
||||||
|
name: 'tabId',
|
||||||
|
type: 'number',
|
||||||
|
default: 0,
|
||||||
|
description: 'Target tab ID. Leave 0 for the active tab.',
|
||||||
|
displayOptions: { show: showFor('storage', ['get', 'set']) },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Search',
|
||||||
|
name: 'search',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
required: true,
|
||||||
|
placeholder: 'github',
|
||||||
|
description: 'Substring to match against tab/group titles and URLs',
|
||||||
|
displayOptions: { show: { resource: ['tab', 'group'], operation: ['query'] } },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'URL Pattern',
|
||||||
|
name: 'pattern',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
placeholder: 'twitch.tv/* or twitch.tv',
|
||||||
|
description: 'Matched against the full tab URL. A plain string is a case-sensitive substring match ("twitch.tv"); a pattern with "*" or "?" is a glob ("twitch.tv/*", "*.twitch.tv"). Glob needs the serve-side extension at 0.16.4+; older extensions treat the whole pattern as a literal substring. Required for Filter; optional for Count (omit to count all).',
|
||||||
|
displayOptions: { show: showFor('tab', ['filter', 'count']) },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Window ID',
|
||||||
|
name: 'windowId',
|
||||||
|
type: 'number',
|
||||||
|
default: 0,
|
||||||
|
required: true,
|
||||||
|
displayOptions: { show: showFor('tab', ['activeInWindow']) },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Window ID',
|
||||||
|
name: 'windowId',
|
||||||
|
type: 'number',
|
||||||
|
default: 0,
|
||||||
|
required: true,
|
||||||
|
displayOptions: { show: showFor('window', ['close', 'rename']) },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Window ID',
|
||||||
|
name: 'windowId',
|
||||||
|
type: 'number',
|
||||||
|
default: 0,
|
||||||
|
description: 'Move the tab to this window. Leave 0 to keep it in the current window.',
|
||||||
|
displayOptions: { show: showFor('tab', ['move']) },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Index',
|
||||||
|
name: 'index',
|
||||||
|
type: 'number',
|
||||||
|
default: 0,
|
||||||
|
description: 'Target position within the window (0-based)',
|
||||||
|
displayOptions: { show: showFor('tab', ['move']) },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Format',
|
||||||
|
name: 'format',
|
||||||
|
type: 'options',
|
||||||
|
default: 'png',
|
||||||
|
options: [
|
||||||
|
{ name: 'PNG', value: 'png' },
|
||||||
|
{ name: 'JPEG', value: 'jpeg' },
|
||||||
|
],
|
||||||
|
displayOptions: { show: showFor('tab', ['screenshot']) },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Quality',
|
||||||
|
name: 'quality',
|
||||||
|
type: 'number',
|
||||||
|
default: 80,
|
||||||
|
description: 'JPEG quality 0-100 (ignored for PNG)',
|
||||||
|
displayOptions: { show: { resource: ['tab'], operation: ['screenshot'], format: ['jpeg'] } },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Gentle Mode',
|
||||||
|
name: 'gentleMode',
|
||||||
|
type: 'options',
|
||||||
|
default: 'auto',
|
||||||
|
description: 'How aggressively to rearrange tabs',
|
||||||
|
options: [
|
||||||
|
{ name: 'Auto', value: 'auto' },
|
||||||
|
{ name: 'On', value: 'on' },
|
||||||
|
{ name: 'Off', value: 'off' },
|
||||||
|
],
|
||||||
|
displayOptions: { show: showFor('tab', ['dedupe', 'sort', 'mergeWindows']) },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Gentle Mode',
|
||||||
|
name: 'gentleMode',
|
||||||
|
type: 'options',
|
||||||
|
default: 'auto',
|
||||||
|
options: [
|
||||||
|
{ name: 'Auto', value: 'auto' },
|
||||||
|
{ name: 'On', value: 'on' },
|
||||||
|
{ name: 'Off', value: 'off' },
|
||||||
|
],
|
||||||
|
displayOptions: { show: showFor('group', ['close']) },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Sort By',
|
||||||
|
name: 'by',
|
||||||
|
type: 'options',
|
||||||
|
default: 'domain',
|
||||||
|
options: [
|
||||||
|
{ name: 'Domain', value: 'domain' },
|
||||||
|
{ name: 'Title', value: 'title' },
|
||||||
|
{ name: 'Time', value: 'time' },
|
||||||
|
],
|
||||||
|
displayOptions: { show: showFor('tab', ['sort']) },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
displayName: 'Selector',
|
displayName: 'Selector',
|
||||||
@@ -196,7 +457,11 @@ export class BrowserCli implements INodeType {
|
|||||||
displayOptions: {
|
displayOptions: {
|
||||||
show: {
|
show: {
|
||||||
resource: ['dom', 'page'],
|
resource: ['dom', 'page'],
|
||||||
operation: ['query', 'click', 'type', 'extractText', 'extractLinks', 'extractImages', 'extractHtml', 'extractMarkdown'],
|
operation: [
|
||||||
|
'query', 'text', 'attr', 'exists', 'click', 'type', 'select', 'hover', 'focus',
|
||||||
|
'check', 'uncheck', 'clear', 'submit', 'scroll', 'key',
|
||||||
|
'extractText', 'extractLinks', 'extractImages', 'extractHtml', 'extractMarkdown', 'extractJson',
|
||||||
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -208,6 +473,51 @@ export class BrowserCli implements INodeType {
|
|||||||
required: true,
|
required: true,
|
||||||
displayOptions: { show: showFor('dom', ['type']) },
|
displayOptions: { show: showFor('dom', ['type']) },
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Attribute',
|
||||||
|
name: 'attr',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
required: true,
|
||||||
|
placeholder: 'href',
|
||||||
|
description: 'Attribute name to read from the matched element',
|
||||||
|
displayOptions: { show: showFor('dom', ['attr']) },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Value',
|
||||||
|
name: 'value',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
required: true,
|
||||||
|
description: 'Option value to select in the dropdown',
|
||||||
|
displayOptions: { show: showFor('dom', ['select']) },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Key',
|
||||||
|
name: 'key',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
required: true,
|
||||||
|
placeholder: 'Enter',
|
||||||
|
description: 'Keyboard key to send, e.g. Enter, Escape, ArrowDown',
|
||||||
|
displayOptions: { show: showFor('dom', ['key']) },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'X',
|
||||||
|
name: 'x',
|
||||||
|
type: 'number',
|
||||||
|
default: 0,
|
||||||
|
description: 'Horizontal scroll position (used when no selector is given)',
|
||||||
|
displayOptions: { show: showFor('dom', ['scroll']) },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Y',
|
||||||
|
name: 'y',
|
||||||
|
type: 'number',
|
||||||
|
default: 0,
|
||||||
|
description: 'Vertical scroll position (used when no selector is given)',
|
||||||
|
displayOptions: { show: showFor('dom', ['scroll']) },
|
||||||
|
},
|
||||||
{
|
{
|
||||||
displayName: 'JavaScript',
|
displayName: 'JavaScript',
|
||||||
name: 'code',
|
name: 'code',
|
||||||
@@ -220,12 +530,97 @@ export class BrowserCli implements INodeType {
|
|||||||
displayOptions: { show: showFor('dom', ['eval']) },
|
displayOptions: { show: showFor('dom', ['eval']) },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
displayName: 'Tab ID',
|
displayName: 'Group ID',
|
||||||
name: 'tabId',
|
name: 'groupId',
|
||||||
type: 'number',
|
type: 'number',
|
||||||
default: 0,
|
default: 0,
|
||||||
description: 'Target tab ID. Leave 0 for the active tab.',
|
required: true,
|
||||||
displayOptions: { show: { resource: ['dom'], operation: ['eval'] } },
|
displayOptions: { show: showFor('group', ['tabs', 'close']) },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Group',
|
||||||
|
name: 'group',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
required: true,
|
||||||
|
placeholder: 'Research or 12',
|
||||||
|
description: 'Target group by name or numeric ID',
|
||||||
|
displayOptions: { show: showFor('group', ['addTab', 'move']) },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Direction',
|
||||||
|
name: 'direction',
|
||||||
|
type: 'options',
|
||||||
|
default: 'forward',
|
||||||
|
options: [
|
||||||
|
{ name: 'Forward', value: 'forward' },
|
||||||
|
{ name: 'Backward', value: 'backward' },
|
||||||
|
],
|
||||||
|
displayOptions: { show: showFor('group', ['move']) },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Name',
|
||||||
|
name: 'name',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
description: 'Name for the group/window/session. Required for all but Export (which dumps the active session when empty).',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
resource: ['group', 'window', 'session'],
|
||||||
|
operation: ['create', 'rename', 'save', 'load', 'remove', 'export'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Session A',
|
||||||
|
name: 'nameA',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
required: true,
|
||||||
|
displayOptions: { show: showFor('session', ['diff']) },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Session B',
|
||||||
|
name: 'nameB',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
required: true,
|
||||||
|
displayOptions: { show: showFor('session', ['diff']) },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Enabled',
|
||||||
|
name: 'enabled',
|
||||||
|
type: 'boolean',
|
||||||
|
default: true,
|
||||||
|
description: 'Whether to turn session auto-save on',
|
||||||
|
displayOptions: { show: showFor('session', ['autoSave']) },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Storage Key',
|
||||||
|
name: 'key',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
description: 'Storage key. Required for Set; on Get, omit to dump all keys.',
|
||||||
|
displayOptions: { show: showFor('storage', ['get', 'set']) },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Storage Value',
|
||||||
|
name: 'value',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
required: true,
|
||||||
|
displayOptions: { show: showFor('storage', ['set']) },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Storage Type',
|
||||||
|
name: 'storeType',
|
||||||
|
type: 'options',
|
||||||
|
default: 'local',
|
||||||
|
options: [
|
||||||
|
{ name: 'localStorage', value: 'local' },
|
||||||
|
{ name: 'sessionStorage', value: 'session' },
|
||||||
|
],
|
||||||
|
displayOptions: { show: showFor('storage', ['get', 'set']) },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
displayName: 'Command',
|
displayName: 'Command',
|
||||||
@@ -332,6 +727,8 @@ function connectOptionsFromCredentials(creds: IDataObject): ServeConnectOptions
|
|||||||
rejectUnauthorized: !creds.allowUnauthorizedCerts,
|
rejectUnauthorized: !creds.allowUnauthorizedCerts,
|
||||||
privateKeyPem: creds.privateKey ? String(creds.privateKey) : null,
|
privateKeyPem: creds.privateKey ? String(creds.privateKey) : null,
|
||||||
route: creds.browser ? String(creds.browser) : null,
|
route: creds.browser ? String(creds.browser) : null,
|
||||||
|
serverIdentity: creds.serverIdentity ? String(creds.serverIdentity) : null,
|
||||||
|
allowUnknownServerIdentity: Boolean(creds.allowUnknownServerIdentity),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -357,25 +754,116 @@ function collectParams(
|
|||||||
}
|
}
|
||||||
return { command: get('command'), args };
|
return { command: get('command'), args };
|
||||||
}
|
}
|
||||||
|
// --- Tabs -------------------------------------------------------------
|
||||||
case 'tab:open':
|
case 'tab:open':
|
||||||
return { url: get('url'), focus: get('focus', false) };
|
return { url: get('url'), focus: get('focus', false) };
|
||||||
|
case 'tab:navigateTo':
|
||||||
|
return { tabId: get('tabId', 0), url: get('url') };
|
||||||
case 'tab:close':
|
case 'tab:close':
|
||||||
return { mode: get('mode', 'ids'), tabIds: get('tabIds', '') };
|
return { mode: get('mode', 'ids'), tabIds: get('tabIds', '') };
|
||||||
case 'tab:getHtml':
|
case 'tab:query':
|
||||||
|
return { search: get('search', '') };
|
||||||
|
case 'tab:filter':
|
||||||
|
case 'tab:count':
|
||||||
|
return { pattern: get('pattern', '') };
|
||||||
|
case 'tab:activeInWindow':
|
||||||
|
return { windowId: get('windowId', 0) };
|
||||||
|
case 'tab:activate':
|
||||||
return { tabId: get('tabId', 0) };
|
return { tabId: get('tabId', 0) };
|
||||||
|
case 'tab:move':
|
||||||
|
return { tabId: get('tabId', 0), windowId: get('windowId', 0), index: get('index', '') };
|
||||||
|
case 'tab:get':
|
||||||
|
case 'tab:getHtml':
|
||||||
|
case 'tab:reload':
|
||||||
|
case 'tab:hardReload':
|
||||||
|
case 'tab:back':
|
||||||
|
case 'tab:forward':
|
||||||
|
case 'tab:mute':
|
||||||
|
case 'tab:unmute':
|
||||||
|
case 'tab:pin':
|
||||||
|
case 'tab:unpin':
|
||||||
|
return { tabId: get('tabId', 0) };
|
||||||
|
case 'tab:dedupe':
|
||||||
|
case 'tab:mergeWindows':
|
||||||
|
return { gentleMode: get('gentleMode', 'auto') };
|
||||||
|
case 'tab:sort':
|
||||||
|
return { by: get('by', 'domain'), gentleMode: get('gentleMode', 'auto') };
|
||||||
|
case 'tab:screenshot':
|
||||||
|
return { tabId: get('tabId', 0), format: get('format', 'png'), quality: get('quality', '') };
|
||||||
|
|
||||||
|
// --- DOM --------------------------------------------------------------
|
||||||
case 'dom:query':
|
case 'dom:query':
|
||||||
|
case 'dom:text':
|
||||||
|
case 'dom:exists':
|
||||||
case 'dom:click':
|
case 'dom:click':
|
||||||
|
case 'dom:hover':
|
||||||
|
case 'dom:focus':
|
||||||
|
case 'dom:check':
|
||||||
|
case 'dom:uncheck':
|
||||||
|
case 'dom:clear':
|
||||||
|
case 'dom:submit':
|
||||||
return { selector: get('selector', '') };
|
return { selector: get('selector', '') };
|
||||||
case 'dom:type':
|
case 'dom:type':
|
||||||
return { selector: get('selector', ''), text: get('text', '') };
|
return { selector: get('selector', ''), text: get('text', '') };
|
||||||
|
case 'dom:attr':
|
||||||
|
return { selector: get('selector', ''), attr: get('attr', '') };
|
||||||
|
case 'dom:select':
|
||||||
|
return { selector: get('selector', ''), value: get('value', '') };
|
||||||
|
case 'dom:key':
|
||||||
|
return { selector: get('selector', ''), key: get('key', '') };
|
||||||
|
case 'dom:scroll':
|
||||||
|
return { selector: get('selector', ''), x: get('x', ''), y: get('y', '') };
|
||||||
case 'dom:eval':
|
case 'dom:eval':
|
||||||
return { code: get('code', ''), tabId: get('tabId', 0) };
|
return { code: get('code', ''), tabId: get('tabId', 0) };
|
||||||
|
|
||||||
|
// --- Page / extraction ------------------------------------------------
|
||||||
case 'page:extractText':
|
case 'page:extractText':
|
||||||
case 'page:extractLinks':
|
case 'page:extractLinks':
|
||||||
case 'page:extractImages':
|
case 'page:extractImages':
|
||||||
case 'page:extractHtml':
|
case 'page:extractHtml':
|
||||||
case 'page:extractMarkdown':
|
case 'page:extractMarkdown':
|
||||||
|
case 'page:extractJson':
|
||||||
return { selector: get('selector', '') };
|
return { selector: get('selector', '') };
|
||||||
|
|
||||||
|
// --- Groups -----------------------------------------------------------
|
||||||
|
case 'group:query':
|
||||||
|
return { search: get('search', '') };
|
||||||
|
case 'group:tabs':
|
||||||
|
return { groupId: get('groupId', 0) };
|
||||||
|
case 'group:close':
|
||||||
|
return { groupId: get('groupId', 0), gentleMode: get('gentleMode', 'auto') };
|
||||||
|
case 'group:create':
|
||||||
|
return { name: get('name', '') };
|
||||||
|
case 'group:addTab':
|
||||||
|
return { group: get('group', ''), url: get('url', '') };
|
||||||
|
case 'group:move':
|
||||||
|
return { group: get('group', ''), direction: get('direction', 'forward') };
|
||||||
|
|
||||||
|
// --- Windows ----------------------------------------------------------
|
||||||
|
case 'window:open':
|
||||||
|
return { url: get('url', '') };
|
||||||
|
case 'window:close':
|
||||||
|
return { windowId: get('windowId', 0) };
|
||||||
|
case 'window:rename':
|
||||||
|
return { windowId: get('windowId', 0), name: get('name', '') };
|
||||||
|
|
||||||
|
// --- Sessions ---------------------------------------------------------
|
||||||
|
case 'session:save':
|
||||||
|
case 'session:load':
|
||||||
|
case 'session:remove':
|
||||||
|
case 'session:export':
|
||||||
|
return { name: get('name', '') };
|
||||||
|
case 'session:diff':
|
||||||
|
return { nameA: get('nameA', ''), nameB: get('nameB', '') };
|
||||||
|
case 'session:autoSave':
|
||||||
|
return { enabled: get('enabled', true) };
|
||||||
|
|
||||||
|
// --- Storage ----------------------------------------------------------
|
||||||
|
case 'storage:get':
|
||||||
|
return { key: get('key', ''), storeType: get('storeType', 'local'), tabId: get('tabId', 0) };
|
||||||
|
case 'storage:set':
|
||||||
|
return { key: get('key', ''), value: get('value', ''), storeType: get('storeType', 'local'), tabId: get('tabId', 0) };
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" role="img" aria-labelledby="title">
|
<svg xmlns="http://www.w3.org/2000/svg" width="60" height="60" viewBox="0 0 128 128" role="img" aria-labelledby="title">
|
||||||
<title>browser-cli icon</title>
|
<title>browser-cli icon</title>
|
||||||
<defs>
|
<defs>
|
||||||
<linearGradient id="bg" x1="16" y1="16" x2="112" y2="112" gradientUnits="userSpaceOnUse">
|
<linearGradient id="bg" x1="16" y1="16" x2="112" y2="112" gradientUnits="userSpaceOnUse">
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
@@ -24,6 +24,7 @@ import {
|
|||||||
createPrivateKey,
|
createPrivateKey,
|
||||||
createPublicKey,
|
createPublicKey,
|
||||||
createHash,
|
createHash,
|
||||||
|
verify as nodeVerify,
|
||||||
createHmac,
|
createHmac,
|
||||||
createCipheriv,
|
createCipheriv,
|
||||||
createDecipheriv,
|
createDecipheriv,
|
||||||
@@ -234,6 +235,8 @@ export interface Challenge {
|
|||||||
nonce?: string;
|
nonce?: string;
|
||||||
min_client_version?: string;
|
min_client_version?: string;
|
||||||
pq_kex?: { alg?: string; public_key?: string };
|
pq_kex?: { alg?: string; public_key?: string };
|
||||||
|
server_pubkey?: string;
|
||||||
|
server_sig?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AuthPayload {
|
export interface AuthPayload {
|
||||||
@@ -247,6 +250,81 @@ function pqPublicKey(challenge: Challenge): string | null {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function base64Url(buffer: Buffer): string {
|
||||||
|
return buffer.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
function ed25519PublicKeyFromHex(pubkeyHex: string): KeyObject {
|
||||||
|
if (!/^[0-9a-fA-F]{64}$/.test(pubkeyHex)) throw new Error('server public key must be 32-byte hex');
|
||||||
|
return createPublicKey({ key: { kty: 'OKP', crv: 'Ed25519', x: base64Url(Buffer.from(pubkeyHex, 'hex')) }, format: 'jwk' });
|
||||||
|
}
|
||||||
|
|
||||||
|
function signedChallenge(challenge: Challenge): Record<string, unknown> {
|
||||||
|
const { server_sig: _serverSig, ...rest } = challenge;
|
||||||
|
return rest as Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function serverFingerprint(pubkeyHex: string): string {
|
||||||
|
return 'SHA256:' + createHash('sha256').update(Buffer.from(pubkeyHex, 'hex')).digest('base64').replace(/=+$/g, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function verifyServerChallengeSignature(challenge: Challenge): boolean {
|
||||||
|
const pubkey = challenge.server_pubkey;
|
||||||
|
const sig = challenge.server_sig;
|
||||||
|
if (!pubkey || !sig) return false;
|
||||||
|
try {
|
||||||
|
return nodeVerify(
|
||||||
|
null,
|
||||||
|
Buffer.from(canonicalJson(signedChallenge(challenge)), 'utf8'),
|
||||||
|
ed25519PublicKeyFromHex(pubkey),
|
||||||
|
Buffer.from(sig, 'hex'),
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function verifyServerIdentity(
|
||||||
|
challenge: Challenge,
|
||||||
|
expectedServerIdentity: string | null | undefined,
|
||||||
|
endpoint: string,
|
||||||
|
allowUnknown: boolean,
|
||||||
|
): void {
|
||||||
|
const pubkey = challenge.server_pubkey;
|
||||||
|
const expected = (expectedServerIdentity || '').trim();
|
||||||
|
|
||||||
|
if (!pubkey) {
|
||||||
|
if (expected) throw new Error(`server ${endpoint} did not advertise a server identity key`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!verifyServerChallengeSignature(challenge)) {
|
||||||
|
throw new Error(`server ${endpoint} identity signature is invalid`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const seenFingerprint = serverFingerprint(pubkey);
|
||||||
|
if (!expected) {
|
||||||
|
if (allowUnknown) return;
|
||||||
|
throw new Error(
|
||||||
|
`Unknown browser-cli server identity for ${endpoint} (${seenFingerprint}). ` +
|
||||||
|
'Set the expected Server Public Key/Fingerprint in the Browser CLI credential.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (expected.startsWith('SHA256:')) {
|
||||||
|
if (expected !== seenFingerprint) {
|
||||||
|
throw new Error(`REMOTE SERVER IDENTITY CHANGED for ${endpoint}: expected ${expected}, seen ${seenFingerprint}`);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedExpected = expected.toLowerCase();
|
||||||
|
if (normalizedExpected !== pubkey.toLowerCase()) {
|
||||||
|
throw new Error(
|
||||||
|
`REMOTE SERVER IDENTITY CHANGED for ${endpoint}: expected ${serverFingerprint(normalizedExpected)}, seen ${seenFingerprint}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build the single framed message a client sends in response to the challenge.
|
* Build the single framed message a client sends in response to the challenge.
|
||||||
* Mirrors `browser_cli.remote.auth.build_auth_message` + `signed_payload`.
|
* Mirrors `browser_cli.remote.auth.build_auth_message` + `signed_payload`.
|
||||||
|
|||||||
@@ -7,6 +7,10 @@
|
|||||||
* (see `serveClient.ts`). Every operation maps to one raw extension command;
|
* (see `serveClient.ts`). Every operation maps to one raw extension command;
|
||||||
* what the server returns is the *raw* command result (no SDK-side rendering),
|
* what the server returns is the *raw* command result (no SDK-side rendering),
|
||||||
* still subject to the server's --allow-* policy noted per operation.
|
* still subject to the server's --allow-* policy noted per operation.
|
||||||
|
*
|
||||||
|
* Command names and argument shapes mirror the Python SDK (browser_cli/sdk/*)
|
||||||
|
* and the server-side policy in browser_cli/command_security.py. Gating per
|
||||||
|
* operation is documented in BrowserCli.node.ts next to each operation.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export type CommandParams = Record<string, unknown>;
|
export type CommandParams = Record<string, unknown>;
|
||||||
@@ -51,6 +55,16 @@ export function buildCommand(
|
|||||||
// --- Tabs -------------------------------------------------------------
|
// --- Tabs -------------------------------------------------------------
|
||||||
case 'tab:list':
|
case 'tab:list':
|
||||||
return { command: 'tabs.list', args: {} };
|
return { command: 'tabs.list', args: {} };
|
||||||
|
case 'tab:query':
|
||||||
|
return { command: 'tabs.query', args: { search: str(params, 'search') } };
|
||||||
|
case 'tab:get':
|
||||||
|
return { command: 'tabs.status', args: compact({ tabId: tabIdArg(params.tabId) }) };
|
||||||
|
case 'tab:count':
|
||||||
|
return { command: 'tabs.count', args: compact({ pattern: str(params, 'pattern') }) };
|
||||||
|
case 'tab:filter':
|
||||||
|
return { command: 'tabs.filter', args: { pattern: str(params, 'pattern') } };
|
||||||
|
case 'tab:activeInWindow':
|
||||||
|
return { command: 'tabs.active_in_window', args: { windowId: numArg(params.windowId) } };
|
||||||
case 'tab:open': {
|
case 'tab:open': {
|
||||||
const focus = Boolean(params.focus);
|
const focus = Boolean(params.focus);
|
||||||
return { command: 'navigate.open', args: compact({ url: str(params, 'url'), focus, background: !focus }) };
|
return { command: 'navigate.open', args: compact({ url: str(params, 'url'), focus, background: !focus }) };
|
||||||
@@ -63,6 +77,50 @@ export function buildCommand(
|
|||||||
}
|
}
|
||||||
case 'tab:getHtml':
|
case 'tab:getHtml':
|
||||||
return { command: 'tabs.html', args: compact({ tabId: tabIdArg(params.tabId) }) };
|
return { command: 'tabs.html', args: compact({ tabId: tabIdArg(params.tabId) }) };
|
||||||
|
case 'tab:activate':
|
||||||
|
return { command: 'tabs.active', args: { tabId: numArg(params.tabId) } };
|
||||||
|
case 'tab:move':
|
||||||
|
return {
|
||||||
|
command: 'tabs.move',
|
||||||
|
args: compact({
|
||||||
|
tabId: numArg(params.tabId),
|
||||||
|
windowId: tabIdArg(params.windowId),
|
||||||
|
index: indexArg(params.index),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
case 'tab:reload':
|
||||||
|
return { command: 'navigate.reload', args: compact({ tabId: tabIdArg(params.tabId) }) };
|
||||||
|
case 'tab:hardReload':
|
||||||
|
return { command: 'navigate.hard_reload', args: compact({ tabId: tabIdArg(params.tabId) }) };
|
||||||
|
case 'tab:back':
|
||||||
|
return { command: 'navigate.back', args: compact({ tabId: tabIdArg(params.tabId) }) };
|
||||||
|
case 'tab:forward':
|
||||||
|
return { command: 'navigate.forward', args: compact({ tabId: tabIdArg(params.tabId) }) };
|
||||||
|
case 'tab:navigateTo':
|
||||||
|
return { command: 'navigate.to', args: { tabId: numArg(params.tabId), url: str(params, 'url') } };
|
||||||
|
case 'tab:mute':
|
||||||
|
return { command: 'tabs.mute', args: compact({ tabId: tabIdArg(params.tabId) }) };
|
||||||
|
case 'tab:unmute':
|
||||||
|
return { command: 'tabs.unmute', args: compact({ tabId: tabIdArg(params.tabId) }) };
|
||||||
|
case 'tab:pin':
|
||||||
|
return { command: 'tabs.pin', args: compact({ tabId: tabIdArg(params.tabId) }) };
|
||||||
|
case 'tab:unpin':
|
||||||
|
return { command: 'tabs.unpin', args: compact({ tabId: tabIdArg(params.tabId) }) };
|
||||||
|
case 'tab:dedupe':
|
||||||
|
return { command: 'tabs.dedupe', args: { gentleMode: str(params, 'gentleMode') || 'auto' } };
|
||||||
|
case 'tab:sort':
|
||||||
|
return { command: 'tabs.sort', args: { by: str(params, 'by') || 'domain', gentleMode: str(params, 'gentleMode') || 'auto' } };
|
||||||
|
case 'tab:mergeWindows':
|
||||||
|
return { command: 'tabs.merge_windows', args: { gentleMode: str(params, 'gentleMode') || 'auto' } };
|
||||||
|
case 'tab:screenshot':
|
||||||
|
return {
|
||||||
|
command: 'tabs.screenshot',
|
||||||
|
args: compact({
|
||||||
|
tabId: tabIdArg(params.tabId),
|
||||||
|
format: str(params, 'format') || 'png',
|
||||||
|
quality: indexArg(params.quality),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
// --- Page / extraction ------------------------------------------------
|
// --- Page / extraction ------------------------------------------------
|
||||||
case 'page:info':
|
case 'page:info':
|
||||||
@@ -77,6 +135,8 @@ export function buildCommand(
|
|||||||
return { command: 'extract.html', args: compact({ selector: str(params, 'selector') }) };
|
return { command: 'extract.html', args: compact({ selector: str(params, 'selector') }) };
|
||||||
case 'page:extractMarkdown':
|
case 'page:extractMarkdown':
|
||||||
return { command: 'extract.markdown', args: compact({ selector: str(params, 'selector') }) };
|
return { command: 'extract.markdown', args: compact({ selector: str(params, 'selector') }) };
|
||||||
|
case 'page:extractJson':
|
||||||
|
return { command: 'extract.json', args: { selector: str(params, 'selector') } };
|
||||||
|
|
||||||
// --- DOM --------------------------------------------------------------
|
// --- DOM --------------------------------------------------------------
|
||||||
case 'dom:query':
|
case 'dom:query':
|
||||||
@@ -85,17 +145,118 @@ export function buildCommand(
|
|||||||
return { command: 'dom.click', args: { selector: str(params, 'selector') } };
|
return { command: 'dom.click', args: { selector: str(params, 'selector') } };
|
||||||
case 'dom:type':
|
case 'dom:type':
|
||||||
return { command: 'dom.type', args: { selector: str(params, 'selector'), text: str(params, 'text') } };
|
return { command: 'dom.type', args: { selector: str(params, 'selector'), text: str(params, 'text') } };
|
||||||
|
case 'dom:attr':
|
||||||
|
return { command: 'dom.attr', args: { selector: str(params, 'selector'), attr: str(params, 'attr') } };
|
||||||
|
case 'dom:text':
|
||||||
|
return { command: 'dom.text', args: { selector: str(params, 'selector') } };
|
||||||
|
case 'dom:exists':
|
||||||
|
return { command: 'dom.exists', args: { selector: str(params, 'selector') } };
|
||||||
|
case 'dom:scroll':
|
||||||
|
return {
|
||||||
|
command: 'dom.scroll',
|
||||||
|
args: compact({ selector: str(params, 'selector'), x: indexArg(params.x), y: indexArg(params.y) }),
|
||||||
|
};
|
||||||
|
case 'dom:select':
|
||||||
|
return { command: 'dom.select', args: { selector: str(params, 'selector'), value: str(params, 'value') } };
|
||||||
|
case 'dom:hover':
|
||||||
|
return { command: 'dom.hover', args: { selector: str(params, 'selector') } };
|
||||||
|
case 'dom:check':
|
||||||
|
return { command: 'dom.check', args: { selector: str(params, 'selector') } };
|
||||||
|
case 'dom:uncheck':
|
||||||
|
return { command: 'dom.uncheck', args: { selector: str(params, 'selector') } };
|
||||||
|
case 'dom:clear':
|
||||||
|
return { command: 'dom.clear', args: { selector: str(params, 'selector') } };
|
||||||
|
case 'dom:focus':
|
||||||
|
return { command: 'dom.focus', args: { selector: str(params, 'selector') } };
|
||||||
|
case 'dom:submit':
|
||||||
|
return { command: 'dom.submit', args: { selector: str(params, 'selector') } };
|
||||||
|
case 'dom:key':
|
||||||
|
return { command: 'dom.key', args: compact({ key: str(params, 'key'), selector: str(params, 'selector') }) };
|
||||||
case 'dom:eval':
|
case 'dom:eval':
|
||||||
return { command: 'dom.eval', args: compact({ code: str(params, 'code'), tabId: tabIdArg(params.tabId) }) };
|
return { command: 'dom.eval', args: compact({ code: str(params, 'code'), tabId: tabIdArg(params.tabId) }) };
|
||||||
|
|
||||||
|
// --- Groups -----------------------------------------------------------
|
||||||
|
case 'group:list':
|
||||||
|
return { command: 'group.list', args: {} };
|
||||||
|
case 'group:query':
|
||||||
|
return { command: 'group.query', args: { search: str(params, 'search') } };
|
||||||
|
case 'group:tabs':
|
||||||
|
return { command: 'group.tabs', args: { groupId: numArg(params.groupId) } };
|
||||||
|
case 'group:count':
|
||||||
|
return { command: 'group.count', args: {} };
|
||||||
|
case 'group:create':
|
||||||
|
return { command: 'group.open', args: { name: str(params, 'name') } };
|
||||||
|
case 'group:addTab':
|
||||||
|
return { command: 'group.add_tab', args: compact({ group: str(params, 'group'), url: str(params, 'url') }) };
|
||||||
|
case 'group:move': {
|
||||||
|
const direction = str(params, 'direction');
|
||||||
|
return {
|
||||||
|
command: 'group.move',
|
||||||
|
args: { group: str(params, 'group'), forward: direction === 'forward', backward: direction === 'backward' },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case 'group:close':
|
||||||
|
return { command: 'group.close', args: { groupId: numArg(params.groupId), gentleMode: str(params, 'gentleMode') || 'auto' } };
|
||||||
|
|
||||||
|
// --- Windows ----------------------------------------------------------
|
||||||
|
case 'window:list':
|
||||||
|
return { command: 'windows.list', args: {} };
|
||||||
|
case 'window:open':
|
||||||
|
return { command: 'windows.open', args: compact({ url: str(params, 'url') }) };
|
||||||
|
case 'window:close':
|
||||||
|
return { command: 'windows.close', args: { windowId: numArg(params.windowId) } };
|
||||||
|
case 'window:rename':
|
||||||
|
return { command: 'windows.rename', args: { windowId: numArg(params.windowId), name: str(params, 'name') } };
|
||||||
|
|
||||||
|
// --- Sessions ---------------------------------------------------------
|
||||||
|
case 'session:list':
|
||||||
|
return { command: 'session.list', args: {} };
|
||||||
|
case 'session:save':
|
||||||
|
return { command: 'session.save', args: { name: str(params, 'name') } };
|
||||||
|
case 'session:load':
|
||||||
|
return { command: 'session.load', args: { name: str(params, 'name') } };
|
||||||
|
case 'session:remove':
|
||||||
|
return { command: 'session.remove', args: { name: str(params, 'name') } };
|
||||||
|
case 'session:export':
|
||||||
|
return { command: 'session.export', args: compact({ name: str(params, 'name') }) };
|
||||||
|
case 'session:diff':
|
||||||
|
return { command: 'session.diff', args: { nameA: str(params, 'nameA'), nameB: str(params, 'nameB') } };
|
||||||
|
case 'session:autoSave':
|
||||||
|
return { command: 'session.auto_save', args: { enabled: Boolean(params.enabled) } };
|
||||||
|
|
||||||
|
// --- Storage ----------------------------------------------------------
|
||||||
|
case 'storage:get':
|
||||||
|
return {
|
||||||
|
command: 'storage.get',
|
||||||
|
args: compact({ key: str(params, 'key'), type: str(params, 'storeType') || 'local', tabId: tabIdArg(params.tabId) }),
|
||||||
|
};
|
||||||
|
case 'storage:set':
|
||||||
|
return {
|
||||||
|
command: 'storage.set',
|
||||||
|
args: compact({
|
||||||
|
key: str(params, 'key'),
|
||||||
|
value: str(params, 'value'),
|
||||||
|
type: str(params, 'storeType') || 'local',
|
||||||
|
tabId: tabIdArg(params.tabId),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Performance ------------------------------------------------------
|
||||||
|
case 'perf:status':
|
||||||
|
return { command: 'perf.status', args: {} };
|
||||||
|
|
||||||
|
// --- Extension --------------------------------------------------------
|
||||||
|
case 'extension:info':
|
||||||
|
return { command: 'extension.info', args: {} };
|
||||||
|
case 'extension:capabilities':
|
||||||
|
return { command: 'extension.capabilities', args: {} };
|
||||||
|
case 'extension:reload':
|
||||||
|
return { command: 'extension.reload', args: {} };
|
||||||
|
|
||||||
// --- Clients ----------------------------------------------------------
|
// --- Clients ----------------------------------------------------------
|
||||||
case 'client:list':
|
case 'client:list':
|
||||||
return { command: 'clients.list', args: {} };
|
return { command: 'clients.list', args: {} };
|
||||||
|
|
||||||
// --- Gateway: serve has no health route, so ping with a safe command --
|
|
||||||
case 'gateway:health':
|
|
||||||
return { command: 'tabs.list', args: {} };
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unsupported operation "${operation}" for resource "${resource}"`);
|
throw new Error(`Unsupported operation "${operation}" for resource "${resource}"`);
|
||||||
}
|
}
|
||||||
@@ -108,6 +269,19 @@ function tabIdArg(value: unknown): number | undefined {
|
|||||||
return Number.isFinite(n) && n > 0 ? n : undefined;
|
return Number.isFinite(n) && n > 0 ? n : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** A required numeric arg; non-finite values fall through as 0. */
|
||||||
|
function numArg(value: unknown): number {
|
||||||
|
const n = Number(value);
|
||||||
|
return Number.isFinite(n) ? n : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** An optional numeric arg that keeps 0 (a meaningful index/coordinate). */
|
||||||
|
function indexArg(value: unknown): number | undefined {
|
||||||
|
if (value === undefined || value === null || value === '') return undefined;
|
||||||
|
const n = Number(value);
|
||||||
|
return Number.isFinite(n) ? n : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
/** Accept an array, a JSON array string, or a comma/space separated list. */
|
/** Accept an array, a JSON array string, or a comma/space separated list. */
|
||||||
export function parseTabIds(value: unknown): number[] {
|
export function parseTabIds(value: unknown): number[] {
|
||||||
if (Array.isArray(value)) return value.map(Number).filter(Number.isFinite);
|
if (Array.isArray(value)) return value.map(Number).filter(Number.isFinite);
|
||||||
|
|||||||
@@ -10,12 +10,12 @@ import { connect as tlsConnect } from 'node:tls';
|
|||||||
import type { Socket } from 'node:net';
|
import type { Socket } from 'node:net';
|
||||||
import { randomUUID } from 'node:crypto';
|
import { randomUUID } from 'node:crypto';
|
||||||
|
|
||||||
import { buildAuthPayload, decodeResponse, frame, type Challenge } from './protocol';
|
import { buildAuthPayload, decodeResponse, frame, verifyServerIdentity, type Challenge } from './protocol';
|
||||||
|
|
||||||
/** Version advertised to the server. Must be >= the server's PROTOCOL_MIN_CLIENT
|
/** Version advertised to the server. Must be >= the server's PROTOCOL_MIN_CLIENT
|
||||||
* (0.9.0) and >= 0.9.5 so the server enforces the post-quantum handshake this
|
* (0.9.0) and >= 0.9.5 so the server enforces the post-quantum handshake this
|
||||||
* client implements. */
|
* client implements. */
|
||||||
const CLIENT_VERSION = '0.15.4';
|
const CLIENT_VERSION = '0.16.0';
|
||||||
const USER_AGENT = `browser-cli/${CLIENT_VERSION}`;
|
const USER_AGENT = `browser-cli/${CLIENT_VERSION}`;
|
||||||
// Force a plain-JSON, uncompressed response so no msgpack/zstd decoder is needed.
|
// Force a plain-JSON, uncompressed response so no msgpack/zstd decoder is needed.
|
||||||
const ACCEPT_ENCODING = { ser: ['json'], comp: [] as string[] };
|
const ACCEPT_ENCODING = { ser: ['json'], comp: [] as string[] };
|
||||||
@@ -33,6 +33,10 @@ export interface ServeConnectOptions {
|
|||||||
privateKeyPem?: string | null;
|
privateKeyPem?: string | null;
|
||||||
/** Optional `_route` target for a multi-browser serve. */
|
/** Optional `_route` target for a multi-browser serve. */
|
||||||
route?: string | null;
|
route?: string | null;
|
||||||
|
/** Expected server identity: raw Ed25519 public key hex or SHA256 fingerprint. */
|
||||||
|
serverIdentity?: string | null;
|
||||||
|
/** Allow unknown server identities (TOFU disabled). Intended for loopback/dev only. */
|
||||||
|
allowUnknownServerIdentity?: boolean;
|
||||||
timeoutMs?: number;
|
timeoutMs?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,6 +125,12 @@ export async function sendServeCommand(
|
|||||||
const run = async () => {
|
const run = async () => {
|
||||||
const challengeRaw = await reader.next();
|
const challengeRaw = await reader.next();
|
||||||
const challenge = JSON.parse(challengeRaw.toString('utf8')) as Challenge;
|
const challenge = JSON.parse(challengeRaw.toString('utf8')) as Challenge;
|
||||||
|
verifyServerIdentity(
|
||||||
|
challenge,
|
||||||
|
opts.serverIdentity,
|
||||||
|
`${opts.host}:${opts.port}`,
|
||||||
|
Boolean(opts.allowUnknownServerIdentity),
|
||||||
|
);
|
||||||
|
|
||||||
const baseMsg: Record<string, unknown> = {
|
const baseMsg: Record<string, unknown> = {
|
||||||
id: randomUUID(),
|
id: randomUUID(),
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "n8n-nodes-browser-cli",
|
"name": "n8n-nodes-browser-cli",
|
||||||
"version": "0.2.4",
|
"version": "0.3.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "n8n-nodes-browser-cli",
|
"name": "n8n-nodes-browser-cli",
|
||||||
"version": "0.2.4",
|
"version": "0.3.1",
|
||||||
"license": "PolyForm-Noncommercial-1.0.0",
|
"license": "PolyForm-Noncommercial-1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@noble/post-quantum": "^0.6.1"
|
"@noble/post-quantum": "^0.6.1"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "n8n-nodes-browser-cli",
|
"name": "n8n-nodes-browser-cli",
|
||||||
"version": "0.2.4",
|
"version": "0.3.1",
|
||||||
"description": "n8n community node that controls a remote browser by talking directly to a browser-cli serve endpoint (Ed25519 + post-quantum encrypted)",
|
"description": "n8n community node that controls a remote browser by talking directly to a browser-cli serve endpoint (Ed25519 + post-quantum encrypted)",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"n8n-community-node-package",
|
"n8n-community-node-package",
|
||||||
|
|||||||
@@ -11,7 +11,10 @@ import {
|
|||||||
pqDecrypt,
|
pqDecrypt,
|
||||||
pqEncrypt,
|
pqEncrypt,
|
||||||
pqTransportKey,
|
pqTransportKey,
|
||||||
|
serverFingerprint,
|
||||||
signAuth,
|
signAuth,
|
||||||
|
verifyServerChallengeSignature,
|
||||||
|
verifyServerIdentity,
|
||||||
} from '../nodes/BrowserCli/protocol';
|
} from '../nodes/BrowserCli/protocol';
|
||||||
|
|
||||||
// Known-answer vectors produced by the real Python implementation
|
// Known-answer vectors produced by the real Python implementation
|
||||||
@@ -49,6 +52,19 @@ const DECRYPT_ENV = {
|
|||||||
ciphertext: '7e4f75fa68098ea9a162dfd49af7824526186b77e9ac346b58f30d73df2bef88d5e6cd',
|
ciphertext: '7e4f75fa68098ea9a162dfd49af7824526186b77e9ac346b58f30d73df2bef88d5e6cd',
|
||||||
};
|
};
|
||||||
const DECRYPT_PLAIN = 'hello world payload';
|
const DECRYPT_PLAIN = 'hello world payload';
|
||||||
|
const SERVER_PUB_HEX = '982c13bda72ef7b2bf4a8cd9756e4f283faaf4a34f9dec8e6c65585b64d9a902';
|
||||||
|
const SERVER_SIG =
|
||||||
|
'fa6897bb7f00f711ee8af151384eda736a503008f8b8e11b972cfea46a0366d5' +
|
||||||
|
'3127c2f1e9ba01e72805b267d023c552756645ae7d95fd00fa277ed20a43ee09';
|
||||||
|
const SERVER_FP = 'SHA256:wsYDqD4OnF/Sfvr3RKvVCOW8ET802H2qHSvWfnQwQrs';
|
||||||
|
const SERVER_CHALLENGE = {
|
||||||
|
type: 'challenge',
|
||||||
|
nonce: NONCE_HEX,
|
||||||
|
server_version: '0.16.4',
|
||||||
|
min_client_version: '0.9.0',
|
||||||
|
server_pubkey: SERVER_PUB_HEX,
|
||||||
|
server_sig: SERVER_SIG,
|
||||||
|
};
|
||||||
|
|
||||||
test('canonicalJson matches Python json.dumps(sort_keys, ensure_ascii)', () => {
|
test('canonicalJson matches Python json.dumps(sort_keys, ensure_ascii)', () => {
|
||||||
assert.equal(canonicalJson(MSG), CANON);
|
assert.equal(canonicalJson(MSG), CANON);
|
||||||
@@ -139,3 +155,26 @@ test('decodeResponse parses plain JSON and decrypts PQ envelopes', () => {
|
|||||||
const raw = Buffer.from(JSON.stringify({ encrypted: env }));
|
const raw = Buffer.from(JSON.stringify({ encrypted: env }));
|
||||||
assert.deepEqual(decodeResponse(raw, secret), { success: true, data: 1 });
|
assert.deepEqual(decodeResponse(raw, secret), { success: true, data: 1 });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('server identity fingerprint and signature match Python challenge signing', () => {
|
||||||
|
assert.equal(serverFingerprint(SERVER_PUB_HEX), SERVER_FP);
|
||||||
|
assert.equal(verifyServerChallengeSignature(SERVER_CHALLENGE), true);
|
||||||
|
assert.equal(verifyServerChallengeSignature({ ...SERVER_CHALLENGE, nonce: '22'.repeat(32) }), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('verifyServerIdentity accepts pinned pubkey or fingerprint', () => {
|
||||||
|
assert.doesNotThrow(() => verifyServerIdentity(SERVER_CHALLENGE, SERVER_PUB_HEX, 'browser-host.example:8765', false));
|
||||||
|
assert.doesNotThrow(() => verifyServerIdentity(SERVER_CHALLENGE, SERVER_FP, 'browser-host.example:8765', false));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('verifyServerIdentity rejects unknown and changed identities', () => {
|
||||||
|
assert.throws(
|
||||||
|
() => verifyServerIdentity(SERVER_CHALLENGE, null, 'browser-host.example:8765', false),
|
||||||
|
/Unknown browser-cli server identity/,
|
||||||
|
);
|
||||||
|
assert.throws(
|
||||||
|
() => verifyServerIdentity(SERVER_CHALLENGE, '00'.repeat(32), 'browser-host.example:8765', false),
|
||||||
|
/REMOTE SERVER IDENTITY CHANGED/,
|
||||||
|
);
|
||||||
|
assert.doesNotThrow(() => verifyServerIdentity(SERVER_CHALLENGE, null, 'browser-host.example:8765', true));
|
||||||
|
});
|
||||||
|
|||||||
@@ -11,10 +11,6 @@ test('client:list maps to clients.list', () => {
|
|||||||
assert.deepEqual(buildCommand('client', 'list', {}), { command: 'clients.list', args: {} });
|
assert.deepEqual(buildCommand('client', 'list', {}), { command: 'clients.list', args: {} });
|
||||||
});
|
});
|
||||||
|
|
||||||
test('gateway:health pings with tabs.list (serve has no health route)', () => {
|
|
||||||
assert.deepEqual(buildCommand('gateway', 'health', {}), { command: 'tabs.list', args: {} });
|
|
||||||
});
|
|
||||||
|
|
||||||
test('tab:open sends navigate.open with background derived from focus', () => {
|
test('tab:open sends navigate.open with background derived from focus', () => {
|
||||||
const bg = buildCommand('tab', 'open', { url: 'https://example.com', focus: false });
|
const bg = buildCommand('tab', 'open', { url: 'https://example.com', focus: false });
|
||||||
assert.deepEqual(bg, {
|
assert.deepEqual(bg, {
|
||||||
@@ -60,8 +56,128 @@ test('command:execute passes command and args through', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('tab read ops map to safe commands', () => {
|
||||||
|
assert.deepEqual(buildCommand('tab', 'query', { search: 'docs' }), { command: 'tabs.query', args: { search: 'docs' } });
|
||||||
|
assert.deepEqual(buildCommand('tab', 'filter', { pattern: '*.dev/*' }), { command: 'tabs.filter', args: { pattern: '*.dev/*' } });
|
||||||
|
assert.deepEqual(buildCommand('tab', 'count', { pattern: '' }).args, {}, 'empty pattern is dropped');
|
||||||
|
assert.deepEqual(buildCommand('tab', 'get', { tabId: 0 }).args, {}, 'tabId 0 means active tab');
|
||||||
|
assert.deepEqual(buildCommand('tab', 'get', { tabId: 5 }), { command: 'tabs.status', args: { tabId: 5 } });
|
||||||
|
assert.deepEqual(buildCommand('tab', 'activeInWindow', { windowId: 3 }), {
|
||||||
|
command: 'tabs.active_in_window',
|
||||||
|
args: { windowId: 3 },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('tab control ops map to navigate/tabs commands', () => {
|
||||||
|
assert.deepEqual(buildCommand('tab', 'activate', { tabId: 7 }), { command: 'tabs.active', args: { tabId: 7 } });
|
||||||
|
assert.deepEqual(buildCommand('tab', 'navigateTo', { tabId: 7, url: 'https://x.dev' }), {
|
||||||
|
command: 'navigate.to',
|
||||||
|
args: { tabId: 7, url: 'https://x.dev' },
|
||||||
|
});
|
||||||
|
assert.deepEqual(buildCommand('tab', 'reload', { tabId: 0 }), { command: 'navigate.reload', args: {} });
|
||||||
|
assert.deepEqual(buildCommand('tab', 'back', { tabId: 0 }).command, 'navigate.back');
|
||||||
|
assert.deepEqual(buildCommand('tab', 'mute', { tabId: 2 }), { command: 'tabs.mute', args: { tabId: 2 } });
|
||||||
|
assert.deepEqual(buildCommand('tab', 'pin', { tabId: 0 }), { command: 'tabs.pin', args: {} });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('tab move keeps index 0 but drops active tabId fallback for window', () => {
|
||||||
|
assert.deepEqual(buildCommand('tab', 'move', { tabId: 4, windowId: 0, index: 0 }), {
|
||||||
|
command: 'tabs.move',
|
||||||
|
args: { tabId: 4, index: 0 },
|
||||||
|
});
|
||||||
|
assert.deepEqual(buildCommand('tab', 'move', { tabId: 4, windowId: 9, index: '' }).args, { tabId: 4, windowId: 9 });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('tab rearrange ops default gentleMode and sort key', () => {
|
||||||
|
assert.deepEqual(buildCommand('tab', 'dedupe', {}), { command: 'tabs.dedupe', args: { gentleMode: 'auto' } });
|
||||||
|
assert.deepEqual(buildCommand('tab', 'sort', { by: 'title' }), {
|
||||||
|
command: 'tabs.sort',
|
||||||
|
args: { by: 'title', gentleMode: 'auto' },
|
||||||
|
});
|
||||||
|
assert.deepEqual(buildCommand('tab', 'mergeWindows', {}).command, 'tabs.merge_windows');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('tab screenshot includes quality only when set', () => {
|
||||||
|
assert.deepEqual(buildCommand('tab', 'screenshot', { tabId: 0, format: 'png', quality: '' }).args, { format: 'png' });
|
||||||
|
assert.deepEqual(buildCommand('tab', 'screenshot', { tabId: 1, format: 'jpeg', quality: 80 }).args, {
|
||||||
|
tabId: 1,
|
||||||
|
format: 'jpeg',
|
||||||
|
quality: 80,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('dom interaction ops map to dom.* commands', () => {
|
||||||
|
assert.deepEqual(buildCommand('dom', 'attr', { selector: 'a', attr: 'href' }), {
|
||||||
|
command: 'dom.attr',
|
||||||
|
args: { selector: 'a', attr: 'href' },
|
||||||
|
});
|
||||||
|
assert.deepEqual(buildCommand('dom', 'select', { selector: '#s', value: 'v' }), {
|
||||||
|
command: 'dom.select',
|
||||||
|
args: { selector: '#s', value: 'v' },
|
||||||
|
});
|
||||||
|
assert.deepEqual(buildCommand('dom', 'key', { key: 'Enter', selector: '' }).args, { key: 'Enter' });
|
||||||
|
assert.deepEqual(buildCommand('dom', 'scroll', { selector: '', x: '', y: 500 }).args, { y: 500 });
|
||||||
|
assert.deepEqual(buildCommand('dom', 'exists', { selector: '#x' }), { command: 'dom.exists', args: { selector: '#x' } });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('page extractJson sends selector', () => {
|
||||||
|
assert.deepEqual(buildCommand('page', 'extractJson', { selector: 'script' }), {
|
||||||
|
command: 'extract.json',
|
||||||
|
args: { selector: 'script' },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('group ops map to group.* commands', () => {
|
||||||
|
assert.deepEqual(buildCommand('group', 'create', { name: 'Research' }), { command: 'group.open', args: { name: 'Research' } });
|
||||||
|
assert.deepEqual(buildCommand('group', 'tabs', { groupId: 3 }), { command: 'group.tabs', args: { groupId: 3 } });
|
||||||
|
assert.deepEqual(buildCommand('group', 'addTab', { group: 'Research', url: '' }).args, { group: 'Research' });
|
||||||
|
assert.deepEqual(buildCommand('group', 'move', { group: '5', direction: 'backward' }), {
|
||||||
|
command: 'group.move',
|
||||||
|
args: { group: '5', forward: false, backward: true },
|
||||||
|
});
|
||||||
|
assert.deepEqual(buildCommand('group', 'close', { groupId: 2, gentleMode: 'off' }).args, { groupId: 2, gentleMode: 'off' });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('window ops map to windows.* commands', () => {
|
||||||
|
assert.deepEqual(buildCommand('window', 'open', { url: '' }), { command: 'windows.open', args: {} });
|
||||||
|
assert.deepEqual(buildCommand('window', 'rename', { windowId: 1, name: 'Work' }), {
|
||||||
|
command: 'windows.rename',
|
||||||
|
args: { windowId: 1, name: 'Work' },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('session ops map to session.* commands', () => {
|
||||||
|
assert.deepEqual(buildCommand('session', 'save', { name: 'morning' }), { command: 'session.save', args: { name: 'morning' } });
|
||||||
|
assert.deepEqual(buildCommand('session', 'export', { name: '' }), { command: 'session.export', args: {} });
|
||||||
|
assert.deepEqual(buildCommand('session', 'diff', { nameA: 'a', nameB: 'b' }).args, { nameA: 'a', nameB: 'b' });
|
||||||
|
assert.deepEqual(buildCommand('session', 'autoSave', { enabled: false }), {
|
||||||
|
command: 'session.auto_save',
|
||||||
|
args: { enabled: false },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('storage ops default to local and drop active tabId', () => {
|
||||||
|
assert.deepEqual(buildCommand('storage', 'get', { key: 'token', storeType: 'local', tabId: 0 }), {
|
||||||
|
command: 'storage.get',
|
||||||
|
args: { key: 'token', type: 'local' },
|
||||||
|
});
|
||||||
|
assert.deepEqual(buildCommand('storage', 'set', { key: 'k', value: 'v', storeType: 'session', tabId: 4 }), {
|
||||||
|
command: 'storage.set',
|
||||||
|
args: { key: 'k', value: 'v', type: 'session', tabId: 4 },
|
||||||
|
});
|
||||||
|
assert.deepEqual(buildCommand('storage', 'get', { key: '', storeType: 'local', tabId: 0 }).args, { type: 'local' });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('perf and extension ops map to safe/control commands', () => {
|
||||||
|
assert.deepEqual(buildCommand('perf', 'status', {}), { command: 'perf.status', args: {} });
|
||||||
|
assert.deepEqual(buildCommand('extension', 'capabilities', {}), { command: 'extension.capabilities', args: {} });
|
||||||
|
assert.deepEqual(buildCommand('extension', 'reload', {}), { command: 'extension.reload', args: {} });
|
||||||
|
});
|
||||||
|
|
||||||
test('unknown operation throws', () => {
|
test('unknown operation throws', () => {
|
||||||
assert.throws(() => buildCommand('tab', 'nope', {}), /Unsupported operation/);
|
assert.throws(() => buildCommand('tab', 'nope', {}), /Unsupported operation/);
|
||||||
|
assert.throws(() => buildCommand('connection', 'health', {}), /Unsupported operation/);
|
||||||
|
assert.throws(() => buildCommand('gateway', 'health', {}), /Unsupported operation/);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('parseTabIds accepts array, json, and delimited strings', () => {
|
test('parseTabIds accepts array, json, and delimited strings', () => {
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "real-browser-cli"
|
name = "real-browser-cli"
|
||||||
version = "0.16.3"
|
version = "0.16.6"
|
||||||
description = "Control your real running browser from the terminal or Python SDK"
|
description = "Control your real running browser from the terminal or Python SDK"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
license = { file = "LICENSE" }
|
license = { file = "LICENSE" }
|
||||||
|
|||||||
+34
-11
@@ -294,29 +294,52 @@ def test_clients_reads_registry_with_trailing_garbage(tmp_path):
|
|||||||
assert "0.8.2" in result.output
|
assert "0.8.2" in result.output
|
||||||
|
|
||||||
def test_clients_remote_uses_remote_endpoint_without_local_registry():
|
def test_clients_remote_uses_remote_endpoint_without_local_registry():
|
||||||
def fake_send_command(command, args=None, profile=None, remote=None, key=None):
|
target = BrowserTarget(
|
||||||
assert command == "clients.list"
|
profile="work",
|
||||||
assert profile is None
|
display_name="127.0.0.1:work",
|
||||||
assert remote == "127.0.0.1:8765"
|
socket_path="",
|
||||||
return [{"name": "Chrome", "version": "1", "extensionVersion": "2.3.4"}]
|
remote="127.0.0.1:8765",
|
||||||
|
browser_name="Chrome",
|
||||||
|
display_group="127.0.0.1",
|
||||||
|
version="1",
|
||||||
|
extension_version="2.3.4",
|
||||||
|
)
|
||||||
|
|
||||||
with patch.dict(os.environ, {}, clear=True), patch(
|
with patch.dict(os.environ, {}, clear=True), patch(
|
||||||
"browser_cli.commands.clients.REGISTRY_PATH", Path("/nonexistent/browser-cli-registry.json")
|
"browser_cli.commands.clients.REGISTRY_PATH", Path("/nonexistent/browser-cli-registry.json")
|
||||||
), patch("browser_cli.client.core.send_command", side_effect=fake_send_command) as send_command:
|
), patch("browser_cli.client.core.remote_browser_targets", return_value=[target]), patch(
|
||||||
|
"browser_cli.client.core.send_command"
|
||||||
|
) as send_command:
|
||||||
result = CliRunner().invoke(main, ["--remote", "127.0.0.1:8765", "clients"])
|
result = CliRunner().invoke(main, ["--remote", "127.0.0.1:8765", "clients"])
|
||||||
|
|
||||||
assert result.exit_code == 0
|
assert result.exit_code == 0
|
||||||
send_command.assert_called_once()
|
send_command.assert_not_called()
|
||||||
assert "remote" in result.output
|
assert "work" in result.output
|
||||||
|
assert "127.0.0.1" not in result.output
|
||||||
assert "Chrome" in result.output
|
assert "Chrome" in result.output
|
||||||
assert "2.3.4" in result.output
|
assert "2.3.4" in result.output
|
||||||
|
|
||||||
def test_clients_remote_respects_global_browser_route():
|
def test_clients_remote_respects_global_browser_route():
|
||||||
with patch.dict(os.environ, {}, clear=True), patch("browser_cli.client.core.send_command", return_value=[]) as send_command:
|
target = BrowserTarget(
|
||||||
|
profile="work",
|
||||||
|
display_name="127.0.0.1:work",
|
||||||
|
socket_path="",
|
||||||
|
remote="127.0.0.1:8765",
|
||||||
|
browser_name="Chrome",
|
||||||
|
display_group="127.0.0.1",
|
||||||
|
version="1",
|
||||||
|
extension_version="2.3.4",
|
||||||
|
)
|
||||||
|
|
||||||
|
with patch.dict(os.environ, {}, clear=True), patch(
|
||||||
|
"browser_cli.client.core.remote_browser_targets", return_value=[target]
|
||||||
|
), patch("browser_cli.client.core.send_command") as send_command:
|
||||||
result = CliRunner().invoke(main, ["--remote", "127.0.0.1:8765", "--browser", "work", "clients"])
|
result = CliRunner().invoke(main, ["--remote", "127.0.0.1:8765", "--browser", "work", "clients"])
|
||||||
|
|
||||||
assert result.exit_code == 1
|
assert result.exit_code == 0
|
||||||
send_command.assert_called_once_with("clients.list", profile="work", remote="127.0.0.1:8765", key=None)
|
send_command.assert_not_called()
|
||||||
|
assert "work" in result.output
|
||||||
|
assert "127.0.0.1" not in result.output
|
||||||
|
|
||||||
def test_clients_browser_alias_resolves_to_remote():
|
def test_clients_browser_alias_resolves_to_remote():
|
||||||
"""--browser <host> without --remote resolves the alias, fetches all targets from that remote,
|
"""--browser <host> without --remote resolves the alias, fetches all targets from that remote,
|
||||||
|
|||||||
@@ -649,6 +649,57 @@ def test_collect_browser_clients_uses_cached_target_version(monkeypatch, tmp_pat
|
|||||||
"extensionVersion": "0.15.6",
|
"extensionVersion": "0.15.6",
|
||||||
}]
|
}]
|
||||||
|
|
||||||
|
def test_collect_browser_clients_with_explicit_remote_lists_all_targets(monkeypatch, tmp_path):
|
||||||
|
"""`browser-cli --remote host clients` should list all profiles, not auto-route and fail as ambiguous."""
|
||||||
|
from browser_cli.client import collect_browser_clients
|
||||||
|
import browser_cli.client.core as core
|
||||||
|
|
||||||
|
targets = [
|
||||||
|
BrowserTarget(
|
||||||
|
profile="main",
|
||||||
|
display_name="browser-host.example:main",
|
||||||
|
socket_path="",
|
||||||
|
remote="browser-host.example:8765",
|
||||||
|
browser_name="Chrome",
|
||||||
|
display_group="browser-host.example",
|
||||||
|
version="149.0.0.0",
|
||||||
|
extension_version="0.16.4",
|
||||||
|
),
|
||||||
|
BrowserTarget(
|
||||||
|
profile="work",
|
||||||
|
display_name="browser-host.example:work",
|
||||||
|
socket_path="",
|
||||||
|
remote="browser-host.example:8765",
|
||||||
|
browser_name="Firefox",
|
||||||
|
display_group="browser-host.example",
|
||||||
|
version="151.0",
|
||||||
|
extension_version="0.16.4",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
monkeypatch.setattr(core, "remote_browser_targets", lambda endpoint, key=None: targets)
|
||||||
|
monkeypatch.setattr(core, "send_command", lambda *a, **k: pytest.fail("clients.list must not auto-route for cached targets"))
|
||||||
|
|
||||||
|
rows = collect_browser_clients(remote="browser-host.example:8765", registry_path=tmp_path / "missing-registry.json")
|
||||||
|
|
||||||
|
assert [row["profile"] for row in rows] == ["main", "work"]
|
||||||
|
assert [row.get("profileGroup") for row in rows] == [None, None]
|
||||||
|
assert [row["name"] for row in rows] == ["Chrome", "Firefox"]
|
||||||
|
|
||||||
|
def test_collect_browser_clients_with_explicit_remote_and_browser_filters_target(monkeypatch, tmp_path):
|
||||||
|
from browser_cli.client import collect_browser_clients
|
||||||
|
import browser_cli.client.core as core
|
||||||
|
|
||||||
|
targets = [
|
||||||
|
BrowserTarget("main", "browser-host.example:main", "", remote="browser-host.example:8765", version="1"),
|
||||||
|
BrowserTarget("work", "browser-host.example:work", "", remote="browser-host.example:8765", version="1"),
|
||||||
|
]
|
||||||
|
monkeypatch.setattr(core, "remote_browser_targets", lambda endpoint, key=None: targets)
|
||||||
|
|
||||||
|
rows = collect_browser_clients(remote="browser-host.example:8765", browser_alias="work", registry_path=tmp_path / "missing-registry.json")
|
||||||
|
|
||||||
|
assert [row["profile"] for row in rows] == ["work"]
|
||||||
|
assert rows[0].get("profileGroup") is None
|
||||||
|
|
||||||
def test_collect_browser_clients_falls_back_when_version_unknown(monkeypatch, tmp_path):
|
def test_collect_browser_clients_falls_back_when_version_unknown(monkeypatch, tmp_path):
|
||||||
"""An older remote (no advertised version) still triggers a clients.list query."""
|
"""An older remote (no advertised version) still triggers a clients.list query."""
|
||||||
from browser_cli.client import collect_browser_clients
|
from browser_cli.client import collect_browser_clients
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
"""Compat shim framework.
|
||||||
|
|
||||||
|
The registries are empty today (no legacy-client shim has been needed since the
|
||||||
|
first public release, 0.14.1), so every adapter must be a verbatim pass-through
|
||||||
|
regardless of client version. These tests lock that in and exercise the
|
||||||
|
empty-registry short-circuit so the seam can't silently start mutating traffic.
|
||||||
|
"""
|
||||||
|
import browser_cli.compat as compat
|
||||||
|
from browser_cli.compat import adapt_auth, adapt_request, adapt_response
|
||||||
|
|
||||||
|
def test_registries_are_empty():
|
||||||
|
assert compat.commands._COMPAT == []
|
||||||
|
assert compat.auth._AUTH_COMPAT == []
|
||||||
|
|
||||||
|
def test_adapt_auth_is_passthrough_for_any_version():
|
||||||
|
msg = {"id": "1", "command": "tabs.list", "pubkey": "ABCdef", "args": {"x": 1}}
|
||||||
|
for version in ("0.9.0", "0.14.1", "0.16.4", "99.0.0"):
|
||||||
|
out = adapt_auth(msg, version)
|
||||||
|
assert out == msg
|
||||||
|
# pubkey casing is NOT normalized anymore (the old <0.9.3 shim is gone)
|
||||||
|
assert out["pubkey"] == "ABCdef"
|
||||||
|
|
||||||
|
def test_adapt_request_is_passthrough():
|
||||||
|
msg = {"command": "tabs.query", "args": {"search": "docs"}}
|
||||||
|
assert adapt_request(msg, "0.9.0") == msg
|
||||||
|
assert adapt_request(msg, "0.16.4") == msg
|
||||||
|
|
||||||
|
def test_adapt_response_is_passthrough():
|
||||||
|
resp = b'{"id":"1","success":true,"data":[]}'
|
||||||
|
assert adapt_response(resp, "tabs.list", "0.9.0") == resp
|
||||||
|
assert adapt_response(resp, "tabs.list", "0.16.4") == resp
|
||||||
|
|
||||||
|
def test_empty_guard_skips_version_parsing(monkeypatch):
|
||||||
|
"""With empty registries the adapters return before parse_version runs."""
|
||||||
|
called = False
|
||||||
|
|
||||||
|
def _boom(_v):
|
||||||
|
nonlocal called
|
||||||
|
called = True
|
||||||
|
raise AssertionError("parse_version should not be called on an empty registry")
|
||||||
|
|
||||||
|
monkeypatch.setattr(compat.auth, "parse_version", _boom)
|
||||||
|
monkeypatch.setattr(compat.commands, "parse_version", _boom)
|
||||||
|
|
||||||
|
assert adapt_auth({"a": 1}, "0.9.0") == {"a": 1}
|
||||||
|
assert adapt_request({"a": 1}, "0.9.0") == {"a": 1}
|
||||||
|
assert adapt_response(b"x", "cmd", "0.9.0") == b"x"
|
||||||
|
assert called is False
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import json
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from browser_cli.auth.server_identity import load_or_create_server_identity, public_key_hex, sign_challenge, verify_challenge_signature
|
||||||
|
from browser_cli.errors import BrowserNotConnected
|
||||||
|
from browser_cli.remote import known_hosts
|
||||||
|
|
||||||
|
def _challenge(tmp_path):
|
||||||
|
key = load_or_create_server_identity(tmp_path / "server.pem")
|
||||||
|
msg = {
|
||||||
|
"type": "challenge",
|
||||||
|
"nonce": "00" * 32,
|
||||||
|
"server_version": "0.16.4",
|
||||||
|
"min_client_version": "0.9.0",
|
||||||
|
"server_pubkey": public_key_hex(key),
|
||||||
|
}
|
||||||
|
msg["server_sig"] = sign_challenge(msg, key)
|
||||||
|
return msg
|
||||||
|
|
||||||
|
def test_challenge_signature_verifies(tmp_path):
|
||||||
|
challenge = _challenge(tmp_path)
|
||||||
|
|
||||||
|
assert verify_challenge_signature(challenge) is True
|
||||||
|
|
||||||
|
challenge["nonce"] = "11" * 32
|
||||||
|
assert verify_challenge_signature(challenge) is False
|
||||||
|
|
||||||
|
def test_known_host_mismatch_is_rejected(monkeypatch, tmp_path):
|
||||||
|
path = tmp_path / "known_hosts.json"
|
||||||
|
challenge = _challenge(tmp_path)
|
||||||
|
monkeypatch.setattr(known_hosts, "KNOWN_HOSTS_PATH", path)
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
path.write_text(json.dumps({"browser-host.example": "00" * 32}), encoding="utf-8")
|
||||||
|
|
||||||
|
with pytest.raises(BrowserNotConnected, match="REMOTE SERVER IDENTITY CHANGED"):
|
||||||
|
known_hosts.verify_known_host("browser-host.example", challenge)
|
||||||
|
|
||||||
|
def test_unknown_non_interactive_host_is_rejected(monkeypatch, tmp_path):
|
||||||
|
path = tmp_path / "known_hosts.json"
|
||||||
|
challenge = _challenge(tmp_path)
|
||||||
|
monkeypatch.setattr(known_hosts, "KNOWN_HOSTS_PATH", path)
|
||||||
|
monkeypatch.setattr("sys.stdin.isatty", lambda: False)
|
||||||
|
|
||||||
|
with pytest.raises(BrowserNotConnected, match="Unknown remote server identity"):
|
||||||
|
known_hosts.verify_known_host("browser-host.example", challenge)
|
||||||
|
|
||||||
|
def test_loopback_unknown_host_is_allowed(monkeypatch, tmp_path):
|
||||||
|
path = tmp_path / "known_hosts.json"
|
||||||
|
challenge = _challenge(tmp_path)
|
||||||
|
monkeypatch.setattr(known_hosts, "KNOWN_HOSTS_PATH", path)
|
||||||
|
monkeypatch.setattr("sys.stdin.isatty", lambda: False)
|
||||||
|
|
||||||
|
known_hosts.verify_known_host("127.0.0.1:8765", challenge)
|
||||||
|
|
||||||
|
assert not path.exists()
|
||||||
|
|
||||||
|
def test_save_and_remove_known_host(tmp_path):
|
||||||
|
path = tmp_path / "known_hosts.json"
|
||||||
|
known_hosts.save_known_host("browser-host.example:443", "11" * 32, path)
|
||||||
|
|
||||||
|
assert json.loads(path.read_text(encoding="utf-8")) == {"browser-host.example": "11" * 32}
|
||||||
|
assert known_hosts.remove_known_host("browser-host.example", path) is True
|
||||||
|
assert known_hosts.load_known_hosts(path) == {}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import json
|
||||||
|
|
||||||
|
from click.testing import CliRunner
|
||||||
|
|
||||||
|
from browser_cli.commands.remote import remote_group
|
||||||
|
from browser_cli.remote import registry as remote_registry
|
||||||
|
|
||||||
|
def test_save_remote_persists_endpoint_without_key(monkeypatch, tmp_path):
|
||||||
|
path = tmp_path / "remotes.json"
|
||||||
|
monkeypatch.setattr(remote_registry, "REMOTE_REGISTRY_PATH", path)
|
||||||
|
|
||||||
|
remote_registry.save_remote("browser-host.example:443")
|
||||||
|
|
||||||
|
assert json.loads(path.read_text(encoding="utf-8")) == {"browser-host.example": {}}
|
||||||
|
|
||||||
|
def test_save_remote_with_key_and_remove(monkeypatch, tmp_path):
|
||||||
|
path = tmp_path / "remotes.json"
|
||||||
|
monkeypatch.setattr(remote_registry, "REMOTE_REGISTRY_PATH", path)
|
||||||
|
|
||||||
|
remote_registry.save_remote("browser-host.example", "agent")
|
||||||
|
|
||||||
|
assert remote_registry.load_remotes() == {"browser-host.example": {"key": "agent"}}
|
||||||
|
assert remote_registry.remove_remote("browser-host.example:443") is True
|
||||||
|
assert remote_registry.load_remotes() == {}
|
||||||
|
|
||||||
|
def test_resolve_remote_endpoint_prefers_remembered_explicit_port(monkeypatch, tmp_path):
|
||||||
|
path = tmp_path / "remotes.json"
|
||||||
|
monkeypatch.setattr(remote_registry, "REMOTE_REGISTRY_PATH", path)
|
||||||
|
path.write_text(json.dumps({"browser-host.example": {}, "browser-host.example:8765": {}}), encoding="utf-8")
|
||||||
|
|
||||||
|
assert remote_registry.resolve_remote_endpoint("browser-host.example") == "browser-host.example:8765"
|
||||||
|
|
||||||
|
def test_resolve_remote_endpoint_keeps_bare_domain_without_unique_port_match(monkeypatch, tmp_path):
|
||||||
|
path = tmp_path / "remotes.json"
|
||||||
|
monkeypatch.setattr(remote_registry, "REMOTE_REGISTRY_PATH", path)
|
||||||
|
path.write_text(
|
||||||
|
json.dumps({"browser-host.example:8765": {}, "browser-host.example:9000": {}}),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert remote_registry.resolve_remote_endpoint("browser-host.example") == "browser-host.example"
|
||||||
|
|
||||||
|
def test_remote_add_list_remove_cli(monkeypatch, tmp_path):
|
||||||
|
path = tmp_path / "remotes.json"
|
||||||
|
monkeypatch.setattr(remote_registry, "REMOTE_REGISTRY_PATH", path)
|
||||||
|
runner = CliRunner()
|
||||||
|
|
||||||
|
add_result = runner.invoke(remote_group, ["add", "browser-host.example", "--key", "agent"])
|
||||||
|
list_result = runner.invoke(remote_group, ["list"])
|
||||||
|
remove_result = runner.invoke(remote_group, ["remove", "browser-host.example"])
|
||||||
|
|
||||||
|
assert add_result.exit_code == 0
|
||||||
|
assert "Added remote browser-host.example with key agent" in add_result.output
|
||||||
|
assert list_result.exit_code == 0
|
||||||
|
assert "browser-host.example" in list_result.output
|
||||||
|
assert "agent" in list_result.output
|
||||||
|
assert remove_result.exit_code == 0
|
||||||
|
assert "Removed browser-host.example" in remove_result.output
|
||||||
|
assert remote_registry.load_remotes() == {}
|
||||||
@@ -228,33 +228,6 @@ class TestAuthSuccess:
|
|||||||
client.close()
|
client.close()
|
||||||
t.join(timeout=2)
|
t.join(timeout=2)
|
||||||
|
|
||||||
def test_uppercase_pubkey_normalized_by_compat(self, tmp_path, monkeypatch):
|
|
||||||
"""Clients < 0.9.3 may send uppercase pubkeys; compat layer normalises before auth."""
|
|
||||||
path = tmp_path / "authorized_keys"
|
|
||||||
pem, pub = generate_keypair() # pub is lowercase hex
|
|
||||||
path.write_text(pub + "\n")
|
|
||||||
key_path = tmp_path / "client.key.pem"
|
|
||||||
key_path.write_bytes(pem)
|
|
||||||
priv = load_private_key(key_path)
|
|
||||||
|
|
||||||
monkeypatch.setattr("browser_cli.client.targets.resolve_socket", _mock_no_browser)
|
|
||||||
|
|
||||||
client, server = _pair()
|
|
||||||
t = _spawn(server, path)
|
|
||||||
|
|
||||||
challenge = _recv_framed(client)
|
|
||||||
nonce = bytes.fromhex(challenge["nonce"])
|
|
||||||
# old client sends uppercase pubkey
|
|
||||||
msg = {"id": "x", "command": "tabs.list", "args": {}, "user_agent": "browser-cli/0.9.2", "pubkey": pub.upper()}
|
|
||||||
msg["sig"] = sign(priv, nonce, msg).hex()
|
|
||||||
_send_framed(client, json.dumps(msg).encode())
|
|
||||||
resp = _recv_framed(client)
|
|
||||||
|
|
||||||
assert "unauthorized" not in resp.get("error", "").lower()
|
|
||||||
assert "browser" in resp.get("error", "").lower() or "connected" in resp.get("error", "").lower()
|
|
||||||
client.close()
|
|
||||||
t.join(timeout=2)
|
|
||||||
|
|
||||||
def test_post_quantum_kex_auth_reaches_proxy(self, tmp_path, monkeypatch):
|
def test_post_quantum_kex_auth_reaches_proxy(self, tmp_path, monkeypatch):
|
||||||
"""ML-KEM shared secret is decapsulated and bound to the auth signature."""
|
"""ML-KEM shared secret is decapsulated and bound to the auth signature."""
|
||||||
monkeypatch.setattr("browser_cli.client.targets.resolve_socket", _mock_no_browser)
|
monkeypatch.setattr("browser_cli.client.targets.resolve_socket", _mock_no_browser)
|
||||||
|
|||||||
@@ -4,96 +4,124 @@ requires-python = ">=3.10"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cffi"
|
name = "cffi"
|
||||||
version = "2.0.0"
|
version = "2.1.0"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "pycparser", marker = "implementation_name != 'PyPy'" },
|
{ name = "pycparser", marker = "implementation_name != '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" }
|
sdist = { url = "https://files.pythonhosted.org/packages/57/5f/ff100cae70ebe9d8df1c01a00e510e45d9adb5c1fdda84791b199141de97/cffi-2.1.0.tar.gz", hash = "sha256:efc1cdd798b1aaf39b4610bba7aad28c9bea9b910f25c784ccf9ec1fa719d1f9", size = 531036, upload-time = "2026-07-06T21:34:30.382Z" }
|
||||||
wheels = [
|
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/c0/e9/6d7724983b3d5a0908dbf74f64038ade77c18646ff6636ec7894fd392ce1/cffi-2.1.0-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:b65f590ef2a44640f9a05dbb548a429b4ade77913ce683ac8b1480777658a6c0", size = 183837, upload-time = "2026-07-06T21:32:09.655Z" },
|
||||||
{ 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/69/aa/24580a278de21fd7322635556334d9b535f1cbc00b0a3919447cdf464c65/cffi-2.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:164bff1657b2a74f0b6d54e11c9b375bc97b931f2ca9c43fcf875838da1570dd", size = 184226, upload-time = "2026-07-06T21:32:11.196Z" },
|
||||||
{ 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/88/a9/02cae418ec4beb282ace11958d9d4737793439d561fadc7e6d56f2e2b354/cffi-2.1.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:c941bb58d5a6e1c3892d86e42927ed6c180302f07e6d395d08c416e594b98b46", size = 211107, upload-time = "2026-07-06T21:32:12.328Z" },
|
||||||
{ 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/3b/30/c806937ed5e4c2c7ac30d9d6b76b5dc57ff8b75d83800d9bb11a8253cf2a/cffi-2.1.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a016194dbe13d14ee9556e734b772d8d67b947092b268d757fd4290e3ba2dfc2", size = 218733, upload-time = "2026-07-06T21:32:13.67Z" },
|
||||||
{ 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/f9/cf/398272b8bbfd58aa314fda5a7f1cdbb26d1d78ae324a11211521315dd1f0/cffi-2.1.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:03e9810d18c646077e501f661b682fbf5dee4676048527ca3cffe66faa9960dd", size = 205543, upload-time = "2026-07-06T21:32:15.148Z" },
|
||||||
{ 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/45/ca/f91641185cdd90c36d317a9dc7f85e88ef8682d8b300977baff5e23c35d8/cffi-2.1.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:19c54ac121cad98450b4896fa9a43ee0180d57bc4bc911a33db6cab1efab6cd3", size = 205460, upload-time = "2026-07-06T21:32:16.479Z" },
|
||||||
{ 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/38/66/04781a77b411f0bb5b234d62c1814754ab75ebe455ccff1b08e8d7aae98f/cffi-2.1.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d433a51f1870e43a13b6732f92aaf540ff77c2015097c78556f75a2d6c030e0", size = 218760, upload-time = "2026-07-06T21:32:17.98Z" },
|
||||||
{ 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/d0/9a/bb1d5ed9c3fcae158e9f6391bf309c95d98c2ac37ed56573228471d0af5e/cffi-2.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3d7f118b5adbfdfead90c25822690b02bc8074fba949bb7858bec4ebd55adb43", size = 221230, upload-time = "2026-07-06T21:32:19.407Z" },
|
||||||
{ 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/41/aa/3c1409cdd26094efacd1c36c66e0a6eb9d4296e4fd4f9901b8b2042f4323/cffi-2.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:c5f5df567f6eb216de69be06ce55c8b714090fae02b18a3b40da8163b8c5fa9c", size = 213524, upload-time = "2026-07-06T21:32:20.828Z" },
|
||||||
{ 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/fa/75/74dfb7c3fc6ebbd408038476bd4c1d7e925c62614e7b9c534ecc34218288/cffi-2.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:11b3fb55f4f8ad92274ed26705f65d8f91457de71f5380061eb6d125a768fecd", size = 220341, upload-time = "2026-07-06T21:32:21.9Z" },
|
||||||
{ 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/70/b6/9003c33a3e7d2c1306f5962e646457dcfe5a8cd8fce6bbe02d7af25db783/cffi-2.1.0-cp310-cp310-win32.whl", hash = "sha256:9d72af0cf10a76a600a9690078fe31c63b9588c8e86bf9fd353f713c84b5db0f", size = 174578, upload-time = "2026-07-06T21:32:23.073Z" },
|
||||||
{ 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/8a/26/710688310447531c7a22f857c7f79d9855ec18b03e04494ced723fb37e2f/cffi-2.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:fb62edb5bb52cca65fab91a63afa7561607120d26090a7e8fda6fb9f064726da", size = 185071, upload-time = "2026-07-06T21:32:24.671Z" },
|
||||||
{ 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/d3/67/85c89a59ba36a671e79638f44d466749f08179266a57e4f2ffdf92174072/cffi-2.1.0-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:02cb7ff33ded4f1532476731f89ede53e2e488a8e6205515a82144246ffa7dcc", size = 183845, upload-time = "2026-07-06T21:32:26.32Z" },
|
||||||
{ 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/ea/dd/e3b0baa2d3d6a857ac72b7efbf18e32e487c9cdafcc13049ad765495b15e/cffi-2.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f5bce581e6b8c235e566a14768a943b172ada3ed73537bb0c0be1edee312d4e7", size = 184186, upload-time = "2026-07-06T21:32:28.025Z" },
|
||||||
{ 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/65/68/9f3ef890cf3c6ab97bd531c5677f67613d302165d16f8142b2811782a614/cffi-2.1.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:30b65779d598c370374fefabf138d456fd6f3216bfa7bedfab1ba82025b0cd93", size = 211892, upload-time = "2026-07-06T21:32:29.565Z" },
|
||||||
{ 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/22/d7/1a74539db16d8bfd839ff1515948948efbb162e574650fd3d846896eea95/cffi-2.1.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88023dfe18799507b73f1dbb0d14326a17465de1bc9c9c7655c22845e9ddc3a2", size = 218793, upload-time = "2026-07-06T21:32:30.951Z" },
|
||||||
{ 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/ec/d1/9a5b7169499e8e8d8e636de70b97ac7c9447104d2ff1a2cd94790cea5162/cffi-2.1.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:0a96b74cda968eebbad56d973efe5098974f0a9fb323865bf99ea1fd24e3e64c", size = 205737, upload-time = "2026-07-06T21:32:32.216Z" },
|
||||||
{ 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/ba/b0/e131a9c41f10607926278453d9596163594fe1c4ebc46efe3b5e5b34eb84/cffi-2.1.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a5781494d4d400a3f47f8f1da94b324f6e6b440a53387774002890a2a2f4b50f", size = 204909, upload-time = "2026-07-06T21:32:33.655Z" },
|
||||||
{ 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/fb/d2/4398416cd699b35167947c6e22aca52c47e69ad5695073c9f1f2c52e04aa/cffi-2.1.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aa7a1b53a2a4452ada2d1b5dade9960b2522f1e61293a811a077439e39029565", size = 217883, upload-time = "2026-07-06T21:32:35.173Z" },
|
||||||
{ 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/a2/a5/d4fe77b589e5e82d43ebc809bf2e6474afe8e48e32ea050b9357645b6471/cffi-2.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9d8272c0e483b024e1b9ad029821470ed8ec65631dbd90217469da0e7cd89f1c", size = 221251, upload-time = "2026-07-06T21:32:36.527Z" },
|
||||||
{ 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/22/f0/a2fc43084c0433caf7f461bccc013e28f848d04ee1c5ed7fce71423cf4d9/cffi-2.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7762faa47e8ff7eb80bd261d9a7d8eea2d8baa69de5e95b70c1f338bbe712f02", size = 214250, upload-time = "2026-07-06T21:32:37.852Z" },
|
||||||
{ 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/04/8c/b925975448cf20634a9fbd5efceb807219db452653648d2897c0989cab2d/cffi-2.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:89095c1968b4ba8285840e131bf2891b09ae137fe2146905acae0354fbce1b5e", size = 219441, upload-time = "2026-07-06T21:32:39.146Z" },
|
||||||
{ 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/eb/da/5c4918a2d61d86fa927d716cb3d8e4626ef8dc8f605a599d32f33897f59a/cffi-2.1.0-cp311-cp311-win32.whl", hash = "sha256:64c753a0f87a256020004f37a1c8c02c480e725f910f0b2a0f3f07debd1b2479", size = 174496, upload-time = "2026-07-06T21:32:40.467Z" },
|
||||||
{ 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/f9/c8/6c2de1d55cf35ef8b92885d5ef280790f0fb9634d87ea1cc315176aecd61/cffi-2.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:4f26194e3d95e06501b942642855aed4f953d55e95d7d01b7c4483db3ecff458", size = 185113, upload-time = "2026-07-06T21:32:41.761Z" },
|
||||||
{ 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/9e/4e/e8d7cb5783f1841a3c8fb3a7735838d7484d08ec08c9f984b14cac1ac0e9/cffi-2.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:35aaea0c7ee0e58a5cd8c2fd1a48fdf7ece0d2699b7ecdda08194e9ce5dd9b3d", size = 179927, upload-time = "2026-07-06T21:32:42.961Z" },
|
||||||
{ 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/1e/85/990925db5df586ec90beb97529c853497e7f85ba0234830447faf41c3057/cffi-2.1.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:df2b82571a1b30f58a87bf4e5a9e78d2b1eff6c6ce8fd3aa3757221f93f0863f", size = 184829, upload-time = "2026-07-06T21:32:44.324Z" },
|
||||||
{ 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/4b/92/e7bb136ad6b5352603732cf907ef862ca103f20f2031c1735a46300c20c9/cffi-2.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:78474632761faa0fb96f30b1c928c84ebcf68713cbb80d15bab09dfe61640fde", size = 184728, upload-time = "2026-07-06T21:32:45.683Z" },
|
||||||
{ 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/c3/c0/d1ec30ffb370f748f2fb54425972bfef9871e0132e82fb589c46b6676049/cffi-2.1.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:5972433ad71a9e46516584ef60a0fda12d9dc459938d1539c3ddecf9bdc1368d", size = 214815, upload-time = "2026-07-06T21:32:48.557Z" },
|
||||||
{ 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/1b/dc/5620cf930688be01f2d673804291de757a934c90b946dbdc3d84130c2ea4/cffi-2.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b6422532152adf4e59b110cb2808cee7a033800952f5c036b4af047ee43199e7", size = 222429, upload-time = "2026-07-06T21:32:49.848Z" },
|
||||||
{ 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/4b/a4/77b53abbf7a1e0beb9637edbef2a94d15f9c822f591e85d439ffd91519a6/cffi-2.1.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:46b1c8db8f6122420f32d02fffb924c2fe9bc772d228c7c711748fff56aabb2b", size = 210315, upload-time = "2026-07-06T21:32:51.221Z" },
|
||||||
{ 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/58/0c/f528df19cc94b675087324d4760d9e6d5bfae97d6217aa4fac43de4f5fcc/cffi-2.1.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9fafc5aa2e2a39aaf7f8cc0c1f044a9b07fca12e558dca53a3cc5c654ad67a7", size = 208859, upload-time = "2026-07-06T21:32:52.512Z" },
|
||||||
{ 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/62/f2/c9522a81c32132799a1972c39f5c5f8b4c8b9f00488a23feaa6c06f07741/cffi-2.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1e9f50d192a3e525b15a75ab5114e442d83d657b7ec29182a991bc9a88fd3a66", size = 221844, upload-time = "2026-07-06T21:32:53.704Z" },
|
||||||
{ 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/6e/28/bd53988b9833e8f8ad539d26f4c07a6b3f6bcb1e9e02e7ca038250b3428d/cffi-2.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:98fff996e983a36d3aa2eca83af40c5821202e7e6f32d13ae94e3d2286f10cfe", size = 225287, upload-time = "2026-07-06T21:32:54.907Z" },
|
||||||
{ 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/79/99/0d0fd37f055224085f42bbb2c022d002e17dde4a97972822327b07d84101/cffi-2.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:379de10ce1ba048b1448599d1b37b24caee16309d1ac98d3982fc997f768700b", size = 223681, upload-time = "2026-07-06T21:32:56.329Z" },
|
||||||
{ 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/b0/80/c138990aa2a70b1a269f6e06348729836d733d6f970867943f61d367f8cc/cffi-2.1.0-cp312-cp312-win32.whl", hash = "sha256:9b8f0f26ca4e7513c534d351eca551947d053fac438f2a04ac96d882909b0d3a", size = 175269, upload-time = "2026-07-06T21:32:57.777Z" },
|
||||||
{ 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/a8/eb/f636456ff21a83fc13c032b58cc5dde061691546ac79efa284b2989b7982/cffi-2.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:c97f080ea627e2863524c5af3836e2270b5f5dfff1f104392b959f8df0c5d384", size = 185881, upload-time = "2026-07-06T21:32:59.253Z" },
|
||||||
{ 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/dd/2c/400ea43e721727dca8a65c4521390e9196757caba4a45643acb2b63271b8/cffi-2.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:6d194185eabd279f1c05ebe3504265ddfc5ad2b58d0714f7db9f01da592e9eb6", size = 180088, upload-time = "2026-07-06T21:33:02.278Z" },
|
||||||
{ 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/96/88/a996879e2eeccb815f6e3a5967b12a308257412acec882039d386bd2aa7b/cffi-2.1.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:10537b1df4967ca26d21e5072d7d54188354483b91dc75058968d3f0cf13fbda", size = 194331, upload-time = "2026-07-06T21:33:03.697Z" },
|
||||||
{ 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/58/85/7ae00d5c8dd6266f4e944c3db630f3c5c9a98b61d469c714d848b1d8138a/cffi-2.1.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:a95b05f9baf29b91171b3a8bd2020b028835243e7b0ff6bb23e2a3c228518b1b", size = 196966, upload-time = "2026-07-06T21:33:05.353Z" },
|
||||||
{ 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/8c/e9/45c3a76ad8d43ad9261f4c95436da61128d3ca545d72b9612c0ab5be0b1c/cffi-2.1.0-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:15faec4adfff450819f3aee0e2e02c812de6edb88203aa58807955db2003472a", size = 184795, upload-time = "2026-07-06T21:33:06.699Z" },
|
||||||
{ 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/84/4c/82f132cb4418ee6d953d982b19191e87e2a6372c8a4ce36e50b69d6ade4a/cffi-2.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:716ff8ec22f20b4d988b12884086bcef0fc99737043e503f7a3935a6be99b1ea", size = 184746, upload-time = "2026-07-06T21:33:08.071Z" },
|
||||||
{ 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/a0/1c/4ed5a0e5bdca6cbc275556de3328dd1b76fd0c11cc13c88fe66d1d8715f2/cffi-2.1.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:63960549e4f8dc41e31accb97b975abaecfc44c03e396c093a6436763c2ea7db", size = 214747, upload-time = "2026-07-06T21:33:09.671Z" },
|
||||||
{ 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/3a/a6/e879bb68cc23a2bc9ba8f4b7d8019f0c2694bad2ab6c4a3701d429439f58/cffi-2.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ff067a8d8d880e7809e4ac88eb009bb848870115317b306666502ccad30b147f", size = 222392, upload-time = "2026-07-06T21:33:10.896Z" },
|
||||||
{ 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/88/f6/01890cfd63c08f8eb96a8319b0443690197d240a8bd6346048cf7bde9190/cffi-2.1.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:3b926723c13eba9f81d2ef3820d63aeceec3b2d4639906047bf675cb8a7a500d", size = 210285, upload-time = "2026-07-06T21:33:12.251Z" },
|
||||||
{ 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/a6/cf/2b684132056f438567b61e19d690dd31cd0921ace051e0a458be6074369e/cffi-2.1.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:47ff3a8bfd8cb9da1af7524b965127095055654c177fcfc7578debcb015eecd0", size = 208801, upload-time = "2026-07-06T21:33:13.617Z" },
|
||||||
{ 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/6f/08/f2e7d62c460faae0926f2d6e423694aa409ced3bc1fe2927a0a6e5f05416/cffi-2.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:799416bae98336e400981ff6e532d67d5c709cfb30afb79865a1315f94b0e224", size = 221808, upload-time = "2026-07-06T21:33:15.466Z" },
|
||||||
{ 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/38/37/04f54b8e63a02f3d908332c9effbf8c366167c6f733ed8a3d4f79b7e2a1e/cffi-2.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:961be50688f7fba2fa65f63712d3b9b341a22311f5253460ce933f52f0de1c8c", size = 225241, upload-time = "2026-07-06T21:33:16.869Z" },
|
||||||
{ 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/a9/d6/c72eecca433cd3e681c65ed313ab4835d9d4a379704d0f628a6a05f51c2e/cffi-2.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bf5c6cf48238b0eb4c086978c492ad1cbc22373fc5b2d7353b3a598ce6db887a", size = 223588, upload-time = "2026-07-06T21:33:18.239Z" },
|
||||||
{ 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/c6/4b/e706f67279140f92939da3475ad610df18bfd52d50f14953a8e5fede71d5/cffi-2.1.0-cp313-cp313-win32.whl", hash = "sha256:db3eb7d46527159a878ec3460e9d40615bc25ba337d477db681aea6e4f05c5d2", size = 175248, upload-time = "2026-07-06T21:33:19.799Z" },
|
||||||
{ 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/5a/47/59eb7975cb0e4ef0afa764ea945b29a5bb4537a9f771cb7d6c8a5dd74c95/cffi-2.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:8e74a6135550c4748af665b1b1118b6aab33b1fc6a16f9aff630af107c3b4512", size = 185717, upload-time = "2026-07-06T21:33:21.47Z" },
|
||||||
{ 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/5a/af/34fee85c48f8d94efc8597bc09470c9dd274c145f1c12e0fbc6ab6d38d74/cffi-2.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:2282cd5e38aa8accd03e99d1256af8411c84cdbee6a89d841b563fdbd1f3e50f", size = 180114, upload-time = "2026-07-06T21:33:22.515Z" },
|
||||||
{ 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/d8/f0/81478e482afa03f6d18dc8f2afb5edc45b3080853b634b5ed91961be0998/cffi-2.1.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:d2117334c3af3bdcb9a88522b844a2bdb5efdc4f71c6c822df55486ae1c3347a", size = 194142, upload-time = "2026-07-06T21:33:23.657Z" },
|
||||||
{ 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/7d/95/8de304305cd9204974b0ca051b86d307cafca13aa575a0ef1b44d92c0d8c/cffi-2.1.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:702c436735fbe99d59ada02a1f65cfc0d31c0ee8b7290912f8fbc5cd1e4b16c3", size = 196819, upload-time = "2026-07-06T21:33:25.007Z" },
|
||||||
{ 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/20/71/7c8372d30e42415602ed9f268f7cfd66f1b855fed881ecd168bcb45dbc0b/cffi-2.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:1ff3456eab0d889592d1936d6125bbfbc7ae4d3354a700f8bd80450a66445d4d", size = 184965, upload-time = "2026-07-06T21:33:26.605Z" },
|
||||||
{ 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/d6/5c/584e626835f0375c928176c04137c96927165cb8733cdb3150ec04e5ee5e/cffi-2.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c4165821e131d6d4ca444347c2b694e2311bcfa3fe5a861cc72968f28867beac", size = 184952, upload-time = "2026-07-06T21:33:27.823Z" },
|
||||||
{ 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/2e/d2/065fcae1c73979fac8e054462478d0ff8a29c40cdc2ed7ea5676a061df53/cffi-2.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:276f20fffd7b396e12516ba8edf9509210ac248cbbc5acbc39cd512f9f59ebe6", size = 222353, upload-time = "2026-07-06T21:33:29.178Z" },
|
||||||
{ 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/ed/a5/e8bbb1ce5b3ac2f53ad6a10bde44318a5a8d99d4f4a000d44a6e39aeb3e4/cffi-2.1.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:7d5980a3433d4b71a5e120f9dd551403d7824e31e2e67124fe2769c404c06913", size = 210051, upload-time = "2026-07-06T21:33:30.534Z" },
|
||||||
{ 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/28/ed/c127d3ac36e899c965e3361357c3befacd6578c03f40125183e41c3b219e/cffi-2.1.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:6ca4919c6e4f89aa99c42510b42cf54596892c00b3f9077f6bdd1505e24b9c8d", size = 208630, upload-time = "2026-07-06T21:33:31.753Z" },
|
||||||
{ 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/cc/d7/97d3136f81db489ec8d1d67748c110d6c994268fd7528014aa9f2b085e4e/cffi-2.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d53d10f7da99ae46f7373b9150393e9c5eab9b224909982b43832668de4779f5", size = 221593, upload-time = "2026-07-06T21:33:33.044Z" },
|
||||||
{ 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/d3/27/93195977168ee63aed233a1a0993a2178798654d1f4bddcdd321d6fd3b21/cffi-2.1.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c351efb95e832a853a29361675f33a7ce53de1a109cd73fd47af0712213aa4ce", size = 225146, upload-time = "2026-07-06T21:33:34.224Z" },
|
||||||
{ 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/b3/c1/6dbd291ee2ae5a50a034aa057207081f545923bbf15dad4511e985aafff5/cffi-2.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:dbf7c7a88e2bac086f06d14577332760bdeecc42bdec8ac4077f6260557d9326", size = 223240, upload-time = "2026-07-06T21:33:35.57Z" },
|
||||||
{ 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/0f/6f/ade5ce9863a57992a6ea3d0d10d7e29b8749fc127204b3d493d667b2815f/cffi-2.1.0-cp314-cp314-win32.whl", hash = "sha256:1854b724d00f6654c742097d5387569021be12d3a0f770eae1df8f8acfcc6acd", size = 177723, upload-time = "2026-07-06T21:33:51.626Z" },
|
||||||
{ 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/41/de/92b9eeed4ae4a21d6fd9b2a2c8505cbed573299902ea73981cc13f7ff62c/cffi-2.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:1b96bfe2c4bd825681b7d311ad6d9b7280a091f43e8f63da5729638083cd3bfb", size = 187937, upload-time = "2026-07-06T21:33:53.403Z" },
|
||||||
{ 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/2e/1a/cc6ae6c2913a03aab8898eee57963cf1035b8df5872ed8b9115fcc7e2be8/cffi-2.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:7d28dff1db6764108bc30788d85d61c876beff416d9a49cb9dd7c5a9f34f5804", size = 183001, upload-time = "2026-07-06T21:33:54.74Z" },
|
||||||
{ 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/14/f0/134c00ce0779ec86dea2aa1aac69339c2741a8045072676763512363a2ea/cffi-2.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7ea6b3e2c4250ff1de21c630fe72d0f63eb95c2c32ffbf64a358cf4a8836d714", size = 188538, upload-time = "2026-07-06T21:33:36.792Z" },
|
||||||
{ 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/50/d8/3b86aba791cb610d24e8a3e1b2cd529e71fa15096b04e4d4e360049d4a4c/cffi-2.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6af371f3767faeffc6ac1ef57cdfd25844403e9d3f476c5537caee499de96376", size = 188230, upload-time = "2026-07-06T21:33:38.011Z" },
|
||||||
{ 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/14/d0/117dcd9209255ad8571fbc8c92ef32593a1d294dcec91ddc4e4db50606f2/cffi-2.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eb4e8997a49aa2c08a3e43c9045d224448b8941d88e7ac163c7d383e560cbf98", size = 223899, upload-time = "2026-07-06T21:33:39.514Z" },
|
||||||
{ 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/b6/3d/f20f8b886b254e3ad10e15cd4186d3aed49f3e6a35ab37aab9f8f25f7c03/cffi-2.1.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:bf01d8c84cbea96b944c73b22182e6c7c432b3475632b8111dbfdc95ddad6e13", size = 211652, upload-time = "2026-07-06T21:33:40.851Z" },
|
||||||
{ 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/28/3b/fad54de07260b93ddeef4b96d0131d57ea900675df1d410ae1deee52d7a6/cffi-2.1.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:33eb1ad83ebe8f313e0df035c406227d55a79456704a863fad9842136af5ad7d", size = 210755, upload-time = "2026-07-06T21:33:42.183Z" },
|
||||||
{ 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/cc/82/3d5c705acb7abbba9bbd7d79b8e62e0f25b6120eb7ae6ac49f1b721722fe/cffi-2.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ac0f1a2d0cfa7eea3f2aaf006ab6e70e8feeb16b75d65b7e5939982ca2f11056", size = 223933, upload-time = "2026-07-06T21:33:43.603Z" },
|
||||||
{ 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" },
|
{ url = "https://files.pythonhosted.org/packages/6c/d0/47e338384ab6b1004241002fa616301020cea4fc95f283506565d252f276/cffi-2.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c16914df9fb7f500e440e6875fa23ff5e0b31db01fa9c06af98d59a91f0dc2e4", size = 226749, upload-time = "2026-07-06T21:33:45.046Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/70/25/65bd5b58ea4bfdfc15cde02cb5365f89ef8ab8b2adfb8fe5c4bd4233382f/cffi-2.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5ecbd0499275d57506d397eebe1981cee87b47fcd9ef5c22cab7ed7644a39a94", size = 225703, upload-time = "2026-07-06T21:33:46.374Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/dc/78/aa01ac599a8a4322533d45a1f9bc93b338276d2d59dabbe7c6d92a775c81/cffi-2.1.0-cp314-cp314t-win32.whl", hash = "sha256:7d034dcffa09e9a46c93fa3a3be402096cb5354ac6e41ab8e5cc9cd8b642ad76", size = 182857, upload-time = "2026-07-06T21:33:47.696Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b9/26/d00496b22de4d4228f32dde94ad996f350c8aad676d63bcca0743c8dea4d/cffi-2.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0582a58f3051372229ca8e7f5f589f9e5632678208d8636fea3676711fdf7fe5", size = 194065, upload-time = "2026-07-06T21:33:48.953Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d5/dd/0c7dbf815a579ff005008a2d815a55d6bb047c349eef536d9dc53d3f0a8d/cffi-2.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:510aeeeac94811b138077451da1fb18b308a5feab47dd2b603af55804155e1c8", size = 186404, upload-time = "2026-07-06T21:33:50.309Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/55/c7/8c8c50cb11c6750051daf12164098a9a6f027ac4356967fd4d800a07f242/cffi-2.1.0-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:2e9dabb9abcb7ad15938c7196ad5c1718a4e6d33cc79b4c0209bdb64c4a54a5c", size = 194121, upload-time = "2026-07-06T21:33:56.109Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/99/e2/67680bf19a6b60d2bb7ff83baefa2a4c3d2d7dc0f3277034b802e1fc504c/cffi-2.1.0-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:37f525a7e7e50c017fdebe58b787be310ad59357ae43a053943a6e1a6c526001", size = 196820, upload-time = "2026-07-06T21:33:57.288Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ed/da/4bbe583a3b3a5c8c60892124fe17f3fa3656523faf0d3484eae90f091853/cffi-2.1.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:95f2954c2c9473d892eca6e0409f3568b37ab62a8eedb122461f73cc273476e3", size = 184936, upload-time = "2026-07-06T21:33:58.765Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e5/4b/1f4c36ab273980d7aa75bb126ea4f8971f24a96108acad3a0a084028c57b/cffi-2.1.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:cdf2448aab5f661c9315308ec8b93f4e8a1a67a3c733f8631067a2b67d5913dc", size = 185045, upload-time = "2026-07-06T21:34:00.085Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ef/c3/ad299dc38f3583f8d916b299f028af418a9ec98bc695fcbebeae7420691c/cffi-2.1.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:90bec57cf82089383bd06a605b3eb8daebf7e5a668520beaf6e327a83a947699", size = 222342, upload-time = "2026-07-06T21:34:01.814Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/eb/d8/df4543cc087245044ed02ef3ad8e0a26619d0075ac7a77a12dc81177851b/cffi-2.1.0-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6274dcb2d15cef48daa73ed1be5a40d501d74dccd0cd6db364776d12cb6ba022", size = 210073, upload-time = "2026-07-06T21:34:03.255Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2c/0e/fac738d73728c6cea2a88a2883dca54892496cbba88a1dc1f2909cb8a6f5/cffi-2.1.0-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:2b71d409cccee78310ab5dec549aed052aaea483346e282c7b02362596e01bb0", size = 208551, upload-time = "2026-07-06T21:34:04.433Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e6/3f/0b04a700dd64f465c93020253a793a82c9b4dff9961f48facd0df945d9b8/cffi-2.1.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7d3538f9c0e50670f4deb93dbb696576e60590369cae2faf7de681e597a8a1f1", size = 221649, upload-time = "2026-07-06T21:34:06.157Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5d/7c/b7379a5704c79eda57ce075869ba70a0368d1c850f803b3c0d078d39dcaf/cffi-2.1.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:8f9ec95b8a043d3dfbc74d9abc6f7baf524dd27a8dc160b0a32ff9cdab650c28", size = 225203, upload-time = "2026-07-06T21:34:07.489Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5a/02/d5e6c43ea85c41bda2a184a3418f195fe7cf602967a8d2b94e085b83deef/cffi-2.1.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:af5e2915d41fe6c961694d7bfdc8562942638200f3ce2765dfb8b745cf997629", size = 223263, upload-time = "2026-07-06T21:34:08.712Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2c/d8/772b8259bf75749adffb1c546828978381fb516f60cf701f6c83daf60c85/cffi-2.1.0-cp315-cp315-win32.whl", hash = "sha256:0a42c688d19fca6e095a53c6a6e2295a5b050a8b289f109adab02a9e61a25de6", size = 177696, upload-time = "2026-07-06T21:34:26.355Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2f/dd/afa2191fc6d57fedd26e5844a2fe2fcc0bbfa00961bbaa5a41e4921e7cca/cffi-2.1.0-cp315-cp315-win_amd64.whl", hash = "sha256:bccbbb5ee76a61f9d99b5bf3846a51d7fca4b6a732fe46f89295610edaf41853", size = 187914, upload-time = "2026-07-06T21:34:27.58Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/05/ef/6cd4f8c671517162379dc79cfae5aea9106bc38abb89628d5c16adf6a838/cffi-2.1.0-cp315-cp315-win_arm64.whl", hash = "sha256:8d35c139744adb3e727cd51b1a18324bbe44b8bd41bf8322bca4d41289f48eda", size = 183004, upload-time = "2026-07-06T21:34:28.905Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/11/b6/12fc55092817a5faa26fb8c40c7f9d662e11a46ee248c137aafc42517d92/cffi-2.1.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:f9912624a0c0b834b7520d7769b3644453aabc0a7e1c839da7359f050750e9bc", size = 188378, upload-time = "2026-07-06T21:34:09.926Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8d/2e/cdac88979f295fde5daa69622c7d2111e56e7ceb94f211357fbe452339e4/cffi-2.1.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:df92f2aba50eb4d96718b68ef76f2e57a57b54f2fa62333496d16c6d585a85ca", size = 188319, upload-time = "2026-07-06T21:34:11.101Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e0/27/1d0b408497e41a74795af122d7b603c418c5fed0171450f899afd04e594f/cffi-2.1.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0520e1f4c35f44e209cbbb421b67eec42e6a157f59444dfb6058874ff3610e5d", size = 223904, upload-time = "2026-07-06T21:34:12.606Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8b/31/e115c985105dd7ffb32444505f18ceb874bb42d992af05d5dced7ecf1980/cffi-2.1.0-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:3681e031db29958a7502f5c0c9d6bbc4c36cb20f7b104086fa642d1799631ff8", size = 211554, upload-time = "2026-07-06T21:34:13.987Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5a/67/9e6e09409336d9e515c58367e7cfcf4f89df06ad25252675595a58eb59d5/cffi-2.1.0-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:762f99479dcb369f60ab9017ad4ab97a36a1dd7c1ee5a3b15db0f4b8659120cd", size = 210795, upload-time = "2026-07-06T21:34:15.972Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/19/e5/d3cc82a4a0be7902af279c04181ad038449c096734464a5ae1de3e1401bd/cffi-2.1.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0611e7ebf90573a535ebdc33ae9da222d037853983e13359f580fab781ca017f", size = 223843, upload-time = "2026-07-06T21:34:17.509Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b9/65/b434abc97ce7cecc2c640fde160507c0ecc7e21544b483ba3325d2e2ea17/cffi-2.1.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:86cf8755a791f72c85dc287128cc62d4f24d392e3f1e15837245623f4a33cccc", size = 226773, upload-time = "2026-07-06T21:34:19.05Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b5/9f/d4dc66ca651eb1145a133314cda721abf13cfac3d28c4a0402263ae6ad75/cffi-2.1.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:ba00f661f8ba35d075c937174e27c2c421cec3942fd2e0ea3e66996757c0fdd9", size = 225719, upload-time = "2026-07-06T21:34:20.576Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/68/5a/e536c528bc8057496c360c0978559a2dc45653f89dd6151078aa7d8fca1a/cffi-2.1.0-cp315-cp315t-win32.whl", hash = "sha256:cb96698e3c7413d906ce83f8ffd245ec1bd94707541f299d0ce4d6b0193e982b", size = 182760, upload-time = "2026-07-06T21:34:22.059Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d3/0b/0ffe8b82d3875bced5fa1e7986a7a46b748262a40ab7f60b475eb9fb1bb3/cffi-2.1.0-cp315-cp315t-win_amd64.whl", hash = "sha256:f146d154428a2523f9cc7936c02353c2459b8f6cf07d3cd1ee1c0a611109c5d5", size = 193769, upload-time = "2026-07-06T21:34:23.589Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a0/17/1073b53b68c9b5ca6914adf5f8bf55aacc2d3be102418c90700160ea8605/cffi-2.1.0-cp315-cp315t-win_arm64.whl", hash = "sha256:cbb7640ce37159548d2147b5b8c241f962143d4c71231431820783f4dc78f210", size = 186405, upload-time = "2026-07-06T21:34:24.857Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "click"
|
name = "click"
|
||||||
version = "8.4.1"
|
version = "8.4.2"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/9b/98/518d8e5081007684232226f475082b30087d0f585e8457db087298259f49/click-8.4.1.tar.gz", hash = "sha256:918b5633eddf6b41c32d4f454bf0de810065c74e3f7dbf8ee5452f8be88d3e96", size = 353007, upload-time = "2026-05-22T04:08:37.769Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/76/d4/81420972a676e8ffea40450d8c8c92943e7218a78fe9b64359836cc9876b/click-8.4.2.tar.gz", hash = "sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6", size = 338000, upload-time = "2026-06-24T17:45:15.148Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/c7/0d/67e5b4109ea4a837e80daa87c2c696711955e40449a97e8926672534def2/click-8.4.1-py3-none-any.whl", hash = "sha256:482be17c6991b8c19c5429a1e995d9b0efdbb63172824c41f99965dc0ade8ec2", size = 116639, upload-time = "2026-05-22T04:08:35.26Z" },
|
{ url = "https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl", hash = "sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76", size = 119243, upload-time = "2026-06-24T17:45:13.73Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -107,115 +135,100 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "coverage"
|
name = "coverage"
|
||||||
version = "7.14.1"
|
version = "7.15.0"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/54/fd/0ab2772530e946e1be1abd0bc09e647ec9b02e88f0867857601fefca8953/coverage-7.14.1.tar.gz", hash = "sha256:30c08f7d90415aa98b3c990385dea2939b0da55f38515e5b369b83655f8523be", size = 920132, upload-time = "2026-05-26T20:41:36.783Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/cc/8b/adeb62ea8951f13c4c7fef2e7a85e1a06b499c8d8237ea589d496029e53f/coverage-7.15.0.tar.gz", hash = "sha256:9ac3fe7a1435986463eaa8ee253ae2f2a268709ba4ae5c7dd1f52a05391ad78f", size = 925362, upload-time = "2026-07-02T13:10:50.535Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/92/69/0d2ef01ff4b8fcecd4cba920d11e92fa4f96ae412441d3b56a90a258e69b/coverage-7.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3e3680291c4a1d0dadfa84a2c459576a4af5133abb617905714339a0c73138cf", size = 219722, upload-time = "2026-05-26T20:38:14.002Z" },
|
{ url = "https://files.pythonhosted.org/packages/2a/97/c52dc440c390b6cfa87be9432b141a956e2d56d9b9f5fc8bd71c5f471722/coverage-7.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:50913d4bf5ddafa6ca3693da5e4dd833dd1b772e0283c99ca7f7d287db67331a", size = 220539, upload-time = "2026-07-02T13:08:19.252Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/f8/ae/9afdeaa31b9d9ce98124b6abf8bb49119bf71aecae04f8567c189d91299f/coverage-7.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a5274669f37f2343635a347b91a60777621341ab3378e9c6ac9335eee704bddf", size = 220240, upload-time = "2026-05-26T20:38:17.424Z" },
|
{ url = "https://files.pythonhosted.org/packages/3f/26/602de8c2aec7e2e3e99ebfb8e04ba65598f746275396eea5f6794ff4673f/coverage-7.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:359e141ccd33893ce3f1ad5525f8b96083003677c82182e5907d62d4ea5799fc", size = 221058, upload-time = "2026-07-02T13:08:21.013Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/51/69/c998589871df7ea7dba865cc5ee32b5a3e1d47ba6c68ef91104c7c46fa5e/coverage-7.14.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cfe5a5fec635799ef33428f1e5e61bafa45a92a96190ba731561ba558ccc214d", size = 246981, upload-time = "2026-05-26T20:38:19.266Z" },
|
{ url = "https://files.pythonhosted.org/packages/fc/13/ebab0743138891c1d646d61e247ec29639afcbb6c4e1905e6a0f0c75291a/coverage-7.15.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3200b6204935f928c64b2ca1f923ab8c1acb7c9de45ec61569711b34d25cccaf", size = 247797, upload-time = "2026-07-02T13:08:22.474Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/fc/10/1c7d04c13040dac531d21b712bbe08f902e6dd9b58f5d77875c4d030f8f2/coverage-7.14.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:62a9f70b52e0b5a95cfef4a5c5641b06983cadc5e538a3feeb5c00211f523ac2", size = 248812, upload-time = "2026-05-26T20:38:20.75Z" },
|
{ url = "https://files.pythonhosted.org/packages/d3/b7/b6ffb9e042aa48dc4144a8a65529affaec8dca0685309353614a2a7386ad/coverage-7.15.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:be616bf61346883b2cfdc5178669647e03531d81ab761a7e378558b7e8bcb628", size = 249626, upload-time = "2026-07-02T13:08:23.803Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/c1/65/2a38a4607ef27cadcfbcee034dba5830ae2569f90144a0f4c7dbf47d30b0/coverage-7.14.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3c18ebc343e15be53049b3a2dce38fe82d58f37e20ab9094b3a39c0aa4f6bb47", size = 250675, upload-time = "2026-05-26T20:38:22.159Z" },
|
{ url = "https://files.pythonhosted.org/packages/9c/06/243ff05b652333d8e3d060c11223efc2723b19cacf6605e433fa686ab5d4/coverage-7.15.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc7bafc3fe1059463a8fdd97ca79972d6e2bf819d775c7d54991b5b1971201d6", size = 251493, upload-time = "2026-07-02T13:08:25.397Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/c9/a2/a446ed9752a4a59b79e0fb6cbb319f6facb2183045c0725462625e66f87e/coverage-7.14.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b84ffdf877644e7096aa936991efeed873f7f3df57b9cd001312b7668ab08550", size = 252590, upload-time = "2026-05-26T20:38:23.63Z" },
|
{ url = "https://files.pythonhosted.org/packages/d3/2b/867faa17030a806114dae388b32a3fa929d8cd4bf39226fbc11f6e6bb705/coverage-7.15.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b713aa7fcf325a01d4184d848acb46fd84f78fdb0978470c636b23a06a753d91", size = 253406, upload-time = "2026-07-02T13:08:26.842Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/9e/fd/e81fbd7ba752365546e9842b1cbdaad3d6919d2a522c590aef16a281ec5e/coverage-7.14.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e854312c4103f2ad4c0dc023b69b77ebfd2c89db5f86c4c94dc2353f9a92167e", size = 247691, upload-time = "2026-05-26T20:38:25.057Z" },
|
{ url = "https://files.pythonhosted.org/packages/94/c0/d789ce18f6605afc4895db75723424be2ef494282f77f61d8e5832923183/coverage-7.15.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e38e6fba2d56652fdfaf0231f8f78aeb805234a912de25dc291ee5cce5b8faa4", size = 248512, upload-time = "2026-07-02T13:08:28.398Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/53/35/f3c26fdaae9ea937d154ca4d372e5ea0a4167ff70d36c6074ac2eacb2f83/coverage-7.14.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c643734307300234fafa36bf2a040a7235f8f177ea1fd6ec1423aea6fb7b929f", size = 248716, upload-time = "2026-05-26T20:38:26.406Z" },
|
{ url = "https://files.pythonhosted.org/packages/c9/b6/b2673c30739f4a2e06649a0a38ad8b093c4d865462dc7bab0e9524a2c3b1/coverage-7.15.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:884499f42e382675be80770391983b90e0c0c774d87dbeeebf5f991cf6612b20", size = 249532, upload-time = "2026-07-02T13:08:29.731Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/2e/14/940b6c49551fd343e8507ee2b0ba7af5d0aa04ed5bf768285cb7c72a9884/coverage-7.14.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:84ac9499e48700399a5dd0ea7085b5091961fec52c68d66b4ec0d3cf7f4441b1", size = 246721, upload-time = "2026-05-26T20:38:28.282Z" },
|
{ url = "https://files.pythonhosted.org/packages/3c/2e/acd79e9a41beabee92b623afe4f30b549916f48566271475f2907e752828/coverage-7.15.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:840481b12e083dbcbafab14794a8781a958edf327c8d3d70b4eee42f9b8253aa", size = 247537, upload-time = "2026-07-02T13:08:31.173Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/aa/2c/40fc0634186c28292a662dff578866b3913983d6c375a3c2a74020938719/coverage-7.14.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:7f02d09f70776579b926d889a4c9c235070a1f47c40458aeaca563fae5acfdb5", size = 250533, upload-time = "2026-05-26T20:38:29.753Z" },
|
{ url = "https://files.pythonhosted.org/packages/12/d4/2d301c4d1b3238d7c88b70ab9d13fd53ed9505662a7ff1b46ba1e2e4e3c3/coverage-7.15.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:276646e9481703d09f854f3b2f018f24e19fd7049ae670a92570043eb97203b1", size = 251348, upload-time = "2026-07-02T13:08:32.63Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/de/e3/2c26bf1e811f9df991ff2a9bdddebdd13ee0665d564df7d05979f9146297/coverage-7.14.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:ce66d8e46da2bb5ee313a745cbd2e391d319176c1f7a9451bfcd3a2fb920859b", size = 246990, upload-time = "2026-05-26T20:38:31.516Z" },
|
{ url = "https://files.pythonhosted.org/packages/35/bb/c67708b2bc00f32e12805ec23d5fa677a0a51652f449341a89f9d6b1b715/coverage-7.15.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:4de4b4d3f5545aa6c60dc4efd9c63b5b5dcc3bf00fe83146b2bdfffb8f6613bd", size = 247806, upload-time = "2026-07-02T13:08:33.931Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/a8/b0/060260ef56bd92363ebdce0c7095ce422b06e69aae71828efeca473ab1ca/coverage-7.14.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c912c259304cfb5ee584481cfb7ce1ff932b4d61e6c9140b8f19cb7b5ed82332", size = 247593, upload-time = "2026-05-26T20:38:33.065Z" },
|
{ url = "https://files.pythonhosted.org/packages/eb/6c/57c4f653c47a6e917748f8938e389e72fbcae44e3643cd906664f0477a13/coverage-7.15.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5c504097b2a89b1e85bc6070d920df77daec701337e3aeef2c17775a5dd0ca90", size = 248410, upload-time = "2026-07-02T13:08:35.189Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/63/f3/501502046efeb0d6d94b5ca54941d95f1184183dd6bdb7f283985783bb4a/coverage-7.14.1-cp310-cp310-win32.whl", hash = "sha256:1238cb94638e610e972c60dac68e813f868dc7d6e982535270558443058d9d59", size = 222330, upload-time = "2026-05-26T20:38:35.36Z" },
|
{ url = "https://files.pythonhosted.org/packages/6c/94/bb083041aef828903668f134273f319f2bd49224962875359c52faa5497f/coverage-7.15.0-cp310-cp310-win32.whl", hash = "sha256:f6e80ed91f98316e86b9c137206b04b2bcfbffccbdff49bd2eb09dddb1cf14e0", size = 222588, upload-time = "2026-07-02T13:08:36.486Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/a0/5d/1bf99f2c558f128faf7906817ccbdb576ba815d3b41ce2ac1719b70a3663/coverage-7.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:fc459e5d73be2d6332fcfe8dbf3d8994671fe33c700f4565988ecfa511547253", size = 223261, upload-time = "2026-05-26T20:38:37.196Z" },
|
{ url = "https://files.pythonhosted.org/packages/ef/94/a09d8ee618956f626741b0734854bac4425a00e10c0565f5abca64e7e751/coverage-7.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:b3b3e22030f3f6f5e01a5ce69936552a5c0f6992b7698777377b99041961031f", size = 223214, upload-time = "2026-07-02T13:08:37.885Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/7d/d7/477ad149490e6cb849f28abea1dabb9c823cea72e7500c81b4240ce619c0/coverage-7.14.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:478b5bcd63c2e1357c5c7e16c070690df7b07f676b1c114d7b93e533c664309f", size = 219848, upload-time = "2026-05-26T20:38:38.715Z" },
|
{ url = "https://files.pythonhosted.org/packages/ae/23/82e910835ef4b8391047025e1d53aa48d66029f444eb8b25373c849bf503/coverage-7.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:003fff99412ea848c0aaebcc78ed2b6ce7d8a1227ed17e68470672770b78a02a", size = 220662, upload-time = "2026-07-02T13:08:39.205Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/91/82/a5eb47257c50601bb7b9a9d2857c67b7a3a85ad74180eb2c98bb1fbe0ce5/coverage-7.14.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a24a81f9715ee42ef59a316cc11611c98fe23920f7c81861315c9f3ff4a230f4", size = 220354, upload-time = "2026-05-26T20:38:40.232Z" },
|
{ url = "https://files.pythonhosted.org/packages/6d/0d/c7b213dde2f1579de5231062b386d8413f79c11667eb58c39319b25991da/coverage-7.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5cbd804bf2784ce7b45114516050f346ecd50f960c4bb630a7ee9e1d78fa2118", size = 221168, upload-time = "2026-07-02T13:08:40.471Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/43/8b/78419b5391a5cb706b6544390507e469d83ffc9a8248b02c4011aceb9365/coverage-7.14.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:196a13319ad88d6d8ef5ab489ec4f44ddde2143c0c7d5b27786f6c3ffd56a7e1", size = 250771, upload-time = "2026-05-26T20:38:41.782Z" },
|
{ url = "https://files.pythonhosted.org/packages/33/77/d000aeedfac085088337b3c7becdad328474b1f8a9e4c9368a0c99605d68/coverage-7.15.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8773e15c23305b58882a4611fb9b2755977eae0dc2e515366a1b6c98866cc4c2", size = 251587, upload-time = "2026-07-02T13:08:42.033Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/77/63/e77aaacd491182210d639636b7a8bba23ffffa9b82aa3762da9431855fa9/coverage-7.14.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3d452fd08b5c72c5167c93e6867b5c08500bd40f2a21e1e854a500550b6cc36f", size = 252683, upload-time = "2026-05-26T20:38:43.305Z" },
|
{ url = "https://files.pythonhosted.org/packages/cc/e0/86787c56b9df17afd370d5e293515dd4d9a107a561d13054873eefad8ecc/coverage-7.15.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f50e40081494c1dc4239ebb202014cbcc3306ea96fb6302a34c8cc0967fc5ae8", size = 253497, upload-time = "2026-07-02T13:08:43.387Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/65/1c/a022e3cfbec2ac241640003cb3a817e161d9c7f5aa9b49173756cdc03204/coverage-7.14.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:23bf7fa51ac02e07fc7c96849b82946da47ae862dc8f86d183b2a4864fc38129", size = 254791, upload-time = "2026-05-26T20:38:45.361Z" },
|
{ url = "https://files.pythonhosted.org/packages/3f/02/181bc917359299c07dead6270f94e411151c8b60cec905c33499da69afe6/coverage-7.15.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:daf96f37f5fc3a7b6c6da862eb4aee61c426bd63da236ed4a73ef0e503b4bca5", size = 255607, upload-time = "2026-07-02T13:08:44.897Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/61/d6/967e408aca4c1ceb88cb0cc677169110ae7f5995fb5eaf5fb1f5a1bb8f5d/coverage-7.14.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bcaa50684dcaadfa599ac48f81103c756d791cfd85c97203d2217c593d48b860", size = 256748, upload-time = "2026-05-26T20:38:46.91Z" },
|
{ url = "https://files.pythonhosted.org/packages/b9/35/ca5e7427699913da6788c4f910e73ab16c5f4b59ec5d3a999dce2a45112f/coverage-7.15.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:51aa20f6ae2788fd197747766edf4cd8234fd9423309b934257fa6b21a592723", size = 257563, upload-time = "2026-07-02T13:08:46.334Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/b8/be/869188f7fe28638078ec479331ace6dc5f7b40b7153eb616f47ab79404d8/coverage-7.14.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4ea1c034f95c9b056e856b794630b17f9fa3d57e4800ff1e503d3be0f9c9078c", size = 250907, upload-time = "2026-05-26T20:38:48.493Z" },
|
{ url = "https://files.pythonhosted.org/packages/0b/4d/b8220bacc2fc3c4e9078e27c32e99fb411479a4718a72bdd00036a9891c8/coverage-7.15.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:03d1f922757662eb7af586e77834792274cff776bc7b1d1a0b66a49ea9d84735", size = 251726, upload-time = "2026-07-02T13:08:47.941Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/07/aa/adb7d3b4278d690e68703abcd76ab1b948242e3668d921711551b78f9ddb/coverage-7.14.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c7e057326434e441306226fbeb5d1aaf14a2637efe97ba668306635835f32ad7", size = 252483, upload-time = "2026-05-26T20:38:50.074Z" },
|
{ url = "https://files.pythonhosted.org/packages/c4/e4/2e145da1991d72189b9c3cf7eca05c716ee7080d099aaea6757cfc7df008/coverage-7.15.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a6d6acc9a7666245e6133dd15144ca038a85a9cd5026bb06d6bbae9e77440dc9", size = 253301, upload-time = "2026-07-02T13:08:49.5Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/43/61/331c74103c62dcb0c4b9b3a0de9a61aca016208b0a90f109592a9f9ecc28/coverage-7.14.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:59baf88468dbc8d63b1887afd92bda52e40bb1561696e5819670601403810cec", size = 250545, upload-time = "2026-05-26T20:38:51.613Z" },
|
{ url = "https://files.pythonhosted.org/packages/72/28/d2c841d698bf762e481f08bd4839d370246b6d9b61dab085a7b20b201a08/coverage-7.15.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1ac2c4c27c7df851dc9a017c2d7de00b69147e84ba3d96f37a530b0b6fb51035", size = 251361, upload-time = "2026-07-02T13:08:51.304Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/f6/b6/c5dae3c104d89be04828f61810e6b3473825482e4c288cc4ed04553e08ae/coverage-7.14.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:d34d75f892b3ab73ba11cab5442cce7b3e168fd64162b16f0e1e0d09c508edef", size = 254310, upload-time = "2026-05-26T20:38:53.503Z" },
|
{ url = "https://files.pythonhosted.org/packages/9d/ed/55d9ffde994fba3897c0c783f77a7d053b0c18787f6892ed5b0aed73f469/coverage-7.15.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b761a1d504fd4bd1f20f418753964dca9f5862a511fc854dac58296b3b223671", size = 255129, upload-time = "2026-07-02T13:08:52.661Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/ad/a1/2b9d5863e3b83c01ad8199e3c597802fbb3a9dc90b058885804c20296d31/coverage-7.14.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:3a56abc20a472baf0304c455721bc601477440d28ecfde8a03dde79ede07e0df", size = 250266, upload-time = "2026-05-26T20:38:55.414Z" },
|
{ url = "https://files.pythonhosted.org/packages/1d/c0/ecbf33b8c460ea2718aeb813e2df8140d0370e5f67261c31524ceb0a2a8d/coverage-7.15.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:e43b045e11c16e897895758ae90e4a90cf99e93d58549e2f90c0e2272e155695", size = 251081, upload-time = "2026-07-02T13:08:54.188Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/7f/5e/0e511fbdb269359be26fe678a1c3fa1f2aa2a01573cc3f54268c8d6d4797/coverage-7.14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6a3cb83d1552c0cd1b4906655b6a33fd4a8473229633a901c6b73bf86914dee9", size = 251174, upload-time = "2026-05-26T20:38:57.141Z" },
|
{ url = "https://files.pythonhosted.org/packages/a9/de/fb87b4261f54448dd2b9504ef19a58be42cef0d9520595fbfe1219b15234/coverage-7.15.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:589b54513e901739f4b4582c705ce96b80c96f57641b1464607e2367a270e540", size = 251988, upload-time = "2026-07-02T13:08:55.726Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/85/10/e55307b622b3dd9671cb321824502dc10f93e72f2802b9946159a8edadeb/coverage-7.14.1-cp311-cp311-win32.whl", hash = "sha256:10274a1fbeb8ec5d72966e17bb198a3104257aca4ac09d98667c5f8aca8c8548", size = 222354, upload-time = "2026-05-26T20:38:58.727Z" },
|
{ url = "https://files.pythonhosted.org/packages/df/27/3494d5f291b9a4cb868f73c11221a8bd2d5bd761a8f9acea61ff57128dd1/coverage-7.15.0-cp311-cp311-win32.whl", hash = "sha256:106781b8482749162d0b47056937ba0933508e5d9447f65a5e7d5c422f0d6bb4", size = 222754, upload-time = "2026-07-02T13:08:57.091Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/71/cf/107421693cfb71e4f1ca5bf70443f64d4161878068d07a3e51c7ad21d17b/coverage-7.14.1-cp311-cp311-win_amd64.whl", hash = "sha256:87ebdf787d4888e3f3f2d523eadc6e18c6d18c6d0eb173801a189641627fb37e", size = 223290, upload-time = "2026-05-26T20:39:00.413Z" },
|
{ url = "https://files.pythonhosted.org/packages/2a/ee/cd4847ebc9be6a9c0123d763645a6f1f3be6b8c58c962706368b79cbac07/coverage-7.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:821e92b3631d762a339695824cadbbc73020354eba2a23a551a99ad34938fbe6", size = 223225, upload-time = "2026-07-02T13:08:58.594Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/b8/1d/3e3644585eb29e9dafefb19555078529a4d7cce12bd21929664eea989277/coverage-7.14.1-cp311-cp311-win_arm64.whl", hash = "sha256:dd34767fa19848d35659ffc0a75314f58c7af3f1cd87ec521e8292a1238398a3", size = 221953, upload-time = "2026-05-26T20:39:02.159Z" },
|
{ url = "https://files.pythonhosted.org/packages/57/37/5011581aa7f2be498b97dcc7c9902192442a42f4f9a748aeadb3d6506b42/coverage-7.15.0-cp311-cp311-win_arm64.whl", hash = "sha256:309990eb5fb8014b9f67cb211f7fd41876ec8a88a88d3ae76de0ed1d611e3640", size = 222774, upload-time = "2026-07-02T13:09:00.074Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/3d/b7/bdbb725ba02c5b42825b200c940f38b7a54fcad24627b7192f78f8110d76/coverage-7.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a06c76364a9360e33d6d23769aefdf7f66f38e2ffb60ceb1baaa4989d83b695c", size = 220022, upload-time = "2026-05-26T20:39:03.702Z" },
|
{ url = "https://files.pythonhosted.org/packages/2a/74/fd4c0901137c4f8d81a76ada99e43c65163b4c94a02ece107a4ec0c6b615/coverage-7.15.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b75ee5e8cb7575636ac598719b4307ac529ec8fcd79608a35c3cd4d4dada812d", size = 220838, upload-time = "2026-07-02T13:09:02.084Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/72/81/fdc0898a55c6219223291ec1a1fe89966ef212ce82276aa0899df84b5de0/coverage-7.14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fad54e871165f6ec2f536063ac74c3104508a12963e64072ba44bd822de52b0c", size = 220379, upload-time = "2026-05-26T20:39:05.381Z" },
|
{ url = "https://files.pythonhosted.org/packages/0f/2e/2347583467bd7f0402635101a916961915cc68fce652cd0db5f173ea04fc/coverage-7.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffb31267816b93b075302248cc1737506081b4f163df4401e9df1a6424aafabe", size = 221197, upload-time = "2026-07-02T13:09:03.617Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/de/72/de048c4a25e13bce59ac6a339351c10bdf2515e07459afcdaf04dc3143a2/coverage-7.14.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:84b535f00655ecafe1d929d1fb00ed5d6fa3051ea643ab2c161a3887b86f294b", size = 251888, upload-time = "2026-05-26T20:39:07.367Z" },
|
{ url = "https://files.pythonhosted.org/packages/f0/17/99fa688541ae1d6e84543a0e544f83de0c944815b63e9e7b1ed411d15036/coverage-7.15.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e4d0bb73455bf97ab243a8f12c37c686ccf1c13bb614b7b85f1d062f06f42b2c", size = 252705, upload-time = "2026-07-02T13:09:05.059Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/28/30/300c343f68beb9d4cbb64ec81e58c5b6b80b56927f72d2b38654ac26e013/coverage-7.14.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6b6b0853b895fe0e98cbfc580d1ec3393d9302b4b1e96a77b3f5c91fdab899e6", size = 254624, upload-time = "2026-05-26T20:39:09.037Z" },
|
{ url = "https://files.pythonhosted.org/packages/fb/02/6a95a5cd83b74839017ef9cf48d2d8c9ae60af919e17a3f336e6f9f1b7bd/coverage-7.15.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:20d9ccc4ebd0edc434d86dfd2a1dd2a8efa6b6b3073d0485a394fee86459ebb4", size = 255441, upload-time = "2026-07-02T13:09:06.559Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/b1/ed/7b25642496e8170b6bac14adce00537c6e5fa2d586159401a4de3e8b49e6/coverage-7.14.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:442cc9c952b2df400cda54bb04ab87330cf2cd08a8692cbbea36773531eb6f37", size = 255739, upload-time = "2026-05-26T20:39:10.889Z" },
|
{ url = "https://files.pythonhosted.org/packages/67/f2/406f6c57d600f68185942422c4c00f1a3255d60aee6e5fd961425cd9987e/coverage-7.15.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:20c8a976c365c8cb12f0cbd099508772ea41fb5fa80657a8506df0e11bd278c5", size = 256556, upload-time = "2026-07-02T13:09:08.197Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/7f/a2/abd210b8c4e29c24e4624916db97bb519097a91034aaeb767f937e7da794/coverage-7.14.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8270544c361ed405a27a060dbc9ed2c124b084d96dfdc2d9a2510482aef981ad", size = 257998, upload-time = "2026-05-26T20:39:12.722Z" },
|
{ url = "https://files.pythonhosted.org/packages/74/8e/d3fa48489c15ecdec1ba48fd61f68798555dddd2f6716f9ad42adeb1a2a9/coverage-7.15.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f948fd5ba1b9cbca91f0ae08b4c1ce2b139509149a435e2585d056d57d70bf01", size = 258815, upload-time = "2026-07-02T13:09:09.691Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/7f/24/7c50beed3792fe62f6ce0545c6686ce83379719e2c0276179333d97eae92/coverage-7.14.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:48b283b1dd6372e8de2a7a9a4c4d5dc06f4d4fd209b876f3c88a7a205a0c8f84", size = 252296, upload-time = "2026-05-26T20:39:14.259Z" },
|
{ url = "https://files.pythonhosted.org/packages/47/2e/2d40ddd110462c6a2769677cf7f1c119a52b45f568978fc6c98e4cc0dd0f/coverage-7.15.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f58185f06edf6ad68ec9fb155d63ef650c82f3fbd7e1770e2867751fb13158f4", size = 253117, upload-time = "2026-07-02T13:09:11.212Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/15/05/0f874628ebcbfc77ead559ff210281ef06a97db08481832e7dd39274a135/coverage-7.14.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5b0c99ba93a07d56f6df340bb79be53202a082b2fdb81bfe6190b741a3470d54", size = 253658, upload-time = "2026-05-26T20:39:15.923Z" },
|
{ url = "https://files.pythonhosted.org/packages/51/c0/310782f0d7c3cb2b5ac05ba8d205fe91f24a36f6bf3256098f1782181c38/coverage-7.15.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:02adc79a920c73c647c5d117f55747df7f2de94571884758ce8bc58e04f0a796", size = 254475, upload-time = "2026-07-02T13:09:13.029Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/99/6f/ca6ad067364b337ef997802115e7ecad2abd2248b05471464b0dea02b4d4/coverage-7.14.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e471bc5769ff073b058cfadb0d736b56ce067c8560eabeb0da88462df98c23e7", size = 251803, upload-time = "2026-05-26T20:39:17.537Z" },
|
{ url = "https://files.pythonhosted.org/packages/86/f7/702da6c275f8ae6ade423d2877243122932c9b27f5403003b9ef8c927d12/coverage-7.15.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:6eb7c300fbed667fd6e3588eba71c1904cdb06110ca6fdf908c26bdd88b8e382", size = 252619, upload-time = "2026-07-02T13:09:14.699Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/c0/30/b9b4d377cd9f40baf228068f5a81faf8450c6228503011bd499708483a50/coverage-7.14.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f497a1ea81d4cd7c10ddcaa685135b9aabd291af3d55775a9ddf3cb7a364cdd9", size = 255873, upload-time = "2026-05-26T20:39:19.414Z" },
|
{ url = "https://files.pythonhosted.org/packages/fb/84/c5b15a7e5ecba4e56218d772d99fe80a63e63f8d11f12783723a6005ab45/coverage-7.15.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:b5fb23fa2de9dce1f5c36c09066d8fcda16cd96e8e26686caa2d7cb9b567d65c", size = 256689, upload-time = "2026-07-02T13:09:16.103Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/3c/21/7c721a9e5e6bb88547d30a787aefb97512d3f54c1324c7488d9b3743f7f9/coverage-7.14.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:2222be86d0b54f5dd5a38f45f17f315f737245e857bf0bdedc70734f84a13c02", size = 251372, upload-time = "2026-05-26T20:39:21.169Z" },
|
{ url = "https://files.pythonhosted.org/packages/95/2f/c8b07559b57701230c61b23a953858c052890c12ef568d81780c6c46e92e/coverage-7.15.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:cec79341dbe6281484024979976d0c7f22beae08b4a254655decd25d42cbe766", size = 252189, upload-time = "2026-07-02T13:09:17.828Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/9d/8c/f8ae5a2200130e1503cd7661a6cd3b2b7bacef98277fbf3571fb13f8b766/coverage-7.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:85e85586565842f6932abebd4c18bcb1074223dc0b3576e7d173ca710622813a", size = 253245, upload-time = "2026-05-26T20:39:23.097Z" },
|
{ url = "https://files.pythonhosted.org/packages/6b/80/6d2f049dd3fd3dbfd60b62ba6b2162a04009e2c002ce70b24cf3878dec7a/coverage-7.15.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6c664c5444b1d970b1b2a450e21fb19ee5c9cfdf151ded2dda37260031cca0da", size = 254059, upload-time = "2026-07-02T13:09:19.304Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/34/62/70a9024672a5f6910517d9628c52c9afbdd3cf8f46426af52bb148a56fff/coverage-7.14.1-cp312-cp312-win32.whl", hash = "sha256:4a28fd227808366b196a75476dced2eb35b351d6766ba9c858dc93319e87f4f1", size = 222567, upload-time = "2026-05-26T20:39:24.868Z" },
|
{ url = "https://files.pythonhosted.org/packages/ce/92/b0287a2c42031d25c628f815f89a3cd9f8268ee78bb1252c9356cda1c689/coverage-7.15.0-cp312-cp312-win32.whl", hash = "sha256:5f764a3fa339bde6b3aa97657f5a6a3a9451e4a5b4ea98a2892c773a43525f77", size = 222893, upload-time = "2026-07-02T13:09:20.812Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/f6/81/8b7cd386839b039ebe1855733b9f9449a8dec5d79564018234f185a7fa70/coverage-7.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:54acdb6674a4661768d7bf7db32dfb9f46ab1d764f8aba6df75ce1a6a088724e", size = 223372, upload-time = "2026-05-26T20:39:26.603Z" },
|
{ url = "https://files.pythonhosted.org/packages/a9/69/e34c481915fecb499b3146975061dac528752e37706edc1804f32c822469/coverage-7.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:52f9a4d2c4c56c8848bc2f524916698354b0211488b38c49ad9ae54f6cafbff6", size = 223429, upload-time = "2026-07-02T13:09:22.315Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/ae/ba/b44d472022f620d289d95fa830143235c0c36461c6f2437ea8d51e5481ed/coverage-7.14.1-cp312-cp312-win_arm64.whl", hash = "sha256:99cd41ff91afd94896fea3bc002706b6ae4ce95727d06e4a0f39c0a8d8bd8b1a", size = 221989, upload-time = "2026-05-26T20:39:28.242Z" },
|
{ url = "https://files.pythonhosted.org/packages/fe/98/6e878f0b571d32684ef3f38d7c03db241ca5b82a5da8a5391596a8f209c4/coverage-7.15.0-cp312-cp312-win_arm64.whl", hash = "sha256:31e5c3e70c85307ea35a12964e2e40f56ca2ee4b1c8c721ccf4609d17071080b", size = 222810, upload-time = "2026-07-02T13:09:23.812Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/8a/9e/5f6d56327c62b185225d145191c607e07515294a0aa6338e58805cd4a5ac/coverage-7.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:be9f2c802dcfce3f71298303aa5dad0dce440a76c52f2f60dacd8656dab78793", size = 220044, upload-time = "2026-05-26T20:39:29.902Z" },
|
{ url = "https://files.pythonhosted.org/packages/76/04/145a3748098bcc86b631a85408d2c3dc5c104e0bd86d605468239b25b6c4/coverage-7.15.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5be4caf3b28836f078abe700f8944dac4a65d78f16d6c600c89cb624e5535782", size = 220863, upload-time = "2026-07-02T13:09:25.371Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/75/92/e82aca356744cbbc0f77a0b623e38918c1872361963413a3bab5d0340393/coverage-7.14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6223a72fd0e4c7156353ec0f08a5f93623e1d3034d0e2683b9bb8ea674131b1d", size = 220412, upload-time = "2026-05-26T20:39:31.561Z" },
|
{ url = "https://files.pythonhosted.org/packages/a4/5c/4ed55708fed2c64b63c9bc5715daef670872202101938869b7fe5d5fbb8f/coverage-7.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dd58ad1404704303ca8d4f4b8a1095e7cbc7040ef17a66df1e6619aa10176430", size = 221230, upload-time = "2026-07-02T13:09:26.897Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/27/c9/385bde0bf7ed0f4bf3a7ee5367060a86b5d218718cfd6fb943c0f836b34f/coverage-7.14.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7279d2110a28cebc738b6459ecda2771735a4c18465fbbd36b3288fe5ed92247", size = 251412, upload-time = "2026-05-26T20:39:33.337Z" },
|
{ url = "https://files.pythonhosted.org/packages/7b/19/3a80b97d3b2a5c77a01ae359c6bed20c13738fe3d9380f08616d4fec0281/coverage-7.15.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bbcbb317c2e5ded5b21104af81c29f391be2af98d065693ffbe8d23949b948e5", size = 252227, upload-time = "2026-07-02T13:09:28.543Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/51/8c/23faf6a2343a0d17f960a4bd56c43bc7eb4cf312f774dd6ceebd82c7d8fc/coverage-7.14.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9eeb3fcbc13ba40dfbdb22d01d196a28e9cef9ed4c29b60061a1e0e823a9929d", size = 254008, upload-time = "2026-05-26T20:39:35.009Z" },
|
{ url = "https://files.pythonhosted.org/packages/a1/fa/b70062750686bd7da454da27927622f48bbac6990ac7a4c4a4653e7b0036/coverage-7.15.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:27f31ecb458da3f859aab3f15ada871eb7a7768807d88df4a9f186bb17737970", size = 254823, upload-time = "2026-07-02T13:09:30.177Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/42/06/36f4aa9ca8a815e6036156e80706a67828bb97bd826948244f6996dda957/coverage-7.14.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f0cfc27c539f07cf5c0a4cfe211d0b6cae039f8f40526dbaa71944e64b50a7b", size = 255241, upload-time = "2026-05-26T20:39:36.71Z" },
|
{ url = "https://files.pythonhosted.org/packages/a9/09/dad6a75a2e561b9dc5086a8c5257a7591d584246f67e23e70d2995b89ab6/coverage-7.15.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13fb759be317fdc62e0f56bffdf61cfcb45c7761ad6b71e3e583e71a67ae753c", size = 256059, upload-time = "2026-07-02T13:09:31.979Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/ca/79/95266316352f90f6b1c6736bb413302edfde2453fb32422d3911642691b3/coverage-7.14.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:221c70f316241a78e77e607c227cefc8808d4e08f28d99c04f35694690e940be", size = 257373, upload-time = "2026-05-26T20:39:38.412Z" },
|
{ url = "https://files.pythonhosted.org/packages/e6/e7/b5d2941fa9564573d44b693a871ff3156f0c42cbefe977a09fa7fdc59971/coverage-7.15.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d5cf007add5ab4bb8fa9f4c77e3732127c9e6cad501d7db43355fbfafca0be84", size = 258190, upload-time = "2026-07-02T13:09:34.035Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/e3/9c/58316d1f66c488b5fca8a0eb3e98348807813efa8a0d0833b9021be27488/coverage-7.14.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:da028256b04ec30e5e0114b6f76172938c313991f0a2d3d894271315cf5d5e43", size = 251635, upload-time = "2026-05-26T20:39:40.268Z" },
|
{ url = "https://files.pythonhosted.org/packages/7c/1d/8e895bcde3c57ccd46d896dda5f2b3d5df761a1b0c6c9d450d175dedc632/coverage-7.15.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cc78d9843bd576fbe2118248258d485e968dc535f95ed504a7b0867ba9b51389", size = 252456, upload-time = "2026-07-02T13:09:35.765Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/ef/5a/ca2398a568e16fed7bb713e84ba3603a7164fb65779abe645c565ec890d5/coverage-7.14.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:76a085d7005236a767e3426148b2c407e53ad61695c562f8a81da2d373324901", size = 253373, upload-time = "2026-05-26T20:39:42.145Z" },
|
{ url = "https://files.pythonhosted.org/packages/14/4c/f6997da343ddeb959be82c3b05322793f92c071ad45f7cb8a96336e2dd5f/coverage-7.15.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a263060f1de0b4b74b4e089c2a70b8003b3781c733329a9c8fd54995328f9950", size = 254192, upload-time = "2026-07-02T13:09:37.445Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/6e/2c/0396562c32deaebe7be51d865b3a41e9a87d7561acafe1a28f53b07e019a/coverage-7.14.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b553d04b5e778a8e56d57eb134aff42a92718ecba45e79c4764ecfa40efd92ff", size = 251341, upload-time = "2026-05-26T20:39:43.907Z" },
|
{ url = "https://files.pythonhosted.org/packages/17/27/a0bc09d032267b9da89d95a2d874cfbef2a5aebbf0e87cf7aba221d79a99/coverage-7.15.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:c48decf16e0dfd5b049c7d5e82200c23c08126719142998d4f172444e3d0529e", size = 252153, upload-time = "2026-07-02T13:09:39.422Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/fd/8f/a94f9221184c9cae1ee115820e3798e48b6b17777a9f19e46fb9a0c8dc74/coverage-7.14.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:46f714d2fb8ae2f4f29f23ada7f1e79b759fff5a70f94a1dac23af204c3ec9e4", size = 255497, upload-time = "2026-05-26T20:39:46.166Z" },
|
{ url = "https://files.pythonhosted.org/packages/54/c0/77fc233d9fba07b244c40948c53fe27308b8f21732fb3417f87fbd6fd992/coverage-7.15.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:08fb028000ed0aaa0a4cbdfbb98be7cb42f370db973fbbb469733505ab20e13e", size = 256310, upload-time = "2026-07-02T13:09:41.006Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/71/69/505d70e47db1eaebcd002c39759707621ef184cd6b1ae084d9f41293f323/coverage-7.14.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:1896f5e19ff3f0431c7ce2172adc54890fd97f86b59ced8ca1649145d9ffe35d", size = 251159, upload-time = "2026-05-26T20:39:48.03Z" },
|
{ url = "https://files.pythonhosted.org/packages/d5/24/601cecfb5825becacb8d45219a018a3b55b9dbaec624efdb0ea249d08be2/coverage-7.15.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:fb7dc0c3b7d8a1077abea0b8546ebc5e26d6ef6ecefc2f0f5ad2b8a53bdad837", size = 251974, upload-time = "2026-07-02T13:09:42.733Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/e0/aa/58681c383aa33a9d2ed40a02d7a22fbf780d1fa4d575396365777828198c/coverage-7.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:62fd185ef9df3c33d1c8178c5af105f762afbad96038de9a4ae100aa6297ca33", size = 252934, upload-time = "2026-05-26T20:39:49.872Z" },
|
{ url = "https://files.pythonhosted.org/packages/47/1e/6f45e5a5b3d5484318d368702af6716b5ab8913b0428bec981a562fcf296/coverage-7.15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6cb3602054ccbe9f0d8c2dc04bbeba90d5719236e2cd06e042ddd6d3fc7b6e37", size = 253745, upload-time = "2026-07-02T13:09:44.376Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/eb/fd/11c928cd6bdffc7074bb5965c173d9ebf517fb00205e1da524b98d29ef92/coverage-7.14.1-cp313-cp313-win32.whl", hash = "sha256:ab4af6352741a604c431c6072fce5bee33bf0f20dc7a56618d6bf6bb89e9810c", size = 222584, upload-time = "2026-05-26T20:39:51.68Z" },
|
{ url = "https://files.pythonhosted.org/packages/8e/db/4df027a77bd11d0e527f44c53557c76e54ad027413d0304252ea3a78d67e/coverage-7.15.0-cp313-cp313-win32.whl", hash = "sha256:0bf781da64326b677be344df505171435b6f58716108606621d5d27d964fff8b", size = 222902, upload-time = "2026-07-02T13:09:46.122Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/6f/92/fb416fc26d340dcba19518c418d6048e913186e17243982c5e435e41fa7a/coverage-7.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:7af486dabe8954d03b087f0021540897afe084f04e16ff5579e08cc46f871416", size = 223394, upload-time = "2026-05-26T20:39:53.472Z" },
|
{ url = "https://files.pythonhosted.org/packages/a0/10/0355894d34e231f2c5449e71287e81a50793a325df2e2b027b7bcd9dfd19/coverage-7.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:2c57a275078ee3fa185f83e400f765bc764a549de66d99b47881645cbd4ea629", size = 223444, upload-time = "2026-07-02T13:09:47.687Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/73/c6/02d56e3867972f77d5036de924643f26c056e848f00452cafb4dbc3c29b4/coverage-7.14.1-cp313-cp313-win_arm64.whl", hash = "sha256:2224f89ffd0c5605ccce1ed7a584da162bc7c55f601ab1c946bc9de31a486b42", size = 222015, upload-time = "2026-05-26T20:39:55.374Z" },
|
{ url = "https://files.pythonhosted.org/packages/06/ef/bb725f263befaaff851203ab338e68af15e195d7f7b5f323162532d9b6a8/coverage-7.15.0-cp313-cp313-win_arm64.whl", hash = "sha256:3812c61afc6685c7999b39320779ab8f43b7a3081fdb0def39976e56fbdb9a21", size = 222839, upload-time = "2026-07-02T13:09:49.717Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/4d/9e/fcc77914050df73f7662fa1f00902774c79c075a8388ab334074574bf77e/coverage-7.14.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:de286598cc65d2b489411174b1faec2f5a7775fb3201fd925db2a76b4030f37d", size = 220733, upload-time = "2026-05-26T20:39:57.189Z" },
|
{ url = "https://files.pythonhosted.org/packages/4f/9c/1e3ca54f72a3185ece06c58d871099898c48f0ed6430d17b6ab75f0d180a/coverage-7.15.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:41cb79af843222e11da87127ad0ecbfa878abadd0f770a4a99391a27d3887324", size = 220906, upload-time = "2026-07-02T13:09:51.339Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/f7/67/2963cbdaf5cbadec44efa3a1e39eaa1f02df4079585f05387607a221e126/coverage-7.14.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:042c46ded7c288aeb07cf14a28b6c1e10b78fcba40171c3fa1e939377eeef0b5", size = 221086, upload-time = "2026-05-26T20:39:59.019Z" },
|
{ url = "https://files.pythonhosted.org/packages/09/37/f718613d83b274880382f6b67e78f3802549ae39b0b3e65ae5b5974df56e/coverage-7.15.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7d2008989ef8fe54188d3f3bfa2e3099b025af11e90a6a1b9e7dc433d04263d8", size = 221239, upload-time = "2026-07-02T13:09:53.138Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/c8/c5/8701645574e11881f2f47d8930f98bc48b5d43b25eb5b4430dfc4a2f9f48/coverage-7.14.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f4ddbe407477f04c45115d1a4e5bc480f753553b534d338d4c3358b1cdd0ea52", size = 262381, upload-time = "2026-05-26T20:40:00.822Z" },
|
{ url = "https://files.pythonhosted.org/packages/a7/ce/22bae91e0b75445f68d365c7643ed0aa4880bbf77450ee74ca65bdae53a7/coverage-7.15.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:769e8ece11a596315ebf5aa7ec383aeeed016c091d2bf6363ffb996d41529092", size = 252286, upload-time = "2026-07-02T13:09:54.996Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/7c/28/7a64d73598263e0c5abd5084211a8474488d31b3c552ff531c719dfcff62/coverage-7.14.1-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d13e6725992e2d2fd7d81d4f5241952d13740121dfd501da09201be39b2c003a", size = 264458, upload-time = "2026-05-26T20:40:02.506Z" },
|
{ url = "https://files.pythonhosted.org/packages/dd/1e/bec5e32aa508615d9d7a2790effb25fb4dc28606e995816afe400b25ece3/coverage-7.15.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:65a6b6164ee5c39e2f3803f314292d6c61a607ba7fee253d1e03c42dc3903502", size = 254789, upload-time = "2026-07-02T13:09:56.678Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/fa/d8/4969179db9f7eb4df218e69540adf829d1c835f59452513d065d15446802/coverage-7.14.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f747dc8edcfe740130f28f32f3995e955494285717e86ee25af51db2219df08a", size = 266884, upload-time = "2026-05-26T20:40:04.421Z" },
|
{ url = "https://files.pythonhosted.org/packages/17/29/0e865435b4354e4a7c03b1b7920046d31d0a273d55decefea27e011cb9bf/coverage-7.15.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:75128817f95a5c45bb01d65fd2d8b9cb54bbe03d81608fb70e3e14b437ad56c2", size = 256135, upload-time = "2026-07-02T13:09:58.343Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/a6/78/a45d5794dbc9bafd97afc96a4377c86c7820d78b6cf51b89bc1d4e919275/coverage-7.14.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ced2f09ef276fd58611a1ef502164ad266d2b75174e5a40cabbdb4033f9f6cf2", size = 268022, upload-time = "2026-05-26T20:40:06.298Z" },
|
{ url = "https://files.pythonhosted.org/packages/84/ff/33a870b58a13325d62fc0a6c8f01fa0ff667cef60c7498e2382a147dfa18/coverage-7.15.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9887bb428fe2d4cd4bee89bac1a6c9932f484afd5b36fbd4ff6ea5f825bb1f5e", size = 258449, upload-time = "2026-07-02T13:10:00.057Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/21/cb/4f5e354e9e3e67af96bd4e57113e6db6b22298c7168b13eec408a549903d/coverage-7.14.1-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b84800013769a78ccb9ef4659402e26d06867e337b61ec365f77ad008adea80e", size = 261631, upload-time = "2026-05-26T20:40:08.226Z" },
|
{ url = "https://files.pythonhosted.org/packages/18/7b/6fffe596bf3ddba8462758d02c5dad730fd91055a6634aa2e4226229181a/coverage-7.15.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0bfc0be1f702042207a93a00523b1065ee1fe951e96edf311581c0bbc2e34888", size = 252313, upload-time = "2026-07-02T13:10:01.946Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/ec/49/eced49af4cb996d5d8b7e94e736175c513e4facd3398507b89892b4326d8/coverage-7.14.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:ea8cd6ca0ee9f616aaef3afc6882e32c2cbf18b00d96313ffd76af650574034d", size = 264443, upload-time = "2026-05-26T20:40:10.137Z" },
|
{ url = "https://files.pythonhosted.org/packages/58/1b/11468dd6c1676ab831a70cb9a8d4e198e8607fa0b7220ab918b73fe9bfbd/coverage-7.15.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f64627d55def5a43282d70e08396672692f77e4da610a5bb8bb4060b432b6859", size = 254142, upload-time = "2026-07-02T13:10:04.065Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/f1/d8/5603a88a7c5913a6b54f6cb1a8c46f7b39cbb30f27cd3f492908da09b2d7/coverage-7.14.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:aa5e304a873fabddc11e484e9b6b738bd38bd7bed17b09aa84eecf5332e8b8bb", size = 262069, upload-time = "2026-05-26T20:40:11.999Z" },
|
{ url = "https://files.pythonhosted.org/packages/79/41/29328e21d16b1b95092c30dd700e08cf915bd3734f836df8f3bdb0e8fa9f/coverage-7.15.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:2c6f0fa473003905c6d5bac328ee4eba9fbea654f15bc24b8a3274b23363fa99", size = 252108, upload-time = "2026-07-02T13:10:06.11Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/f0/59/2ae3cb79da554a06c8619d6c88ea19dd1e4aed4b834b6a83bb1fa243bdc5/coverage-7.14.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:5a1c5215be81035e629d5bc756650634d0bf31991038db7a0eccb90f025ce16d", size = 265780, upload-time = "2026-05-26T20:40:13.858Z" },
|
{ url = "https://files.pythonhosted.org/packages/9b/de/05ccfb990439655b35afbfd8e0d13fe66677565a7d4eb38c3f5ef2635e1c/coverage-7.15.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2bcf9afaf064172c6ec3c58a325a9957ad1178c05dd934e25f253321776e0676", size = 256385, upload-time = "2026-07-02T13:10:08.141Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/af/5f/b130c1dc999031f2648bd25317fbce505ad8d5562079b4ed81e736a84967/coverage-7.14.1-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:79058c47dae6788504b5effb319961bcd72d7240551464b91d474bc0ed186d69", size = 260970, upload-time = "2026-05-26T20:40:16.142Z" },
|
{ url = "https://files.pythonhosted.org/packages/51/0e/486828a3d2695ea7a2609f17ff572f6b01905e608379440a11da4b8dffbe/coverage-7.15.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:baf06bc987115d6fb938d403f7eab684a057766c490367999a2b71a6883110c6", size = 251923, upload-time = "2026-07-02T13:10:10.179Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/87/d1/ec13ccddeb48ec963bdfa72a11224bac2584bd045ba13beca82f8113e9c7/coverage-7.14.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:370c5afae3fa0658e11694a32b24c2778f6bc2d17718121f94ee185e69f26b54", size = 263157, upload-time = "2026-05-26T20:40:18.382Z" },
|
{ url = "https://files.pythonhosted.org/packages/18/c7/03582b6715f078e5e558354c87616d945b9894cda2dace8e4009b17035e4/coverage-7.15.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f0405f2ff97b1c4c0e782cb32e02f32369bcf2e6b618b591d67e1ea754575dfe", size = 253580, upload-time = "2026-07-02T13:10:12.052Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/cf/c2/cd91ead503045161092d3845f7bb95ea2f25131ce96d3e314dd835d91b9c/coverage-7.14.1-cp313-cp313t-win32.whl", hash = "sha256:3758dd0a7f1fa57365ef2e781df0f0731d38b6e3772259d13dae4bd8a958d4b1", size = 223259, upload-time = "2026-05-26T20:40:20.381Z" },
|
{ url = "https://files.pythonhosted.org/packages/db/dc/9e578bbaf2ecb4959a81b7e7601ad8cca772cba2892e8d144cb749b4a71a/coverage-7.15.0-cp314-cp314-win32.whl", hash = "sha256:ab282853ed5fbd64bbb162f19cb8fcb7087187508a6374b4f9c34ec1577c4e8f", size = 223107, upload-time = "2026-07-02T13:10:13.994Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/71/9f/1e28d97e6bd2c76b07f38b7c02870f1371255ff6717f54eca578fcbbdd0e/coverage-7.14.1-cp313-cp313t-win_amd64.whl", hash = "sha256:6ff665fb023a77386fe11685190cee1f60a7d635994a30d9b0a061533d470fce", size = 224320, upload-time = "2026-05-26T20:40:22.316Z" },
|
{ url = "https://files.pythonhosted.org/packages/ae/3e/c8c3b75d8dbe0e35f7b0cc3ff5e949fc59500f70b21d0398813f66740664/coverage-7.15.0-cp314-cp314-win_amd64.whl", hash = "sha256:3bb3040e9f4bbe26fcb0cd7cc85ac63e630d3f3a9c74f027abf4caa27e706663", size = 223597, upload-time = "2026-07-02T13:10:15.906Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/a9/e0/d936e908f0e1efa55e52b91e01b52f1055cef5e1ab2718493390ed8e2fb8/coverage-7.14.1-cp313-cp313t-win_arm64.whl", hash = "sha256:17a5a241e5997621a956a7f402a7433ef4221e5152809b785bec79e2323799f1", size = 222577, upload-time = "2026-05-26T20:40:24.894Z" },
|
{ url = "https://files.pythonhosted.org/packages/cd/bc/3cbc9fb036eb388519bccd521f783499c39b64256013fbc362782f196fe1/coverage-7.15.0-cp314-cp314-win_arm64.whl", hash = "sha256:346771144d34f7fa84ec28386f78e0f31653f33cf35e19d253d5b35f9e8201da", size = 223020, upload-time = "2026-07-02T13:10:17.844Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/d6/34/fc2f101b151af3799a101f0550b0454aa008afdc0add677394ec4aa8ea10/coverage-7.14.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d5ed429d0b8edaac649e889b4ffcedb6c80b06629a3f93050e3dddfb99235bee", size = 220091, upload-time = "2026-05-26T20:40:27.249Z" },
|
{ url = "https://files.pythonhosted.org/packages/28/00/199c4a8d656dff63102577a056c0fce2ff6a79e40adac092fc986c49cbf1/coverage-7.15.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d34a010905fb6401324ba016b5da03d574967f7b21ce48ea41e66f0f1f95f641", size = 221638, upload-time = "2026-07-02T13:10:19.703Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/3d/a7/1ebae2ab5b961b5c79bb09fe7b3ac99edb190d8be4a8c510b2cf66f46468/coverage-7.14.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8011224a62280e50dab346960c03cf47aca1a1e09e608c0fb33fd6e0cc8e9500", size = 220421, upload-time = "2026-05-26T20:40:30.084Z" },
|
{ url = "https://files.pythonhosted.org/packages/ba/8e/9d0092c96a3d3a26951ecc7020826aa57bcb1b119ca81acbba996884ab13/coverage-7.15.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:bb25d825d885ca8036795dacfc3924d33091fc76d71ebc99420c6b79e77d96fa", size = 221903, upload-time = "2026-07-02T13:10:21.514Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/5e/90/92aca9cf0acc95123c96cd1eb1f08917897a7f5dee01e15738922971ec31/coverage-7.14.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:12c42ec1e14f553c4f817e989365982e646e27211f10a0f717855b94a79c8906", size = 251466, upload-time = "2026-05-26T20:40:32.542Z" },
|
{ url = "https://files.pythonhosted.org/packages/6d/b4/c0ca3028f42c9a08e51feb4561ef1192e5de99797cd1db5b04590c215bda/coverage-7.15.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:94c9686bfe8a9a6810297aecbd99beaa3445f9e8dc2f80b1382cca0d86b64461", size = 263267, upload-time = "2026-07-02T13:10:23.261Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/26/2b/78048cbe3b999f6cbf9cc0d90abba6a88a3e0863a8c1c6cbc762f3f8802f/coverage-7.14.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:06144cd511cf2624873a035c5069cf297144f6e77a73ee3d7a55b605ec5efb42", size = 253973, upload-time = "2026-05-26T20:40:34.473Z" },
|
{ url = "https://files.pythonhosted.org/packages/5f/aa/a375e3846e5d3c013dc600b2a3231089055c73d77f5393dd2192a8d64da6/coverage-7.15.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9bd671c25f9d85f09d7ec481d0e43d5139f486c06a37139847a7ce569788af72", size = 265390, upload-time = "2026-07-02T13:10:25.152Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/8e/21/c2e33b29d1cfde484a19d437afc343c6cd30b08d78cbbf9f5aff14e57b2b/coverage-7.14.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a311d8e1da24be5c1ccf85cbfb06315dbaa1703d5a1eab3f6432c72b837917c8", size = 255318, upload-time = "2026-05-26T20:40:38.154Z" },
|
{ url = "https://files.pythonhosted.org/packages/92/e1/5783cdabb797305e1c9e4809fea496d31834c51fa772514f73dc148bcfc9/coverage-7.15.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:110cbdf8d2e216577312cf06ccf85539c0e5a5420ef747e4a4719b5e483c88cd", size = 267811, upload-time = "2026-07-02T13:10:27.249Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/8e/ee/aad2f108d63b769121005302f16bf66db8625c88ceaba466942e09a2607e/coverage-7.14.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c79cead5b5bc584d9c71451cb984d0e3a84e0c0937379c8efcbf27c8d661b851", size = 257633, upload-time = "2026-05-26T20:40:40.164Z" },
|
{ url = "https://files.pythonhosted.org/packages/85/31/96d8bbf58b8e9193bc8389574a91a0db48355ee98feb66aa6bf8d1b32eea/coverage-7.15.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2c5d4619214f1d9993e7b00a8600d14614b7e9d84e89507460b126aa5e6559e5", size = 268928, upload-time = "2026-07-02T13:10:29.242Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/c2/f8/11a2c29b4fd76d9849f81d0bb812ec0017a9396df3217214e38934a8c837/coverage-7.14.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dcbf65f1f66a26cdd88c35cf68fb4729c5d1cd2e88added72420541dfb212034", size = 251488, upload-time = "2026-05-26T20:40:42.631Z" },
|
{ url = "https://files.pythonhosted.org/packages/5e/7a/5294567e811a1cb7eda93140c628fa050d66189da28da320f93d1d815c73/coverage-7.15.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:781a704516e2d8346fbbd5be6c6f3412dd824785146528b3a01816f26c081007", size = 262378, upload-time = "2026-07-02T13:10:31.107Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/c9/b8/9a5820de4b8ac2b71d85e3b5fb49108d7469c665f0e2ad0dd7569023e305/coverage-7.14.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fd86572566fb40189a8260446158235159bc7a82dfbc87a3b39cf4fb57fcec1c", size = 253329, upload-time = "2026-05-26T20:40:45.208Z" },
|
{ url = "https://files.pythonhosted.org/packages/69/3f/3f48538421f899f28946f90a3d272136a4686e1abf461cc9249a783ee0f3/coverage-7.15.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bd4a1b44bcb65ee29e947ac92bbee04956df3a6bfc6143641bb6cae7ede00fc9", size = 265263, upload-time = "2026-07-02T13:10:32.942Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/6b/ff/f33e4823667e27548e8fd8df44217515303f9808d0ff29817db56f87d990/coverage-7.14.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:7771b601718fdde84832c3a434ca9bbf4ae9adbc49d84198b4110700c3c77c36", size = 251291, upload-time = "2026-05-26T20:40:47.502Z" },
|
{ url = "https://files.pythonhosted.org/packages/ce/d3/092df15efcab8a9c1467ee960eb8019bbad3f9300d115d89ea6195f369ff/coverage-7.15.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0e4950c9d6d3e39c64c991814ff315e2d0b9cb8152363594212c9e55208c0a8f", size = 262866, upload-time = "2026-07-02T13:10:35.104Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/68/9b/489db0ebb209054766b90a9014a45f6d26eb724c02ec21311c3733b5a644/coverage-7.14.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:39b21e212c55af06fa375e3dbf90a8a8e38792f3a910c580066d23563830ddd5", size = 255564, upload-time = "2026-05-26T20:40:49.372Z" },
|
{ url = "https://files.pythonhosted.org/packages/e5/ab/0254d2b88665efb2c57ad368cc77ab5de3435bd8d5add4729c1b0e79431e/coverage-7.15.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:fe9c87ff42e5472d80d21704972e1f96e104a0a599d77c5e35db5a3c562e2571", size = 266599, upload-time = "2026-07-02T13:10:37.05Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/27/b5/16bc2d4c2409b23c7737edb68c83bc89e345f378050549fe1d75ac7d34d5/coverage-7.14.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:f2302660e32562a532b442480121aef8aa61a5bdb20b30bf0adab29f10a5a4b4", size = 251107, upload-time = "2026-05-26T20:40:51.677Z" },
|
{ url = "https://files.pythonhosted.org/packages/a8/79/1cfa4023e489ce6fbc7be4a5d442dbc375edb4f4fda39a352cedb53263c2/coverage-7.15.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f00d5ae1dd2fe13fb8186e3e7d37bcbd8b25c0d764ff7d1b32cef9be058510a8", size = 261714, upload-time = "2026-07-02T13:10:38.966Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/7d/0c/2629997469a00cd069d588a41c9dc887610f2775ae89d250c4791e65272a/coverage-7.14.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:03a6f93c1ec3b7f2e77b5dbcc5573a2c21f12529a5c6bbe0f16f72303cc2fa4d", size = 252764, upload-time = "2026-05-26T20:40:54.267Z" },
|
{ url = "https://files.pythonhosted.org/packages/b7/eb/fee5c8665656be63f497418d410484637c438172568688e8ac92e06574e7/coverage-7.15.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:363ab38cc78b615f11c9cac3cf1d7eef950c18b9fdedfb9066f59461dcf84d68", size = 264025, upload-time = "2026-07-02T13:10:40.789Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/d2/ee/f78d63c8f079e0d7211c7e2401fa17e311514534ba61bae03e4b287ce4ab/coverage-7.14.1-cp314-cp314-win32.whl", hash = "sha256:8a3ce026d73290f42f08dafecbd82c193a74df280461fbf97300fec51fd133ee", size = 222837, upload-time = "2026-05-26T20:40:56.496Z" },
|
{ url = "https://files.pythonhosted.org/packages/ab/99/63005db722f91edc81abc16302f9cc2f6228c1679e46e15be9ae144b14d0/coverage-7.15.0-cp314-cp314t-win32.whl", hash = "sha256:54fd9c53a5fafff509195f1b6a3f9be615d8e8362a3629ff1de23d270c03c86b", size = 223413, upload-time = "2026-07-02T13:10:42.597Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/dc/b9/be539854f93a70dfbeec69117f33ec70dc42ff0b65b5b07ab8d40d04228e/coverage-7.14.1-cp314-cp314-win_amd64.whl", hash = "sha256:114c95ef29302423b87d159075805f4ab973254a2638a5d7d046c94887cc87d7", size = 223650, upload-time = "2026-05-26T20:40:58.351Z" },
|
{ url = "https://files.pythonhosted.org/packages/c1/e8/2bc6181c4fb06f1a6b981eb85330cc57bfad7e3f710fc9c9d350013ba228/coverage-7.15.0-cp314-cp314t-win_amd64.whl", hash = "sha256:87b47553097ba185ed964866078e7e63adea9f5f51b5f39691c34f30afd21080", size = 224245, upload-time = "2026-07-02T13:10:44.47Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/fe/9e/24e2842fef40f35ac82ba3a7719c8023d011bf3bf652d0675316a9d088a1/coverage-7.14.1-cp314-cp314-win_arm64.whl", hash = "sha256:a07891c3f4805442b31b71e84ba3cf29ed1aa9a428284e06deeb4b23e5b46343", size = 222218, upload-time = "2026-05-26T20:41:00.321Z" },
|
{ url = "https://files.pythonhosted.org/packages/79/b8/4d959bf9cc45d0cfed2f4d35cafcab978cdb6ea02eb5100009cd740632a3/coverage-7.15.0-cp314-cp314t-win_arm64.whl", hash = "sha256:aeefb2dd178fe7eee79f0ad25d75855cb35ee9ed472db2c5ea06f5b4fd00cec5", size = 223558, upload-time = "2026-07-02T13:10:46.368Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/0a/1d/ac0a9df5fe31c1e8bdd658074905fc12844a05c1a7e3fdb8417e97c31e23/coverage-7.14.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1101a5ebb083aecb625ebb6209d4105b58f647b093cb2dc8122d7b33f743cfe1", size = 220822, upload-time = "2026-05-26T20:41:02.281Z" },
|
{ url = "https://files.pythonhosted.org/packages/52/30/21b2ad45959cd50e909e02ebac1e30b4ceb7162e91c11d4c570223a458b7/coverage-7.15.0-py3-none-any.whl", hash = "sha256:56da6a4cbe8f7e9e80bd072ca9cefe67d7106a440a7ec06519ec6507ac94ad19", size = 212632, upload-time = "2026-07-02T13:10:48.641Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/32/cf/f964fd9aff20323f9f1a726c97135f8a76bcd87b92dad141a456a43f3c64/coverage-7.14.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:851b9e1e4e8a4608e77c79714b2e77c0970d2ed7202a05e92ae407817481887b", size = 221084, upload-time = "2026-05-26T20:41:04.593Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d8/5e/7e5ef2aba844de2b80d678619fcf0841b42e3f37f16411226f3fe4c1016f/coverage-7.14.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d5b89cdfb2ee051b71e8c3c70bd81a9eff81100f736a269136fe1a68efe00474", size = 262454, upload-time = "2026-05-26T20:41:06.641Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/64/62/75809bded87015cc4935524218a2a8ed8dd1a8498bfed30a2f4f7a4b4d34/coverage-7.14.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0177614a0370f227888b4e436a7c55686d6a9f90eb1ade2b624ba685a1686e86", size = 264578, upload-time = "2026-05-26T20:41:08.556Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f3/42/d33392dc14633525012d2d504fa1a33b05538bf535f5c1d64675e5754b78/coverage-7.14.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2d69af5dea2de76fc485a83032a630523f985198b7e25be901ec60181587b01e", size = 266981, upload-time = "2026-05-26T20:41:10.824Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/2a/49/0157c4428c2aca7f1e09d5565930586fd5ae36f1655f08b0daa7cf1fcae1/coverage-7.14.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:35ab22d91de736e8966b980dc355cbcdd2c6dbbcfe275f9a2991bc8a91b3df65", size = 268112, upload-time = "2026-05-26T20:41:12.966Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/96/26/86b9ce71f4092b1ed325ce1421698081df1286b833400b6836912834d6e0/coverage-7.14.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:357d4e32935c36588aaba057d734fa32428c360c9fc2e4442afbf1b646beee6e", size = 261558, upload-time = "2026-05-26T20:41:15Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/20/4c/c311210c5472cf5401d8422b0d7812cdd520f24417673afabda6c323faca/coverage-7.14.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:51bd64741cc6fa065abd300ede1afe5a5291ece9c31da8b24884deda48bcc3f8", size = 264447, upload-time = "2026-05-26T20:41:17.369Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/fb/71/59513f8710ed3e6b0ac0a050a5b7e977bb9c9e880354863b5d00d8809256/coverage-7.14.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:9132cd363a68a4c3daa7c8704a654b1e39d3360f6f5b8ddd470608a945236c07", size = 262048, upload-time = "2026-05-26T20:41:19.309Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/84/8d/bceed32dc494f5bbf50f775cd2e78ca814953942b5ea28d3c1c3ac316f14/coverage-7.14.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:07c6290b1697b862c0478eab545eec949a0d0e4d6d03497f446d706da3b4f2de", size = 265781, upload-time = "2026-05-26T20:41:21.559Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e7/c5/9348fe40dbfd4991aaf78df2c6c3098bfb2cc834d1fd362a64b4efef855a/coverage-7.14.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:5ea0c297e27133853b4d8a3eb799bff5a2dbd9f2f41537a240d337ac9b4df890", size = 260896, upload-time = "2026-05-26T20:41:23.428Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ca/92/1ea0f03929da7cf87206b1fa24f4c8e9c158be0455481af29ec0a1f3503f/coverage-7.14.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:01b7733daad0237daa01ef80fe2dfceffc911e6a17fa7b55d14aa8214eaaaecd", size = 263214, upload-time = "2026-05-26T20:41:25.419Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f6/a9/b2493c054c0e01a643266742ab45e15744e60743f9260cd930c7142b1124/coverage-7.14.1-cp314-cp314t-win32.whl", hash = "sha256:6adc5a36984624a70bf11d7184e20fa0a49aa7c47ffab43804106a1a695ea22e", size = 223624, upload-time = "2026-05-26T20:41:27.795Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/fc/bd/3e1e6a57fccd2d7c83fcdf338e93ba98eb85c6e877dd34731ac585375490/coverage-7.14.1-cp314-cp314t-win_amd64.whl", hash = "sha256:ddf799247318f34dbcd2efa8c95a8d0642674e926bb1774cf9b63dfd2a389d1c", size = 224728, upload-time = "2026-05-26T20:41:30.098Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/bb/d7/31066cf1d2f0c6c797fce911bcfa01dd35642dc6da992a950256097c5860/coverage-7.14.1-cp314-cp314t-win_arm64.whl", hash = "sha256:145986fe66647eb489f18d9a997567a3fd358584c4b5a808769113abc07466af", size = 222752, upload-time = "2026-05-26T20:41:32.123Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/8a/3c/1a983b9a745d7f83d53f057bcc5bf79ba6a2bbc08266b3f0c7d6fe630c9b/coverage-7.14.1-py3-none-any.whl", hash = "sha256:a252f21c27e38347e60111a3266b03827422a7d5525951aceee313aa68bab1d2", size = 211815, upload-time = "2026-05-26T20:41:34.078Z" },
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.optional-dependencies]
|
[package.optional-dependencies]
|
||||||
@@ -324,75 +337,75 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "msgpack"
|
name = "msgpack"
|
||||||
version = "1.2.0"
|
version = "1.2.1"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/92/23/6139781ca7aadf656fa8e384fa84693ffb13f299e6931b6526427fe5e297/msgpack-1.2.0.tar.gz", hash = "sha256:8e17af38197bf58e7e819041678f6178f4491493f5b8c8580414f40f7c2c3c41", size = 183017, upload-time = "2026-06-11T04:16:10.775Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/31/f9/c0a1c127f9049db9155afc316952ea571720dd01833ff5e4d7e8e6352dbb/msgpack-1.2.1.tar.gz", hash = "sha256:04c721c2c7448767e9e3f2520a475663d8ee0f09c31890f6d2bd70fd636a9647", size = 183960, upload-time = "2026-06-18T16:13:52.594Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/0f/52/fed22bca455ff3ed28c0ee0d1117398b7cb3ce440270050e85b09240fa8d/msgpack-1.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ed8c9495a0f12d17a2b4b69e23f895b88f26aabe40911c86594d3fbddecfff08", size = 82473, upload-time = "2026-06-11T04:14:38.484Z" },
|
{ url = "https://files.pythonhosted.org/packages/5b/16/f70100614b69feb3ade7285f08c9c52d6cda0a5c03f3f5e2facd63acb211/msgpack-1.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8c7b398c56ff125feae96c2737abfec5595f1fa0aa186df60c56040b8accb95c", size = 82926, upload-time = "2026-06-18T16:12:31.531Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/3b/09/0b54d386024a9fa2073135212c11d1e83b059d98459d943d5a82ba9dcdc9/msgpack-1.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d7384859c90b45a28a4b31aa50b49cca84504c9f27df459cea6e072627650dcb", size = 82150, upload-time = "2026-06-11T04:14:39.985Z" },
|
{ url = "https://files.pythonhosted.org/packages/e4/3c/08ecd5cdfe4e2de43aec79062028ad0f7b2d9b1fea5430068c198ba570da/msgpack-1.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1548006a91aa93c5da81f3bdcebc1a0d10cea2d25969754fbe848da622b2b895", size = 82730, upload-time = "2026-06-18T16:12:32.894Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/44/ba/c6310a6f37e9bf9279b492640ec425e6f6e68a94e4cac4782ab518b05d64/msgpack-1.2.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:63b35e8e65f04ff7ad5c9c70885da587c74f51e4b4eb3db624eac6d250e8cf59", size = 398355, upload-time = "2026-06-11T04:14:41.493Z" },
|
{ url = "https://files.pythonhosted.org/packages/19/9f/a70c9cb1a04ecc134005149367dcfe35d167284e8f65035a1e4156ad17b5/msgpack-1.2.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1dabedcd0f23559f3596428c6589c1cd8c6eaed3a0d720795b07b0225d769203", size = 400729, upload-time = "2026-06-18T16:12:34.052Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/d8/1b/f4bad0e9dea608b14d36065c44e347e4b10c0392f92cca441496cc0598ef/msgpack-1.2.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9004c5a02acd3eca4e15e1ae7b461c32e3711105a28b1ad78be2f6facff4c523", size = 405162, upload-time = "2026-06-11T04:14:42.957Z" },
|
{ url = "https://files.pythonhosted.org/packages/fa/7f/5ce020168cf0439041526e95aa068c722c016aee21624e331aeabeee2e8e/msgpack-1.2.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:83efa1c898e0fc5380fc0cabbf75164c52e3b5cbb45973710d75821928380c73", size = 407625, upload-time = "2026-06-18T16:12:35.239Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/63/34/4653bc7f426bd6ce9803f75133aa362232639e5adb8c6b99550107c71ed5/msgpack-1.2.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7e2032dacb0a973fcbf7bd088415a369dae31c5af40e199d234806be22e86765", size = 372720, upload-time = "2026-06-11T04:14:44.532Z" },
|
{ url = "https://files.pythonhosted.org/packages/79/70/fb7668ce0386819303047057aef6fc1da73b584291d9cff82b821744e2ef/msgpack-1.2.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:01e2dd6c9b19d333a00282330cc8a73d38d8dabc306dc5b42cd668c3ac82e833", size = 377891, upload-time = "2026-06-18T16:12:36.684Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/13/3c/8c607e10db2225af52107ffa918280483248363819fecb4437a35a1f4ae2/msgpack-1.2.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c1feb100651fbe4b39826207cb20af065dfbfbfa43b1bafd7eaa2252abf7acfd", size = 390946, upload-time = "2026-06-11T04:14:46.054Z" },
|
{ url = "https://files.pythonhosted.org/packages/3d/dc/9ebe654a73c3aed2e40aa6b52e3c2a02b5f53ef0085fa235a45d5b367f87/msgpack-1.2.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:350cb813d0af6e65d2f7ef0d729f7ff5be5a8bce03665892f43e5883d4ecc1b8", size = 391987, upload-time = "2026-06-18T16:12:37.839Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/96/05/c4cb5fb30569cff4b4c7be4574adddb0faf7faaf3049bbab000b6f07da5b/msgpack-1.2.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:82487709d4c597d252311a65370220675fb1cc859e7da9269a3060c03ac02cf6", size = 374062, upload-time = "2026-06-11T04:14:47.817Z" },
|
{ url = "https://files.pythonhosted.org/packages/42/eb/b67cf64218a2fa25e1c671fe1d3dbb06cbeb973e71bc4b822da079862d0b/msgpack-1.2.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:ee1d9ed27d0497b848923746cf762ed2e7db24f4be7eec8e5cbe8c766aa707b7", size = 374603, upload-time = "2026-06-18T16:12:39.221Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/40/d7/b51b11e58277e6b678ba5a2f6608f88fdb0778973391a39d7f1a385f5bde/msgpack-1.2.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0268c67a74f5f913f545a0fdbbfaa3f6ebcf23b4c3209bb99704a2ea87e13f90", size = 405458, upload-time = "2026-06-11T04:14:49.618Z" },
|
{ url = "https://files.pythonhosted.org/packages/a2/2e/9ee200cde32fd1a0101b4006202fde554c1860adfb9bf7bff31ea4c08df8/msgpack-1.2.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:633727297ed063441fd1cda2288865487f33ad14eeb8831afb5f0c396a62cfce", size = 405121, upload-time = "2026-06-18T16:12:40.524Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/2c/0e/9eca2961be302a6fc77a3fcb15faec749e325c9f0a8fe9c4c4576fc2cad5/msgpack-1.2.0-cp310-cp310-win32.whl", hash = "sha256:7df87173b0e13ddd134919731f13525dbbf75204145597decf1cb86887ebb492", size = 64010, upload-time = "2026-06-11T04:14:51.071Z" },
|
{ url = "https://files.pythonhosted.org/packages/43/b6/f10117be7ca7a51e8feed699a907b8e663a8cd66e115ae6b4fb30cc7945c/msgpack-1.2.1-cp310-cp310-win32.whl", hash = "sha256:298872ecf9e61950f1c6af4ca969b859ee91783bb920ef6e6172697d0c8aad74", size = 64088, upload-time = "2026-06-18T16:12:41.762Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/e7/e3/55b14ae13ed056ed35364ff71144c6a12af25227c20093045a945d08273a/msgpack-1.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:6371edb47788fbfd8a22016f9a97b5616dd9849bc50abcbb8e82d38f71efa096", size = 69863, upload-time = "2026-06-11T04:14:52.376Z" },
|
{ url = "https://files.pythonhosted.org/packages/ba/93/89976c696fb0224662239d952c47b4d1661b34d79a332ef5584facaa8579/msgpack-1.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:2ff164c1b0bcb740b073b99e945234d0212852fa378e44a208c425379140dbeb", size = 70113, upload-time = "2026-06-18T16:12:42.78Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/ee/23/35de3182a647fcc84ab304160169edfa5dac7bbd8913fbed0a505ddc0d55/msgpack-1.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ec35cd3f127f50806aa10c3f74bf27b749f13ddf1d2217964ada8f38042d1653", size = 82368, upload-time = "2026-06-11T04:14:53.57Z" },
|
{ url = "https://files.pythonhosted.org/packages/f4/6b/e9b1cdc042c4458801d2545ed782a95f3d6ba8e270cce8745b8603c7f748/msgpack-1.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:29a3f6e9667868429d8240dfd063ea5ffdc1321c13d783aa23827a38de0dcb22", size = 82812, upload-time = "2026-06-18T16:12:45.022Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/aa/79/8d9bfdab933b1c7a02aba9518605a81aa30d38e9efd4915ec1a6b2d55778/msgpack-1.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:317eb298297121bfad9173d748124a04a36af27b6ac39c2bbc1db1ce57608dcf", size = 82095, upload-time = "2026-06-11T04:14:54.784Z" },
|
{ url = "https://files.pythonhosted.org/packages/0c/3a/dd518a1bf78ed1e9ad8afe57307c079a00eafe4b3068932a27ca1ea56b4f/msgpack-1.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:aded5bdf32609dc7987a49bbbd15a8ef096193f96dd8bbeb791de729e650acf5", size = 82739, upload-time = "2026-06-18T16:12:46.025Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/d2/e1/b5accbc1354edbcee107fb35ec247db0547e91c3f90e4fabdeaee500a5a6/msgpack-1.2.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:50fe6434de89073273026dd032a62e8b63f8857a261d7a2df5b07c9e72f3a8f7", size = 413818, upload-time = "2026-06-11T04:14:56.1Z" },
|
{ url = "https://files.pythonhosted.org/packages/70/e0/7ba9e1542bf0771a27b8b37c1316e3f95ae9d748fd765284655c476ad4ef/msgpack-1.2.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:146ee4e9ce80b365c6d4c47073da9da7bcec473e58194ceee5dd7620ace77e06", size = 414233, upload-time = "2026-06-18T16:12:47.029Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/82/31/1141cbbf7118d525834f20dcd614d1b85f1f2ffd33bc2a5ce710e6dd2516/msgpack-1.2.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:106c6d333ff3d4eda075b7d4b9695d1752c5bcc635e40d0dbaf4e276c9ed80e1", size = 423790, upload-time = "2026-06-11T04:14:57.509Z" },
|
{ url = "https://files.pythonhosted.org/packages/03/8d/671d81534ea0e2b0e8a121be100020da09eb78861fe3aa8f3ef7dcd3bed1/msgpack-1.2.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a28d076ca7c82b9c8728ad90b7147489449557038bed50e4241eb832395169b4", size = 423843, upload-time = "2026-06-18T16:12:48.19Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/04/e7/9582f2bd4d7546139fe297740de49bd1f7ef2d195eb0bb9fa5efeee88158/msgpack-1.2.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:67055a611e871cb1bd0acb732f2e9f64ca8155ca0bba1d0a5bb362e7209e5541", size = 387521, upload-time = "2026-06-11T04:14:59.08Z" },
|
{ url = "https://files.pythonhosted.org/packages/d2/b6/e5c737515ed1f166664b87601b532f58cbb73d8aa6a90b99f7c2c5037e8e/msgpack-1.2.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7d31c0ac0c640f877804c67cb2bc9f4e23dc2db97e96c2e67fa27d38283b41f8", size = 390772, upload-time = "2026-06-18T16:12:49.624Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/7d/12/5aadd08ff068bfd42e2ac0be6a20aa9819965df8622e87c1f0c6119c1c22/msgpack-1.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ceec7f8e633d5a4b4a32b0416bef90ee3cd1017ea36247f705e523072e576119", size = 406324, upload-time = "2026-06-11T04:15:00.686Z" },
|
{ url = "https://files.pythonhosted.org/packages/a8/46/62ed8c2e87d7021eab19921594d961ef3aa3794eec76c716dc30f3bfd433/msgpack-1.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8ff92d7feeaf5bc26c51495b69e2f99ed97ab79346fb6555f44be7dd2ac6503b", size = 409559, upload-time = "2026-06-18T16:12:50.936Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/39/ee/3041564f0cc4c2fe7c53315aec0edf3d84807fc9b9ea714e6ac07dbdb1db/msgpack-1.2.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7ec5851160a3c2c0f77d68ddec620318cd8e7d88d94f9c058190e8ce0dfa1d31", size = 384242, upload-time = "2026-06-11T04:15:02.121Z" },
|
{ url = "https://files.pythonhosted.org/packages/70/ff/59aa3887b860bbf43532835e192b1c388a17590d6068ae4f8b2bc74c906e/msgpack-1.2.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:779197a6513bab3c3632265e3d0f7cb3227e62510841a6f34f1eaa37efbb345e", size = 387838, upload-time = "2026-06-18T16:12:52.161Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/5d/d4/de94b3dbc266229f4c2ce84485eeb221220351b7f1931029e875995bb232/msgpack-1.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:dd7140f7b09dbe1984a0dff3189375d840247e3e4cf4ac45c5a499b3b599c8d2", size = 420392, upload-time = "2026-06-11T04:15:03.692Z" },
|
{ url = "https://files.pythonhosted.org/packages/09/11/f8563e471093420cf6478cb3271a0175d8402b82d879783d4035d2d03360/msgpack-1.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:67f6dd22fa72a93752643f07889796d62739a13415ee630169a8ce764f86cf9f", size = 421732, upload-time = "2026-06-18T16:12:53.556Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/f7/5d/c4a3fde69a292eecb202caaa87c29df7728644a65118614b821bcaddc05a/msgpack-1.2.0-cp311-cp311-win32.whl", hash = "sha256:cbfd54018d386da0951c7a2be13de0f58559d251313e613b2155e52ed1cbd8f1", size = 63976, upload-time = "2026-06-11T04:15:05.355Z" },
|
{ url = "https://files.pythonhosted.org/packages/57/cf/e673683c4c6c90c1022b24c65af4b03eda72b182a1176ef6449069d66acc/msgpack-1.2.1-cp311-cp311-win32.whl", hash = "sha256:91054a783328e0ea7954b8771095705c8d2243b814743fbaadf14552c9c52c5d", size = 64091, upload-time = "2026-06-18T16:12:54.821Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/18/fa/df47f83115375e7717c985265a30f3ba096c5331518e28fb647b55c46d31/msgpack-1.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:653373c4614c31463ba486a67776e4bb396af289921bd5353e209534b71467fa", size = 70273, upload-time = "2026-06-11T04:15:06.529Z" },
|
{ url = "https://files.pythonhosted.org/packages/3f/07/ca212739d179f9083bff2c7c08c24101c3555a334fadc2b876b18768a3ae/msgpack-1.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2eda0b7ebb1283a98d3e4492ac933c8af6aff59fd3df1c3ed024f536af4b1dc8", size = 70462, upload-time = "2026-06-18T16:12:55.898Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/54/d1/ffd02e54c064aa73b6b53aa08171f92dc406727077ff275d7050c6aca28a/msgpack-1.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:7a260aea1e5e7d6c7f1d9284c7360d29021627b61dc4dd7df144b81210810537", size = 64783, upload-time = "2026-06-11T04:15:07.677Z" },
|
{ url = "https://files.pythonhosted.org/packages/6d/be/6798347b425e26f35db82e69dd83c09716c856a3714e7bffc4c0860fd830/msgpack-1.2.1-cp311-cp311-win_arm64.whl", hash = "sha256:6ee967f7c7e1df2890c671ff2ee51a28ded0efc95da3e507176dee881ce36c66", size = 65059, upload-time = "2026-06-18T16:12:57.053Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/44/07/dcb13f37e670257c8d0e944f116c799c34ac6968ecb48c83619f7e91d8b5/msgpack-1.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e2d6047ccd11a12c96a69f2bfe026471abef67334c3d0494a93e5310e45140a2", size = 82888, upload-time = "2026-06-11T04:15:08.992Z" },
|
{ url = "https://files.pythonhosted.org/packages/bc/dd/9e8cbd8f5582ca4b590336f2b91ee5662f6a6ca562b565abaf696a0f81ff/msgpack-1.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2ef59c659f289eddf8aa6623823f19fa2f40a4029266889eac7a2505dd210c35", size = 83531, upload-time = "2026-06-18T16:12:58.249Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/84/5f/6643b2a6a36ca4bc73c7674831be1d4d581cceecc7eb019dba1915951739/msgpack-1.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0347e3ac0dfee99086d3b68fe959da3f5f657c0019ddbaeaaa259a85f8603422", size = 82223, upload-time = "2026-06-11T04:15:10.182Z" },
|
{ url = "https://files.pythonhosted.org/packages/50/2e/ebdb85a8da151397a2790363676b7ed7c125924fe618e4c6d8befb0cc62c/msgpack-1.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d3567748a5107cb40cdf66a275430c2f87c07777698f4bfd25c35f44d533258c", size = 82657, upload-time = "2026-06-18T16:12:59.396Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/2c/c8/9e1668b9897358e5ab39a18142e38be3cf15807e643757782da9f4a53cb3/msgpack-1.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:25552ff1f2ff3dc8333e27eabb94f702da5929ed0e07969688194a3e9f12e151", size = 409700, upload-time = "2026-06-11T04:15:11.441Z" },
|
{ url = "https://files.pythonhosted.org/packages/26/aa/753ad8b007b464e1d8aa0c8e650b9c5f4f725e658fc5ac8a7635c55b7f6e/msgpack-1.2.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60926b75d00c8e816ef98f3034f484a8bc64242d66839cef4cf7e503142316a0", size = 410634, upload-time = "2026-06-18T16:13:00.383Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/38/ed/b7728573156d70b6b094233b0f38d876fc37340826cf852347ec2c7ca8ca/msgpack-1.2.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0d94420d9d52c56568159a69200af7e45eadb29615fa9d09fada140de1c38c7", size = 420090, upload-time = "2026-06-11T04:15:12.868Z" },
|
{ url = "https://files.pythonhosted.org/packages/6a/fd/6adabd4f6d5e686f97dd02ce7fce3fe4cf672cbac36b8f67ff4040e8ad8b/msgpack-1.2.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:020e881a764b20d8d7ca1a54fc01b8175519d108e3c3f194fddc200bda95951a", size = 419989, upload-time = "2026-06-18T16:13:01.776Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/3f/f7/5ea755a89868c04f9cdf6d96d2d99da4b3d198af10e76a6082dd0fceccc0/msgpack-1.2.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d16e1f2db4a9eebc07b7cc91898d71e710f2eed8358711a605fee802caff8923", size = 378538, upload-time = "2026-06-11T04:15:14.511Z" },
|
{ url = "https://files.pythonhosted.org/packages/5a/cc/85039b7b0eb168aaad7383a23c97e291a11f08351cb45a606ce865e4e3f1/msgpack-1.2.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4202c74688ca06591f78cb18988228bd4cca2cc75d57b60008372892d2f1e6e6", size = 377544, upload-time = "2026-06-18T16:13:03.637Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/80/2d/126e59332a439c94ffd682c38ca0102b23480e2784b3dac48d8959b0bbac/msgpack-1.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e9cb2e700e85f1e27bbb5c9de6cc1c9a4bc5ac64d5404bdcbcb37a0dc7a947a3", size = 399468, upload-time = "2026-06-11T04:15:16.133Z" },
|
{ url = "https://files.pythonhosted.org/packages/ed/bf/35963899493b32030c85fc513b723ae66144ac70c11ebc52e889e16e3d99/msgpack-1.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8b267ce94efb76fbd1b3373511420074ee3187f0f7811bf394531de13294735a", size = 400842, upload-time = "2026-06-18T16:13:05.012Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/da/f9/7abcef683a0ad2e5ab3a4940344aad9f20cdf1f42057ecb0982cf55085d6/msgpack-1.2.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:717d0b166dd176a5f786aeafff081f6439680acf5af193eb63e6266c12b04d3d", size = 374212, upload-time = "2026-06-11T04:15:17.536Z" },
|
{ url = "https://files.pythonhosted.org/packages/a6/df/8e2ac970c8f99264cd9997d1c73df5466bc19da3301d7dc5500862a9b089/msgpack-1.2.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:e4f1d0f8f98ade9634e01fb704a408f9336c0a8f1117b369f5db83dc7551d8b1", size = 374108, upload-time = "2026-06-18T16:13:06.232Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/27/23/2d62cf0e971678e96f8a3cfa9bd77fb719ddb98da73790f63c53fd847ad8/msgpack-1.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e87c7a21654d18111eb1a89bd5c42baba42e61887365d9e89585e112b4203f9e", size = 414361, upload-time = "2026-06-11T04:15:18.99Z" },
|
{ url = "https://files.pythonhosted.org/packages/17/dd/fa8bd265110dfa51c20cb529f9e6d240a16fafe7e645004c6af2d01353ba/msgpack-1.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f02cf17a6ca1abe29b5f980644f7551f94d71f2011509b26d8625ce038f0df64", size = 414939, upload-time = "2026-06-18T16:13:07.478Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/32/fb/f5c153f614037aaf802d291a4653ba1bb731f56feacba886f7c21c109e56/msgpack-1.2.0-cp312-cp312-win32.whl", hash = "sha256:967e0c891f5f23ab65762f2e5dc95922759c79f1ef99ef4c7e1fdd863e0d0af9", size = 64389, upload-time = "2026-06-11T04:15:20.237Z" },
|
{ url = "https://files.pythonhosted.org/packages/2e/b9/8377a5ad8953fc0437c70cc98d9ae29f27fe5ac5109fbec0812085865735/msgpack-1.2.1-cp312-cp312-win32.whl", hash = "sha256:0c0d9802354507bcba62af19c17918e3eb437cc25e6f50657d511b5856a77aac", size = 64504, upload-time = "2026-06-18T16:13:08.822Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/90/af/8aafce6e5544b43b84cb670aca40c8bea7eb5ae8f42bfcbdc7098739987a/msgpack-1.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:6c23e33cee28dcffa112ae205661da4636fd7b06bd9ad1559a890623b92d060b", size = 71185, upload-time = "2026-06-11T04:15:21.51Z" },
|
{ url = "https://files.pythonhosted.org/packages/57/7f/ce1e377df7e62461fefd9eb23bfb93a4a523f40a517b377b8f844d836828/msgpack-1.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:5c24aa15d5963051e1a5c62b12c50cd705992502b5ec1f3bece6046f33c9fc24", size = 71421, upload-time = "2026-06-18T16:13:09.828Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/ba/08/9cc94be1fc1fe3d1379d439326259aef0344274f64623a8138feb54dff68/msgpack-1.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:6eeb771571f63f68045433b1a35c0256b946f31ed62f006997e40b8ad8b735af", size = 64481, upload-time = "2026-06-11T04:15:22.639Z" },
|
{ url = "https://files.pythonhosted.org/packages/8f/32/ebfe84c9929f08f188d56c7a2fd913406a9ddad76a634697c1c43b8112e6/msgpack-1.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:4227224aaec8f7fbcbfbd4272319347b2bb4030366502600f8c45588c5187b07", size = 64775, upload-time = "2026-06-18T16:13:11.056Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/7d/26/2902c6946ab5c8fe1e46e40842dfc32b8824464ad5cd4725364fd83f7a58/msgpack-1.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3a1d30df1f302f2b7a7404afbac2ab76d510036c34cf34dffb01f704a7288e45", size = 82621, upload-time = "2026-06-11T04:15:23.844Z" },
|
{ url = "https://files.pythonhosted.org/packages/b0/ac/dcddcab6f6c20ecb387ca5e980371cdb3f87ff69aeca388be97eebc4c074/msgpack-1.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0a70e3cf2804a300d921bb0940426e35f4e489a23adfb77a808892241db0a064", size = 83151, upload-time = "2026-06-18T16:13:12.173Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/c9/59/7e6b812629d2f919e586041bffc130e1af32079f71bb20699eed54ed6d92/msgpack-1.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:581e317112260d8ca488d490cad9290a5682276f309c41c7de237a85ed8799c8", size = 81866, upload-time = "2026-06-11T04:15:25.032Z" },
|
{ url = "https://files.pythonhosted.org/packages/64/71/fbcfa83a1d6a9c6091942d1cfd070962244664b87427a9a49a6897b1b219/msgpack-1.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:491cc39455ca765fad51fb451bf2915eb2cf41192ab5801ce8d67c1d614fe056", size = 82351, upload-time = "2026-06-18T16:13:13.194Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/31/13/8c291196e60aafdbae38f482205d79432297749ac5d412fe638154fb6f1d/msgpack-1.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c6827d12eacc16873eba62408a1b7bbe8ecfb4a8f7ed78a631ae9bae6ad43cf2", size = 405618, upload-time = "2026-06-11T04:15:26.235Z" },
|
{ url = "https://files.pythonhosted.org/packages/e3/10/ddf7b06db879e8792d13934ddda09ff20bd2a583fd84c9b59aae9b0e650b/msgpack-1.2.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f310233ef7fb9c14e201c93639fe5f5260b005f56f0b29048e999c30935596cc", size = 407518, upload-time = "2026-06-18T16:13:14.233Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/fb/63/68f5d0ea81e167db5f59ddb94dc6f837667062113feff1c73fabf8907061/msgpack-1.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a186027e4279efa4c8bf06ce30605498d7d0d3af0fba0b9799dce85a3fd4a93c", size = 416468, upload-time = "2026-06-11T04:15:27.732Z" },
|
{ url = "https://files.pythonhosted.org/packages/79/d3/36a46a8ed992b781acbc05928bd5bee3c810cb0c3563bf81a7b0c04a1a76/msgpack-1.2.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:787c9bebb5833e8f6fc8abca3c0597683d8d87f56a8842b6b89c75a5f3176e2d", size = 416405, upload-time = "2026-06-18T16:13:15.435Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/73/58/567dddf5c5a2790f673bcd7d80c83466d68e5ee9a9674ebca3db8101c0c8/msgpack-1.2.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a96142c14a11cf1a509e8b9aaf72858a3b742b7613e095ce646913e88ce7bd99", size = 374464, upload-time = "2026-06-11T04:15:29.286Z" },
|
{ url = "https://files.pythonhosted.org/packages/f9/84/e8e9598b557c0ba6ddae901a73780a4c75ac667dddf59414b1e56a42fb34/msgpack-1.2.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dc871b997a9370d855b7394465f2f350e847a5b806dd38dcc9c989e7d87da155", size = 376257, upload-time = "2026-06-18T16:13:17.022Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/0d/30/0c2342fc9092e4498045f5f60bca6ccbe4f4d87789778c2300e6fd6efe82/msgpack-1.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:50c220579b68a6085b95408b2eaa486b259520f55d8e363ddc9b5d7ba5a6ac6d", size = 395879, upload-time = "2026-06-11T04:15:30.973Z" },
|
{ url = "https://files.pythonhosted.org/packages/40/16/738fe6d875ad7e2a9429c165322a4ec088f4f273cdfae63d96a89c467961/msgpack-1.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:85f57e960d877f2977f6430896191b04a21f8901b3b4baf2e4604329f4db5402", size = 397469, upload-time = "2026-06-18T16:13:18.287Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/b9/11/9565b29b58ce3c33e177b490478b7aaeb8f726ecaaeda26d815893c1db5a/msgpack-1.2.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:4dcb9d12ab100ecacdfaaf37a3d72fe8392eacc7054afc1916b12d1b747c8446", size = 371749, upload-time = "2026-06-11T04:15:32.418Z" },
|
{ url = "https://files.pythonhosted.org/packages/ca/be/6d5952df75a7f24f35833af764c3a6860780364cb3a0030beb8099e1b2b4/msgpack-1.2.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:1233ee2dd0cefba127583de50ea654677277047d238303521db35def3d7b2e7c", size = 372802, upload-time = "2026-06-18T16:13:19.685Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/f2/da/7bade19d60b73e2ef73fb76aaf4504c112a70cb760951b7202a0c64b5111/msgpack-1.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a804727188ab0ebb237fadb303b743f04925a69d8c3247292d1e33e679767c15", size = 410416, upload-time = "2026-06-11T04:15:34.053Z" },
|
{ url = "https://files.pythonhosted.org/packages/e1/39/e2ef7dbf0473bcb8dc7c50bf782a892d67414877b63e47fc88eb189ef5e6/msgpack-1.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e3dc2feb0876209d9c38aa56cb1de169bd6c4348f1aa48271f241226590993e6", size = 411273, upload-time = "2026-06-18T16:13:21.028Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/6d/14/c0c619571c02432208a5977a8dbdd3fc65fe1369f8226ca4b6d08cca87d8/msgpack-1.2.0-cp313-cp313-win32.whl", hash = "sha256:1a1ac6ae1fe23298f79380e7b144c8a454e5d05616b0096584f353ba2d750114", size = 64357, upload-time = "2026-06-11T04:15:35.535Z" },
|
{ url = "https://files.pythonhosted.org/packages/ef/c5/133f4512a56e983a93445c836c9d94d88f3bc2e0980ff4b9e577bd8416ce/msgpack-1.2.1-cp313-cp313-win32.whl", hash = "sha256:6d09badf350af2be9d189184e04e64cf54ad93569ab3d96fca58bd3e84aad707", size = 64471, upload-time = "2026-06-18T16:13:22.293Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/50/a5/de06718460909aa965737fec4cfe8a15dedc6544a8c55feeb6956fa0d6e3/msgpack-1.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:1c3c80949d79578f9dc85fd9fb91edfe6694e8a729cd5744634d59d8455fdde3", size = 71057, upload-time = "2026-06-11T04:15:36.83Z" },
|
{ url = "https://files.pythonhosted.org/packages/e2/98/577e10b055096a7dd40732358cabaf7180a20c79ed1dcdbb618e4b9deac7/msgpack-1.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:33f14fba63278b714efe6ad07e50ea5f03d91537aa6a1c5f1ceca4cf44013ca9", size = 71274, upload-time = "2026-06-18T16:13:23.455Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/c7/52/73446b0141c94a856e22b787c56709c0815fc34f185326577e15b26d8cfe/msgpack-1.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:fcf8f76fa587c2395fd0057c7232dbf071241f9ad280b235adb7ab585289989e", size = 64490, upload-time = "2026-06-11T04:15:38.001Z" },
|
{ url = "https://files.pythonhosted.org/packages/ba/ee/0c0048e7cfbef23c6a94791b8959ab28155232e7956de8a305b5ff588f05/msgpack-1.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:afc5febcd4c99effbc02b528e49d6fd0760b2b7d48c05239e345a5fa6e743d9a", size = 64795, upload-time = "2026-06-18T16:13:24.687Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/35/3d/a7e3cdafa8c0cf36c81e2fa848ec4d30cf089459af45b390ad03f9ce6f49/msgpack-1.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f854fa1a8b55d75d82ef9a905d9cdbeffdf7897c088f6020bd221867da5e56a5", size = 83032, upload-time = "2026-06-11T04:15:39.38Z" },
|
{ url = "https://files.pythonhosted.org/packages/77/58/cce442852c6b9e1639c7c8ac8fd9143121cb32dab0f308df4d1426a8eb9c/msgpack-1.2.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:05f340e47e7e47d2da8db9b53e1bb1d294369e9ef45a747441309f6650b8351d", size = 83610, upload-time = "2026-06-18T16:13:25.724Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/ca/aa/53ddfba0e347cc4b484e95f629c5850b9e800ca8390c91ffc604407acf87/msgpack-1.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e90df581f80f53b372d5d9d9349078d729851a3a0d0bd74f53ccb598d01e45b8", size = 82600, upload-time = "2026-06-11T04:15:40.609Z" },
|
{ url = "https://files.pythonhosted.org/packages/60/5c/15b4c7a0182f75ffa90751958ba36a9c01cafee367d49a3edc10ed140b01/msgpack-1.2.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:810b916696c86ef0deb3b74588480224df4c1b071136c34183e4a2a4284d7ac7", size = 83138, upload-time = "2026-06-18T16:13:26.781Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/59/fd/e64c2c776e6dbad0af3c963fe0c0dd1ee1ba09efac478b233ab1db41868f/msgpack-1.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b276ed50d8ac75d1f134a433ae79af8557d0fa25ee5b4737da533dfc2ce382e8", size = 404342, upload-time = "2026-06-11T04:15:41.87Z" },
|
{ url = "https://files.pythonhosted.org/packages/b8/a6/99e58722feaffc5f2fbcc0c8c0d1451ab9f84097f7af87291b46af2390f4/msgpack-1.2.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ca0dacff965c47afdc3749a8469d7302a8f801d6a28758d55120d75e66ce6889", size = 406090, upload-time = "2026-06-18T16:13:28.072Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/1b/60/fb9a08e6ccba882dfd370a5837fe3a07572938fdfe954f0f17fdf3e574b9/msgpack-1.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:544d972459c92aa32e63b800d07c2d9cf2734a3be29cee3a0b478a622850e9f5", size = 412351, upload-time = "2026-06-11T04:15:43.253Z" },
|
{ url = "https://files.pythonhosted.org/packages/19/03/8c63e8cf52958534ef688625965ab04c269a6cadd8caef16758b380a821a/msgpack-1.2.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e2bf9280bceb5efca998435904b5d3e9fdbcc11d90dc9df30aec7973252b720", size = 412106, upload-time = "2026-06-18T16:13:29.427Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/37/4d/df5c575c274fedc68ac9c6c61d045161899efad2afcdc25138efa7edde69/msgpack-1.2.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a070147cc2cf6b8a891734e0f5c8fe8f70ed8739ab30ba140b058005a6e86af4", size = 373331, upload-time = "2026-06-11T04:15:44.754Z" },
|
{ url = "https://files.pythonhosted.org/packages/63/d2/155d9e71b40e41fd934bc0c48b9b2770f22263e1ac20aad8e29fdca7be3f/msgpack-1.2.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:aa6c4be5d1c02a42b066ca6ddb71adf36432868fdcdb6ee87e634e86e0674190", size = 374851, upload-time = "2026-06-18T16:13:30.631Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/7d/a4/c8b98f8191e985ed2003d87664ce3c95cca41db5d0cf6bf4f54327d32ec8/msgpack-1.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7685e23b0f51745a751629c31713fbefdef8896b31b2bb38299dfa4ae6c0740c", size = 394654, upload-time = "2026-06-11T04:15:46.423Z" },
|
{ url = "https://files.pythonhosted.org/packages/98/48/deaf2326262a8d5ea3295ce9649912ecd3f551ba7ec8e33c665d2ba583f3/msgpack-1.2.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec0e675d59150a6269ddc9139087c722292664a37d071a849c05c473350f1f2d", size = 396168, upload-time = "2026-06-18T16:13:31.977Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/d4/49/76f036720a602ea24428cfec5ec806f2487c0380b1bff0a2aa3094e15f87/msgpack-1.2.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:b9204daeee8d91a7ae5acf2d2a8e3983be9a3025f38aa21bfaefbd7eea84a7dc", size = 370624, upload-time = "2026-06-11T04:15:48.062Z" },
|
{ url = "https://files.pythonhosted.org/packages/10/2a/b4410f906c2ec0008f1608d3ab5143afc3ad3f4e6da0fed3ea2231d0bef4/msgpack-1.2.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:dd3bfe82d53edfe4b7fc9a7ec9761e23a7a5b1dac22264505af428253c29ed24", size = 371959, upload-time = "2026-06-18T16:13:33.282Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/9f/38/40af3d29232833705a43b0fce0d07425cc280a7b92ab2b29932425b40df4/msgpack-1.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:bfc057248609742ebbabf6bcd27fea4fd99c4980584e613c168c9b002318298f", size = 408038, upload-time = "2026-06-11T04:15:49.669Z" },
|
{ url = "https://files.pythonhosted.org/packages/59/86/1edc67270099a528fa2093ea60fe191233cd238e4bd30cfacf7db79fc959/msgpack-1.2.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5ad5467fc3f68b5468e06c5f788d712e9f8ffc8b0cd1bcb160c105c1ee92dae7", size = 408457, upload-time = "2026-06-18T16:13:34.567Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/30/b2/f140ca450524dff4d8d0eb81eb9ed75f8f3e0b1f12e49c5b01617cfa0b1c/msgpack-1.2.0-cp314-cp314-win32.whl", hash = "sha256:a3faa7edf2388337ae849239878e92f0298b4dab4488e4f1834062f9d0c410c9", size = 65823, upload-time = "2026-06-11T04:15:51.062Z" },
|
{ url = "https://files.pythonhosted.org/packages/82/90/8b630fef07d8c5ab457b71ff2c217910c83d333c7a68472c186e87cc504a/msgpack-1.2.1-cp314-cp314-win32.whl", hash = "sha256:98b58bdb89c46190e4609bb36abe17c6d4105ad13f9c5f8f6f64d320f8ced3fb", size = 65942, upload-time = "2026-06-18T16:13:36.056Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/4d/13/6517bf966b841c7675ded30701a068ce141f3e698a27aaa35c702d8e078b/msgpack-1.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:1a3effc392a57744e4681e55d05f97d5ee7b598747d718340a9b4b8a970c40e1", size = 72484, upload-time = "2026-06-11T04:15:52.289Z" },
|
{ url = "https://files.pythonhosted.org/packages/16/f1/467b81e98b24dd3885d7b1857728797b4ffc76a7a7483af4fb321a07de3c/msgpack-1.2.1-cp314-cp314-win_amd64.whl", hash = "sha256:74847557e28ce71bd3c438a447ca90e4b507e997ddbdef8a12a7b283b86c156b", size = 72627, upload-time = "2026-06-18T16:13:37.079Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/45/8c/1d948420fdaa24de4efdb8012a6a5bebe09c82ee002b8c2ca745e9917f1f/msgpack-1.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:56a318f7df6bec7b40928d6b0519961f20a510d8baabf6baa393a70444588f0a", size = 66657, upload-time = "2026-06-11T04:15:53.583Z" },
|
{ url = "https://files.pythonhosted.org/packages/a7/1d/5d8c4c89985feb6acefb82a09e501c60392261856d2408d20bfe4f0360b1/msgpack-1.2.1-cp314-cp314-win_arm64.whl", hash = "sha256:b50b727bd652bdc37d950336c848ef20ec54a4cafc38dce19b1cd86ad625d0f7", size = 66908, upload-time = "2026-06-18T16:13:38.23Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/39/16/1674faa1b7bddc19e79b465fd8e88e2cf4e3f7cae90723740701e8541068/msgpack-1.2.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:afa4a65ab2097795e771a74a3a81ea49534aaeba874eaf426a3332268e045ae6", size = 86093, upload-time = "2026-06-11T04:15:54.98Z" },
|
{ url = "https://files.pythonhosted.org/packages/1b/02/ad2afb678b4de94496cd432b581759b756a92c1192d8c767edd6b132efdc/msgpack-1.2.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:8d00f177ca88a77c1cf848d204a38f249751650b601cb6532acc68805d8a8273", size = 86000, upload-time = "2026-06-18T16:13:39.44Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/dd/24/f241bcfdd9e96b2246289357c5a5e5a496189fd41c5844bee802c116aac7/msgpack-1.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:409550770632bb28daa70a11d0ed5763f7db38f40b06f7db9f11dd2794d01102", size = 86372, upload-time = "2026-06-11T04:15:56.381Z" },
|
{ url = "https://files.pythonhosted.org/packages/54/74/0b797484013128837f3b1cbb6cea019277c4de4e377dc512b4d9a0f92940/msgpack-1.2.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5bb9c386f0a329c035ddbab4b72d1028bf9627add8dda41070288563d57ed1b1", size = 86544, upload-time = "2026-06-18T16:13:40.447Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/94/c9/57f8ab98a1b21808c27b6dd6029053e0a796ffbb9b371e460dbe997011a9/msgpack-1.2.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf47e3cd11ce044965a9736a322afdd390b31ed602d1c1b10211d1a841f1d587", size = 428207, upload-time = "2026-06-11T04:15:57.739Z" },
|
{ url = "https://files.pythonhosted.org/packages/a9/b4/b774d7eb95561739907fec675582f83203cf41c597a418c2589b4bfb8e9d/msgpack-1.2.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:20466cca18c49c7292a8984bc15d65857b171e7264bdcb5f96baf8be238791fc", size = 427661, upload-time = "2026-06-18T16:13:41.574Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/17/6b/4fd4aa739f131ded751ca7167c8ee87d2aab32506ebbeea893b60b51d343/msgpack-1.2.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:204bc9f5d6e59c1718c0a4a84fc8ff71b5b4562faac257c1a68bca611ecf9b72", size = 426082, upload-time = "2026-06-11T04:15:59.356Z" },
|
{ url = "https://files.pythonhosted.org/packages/b2/f9/3243191dc9937e00756c8bc1b0272fed8f23758e43df2a3b46f533e5090f/msgpack-1.2.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:196300e7e5d6e74d50f1607ab9c06c4a1484c383cd22defd727902591f7e8dde", size = 426375, upload-time = "2026-06-18T16:13:42.936Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/f9/00/db88e9a08fcd6513decaad06cbd5c168142bc3e662fb2f1aca3a563b7aa1/msgpack-1.2.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:610154307b27267266368bc1d1c7bb8aeb71da7be9356d403cb2442d9e6399f5", size = 378355, upload-time = "2026-06-11T04:16:00.916Z" },
|
{ url = "https://files.pythonhosted.org/packages/23/c7/1693111db9944ba4ad4b67a1e788400d78a0b6af7a6523dc7e4e58f8274b/msgpack-1.2.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:575957e79cd51903a4e8495a242442949641e08f1efd5197b43bebd3ea7682b4", size = 380495, upload-time = "2026-06-18T16:13:44.306Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/54/84/eee4dd703d7a600cf46159d621c070b0b9468cf3dbade4ea8272bf5232a4/msgpack-1.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6799f157bb63e79f11e2e590cfdb28423fc18dd60c270c3914b5b4586ae36f7e", size = 410848, upload-time = "2026-06-11T04:16:02.745Z" },
|
{ url = "https://files.pythonhosted.org/packages/3e/2b/92f86956a0c13e8662f7e2ad630c4eb4db07497b967589bd5245e018b2c1/msgpack-1.2.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8c2ed1e48cc0f460bf3c7780e7137ff21a4e18433451916f2442c1b21036cd7d", size = 410897, upload-time = "2026-06-18T16:13:45.629Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/12/0a/195e2c549fd4631eb7f157d016ff15a10c4c1cf82b6d0a9b1edaef5174b1/msgpack-1.2.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:72bd844902cf0a5ac3af2ef742f253cd0b1e5bcd184f49b4fb9a6a1f7bf305e8", size = 376152, upload-time = "2026-06-11T04:16:04.041Z" },
|
{ url = "https://files.pythonhosted.org/packages/da/ea/1479f72d200313a76fc2f823a79d1e07ed052ab7b8a0280640aa7b95de42/msgpack-1.2.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:5f6277e5f783c36786a145e0247fc189a03f35f84b251646e53592d2bc12b355", size = 378519, upload-time = "2026-06-18T16:13:46.998Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/45/9b/bdd143fa79baec411dc658f5686fed680a18b36fcea5fccb6af1b8c7d832/msgpack-1.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3c0bd450f78d0d81722c80da6cdbf674a856967870a9db2f6c4debc4d8b3c67c", size = 417061, upload-time = "2026-06-11T04:16:05.63Z" },
|
{ url = "https://files.pythonhosted.org/packages/f5/4d/fa006060ffa1011d32bfae826fe766fe73e02982183601633b7121058ab3/msgpack-1.2.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f9389552ecf4784886345ead0647e4edc96bee37cbab05b75540f542f766c48c", size = 419815, upload-time = "2026-06-18T16:13:48.205Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/2d/ce/011ffcd8b919f55196ec53f12ae162e21c879d95afba226894314ff62c07/msgpack-1.2.0-cp314-cp314t-win32.whl", hash = "sha256:378caf74c4c718dfc17590ce68a6d710ed398ff6fcf08237de23b77755730b55", size = 70782, upload-time = "2026-06-11T04:16:07.105Z" },
|
{ url = "https://files.pythonhosted.org/packages/2f/e1/aab6c946570496b78e67804721f3d5e2d62a93081b9b37df77764ef56347/msgpack-1.2.1-cp314-cp314t-win32.whl", hash = "sha256:c1c79a604a2969a868a78b6ebd27a887e00c624f14f66b3038e0590cb23332d1", size = 70914, upload-time = "2026-06-18T16:13:49.385Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/57/a8/9b8791ca96b1be6b9f659c718271e2cb7f99f73f58aad2dd0b30f750f6c0/msgpack-1.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:553b42598165c4dd3235994fd6e4b0dfb1ce5f3fd33d94ba9609442643015f38", size = 77899, upload-time = "2026-06-11T04:16:08.353Z" },
|
{ url = "https://files.pythonhosted.org/packages/13/0a/e608956488a2af014cfe6e3d665e090b8ee42aa14b07f8f95b8880d66b09/msgpack-1.2.1-cp314-cp314t-win_amd64.whl", hash = "sha256:f12038a35fabd52e56a3547bab42401af49a45caa6dd00b34c44de235bc93ee2", size = 77999, upload-time = "2026-06-18T16:13:50.467Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/5b/04/3fa2dffb87bf598696b86bde7cd642d0a7590520c3fa24cd19611dfebeb7/msgpack-1.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2825bb1da548d214ab8a810906b7dd69a10f3838b615a2cc46e5172d3cb44f6e", size = 71004, upload-time = "2026-06-11T04:16:09.556Z" },
|
{ url = "https://files.pythonhosted.org/packages/d2/8a/27e2e57055176e366a46b85d02d68e7a5bcfbdd8474c9706375d965f24d3/msgpack-1.2.1-cp314-cp314t-win_arm64.whl", hash = "sha256:0adcf06ffde0777c0e1a9b771a2b1c4226ba1bbf748c8efcc02fcdeca3299107", size = 71160, upload-time = "2026-06-18T16:13:51.498Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -445,7 +458,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pytest"
|
name = "pytest"
|
||||||
version = "9.1.0"
|
version = "9.1.1"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||||
@@ -456,9 +469,9 @@ dependencies = [
|
|||||||
{ name = "pygments" },
|
{ name = "pygments" },
|
||||||
{ name = "tomli", marker = "python_full_version < '3.11'" },
|
{ name = "tomli", marker = "python_full_version < '3.11'" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/84/0e/b5858858d74958632c49b72cb25a3976ff9f632397626715be71c89d3971/pytest-9.1.0.tar.gz", hash = "sha256:41dd9148c08072446394cefd3d79701701335a9f4cae69ba92e39f6c7f5c061c", size = 1634181, upload-time = "2026-06-13T18:52:45.983Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/8b/5a/ba30a81239b909821b3153e303e7def45178bf353da4f72380e6c5e8793b/pytest-9.1.0-py3-none-any.whl", hash = "sha256:8ebb0e7888bdf2bdfc602ec51f8f62d50200af37356c74e503c79a94f5c81f32", size = 386453, upload-time = "2026-06-13T18:52:44.045Z" },
|
{ url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -489,7 +502,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "real-browser-cli"
|
name = "real-browser-cli"
|
||||||
version = "0.16.3"
|
version = "0.16.6"
|
||||||
source = { editable = "." }
|
source = { editable = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "click" },
|
{ name = "click" },
|
||||||
@@ -598,20 +611,20 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typing-extensions"
|
name = "typing-extensions"
|
||||||
version = "4.15.0"
|
version = "4.16.0"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
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" }
|
sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" }
|
||||||
wheels = [
|
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://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wcwidth"
|
name = "wcwidth"
|
||||||
version = "0.8.1"
|
version = "0.8.2"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/49/b4/51fe890511f0f242d07cb1ebe6a5b6db417262b9d2568b460347c57d95cc/wcwidth-0.8.1.tar.gz", hash = "sha256:faf5b4a5366a72dc49cad48cdf21f52bdf63bdda995178e483ba247ff79089b9", size = 1466072, upload-time = "2026-06-08T05:57:23.146Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/34/74/c6428f875774288bec1396f5bfcbc2d925700a4dad61727fd5f2b12f249d/wcwidth-0.8.2.tar.gz", hash = "sha256:91fbef97204b96a3d4d421609b80340b760cf33e26da123ff243d76b1fda8dda", size = 1466253, upload-time = "2026-06-29T18:11:11.601Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/bd/6e/95b0e537de1f4d4301f76f944642c6da50d1511cc7b3d64dc418a66c7509/wcwidth-0.8.1-py3-none-any.whl", hash = "sha256:f453740b1e4a4f3291faa37944c555d71056c4da08d59809b307ef4feba695c8", size = 323092, upload-time = "2026-06-08T05:57:21.413Z" },
|
{ url = "https://files.pythonhosted.org/packages/96/42/3e5985a0a7e57de470b320c6d6a1a67c844f6737a587f3d44dd13d1819e7/wcwidth-0.8.2-py3-none-any.whl", hash = "sha256:d63947694a0539a1d51e01eda7caf800c291020e6cdd7e28ad7b14dd33ad4f85", size = 323166, upload-time = "2026-06-29T18:11:09.888Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
Reference in New Issue
Block a user