From 8034e7102284720096ef613a5e1cf8bef7515003 Mon Sep 17 00:00:00 2001 From: Serj Babayan Date: Tue, 21 Apr 2026 12:38:10 -0700 Subject: [PATCH 1/2] Add multi org config setup with access tokens --- parallel_web_tools/cli/commands.py | 326 +++- parallel_web_tools/core/__init__.py | 6 +- parallel_web_tools/core/auth.py | 869 ++++++---- parallel_web_tools/core/credentials.py | 208 +++ parallel_web_tools/core/endpoints.py | 45 + parallel_web_tools/core/service.py | 138 ++ parallel_web_tools/core/service_types.py | 98 ++ .../integrations/duckdb/_relation.py | 22 + .../integrations/duckdb/batch.py | 7 +- .../integrations/duckdb/findall.py | 7 +- parallel_web_tools/integrations/duckdb/udf.py | 8 +- .../integrations/spark/streaming.py | 3 + parallel_web_tools/integrations/spark/udf.py | 4 +- pyproject.toml | 1 + scripts/generate_service_types.py | 84 + tests/conftest.py | 65 + tests/test_auth.py | 1456 ++++++++++++----- tests/test_cli.py | 811 ++++++--- tests/test_credentials.py | 258 +++ tests/test_enrichment.py | 11 +- tests/test_service.py | 337 ++++ tests/test_test_harness.py | 38 + uv.lock | 914 ++++++++--- 23 files changed, 4387 insertions(+), 1329 deletions(-) create mode 100644 parallel_web_tools/core/credentials.py create mode 100644 parallel_web_tools/core/endpoints.py create mode 100644 parallel_web_tools/core/service.py create mode 100644 parallel_web_tools/core/service_types.py create mode 100644 parallel_web_tools/integrations/duckdb/_relation.py create mode 100644 scripts/generate_service_types.py create mode 100644 tests/test_credentials.py create mode 100644 tests/test_service.py create mode 100644 tests/test_test_harness.py diff --git a/parallel_web_tools/cli/commands.py b/parallel_web_tools/cli/commands.py index 0422028..3107993 100644 --- a/parallel_web_tools/cli/commands.py +++ b/parallel_web_tools/cli/commands.py @@ -24,6 +24,7 @@ MONITOR_PROCESSORS, MONITOR_TYPES, RESEARCH_PROCESSORS, + ReauthenticationRequired, cancel_findall_run, cancel_monitor, create_findall_run, @@ -33,6 +34,7 @@ extend_findall, get_api_key, get_auth_status, + get_control_api_access_token, get_findall_result, get_findall_schema, get_findall_status, @@ -488,6 +490,10 @@ def auth(output_json: bool): else: console.print("[green]Authenticated via OAuth[/green]") console.print(f" Credentials: {status['token_file']}") + if status.get("selected_org_name"): + console.print(f" Organization: {status['selected_org_name']} ({status['selected_org_id']})") + elif status.get("selected_org_id"): + console.print(f" Organization ID: {status['selected_org_id']}") else: console.print("[yellow]Not authenticated[/yellow]") console.print("\n[cyan]To get started:[/cyan]") @@ -496,39 +502,135 @@ def auth(output_json: bool): console.print(" Or set PARALLEL_API_KEY environment variable") -@main.command() -@click.option("--json", "output_json", is_flag=True, help="Output as JSON") -@click.option("--device", is_flag=True, help="Use device authorization flow (for SSH, containers, etc.)") -def login(output_json: bool, device: bool): - """Authenticate with Parallel API.""" +def _build_login_hint(login_method: str | None, email: str | None) -> str | None: + """Format a platform-compatible ``login_hint`` query value. + + Scheme — the hint always names the method only; any email travels as a + separate top-level query param (see :func:`_login_extra_params`): + + - ``"email"`` → ``login=email`` (requires an email; passed as ``&email=…``) + - ``"google"`` → ``login=google`` + - ``"sso"`` → ``login=sso`` (requires an email; passed as ``&email=…``) + + Returns ``None`` when ``login_method`` is ``None`` so the caller can + skip the query param entirely. + """ + if login_method is None: + return None + if login_method in ("email", "sso"): + if not email: + raise ValueError(f"login_method={login_method!r} requires an email") + return f"login={login_method}" + if login_method == "google": + return "login=google" + raise ValueError(f"Unknown login_method: {login_method!r}") + + +def _login_extra_params(login_method: str | None, email: str | None) -> dict[str, str] | None: + """Extra query params to append alongside ``login_hint``. + + Returns ``{"email": }`` for identity-bearing methods (``email`` + and ``sso``) so the platform's login page receives the address as a + top-level param, e.g. ``...&login_hint=login=sso&email=you@example.com``. + Returns ``None`` for methods that carry no identity (``google``, or + none at all). + """ + if login_method in ("email", "sso") and email: + return {"email": email} + return None + + +def _run_login(output_json: bool, email: str | None, login_method: str | None) -> None: + """Shared body for all ``parallel-cli login`` variants. + + ``login_method`` selects the identity-provider hint and UX flavor: + + - ``None`` → plain device flow: print URL + code, open browser. + - ``"email"`` → email magic-link: POST ``/api/auth/send-magic-link``, + tell the user to check their inbox, do NOT open + the browser. Falls back to manual display on + magic-link failure. + - ``"google"`` → append ``login_hint=login=google`` to the URL + and open the browser. + - ``"sso"`` → append ``login_hint=login=sso,e=`` to the + URL and open the browser. + """ + import webbrowser + + from parallel_web_tools.core.auth import ( + _build_verification_uri, + _ensure_client_id, + _is_headless, + send_magic_link, + ) + + login_hint = _build_login_hint(login_method, email) + extra_params = _login_extra_params(login_method, email) + if not output_json: - if device: - console.print("[bold cyan]Authenticating with Parallel (device flow)...[/bold cyan]\n") - else: - console.print("[bold cyan]Authenticating with Parallel...[/bold cyan]\n") + console.print("[bold cyan]Authenticating with Parallel...[/bold cyan]\n") def _on_device_code(info): + magic_link_sent = False + magic_link_error: str | None = None + if login_method == "email" and email: + try: + send_magic_link(client_id=_ensure_client_id(), email=email, user_code=info.user_code) + magic_link_sent = True + except Exception as e: + magic_link_error = str(e) + + enriched_uri = _build_verification_uri(info.verification_uri_complete, login_hint, extra_params=extra_params) + if output_json: - print( - json.dumps( - { - "status": "waiting_for_authorization", - "verification_uri": info.verification_uri, - "verification_uri_complete": info.verification_uri_complete, - "user_code": info.user_code, - "expires_in": info.expires_in, - } - ), - flush=True, + payload = { + "status": "waiting_for_authorization", + "verification_uri": info.verification_uri, + "verification_uri_complete": enriched_uri, + "user_code": info.user_code, + "expires_in": info.expires_in, + } + if login_method == "email": + payload["magic_link_sent"] = magic_link_sent + if magic_link_error: + payload["magic_link_error"] = magic_link_error + print(json.dumps(payload), flush=True) + return + + if magic_link_sent: + # Email login succeeded: tell the user to check their inbox. + # Still print the URL + code as a fallback in case the mail is + # slow or lands in spam. Do NOT open the browser. + console.print(f"[green]Magic link sent to {email}.[/green] Check your inbox to authorize.") + console.print( + f"\nOr visit [bold cyan]{info.verification_uri}[/bold cyan] " + f"and enter code [bold yellow]{info.user_code}[/bold yellow]." ) - else: - console.print(f"Visit: [bold cyan]{info.verification_uri}[/bold cyan]") - console.print(f"Enter code: [bold yellow]{info.user_code}[/bold yellow]\n") - console.print(f"Or open: [link={info.verification_uri_complete}]{info.verification_uri_complete}[/link]\n") console.print("Waiting for authorization...") + return + + if magic_link_error: + console.print( + f"[yellow]Could not send magic link ({magic_link_error}); " + "falling back to manual authorization.[/yellow]\n" + ) + + console.print(f"Visit: [bold cyan]{info.verification_uri}[/bold cyan]") + console.print(f"Enter code: [bold yellow]{info.user_code}[/bold yellow]\n") + console.print(f"Or open: [link={enriched_uri}]{enriched_uri}[/link]\n") + console.print("Confirm the code matches what your browser shows, then authorize.") + console.print("Waiting for authorization...") + + # Providing an on_device_code callback suppresses auth.py's default + # browser-launch branch, so open it here for interactive CLI use. + if not _is_headless(): + try: + webbrowser.open(enriched_uri) + except Exception: + pass try: - get_api_key(force_login=True, device=device, on_device_code=_on_device_code) + get_api_key(force_login=True, on_device_code=_on_device_code, login_hint=login_hint) if output_json: print(json.dumps({"status": "authenticated"})) else: @@ -537,6 +639,66 @@ def _on_device_code(info): _handle_error(e, output_json=output_json, exit_code=EXIT_AUTH_ERROR, prefix="Authentication failed") +@main.group(invoke_without_command=True) +@click.option("--json", "output_json", is_flag=True, help="Output as JSON") +@click.pass_context +def login(ctx: click.Context, output_json: bool): + """Authenticate with Parallel API (device authorization flow). + + \b + Examples: + parallel-cli login # opens browser for SSO + parallel-cli login email you@example.com # sends a magic-link email + parallel-cli login google # opens browser, hints Google SSO + parallel-cli login sso you@example.com # opens browser, hints SSO + email + """ + ctx.ensure_object(dict) + ctx.obj["output_json"] = output_json + if ctx.invoked_subcommand is None: + _run_login(output_json=output_json, email=None, login_method=None) + + +@login.command("email") +@click.argument("user_email") +@click.pass_context +def login_email(ctx: click.Context, user_email: str): + """Send a magic-link email to USER_EMAIL that auto-confirms the CLI's device code. + + No browser is opened — the link in the email handles authorization. If the + email can't be sent, the CLI falls back to printing the URL and code for + manual entry. + """ + output_json = ctx.obj.get("output_json", False) if ctx.obj else False + _run_login(output_json=output_json, email=user_email, login_method="email") + + +@login.command("google") +@click.pass_context +def login_google(ctx: click.Context): + """Authenticate via Google SSO. + + Opens the browser on a verification URL that hints ``login=google`` so the + landing page auto-routes to Google's SSO (and auto-submits where it can + if the user is already signed in). + """ + output_json = ctx.obj.get("output_json", False) if ctx.obj else False + _run_login(output_json=output_json, email=None, login_method="google") + + +@login.command("sso") +@click.argument("user_email") +@click.pass_context +def login_sso(ctx: click.Context, user_email: str): + """Authenticate via enterprise SSO for USER_EMAIL. + + Opens the browser on a verification URL that hints ``login=sso,e=`` + so the landing page resolves the right SSO tenant for the email domain + and pre-fills the address. + """ + output_json = ctx.obj.get("output_json", False) if ctx.obj else False + _run_login(output_json=output_json, email=user_email, login_method="sso") + + @main.command(name="logout") @click.option("--json", "output_json", is_flag=True, help="Output as JSON") def logout_cmd(output_json: bool): @@ -550,6 +712,106 @@ def logout_cmd(output_json: bool): console.print("[yellow]No stored credentials found[/yellow]") +def _format_cents(cents: int | float) -> str: + """Render a cents amount as ``$X.YZ (N¢)``.""" + return f"${cents / 100:.2f} ({int(cents)}¢)" + + +def _derive_idempotency_key(amount_cents: int) -> str: + """Build a deterministic idempotency key for ``balance add``. + + Format: ``{client_id}-{amount_cents}-{five_min_bucket}``, where + ``five_min_bucket`` is the current unix time rounded down to the nearest + 300 seconds. Identical repeat requests inside the same 5-minute window + reuse the same key, so Stripe's idempotency dedupes them server-side. + """ + from parallel_web_tools.core.auth import _ensure_client_id + + client_id = _ensure_client_id() + five_min_bucket = int(time.time() // 300) * 300 + return f"{client_id}-{amount_cents}-{five_min_bucket}" + + +def _render_balance(resp, output_json: bool, *, prefix_lines: list[str] | None = None) -> None: + """Render a :class:`BalanceResponse` in JSON or Rich-console form.""" + if output_json: + print(json.dumps(resp.model_dump(), indent=2)) + return + + for line in prefix_lines or []: + console.print(line) + console.print(f"Organization: [cyan]{resp.org_id}[/cyan]") + console.print(f"Credit balance: [bold green]{_format_cents(resp.credit_balance_cents)}[/bold green]") + pending = resp.pending_debit_balance_cents or 0 + if pending: + console.print(f"Pending debit: [yellow]{_format_cents(pending)}[/yellow]") + if resp.will_invoice: + console.print("[dim]Billed by invoice (postpaid)[/dim]") + + +@main.group(name="balance") +@click.option("--json", "output_json", is_flag=True, help="Output as JSON") +@click.pass_context +def balance(ctx: click.Context, output_json: bool): + """Inspect or top up the org's prepaid credit balance.""" + ctx.ensure_object(dict) + ctx.obj["output_json"] = output_json + + +@balance.command("get") +@click.pass_context +def balance_get(ctx: click.Context): + """Show the current credit balance.""" + from parallel_web_tools.core import service + + output_json = ctx.obj.get("output_json", False) if ctx.obj else False + try: + token = get_control_api_access_token() + resp = service.get_balance(token) + except ReauthenticationRequired as e: + _handle_error(e, output_json=output_json, exit_code=EXIT_AUTH_ERROR, prefix="Authentication required") + return + except Exception as e: + _handle_error(e, output_json=output_json, exit_code=EXIT_API_ERROR, prefix="Balance API error") + return + + _render_balance(resp, output_json) + + +@balance.command("add") +@click.argument("amount_cents", type=int) +@click.option( + "--idempotency-key", + "idempotency_key_override", + default=None, + help="Override the auto-derived idempotency key. Defaults to " + "{client_id}-{amount_cents}-{5min_bucket} so repeat attempts inside " + "the same 5-minute window dedupe server-side.", +) +@click.pass_context +def balance_add(ctx: click.Context, amount_cents: int, idempotency_key_override: str | None): + """Charge and top up the prepaid balance by AMOUNT_CENTS.""" + from parallel_web_tools.core import service + + output_json = ctx.obj.get("output_json", False) if ctx.obj else False + idempotency_key = idempotency_key_override or _derive_idempotency_key(amount_cents) + try: + token = get_control_api_access_token() + resp = service.add_balance(token, amount_cents, idempotency_key) + except ReauthenticationRequired as e: + _handle_error(e, output_json=output_json, exit_code=EXIT_AUTH_ERROR, prefix="Authentication required") + return + except Exception as e: + _handle_error(e, output_json=output_json, exit_code=EXIT_API_ERROR, prefix="Balance API error") + return + + _render_balance( + resp, + output_json, + prefix_lines=[f"[green]Added {_format_cents(amount_cents)} to balance.[/green]"], + ) + + @main.command(name="update") @click.option("--check", is_flag=True, help="Check for updates without installing") @click.option("--force", is_flag=True, help="Reinstall even if already at latest version") @@ -831,12 +1093,9 @@ def search( source_policy["after_date"] = after_date try: - from parallel import Parallel - - from parallel_web_tools.core import get_default_headers + from parallel_web_tools.core.auth import get_client - api_key = get_api_key() - client = Parallel(api_key=api_key, default_headers=get_default_headers("cli")) + client = get_client(source="cli") fetch_policy: dict[str, Any] = {} if max_age_seconds is not None: @@ -999,12 +1258,9 @@ def extract( raise click.UsageError(f"--objective must be 5000 characters or fewer (got {len(objective)}).") try: - from parallel import Parallel + from parallel_web_tools.core.auth import get_client - from parallel_web_tools.core import get_default_headers - - api_key = get_api_key() - client = Parallel(api_key=api_key, default_headers=get_default_headers("cli")) + client = get_client(source="cli") fetch_policy: dict[str, Any] = {} if max_age_seconds is not None: diff --git a/parallel_web_tools/core/__init__.py b/parallel_web_tools/core/__init__.py index 0c11d39..cb54908 100644 --- a/parallel_web_tools/core/__init__.py +++ b/parallel_web_tools/core/__init__.py @@ -2,11 +2,13 @@ from parallel_web_tools.core.auth import ( DeviceCodeInfo, + ReauthenticationRequired, create_client, get_api_key, get_async_client, get_auth_status, get_client, + get_control_api_access_token, logout, poll_device_token, request_device_code, @@ -87,11 +89,13 @@ __all__ = [ # Auth "DeviceCodeInfo", + "ReauthenticationRequired", "create_client", "get_api_key", + "get_async_client", "get_auth_status", "get_client", - "get_async_client", + "get_control_api_access_token", "logout", "poll_device_token", "request_device_code", diff --git a/parallel_web_tools/core/auth.py b/parallel_web_tools/core/auth.py index f8087f2..a29154d 100644 --- a/parallel_web_tools/core/auth.py +++ b/parallel_web_tools/core/auth.py @@ -1,13 +1,21 @@ -"""OAuth Authentication for Parallel API.""" +"""Device-flow authentication for parallel-cli. + +Authentication happens exclusively via the OAuth 2.0 Device Authorization Grant +(RFC 8628) against the platform's ``/getServiceKeys/*`` endpoints. After a +successful device flow the CLI additionally provisions a data-API key against +the service API so that subsequent commands (search, extract, etc.) have a key +to use. + +All endpoints are built from :mod:`parallel_web_tools.core.endpoints`, so a +local dev stack can be reached via ``PARALLEL_PLATFORM_URL`` / +``PARALLEL_SERVICE_API_URL`` env vars. +""" + +from __future__ import annotations -import base64 -import hashlib -import html -import http.server import json import os -import secrets -import socketserver +import platform as _platform import sys import time import urllib.error @@ -16,20 +24,34 @@ import webbrowser from collections.abc import Callable from dataclasses import dataclass -from pathlib import Path from parallel import AsyncParallel, Parallel +from parallel_web_tools.core import credentials, service +from parallel_web_tools.core.endpoints import ( + CLIENT_ID, + DEFAULT_SCOPE, + get_api_url, + get_platform_url, +) from parallel_web_tools.core.user_agent import ClientSource, get_default_headers -# OAuth Configuration -OAUTH_PROVIDER_HOST = "platform.parallel.ai" -OAUTH_PROVIDER_PATH_PREFIX = "/getKeys" -OAUTH_SCOPE = "key:read" -TOKEN_FILE = Path.home() / ".config" / "parallel-web-tools" / "credentials.json" - -# Device flow grant type (RFC 8628) DEVICE_CODE_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code" +REFRESH_TOKEN_GRANT_TYPE = "refresh_token" + +# Proactively refresh when the access token is within this many seconds of its +# absolute expiry, so callers don't get a token that dies mid-request under clock +# skew or network latency. +ACCESS_TOKEN_SKEW_SECONDS = 30 + + +class ReauthenticationRequired(Exception): + """Raised when the control-API grant can no longer be refreshed silently. + + The caller must run ``parallel-cli login`` before any control-API call + will succeed — the authorization grant, the refresh token, or both have + expired (or never existed), so no silent refresh is possible. + """ @dataclass @@ -37,221 +59,235 @@ class DeviceCodeInfo: """Response from the device authorization endpoint (RFC 8628).""" device_code: str - """Opaque code used to poll the token endpoint. Never shown to user.""" - user_code: str - """Human-readable code the user enters at the verification URL (e.g. BCDF-GHJK).""" - verification_uri: str - """URL the user visits to enter the code.""" - verification_uri_complete: str - """URL with user_code pre-filled as a query parameter.""" + expires_in: int + interval: int + +@dataclass +class TokenResponse: + """Response from ``/getServiceKeys/token`` (device or refresh grant).""" + + access_token: str + refresh_token: str expires_in: int - """Seconds until the device code expires (default 600).""" + refresh_token_expires_in: int + authorization_expires_in: int + org_id: str + org_name: str | None = None + scope: str = "" + token_type: str = "Bearer" - interval: int - """Minimum polling interval in seconds (default 5).""" + @property + def scopes(self) -> list[str]: + return self.scope.split() if self.scope else [] -def _generate_code_verifier() -> str: - """Generate a random code verifier for PKCE.""" - return secrets.token_urlsafe(32) +def _platform_path(path: str) -> str: + return f"{get_platform_url()}{path}" -def _generate_code_challenge(verifier: str) -> str: - """Generate code challenge from verifier using S256.""" - digest = hashlib.sha256(verifier.encode()).digest() - return base64.urlsafe_b64encode(digest).rstrip(b"=").decode() +def _is_headless() -> bool: + """Detect if the environment cannot open a browser.""" + if os.environ.get("SSH_CLIENT") or os.environ.get("SSH_TTY"): + return True + if os.environ.get("CI"): + return True + if sys.platform == "linux" and not os.environ.get("DISPLAY") and not os.environ.get("WAYLAND_DISPLAY"): + return True + if os.path.exists("/.dockerenv") or os.environ.get("container"): + return True + return False -def _load_stored_token() -> str | None: - """Load stored OAuth token from file.""" - if not TOKEN_FILE.exists(): - return None - try: - with open(TOKEN_FILE) as f: - data = json.load(f) - return data.get("access_token") - except (OSError, json.JSONDecodeError): - return None - - -def _save_token(access_token: str) -> None: - """Save OAuth token to file with secure permissions.""" - TOKEN_FILE.parent.mkdir(parents=True, exist_ok=True) - with open(TOKEN_FILE, "w") as f: - json.dump({"access_token": access_token}, f) - os.chmod(TOKEN_FILE, 0o600) - - -class OAuthCallbackHandler(http.server.BaseHTTPRequestHandler): - """HTTP handler to receive OAuth callback.""" - - auth_code: str | None = None - error: str | None = None - - def log_message(self, format, *args): - pass - - def do_GET(self): - parsed = urllib.parse.urlparse(self.path) - params = urllib.parse.parse_qs(parsed.query) - - if "code" in params: - OAuthCallbackHandler.auth_code = params["code"][0] - self.send_response(200) - self.send_header("Content-Type", "text/html") - self.end_headers() - self.wfile.write( - b""" - -

Authentication Successful!

-

You can close this window and return to the terminal.

- - """ - ) - elif "error" in params: - OAuthCallbackHandler.error = params.get("error_description", params["error"])[0] - self.send_response(400) - self.send_header("Content-Type", "text/html") - self.end_headers() - self.wfile.write( - f""" - -

Authentication Failed

-

{html.escape(OAuthCallbackHandler.error)}

- - """.encode() - ) - else: - self.send_response(404) - self.end_headers() - - -def _do_oauth_flow() -> str: - """Perform OAuth authorization code flow with PKCE.""" - OAuthCallbackHandler.auth_code = None - OAuthCallbackHandler.error = None - - with socketserver.TCPServer(("127.0.0.1", 0), OAuthCallbackHandler) as httpd: - port = httpd.server_address[1] - redirect_uri = f"http://localhost:{port}/callback" - - code_verifier = _generate_code_verifier() - code_challenge = _generate_code_challenge(code_verifier) - - auth_params = { - "client_id": "localhost", - "redirect_uri": redirect_uri, - "response_type": "code", - "scope": OAUTH_SCOPE, - "resource": f"http://localhost:{port}", - "code_challenge": code_challenge, - "code_challenge_method": "S256", - } - auth_url = f"https://{OAUTH_PROVIDER_HOST}{OAUTH_PROVIDER_PATH_PREFIX}/authorize?" + urllib.parse.urlencode( - auth_params - ) +def _post_form(url: str, data: dict[str, str], headers: dict[str, str] | None = None, timeout: int = 30) -> dict: + """POST a form-encoded request, return parsed JSON body. - print("\nOpening browser for authentication...", file=sys.stderr) - print(f"If browser doesn't open, visit: {auth_url}", file=sys.stderr) + Raises ``urllib.error.HTTPError`` on HTTP error (body still readable via ``e.read()``). + """ + body = urllib.parse.urlencode(data).encode() + req_headers = {"Content-Type": "application/x-www-form-urlencoded"} + req_headers.update(_platform_bypass_headers(url)) + if headers: + req_headers.update(headers) + req = urllib.request.Request(url, data=body, headers=req_headers) + with urllib.request.urlopen(req, timeout=timeout) as response: + return json.loads(response.read().decode()) + + +def _post_json(url: str, body: dict, timeout: int = 30) -> dict: + """POST a JSON body, return parsed JSON response.""" + data = json.dumps(body).encode() + req_headers = {"Content-Type": "application/json", "Accept": "application/json"} + req_headers.update(_platform_bypass_headers(url)) + req = urllib.request.Request( + url, + data=data, + headers=req_headers, + ) + with urllib.request.urlopen(req, timeout=timeout) as response: + return json.loads(response.read().decode()) - webbrowser.open(auth_url) - httpd.timeout = 300 - while OAuthCallbackHandler.auth_code is None and OAuthCallbackHandler.error is None: - httpd.handle_request() +def _platform_bypass_headers(url: str) -> dict[str, str]: + """Add Vercel protection bypass header for platform requests when configured.""" + token = os.environ.get("VERCEL_PROTECTION_BYPASS_TOKEN") + if not token: + return {} + if not url.startswith(get_platform_url()): + return {} + return {"X-Vercel-Protection-Bypass": token} - if OAuthCallbackHandler.error: - raise Exception(f"OAuth error: {OAuthCallbackHandler.error}") - auth_code = OAuthCallbackHandler.auth_code +def _get_platform_info() -> dict[str, str]: + """Best-effort OS/arch metadata for the registration payload. - token_url = f"https://{OAUTH_PROVIDER_HOST}{OAUTH_PROVIDER_PATH_PREFIX}/token" - token_data = urllib.parse.urlencode( - { - "grant_type": "authorization_code", - "code": auth_code, - "client_id": "localhost", - "redirect_uri": redirect_uri, - "code_verifier": code_verifier, - "resource": f"http://localhost:{port}", - } - ).encode() + Mirrors the TS ``ClientPlatform`` type: every field is optional. We drop + any key whose value is falsy (e.g. ``platform.processor()`` returns ``""`` + on some Linux distros), so the payload only carries meaningful fields. + """ - req = urllib.request.Request( - token_url, - data=token_data, - headers={"Content-Type": "application/x-www-form-urlencoded"}, - ) + def safe(getter: Callable[[], str]) -> str: + try: + return getter() + except Exception: + return "" + + raw = { + "system": safe(_platform.system), + "release": safe(_platform.release), + "machine": safe(_platform.machine), + "processor": safe(_platform.processor), + "version": safe(_platform.version), + "os_name": os.name, + } + return {k: v for k, v in raw.items() if v} + + +def register_client(client_name: str = "parallel-cli") -> str: + """Register this CLI install with the platform and return the new ``client_id``. + POSTs to ``/getServiceKeys/register`` with the client name and OS platform + metadata. The platform assigns and returns a unique ``client_id`` used on + subsequent OAuth calls. + """ + url = _platform_path("/getServiceKeys/register") + body: dict = {"client_name": client_name, "platform": _get_platform_info()} try: - with urllib.request.urlopen(req, timeout=30) as response: - token_response = json.loads(response.read().decode()) - access_token = token_response.get("access_token") - if not access_token: - raise Exception("No access token in response") - return access_token + data = _post_json(url, body) except urllib.error.HTTPError as e: - error_body = e.read().decode() - raise Exception(f"Token exchange failed: {e.code} - {error_body}") from e + err_body = e.read().decode() + raise Exception(f"Client registration failed: {e.code} - {err_body}") from e + return data["client_id"] -def _is_headless() -> bool: - """Detect if the environment cannot open a browser for OAuth. +def send_magic_link(client_id: str, email: str, user_code: str, email_type: str = "deviceCode") -> None: + """Ask the platform to email a magic link that auto-authorizes ``user_code``. + + POSTs to ``/api/auth/send-magic-link`` with: - Returns True for SSH sessions, containers, CI, and other headless - environments where the authorization code flow won't work. + - ``client_id`` — the registered CLI client. + - ``email`` — recipient. + - ``emailType`` — ``"deviceCode"`` routes the template that confirms a + pending device-flow user code. + - ``queryParams.user_code`` — echoed into the magic-link URL so the + landing page can pre-confirm the CLI's device code in one click. + + Raises ``Exception`` on any HTTP error so the caller can fall back to + the manual URL-and-code flow. """ - # SSH session - if os.environ.get("SSH_CLIENT") or os.environ.get("SSH_TTY"): - return True + url = f"{get_platform_url()}/api/auth/send-magic-link" + body = { + "client_id": client_id, + "email": email, + "emailType": email_type, + "queryParams": {"user_code": user_code}, + } + try: + _post_json(url, body) + except urllib.error.HTTPError as e: + err_body = e.read().decode() + raise Exception(f"Magic link send failed: {e.code} - {err_body}") from e - # CI environments - if os.environ.get("CI"): - return True - # No display on Linux - if sys.platform == "linux" and not os.environ.get("DISPLAY") and not os.environ.get("WAYLAND_DISPLAY"): - return True +def _ensure_client_id() -> str: + """Return a registered ``client_id``, registering if none is stored yet. - # Container indicators - if os.path.exists("/.dockerenv") or os.environ.get("container"): - return True + - If the credentials file already has a ``client_id``, returns it. + - Otherwise calls :func:`register_client` and persists the result. + - If registration fails, emits a single-line stderr warning and falls + back to the hardcoded ``CLIENT_ID``. The stored ``client_id`` stays + unset so the next login attempt retries transparently. + """ + creds = credentials.load() or credentials.Credentials() + if creds.client_id: + return creds.client_id + try: + client_id = register_client() + except Exception as e: + print(f"Warning: client registration failed ({e}); using fallback client_id.", file=sys.stderr) + return CLIENT_ID + creds.client_id = client_id + credentials.save(creds) + return client_id - return False +def _replace_client_id(client_id: str) -> None: + """Persist a freshly registered client_id, replacing any stale value.""" + creds = credentials.load() or credentials.Credentials() + creds.client_id = client_id + credentials.save(creds) -def request_device_code() -> DeviceCodeInfo: - """Request a device code from the authorization server (RFC 8628 Step 1). - Returns a DeviceCodeInfo with the user_code, verification URL, and device_code - needed for the rest of the flow. Callers should present the verification_uri and - user_code to the user, then call poll_device_token() to wait for authorization. +def _is_unknown_client_error(error: Exception) -> bool: + """Return True when the platform rejects an unknown/stale client_id.""" + message = str(error) + return '"error":"invalid_client"' in message and "Unknown client_id" in message - Example:: - info = request_device_code() - print(f"Visit {info.verification_uri} and enter code: {info.user_code}") - token = poll_device_token(info) - """ - device_code_url = f"https://{OAUTH_PROVIDER_HOST}{OAUTH_PROVIDER_PATH_PREFIX}/device/code" +def _reregister_client_id() -> str: + """Register a new client_id and persist it, raising on failure.""" + client_id = register_client() + _replace_client_id(client_id) + return client_id - request_data = urllib.parse.urlencode({"client_id": "localhost", "scope": OAUTH_SCOPE}).encode() - req = urllib.request.Request( - device_code_url, - data=request_data, - headers={"Content-Type": "application/x-www-form-urlencoded"}, - ) +def _build_verification_uri( + base: str, + login_hint: str | None, + extra_params: dict[str, str] | None = None, +) -> str: + """Append ``agent=true``, an optional ``login_hint``, and any ``extra_params``. + + ``login_hint`` is appended verbatim as a query param. Callers format it + per the platform's scheme — e.g. ``login=email``, ``login=google``, + ``login=sso`` — so the landing page can route the user to the right + identity provider. + + ``extra_params`` appends additional top-level query params (e.g. + ``email=``) alongside ``login_hint``. Used by the email and SSO + flows where the identity lives outside the ``login_hint`` value. + """ + parsed = urllib.parse.urlparse(base) + query = urllib.parse.parse_qsl(parsed.query, keep_blank_values=True) + query.append(("onboard_variant", "agent")) + if login_hint: + query.append(("login_hint", login_hint)) + for k, v in (extra_params or {}).items(): + query.append((k, v)) + return urllib.parse.urlunparse(parsed._replace(query=urllib.parse.urlencode(query))) + + +def request_device_code(scope: str = DEFAULT_SCOPE, client_id: str | None = None) -> DeviceCodeInfo: + """Request a device code from ``/getServiceKeys/device/code`` (RFC 8628 Step 1).""" + url = _platform_path("/getServiceKeys/device/code") try: - with urllib.request.urlopen(req, timeout=30) as response: - data = json.loads(response.read().decode()) + data = _post_form(url, {"client_id": client_id or CLIENT_ID, "scope": scope}) except urllib.error.HTTPError as e: - error_body = e.read().decode() - raise Exception(f"Device code request failed: {e.code} - {error_body}") from e + body = e.read().decode() + raise Exception(f"Device code request failed: {e.code} - {body}") from e return DeviceCodeInfo( device_code=data["device_code"], @@ -263,250 +299,375 @@ def request_device_code() -> DeviceCodeInfo: ) -def poll_device_token(info: DeviceCodeInfo) -> str: - """Poll the token endpoint until the user authorizes (RFC 8628 Step 3). +def _parse_token_response(data: dict) -> TokenResponse: + return TokenResponse( + access_token=data["access_token"], + refresh_token=data["refresh_token"], + expires_in=int(data.get("expires_in", 0)), + refresh_token_expires_in=int(data.get("refresh_token_expires_in", 0)), + authorization_expires_in=int(data.get("authorization_expires_in", 0)), + org_id=data["org_id"], + org_name=data.get("org_name"), + scope=data.get("scope", ""), + token_type=data.get("token_type", "Bearer"), + ) - Args: - info: DeviceCodeInfo from request_device_code(). - Returns: - The access token string. +def poll_device_token(info: DeviceCodeInfo, client_id: str | None = None) -> TokenResponse: + """Poll ``/getServiceKeys/token`` until the user authorizes (RFC 8628 Step 3). - Raises: - Exception: On expiry, denial, or other errors. + Polls the token endpoint immediately on entry, then waits ``interval`` + seconds between subsequent polls. RFC 8628 only requires waiting *between* + requests, so polling right away makes fast authorizations feel snappy + instead of eating a silent ``interval``-second delay. """ - token_url = f"https://{OAUTH_PROVIDER_HOST}{OAUTH_PROVIDER_PATH_PREFIX}/token" + url = _platform_path("/getServiceKeys/token") interval = info.interval deadline = time.monotonic() + info.expires_in while time.monotonic() < deadline: - time.sleep(interval) - - poll_data = urllib.parse.urlencode( - { - "grant_type": DEVICE_CODE_GRANT_TYPE, - "device_code": info.device_code, - "client_id": "localhost", - } - ).encode() - - poll_req = urllib.request.Request( - token_url, - data=poll_data, - headers={"Content-Type": "application/x-www-form-urlencoded"}, - ) - try: - with urllib.request.urlopen(poll_req, timeout=30) as response: - token_response = json.loads(response.read().decode()) - access_token = token_response.get("access_token") - if access_token: - return access_token - raise Exception("No access token in response") + data = _post_form( + url, + { + "grant_type": DEVICE_CODE_GRANT_TYPE, + "device_code": info.device_code, + "client_id": client_id or CLIENT_ID, + }, + ) + return _parse_token_response(data) except urllib.error.HTTPError as e: - error_body = json.loads(e.read().decode()) - error_code = error_body.get("error", "") - + body = json.loads(e.read().decode()) + error_code = body.get("error", "") if error_code == "authorization_pending": - continue + pass elif error_code == "slow_down": interval += 5 - continue elif error_code == "expired_token": raise Exception("Device code expired. Please try again.") from e elif error_code == "access_denied": raise Exception("Authorization denied by user.") from e else: - raise Exception(f"Token exchange failed: {error_body.get('error_description', error_code)}") from e + raise Exception(f"Token exchange failed: {body.get('error_description', error_code)}") from e + time.sleep(interval) raise Exception("Device code expired (timeout). Please try again.") -def _do_device_flow(on_device_code: Callable[[DeviceCodeInfo], None] | None = None) -> str: - """Perform the full device authorization flow (request + poll). +def refresh_access_token(refresh_token: str, client_id: str | None = None) -> TokenResponse: + """Exchange a refresh token for a new access+refresh token pair.""" + url = _platform_path("/getServiceKeys/token") + try: + data = _post_form( + url, + { + "grant_type": REFRESH_TOKEN_GRANT_TYPE, + "refresh_token": refresh_token, + "client_id": client_id or CLIENT_ID, + }, + ) + except urllib.error.HTTPError as e: + body = e.read().decode() + raise Exception(f"Token refresh failed: {e.code} - {body}") from e + return _parse_token_response(data) + + +def revoke_token(refresh_token: str) -> None: + """Revoke a refresh token via form-encoded POST. - Args: - on_device_code: Optional callback invoked with the DeviceCodeInfo after requesting - the device code. Use this to present the verification URL and user code to the - user in a custom way (e.g., in a chat message). If not provided, prints - instructions to stderr and attempts to open the browser. + Body shape: ``refresh_token=`` (application/x-www-form-urlencoded). + The endpoint identifies the caller from the token itself — no bearer auth. """ - info = request_device_code() + url = _platform_path("/getServiceKeys/token/revoke") + body = urllib.parse.urlencode({"refresh_token": refresh_token}).encode() + req = urllib.request.Request( + url, + data=body, + headers={ + "Content-Type": "application/x-www-form-urlencoded", + "Accept": "application/json", + **_platform_bypass_headers(url), + }, + method="POST", + ) + try: + with urllib.request.urlopen(req, timeout=30): + pass + except urllib.error.HTTPError as e: + err_body = e.read().decode() + raise Exception(f"Token revocation failed: {e.code} - {err_body}") from e + + +def _do_device_flow( + login_hint: str | None = None, + on_device_code: Callable[[DeviceCodeInfo], None] | None = None, + client_id: str | None = None, +) -> TokenResponse: + """Run the full device authorization flow (request + poll) and return tokens. + + ``login_hint`` — if set — is appended to the verification URI so the + platform login page can pre-fill / auto-submit the right identity + provider. Only observable in the default (no-callback) fallback path; + callers that provide ``on_device_code`` build their own enriched URI. + """ + try: + info = request_device_code(client_id=client_id) + except Exception as e: + if client_id and _is_unknown_client_error(e): + client_id = _reregister_client_id() + info = request_device_code(client_id=client_id) + else: + raise + + enriched_uri = _build_verification_uri(info.verification_uri_complete, login_hint) if on_device_code: on_device_code(info) else: - # Default: print to stderr and try to open browser print(f"\nTo authenticate, visit: {info.verification_uri}", file=sys.stderr) print(f"And enter code: {info.user_code}\n", file=sys.stderr) - print(f"Or open: {info.verification_uri_complete}\n", file=sys.stderr) + print(f"Or open: {enriched_uri}\n", file=sys.stderr) print(f"Waiting for authorization (expires in {info.expires_in // 60} minutes)...", file=sys.stderr) - try: - webbrowser.open(info.verification_uri_complete) - except Exception: - pass + if not _is_headless(): + try: + webbrowser.open(enriched_uri) + except Exception: + pass + + return poll_device_token(info, client_id=client_id) + + +def _persist_token_response(resp: TokenResponse) -> None: + """Write a TokenResponse into credentials under its org_id, selecting it.""" + now = int(time.time()) + creds = credentials.load() or credentials.Credentials() + org = creds.orgs.get(resp.org_id) or credentials.OrgCredentials() + org.org_name = resp.org_name + org.control_api = credentials.ControlApiTokens( + access_token=resp.access_token, + access_token_expires_at=now + resp.expires_in, + access_token_scopes=resp.scopes, + refresh_token=resp.refresh_token, + refresh_token_expires_at=now + resp.refresh_token_expires_in, + authorization_expires_at=now + resp.authorization_expires_in, + ) + creds.orgs[resp.org_id] = org + creds.selected_org_id = resp.org_id + credentials.save(creds) - return poll_device_token(info) +def get_control_api_access_token() -> str: + """Return a currently-valid control-API access token for the selected org. -def resolve_api_key(api_key: str | None = None) -> str: - """Resolve API key from parameter, environment, or stored credentials. + Transparently refreshes the access token when it has expired (or is about + to expire within ``ACCESS_TOKEN_SKEW_SECONDS``), persisting the refreshed + tokens back to the credentials file. - This is the non-interactive version that raises an error if no key is found. - Use get_api_key() if you want interactive OAuth flow as a fallback. + Raises: + ReauthenticationRequired: The caller must run ``parallel-cli login``. + Reasons: no stored credentials, no control-API tokens for the + selected org, ``authorization_expires_at`` in the past, or + ``refresh_token_expires_at`` in the past. + """ + creds = credentials.load() + org = creds.selected_org() if creds else None + if org is None: + raise ReauthenticationRequired("not logged in; run 'parallel-cli login'") - Args: - api_key: Optional API key. If provided, returns it directly. + tokens = org.control_api + access_token = tokens.access_token + if not access_token: + raise ReauthenticationRequired("not logged in; run 'parallel-cli login'") - Returns: - The resolved API key string. + now = int(time.time()) - Raises: - ValueError: If no API key can be found. + if tokens.authorization_expires_at is not None and now >= tokens.authorization_expires_at: + raise ReauthenticationRequired("authorization grant has expired; run 'parallel-cli login'") + + # Fast path: current access token still valid beyond the skew buffer. + if tokens.access_token_expires_at is None or now < tokens.access_token_expires_at - ACCESS_TOKEN_SKEW_SECONDS: + return access_token + + # Access token is (about to be) expired. Can we refresh? + refresh_token_value = tokens.refresh_token + if not refresh_token_value: + raise ReauthenticationRequired("no refresh token available; run 'parallel-cli login'") + if tokens.refresh_token_expires_at is not None and now >= tokens.refresh_token_expires_at: + raise ReauthenticationRequired("refresh token has expired; run 'parallel-cli login'") + + new_tokens = refresh_access_token(refresh_token_value, client_id=_ensure_client_id()) + _persist_token_response(new_tokens) + return new_tokens.access_token + + +def login_flow( + login_hint: str | None = None, + on_device_code: Callable[[DeviceCodeInfo], None] | None = None, +) -> str: + """Run the full CLI login: register client → device flow → persist tokens → auto-mint data API key. + + ``login_hint`` is forwarded to the device flow's URL enrichment (see + :func:`_build_verification_uri`). Returns the newly-minted data API key. + """ + client_id = _ensure_client_id() + token_resp = _do_device_flow(login_hint=login_hint, on_device_code=on_device_code, client_id=client_id) + _persist_token_response(token_resp) + + api_key, key_name = service.provision_cli_api_key(token_resp.access_token, client_id=client_id) + + creds = credentials.load() + assert creds is not None and creds.selected_org_id == token_resp.org_id + creds.orgs[token_resp.org_id].api_key = api_key + # Drop the v0→v1 legacy placeholder org now that the user is properly + # authenticated against a real org. It only existed for backwards compat + # during migration; keeping it around after login would be dead state. + if credentials.LEGACY_ORG_ID != token_resp.org_id: + creds.orgs.pop(credentials.LEGACY_ORG_ID, None) + credentials.save(creds) + + if not on_device_code: + print(f"Authentication successful! Provisioned data API key: {key_name}", file=sys.stderr) + + return api_key + + +def resolve_api_key(api_key: str | None = None) -> str: + """Resolve API key from parameter, stored credentials, or environment. + + Priority: explicit ``api_key`` argument → stored credentials → ``PARALLEL_API_KEY``. + Raises ``ValueError`` if no key is available. """ if api_key: return api_key - + stored = credentials.get_selected_api_key() + if stored: + return stored env_key = os.environ.get("PARALLEL_API_KEY") if env_key: return env_key - - stored_token = _load_stored_token() - if stored_token: - return stored_token - raise ValueError( - "Parallel API key required. Provide via api_key parameter, " - "PARALLEL_API_KEY environment variable, or run 'parallel-cli login'." + "Parallel API key required. Run 'parallel-cli login', set the " + "PARALLEL_API_KEY environment variable, or pass api_key explicitly." ) def get_api_key( force_login: bool = False, - device: bool = False, on_device_code: Callable[[DeviceCodeInfo], None] | None = None, + login_hint: str | None = None, ) -> str: - """Get API key/token for Parallel API with interactive OAuth fallback. - - Priority: - 1. PARALLEL_API_KEY environment variable - 2. Stored OAuth token - 3. Interactive OAuth flow (or device flow if headless/requested) - - Args: - force_login: Force a new login flow, ignoring stored credentials. - device: Force device authorization flow instead of browser-based PKCE. - on_device_code: Callback invoked with DeviceCodeInfo when using device flow. - Use this to present the verification URL and user code to the user - programmatically (e.g., in a chat message from an AI agent). If not - provided, instructions are printed to stderr. - """ - api_key = os.environ.get("PARALLEL_API_KEY") - if api_key and not force_login: - return api_key + """Get API key, triggering device-flow login + auto-mint as a fallback. + Priority (when not ``force_login``): stored credentials → service-API key + provisioning from stored control-API tokens → ``PARALLEL_API_KEY``. + + ``login_hint`` is forwarded to :func:`login_flow` — see + :func:`_build_verification_uri` for the supported hint format. + """ if not force_login: - stored_token = _load_stored_token() - if stored_token: - return stored_token + stored = credentials.get_selected_api_key() + if stored: + return stored - use_device = device or _is_headless() + # If we still have valid control-API auth but no data API key saved, + # mint a new data key via service API before forcing an interactive + # device-authorization flow or falling back to the environment. + try: + access_token = get_control_api_access_token() + client_id = _ensure_client_id() + minted_api_key, _ = service.provision_cli_api_key(access_token, client_id=client_id) + creds = credentials.load() + if creds is not None: + org = creds.selected_org() + if org is not None: + org.api_key = minted_api_key + credentials.save(creds) + return minted_api_key + except ReauthenticationRequired: + pass + except service.ServiceApiError: + pass - if use_device: - if not on_device_code: - print("Starting device authorization...", file=sys.stderr) - access_token = _do_device_flow(on_device_code=on_device_code) - else: - print("Starting authentication...", file=sys.stderr) - access_token = _do_oauth_flow() + env_key = os.environ.get("PARALLEL_API_KEY") + if env_key: + return env_key - _save_token(access_token) if not on_device_code: - print("Authentication successful! Credentials saved.", file=sys.stderr) - - return access_token + print("Starting device authorization...", file=sys.stderr) + return login_flow(login_hint=login_hint, on_device_code=on_device_code) -def create_client( - api_key: str | None = None, - source: ClientSource = "python", -) -> Parallel: - """Create a configured Parallel client, resolving the API key if not provided. - - Unlike get_client(), this uses resolve_api_key() which raises ValueError - instead of triggering interactive OAuth if no key is found. - - Args: - api_key: Optional API key. Resolved from env/stored credentials if not provided. - source: Source identifier for User-Agent (cli, duckdb, bigquery, etc.) - - Returns: - A configured Parallel client. - """ +def create_client(api_key: str | None = None, source: ClientSource = "python") -> Parallel: + """Create a configured Parallel client, resolving the API key if not provided.""" return Parallel( + base_url=get_api_url(), api_key=resolve_api_key(api_key), default_headers=get_default_headers(source), ) -def get_client( - force_login: bool = False, - source: ClientSource = "python", -) -> Parallel: - """Get a configured Parallel client with interactive OAuth fallback. - - Args: - force_login: Force a new OAuth login flow. - source: Source identifier for User-Agent (cli, duckdb, bigquery, etc.) - - Returns: - A configured Parallel client. - """ - api_key = get_api_key(force_login=force_login) +def get_client(force_login: bool = False, source: ClientSource = "python") -> Parallel: + """Get a configured Parallel client with interactive device-flow fallback.""" return Parallel( - api_key=api_key, + base_url=get_api_url(), + api_key=get_api_key(force_login=force_login), default_headers=get_default_headers(source), ) -def get_async_client( - force_login: bool = False, - source: ClientSource = "python", -) -> AsyncParallel: - """Get a configured async Parallel client with User-Agent header. - - Args: - force_login: Force a new OAuth login flow. - source: Source identifier for User-Agent (cli, duckdb, bigquery, etc.) - - Returns: - A configured async Parallel client. - """ - api_key = get_api_key(force_login=force_login) +def get_async_client(force_login: bool = False, source: ClientSource = "python") -> AsyncParallel: + """Get a configured async Parallel client.""" return AsyncParallel( - base_url="https://api.parallel.ai", - api_key=api_key, + base_url=get_api_url(), + api_key=get_api_key(force_login=force_login), default_headers=get_default_headers(source), ) def logout() -> bool: - """Remove stored OAuth token.""" - if TOKEN_FILE.exists(): - TOKEN_FILE.unlink() - return True - return False - + """Revoke all stored refresh tokens (best-effort) and remove auth files.""" + creds = credentials.load() + if creds is not None: + seen_refresh_tokens: set[str] = set() + for org in creds.orgs.values(): + refresh_token = org.control_api.refresh_token + if not refresh_token or refresh_token in seen_refresh_tokens: + continue + seen_refresh_tokens.add(refresh_token) + try: + revoke_token(refresh_token) + except Exception as e: + print( + f"Warning: refresh token revocation failed ({e}); removing local credentials anyway.", + file=sys.stderr, + ) + return credentials.delete() + + +def get_auth_status() -> dict: + """Get current authentication status. + + Priority matches :func:`resolve_api_key`: stored credentials beat the + ``PARALLEL_API_KEY`` env var. + """ + creds = credentials.load() + if creds is not None: + org = creds.selected_org() + if org and org.api_key: + token_file = credentials.get_active_credentials_file() or credentials.CREDENTIALS_FILE + return { + "authenticated": True, + "method": "oauth", + "token_file": str(token_file), + "version": creds.version, + "selected_org_id": creds.selected_org_id, + "selected_org_name": org.org_name, + "has_control_api_tokens": bool(org.control_api.refresh_token), + } -def get_auth_status() -> dict[str, str | bool | None]: - """Get current authentication status.""" api_key = os.environ.get("PARALLEL_API_KEY") if api_key: return {"authenticated": True, "method": "environment", "token_file": None} - stored_token = _load_stored_token() - if stored_token: - return {"authenticated": True, "method": "oauth", "token_file": str(TOKEN_FILE)} - return {"authenticated": False, "method": None, "token_file": None} diff --git a/parallel_web_tools/core/credentials.py b/parallel_web_tools/core/credentials.py new file mode 100644 index 0000000..b539bab --- /dev/null +++ b/parallel_web_tools/core/credentials.py @@ -0,0 +1,208 @@ +"""Versioned credentials storage for parallel-cli. + +New structured auth state lives in ``auth.json``. The legacy flat +``credentials.json`` file is left in its old shape for backward compatibility +with older CLI releases. +""" + +from __future__ import annotations + +import json +import os +import tempfile +from dataclasses import asdict, dataclass, field +from pathlib import Path + +AUTH_FILE = Path.home() / ".config" / "parallel-web-tools" / "auth.json" +LEGACY_CREDENTIALS_FILE = Path.home() / ".config" / "parallel-web-tools" / "credentials.json" +# Backward-compatible alias used across the codebase/tests for the new auth file. +CREDENTIALS_FILE = AUTH_FILE +CURRENT_VERSION = 1 +LEGACY_ORG_ID = "legacy" + + +@dataclass +class ControlApiTokens: + access_token: str | None = None + access_token_expires_at: int | None = None + access_token_scopes: list[str] = field(default_factory=list) + refresh_token: str | None = None + refresh_token_expires_at: int | None = None + authorization_expires_at: int | None = None + + +@dataclass +class OrgCredentials: + api_key: str | None = None + org_name: str | None = None + control_api: ControlApiTokens = field(default_factory=ControlApiTokens) + + +@dataclass +class Credentials: + version: int = CURRENT_VERSION + selected_org_id: str | None = None + orgs: dict[str, OrgCredentials] = field(default_factory=dict) + # Dynamically-registered OAuth client_id returned by + # ``/getServiceKeys/register``. ``None`` means registration hasn't + # succeeded yet (first boot, migrated v0 file, or prior failure) — the + # next login attempt will retry and fall back to a hardcoded id on error. + client_id: str | None = None + + def selected_org(self) -> OrgCredentials | None: + if self.selected_org_id is None: + return None + return self.orgs.get(self.selected_org_id) + + +def _migrate_v0(raw: dict) -> dict: + """Transform a v0 credentials dict into v1 shape. + + v0 shape: ``{"access_token": ""}`` — a single API key with no org context. + The token is wrapped into a placeholder ``legacy`` org so existing users keep + working without re-authenticating. + """ + legacy_token = raw.get("access_token") + org: dict = {} + if legacy_token: + org["api_key"] = legacy_token + return { + "version": CURRENT_VERSION, + "selected_org_id": LEGACY_ORG_ID if legacy_token else None, + "orgs": {LEGACY_ORG_ID: org} if legacy_token else {}, + } + + +def _credentials_from_dict(data: dict) -> Credentials: + orgs_raw = data.get("orgs") or {} + orgs: dict[str, OrgCredentials] = {} + for org_id, org_data in orgs_raw.items(): + control_raw = (org_data or {}).get("control_api") or {} + orgs[org_id] = OrgCredentials( + api_key=(org_data or {}).get("api_key"), + org_name=(org_data or {}).get("org_name"), + control_api=ControlApiTokens( + access_token=control_raw.get("access_token"), + access_token_expires_at=control_raw.get("access_token_expires_at"), + access_token_scopes=list(control_raw.get("access_token_scopes") or []), + refresh_token=control_raw.get("refresh_token"), + refresh_token_expires_at=control_raw.get("refresh_token_expires_at"), + authorization_expires_at=control_raw.get("authorization_expires_at"), + ), + ) + return Credentials( + version=data.get("version", CURRENT_VERSION), + selected_org_id=data.get("selected_org_id"), + orgs=orgs, + client_id=data.get("client_id"), + ) + + +def _load_json_file(path: Path) -> dict | None: + if not path.exists(): + return None + try: + with open(path) as f: + raw = json.load(f) + except (OSError, json.JSONDecodeError): + return None + if not isinstance(raw, dict): + return None + return raw + + +def _write_json_file(path: Path, payload: dict, temp_prefix: str) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + fd, tmp_path = tempfile.mkstemp( + prefix=temp_prefix, + suffix=".tmp", + dir=str(path.parent), + ) + try: + with os.fdopen(fd, "w") as f: + json.dump(payload, f, indent=2) + os.chmod(tmp_path, 0o600) + os.replace(tmp_path, path) + except Exception: + if os.path.exists(tmp_path): + try: + os.unlink(tmp_path) + except OSError: + pass + raise + + +def load() -> Credentials | None: + """Load credentials from disk. + + Preference order: + 1. ``auth.json`` in the new structured format. + 2. Legacy ``credentials.json`` in the flat v0 format, migrated forward into + ``auth.json`` when the new file does not exist. + + Returns ``None`` if neither file exists or both are unreadable/corrupt. + """ + raw = _load_json_file(CREDENTIALS_FILE) + if raw is not None: + if "version" not in raw: + return _credentials_from_dict(_migrate_v0(raw)) + return _credentials_from_dict(raw) + + legacy_raw = _load_json_file(LEGACY_CREDENTIALS_FILE) + if legacy_raw is None: + return None + creds = _credentials_from_dict(_migrate_v0(legacy_raw)) + save(creds) + return creds + + +def save(creds: Credentials) -> None: + """Write structured auth atomically to ``auth.json``.""" + payload = asdict(creds) + _write_json_file(CREDENTIALS_FILE, payload, ".auth.") + + +def delete() -> bool: + """Remove auth files. Returns True if any local auth file was removed.""" + removed = False + for path in (CREDENTIALS_FILE, LEGACY_CREDENTIALS_FILE): + if path.exists(): + path.unlink() + removed = True + return removed + + +def get_active_credentials_file() -> Path | None: + """Return the on-disk auth file currently backing ``load()``, if any.""" + raw = _load_json_file(CREDENTIALS_FILE) + if raw is not None: + return CREDENTIALS_FILE + legacy_raw = _load_json_file(LEGACY_CREDENTIALS_FILE) + if legacy_raw is not None: + return LEGACY_CREDENTIALS_FILE + return None + + +def get_selected_api_key() -> str | None: + """Return the API key for the currently selected org, or None.""" + creds = load() + if creds is None: + return None + org = creds.selected_org() + if org is None: + return None + return org.api_key + + +def set_api_key_for_org(org_id: str, api_key: str) -> None: + """Write an API key into the given org, creating the org if missing. + + If no org is currently selected, ``selected_org_id`` is set to ``org_id``. + """ + creds = load() or Credentials() + org = creds.orgs.get(org_id) or OrgCredentials() + org.api_key = api_key + creds.orgs[org_id] = org + if creds.selected_org_id is None: + creds.selected_org_id = org_id + save(creds) diff --git a/parallel_web_tools/core/endpoints.py b/parallel_web_tools/core/endpoints.py new file mode 100644 index 0000000..4b8a6dc --- /dev/null +++ b/parallel_web_tools/core/endpoints.py @@ -0,0 +1,45 @@ +"""Endpoint and client-identity configuration for parallel-cli. + +Three base URLs are configurable via env vars so the CLI can be pointed at a +local dev stack: + +- ``PARALLEL_PLATFORM_URL`` — the platform that serves ``/getServiceKeys/*`` + (device authorization, token exchange, revocation). Default + ``https://platform.parallel.ai``; for local dev set to ``http://localhost:3000``. + +- ``PARALLEL_SERVICE_API_URL`` — the service/account API that serves + ``/service/v1/*`` (apps, API-key management). Default + ``https://api.parallel.ai/account``; for local dev set to + ``http://localhost:8090``. + +- ``PARALLEL_API_URL`` — the data API (search, extract, research, enrich, + findall, monitor). Default ``https://api.parallel.ai``. +""" + +from __future__ import annotations + +import os + +DEFAULT_PLATFORM_URL = "https://platform.parallel.ai" +DEFAULT_SERVICE_API_URL = "https://api.parallel.ai/account" +DEFAULT_API_URL = "https://api.parallel.ai" + +CLIENT_ID = "parallel-cli" +DEFAULT_SCOPE = "keys:read keys:create keys:delete apps:read apps:create apps:delete balance:read balance:add" + +PARALLEL_CLI_APP_NAME = "parallel-cli Users" + + +def get_platform_url() -> str: + """Return the platform base URL (no trailing slash).""" + return os.environ.get("PARALLEL_PLATFORM_URL", DEFAULT_PLATFORM_URL).rstrip("/") + + +def get_service_api_url() -> str: + """Return the service API base URL (no trailing slash).""" + return os.environ.get("PARALLEL_SERVICE_API_URL", DEFAULT_SERVICE_API_URL).rstrip("/") + + +def get_api_url() -> str: + """Return the data API base URL (no trailing slash).""" + return os.environ.get("PARALLEL_API_URL", DEFAULT_API_URL).rstrip("/") diff --git a/parallel_web_tools/core/service.py b/parallel_web_tools/core/service.py new file mode 100644 index 0000000..bb2d4bb --- /dev/null +++ b/parallel_web_tools/core/service.py @@ -0,0 +1,138 @@ +"""Service API client for parallel-cli. + +Wraps the subset of ``/service/v1/*`` endpoints the CLI consumes: + +- ``GET /service/v1/apps`` — list apps for the caller's org +- ``POST /service/v1/apps/{app_id}/keys`` — create an API key on an app +- ``GET /service/v1/balance`` — read the org's prepaid balance +- ``POST /service/v1/balance/add`` — charge Stripe and top up balance + +Request and response shapes are parsed with the Pydantic models in +:mod:`parallel_web_tools.core.service_types` (auto-generated from the OpenAPI +spec; regenerate with ``scripts/generate_service_types.py``). +""" + +from __future__ import annotations + +import json +import time +import urllib.error +import urllib.request +from typing import Any + +from pydantic import ValidationError + +from parallel_web_tools.core.endpoints import PARALLEL_CLI_APP_NAME, get_service_api_url +from parallel_web_tools.core.service_types import ( + AddBalanceRequest, + AppItem, + BalanceResponse, + CreateApiKeyRequestModel, + CreateKeyResponse, + GetAppsForOrgResponseModel, +) + + +class ServiceApiError(Exception): + """Raised when the service API returns an error or an unexpected payload.""" + + +def _request( + method: str, + path: str, + access_token: str, + body: dict | None = None, + timeout: int = 30, +) -> Any: + url = f"{get_service_api_url()}{path}" + data = json.dumps(body).encode() if body is not None else None + headers = { + "Authorization": f"Bearer {access_token}", + "Accept": "application/json", + } + if data is not None: + headers["Content-Type"] = "application/json" + req = urllib.request.Request(url, data=data, headers=headers, method=method) + try: + with urllib.request.urlopen(req, timeout=timeout) as response: + raw = response.read().decode() + return json.loads(raw) if raw else None + except urllib.error.HTTPError as e: + body_text = e.read().decode() + raise ServiceApiError(f"{method} {path} failed: {e.code} - {body_text}") from e + + +def list_apps(access_token: str) -> list[AppItem]: + """Return all apps for the caller's org.""" + data = _request("GET", "/service/v1/apps", access_token) + try: + resp = GetAppsForOrgResponseModel.model_validate(data or {}) + except ValidationError as e: + raise ServiceApiError(f"Unexpected /service/v1/apps response: {e}") from e + return resp.apps or [] + + +def create_api_key(access_token: str, app_id: str, api_key_name: str) -> CreateKeyResponse: + """Create an API key on the given app and return the typed result.""" + body = CreateApiKeyRequestModel(api_key_name=api_key_name).model_dump() + data = _request("POST", f"/service/v1/apps/{app_id}/keys", access_token, body=body) + try: + return CreateKeyResponse.model_validate(data) + except ValidationError as e: + raise ServiceApiError(f"Unexpected create_api_key response: {e}") from e + + +def _build_key_name(client_id: str | None = None, now: float | None = None) -> str: + """Return a CLI-minted key name. + + Uses the registered OAuth ``client_id`` as the high-entropy prefix, with a + ``YYYY-MM-DD-HHMM`` suffix so the same client can mint multiple keys and + still distinguish them. Falls back to the plain ``parallel-cli`` prefix + when no ``client_id`` is available (e.g. registration failed earlier). + """ + prefix = client_id or "parallel-cli" + return f"{prefix}-{time.strftime('%Y-%m-%d-%H%M', time.localtime(now))}" + + +def get_balance(access_token: str) -> BalanceResponse: + """Return the caller's current org balance.""" + data = _request("GET", "/service/v1/balance", access_token) + try: + return BalanceResponse.model_validate(data) + except ValidationError as e: + raise ServiceApiError(f"Unexpected /service/v1/balance response: {e}") from e + + +def add_balance(access_token: str, amount_cents: int, idempotency_key: str) -> BalanceResponse: + """Charge the org's payment method and top up the prepaid balance. + + Returns the updated :class:`BalanceResponse`. ``idempotency_key`` must be + high-entropy; the server dedupes repeat charges for at least 24h when the + same key is submitted. + """ + body = AddBalanceRequest(amount_cents=amount_cents, idempotency_key=idempotency_key).model_dump() + data = _request("POST", "/service/v1/balance/add", access_token, body=body) + try: + return BalanceResponse.model_validate(data) + except ValidationError as e: + raise ServiceApiError(f"Unexpected /service/v1/balance/add response: {e}") from e + + +def provision_cli_api_key(access_token: str, client_id: str | None = None) -> tuple[str, str]: + """Find the ``parallel-cli Users`` app and mint a fresh API key on it. + + Returns ``(raw_api_key, key_name)``. The raw key is only returned once by + the server — at creation time — so the caller must persist it immediately. + """ + apps = list_apps(access_token) + match = next((a for a in apps if a.app_name == PARALLEL_CLI_APP_NAME), None) + if match is None: + raise ServiceApiError( + f"No app named {PARALLEL_CLI_APP_NAME!r} found for this org. " + "It should be auto-created during device authorization; contact support if missing." + ) + key_name = _build_key_name(client_id) + created = create_api_key(access_token, match.app_id, key_name) + if not created.raw_api_key: + raise ServiceApiError("Server returned no raw_api_key on key creation; cannot persist a usable key without it.") + return created.raw_api_key, key_name diff --git a/parallel_web_tools/core/service_types.py b/parallel_web_tools/core/service_types.py new file mode 100644 index 0000000..e5154bf --- /dev/null +++ b/parallel_web_tools/core/service_types.py @@ -0,0 +1,98 @@ +# generated by datamodel-codegen: +# filename: http://localhost:8090/service/openapi.json +# timestamp: 2026-04-22T20:07:38+00:00 + +from __future__ import annotations + +from typing import Annotated + +from pydantic import BaseModel, Field + + +class BalanceResponse(BaseModel): + org_id: Annotated[str, Field(description="Organization ID", title="Org Id")] + credit_balance_cents: Annotated[ + float, + Field( + description="Total available prepaid balance in cents (credits + prepaid commits). Always 0 when will_invoice is true.", + title="Credit Balance Cents", + ), + ] + pending_debit_balance_cents: Annotated[ + float | None, + Field( + description="Balance in cents currently held for inflight tasks plus charges not yet synced to the billing provider. Always 0 when will_invoice is true.", + title="Pending Debit Balance Cents", + ), + ] = 0 + will_invoice: Annotated[ + bool | None, + Field( + description="True if this organization is billed by invoice (postpaid) rather than from a prepaid credit balance. Invoice-only orgs cannot add balance via this API.", + title="Will Invoice", + ), + ] = False + + +class ValidationError(BaseModel): + loc: Annotated[list[str | int], Field(title="Location")] + msg: Annotated[str, Field(title="Message")] + type: Annotated[str, Field(title="Error Type")] + + +class CreateAppResponseModel(BaseModel): + app_id: Annotated[str, Field(description="App ID", title="App Id")] + + +class AddBalanceRequest(BaseModel): + amount_cents: Annotated[ + int, + Field( + description="Amount in cents to charge and add to the balance.", + title="Amount Cents", + ), + ] + idempotency_key: Annotated[ + str, + Field( + description="Required idempotency key. Stripe dedupes the charge server-side for at least 24h when the same key is submitted again for the same org (see https://docs.stripe.com/api/idempotent_requests). Pick a high-entropy value (e.g. a UUID) so distinct agent attempts do not collide.", + max_length=128, + min_length=1, + title="Idempotency Key", + ), + ] + + +class CreateKeyResponse(BaseModel): + api_key_id: Annotated[str, Field(description="API Key ID", title="Api Key Id")] + api_key_name: Annotated[str, Field(description="API Key Name", title="Api Key Name")] + app_id: Annotated[str, Field(description="App ID", title="App Id")] + app_name: Annotated[str, Field(description="App Name", title="App Name")] + created_by_user_id: Annotated[str, Field(description="Created by User ID", title="Created By User Id")] + created_by_user_email: Annotated[str, Field(description="Created by User Email", title="Created By User Email")] + display_value: Annotated[str, Field(description="Display Value", title="Display Value")] + raw_api_key: Annotated[str | None, Field(description="Raw API Key", title="Raw Api Key")] = None + created_at: Annotated[int, Field(description="Created At", title="Created At")] + + +class CreateApiKeyRequestModel(BaseModel): + api_key_name: Annotated[str, Field(description="API Key Name", title="Api Key Name")] + + +class AppItem(BaseModel): + app_name: Annotated[str, Field(description="App name", title="App Name")] + org_name: Annotated[str | None, Field(description="Organization name", title="Org Name")] + app_id: Annotated[str, Field(description="App ID", title="App Id")] + org_id: Annotated[str, Field(description="Organization ID", title="Org Id")] + + +class CreateAppRequestModel(BaseModel): + app_name: Annotated[str, Field(description="App name", title="App Name")] + + +class HTTPValidationError(BaseModel): + detail: Annotated[list[ValidationError] | None, Field(title="Detail")] = None + + +class GetAppsForOrgResponseModel(BaseModel): + apps: Annotated[list[AppItem] | None, Field(description="List of apps", title="Apps")] = None diff --git a/parallel_web_tools/integrations/duckdb/_relation.py b/parallel_web_tools/integrations/duckdb/_relation.py new file mode 100644 index 0000000..2045295 --- /dev/null +++ b/parallel_web_tools/integrations/duckdb/_relation.py @@ -0,0 +1,22 @@ +from __future__ import annotations + +import pandas as pd + + +class DuckDBResultRelation: + """Thin wrapper that preserves `None` values in `fetchdf()` output.""" + + def __init__(self, relation): + self._relation = relation + + def fetchdf(self): + df = self._relation.fetchdf() + return df.astype(object).where(pd.notna(df), None) + + def __getattr__(self, name): + return getattr(self._relation, name) + + +def wrap_relation(relation): + """Wrap a DuckDB relation so pandas materialization keeps nulls as `None`.""" + return DuckDBResultRelation(relation) diff --git a/parallel_web_tools/integrations/duckdb/batch.py b/parallel_web_tools/integrations/duckdb/batch.py index 1ff0b55..f7d57f6 100644 --- a/parallel_web_tools/integrations/duckdb/batch.py +++ b/parallel_web_tools/integrations/duckdb/batch.py @@ -31,6 +31,7 @@ from parallel_web_tools.core import EnrichmentResult, build_output_schema, enrich_batch from parallel_web_tools.core.sql_utils import quote_identifier +from parallel_web_tools.integrations.duckdb._relation import wrap_relation if TYPE_CHECKING: DuckDBEnrichmentResult = EnrichmentResult[duckdb.DuckDBPyRelation] @@ -119,7 +120,7 @@ def enrich_table( if include_basis: empty_cols += ", NULL::VARCHAR AS _basis" empty_query = f"SELECT {select_cols}, {empty_cols} FROM {quote_identifier(source_table)} WHERE 1=0" - rel = conn.sql(empty_query) + rel = wrap_relation(conn.sql(empty_query)) return EnrichmentResult( result=rel, @@ -209,9 +210,9 @@ def enrich_table( if result_table: result_quoted = quote_identifier(result_table) conn.execute(f"CREATE TABLE {result_quoted} AS SELECT * FROM {temp_quoted}") - rel = conn.sql(f"SELECT * FROM {result_quoted}") + rel = wrap_relation(conn.sql(f"SELECT * FROM {result_quoted}")) else: - rel = conn.sql(f"SELECT * FROM {temp_quoted}") + rel = wrap_relation(conn.sql(f"SELECT * FROM {temp_quoted}")) elapsed = time.time() - start_time diff --git a/parallel_web_tools/integrations/duckdb/findall.py b/parallel_web_tools/integrations/duckdb/findall.py index 5d7ed24..1eefbd6 100644 --- a/parallel_web_tools/integrations/duckdb/findall.py +++ b/parallel_web_tools/integrations/duckdb/findall.py @@ -29,6 +29,7 @@ from parallel_web_tools.core.findall import run_findall from parallel_web_tools.core.result import EnrichmentResult from parallel_web_tools.core.sql_utils import quote_identifier +from parallel_web_tools.integrations.duckdb._relation import wrap_relation def _unpack_output(candidate: dict[str, Any]) -> dict[str, Any]: @@ -165,7 +166,7 @@ def findall_table( if not col_names: # No results — return an empty relation - rel = conn.sql("SELECT 1 WHERE 1=0") + rel = wrap_relation(conn.sql("SELECT 1 WHERE 1=0")) return EnrichmentResult( result=rel, success_count=0, @@ -193,9 +194,9 @@ def findall_table( if result_table: result_quoted = quote_identifier(result_table) conn.execute(f"CREATE OR REPLACE TABLE {result_quoted} AS SELECT * FROM {temp_quoted}") - rel = conn.sql(f"SELECT * FROM {result_quoted}") + rel = wrap_relation(conn.sql(f"SELECT * FROM {result_quoted}")) else: - rel = conn.sql(f"SELECT * FROM {temp_quoted}") + rel = wrap_relation(conn.sql(f"SELECT * FROM {temp_quoted}")) return EnrichmentResult( result=rel, diff --git a/parallel_web_tools/integrations/duckdb/udf.py b/parallel_web_tools/integrations/duckdb/udf.py index d110400..f929a55 100644 --- a/parallel_web_tools/integrations/duckdb/udf.py +++ b/parallel_web_tools/integrations/duckdb/udf.py @@ -62,7 +62,13 @@ async def _enrich_all_async( from parallel import AsyncParallel from parallel.types import JsonSchemaParam, TaskSpecParam - client = AsyncParallel(api_key=api_key, default_headers=get_default_headers("duckdb")) + from parallel_web_tools.core.endpoints import get_api_url + + client = AsyncParallel( + base_url=get_api_url(), + api_key=api_key, + default_headers=get_default_headers("duckdb"), + ) output_schema = build_output_schema(output_columns) task_spec = TaskSpecParam(output_schema=JsonSchemaParam(type="json", json_schema=output_schema)) diff --git a/parallel_web_tools/integrations/spark/streaming.py b/parallel_web_tools/integrations/spark/streaming.py index 38affe7..5ee4ffb 100644 --- a/parallel_web_tools/integrations/spark/streaming.py +++ b/parallel_web_tools/integrations/spark/streaming.py @@ -178,7 +178,10 @@ def enrich_streaming_batch( try: from parallel import Parallel + from parallel_web_tools.core.endpoints import get_api_url + client = Parallel( + base_url=get_api_url(), api_key=resolve_api_key(api_key), default_headers=get_default_headers("spark"), ) diff --git a/parallel_web_tools/integrations/spark/udf.py b/parallel_web_tools/integrations/spark/udf.py index 5971e5a..1959509 100644 --- a/parallel_web_tools/integrations/spark/udf.py +++ b/parallel_web_tools/integrations/spark/udf.py @@ -68,7 +68,7 @@ def _parallel_enrich_partition( valid_indices.append(i) if not valid_items: - return pd.Series([None] * len(items)) + return pd.Series([None] * len(items), dtype=object) # Process valid items in chunks of _MAX_CHUNK_SIZE via enrich_batch all_results: list[dict] = [] @@ -100,7 +100,7 @@ def _parallel_enrich_partition( for i, result in zip(valid_indices, json_results, strict=True): output[i] = result - return pd.Series(output) + return pd.Series(output, dtype=object) def create_parallel_enrich_udf( diff --git a/pyproject.toml b/pyproject.toml index 37c9bc1..717452c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -98,6 +98,7 @@ dev = [ "pre-commit>=4.6.0", "ruff>=0.15.0", "ty>=0.0.33", + "datamodel-code-generator[ruff]>=0.26.0", ] [tool.hatch.build.targets.wheel] diff --git a/scripts/generate_service_types.py b/scripts/generate_service_types.py new file mode 100644 index 0000000..193035e --- /dev/null +++ b/scripts/generate_service_types.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python3 +"""Generate Pydantic models from the service API OpenAPI spec. + +Mirrors the npm workflow used elsewhere: + + openapi-typescript http://127.0.0.1:8090/openapi.json -o ./app/api/account-service-types.ts + +Usage: + + uv run python scripts/generate_service_types.py # prod (default) + uv run python scripts/generate_service_types.py --env dev # localhost:8090 + uv run python scripts/generate_service_types.py --url + +Output is written to ``parallel_web_tools/core/service_types.py``. +Requires ``datamodel-code-generator`` (installed via the ``dev`` extra). +""" + +from __future__ import annotations + +import argparse +import subprocess +import sys +from pathlib import Path + +ENV_URLS = { + "prod": "https://api.parallel.ai/account/service/openapi.json", + "dev": "http://localhost:8090/service/openapi.json", +} + +OUTPUT_PATH = Path(__file__).resolve().parent.parent / "parallel_web_tools" / "core" / "service_types.py" + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "--env", + choices=("prod", "dev"), + default="prod", + help="Which environment's OpenAPI spec to fetch (default: prod).", + ) + parser.add_argument( + "--url", + help="Custom OpenAPI URL; overrides --env.", + ) + parser.add_argument( + "--output", + type=Path, + default=OUTPUT_PATH, + help=f"Output file (default: {OUTPUT_PATH.relative_to(Path.cwd()) if OUTPUT_PATH.is_relative_to(Path.cwd()) else OUTPUT_PATH}).", + ) + args = parser.parse_args() + + url = args.url or ENV_URLS[args.env] + print(f"Generating service types from {url} → {args.output}", file=sys.stderr) + + args.output.parent.mkdir(parents=True, exist_ok=True) + cmd = [ + sys.executable, + "-m", + "datamodel_code_generator", + "--url", + url, + "--input-file-type", + "openapi", + "--output", + str(args.output), + "--output-model-type", + "pydantic_v2.BaseModel", + "--target-python-version", + "3.10", + "--use-standard-collections", + "--use-union-operator", + "--use-annotated", + "--snake-case-field", + "--formatters", + "ruff-format", + "ruff-check", + ] + result = subprocess.run(cmd) + return result.returncode + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tests/conftest.py b/tests/conftest.py index 5d521ba..ea743ad 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,8 +1,22 @@ """Shared pytest fixtures for the parallel-web-tools test suite.""" +import asyncio +import socket +import subprocess +import urllib.request +import webbrowser + +import httpx import pytest +def _blocked_external_io(kind: str): + def fail(*args, **kwargs): + raise AssertionError(f"{kind} is disabled in tests; mock the request instead.") + + return fail + + @pytest.fixture(autouse=True) def _isolate_cwd(tmp_path, monkeypatch): """Run every test in a fresh tmp dir. @@ -14,3 +28,54 @@ def _isolate_cwd(tmp_path, monkeypatch): drop the per-test `monkeypatch.chdir(tmp_path)` boilerplate. """ monkeypatch.chdir(tmp_path) + + +@pytest.fixture(autouse=True) +def _block_network(monkeypatch): + """Prevent accidental outbound network calls during tests. + + Tests should mock the specific transport layer they exercise. If something + reaches the real socket layer, fail fast instead of hanging on live auth or + API calls. + """ + + fail = _blocked_external_io("Network access") + + monkeypatch.setattr(socket, "create_connection", fail) + monkeypatch.setattr(socket, "getaddrinfo", fail) + monkeypatch.setattr(socket, "gethostbyname", fail) + monkeypatch.setattr(socket, "gethostbyname_ex", fail) + monkeypatch.setattr(socket, "gethostbyaddr", fail) + monkeypatch.setattr(socket, "getnameinfo", fail) + monkeypatch.setattr(socket.socket, "connect", fail) + monkeypatch.setattr(socket.socket, "connect_ex", fail) + monkeypatch.setattr(asyncio, "open_connection", fail) + monkeypatch.setattr(urllib.request, "urlopen", fail) + monkeypatch.setattr(httpx, "get", fail) + monkeypatch.setattr(httpx, "post", fail) + monkeypatch.setattr(httpx, "request", fail) + monkeypatch.setattr(httpx, "stream", fail) + monkeypatch.setattr(httpx.Client, "send", fail) + monkeypatch.setattr(httpx.AsyncClient, "send", fail) + + +@pytest.fixture(autouse=True) +def _block_subprocess(monkeypatch): + """Prevent subprocesses from escaping the in-process test harness.""" + + fail = _blocked_external_io("Subprocess execution") + + monkeypatch.setattr(subprocess, "Popen", fail) + monkeypatch.setattr(subprocess, "run", fail) + monkeypatch.setattr(subprocess, "call", fail) + monkeypatch.setattr(subprocess, "check_call", fail) + monkeypatch.setattr(subprocess, "check_output", fail) + + +@pytest.fixture(autouse=True) +def _block_browser_launch(monkeypatch): + """Prevent tests from opening a real browser window.""" + + monkeypatch.setattr(webbrowser, "open", lambda *args, **kwargs: True) + monkeypatch.setattr(webbrowser, "open_new", lambda *args, **kwargs: True) + monkeypatch.setattr(webbrowser, "open_new_tab", lambda *args, **kwargs: True) diff --git a/tests/test_auth.py b/tests/test_auth.py index f5f18c9..1368b6f 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -1,584 +1,1138 @@ -"""Tests for the auth module.""" +"""Tests for the auth module (device flow against /getServiceKeys).""" +import io import json import os import urllib.error +from contextlib import contextmanager +from dataclasses import replace +from email.message import Message from unittest import mock import pytest +from parallel_web_tools.core import credentials from parallel_web_tools.core.auth import ( + ACCESS_TOKEN_SKEW_SECONDS, DeviceCodeInfo, + ReauthenticationRequired, + TokenResponse, + _build_verification_uri, _do_device_flow, - _generate_code_challenge, - _generate_code_verifier, + _ensure_client_id, _is_headless, - _load_stored_token, - _save_token, + _persist_token_response, create_client, get_api_key, get_auth_status, + get_control_api_access_token, + login_flow, logout, poll_device_token, + refresh_access_token, + register_client, request_device_code, resolve_api_key, + revoke_token, + send_magic_link, ) +# --------------------------------------------------------------------------- +# Shared helpers / fixtures +# --------------------------------------------------------------------------- -class TestPKCE: - """Tests for PKCE code generation.""" - def test_generate_code_verifier_length(self): - """Code verifier should be URL-safe base64.""" - verifier = _generate_code_verifier() - assert len(verifier) >= 43 # Base64 encoded 32 bytes - assert verifier.replace("-", "").replace("_", "").isalnum() +@pytest.fixture +def creds_file(tmp_path, monkeypatch): + """Patch auth.json path to a tmp path for isolation.""" + path = tmp_path / "auth.json" + monkeypatch.setattr(credentials, "AUTH_FILE", path) + monkeypatch.setattr(credentials, "CREDENTIALS_FILE", path) + monkeypatch.setattr(credentials, "LEGACY_CREDENTIALS_FILE", tmp_path / "credentials.json") + return path - def test_generate_code_verifier_unique(self): - """Each code verifier should be unique.""" - verifiers = [_generate_code_verifier() for _ in range(10)] - assert len(set(verifiers)) == 10 - def test_generate_code_challenge(self): - """Code challenge should be SHA256 of verifier, base64 encoded.""" - verifier = "test_verifier_12345" - challenge = _generate_code_challenge(verifier) +@pytest.fixture +def legacy_file(tmp_path): + return tmp_path / "credentials.json" - # Challenge should be URL-safe base64 without padding - assert "=" not in challenge - assert challenge.replace("-", "").replace("_", "").isalnum() +@pytest.fixture +def no_sleep(monkeypatch): + """Skip real sleeps in the device-code poll loop.""" + monkeypatch.setattr("parallel_web_tools.core.auth.time.sleep", mock.MagicMock()) -class TestTokenStorage: - """Tests for token storage functions.""" - def test_save_and_load_token(self, tmp_path): - """Token should be saveable and loadable.""" - test_token = "test_token_12345" - token_file = tmp_path / "tokens.json" +@pytest.fixture +def mock_ensure_client_id(monkeypatch): + """Stub out _ensure_client_id to avoid real /getServiceKeys/register calls. - with mock.patch("parallel_web_tools.core.auth.TOKEN_FILE", token_file): - _save_token(test_token) - - # File should exist with correct permissions - assert token_file.exists() - assert oct(token_file.stat().st_mode)[-3:] == "600" - - # Token should be loadable - loaded = _load_stored_token() - assert loaded == test_token - - def test_load_nonexistent_token(self, tmp_path): - """Loading from nonexistent file should return None.""" - token_file = tmp_path / "nonexistent.json" - - with mock.patch("parallel_web_tools.core.auth.TOKEN_FILE", token_file): - loaded = _load_stored_token() - assert loaded is None - - def test_load_corrupted_token(self, tmp_path): - """Loading corrupted JSON should return None.""" - token_file = tmp_path / "corrupted.json" - token_file.write_text("not valid json {{{") - - with mock.patch("parallel_web_tools.core.auth.TOKEN_FILE", token_file): - loaded = _load_stored_token() - assert loaded is None - - -class TestGetApiKey: - """Tests for get_api_key function.""" - - def test_env_var_priority(self, tmp_path): - """Environment variable should take priority.""" - env_key = "test_env_key_12345" - token_file = tmp_path / "tokens.json" - - with mock.patch.dict(os.environ, {"PARALLEL_API_KEY": env_key}): - with mock.patch("parallel_web_tools.core.auth.TOKEN_FILE", token_file): - result = get_api_key() - assert result == env_key + Returns the value the stub will produce so tests can assert on it. + """ + value = "cid_test" + monkeypatch.setattr("parallel_web_tools.core.auth._ensure_client_id", lambda: value) + return value - def test_stored_token_second_priority(self, tmp_path): - """Stored token should be used if no env var.""" - stored_token = "stored_token_12345" - token_file = tmp_path / "tokens.json" - token_file.parent.mkdir(parents=True, exist_ok=True) - token_file.write_text(json.dumps({"access_token": stored_token})) - with mock.patch.dict(os.environ, {}, clear=True): - # Remove PARALLEL_API_KEY if it exists - os.environ.pop("PARALLEL_API_KEY", None) +def _http_error(status: int, body: dict) -> urllib.error.HTTPError: + return urllib.error.HTTPError( + url="https://example.com", + code=status, + msg="Error", + hdrs=Message(), + fp=io.BytesIO(json.dumps(body).encode()), + ) - with mock.patch("parallel_web_tools.core.auth.TOKEN_FILE", token_file): - result = get_api_key() - assert result == stored_token - def test_force_login_ignores_env_var(self, tmp_path): - """force_login should skip env var and stored token.""" - env_key = "test_env_key" - token_file = tmp_path / "tokens.json" +def _urlopen_stub(responses, capture: dict | None = None): + """Build a urlopen side_effect that yields each response in order. - with mock.patch.dict(os.environ, {"PARALLEL_API_KEY": env_key}): - with mock.patch("parallel_web_tools.core.auth.TOKEN_FILE", token_file): - with mock.patch("parallel_web_tools.core.auth._do_oauth_flow") as mock_oauth: - with mock.patch("parallel_web_tools.core.auth._do_device_flow") as mock_device: - mock_oauth.return_value = "new_oauth_token" - mock_device.return_value = "new_device_token" + Each entry in ``responses`` is a dict (JSON-encoded body), bytes (raw body), + or pre-built HTTPError. A single value may be passed directly. When + ``capture`` is provided it is populated on each call with url/body/headers/method. + """ + if not isinstance(responses, list): + responses = [responses] + idx = [0] - result = get_api_key(force_login=True) + @contextmanager + def impl(req, timeout=None): + if capture is not None: + capture["url"] = req.full_url + capture["body"] = req.data.decode() if req.data else "" + capture["headers"] = dict(req.header_items()) + capture["method"] = req.get_method() + i = min(idx[0], len(responses) - 1) + idx[0] += 1 + r = responses[i] + if isinstance(r, urllib.error.HTTPError): + raise r + payload = r if isinstance(r, (bytes, bytearray)) else json.dumps(r).encode() + yield io.BytesIO(bytes(payload)) + + return impl + + +def _patch_auth_urlopen(responses, capture: dict | None = None): + return mock.patch( + "parallel_web_tools.core.auth.urllib.request.urlopen", + side_effect=_urlopen_stub(responses, capture), + ) - # Either flow may be chosen depending on environment - assert result in ("new_oauth_token", "new_device_token") +DEVICE_RESPONSE = { + "device_code": "a" * 48, + "user_code": "BCDF-GHJK", + "verification_uri": "http://localhost:3000/getServiceKeys/device", + "verification_uri_complete": "http://localhost:3000/getServiceKeys/device?user_code=BCDF-GHJK", + "expires_in": 600, + "interval": 5, +} -class TestAuthStatus: - """Tests for get_auth_status function.""" - def test_status_with_env_var(self): - """Status should show environment method when env var set.""" - with mock.patch.dict(os.environ, {"PARALLEL_API_KEY": "test_key"}): - status = get_auth_status() - assert status["authenticated"] is True - assert status["method"] == "environment" +TOKEN_RESPONSE_JSON = { + "access_token": "at_123", + "refresh_token": "rt_123", + "expires_in": 600, + "refresh_token_expires_in": 604800, + "authorization_expires_in": 7776000, + "org_id": "org_abc", + "org_name": "Acme Org", + "scope": "keys:read balance:write", + "token_type": "Bearer", +} - def test_status_with_stored_token(self, tmp_path): - """Status should show oauth method when token stored.""" - token_file = tmp_path / "tokens.json" - token_file.parent.mkdir(parents=True, exist_ok=True) - token_file.write_text(json.dumps({"access_token": "stored_token"})) - with mock.patch.dict(os.environ, {}, clear=True): - os.environ.pop("PARALLEL_API_KEY", None) +SAMPLE_DEVICE_CODE_INFO = DeviceCodeInfo( + device_code="a" * 48, + user_code="BCDF-GHJK", + verification_uri="http://localhost:3000/getServiceKeys/device", + verification_uri_complete="http://localhost:3000/getServiceKeys/device?user_code=BCDF-GHJK", + expires_in=600, + interval=5, +) - with mock.patch("parallel_web_tools.core.auth.TOKEN_FILE", token_file): - status = get_auth_status() - assert status["authenticated"] is True - assert status["method"] == "oauth" - assert status["token_file"] == str(token_file) - def test_status_not_authenticated(self, tmp_path): - """Status should show not authenticated when nothing configured.""" - token_file = tmp_path / "nonexistent.json" +_TOKEN_RESPONSE_DEFAULT = TokenResponse( + access_token="at_123", + refresh_token="rt_123", + expires_in=600, + refresh_token_expires_in=604800, + authorization_expires_in=7776000, + org_id="org_abc", + org_name="Acme Org", + scope="keys:read balance:write", +) - with mock.patch.dict(os.environ, {}, clear=True): - os.environ.pop("PARALLEL_API_KEY", None) - with mock.patch("parallel_web_tools.core.auth.TOKEN_FILE", token_file): - status = get_auth_status() - assert status["authenticated"] is False - assert status["method"] is None +def _token_response(**overrides) -> TokenResponse: + """Build a TokenResponse with test defaults.""" + return replace(_TOKEN_RESPONSE_DEFAULT, **overrides) -class TestLogout: - """Tests for logout function.""" +# --------------------------------------------------------------------------- +# _build_verification_uri +# --------------------------------------------------------------------------- - def test_logout_removes_token(self, tmp_path): - """Logout should remove stored token file.""" - token_file = tmp_path / "tokens.json" - token_file.parent.mkdir(parents=True, exist_ok=True) - token_file.write_text(json.dumps({"access_token": "test"})) - with mock.patch("parallel_web_tools.core.auth.TOKEN_FILE", token_file): - result = logout() - assert result is True - assert not token_file.exists() +class TestBuildVerificationUri: + def test_appends_agent_true(self): + url = _build_verification_uri("http://localhost:3000/getServiceKeys/device?user_code=ABCD", None) + assert "user_code=ABCD" in url - def test_logout_no_token(self, tmp_path): - """Logout should return False if no token exists.""" - token_file = tmp_path / "nonexistent.json" + def test_passes_login_hint_through_url_encoded(self): + url = _build_verification_uri( + "http://localhost:3000/getServiceKeys/device?user_code=ABCD", + "login=email,e=user@example.com", + ) + # The hint value is URL-encoded: ','→'%2C', '='→'%3D', '@'→'%40' + assert "login_hint=login%3Demail%2Ce%3Duser%40example.com" in url + + def test_supports_non_email_methods(self): + # google: no email needed + google_url = _build_verification_uri("http://localhost:3000/d", "login=google") + assert "login_hint=login%3Dgoogle" in google_url + # sso: email carried as ,e=… + sso_url = _build_verification_uri("http://localhost:3000/d", "login=sso,e=u@example.com") + assert "login_hint=login%3Dsso%2Ce%3Du%40example.com" in sso_url + + def test_no_hint_omits_login_hint(self): + url = _build_verification_uri("http://localhost:3000/getServiceKeys/device", None) + assert "login_hint" not in url + + +# --------------------------------------------------------------------------- +# register_client / _ensure_client_id +# --------------------------------------------------------------------------- + + +class TestRegisterClient: + def test_returns_client_id_from_response(self): + with _patch_auth_urlopen({"client_id": "cid_xyz"}): + assert register_client() == "cid_xyz" + + def test_posts_json_with_expected_payload(self): + captured: dict = {} + with _patch_auth_urlopen({"client_id": "cid_xyz"}, capture=captured): + register_client() + + assert "/getServiceKeys/register" in captured["url"] + assert captured["method"] == "POST" + body = json.loads(captured["body"]) + assert body["client_name"] == "parallel-cli" + # Per user request, no redirect_uris field is sent. + assert "redirect_uris" not in body + # Platform block present with at least system/machine (always populated + # by the stdlib platform module). + assert "system" in body["platform"] + assert "machine" in body["platform"] + assert body["platform"]["os_name"] == os.name - with mock.patch("parallel_web_tools.core.auth.TOKEN_FILE", token_file): - result = logout() - assert result is False + def test_raises_on_http_error(self): + with _patch_auth_urlopen(_http_error(500, {"error": "internal"})): + with pytest.raises(Exception, match="Client registration failed"): + register_client() + + def test_ignores_platform_processor_failures(self): + with ( + mock.patch("parallel_web_tools.core.auth._platform.processor", side_effect=AssertionError("blocked")), + _patch_auth_urlopen({"client_id": "cid_xyz"}), + ): + assert register_client() == "cid_xyz" + + +class TestEnsureClientId: + def test_returns_stored_client_id_without_registering(self, creds_file): + credentials.save(credentials.Credentials(client_id="cid_stored")) + with mock.patch("parallel_web_tools.core.auth.register_client") as mock_reg: + assert _ensure_client_id() == "cid_stored" + mock_reg.assert_not_called() + + def test_registers_and_persists_when_missing(self, creds_file): + with mock.patch("parallel_web_tools.core.auth.register_client", return_value="cid_fresh"): + assert _ensure_client_id() == "cid_fresh" + + creds = credentials.load() + assert creds is not None + assert creds.client_id == "cid_fresh" + + def test_registers_again_when_stored_client_id_is_none(self, creds_file): + # Simulate a prior registration failure: file exists but client_id is None. + credentials.save(credentials.Credentials(selected_org_id="x", orgs={"x": credentials.OrgCredentials()})) + with mock.patch("parallel_web_tools.core.auth.register_client", return_value="cid_new") as mock_reg: + assert _ensure_client_id() == "cid_new" + mock_reg.assert_called_once() + + creds = credentials.load() + assert creds is not None + assert creds.client_id == "cid_new" + + def test_falls_back_to_hardcoded_on_registration_failure(self, creds_file, capsys): + with mock.patch( + "parallel_web_tools.core.auth.register_client", + side_effect=Exception("server down"), + ): + assert _ensure_client_id() == "parallel-cli" + + # Failure leaves client_id unset so the next call retries. + creds = credentials.load() + assert creds is None or creds.client_id is None + err = capsys.readouterr().err + assert "client registration failed" in err + + +# --------------------------------------------------------------------------- +# send_magic_link +# --------------------------------------------------------------------------- + + +class TestSendMagicLink: + def test_happy_path(self): + with _patch_auth_urlopen({"ok": True}): + # No return value; success = no exception. + send_magic_link(client_id="cid_xyz", email="u@example.com", user_code="ABCD-1234") + + def test_posts_expected_payload(self): + captured: dict = {} + with _patch_auth_urlopen({"ok": True}, capture=captured): + send_magic_link(client_id="cid_xyz", email="u@example.com", user_code="ABCD-1234") + + assert captured["method"] == "POST" + assert captured["url"].endswith("/api/auth/send-magic-link") + body = json.loads(captured["body"]) + assert body == { + "client_id": "cid_xyz", + "email": "u@example.com", + "emailType": "deviceCode", + "queryParams": {"user_code": "ABCD-1234"}, + } + assert any(v == "application/json" for v in captured["headers"].values()) + + def test_custom_email_type(self): + captured: dict = {} + with _patch_auth_urlopen({"ok": True}, capture=captured): + send_magic_link( + client_id="cid_xyz", + email="u@example.com", + user_code="ABCD-1234", + email_type="customType", + ) + assert json.loads(captured["body"])["emailType"] == "customType" + def test_raises_on_http_error(self): + with _patch_auth_urlopen(_http_error(422, {"error": "invalid_email"})): + with pytest.raises(Exception, match="Magic link send failed: 422"): + send_magic_link(client_id="cid_xyz", email="bad@x", user_code="ABCD-1234") -class TestCreateClient: - """Tests for create_client function.""" - def test_creates_client_with_explicit_key(self): - """Should create Parallel client with explicit API key.""" - with mock.patch("parallel_web_tools.core.auth.Parallel") as mock_parallel: - create_client(api_key="test-key-123", source="cli") +# --------------------------------------------------------------------------- +# request_device_code +# --------------------------------------------------------------------------- - mock_parallel.assert_called_once() - call_kwargs = mock_parallel.call_args.kwargs - assert call_kwargs["api_key"] == "test-key-123" - assert "User-Agent" in call_kwargs["default_headers"] - assert "(cli)" in call_kwargs["default_headers"]["User-Agent"] - def test_creates_client_with_env_key(self): - """Should resolve API key from environment when not explicit.""" - with mock.patch.dict(os.environ, {"PARALLEL_API_KEY": "env-key"}): - with mock.patch("parallel_web_tools.core.auth.Parallel") as mock_parallel: - create_client(source="duckdb") +class TestRequestDeviceCode: + def test_returns_device_code_info(self): + with _patch_auth_urlopen(DEVICE_RESPONSE): + info = request_device_code() + assert isinstance(info, DeviceCodeInfo) + assert info.user_code == "BCDF-GHJK" + assert info.expires_in == 600 - call_kwargs = mock_parallel.call_args.kwargs - assert call_kwargs["api_key"] == "env-key" + def test_hits_get_service_keys_endpoint(self): + from parallel_web_tools.core.endpoints import DEFAULT_SCOPE - def test_raises_without_key(self, tmp_path): - """Should raise ValueError when no API key is available.""" - token_file = tmp_path / "nonexistent.json" + captured: dict = {} + with _patch_auth_urlopen(DEVICE_RESPONSE, capture=captured): + request_device_code() - with mock.patch.dict(os.environ, {}, clear=True): - os.environ.pop("PARALLEL_API_KEY", None) - with mock.patch("parallel_web_tools.core.auth.TOKEN_FILE", token_file): - with pytest.raises(ValueError, match="Parallel API key required"): - create_client() + assert "/getServiceKeys/device/code" in captured["url"] + assert "client_id=parallel-cli" in captured["body"] + # Scope must be present and URL-form-encoded — check for its head and a colon-encoded marker. + first_scope = DEFAULT_SCOPE.split()[0] # e.g. "keys:read" + assert first_scope.replace(":", "%3A") in captured["body"] or first_scope in captured["body"] - def test_default_source_is_python(self): - """Should default to python source.""" - with mock.patch.dict(os.environ, {"PARALLEL_API_KEY": "test-key"}): - with mock.patch("parallel_web_tools.core.auth.Parallel") as mock_parallel: - create_client() + def test_respects_platform_url_env_var(self, monkeypatch): + monkeypatch.setenv("PARALLEL_PLATFORM_URL", "http://localhost:3000") + captured: dict = {} + with _patch_auth_urlopen(DEVICE_RESPONSE, capture=captured): + request_device_code() + assert captured["url"].startswith("http://localhost:3000/") - call_kwargs = mock_parallel.call_args.kwargs - assert "(python)" in call_kwargs["default_headers"]["User-Agent"] + def test_raises_on_http_error(self): + with _patch_auth_urlopen(_http_error(500, {"error": "internal"})): + with pytest.raises(Exception, match="Device code request failed"): + request_device_code() -class TestResolveApiKeyInAuth: - """Additional tests for resolve_api_key edge cases.""" +# --------------------------------------------------------------------------- +# poll_device_token +# --------------------------------------------------------------------------- - def test_empty_string_key_is_falsy(self): - """Empty string api_key should fall through to env var.""" - with mock.patch.dict(os.environ, {"PARALLEL_API_KEY": "env-key"}): - result = resolve_api_key(api_key="") - assert result == "env-key" - def test_stored_token_used_as_fallback(self, tmp_path): - """Should use stored OAuth token when no env var.""" - token_file = tmp_path / "creds.json" - token_file.write_text(json.dumps({"access_token": "stored-token"})) +class TestPollDeviceToken: + def test_returns_token_response_on_success(self, no_sleep): + with _patch_auth_urlopen(TOKEN_RESPONSE_JSON): + resp = poll_device_token(SAMPLE_DEVICE_CODE_INFO) + assert isinstance(resp, TokenResponse) + assert resp.access_token == "at_123" + assert resp.refresh_token == "rt_123" + assert resp.org_id == "org_abc" + assert resp.scopes == ["keys:read", "balance:write"] + + def test_polls_through_pending(self, monkeypatch): + sleep_mock = mock.MagicMock() + monkeypatch.setattr("parallel_web_tools.core.auth.time.sleep", sleep_mock) + responses = [ + _http_error(400, {"error": "authorization_pending"}), + _http_error(400, {"error": "authorization_pending"}), + TOKEN_RESPONSE_JSON, + ] + with _patch_auth_urlopen(responses): + resp = poll_device_token(SAMPLE_DEVICE_CODE_INFO) + assert resp.access_token == "at_123" + # Polls first, then sleeps between polls — 3 polls means 2 sleeps. + assert sleep_mock.call_count == 2 + + def test_slow_down_increases_interval(self, monkeypatch): + sleep_mock = mock.MagicMock() + monkeypatch.setattr("parallel_web_tools.core.auth.time.sleep", sleep_mock) + responses = [_http_error(400, {"error": "slow_down"}), TOKEN_RESPONSE_JSON] + with _patch_auth_urlopen(responses): + poll_device_token(SAMPLE_DEVICE_CODE_INFO) + # First poll returns slow_down; we bump interval to 10, sleep 10, poll again. + assert sleep_mock.call_args_list == [mock.call(10)] + + def test_polls_immediately_without_initial_sleep(self, monkeypatch): + """Happy path: auth already granted at entry → return on first poll, zero sleeps.""" + sleep_mock = mock.MagicMock() + monkeypatch.setattr("parallel_web_tools.core.auth.time.sleep", sleep_mock) + with _patch_auth_urlopen(TOKEN_RESPONSE_JSON): + poll_device_token(SAMPLE_DEVICE_CODE_INFO) + sleep_mock.assert_not_called() - with mock.patch.dict(os.environ, {}, clear=True): - os.environ.pop("PARALLEL_API_KEY", None) - with mock.patch("parallel_web_tools.core.auth.TOKEN_FILE", token_file): - result = resolve_api_key() - assert result == "stored-token" + def test_raises_on_access_denied(self, no_sleep): + with _patch_auth_urlopen(_http_error(400, {"error": "access_denied"})): + with pytest.raises(Exception, match="Authorization denied"): + poll_device_token(SAMPLE_DEVICE_CODE_INFO) + def test_raises_on_expired_token(self, no_sleep): + with _patch_auth_urlopen(_http_error(400, {"error": "expired_token"})): + with pytest.raises(Exception, match="expired"): + poll_device_token(SAMPLE_DEVICE_CODE_INFO) -class TestIsHeadless: - """Tests for headless environment detection.""" - def test_ssh_client_detected(self): - with mock.patch.dict(os.environ, {"SSH_CLIENT": "1.2.3.4 54321 22"}): - assert _is_headless() is True +# --------------------------------------------------------------------------- +# refresh_access_token / revoke_token +# --------------------------------------------------------------------------- - def test_ssh_tty_detected(self): - with mock.patch.dict(os.environ, {"SSH_TTY": "/dev/pts/0"}): - assert _is_headless() is True - def test_ci_detected(self): - with mock.patch.dict(os.environ, {"CI": "true"}): - assert _is_headless() is True +class TestRefreshAccessToken: + def test_returns_new_token_response(self): + with _patch_auth_urlopen(TOKEN_RESPONSE_JSON): + resp = refresh_access_token("rt_old") + assert resp.access_token == "at_123" - def test_docker_detected(self): - with mock.patch("os.path.exists", return_value=True): - assert _is_headless() is True + def test_hits_token_endpoint_with_refresh_grant(self): + captured: dict = {} + with _patch_auth_urlopen(TOKEN_RESPONSE_JSON, capture=captured): + refresh_access_token("rt_old") - def test_container_env_detected(self): - with mock.patch.dict(os.environ, {"container": "podman"}): - with mock.patch("os.path.exists", return_value=False): - assert _is_headless() is True + assert "/getServiceKeys/token" in captured["url"] + assert "grant_type=refresh_token" in captured["body"] + assert "refresh_token=rt_old" in captured["body"] - def test_normal_env_not_headless(self): - env = {k: v for k, v in os.environ.items() if k not in ("SSH_CLIENT", "SSH_TTY", "CI", "container")} - env["DISPLAY"] = ":0" # Ensure Linux display check passes - with mock.patch.dict(os.environ, env, clear=True): - with mock.patch("os.path.exists", return_value=False): - assert _is_headless() is False +class TestRevokeToken: + def test_sends_form_encoded_refresh_token(self): + captured: dict = {} + with _patch_auth_urlopen(b"", capture=captured): + revoke_token("rt_xyz") -def _make_http_error(status, body): - """Helper to create a urllib HTTPError with a JSON body.""" - import io - from email.message import Message + assert "/getServiceKeys/token/revoke" in captured["url"] + assert captured["method"] == "POST" + # Body is form-encoded refresh_token=; no bearer header. + assert captured["body"] == "refresh_token=rt_xyz" + assert any(v == "application/x-www-form-urlencoded" for v in captured["headers"].values()) + assert not any(k.lower() == "authorization" for k in captured["headers"]) - resp = io.BytesIO(json.dumps(body).encode()) - return urllib.error.HTTPError( - url="https://example.com", - code=status, - msg="Bad Request", - hdrs=Message(), - fp=resp, - ) + def test_raises_on_http_error(self): + with _patch_auth_urlopen(_http_error(400, {"error": "invalid_request"})): + with pytest.raises(Exception, match="Token revocation failed: 400"): + revoke_token("rt_bad") -SAMPLE_DEVICE_CODE_INFO = DeviceCodeInfo( - device_code="a" * 48, - user_code="BCDF-GHJK", - verification_uri="https://platform.parallel.ai/getKeys/device", - verification_uri_complete="https://platform.parallel.ai/getKeys/device?user_code=BCDF-GHJK", - expires_in=600, - interval=5, -) +# --------------------------------------------------------------------------- +# _do_device_flow +# --------------------------------------------------------------------------- -def _mock_urlopen_sequence(responses): - """Create a mock urlopen that returns a sequence of responses. - - Each response is either a dict (success) or an HTTPError (error). - """ - import io - from contextlib import contextmanager +class TestDoDeviceFlow: + @mock.patch("parallel_web_tools.core.auth.webbrowser.open") + @mock.patch("parallel_web_tools.core.auth._is_headless", return_value=False) + def test_opens_browser_when_not_headless(self, _headless, mock_browser_open, no_sleep): + with _patch_auth_urlopen([DEVICE_RESPONSE, TOKEN_RESPONSE_JSON]): + resp = _do_device_flow() + assert isinstance(resp, TokenResponse) + mock_browser_open.assert_called_once() - call_count = 0 + @mock.patch("parallel_web_tools.core.auth.webbrowser.open") + @mock.patch("parallel_web_tools.core.auth._is_headless", return_value=True) + def test_skips_browser_when_headless(self, _headless, mock_browser_open, no_sleep): + with _patch_auth_urlopen([DEVICE_RESPONSE, TOKEN_RESPONSE_JSON]): + _do_device_flow() + mock_browser_open.assert_not_called() - @contextmanager - def mock_urlopen(req, timeout=None): - nonlocal call_count - idx = min(call_count, len(responses) - 1) - resp = responses[idx] - call_count += 1 + @mock.patch("parallel_web_tools.core.auth.webbrowser.open") + @mock.patch("parallel_web_tools.core.auth._is_headless", return_value=False) + def test_opens_browser_with_login_hint(self, _headless, mock_browser_open, no_sleep): + with _patch_auth_urlopen([DEVICE_RESPONSE, TOKEN_RESPONSE_JSON]): + _do_device_flow(login_hint="login=email,e=user@example.com") + opened_url = mock_browser_open.call_args.args[0] + assert "login_hint=login%3Demail%2Ce%3Duser%40example.com" in opened_url - if isinstance(resp, urllib.error.HTTPError): - raise resp + @mock.patch("parallel_web_tools.core.auth.webbrowser.open") + def test_callback_receives_device_code_info(self, mock_browser_open, no_sleep): + captured = [] + with _patch_auth_urlopen([DEVICE_RESPONSE, TOKEN_RESPONSE_JSON]): + _do_device_flow(on_device_code=lambda info: captured.append(info)) + assert len(captured) == 1 + assert isinstance(captured[0], DeviceCodeInfo) + # Browser should NOT be opened when callback is provided. + mock_browser_open.assert_not_called() - body = json.dumps(resp).encode() - fp = io.BytesIO(body) - yield fp + def test_reregisters_and_retries_when_client_id_is_unknown(self, creds_file, no_sleep): + invalid_client = Exception( + 'Device code request failed: 401 - {"error":"invalid_client","error_description":"Unknown client_id. Register the client first."}' + ) + credentials.save(credentials.Credentials(client_id="cid_stale")) + + with ( + mock.patch( + "parallel_web_tools.core.auth.request_device_code", + side_effect=[invalid_client, SAMPLE_DEVICE_CODE_INFO], + ) as mock_request, + mock.patch("parallel_web_tools.core.auth.register_client", return_value="cid_fresh") as mock_register, + mock.patch("parallel_web_tools.core.auth.poll_device_token", return_value=_token_response()) as mock_poll, + ): + _do_device_flow(client_id="cid_stale") + + assert mock_request.call_args_list[0].kwargs["client_id"] == "cid_stale" + assert mock_request.call_args_list[1].kwargs["client_id"] == "cid_fresh" + mock_register.assert_called_once_with() + mock_poll.assert_called_once_with(SAMPLE_DEVICE_CODE_INFO, client_id="cid_fresh") + creds = credentials.load() + assert creds is not None + assert creds.client_id == "cid_fresh" + + +# --------------------------------------------------------------------------- +# _persist_token_response +# --------------------------------------------------------------------------- + + +class TestPersistTokenResponse: + def test_writes_control_api_tokens_to_selected_org(self, creds_file): + _persist_token_response(_token_response(access_token="at_new", refresh_token="rt_new", org_id="org_real")) + creds = credentials.load() + assert creds is not None + assert creds.selected_org_id == "org_real" + assert creds.orgs["org_real"].org_name == "Acme Org" + control = creds.orgs["org_real"].control_api + assert control.access_token == "at_new" + assert control.refresh_token == "rt_new" + assert control.access_token_scopes == ["keys:read", "balance:write"] + # Expiries are absolute timestamps ordered access < refresh ≤ authorization. + assert control.access_token_expires_at is not None + assert control.refresh_token_expires_at is not None + assert control.authorization_expires_at is not None + assert control.access_token_expires_at > 0 + assert control.refresh_token_expires_at > control.access_token_expires_at + assert control.authorization_expires_at >= control.refresh_token_expires_at + + +# --------------------------------------------------------------------------- +# login_flow + get_api_key +# --------------------------------------------------------------------------- + + +class TestLoginFlow: + def test_provisions_api_key_and_stores(self, creds_file, mock_ensure_client_id): + token_resp = _token_response(access_token="at_x", refresh_token="rt_x", org_id="org_real") + with ( + mock.patch("parallel_web_tools.core.auth._do_device_flow", return_value=token_resp) as mock_flow, + mock.patch( + "parallel_web_tools.core.auth.service.provision_cli_api_key", + return_value=("sk_minted", "cid_test-2026-04-21-1432"), + ) as mock_provision, + ): + api_key = login_flow(login_hint="login=email,e=user@example.com") + + assert api_key == "sk_minted" + # The registered client_id must be threaded into both the device flow + # and the data-API key provisioning call. + assert mock_flow.call_args.kwargs.get("client_id") == mock_ensure_client_id + mock_provision.assert_called_once_with("at_x", client_id=mock_ensure_client_id) + + creds = credentials.load() + assert creds is not None + assert creds.selected_org_id == "org_real" + assert creds.orgs["org_real"].api_key == "sk_minted" + assert creds.orgs["org_real"].org_name == "Acme Org" + assert creds.orgs["org_real"].control_api.access_token == "at_x" + + def test_removes_legacy_org_after_successful_login(self, creds_file, mock_ensure_client_id): + # Seed credentials with a v0-style legacy entry, as if the user was upgraded + # from an older credentials file before running their first real login. + credentials.save( + credentials.Credentials( + selected_org_id=credentials.LEGACY_ORG_ID, + orgs={credentials.LEGACY_ORG_ID: credentials.OrgCredentials(api_key="legacy_key")}, + ) + ) - return mock_urlopen + token_resp = _token_response(access_token="at_new", refresh_token="rt_new", org_id="org_real") + with ( + mock.patch("parallel_web_tools.core.auth._do_device_flow", return_value=token_resp), + mock.patch( + "parallel_web_tools.core.auth.service.provision_cli_api_key", + return_value=("sk_minted", "name"), + ), + ): + login_flow() + + creds = credentials.load() + assert creds is not None + assert creds.selected_org_id == "org_real" + assert "org_real" in creds.orgs + # Legacy placeholder must be purged after a successful login. + assert credentials.LEGACY_ORG_ID not in creds.orgs + + def test_registers_client_when_missing(self, creds_file): + """First-boot login triggers /getServiceKeys/register and persists the id.""" + token_resp = _token_response(org_id="org_real") + with ( + mock.patch("parallel_web_tools.core.auth.register_client", return_value="cid_fresh") as mock_reg, + mock.patch("parallel_web_tools.core.auth._do_device_flow", return_value=token_resp) as mock_flow, + mock.patch( + "parallel_web_tools.core.auth.service.provision_cli_api_key", + return_value=("sk_minted", "name"), + ), + ): + login_flow() + + mock_reg.assert_called_once() + assert mock_flow.call_args.kwargs.get("client_id") == "cid_fresh" + + creds = credentials.load() + assert creds is not None + assert creds.client_id == "cid_fresh" + + def test_skips_registration_when_client_id_already_stored(self, creds_file): + credentials.save(credentials.Credentials(client_id="cid_existing")) + token_resp = _token_response(org_id="org_real") + with ( + mock.patch("parallel_web_tools.core.auth.register_client") as mock_reg, + mock.patch("parallel_web_tools.core.auth._do_device_flow", return_value=token_resp) as mock_flow, + mock.patch( + "parallel_web_tools.core.auth.service.provision_cli_api_key", + return_value=("sk_minted", "name"), + ), + ): + login_flow() + + mock_reg.assert_not_called() + assert mock_flow.call_args.kwargs.get("client_id") == "cid_existing" -class TestRequestDeviceCode: - """Tests for the request_device_code public function.""" +class TestGetApiKey: + def test_stored_token_first_priority(self, creds_file, monkeypatch): + monkeypatch.setenv("PARALLEL_API_KEY", "env_key") + credentials.set_api_key_for_org("org_a", "stored_key") + # Stored credentials must win over the env var. + assert get_api_key() == "stored_key" + + def test_env_var_used_when_no_stored_key(self, creds_file, monkeypatch): + monkeypatch.setenv("PARALLEL_API_KEY", "env_key") + assert get_api_key() == "env_key" + + def test_service_api_mint_beats_env_var_when_auth_json_exists(self, creds_file, monkeypatch): + monkeypatch.setenv("PARALLEL_API_KEY", "env_key") + credentials.save( + credentials.Credentials( + selected_org_id="org_a", + orgs={"org_a": credentials.OrgCredentials()}, + ) + ) - DEVICE_RESPONSE = { - "device_code": "a" * 48, - "user_code": "BCDF-GHJK", - "verification_uri": "https://platform.parallel.ai/getKeys/device", - "verification_uri_complete": "https://platform.parallel.ai/getKeys/device?user_code=BCDF-GHJK", - "expires_in": 600, - "interval": 5, - } + with ( + mock.patch( + "parallel_web_tools.core.auth.get_control_api_access_token", return_value="at_existing" + ) as mock_at, + mock.patch("parallel_web_tools.core.auth._ensure_client_id", return_value="cid_existing") as mock_client_id, + mock.patch( + "parallel_web_tools.core.auth.service.provision_cli_api_key", + return_value=("sk_minted", "cid_existing-2026-04-23-1212"), + ) as mock_provision, + ): + assert get_api_key() == "sk_minted" + + mock_at.assert_called_once_with() + mock_client_id.assert_called_once_with() + mock_provision.assert_called_once_with("at_existing", client_id="cid_existing") + + def test_stored_only_without_env(self, creds_file, monkeypatch): + monkeypatch.delenv("PARALLEL_API_KEY", raising=False) + credentials.set_api_key_for_org("org_a", "stored_key") + assert get_api_key() == "stored_key" + + def test_force_login_runs_login_flow(self, creds_file, monkeypatch): + monkeypatch.setenv("PARALLEL_API_KEY", "env_key") # should still be ignored with force_login + with mock.patch("parallel_web_tools.core.auth.login_flow", return_value="minted_key") as mock_flow: + result = get_api_key(force_login=True, login_hint="login=google") + assert result == "minted_key" + assert mock_flow.call_args.kwargs.get("login_hint") == "login=google" + + def test_provisions_via_service_api_when_stored_api_key_missing(self, creds_file, monkeypatch): + monkeypatch.delenv("PARALLEL_API_KEY", raising=False) + credentials.save( + credentials.Credentials( + selected_org_id="org_a", + orgs={"org_a": credentials.OrgCredentials()}, + ) + ) - def test_returns_device_code_info(self): - """Should return a DeviceCodeInfo dataclass.""" - mock_urlopen = _mock_urlopen_sequence([self.DEVICE_RESPONSE]) + with ( + mock.patch( + "parallel_web_tools.core.auth.get_control_api_access_token", return_value="at_existing" + ) as mock_at, + mock.patch("parallel_web_tools.core.auth._ensure_client_id", return_value="cid_existing") as mock_client_id, + mock.patch( + "parallel_web_tools.core.auth.service.provision_cli_api_key", + return_value=("sk_minted", "cid_existing-2026-04-23-1212"), + ) as mock_provision, + mock.patch("parallel_web_tools.core.auth.login_flow") as mock_login, + ): + assert get_api_key() == "sk_minted" + + mock_at.assert_called_once_with() + mock_client_id.assert_called_once_with() + mock_provision.assert_called_once_with("at_existing", client_id="cid_existing") + mock_login.assert_not_called() + + creds = credentials.load() + assert creds is not None + assert creds.orgs["org_a"].api_key == "sk_minted" + + def test_falls_back_to_login_when_control_api_requires_reauth(self, creds_file, monkeypatch): + monkeypatch.delenv("PARALLEL_API_KEY", raising=False) + with ( + mock.patch( + "parallel_web_tools.core.auth.get_control_api_access_token", + side_effect=ReauthenticationRequired("not logged in; run 'parallel-cli login'"), + ), + mock.patch("parallel_web_tools.core.auth.login_flow", return_value="sk_from_login") as mock_login, + ): + assert get_api_key(login_hint="user@example.com") == "sk_from_login" + + assert mock_login.call_args.kwargs.get("login_hint") == "user@example.com" + + def test_falls_back_to_env_when_service_api_path_cannot_mint(self, creds_file, monkeypatch): + monkeypatch.setenv("PARALLEL_API_KEY", "env_key") + credentials.save( + credentials.Credentials( + selected_org_id="org_a", + orgs={"org_a": credentials.OrgCredentials()}, + ) + ) - with mock.patch("parallel_web_tools.core.auth.urllib.request.urlopen", side_effect=mock_urlopen): - info = request_device_code() + with ( + mock.patch( + "parallel_web_tools.core.auth.get_control_api_access_token", + side_effect=ReauthenticationRequired("not logged in; run 'parallel-cli login'"), + ), + mock.patch("parallel_web_tools.core.auth.login_flow") as mock_login, + ): + assert get_api_key() == "env_key" - assert isinstance(info, DeviceCodeInfo) - assert info.device_code == "a" * 48 - assert info.user_code == "BCDF-GHJK" - assert info.verification_uri == "https://platform.parallel.ai/getKeys/device" - assert info.expires_in == 600 - assert info.interval == 5 + mock_login.assert_not_called() - def test_raises_on_http_error(self): - """Should raise on server error.""" - error = _make_http_error(500, {"error": "internal"}) - mock_urlopen = _mock_urlopen_sequence([error]) - with mock.patch("parallel_web_tools.core.auth.urllib.request.urlopen", side_effect=mock_urlopen): - with pytest.raises(Exception, match="Device code request failed"): - request_device_code() +# --------------------------------------------------------------------------- +# get_auth_status / logout +# --------------------------------------------------------------------------- -class TestPollDeviceToken: - """Tests for the poll_device_token public function.""" - - TOKEN_RESPONSE = { - "access_token": "test-api-key-from-device", - "token_type": "bearer", - "scope": "key:read", - } - - @mock.patch("parallel_web_tools.core.auth.time.sleep") - def test_returns_token_on_success(self, mock_sleep): - """Should return access token when approved.""" - mock_urlopen = _mock_urlopen_sequence([self.TOKEN_RESPONSE]) - - with mock.patch("parallel_web_tools.core.auth.urllib.request.urlopen", side_effect=mock_urlopen): - token = poll_device_token(SAMPLE_DEVICE_CODE_INFO) - - assert token == "test-api-key-from-device" - - @mock.patch("parallel_web_tools.core.auth.time.sleep") - def test_polls_through_pending(self, mock_sleep): - """Should keep polling on authorization_pending.""" - mock_urlopen = _mock_urlopen_sequence( - [ - _make_http_error(400, {"error": "authorization_pending"}), - _make_http_error(400, {"error": "authorization_pending"}), - self.TOKEN_RESPONSE, - ] +class TestAuthStatus: + def test_status_with_env_var(self, creds_file, monkeypatch): + monkeypatch.setenv("PARALLEL_API_KEY", "test_key") + status = get_auth_status() + assert status["authenticated"] is True + assert status["method"] == "environment" + + def test_stored_beats_env_var_in_status(self, creds_file, monkeypatch): + credentials.save( + credentials.Credentials( + selected_org_id="org_a", + orgs={"org_a": credentials.OrgCredentials(api_key="stored_key", org_name="Acme Org")}, + ) ) + monkeypatch.setenv("PARALLEL_API_KEY", "env_key") + status = get_auth_status() + assert status["authenticated"] is True + assert status["method"] == "oauth" # stored credentials win + assert status["selected_org_name"] == "Acme Org" + + def test_status_with_stored_token(self, creds_file, legacy_file, monkeypatch): + legacy_file.parent.mkdir(parents=True, exist_ok=True) + legacy_file.write_text(json.dumps({"access_token": "stored_token"})) + monkeypatch.delenv("PARALLEL_API_KEY", raising=False) + + status = get_auth_status() + assert status["authenticated"] is True + assert status["method"] == "oauth" + assert status["version"] == 1 + assert status["selected_org_id"] == "legacy" + assert status["selected_org_name"] is None + assert status["has_control_api_tokens"] is False + assert status["token_file"] == str(creds_file) + assert creds_file.exists() + + def test_status_not_authenticated(self, creds_file, monkeypatch): + monkeypatch.delenv("PARALLEL_API_KEY", raising=False) + status = get_auth_status() + assert status["authenticated"] is False + assert status["method"] is None - with mock.patch("parallel_web_tools.core.auth.urllib.request.urlopen", side_effect=mock_urlopen): - token = poll_device_token(SAMPLE_DEVICE_CODE_INFO) - assert token == "test-api-key-from-device" - assert mock_sleep.call_count == 3 - - @mock.patch("parallel_web_tools.core.auth.time.sleep") - def test_slow_down_increases_interval(self, mock_sleep): - """slow_down should increase polling interval by 5 seconds.""" - mock_urlopen = _mock_urlopen_sequence( - [ - _make_http_error(400, {"error": "slow_down"}), - self.TOKEN_RESPONSE, - ] +class TestLogout: + def test_logout_removes_token_no_revoke_when_missing(self, creds_file, legacy_file): + legacy_file.parent.mkdir(parents=True, exist_ok=True) + legacy_file.write_text(json.dumps({"access_token": "test"})) + + with mock.patch("parallel_web_tools.core.auth.revoke_token") as mock_revoke: + assert logout() is True + # Legacy tokens have no refresh_token — revoke should be skipped. + mock_revoke.assert_not_called() + assert not legacy_file.exists() + + def test_login_flow_writes_structured_auth_without_touching_legacy_file( + self, creds_file, legacy_file, mock_ensure_client_id + ): + legacy_file.write_text(json.dumps({"access_token": "legacy_key"})) + token_resp = _token_response(access_token="at_x", refresh_token="rt_x", org_id="org_real") + + with ( + mock.patch("parallel_web_tools.core.auth._do_device_flow", return_value=token_resp), + mock.patch( + "parallel_web_tools.core.auth.service.provision_cli_api_key", + return_value=("sk_minted", "cid_test-2026-04-21-1432"), + ), + ): + login_flow() + + auth_disk = json.loads(creds_file.read_text()) + legacy_disk = json.loads(legacy_file.read_text()) + assert auth_disk["version"] == 1 + assert auth_disk["selected_org_id"] == "org_real" + assert legacy_disk == {"access_token": "legacy_key"} + + def test_logout_revokes_refresh_token_when_present(self, creds_file): + credentials.save( + credentials.Credentials( + selected_org_id="org_a", + orgs={ + "org_a": credentials.OrgCredentials( + api_key="sk", + control_api=credentials.ControlApiTokens(refresh_token="rt_keep"), + ) + }, + ) ) - with mock.patch("parallel_web_tools.core.auth.urllib.request.urlopen", side_effect=mock_urlopen): - poll_device_token(SAMPLE_DEVICE_CODE_INFO) - - assert mock_sleep.call_args_list[0] == mock.call(5) - assert mock_sleep.call_args_list[1] == mock.call(10) - - @mock.patch("parallel_web_tools.core.auth.time.sleep") - def test_raises_on_access_denied(self, mock_sleep): - mock_urlopen = _mock_urlopen_sequence( - [ - _make_http_error(400, {"error": "access_denied"}), - ] + with mock.patch("parallel_web_tools.core.auth.revoke_token") as mock_revoke: + assert logout() is True + mock_revoke.assert_called_once_with("rt_keep") + assert not creds_file.exists() + + def test_logout_revokes_refresh_tokens_for_all_orgs(self, creds_file): + credentials.save( + credentials.Credentials( + selected_org_id="org_a", + orgs={ + "org_a": credentials.OrgCredentials( + control_api=credentials.ControlApiTokens(refresh_token="rt_a"), + ), + "org_b": credentials.OrgCredentials( + control_api=credentials.ControlApiTokens(refresh_token="rt_b"), + ), + "org_c": credentials.OrgCredentials( + control_api=credentials.ControlApiTokens(refresh_token=None), + ), + }, + ) ) - with mock.patch("parallel_web_tools.core.auth.urllib.request.urlopen", side_effect=mock_urlopen): - with pytest.raises(Exception, match="Authorization denied"): - poll_device_token(SAMPLE_DEVICE_CODE_INFO) - - @mock.patch("parallel_web_tools.core.auth.time.sleep") - def test_raises_on_expired_token(self, mock_sleep): - mock_urlopen = _mock_urlopen_sequence( - [ - _make_http_error(400, {"error": "expired_token"}), - ] + with mock.patch("parallel_web_tools.core.auth.revoke_token") as mock_revoke: + assert logout() is True + assert mock_revoke.call_count == 2 + mock_revoke.assert_any_call("rt_a") + mock_revoke.assert_any_call("rt_b") + assert not creds_file.exists() + + def test_logout_deduplicates_shared_refresh_tokens(self, creds_file): + credentials.save( + credentials.Credentials( + selected_org_id="org_a", + orgs={ + "org_a": credentials.OrgCredentials( + control_api=credentials.ControlApiTokens(refresh_token="rt_shared"), + ), + "org_b": credentials.OrgCredentials( + control_api=credentials.ControlApiTokens(refresh_token="rt_shared"), + ), + }, + ) ) - with mock.patch("parallel_web_tools.core.auth.urllib.request.urlopen", side_effect=mock_urlopen): - with pytest.raises(Exception, match="expired"): - poll_device_token(SAMPLE_DEVICE_CODE_INFO) + with mock.patch("parallel_web_tools.core.auth.revoke_token") as mock_revoke: + assert logout() is True + mock_revoke.assert_called_once_with("rt_shared") + + def test_logout_best_effort_on_revoke_failure(self, creds_file): + credentials.save( + credentials.Credentials( + selected_org_id="org_a", + orgs={ + "org_a": credentials.OrgCredentials( + control_api=credentials.ControlApiTokens(refresh_token="rt_bad"), + ) + }, + ) + ) + with mock.patch( + "parallel_web_tools.core.auth.revoke_token", + side_effect=Exception("server down"), + ): + assert logout() is True + assert not creds_file.exists() -class TestDoDeviceFlow: - """Tests for the _do_device_flow convenience wrapper.""" - - DEVICE_RESPONSE = { - "device_code": "a" * 48, - "user_code": "BCDF-GHJK", - "verification_uri": "https://platform.parallel.ai/getKeys/device", - "verification_uri_complete": "https://platform.parallel.ai/getKeys/device?user_code=BCDF-GHJK", - "expires_in": 600, - "interval": 5, - } - - TOKEN_RESPONSE = { - "access_token": "test-api-key-from-device", - "token_type": "bearer", - "scope": "key:read", - } + def test_logout_no_token(self, creds_file): + assert logout() is False - @mock.patch("parallel_web_tools.core.auth.webbrowser.open") - @mock.patch("parallel_web_tools.core.auth.time.sleep") - def test_default_prints_to_stderr(self, mock_sleep, mock_browser_open): - """Without callback, should print instructions to stderr.""" - mock_urlopen = _mock_urlopen_sequence( - [ - self.DEVICE_RESPONSE, - self.TOKEN_RESPONSE, - ] - ) - with mock.patch("parallel_web_tools.core.auth.urllib.request.urlopen", side_effect=mock_urlopen): - token = _do_device_flow() +# --------------------------------------------------------------------------- +# Client creation +# --------------------------------------------------------------------------- - assert token == "test-api-key-from-device" - mock_browser_open.assert_called_once() - @mock.patch("parallel_web_tools.core.auth.webbrowser.open") - @mock.patch("parallel_web_tools.core.auth.time.sleep") - def test_callback_receives_device_code_info(self, mock_sleep, mock_browser_open): - """on_device_code callback should receive DeviceCodeInfo.""" - mock_urlopen = _mock_urlopen_sequence( - [ - self.DEVICE_RESPONSE, - self.TOKEN_RESPONSE, - ] - ) +class TestCreateClient: + def test_creates_client_with_explicit_key(self): + with mock.patch("parallel_web_tools.core.auth.Parallel") as mock_parallel: + create_client(api_key="test-key-123", source="cli") + mock_parallel.assert_called_once() + kwargs = mock_parallel.call_args.kwargs + assert kwargs["api_key"] == "test-key-123" + assert "(cli)" in kwargs["default_headers"]["User-Agent"] - captured = [] + def test_creates_client_with_env_key(self, creds_file, monkeypatch): + monkeypatch.setenv("PARALLEL_API_KEY", "env-key") + with mock.patch("parallel_web_tools.core.auth.Parallel") as mock_parallel: + create_client(source="duckdb") + assert mock_parallel.call_args.kwargs["api_key"] == "env-key" - with mock.patch("parallel_web_tools.core.auth.urllib.request.urlopen", side_effect=mock_urlopen): - token = _do_device_flow(on_device_code=lambda info: captured.append(info)) + def test_raises_without_key(self, creds_file, monkeypatch): + monkeypatch.delenv("PARALLEL_API_KEY", raising=False) + with pytest.raises(ValueError, match="Parallel API key required"): + create_client() - assert token == "test-api-key-from-device" - assert len(captured) == 1 - assert isinstance(captured[0], DeviceCodeInfo) - assert captured[0].user_code == "BCDF-GHJK" - # Browser should NOT be opened when callback is provided - mock_browser_open.assert_not_called() + def test_passes_default_base_url(self): + with mock.patch("parallel_web_tools.core.auth.Parallel") as mock_parallel: + create_client(api_key="k", source="cli") + assert mock_parallel.call_args.kwargs["base_url"] == "https://api.parallel.ai" + def test_respects_parallel_api_url_env(self, monkeypatch): + monkeypatch.setenv("PARALLEL_API_URL", "http://localhost:9000") + with mock.patch("parallel_web_tools.core.auth.Parallel") as mock_parallel: + create_client(api_key="k", source="cli") + assert mock_parallel.call_args.kwargs["base_url"] == "http://localhost:9000" -class TestGetApiKeyDeviceFlow: - """Tests for get_api_key with device flow integration.""" - def test_device_flag_uses_device_flow(self, tmp_path): - """device=True should use device flow instead of browser OAuth.""" - token_file = tmp_path / "tokens.json" +class TestResolveApiKey: + def test_empty_string_key_is_falsy(self, creds_file, monkeypatch): + monkeypatch.setenv("PARALLEL_API_KEY", "env-key") + assert resolve_api_key(api_key="") == "env-key" - with mock.patch.dict(os.environ, {}, clear=True): - os.environ.pop("PARALLEL_API_KEY", None) - with mock.patch("parallel_web_tools.core.auth.TOKEN_FILE", token_file): - with mock.patch("parallel_web_tools.core.auth._do_device_flow") as mock_device: - mock_device.return_value = "device-token" + def test_stored_token_used_as_fallback(self, creds_file, legacy_file, monkeypatch): + legacy_file.write_text(json.dumps({"access_token": "stored-token"})) + monkeypatch.delenv("PARALLEL_API_KEY", raising=False) + assert resolve_api_key() == "stored-token" + assert creds_file.exists() - result = get_api_key(force_login=True, device=True) + def test_stored_beats_env_var(self, creds_file, monkeypatch): + credentials.set_api_key_for_org("org_a", "stored-key") + monkeypatch.setenv("PARALLEL_API_KEY", "env-key") + assert resolve_api_key() == "stored-key" - assert result == "device-token" - mock_device.assert_called_once() - def test_headless_auto_selects_device_flow(self, tmp_path): - """Headless environment should auto-select device flow.""" - token_file = tmp_path / "tokens.json" +# --------------------------------------------------------------------------- +# _is_headless +# --------------------------------------------------------------------------- - with mock.patch.dict(os.environ, {"SSH_CLIENT": "1.2.3.4 54321 22"}, clear=True): - os.environ.pop("PARALLEL_API_KEY", None) - with mock.patch("parallel_web_tools.core.auth.TOKEN_FILE", token_file): - with mock.patch("parallel_web_tools.core.auth._do_device_flow") as mock_device: - mock_device.return_value = "ssh-device-token" - result = get_api_key(force_login=True) +class TestIsHeadless: + def test_ssh_client_detected(self): + with mock.patch.dict(os.environ, {"SSH_CLIENT": "1.2.3.4 54321 22"}): + assert _is_headless() is True - assert result == "ssh-device-token" - mock_device.assert_called_once() + def test_ssh_tty_detected(self): + with mock.patch.dict(os.environ, {"SSH_TTY": "/dev/pts/0"}): + assert _is_headless() is True - def test_non_headless_uses_browser_flow(self, tmp_path): - """Non-headless environment should use browser-based OAuth.""" - token_file = tmp_path / "tokens.json" - env = { - k: v - for k, v in os.environ.items() - if k not in ("SSH_CLIENT", "SSH_TTY", "CI", "container", "PARALLEL_API_KEY") - } - env["DISPLAY"] = ":0" # Ensure Linux display check passes + def test_ci_detected(self): + with mock.patch.dict(os.environ, {"CI": "true"}): + assert _is_headless() is True - with mock.patch.dict(os.environ, env, clear=True): - with mock.patch("parallel_web_tools.core.auth.TOKEN_FILE", token_file): - with mock.patch("os.path.exists", return_value=False): - with mock.patch("parallel_web_tools.core.auth._do_oauth_flow") as mock_oauth: - mock_oauth.return_value = "browser-token" + def test_docker_detected(self): + with mock.patch("os.path.exists", return_value=True): + assert _is_headless() is True - result = get_api_key(force_login=True) + def test_container_env_detected(self): + with mock.patch.dict(os.environ, {"container": "podman"}): + with mock.patch("os.path.exists", return_value=False): + assert _is_headless() is True - assert result == "browser-token" - mock_oauth.assert_called_once() + def test_normal_env_not_headless(self): + env = {k: v for k, v in os.environ.items() if k not in ("SSH_CLIENT", "SSH_TTY", "CI", "container")} + env["DISPLAY"] = ":0" + with mock.patch.dict(os.environ, env, clear=True): + with mock.patch("os.path.exists", return_value=False): + assert _is_headless() is False - def test_on_device_code_callback_passed_through(self, tmp_path): - """on_device_code callback should be passed to _do_device_flow.""" - token_file = tmp_path / "tokens.json" - callback = mock.Mock() - with mock.patch.dict(os.environ, {}, clear=True): - os.environ.pop("PARALLEL_API_KEY", None) - with mock.patch("parallel_web_tools.core.auth.TOKEN_FILE", token_file): - with mock.patch("parallel_web_tools.core.auth._do_device_flow") as mock_device: - mock_device.return_value = "callback-token" +# --------------------------------------------------------------------------- +# get_control_api_access_token +# --------------------------------------------------------------------------- + + +NOW_FIXED = 1_800_000_000 # arbitrary "now" for clock-controlled tests + + +def _seed_control_api( + creds_file, + *, + access_token: str | None = "at_current", + access_token_expires_at: int | None = NOW_FIXED + 600, + refresh_token: str | None = "rt_current", + refresh_token_expires_at: int | None = NOW_FIXED + 604800, + authorization_expires_at: int | None = NOW_FIXED + 7776000, + org_id: str = "org_abc", +) -> None: + """Write a credentials file with a specific control_api state for testing.""" + credentials.save( + credentials.Credentials( + selected_org_id=org_id, + orgs={ + org_id: credentials.OrgCredentials( + api_key="sk_data", + control_api=credentials.ControlApiTokens( + access_token=access_token, + access_token_expires_at=access_token_expires_at, + access_token_scopes=["keys:read", "balance:write"], + refresh_token=refresh_token, + refresh_token_expires_at=refresh_token_expires_at, + authorization_expires_at=authorization_expires_at, + ), + ) + }, + ) + ) - result = get_api_key(force_login=True, device=True, on_device_code=callback) - assert result == "callback-token" - mock_device.assert_called_once_with(on_device_code=callback) +@pytest.fixture +def frozen_now(monkeypatch): + """Freeze auth.time.time() to NOW_FIXED.""" + monkeypatch.setattr("parallel_web_tools.core.auth.time.time", lambda: NOW_FIXED) + + +class TestGetControlApiAccessToken: + def test_returns_cached_when_valid(self, creds_file, frozen_now): + _seed_control_api(creds_file) + with mock.patch("parallel_web_tools.core.auth.refresh_access_token") as mock_refresh: + assert get_control_api_access_token() == "at_current" + mock_refresh.assert_not_called() + + def test_refreshes_when_access_token_expired(self, creds_file, frozen_now, mock_ensure_client_id): + _seed_control_api(creds_file, access_token_expires_at=NOW_FIXED - 10) + with mock.patch( + "parallel_web_tools.core.auth.refresh_access_token", + return_value=_token_response(access_token="at_refreshed", refresh_token="rt_new"), + ) as mock_refresh: + assert get_control_api_access_token() == "at_refreshed" + mock_refresh.assert_called_once_with("rt_current", client_id=mock_ensure_client_id) + + # Refreshed tokens must be persisted — loaded file reflects new state. + creds = credentials.load() + assert creds is not None + org = creds.selected_org() + assert org is not None + assert org.control_api.access_token == "at_refreshed" + assert org.control_api.refresh_token == "rt_new" + # New expiries are computed relative to the mocked "now". + assert org.control_api.access_token_expires_at == NOW_FIXED + 600 + + def test_skew_buffer_triggers_early_refresh(self, creds_file, frozen_now, mock_ensure_client_id): + # Access token technically valid but within the skew buffer — refresh. + _seed_control_api(creds_file, access_token_expires_at=NOW_FIXED + ACCESS_TOKEN_SKEW_SECONDS - 1) + with mock.patch( + "parallel_web_tools.core.auth.refresh_access_token", + return_value=_token_response(access_token="at_refreshed"), + ) as mock_refresh: + assert get_control_api_access_token() == "at_refreshed" + mock_refresh.assert_called_once() + + def test_raises_reauth_when_no_credentials(self, creds_file): + with pytest.raises(ReauthenticationRequired, match="not logged in"): + get_control_api_access_token() + + def test_raises_reauth_when_no_control_api_tokens(self, creds_file): + # Org exists but has no control_api.access_token (e.g. legacy-migrated org). + _seed_control_api( + creds_file, + access_token=None, + access_token_expires_at=None, + refresh_token=None, + refresh_token_expires_at=None, + authorization_expires_at=None, + ) + with mock.patch("parallel_web_tools.core.auth.refresh_access_token") as mock_refresh: + with pytest.raises(ReauthenticationRequired, match="not logged in"): + get_control_api_access_token() + mock_refresh.assert_not_called() + + def test_raises_reauth_when_authorization_expired(self, creds_file, frozen_now): + _seed_control_api(creds_file, authorization_expires_at=NOW_FIXED - 1) + with mock.patch("parallel_web_tools.core.auth.refresh_access_token") as mock_refresh: + with pytest.raises(ReauthenticationRequired, match="authorization grant"): + get_control_api_access_token() + mock_refresh.assert_not_called() + + def test_raises_reauth_when_refresh_token_expired(self, creds_file, frozen_now): + _seed_control_api( + creds_file, + access_token_expires_at=NOW_FIXED - 10, + refresh_token_expires_at=NOW_FIXED - 1, + ) + with mock.patch("parallel_web_tools.core.auth.refresh_access_token") as mock_refresh: + with pytest.raises(ReauthenticationRequired, match="refresh token"): + get_control_api_access_token() + mock_refresh.assert_not_called() + + def test_bubbles_up_refresh_http_error(self, creds_file, frozen_now): + _seed_control_api(creds_file, access_token_expires_at=NOW_FIXED - 10) + with mock.patch( + "parallel_web_tools.core.auth.refresh_access_token", + side_effect=Exception("500 Internal Server Error"), + ): + with pytest.raises(Exception, match="500 Internal Server Error"): + get_control_api_access_token() diff --git a/tests/test_cli.py b/tests/test_cli.py index 0218460..18d89b8 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -2,7 +2,6 @@ import json import os -import sys from unittest import mock import pytest @@ -34,6 +33,14 @@ def runner(): return CliRunner() +@pytest.fixture +def mock_cli_client(): + """Patch CLI client creation and return the injected mock client.""" + client = mock.MagicMock() + with mock.patch("parallel_web_tools.core.auth.get_client", return_value=client): + yield client + + class TestParseCommaSeparated: """Tests for parse_comma_separated helper function.""" @@ -245,12 +252,17 @@ def test_version(self, runner): class TestAuthCommand: """Tests for the auth command.""" - def test_auth_with_env_var(self, runner): - """Should show authenticated via environment.""" + def test_auth_with_env_var(self, runner, tmp_path): + """Should show authenticated via environment when no stored credentials.""" + token_file = tmp_path / "nonexistent.json" with mock.patch.dict(os.environ, {"PARALLEL_API_KEY": "test-key"}): - result = runner.invoke(main, ["auth"]) - assert result.exit_code == 0 - assert "PARALLEL_API_KEY" in result.output or "environment" in result.output + with ( + mock.patch("parallel_web_tools.core.credentials.CREDENTIALS_FILE", token_file), + mock.patch("parallel_web_tools.core.credentials.LEGACY_CREDENTIALS_FILE", token_file), + ): + result = runner.invoke(main, ["auth"]) + assert result.exit_code == 0 + assert "PARALLEL_API_KEY" in result.output or "environment" in result.output def test_auth_not_authenticated(self, runner, tmp_path): """Should show not authenticated when no credentials.""" @@ -258,11 +270,33 @@ def test_auth_not_authenticated(self, runner, tmp_path): with mock.patch.dict(os.environ, {}, clear=True): os.environ.pop("PARALLEL_API_KEY", None) - with mock.patch("parallel_web_tools.core.auth.TOKEN_FILE", token_file): + with ( + mock.patch("parallel_web_tools.core.credentials.CREDENTIALS_FILE", token_file), + mock.patch("parallel_web_tools.core.credentials.LEGACY_CREDENTIALS_FILE", token_file), + ): result = runner.invoke(main, ["auth"]) assert result.exit_code == 0 assert "Not authenticated" in result.output or "not" in result.output.lower() + def test_auth_json_includes_selected_org_name(self, runner): + """Should include selected org name in JSON output for OAuth auth.""" + status = { + "authenticated": True, + "method": "oauth", + "token_file": "/tmp/auth.json", + "version": 1, + "selected_org_id": "org_123", + "selected_org_name": "Acme Org", + "has_control_api_tokens": True, + } + + with mock.patch("parallel_web_tools.cli.commands.get_auth_status", return_value=status): + result = runner.invoke(main, ["auth", "--json"]) + + assert result.exit_code == 0 + output = json.loads(result.output) + assert output["selected_org_name"] == "Acme Org" + class TestLogoutCommand: """Tests for the logout command.""" @@ -271,7 +305,10 @@ def test_logout_no_credentials(self, runner, tmp_path): """Should handle logout when no credentials exist.""" token_file = tmp_path / "nonexistent.json" - with mock.patch("parallel_web_tools.core.auth.TOKEN_FILE", token_file): + with ( + mock.patch("parallel_web_tools.core.credentials.CREDENTIALS_FILE", token_file), + mock.patch("parallel_web_tools.core.credentials.LEGACY_CREDENTIALS_FILE", token_file), + ): result = runner.invoke(main, ["logout"]) assert result.exit_code == 0 assert "No stored credentials" in result.output or "no" in result.output.lower() @@ -976,7 +1013,7 @@ def test_all_none_does_not_raise(self): class TestSearchCommandMocked: """Tests for the search command with mocked Parallel SDK.""" - def test_search_successful_json_output(self, runner): + def test_search_successful_json_output(self, runner, mock_cli_client): """Should output JSON for successful search.""" mock_search_result = mock.MagicMock() mock_search_result.search_id = "search_123" @@ -992,17 +1029,8 @@ def test_search_successful_json_output(self, runner): mock_search_result.usage = None mock_search_result.warnings = [] - with mock.patch("parallel_web_tools.cli.commands.get_api_key", return_value="test-key"): - with mock.patch.dict("sys.modules"): - mock_parallel_mod = mock.MagicMock() - mock_client = mock.MagicMock() - mock_client.search.return_value = mock_search_result - mock_parallel_mod.Parallel.return_value = mock_client - sys.modules["parallel"] = mock_parallel_mod - - result = runner.invoke(main, ["search", "test query", "--json"]) - - del sys.modules["parallel"] + mock_cli_client.search.return_value = mock_search_result + result = runner.invoke(main, ["search", "test query", "--json"]) assert result.exit_code == 0 output = json.loads(result.output) @@ -1011,7 +1039,7 @@ def test_search_successful_json_output(self, runner): assert len(output["results"]) == 1 assert output["results"][0]["url"] == "https://example.com" - def test_search_warnings_serialized_in_json_output(self, runner): + def test_search_warnings_serialized_in_json_output(self, runner, mock_cli_client): """Should serialize SDK Warning objects as dicts in JSON output.""" mock_search_result = mock.MagicMock() mock_search_result.search_id = "search_456" @@ -1032,17 +1060,8 @@ def test_search_warnings_serialized_in_json_output(self, runner): mock_search_result.usage = None mock_search_result.warnings = [warning_obj] - with mock.patch("parallel_web_tools.cli.commands.get_api_key", return_value="test-key"): - with mock.patch.dict("sys.modules"): - mock_parallel_mod = mock.MagicMock() - mock_client = mock.MagicMock() - mock_client.search.return_value = mock_search_result - mock_parallel_mod.Parallel.return_value = mock_client - sys.modules["parallel"] = mock_parallel_mod - - result = runner.invoke(main, ["search", "test query", "--json"]) - - del sys.modules["parallel"] + mock_cli_client.search.return_value = mock_search_result + result = runner.invoke(main, ["search", "test query", "--json"]) assert result.exit_code == 0 output = json.loads(result.output) @@ -1052,38 +1071,20 @@ def test_search_warnings_serialized_in_json_output(self, runner): assert warning["message"] == "Excerpts truncated to 500 characters" assert warning["detail"] == {"max_chars_total": 500} - def test_search_api_error_json_mode(self, runner): + def test_search_api_error_json_mode(self, runner, mock_cli_client): """Should output JSON error when API fails in --json mode.""" - with mock.patch("parallel_web_tools.cli.commands.get_api_key", return_value="test-key"): - with mock.patch.dict("sys.modules"): - mock_parallel_mod = mock.MagicMock() - mock_client = mock.MagicMock() - mock_client.search.side_effect = RuntimeError("API unavailable") - mock_parallel_mod.Parallel.return_value = mock_client - sys.modules["parallel"] = mock_parallel_mod - - result = runner.invoke(main, ["search", "test query", "--json"]) - - del sys.modules["parallel"] + mock_cli_client.search.side_effect = RuntimeError("API unavailable") + result = runner.invoke(main, ["search", "test query", "--json"]) assert result.exit_code == EXIT_API_ERROR output = json.loads(result.output) assert output["error"]["message"] == "API unavailable" assert output["error"]["type"] == "RuntimeError" - def test_search_api_error_console_mode(self, runner): + def test_search_api_error_console_mode(self, runner, mock_cli_client): """Should output formatted error when API fails in console mode.""" - with mock.patch("parallel_web_tools.cli.commands.get_api_key", return_value="test-key"): - with mock.patch.dict("sys.modules"): - mock_parallel_mod = mock.MagicMock() - mock_client = mock.MagicMock() - mock_client.search.side_effect = RuntimeError("API unavailable") - mock_parallel_mod.Parallel.return_value = mock_client - sys.modules["parallel"] = mock_parallel_mod - - result = runner.invoke(main, ["search", "test query"]) - - del sys.modules["parallel"] + mock_cli_client.search.side_effect = RuntimeError("API unavailable") + result = runner.invoke(main, ["search", "test query"]) assert result.exit_code == EXIT_API_ERROR assert "API unavailable" in result.output @@ -1436,27 +1437,18 @@ def test_extract_rejects_more_than_20_urls(self, runner): assert result.exit_code != 0 assert "20 URLs" in result.output - def test_extract_accepts_exactly_20_urls(self, runner): + def test_extract_accepts_exactly_20_urls(self, runner, mock_cli_client): """At-the-limit case should not be blocked by the check.""" urls = [f"https://example.com/{i}" for i in range(20)] - with mock.patch("parallel_web_tools.cli.commands.get_api_key", return_value="test-key"): - with mock.patch.dict("sys.modules"): - mock_parallel_mod = mock.MagicMock() - mock_client = mock.MagicMock() - mock_result = mock.MagicMock() - mock_result.extract_id = "ext_20" - mock_result.session_id = None - mock_result.results = [] - mock_result.errors = [] - mock_result.usage = None - mock_result.warnings = None - mock_client.extract.return_value = mock_result - mock_parallel_mod.Parallel.return_value = mock_client - sys.modules["parallel"] = mock_parallel_mod - - result = runner.invoke(main, ["extract", *urls, "--json"]) - - del sys.modules["parallel"] + mock_result = mock.MagicMock() + mock_result.extract_id = "ext_20" + mock_result.session_id = None + mock_result.results = [] + mock_result.errors = [] + mock_result.usage = None + mock_result.warnings = None + mock_cli_client.extract.return_value = mock_result + result = runner.invoke(main, ["extract", *urls, "--json"]) assert result.exit_code == 0 def test_extract_rejects_objective_over_5000_chars(self, runner): @@ -1489,7 +1481,7 @@ def test_search_rejects_combined_domain_count_over_200(self, runner): class TestV1ResponseFieldsSurfaced: """Tests that V1 response fields (session_id, usage) are surfaced in output.""" - def test_search_output_includes_session_id_and_usage(self, runner): + def test_search_output_includes_session_id_and_usage(self, runner, mock_cli_client): """Search output should include session_id and usage fields when present.""" mock_result = mock.MagicMock() mock_result.search_id = "search_v1" @@ -1501,24 +1493,15 @@ def test_search_output_includes_session_id_and_usage(self, runner): mock_result.usage = [usage_item] mock_result.warnings = [] - with mock.patch("parallel_web_tools.cli.commands.get_api_key", return_value="test-key"): - with mock.patch.dict("sys.modules"): - mock_parallel_mod = mock.MagicMock() - mock_client = mock.MagicMock() - mock_client.search.return_value = mock_result - mock_parallel_mod.Parallel.return_value = mock_client - sys.modules["parallel"] = mock_parallel_mod - - result = runner.invoke(main, ["search", "test", "--json"]) - - del sys.modules["parallel"] + mock_cli_client.search.return_value = mock_result + result = runner.invoke(main, ["search", "test", "--json"]) assert result.exit_code == 0 output = json.loads(result.stdout) assert output["session_id"] == "sess_xyz" assert output["usage"] == [{"name": "search_basic", "count": 1}] - def test_extract_output_includes_session_id_and_usage(self, runner): + def test_extract_output_includes_session_id_and_usage(self, runner, mock_cli_client): """Extract output should include session_id and usage fields when present.""" mock_result = mock.MagicMock() mock_result.extract_id = "ext_v1" @@ -1537,17 +1520,8 @@ def test_extract_output_includes_session_id_and_usage(self, runner): mock_result.usage = [usage_item] mock_result.warnings = None - with mock.patch("parallel_web_tools.cli.commands.get_api_key", return_value="test-key"): - with mock.patch.dict("sys.modules"): - mock_parallel_mod = mock.MagicMock() - mock_client = mock.MagicMock() - mock_client.extract.return_value = mock_result - mock_parallel_mod.Parallel.return_value = mock_client - sys.modules["parallel"] = mock_parallel_mod - - result = runner.invoke(main, ["extract", "https://example.com", "--json"]) - - del sys.modules["parallel"] + mock_cli_client.extract.return_value = mock_result + result = runner.invoke(main, ["extract", "https://example.com", "--json"]) assert result.exit_code == 0 output = json.loads(result.stdout) @@ -1575,22 +1549,13 @@ def _setup_mock_search(self, mock_client): ("agentic", "advanced"), ], ) - def test_deprecated_modes_emit_warning_to_stderr(self, runner, deprecated_mode, expected_new): + def test_deprecated_modes_emit_warning_to_stderr(self, runner, mock_cli_client, deprecated_mode, expected_new): """Should warn on deprecated mode values and translate them.""" - with mock.patch("parallel_web_tools.cli.commands.get_api_key", return_value="test-key"): - with mock.patch.dict("sys.modules"): - mock_parallel_mod = mock.MagicMock() - mock_client = mock.MagicMock() - self._setup_mock_search(mock_client) - mock_parallel_mod.Parallel.return_value = mock_client - sys.modules["parallel"] = mock_parallel_mod - - result = runner.invoke( - main, - ["search", "test", "--mode", deprecated_mode, "--json"], - ) - - del sys.modules["parallel"] + self._setup_mock_search(mock_cli_client) + result = runner.invoke( + main, + ["search", "test", "--mode", deprecated_mode, "--json"], + ) assert result.exit_code == 0 assert "[deprecated]" in result.stderr @@ -1599,26 +1564,17 @@ def test_deprecated_modes_emit_warning_to_stderr(self, runner, deprecated_mode, # JSON stdout must remain clean json.loads(result.stdout) # SDK call uses translated mode - call_kwargs = mock_client.search.call_args.kwargs + call_kwargs = mock_cli_client.search.call_args.kwargs assert call_kwargs["mode"] == expected_new @pytest.mark.parametrize("new_mode", ["basic", "advanced"]) - def test_new_modes_do_not_emit_warning(self, runner, new_mode): + def test_new_modes_do_not_emit_warning(self, runner, mock_cli_client, new_mode): """Should not warn when V1-native mode values are used.""" - with mock.patch("parallel_web_tools.cli.commands.get_api_key", return_value="test-key"): - with mock.patch.dict("sys.modules"): - mock_parallel_mod = mock.MagicMock() - mock_client = mock.MagicMock() - self._setup_mock_search(mock_client) - mock_parallel_mod.Parallel.return_value = mock_client - sys.modules["parallel"] = mock_parallel_mod - - result = runner.invoke( - main, - ["search", "test", "--mode", new_mode, "--json"], - ) - - del sys.modules["parallel"] + self._setup_mock_search(mock_cli_client) + result = runner.invoke( + main, + ["search", "test", "--mode", new_mode, "--json"], + ) assert result.exit_code == 0 assert "[deprecated]" not in result.stderr @@ -1643,22 +1599,13 @@ def _setup_mock_extract(self, mock_client, with_excerpts=True): mock_result.warnings = None mock_client.extract.return_value = mock_result - def test_no_excerpts_emits_warning_and_strips_excerpts_from_output(self, runner): + def test_no_excerpts_emits_warning_and_strips_excerpts_from_output(self, runner, mock_cli_client): """--no-excerpts should warn (semantics changed) and strip excerpts client-side.""" - with mock.patch("parallel_web_tools.cli.commands.get_api_key", return_value="test-key"): - with mock.patch.dict("sys.modules"): - mock_parallel_mod = mock.MagicMock() - mock_client = mock.MagicMock() - self._setup_mock_extract(mock_client) - mock_parallel_mod.Parallel.return_value = mock_client - sys.modules["parallel"] = mock_parallel_mod - - result = runner.invoke( - main, - ["extract", "https://example.com", "--no-excerpts", "--json"], - ) - - del sys.modules["parallel"] + self._setup_mock_extract(mock_cli_client) + result = runner.invoke( + main, + ["extract", "https://example.com", "--no-excerpts", "--json"], + ) assert result.exit_code == 0 assert "[deprecated]" in result.stderr @@ -1667,22 +1614,13 @@ def test_no_excerpts_emits_warning_and_strips_excerpts_from_output(self, runner) # Excerpts should be stripped from the CLI output assert "excerpts" not in output["results"][0] - def test_no_no_excerpts_keeps_excerpts_and_no_warning(self, runner): + def test_no_no_excerpts_keeps_excerpts_and_no_warning(self, runner, mock_cli_client): """Default extract should not warn and should include excerpts.""" - with mock.patch("parallel_web_tools.cli.commands.get_api_key", return_value="test-key"): - with mock.patch.dict("sys.modules"): - mock_parallel_mod = mock.MagicMock() - mock_client = mock.MagicMock() - self._setup_mock_extract(mock_client) - mock_parallel_mod.Parallel.return_value = mock_client - sys.modules["parallel"] = mock_parallel_mod - - result = runner.invoke( - main, - ["extract", "https://example.com", "--json"], - ) - - del sys.modules["parallel"] + self._setup_mock_extract(mock_cli_client) + result = runner.invoke( + main, + ["extract", "https://example.com", "--json"], + ) assert result.exit_code == 0 assert "[deprecated]" not in result.stderr @@ -1693,26 +1631,17 @@ def test_no_no_excerpts_keeps_excerpts_and_no_warning(self, runner): class TestExtractCommandMocked: """Tests for the extract command with mocked Parallel SDK.""" - def test_extract_api_error_json_mode(self, runner): + def test_extract_api_error_json_mode(self, runner, mock_cli_client): """Should output JSON error when extract API fails in --json mode.""" - with mock.patch("parallel_web_tools.cli.commands.get_api_key", return_value="test-key"): - with mock.patch.dict("sys.modules"): - mock_parallel_mod = mock.MagicMock() - mock_client = mock.MagicMock() - mock_client.extract.side_effect = ConnectionError("Network error") - mock_parallel_mod.Parallel.return_value = mock_client - sys.modules["parallel"] = mock_parallel_mod - - result = runner.invoke(main, ["extract", "https://example.com", "--json"]) - - del sys.modules["parallel"] + mock_cli_client.extract.side_effect = ConnectionError("Network error") + result = runner.invoke(main, ["extract", "https://example.com", "--json"]) assert result.exit_code == EXIT_API_ERROR output = json.loads(result.output) assert output["error"]["type"] == "ConnectionError" assert "Network error" in output["error"]["message"] - def test_extract_successful_json_output(self, runner): + def test_extract_successful_json_output(self, runner, mock_cli_client): """Should output structured JSON for successful extraction.""" mock_extract_result = mock.MagicMock() mock_extract_result.extract_id = "ext_123" @@ -1728,17 +1657,8 @@ def test_extract_successful_json_output(self, runner): mock_extract_result.usage = None mock_extract_result.warnings = None - with mock.patch("parallel_web_tools.cli.commands.get_api_key", return_value="test-key"): - with mock.patch.dict("sys.modules"): - mock_parallel_mod = mock.MagicMock() - mock_client = mock.MagicMock() - mock_client.extract.return_value = mock_extract_result - mock_parallel_mod.Parallel.return_value = mock_client - sys.modules["parallel"] = mock_parallel_mod - - result = runner.invoke(main, ["extract", "https://example.com", "--json"]) - - del sys.modules["parallel"] + mock_cli_client.extract.return_value = mock_extract_result + result = runner.invoke(main, ["extract", "https://example.com", "--json"]) assert result.exit_code == 0 output = json.loads(result.output) @@ -1749,7 +1669,7 @@ def test_extract_successful_json_output(self, runner): assert output["results"][0]["publish_date"] == "2025-01-15" assert output["warnings"] == [] - def test_extract_warnings_serialized_in_json_output(self, runner): + def test_extract_warnings_serialized_in_json_output(self, runner, mock_cli_client): """Should serialize SDK Warning objects as dicts in JSON output.""" mock_extract_result = mock.MagicMock() mock_extract_result.extract_id = "ext_456" @@ -1769,17 +1689,8 @@ def test_extract_warnings_serialized_in_json_output(self, runner): mock_extract_result.usage = None mock_extract_result.warnings = [warning_obj] - with mock.patch("parallel_web_tools.cli.commands.get_api_key", return_value="test-key"): - with mock.patch.dict("sys.modules"): - mock_parallel_mod = mock.MagicMock() - mock_client = mock.MagicMock() - mock_client.extract.return_value = mock_extract_result - mock_parallel_mod.Parallel.return_value = mock_client - sys.modules["parallel"] = mock_parallel_mod - - result = runner.invoke(main, ["extract", "https://example.com", "--json"]) - - del sys.modules["parallel"] + mock_cli_client.extract.return_value = mock_extract_result + result = runner.invoke(main, ["extract", "https://example.com", "--json"]) assert result.exit_code == 0 output = json.loads(result.output) @@ -1789,7 +1700,7 @@ def test_extract_warnings_serialized_in_json_output(self, runner): assert warning["message"] == "Excerpts truncated" assert warning["detail"] == {"max_chars_total": 500} - def test_extract_errors_serialized_in_json_output(self, runner): + def test_extract_errors_serialized_in_json_output(self, runner, mock_cli_client): """Should serialize extract errors with correct API field names.""" mock_extract_result = mock.MagicMock() mock_extract_result.extract_id = "ext_789" @@ -1804,17 +1715,8 @@ def test_extract_errors_serialized_in_json_output(self, runner): mock_extract_result.usage = None mock_extract_result.warnings = None - with mock.patch("parallel_web_tools.cli.commands.get_api_key", return_value="test-key"): - with mock.patch.dict("sys.modules"): - mock_parallel_mod = mock.MagicMock() - mock_client = mock.MagicMock() - mock_client.extract.return_value = mock_extract_result - mock_parallel_mod.Parallel.return_value = mock_client - sys.modules["parallel"] = mock_parallel_mod - - result = runner.invoke(main, ["extract", "https://example.com/broken", "--json"]) - - del sys.modules["parallel"] + mock_cli_client.extract.return_value = mock_extract_result + result = runner.invoke(main, ["extract", "https://example.com/broken", "--json"]) assert result.exit_code == 0 output = json.loads(result.output) @@ -2685,3 +2587,496 @@ def test_completion_install_standalone_rejected(self, runner): with mock.patch("parallel_web_tools.cli.commands._STANDALONE_MODE", True): result = runner.invoke(main, ["completion", "install", "--shell", "bash"]) assert result.exit_code != 0 + + +# --------------------------------------------------------------------------- +# login email → magic link +# --------------------------------------------------------------------------- + + +def _device_info(): + from parallel_web_tools.core.auth import DeviceCodeInfo + + return DeviceCodeInfo( + device_code="dc_xyz", + user_code="ABCD-1234", + verification_uri="http://verif.example", + verification_uri_complete="http://verif.example?user_code=ABCD-1234", + expires_in=600, + interval=5, + ) + + +def _fake_get_api_key(info): + """Factory: a get_api_key stub that invokes on_device_code(info) then returns.""" + + def fake(force_login=False, on_device_code=None, login_hint=None, **_): + # Match auth.get_api_key's signature loosely so both kwargs- and args-based calls work. + assert on_device_code is not None + on_device_code(info) + return "sk_fake" + + return fake + + +class TestLoginEmailCommand: + def test_sends_magic_link_and_skips_browser(self, runner): + info = _device_info() + with ( + mock.patch( + "parallel_web_tools.cli.commands.get_api_key", + side_effect=_fake_get_api_key(info), + ), + mock.patch("parallel_web_tools.core.auth.send_magic_link") as mock_send, + mock.patch("parallel_web_tools.core.auth._ensure_client_id", return_value="cid_xyz"), + mock.patch("webbrowser.open") as mock_browser, + ): + result = runner.invoke(main, ["login", "email", "u@example.com"]) + + assert result.exit_code == 0 + mock_send.assert_called_once_with(client_id="cid_xyz", email="u@example.com", user_code="ABCD-1234") + mock_browser.assert_not_called() + assert "Magic link sent to u@example.com" in result.output + # Still shows the code as a fallback path. + assert "ABCD-1234" in result.output + + def test_json_mode_reports_magic_link_sent(self, runner): + info = _device_info() + with ( + mock.patch( + "parallel_web_tools.cli.commands.get_api_key", + side_effect=_fake_get_api_key(info), + ), + mock.patch("parallel_web_tools.core.auth.send_magic_link"), + mock.patch("parallel_web_tools.core.auth._ensure_client_id", return_value="cid_xyz"), + mock.patch("webbrowser.open") as mock_browser, + ): + result = runner.invoke(main, ["login", "--json", "email", "u@example.com"]) + + assert result.exit_code == 0 + mock_browser.assert_not_called() + # First line is the waiting_for_authorization payload; the trailing + # "authenticated" line is appended by _run_login. + first_line = result.output.splitlines()[0] + payload = json.loads(first_line) + assert payload["status"] == "waiting_for_authorization" + assert payload["magic_link_sent"] is True + assert payload["user_code"] == "ABCD-1234" + + def test_falls_back_when_magic_link_fails(self, runner): + info = _device_info() + with ( + mock.patch( + "parallel_web_tools.cli.commands.get_api_key", + side_effect=_fake_get_api_key(info), + ), + mock.patch( + "parallel_web_tools.core.auth.send_magic_link", + side_effect=Exception("SMTP unavailable"), + ), + mock.patch("parallel_web_tools.core.auth._ensure_client_id", return_value="cid_xyz"), + mock.patch( + "parallel_web_tools.core.auth._is_headless", + return_value=True, # keep the test hermetic: don't attempt real browser open + ), + mock.patch("webbrowser.open") as mock_browser, + ): + result = runner.invoke(main, ["login", "email", "u@example.com"]) + + assert result.exit_code == 0 + # Magic-link failure path falls through to the manual-flow display. + assert "Could not send magic link" in result.output + assert "SMTP unavailable" in result.output + assert "ABCD-1234" in result.output + # Headless env: browser must not open even in the fallback path. + mock_browser.assert_not_called() + + def test_json_mode_reports_magic_link_error(self, runner): + info = _device_info() + with ( + mock.patch( + "parallel_web_tools.cli.commands.get_api_key", + side_effect=_fake_get_api_key(info), + ), + mock.patch( + "parallel_web_tools.core.auth.send_magic_link", + side_effect=Exception("SMTP unavailable"), + ), + mock.patch("parallel_web_tools.core.auth._ensure_client_id", return_value="cid_xyz"), + ): + result = runner.invoke(main, ["login", "--json", "email", "u@example.com"]) + + assert result.exit_code == 0 + first_line = result.output.splitlines()[0] + payload = json.loads(first_line) + assert payload["magic_link_sent"] is False + assert "SMTP unavailable" in payload["magic_link_error"] + + +class TestLoginWithoutEmailUnchanged: + def test_no_email_still_opens_browser(self, runner): + info = _device_info() + with ( + mock.patch( + "parallel_web_tools.cli.commands.get_api_key", + side_effect=_fake_get_api_key(info), + ), + mock.patch("parallel_web_tools.core.auth.send_magic_link") as mock_send, + mock.patch("parallel_web_tools.core.auth._is_headless", return_value=False), + mock.patch("webbrowser.open") as mock_browser, + ): + result = runner.invoke(main, ["login"]) + + assert result.exit_code == 0 + # No email → no magic-link call. + mock_send.assert_not_called() + # Browser still opens in the plain `login` flow. + mock_browser.assert_called_once() + + +class TestLoginGoogleCommand: + def test_opens_browser_with_google_login_hint(self, runner): + info = _device_info() + with ( + mock.patch( + "parallel_web_tools.cli.commands.get_api_key", + side_effect=_fake_get_api_key(info), + ), + mock.patch("parallel_web_tools.core.auth.send_magic_link") as mock_send, + mock.patch("parallel_web_tools.core.auth._is_headless", return_value=False), + mock.patch("webbrowser.open") as mock_browser, + ): + result = runner.invoke(main, ["login", "google"]) + + assert result.exit_code == 0 + # No magic-link send on google login. + mock_send.assert_not_called() + # Browser opens with the google hint. + mock_browser.assert_called_once() + opened_url = mock_browser.call_args.args[0] + assert "login_hint=login%3Dgoogle" in opened_url + + +class TestLoginSsoCommand: + def test_opens_browser_with_sso_hint_and_separate_email_param(self, runner): + info = _device_info() + with ( + mock.patch( + "parallel_web_tools.cli.commands.get_api_key", + side_effect=_fake_get_api_key(info), + ), + mock.patch("parallel_web_tools.core.auth.send_magic_link") as mock_send, + mock.patch("parallel_web_tools.core.auth._is_headless", return_value=False), + mock.patch("webbrowser.open") as mock_browser, + ): + result = runner.invoke(main, ["login", "sso", "u@example.com"]) + + assert result.exit_code == 0 + # SSO still uses browser-based auth, no magic link. + mock_send.assert_not_called() + mock_browser.assert_called_once() + opened_url = mock_browser.call_args.args[0] + # URL-encoded login=sso (no comma-email inside the hint). + assert "login_hint=login%3Dsso" in opened_url + # Email is a separate top-level query param. + assert "email=u%40example.com" in opened_url + # And the old bundled form must not leak through. + assert "login%3Dsso%2Ce" not in opened_url + + +class TestBuildLoginHint: + def test_email_hint_does_not_include_email(self): + # Email travels as a separate `email=…` query param via _login_extra_params. + from parallel_web_tools.cli.commands import _build_login_hint + + assert _build_login_hint("email", "u@example.com") == "login=email" + + def test_login_extra_params_carries_email_for_email_and_sso(self): + from parallel_web_tools.cli.commands import _login_extra_params + + assert _login_extra_params("email", "u@example.com") == {"email": "u@example.com"} + assert _login_extra_params("sso", "u@example.com") == {"email": "u@example.com"} + # google / plain carry no identity → no extra param. + assert _login_extra_params("google", None) is None + assert _login_extra_params(None, None) is None + + def test_google_ignores_email(self): + from parallel_web_tools.cli.commands import _build_login_hint + + assert _build_login_hint("google", None) == "login=google" + + def test_sso_hint_does_not_include_email(self): + # SSO email travels as a separate `email=…` query param (see _login_extra_params), + # NOT embedded in the hint value. + from parallel_web_tools.cli.commands import _build_login_hint + + assert _build_login_hint("sso", "u@example.com") == "login=sso" + + def test_none_method_returns_none(self): + from parallel_web_tools.cli.commands import _build_login_hint + + assert _build_login_hint(None, None) is None + assert _build_login_hint(None, "u@example.com") is None + + def test_sso_without_email_errors(self): + from parallel_web_tools.cli.commands import _build_login_hint + + with pytest.raises(ValueError, match="requires an email"): + _build_login_hint("sso", None) + + def test_email_without_email_errors(self): + from parallel_web_tools.cli.commands import _build_login_hint + + with pytest.raises(ValueError, match="requires an email"): + _build_login_hint("email", None) + + def test_unknown_method_errors(self): + from parallel_web_tools.cli.commands import _build_login_hint + + with pytest.raises(ValueError, match="Unknown login_method"): + _build_login_hint("saml", None) + + +# --------------------------------------------------------------------------- +# balance get / balance add +# --------------------------------------------------------------------------- + + +def _balance_model(**overrides): + """Build a BalanceResponse pydantic instance for CLI-level mocking.""" + from parallel_web_tools.core.service_types import BalanceResponse + + base = BalanceResponse( + org_id="org_abc", + credit_balance_cents=1500, + pending_debit_balance_cents=0, + will_invoice=False, + ) + return base.model_copy(update=overrides) if overrides else base + + +class TestBalanceGroup: + def test_group_help_lists_subcommands(self, runner): + result = runner.invoke(main, ["balance", "--help"]) + assert result.exit_code == 0 + assert "get" in result.output + assert "add" in result.output + + def test_get_help(self, runner): + result = runner.invoke(main, ["balance", "get", "--help"]) + assert result.exit_code == 0 + assert "credit balance" in result.output.lower() + + def test_add_help(self, runner): + result = runner.invoke(main, ["balance", "add", "--help"]) + assert result.exit_code == 0 + assert "AMOUNT_CENTS" in result.output + assert "--idempotency-key" in result.output + + +class TestBalanceGetCommand: + def test_json_output(self, runner): + balance = _balance_model(credit_balance_cents=1234, pending_debit_balance_cents=56) + with ( + mock.patch("parallel_web_tools.cli.commands.get_control_api_access_token", return_value="atk"), + mock.patch("parallel_web_tools.core.service.get_balance", return_value=balance) as mock_get, + ): + result = runner.invoke(main, ["balance", "--json", "get"]) + + assert result.exit_code == 0 + mock_get.assert_called_once_with("atk") + output = json.loads(result.output) + assert output["org_id"] == "org_abc" + assert output["credit_balance_cents"] == 1234 + assert output["pending_debit_balance_cents"] == 56 + + def test_console_output(self, runner): + balance = _balance_model(credit_balance_cents=250) + with ( + mock.patch("parallel_web_tools.cli.commands.get_control_api_access_token", return_value="atk"), + mock.patch("parallel_web_tools.core.service.get_balance", return_value=balance), + ): + result = runner.invoke(main, ["balance", "get"]) + + assert result.exit_code == 0 + assert "org_abc" in result.output + # $2.50 with a cents-in-parens suffix. + assert "$2.50" in result.output + assert "250" in result.output + + def test_will_invoice_flag_shown(self, runner): + balance = _balance_model(credit_balance_cents=0, will_invoice=True) + with ( + mock.patch("parallel_web_tools.cli.commands.get_control_api_access_token", return_value="atk"), + mock.patch("parallel_web_tools.core.service.get_balance", return_value=balance), + ): + result = runner.invoke(main, ["balance", "get"]) + assert result.exit_code == 0 + assert "invoice" in result.output.lower() + + def test_reauth_required_exits_auth_error(self, runner): + from parallel_web_tools.core.auth import ReauthenticationRequired + + with mock.patch( + "parallel_web_tools.cli.commands.get_control_api_access_token", + side_effect=ReauthenticationRequired("not logged in"), + ): + result = runner.invoke(main, ["balance", "get"]) + + assert result.exit_code == EXIT_AUTH_ERROR + assert "Authentication required" in result.output + + def test_service_api_error_exits_api_error(self, runner): + from parallel_web_tools.core.service import ServiceApiError + + with ( + mock.patch("parallel_web_tools.cli.commands.get_control_api_access_token", return_value="atk"), + mock.patch("parallel_web_tools.core.service.get_balance", side_effect=ServiceApiError("boom")), + ): + result = runner.invoke(main, ["balance", "get"]) + + assert result.exit_code == EXIT_API_ERROR + assert "Balance API error" in result.output + + +class TestBalanceAddCommand: + def test_json_output_derives_idempotency_key(self, runner): + balance = _balance_model(credit_balance_cents=1600) + captured_key: dict = {} + + def fake_add(token, amount_cents, idempotency_key): + captured_key["key"] = idempotency_key + return balance + + with ( + mock.patch("parallel_web_tools.cli.commands.get_control_api_access_token", return_value="atk"), + mock.patch("parallel_web_tools.core.service.add_balance", side_effect=fake_add), + mock.patch("parallel_web_tools.core.auth._ensure_client_id", return_value="cid_xyz"), + mock.patch("parallel_web_tools.cli.commands.time.time", return_value=1_700_000_123.0), + ): + result = runner.invoke(main, ["balance", "--json", "add", "100"]) + + assert result.exit_code == 0 + # five_min_bucket = floor(1_700_000_123 / 300) * 300 = 1_700_000_100 + assert captured_key["key"] == "cid_xyz-100-1700000100" + output = json.loads(result.output) + assert output["credit_balance_cents"] == 1600 + + def test_console_output_shows_charge_and_new_balance(self, runner): + balance = _balance_model(credit_balance_cents=1600) + with ( + mock.patch("parallel_web_tools.cli.commands.get_control_api_access_token", return_value="atk"), + mock.patch("parallel_web_tools.core.service.add_balance", return_value=balance), + mock.patch("parallel_web_tools.core.auth._ensure_client_id", return_value="cid_xyz"), + ): + result = runner.invoke(main, ["balance", "add", "100"]) + + assert result.exit_code == 0 + assert "$1.00" in result.output # charge amount + assert "$16.00" in result.output # new balance + + def test_explicit_idempotency_key_overrides_derivation(self, runner): + balance = _balance_model() + with ( + mock.patch("parallel_web_tools.cli.commands.get_control_api_access_token", return_value="atk"), + mock.patch("parallel_web_tools.core.service.add_balance", return_value=balance) as mock_add, + mock.patch("parallel_web_tools.core.auth._ensure_client_id") as mock_ensure, + ): + result = runner.invoke(main, ["balance", "add", "100", "--idempotency-key", "fixed-key"]) + + assert result.exit_code == 0 + # _ensure_client_id must NOT be called when an explicit key was provided. + mock_ensure.assert_not_called() + assert mock_add.call_args.args[2] == "fixed-key" + + def test_same_bucket_produces_same_key(self, runner): + """Two invocations inside the same 5-min bucket must derive the same key.""" + keys: list[str] = [] + + def capture_key(token, amount_cents, idempotency_key): + keys.append(idempotency_key) + return _balance_model() + + # 1_700_000_100 is 300-aligned (5_666_667 * 300). Both timestamps fall + # inside the [1_700_000_100, 1_700_000_400) bucket. + with ( + mock.patch("parallel_web_tools.cli.commands.get_control_api_access_token", return_value="atk"), + mock.patch("parallel_web_tools.core.service.add_balance", side_effect=capture_key), + mock.patch("parallel_web_tools.core.auth._ensure_client_id", return_value="cid_xyz"), + mock.patch("parallel_web_tools.cli.commands.time.time", side_effect=[1_700_000_100, 1_700_000_399]), + ): + assert runner.invoke(main, ["balance", "add", "100"]).exit_code == 0 + assert runner.invoke(main, ["balance", "add", "100"]).exit_code == 0 + + assert keys[0] == keys[1] == "cid_xyz-100-1700000100" + + def test_next_bucket_produces_different_key(self, runner): + keys: list[str] = [] + + def capture_key(token, amount_cents, idempotency_key): + keys.append(idempotency_key) + return _balance_model() + + with ( + mock.patch("parallel_web_tools.cli.commands.get_control_api_access_token", return_value="atk"), + mock.patch("parallel_web_tools.core.service.add_balance", side_effect=capture_key), + mock.patch("parallel_web_tools.core.auth._ensure_client_id", return_value="cid_xyz"), + mock.patch("parallel_web_tools.cli.commands.time.time", side_effect=[1_700_000_100, 1_700_000_400]), + ): + runner.invoke(main, ["balance", "add", "100"]) + runner.invoke(main, ["balance", "add", "100"]) + + assert keys[0] == "cid_xyz-100-1700000100" + assert keys[1] == "cid_xyz-100-1700000400" + + def test_zero_amount_passes_through_to_service(self, runner): + balance = _balance_model() + with ( + mock.patch("parallel_web_tools.cli.commands.get_control_api_access_token", return_value="atk"), + mock.patch("parallel_web_tools.core.service.add_balance", return_value=balance) as mock_add, + mock.patch("parallel_web_tools.core.auth._ensure_client_id", return_value="cid_xyz"), + ): + result = runner.invoke(main, ["balance", "add", "0"]) + + assert result.exit_code == 0 + assert mock_add.call_args.args[1] == 0 + + def test_large_amount_passes_through_to_service(self, runner): + balance = _balance_model() + with ( + mock.patch("parallel_web_tools.cli.commands.get_control_api_access_token", return_value="atk"), + mock.patch("parallel_web_tools.core.service.add_balance", return_value=balance) as mock_add, + mock.patch("parallel_web_tools.core.auth._ensure_client_id", return_value="cid_xyz"), + ): + result = runner.invoke(main, ["balance", "add", "1001"]) + + assert result.exit_code == 0 + assert mock_add.call_args.args[1] == 1001 + + def test_reauth_required_exits_auth_error(self, runner): + from parallel_web_tools.core.auth import ReauthenticationRequired + + with ( + mock.patch( + "parallel_web_tools.cli.commands.get_control_api_access_token", + side_effect=ReauthenticationRequired("not logged in"), + ), + mock.patch("parallel_web_tools.core.auth._ensure_client_id", return_value="cid_xyz"), + ): + result = runner.invoke(main, ["balance", "add", "100"]) + + assert result.exit_code == EXIT_AUTH_ERROR + assert "Authentication required" in result.output + + def test_service_api_error_exits_api_error(self, runner): + from parallel_web_tools.core.service import ServiceApiError + + with ( + mock.patch("parallel_web_tools.cli.commands.get_control_api_access_token", return_value="atk"), + mock.patch("parallel_web_tools.core.service.add_balance", side_effect=ServiceApiError("card declined")), + mock.patch("parallel_web_tools.core.auth._ensure_client_id", return_value="cid_xyz"), + ): + result = runner.invoke(main, ["balance", "add", "100"]) + + assert result.exit_code == EXIT_API_ERROR + assert "Balance API error" in result.output diff --git a/tests/test_credentials.py b/tests/test_credentials.py new file mode 100644 index 0000000..f21e42d --- /dev/null +++ b/tests/test_credentials.py @@ -0,0 +1,258 @@ +"""Tests for the versioned credentials storage module.""" + +import json +import os + +import pytest + +from parallel_web_tools.core import credentials +from parallel_web_tools.core.credentials import ( + CURRENT_VERSION, + LEGACY_ORG_ID, + ControlApiTokens, + Credentials, + OrgCredentials, + _migrate_v0, + delete, + get_selected_api_key, + load, + save, + set_api_key_for_org, +) + + +@pytest.fixture +def creds_file(tmp_path, monkeypatch): + """Patch auth.json path to a tmp path for isolation.""" + path = tmp_path / "auth.json" + monkeypatch.setattr(credentials, "AUTH_FILE", path) + monkeypatch.setattr(credentials, "CREDENTIALS_FILE", path) + monkeypatch.setattr(credentials, "LEGACY_CREDENTIALS_FILE", tmp_path / "credentials.json") + return path + + +@pytest.fixture +def legacy_file(tmp_path): + return tmp_path / "credentials.json" + + +class TestMigrationV0: + def test_migrate_v0_with_access_token(self): + result = _migrate_v0({"access_token": "abc123"}) + assert result == { + "version": CURRENT_VERSION, + "selected_org_id": LEGACY_ORG_ID, + "orgs": {LEGACY_ORG_ID: {"api_key": "abc123"}}, + } + + def test_migrate_v0_empty(self): + result = _migrate_v0({}) + assert result["version"] == CURRENT_VERSION + assert result["selected_org_id"] is None + assert result["orgs"] == {} + + +class TestLoad: + def test_load_nonexistent_returns_none(self, creds_file): + assert load() is None + + def test_load_corrupted_returns_none(self, creds_file): + creds_file.parent.mkdir(parents=True, exist_ok=True) + creds_file.write_text("not valid json {{{") + assert load() is None + + def test_load_non_dict_returns_none(self, creds_file): + creds_file.parent.mkdir(parents=True, exist_ok=True) + creds_file.write_text(json.dumps(["a", "b"])) + assert load() is None + + def test_load_v0_migrates_forward_to_auth_without_rewriting_legacy_file(self, creds_file, legacy_file): + legacy_file.parent.mkdir(parents=True, exist_ok=True) + legacy_file.write_text(json.dumps({"access_token": "tok_v0"})) + + creds = load() + + assert creds is not None + assert creds.version == CURRENT_VERSION + assert creds.selected_org_id == LEGACY_ORG_ID + assert creds.orgs[LEGACY_ORG_ID].api_key == "tok_v0" + + assert creds_file.exists() + auth_disk = json.loads(creds_file.read_text()) + assert auth_disk["version"] == CURRENT_VERSION + assert auth_disk["selected_org_id"] == LEGACY_ORG_ID + assert auth_disk["orgs"][LEGACY_ORG_ID]["api_key"] == "tok_v0" + assert json.loads(legacy_file.read_text()) == {"access_token": "tok_v0"} + + def test_load_v1_roundtrip(self, creds_file): + original = Credentials( + selected_org_id="org_abc", + client_id="cid_registered", + orgs={ + "org_abc": OrgCredentials( + api_key="sk_test", + org_name="Acme Org", + control_api=ControlApiTokens( + access_token="atk", + access_token_expires_at=1710000600, + access_token_scopes=["keys:write", "balance:read"], + refresh_token="rtk", + refresh_token_expires_at=1710604800, + authorization_expires_at=1717776000, + ), + ) + }, + ) + save(original) + + loaded = load() + assert loaded == original + assert loaded is not None and loaded.client_id == "cid_registered" + + def test_load_prefers_existing_auth_file_over_legacy_file(self, creds_file, legacy_file): + creds_file.parent.mkdir(parents=True, exist_ok=True) + creds_file.write_text( + json.dumps({"version": 1, "selected_org_id": None, "orgs": {}, "client_id": "cid_registered"}) + ) + legacy_file.write_text(json.dumps({"access_token": "tok_v0"})) + + loaded = load() + + assert loaded is not None + assert loaded.client_id == "cid_registered" + assert loaded.selected_org_id is None + assert loaded.orgs == {} + + def test_migrated_v0_has_no_client_id(self, creds_file, legacy_file): + # v0 files never carried a client_id — migration must leave it unset + # so _ensure_client_id knows to register on the next login. + legacy_file.parent.mkdir(parents=True, exist_ok=True) + legacy_file.write_text(json.dumps({"access_token": "tok_v0"})) + loaded = load() + assert loaded is not None + assert loaded.client_id is None + assert loaded.orgs[LEGACY_ORG_ID].org_name is None + + +class TestSave: + def test_save_creates_parent_dir(self, tmp_path, monkeypatch): + path = tmp_path / "nested" / "subdir" / "auth.json" + monkeypatch.setattr(credentials, "CREDENTIALS_FILE", path) + monkeypatch.setattr(credentials, "AUTH_FILE", path) + monkeypatch.setattr(credentials, "LEGACY_CREDENTIALS_FILE", tmp_path / "nested" / "subdir" / "credentials.json") + save(Credentials()) + assert path.exists() + + def test_save_sets_0600_permissions(self, creds_file): + save(Credentials(selected_org_id="x", orgs={"x": OrgCredentials(api_key="k")})) + mode = oct(creds_file.stat().st_mode)[-3:] + assert mode == "600" + + def test_save_does_not_write_legacy_file(self, creds_file, legacy_file): + save(Credentials(selected_org_id="x", orgs={"x": OrgCredentials(api_key="k")})) + assert not legacy_file.exists() + + def test_atomic_write_preserves_existing_on_failure(self, creds_file, monkeypatch): + # Write an initial valid file. + save(Credentials(selected_org_id="orig", orgs={"orig": OrgCredentials(api_key="original")})) + original_contents = creds_file.read_text() + + # Make os.replace blow up during the next save. + def boom(src, dst): + raise OSError("simulated failure") + + monkeypatch.setattr(os, "replace", boom) + + with pytest.raises(OSError, match="simulated failure"): + save(Credentials(selected_org_id="new", orgs={"new": OrgCredentials(api_key="new")})) + + # Original file should be untouched. + assert creds_file.read_text() == original_contents + + # And there should be no leftover temp files in the parent dir. + leftovers = [p for p in creds_file.parent.iterdir() if p.name.startswith(".auth.")] + assert leftovers == [] + + +class TestDelete: + def test_delete_existing(self, creds_file): + save(Credentials()) + assert delete() is True + assert not creds_file.exists() + + def test_delete_removes_legacy_file_too(self, creds_file, legacy_file): + save(Credentials(selected_org_id="x", orgs={"x": OrgCredentials(api_key="k")})) + assert delete() is True + assert not creds_file.exists() + assert not legacy_file.exists() + + def test_delete_nonexistent(self, creds_file): + assert delete() is False + + +class TestHelpers: + def test_get_selected_api_key_none_when_empty(self, creds_file): + assert get_selected_api_key() is None + + def test_get_selected_api_key_returns_selected(self, creds_file): + save( + Credentials( + selected_org_id="a", + orgs={ + "a": OrgCredentials(api_key="key_a"), + "b": OrgCredentials(api_key="key_b"), + }, + ) + ) + assert get_selected_api_key() == "key_a" + + def test_get_selected_api_key_no_selection(self, creds_file): + save(Credentials(orgs={"a": OrgCredentials(api_key="key_a")})) + assert get_selected_api_key() is None + + def test_set_api_key_for_org_creates_and_selects(self, creds_file): + set_api_key_for_org("org_new", "sk_xyz") + creds = load() + assert creds is not None + assert creds.selected_org_id == "org_new" + assert creds.orgs["org_new"].api_key == "sk_xyz" + + def test_set_api_key_for_org_preserves_selection(self, creds_file): + set_api_key_for_org("org_a", "sk_a") # becomes selected + set_api_key_for_org("org_b", "sk_b") # should NOT change selection + creds = load() + assert creds is not None + assert creds.selected_org_id == "org_a" + assert creds.orgs["org_a"].api_key == "sk_a" + assert creds.orgs["org_b"].api_key == "sk_b" + + def test_set_api_key_for_org_updates_existing(self, creds_file): + set_api_key_for_org("org_a", "old_key") + set_api_key_for_org("org_a", "new_key") + creds = load() + assert creds is not None + assert creds.orgs["org_a"].api_key == "new_key" + + def test_set_api_key_for_org_preserves_control_api(self, creds_file): + # Seed with a control_api block. + save( + Credentials( + selected_org_id="org_a", + orgs={ + "org_a": OrgCredentials( + api_key="old", + control_api=ControlApiTokens( + access_token="atk", + refresh_token="rtk", + ), + ) + }, + ) + ) + set_api_key_for_org("org_a", "new") + + creds = load() + assert creds is not None + assert creds.orgs["org_a"].api_key == "new" + assert creds.orgs["org_a"].control_api.access_token == "atk" + assert creds.orgs["org_a"].control_api.refresh_token == "rtk" diff --git a/tests/test_enrichment.py b/tests/test_enrichment.py index e6e3f84..2c88729 100644 --- a/tests/test_enrichment.py +++ b/tests/test_enrichment.py @@ -29,17 +29,18 @@ def test_explicit_api_key(self): assert result == "test-key-123" def test_env_var_fallback(self): - """Should use PARALLEL_API_KEY env var when no explicit key.""" + """Should use PARALLEL_API_KEY env var when no stored credentials.""" with mock.patch.dict(os.environ, {"PARALLEL_API_KEY": "env-key-456"}): - result = resolve_api_key() - assert result == "env-key-456" + with mock.patch("parallel_web_tools.core.credentials.get_selected_api_key", return_value=None): + result = resolve_api_key() + assert result == "env-key-456" def test_oauth_fallback(self): """Should use stored OAuth credentials when no env var.""" with mock.patch.dict(os.environ, {}, clear=True): os.environ.pop("PARALLEL_API_KEY", None) - with mock.patch("parallel_web_tools.core.auth._load_stored_token") as mock_load: + with mock.patch("parallel_web_tools.core.credentials.get_selected_api_key") as mock_load: mock_load.return_value = "oauth-key-789" result = resolve_api_key() assert result == "oauth-key-789" @@ -49,7 +50,7 @@ def test_no_key_raises_error(self): with mock.patch.dict(os.environ, {}, clear=True): os.environ.pop("PARALLEL_API_KEY", None) - with mock.patch("parallel_web_tools.core.auth._load_stored_token") as mock_load: + with mock.patch("parallel_web_tools.core.credentials.get_selected_api_key") as mock_load: mock_load.return_value = None with pytest.raises(ValueError) as exc_info: diff --git a/tests/test_service.py b/tests/test_service.py new file mode 100644 index 0000000..786d249 --- /dev/null +++ b/tests/test_service.py @@ -0,0 +1,337 @@ +"""Tests for the service API client (apps + keys + balance).""" + +import io +import json +import urllib.error +from contextlib import contextmanager +from email.message import Message +from unittest import mock + +import pytest + +from parallel_web_tools.core import service +from parallel_web_tools.core.service import ( + ServiceApiError, + _build_key_name, + add_balance, + create_api_key, + get_balance, + list_apps, + provision_cli_api_key, +) + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +def _http_error(status: int, body: dict) -> urllib.error.HTTPError: + return urllib.error.HTTPError( + url="https://example.com", + code=status, + msg="Error", + hdrs=Message(), + fp=io.BytesIO(json.dumps(body).encode()), + ) + + +def _patch_urlopen(responses, capture: dict | None = None): + """Patch service.urllib.request.urlopen to yield each response in order. + + ``responses`` may be a single value or a list; each entry is a dict + (JSON-encoded body), bytes (raw body), pre-built HTTPError, or a callable + ``req -> value`` to dispatch by request. When ``capture`` is provided it + is populated on each call with url/body/headers/method. + """ + if not isinstance(responses, list): + responses = [responses] + idx = [0] + + @contextmanager + def impl(req, timeout=None): + if capture is not None: + capture["url"] = req.full_url + capture["body"] = req.data.decode() if req.data else "" + capture["headers"] = dict(req.header_items()) + capture["method"] = req.get_method() + i = min(idx[0], len(responses) - 1) + idx[0] += 1 + r = responses[i] + if callable(r): + r = r(req) + if isinstance(r, urllib.error.HTTPError): + raise r + payload = r if isinstance(r, (bytes, bytearray)) else json.dumps(r).encode() + yield io.BytesIO(bytes(payload)) + + return mock.patch("parallel_web_tools.core.service.urllib.request.urlopen", side_effect=impl) + + +def _app_item(app_name: str, app_id: str = "app_x", org_id: str = "org_x") -> dict: + return {"app_name": app_name, "org_name": None, "app_id": app_id, "org_id": org_id} + + +def _api_key_response(raw_api_key: str | None = "sk_minted", name: str = "parallel-cli-2026-04-21-1432") -> dict: + """Build a full CreateKeyResponse payload (flat — no ``api_key`` wrapper).""" + return { + "api_key_id": "key_1", + "api_key_name": name, + "app_id": "app_cli", + "app_name": service.PARALLEL_CLI_APP_NAME, + "created_by_user_id": "user_1", + "created_by_user_email": "user@example.com", + "display_value": "sk_***1234", + "raw_api_key": raw_api_key, + "created_at": 1776800731, + } + + +def _apps(*names_and_ids: tuple[str, str]) -> dict: + """Shorthand for a GetAppsForOrgResponseModel payload.""" + return {"apps": [_app_item(name, app_id=app_id) for name, app_id in names_and_ids]} + + +def _balance_response(**overrides) -> dict: + """Build a BalanceResponse payload.""" + base = { + "org_id": "org_abc", + "credit_balance_cents": 1500, + "pending_debit_balance_cents": 0, + "will_invoice": False, + } + base.update(overrides) + return base + + +# --------------------------------------------------------------------------- +# list_apps +# --------------------------------------------------------------------------- + + +class TestListApps: + def test_parses_apps_list(self): + with _patch_urlopen(_apps(("parallel-cli Users", "app_1"))): + apps = list_apps("at_123") + assert len(apps) == 1 + assert apps[0].app_name == "parallel-cli Users" + assert apps[0].app_id == "app_1" + + def test_empty_apps_list(self): + with _patch_urlopen({"apps": []}): + assert list_apps("at_123") == [] + + def test_missing_apps_field_returns_empty(self): + # GetAppsForOrgResponseModel.apps is Optional; an omitted key is legal. + with _patch_urlopen({}): + assert list_apps("at_123") == [] + + def test_sends_bearer_auth(self): + captured: dict = {} + with _patch_urlopen({"apps": []}, capture=captured): + list_apps("at_xyz") + + assert captured["method"] == "GET" + assert "/service/v1/apps" in captured["url"] + assert any(v == "Bearer at_xyz" for v in captured["headers"].values()) + + def test_respects_service_api_url_env(self, monkeypatch): + monkeypatch.setenv("PARALLEL_SERVICE_API_URL", "http://localhost:8090") + captured: dict = {} + with _patch_urlopen({"apps": []}, capture=captured): + list_apps("at_xyz") + assert captured["url"].startswith("http://localhost:8090/") + + def test_raises_on_malformed_apps_shape(self): + # apps must be a list; a string is invalid. + with _patch_urlopen({"apps": "nope"}): + with pytest.raises(ServiceApiError, match="Unexpected /service/v1/apps response"): + list_apps("at_xyz") + + def test_raises_on_http_error(self): + with _patch_urlopen(_http_error(401, {"error": "unauthorized"})): + with pytest.raises(ServiceApiError, match="failed: 401"): + list_apps("at_xyz") + + +# --------------------------------------------------------------------------- +# create_api_key +# --------------------------------------------------------------------------- + + +class TestCreateApiKey: + def test_returns_typed_api_key_model(self): + with _patch_urlopen(_api_key_response()): + result = create_api_key("at_xyz", "app_1", "parallel-cli-2026-04-21-1432") + assert result.raw_api_key == "sk_minted" + assert result.api_key_name == "parallel-cli-2026-04-21-1432" + assert result.display_value == "sk_***1234" + + def test_request_body_has_only_api_key_name(self): + captured: dict = {} + with _patch_urlopen(_api_key_response(), capture=captured): + create_api_key("at_xyz", "app_42", "parallel-cli-2026-04-21-1432") + + assert captured["method"] == "POST" + assert captured["url"].endswith("/service/v1/apps/app_42/keys") + assert json.loads(captured["body"]) == {"api_key_name": "parallel-cli-2026-04-21-1432"} + + def test_raises_on_malformed_response(self): + # Missing required fields (e.g. api_key_id) — pydantic validation fails. + with _patch_urlopen({"display_value": "sk_***"}): + with pytest.raises(ServiceApiError, match="Unexpected create_api_key response"): + create_api_key("at_xyz", "app_1", "name_1") + + +# --------------------------------------------------------------------------- +# provision_cli_api_key +# --------------------------------------------------------------------------- + + +class TestProvisionCliApiKey: + def test_happy_path(self): + apps_payload = _apps(("Some Other App", "app_other"), (service.PARALLEL_CLI_APP_NAME, "app_cli")) + captured_paths: list[str] = [] + + def dispatch(req): + captured_paths.append(req.full_url) + if req.get_method() == "GET": + return apps_payload + return _api_key_response(raw_api_key="sk_provisioned") + + with _patch_urlopen([dispatch, dispatch]): + key, name = provision_cli_api_key("at_xyz") + + assert key == "sk_provisioned" + assert name.startswith("parallel-cli-") + # The created key must target the CLI app, not the other one. + assert any("/apps/app_cli/keys" in p for p in captured_paths) + + def test_raises_when_app_not_found(self): + with _patch_urlopen(_apps(("Some Other App", "app_other"))): + with pytest.raises(ServiceApiError, match="No app named"): + provision_cli_api_key("at_xyz") + + def test_raises_when_raw_api_key_missing(self): + apps_payload = _apps((service.PARALLEL_CLI_APP_NAME, "app_cli")) + with _patch_urlopen([apps_payload, _api_key_response(raw_api_key=None)]): + with pytest.raises(ServiceApiError, match="no raw_api_key"): + provision_cli_api_key("at_xyz") + + def test_client_id_is_used_in_created_key_name(self): + apps_payload = _apps((service.PARALLEL_CLI_APP_NAME, "app_cli")) + sent_body: dict = {} + + def dispatch(req): + if req.get_method() == "GET": + return apps_payload + sent_body.update(json.loads(req.data.decode())) + return _api_key_response(raw_api_key="sk_ok") + + with _patch_urlopen([dispatch, dispatch]): + _, name = provision_cli_api_key("at_xyz", client_id="cid_abc") + + assert name.startswith("cid_abc-") + assert sent_body["api_key_name"] == name + + +# --------------------------------------------------------------------------- +# _build_key_name +# --------------------------------------------------------------------------- + + +class TestBuildKeyName: + def test_falls_back_to_parallel_cli_prefix_without_client_id(self): + import re + + name = _build_key_name() + # parallel-cli-YYYY-MM-DD-HHMM (HHMM is 4 digits, no colon) + assert re.match(r"^parallel-cli-\d{4}-\d{2}-\d{2}-\d{4}$", name), name + + def test_uses_client_id_as_prefix_when_provided(self): + import re + + name = _build_key_name(client_id="cid_abc123") + # Same date suffix, but the client_id now carries the entropy. + assert re.match(r"^cid_abc123-\d{4}-\d{2}-\d{2}-\d{4}$", name), name + + +# --------------------------------------------------------------------------- +# get_balance +# --------------------------------------------------------------------------- + + +class TestGetBalance: + def test_parses_balance_response(self): + payload = _balance_response( + credit_balance_cents=1234, + pending_debit_balance_cents=56, + will_invoice=False, + ) + with _patch_urlopen(payload): + resp = get_balance("at_xyz") + assert resp.org_id == "org_abc" + assert resp.credit_balance_cents == 1234 + assert resp.pending_debit_balance_cents == 56 + assert resp.will_invoice is False + + def test_defaults_optional_fields_when_omitted(self): + # pending_debit_balance_cents and will_invoice are optional. + with _patch_urlopen({"org_id": "org_x", "credit_balance_cents": 0}): + resp = get_balance("at_xyz") + assert resp.pending_debit_balance_cents == 0 + assert resp.will_invoice is False + + def test_sends_bearer_auth_to_balance_endpoint(self): + captured: dict = {} + with _patch_urlopen(_balance_response(), capture=captured): + get_balance("at_xyz") + + assert captured["method"] == "GET" + assert captured["url"].endswith("/service/v1/balance") + assert any(v == "Bearer at_xyz" for v in captured["headers"].values()) + + def test_respects_service_api_url_env(self, monkeypatch): + monkeypatch.setenv("PARALLEL_SERVICE_API_URL", "http://localhost:8090") + captured: dict = {} + with _patch_urlopen(_balance_response(), capture=captured): + get_balance("at_xyz") + assert captured["url"].startswith("http://localhost:8090/") + + def test_raises_on_http_error(self): + with _patch_urlopen(_http_error(500, {"error": "internal"})): + with pytest.raises(ServiceApiError, match="failed: 500"): + get_balance("at_xyz") + + def test_raises_on_malformed_payload(self): + # Missing required field org_id. + with _patch_urlopen({"credit_balance_cents": 10}): + with pytest.raises(ServiceApiError, match="Unexpected /service/v1/balance response"): + get_balance("at_xyz") + + +# --------------------------------------------------------------------------- +# add_balance +# --------------------------------------------------------------------------- + + +class TestAddBalance: + def test_posts_expected_body_and_parses_response(self): + captured: dict = {} + with _patch_urlopen(_balance_response(credit_balance_cents=1600), capture=captured): + resp = add_balance("at_xyz", amount_cents=100, idempotency_key="key_123") + + assert captured["method"] == "POST" + assert captured["url"].endswith("/service/v1/balance/add") + assert json.loads(captured["body"]) == {"amount_cents": 100, "idempotency_key": "key_123"} + assert any(v == "Bearer at_xyz" for v in captured["headers"].values()) + assert resp.credit_balance_cents == 1600 + + def test_raises_on_http_error(self): + with _patch_urlopen(_http_error(402, {"error": "card_declined"})): + with pytest.raises(ServiceApiError, match="failed: 402"): + add_balance("at_xyz", amount_cents=100, idempotency_key="k") + + def test_raises_on_malformed_response(self): + with _patch_urlopen({"credit_balance_cents": 10}): + with pytest.raises(ServiceApiError, match="Unexpected /service/v1/balance/add response"): + add_balance("at_xyz", amount_cents=100, idempotency_key="k") diff --git a/tests/test_test_harness.py b/tests/test_test_harness.py new file mode 100644 index 0000000..a8541a0 --- /dev/null +++ b/tests/test_test_harness.py @@ -0,0 +1,38 @@ +"""Regression tests for the shared pytest safety harness.""" + +import asyncio +import socket +import subprocess +import urllib.request +import webbrowser + +import httpx +import pytest + + +class TestNoExternalIoHarness: + def test_blocks_low_level_socket_connections(self): + with pytest.raises(AssertionError, match="Network access is disabled"): + socket.create_connection(("example.com", 443), timeout=1) + + with pytest.raises(AssertionError, match="Network access is disabled"): + socket.getaddrinfo("example.com", 443) + + def test_blocks_high_level_http_clients(self): + with pytest.raises(AssertionError, match="Network access is disabled"): + urllib.request.urlopen("https://example.com") + + with pytest.raises(AssertionError, match="Network access is disabled"): + httpx.get("https://example.com") + + with pytest.raises(AssertionError, match="Network access is disabled"): + asyncio.run(asyncio.open_connection("example.com", 443)) + + def test_blocks_subprocesses(self): + with pytest.raises(AssertionError, match="Subprocess execution is disabled"): + subprocess.run(["true"], check=False) + + def test_browser_launches_are_stubbed(self): + assert webbrowser.open("https://example.com") is True + assert webbrowser.open_new("https://example.com") is True + assert webbrowser.open_new_tab("https://example.com") is True diff --git a/uv.lock b/uv.lock index 728f94b..6b61a31 100644 --- a/uv.lock +++ b/uv.lock @@ -2,10 +2,15 @@ version = 1 revision = 3 requires-python = ">=3.10" resolution-markers = [ - "python_full_version >= '3.14'", - "python_full_version == '3.13.*'", - "python_full_version == '3.12.*'", - "python_full_version == '3.11.*'", + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'win32'", + "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'emscripten'", + "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform == 'emscripten'", + "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version < '3.11'", ] @@ -50,6 +55,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/81/29/5ecc3a15d5a33e31b26c11426c45c501e439cb865d0bff96315d86443b78/appnope-0.1.4-py2.py3-none-any.whl", hash = "sha256:502575ee11cd7a28c0205f379b525beefebab9d161b7c964670864014ed7213c", size = 4321, upload-time = "2024-02-06T09:43:09.663Z" }, ] +[[package]] +name = "argcomplete" +version = "3.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/38/61/0b9ae6399dd4a58d8c1b1dc5a27d6f2808023d0b5dd3104bb99f45a33ff6/argcomplete-3.6.3.tar.gz", hash = "sha256:62e8ed4fd6a45864acc8235409461b72c9a28ee785a2011cc5eb78318786c89c", size = 73754, upload-time = "2025-10-20T03:33:34.741Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/74/f5/9373290775639cb67a2fce7f629a1c240dce9f12fe927bc32b2736e16dfc/argcomplete-3.6.3-py3-none-any.whl", hash = "sha256:f5007b3a600ccac5d25bbce33089211dfd49eab4a7718da3f10e3082525a92ce", size = 43846, upload-time = "2025-10-20T03:33:33.021Z" }, +] + [[package]] name = "asn1crypto" version = "1.5.1" @@ -68,32 +82,76 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d2/39/e7eaf1799466a4aef85b6a4fe7bd175ad2b1c6345066aa33f1f58d4b18d0/asttokens-3.0.1-py3-none-any.whl", hash = "sha256:15a3ebc0f43c2d0a50eeafea25e19046c68398e487b9f1f5b517f7c0f40f976a", size = 27047, upload-time = "2025-11-15T16:43:16.109Z" }, ] +[[package]] +name = "black" +version = "26.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "mypy-extensions" }, + { name = "packaging" }, + { name = "pathspec" }, + { name = "platformdirs" }, + { name = "pytokens" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e1/c5/61175d618685d42b005847464b8fb4743a67b1b8fdb75e50e5a96c31a27a/black-26.3.1.tar.gz", hash = "sha256:2c50f5063a9641c7eed7795014ba37b0f5fa227f3d408b968936e24bc0566b07", size = 666155, upload-time = "2026-03-12T03:36:03.593Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/a8/11170031095655d36ebc6664fe0897866f6023892396900eec0e8fdc4299/black-26.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:86a8b5035fce64f5dcd1b794cf8ec4d31fe458cf6ce3986a30deb434df82a1d2", size = 1866562, upload-time = "2026-03-12T03:39:58.639Z" }, + { url = "https://files.pythonhosted.org/packages/69/ce/9e7548d719c3248c6c2abfd555d11169457cbd584d98d179111338423790/black-26.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5602bdb96d52d2d0672f24f6ffe5218795736dd34807fd0fd55ccd6bf206168b", size = 1703623, upload-time = "2026-03-12T03:40:00.347Z" }, + { url = "https://files.pythonhosted.org/packages/7f/0a/8d17d1a9c06f88d3d030d0b1d4373c1551146e252afe4547ed601c0e697f/black-26.3.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c54a4a82e291a1fee5137371ab488866b7c86a3305af4026bdd4dc78642e1ac", size = 1768388, upload-time = "2026-03-12T03:40:01.765Z" }, + { url = "https://files.pythonhosted.org/packages/52/79/c1ee726e221c863cde5164f925bacf183dfdf0397d4e3f94889439b947b4/black-26.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:6e131579c243c98f35bce64a7e08e87fb2d610544754675d4a0e73a070a5aa3a", size = 1412969, upload-time = "2026-03-12T03:40:03.252Z" }, + { url = "https://files.pythonhosted.org/packages/73/a5/15c01d613f5756f68ed8f6d4ec0a1e24b82b18889fa71affd3d1f7fad058/black-26.3.1-cp310-cp310-win_arm64.whl", hash = "sha256:5ed0ca58586c8d9a487352a96b15272b7fa55d139fc8496b519e78023a8dab0a", size = 1220345, upload-time = "2026-03-12T03:40:04.892Z" }, + { url = "https://files.pythonhosted.org/packages/17/57/5f11c92861f9c92eb9dddf515530bc2d06db843e44bdcf1c83c1427824bc/black-26.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:28ef38aee69e4b12fda8dba75e21f9b4f979b490c8ac0baa7cb505369ac9e1ff", size = 1851987, upload-time = "2026-03-12T03:40:06.248Z" }, + { url = "https://files.pythonhosted.org/packages/54/aa/340a1463660bf6831f9e39646bf774086dbd8ca7fc3cded9d59bbdf4ad0a/black-26.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bf9bf162ed91a26f1adba8efda0b573bc6924ec1408a52cc6f82cb73ec2b142c", size = 1689499, upload-time = "2026-03-12T03:40:07.642Z" }, + { url = "https://files.pythonhosted.org/packages/f3/01/b726c93d717d72733da031d2de10b92c9fa4c8d0c67e8a8a372076579279/black-26.3.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:474c27574d6d7037c1bc875a81d9be0a9a4f9ee95e62800dab3cfaadbf75acd5", size = 1754369, upload-time = "2026-03-12T03:40:09.279Z" }, + { url = "https://files.pythonhosted.org/packages/e3/09/61e91881ca291f150cfc9eb7ba19473c2e59df28859a11a88248b5cbbc4d/black-26.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:5e9d0d86df21f2e1677cc4bd090cd0e446278bcbbe49bf3659c308c3e402843e", size = 1413613, upload-time = "2026-03-12T03:40:10.943Z" }, + { url = "https://files.pythonhosted.org/packages/16/73/544f23891b22e7efe4d8f812371ab85b57f6a01b2fc45e3ba2e52ba985b8/black-26.3.1-cp311-cp311-win_arm64.whl", hash = "sha256:9a5e9f45e5d5e1c5b5c29b3bd4265dcc90e8b92cf4534520896ed77f791f4da5", size = 1219719, upload-time = "2026-03-12T03:40:12.597Z" }, + { url = "https://files.pythonhosted.org/packages/dc/f8/da5eae4fc75e78e6dceb60624e1b9662ab00d6b452996046dfa9b8a6025b/black-26.3.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b5e6f89631eb88a7302d416594a32faeee9fb8fb848290da9d0a5f2903519fc1", size = 1895920, upload-time = "2026-03-12T03:40:13.921Z" }, + { url = "https://files.pythonhosted.org/packages/2c/9f/04e6f26534da2e1629b2b48255c264cabf5eedc5141d04516d9d68a24111/black-26.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:41cd2012d35b47d589cb8a16faf8a32ef7a336f56356babd9fcf70939ad1897f", size = 1718499, upload-time = "2026-03-12T03:40:15.239Z" }, + { url = "https://files.pythonhosted.org/packages/04/91/a5935b2a63e31b331060c4a9fdb5a6c725840858c599032a6f3aac94055f/black-26.3.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f76ff19ec5297dd8e66eb64deda23631e642c9393ab592826fd4bdc97a4bce7", size = 1794994, upload-time = "2026-03-12T03:40:17.124Z" }, + { url = "https://files.pythonhosted.org/packages/e7/0a/86e462cdd311a3c2a8ece708d22aba17d0b2a0d5348ca34b40cdcbea512e/black-26.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:ddb113db38838eb9f043623ba274cfaf7d51d5b0c22ecb30afe58b1bb8322983", size = 1420867, upload-time = "2026-03-12T03:40:18.83Z" }, + { url = "https://files.pythonhosted.org/packages/5b/e5/22515a19cb7eaee3440325a6b0d95d2c0e88dd180cb011b12ae488e031d1/black-26.3.1-cp312-cp312-win_arm64.whl", hash = "sha256:dfdd51fc3e64ea4f35873d1b3fb25326773d55d2329ff8449139ebaad7357efb", size = 1230124, upload-time = "2026-03-12T03:40:20.425Z" }, + { url = "https://files.pythonhosted.org/packages/f5/77/5728052a3c0450c53d9bb3945c4c46b91baa62b2cafab6801411b6271e45/black-26.3.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:855822d90f884905362f602880ed8b5df1b7e3ee7d0db2502d4388a954cc8c54", size = 1895034, upload-time = "2026-03-12T03:40:21.813Z" }, + { url = "https://files.pythonhosted.org/packages/52/73/7cae55fdfdfbe9d19e9a8d25d145018965fe2079fa908101c3733b0c55a0/black-26.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8a33d657f3276328ce00e4d37fe70361e1ec7614da5d7b6e78de5426cb56332f", size = 1718503, upload-time = "2026-03-12T03:40:23.666Z" }, + { url = "https://files.pythonhosted.org/packages/e1/87/af89ad449e8254fdbc74654e6467e3c9381b61472cc532ee350d28cfdafb/black-26.3.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f1cd08e99d2f9317292a311dfe578fd2a24b15dbce97792f9c4d752275c1fa56", size = 1793557, upload-time = "2026-03-12T03:40:25.497Z" }, + { url = "https://files.pythonhosted.org/packages/43/10/d6c06a791d8124b843bf325ab4ac7d2f5b98731dff84d6064eafd687ded1/black-26.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:c7e72339f841b5a237ff14f7d3880ddd0fc7f98a1199e8c4327f9a4f478c1839", size = 1422766, upload-time = "2026-03-12T03:40:27.14Z" }, + { url = "https://files.pythonhosted.org/packages/59/4f/40a582c015f2d841ac24fed6390bd68f0fc896069ff3a886317959c9daf8/black-26.3.1-cp313-cp313-win_arm64.whl", hash = "sha256:afc622538b430aa4c8c853f7f63bc582b3b8030fd8c80b70fb5fa5b834e575c2", size = 1232140, upload-time = "2026-03-12T03:40:28.882Z" }, + { url = "https://files.pythonhosted.org/packages/d5/da/e36e27c9cebc1311b7579210df6f1c86e50f2d7143ae4fcf8a5017dc8809/black-26.3.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2d6bfaf7fd0993b420bed691f20f9492d53ce9a2bcccea4b797d34e947318a78", size = 1889234, upload-time = "2026-03-12T03:40:30.964Z" }, + { url = "https://files.pythonhosted.org/packages/0e/7b/9871acf393f64a5fa33668c19350ca87177b181f44bb3d0c33b2d534f22c/black-26.3.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f89f2ab047c76a9c03f78d0d66ca519e389519902fa27e7a91117ef7611c0568", size = 1720522, upload-time = "2026-03-12T03:40:32.346Z" }, + { url = "https://files.pythonhosted.org/packages/03/87/e766c7f2e90c07fb7586cc787c9ae6462b1eedab390191f2b7fc7f6170a9/black-26.3.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b07fc0dab849d24a80a29cfab8d8a19187d1c4685d8a5e6385a5ce323c1f015f", size = 1787824, upload-time = "2026-03-12T03:40:33.636Z" }, + { url = "https://files.pythonhosted.org/packages/ac/94/2424338fb2d1875e9e83eed4c8e9c67f6905ec25afd826a911aea2b02535/black-26.3.1-cp314-cp314-win_amd64.whl", hash = "sha256:0126ae5b7c09957da2bdbd91a9ba1207453feada9e9fe51992848658c6c8e01c", size = 1445855, upload-time = "2026-03-12T03:40:35.442Z" }, + { url = "https://files.pythonhosted.org/packages/86/43/0c3338bd928afb8ee7471f1a4eec3bdbe2245ccb4a646092a222e8669840/black-26.3.1-cp314-cp314-win_arm64.whl", hash = "sha256:92c0ec1f2cc149551a2b7b47efc32c866406b6891b0ee4625e95967c8f4acfb1", size = 1258109, upload-time = "2026-03-12T03:40:36.832Z" }, + { url = "https://files.pythonhosted.org/packages/8e/0d/52d98722666d6fc6c3dd4c76df339501d6efd40e0ff95e6186a7b7f0befd/black-26.3.1-py3-none-any.whl", hash = "sha256:2bd5aa94fc267d38bb21a70d7410a89f1a1d318841855f698746f8e7f51acd1b", size = 207542, upload-time = "2026-03-12T03:36:01.668Z" }, +] + [[package]] name = "boto3" -version = "1.43.0" +version = "1.43.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "botocore" }, { name = "jmespath" }, { name = "s3transfer" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b7/65/47670987f2f9e181397872c7ee6415b7b95156d711b7eab6c55f66e575bc/boto3-1.43.0.tar.gz", hash = "sha256:80d44a943ef90aba7958ab31d30c155c198acc8a9581b5846b3878b2c8951086", size = 113143, upload-time = "2026-04-29T22:07:49.084Z" } +sdist = { url = "https://files.pythonhosted.org/packages/71/b0/90ba01763dd483bb040d0815dc0ba893421e3f5926672ceab9acbb73b23f/boto3-1.43.5.tar.gz", hash = "sha256:414be7868f25c3b6a0232301c8ab40347911b6b191926b61f00a63f89b97b2bc", size = 113150, upload-time = "2026-05-06T19:56:49.629Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b3/a0/3e6a0b1c1ea6bec76f71473727ef27abf3cd40e9709b3ebcbfbcfaae6f79/boto3-1.43.0-py3-none-any.whl", hash = "sha256:8ebe03754a4b73a5cb6ec2f14cca03ac33bd4760d0adea53da4724845130258b", size = 140497, upload-time = "2026-04-29T22:07:46.216Z" }, + { url = "https://files.pythonhosted.org/packages/10/bb/347307758c2003783df1d9a9b07596928d05a6ca0e17790cea3b18105244/boto3-1.43.5-py3-none-any.whl", hash = "sha256:aa8a296c8db55d812767b282cfe4c7977f0b0eeaa709abdaeb368b9c738e901f", size = 140502, upload-time = "2026-05-06T19:56:46.626Z" }, ] [[package]] name = "botocore" -version = "1.43.0" +version = "1.43.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jmespath" }, { name = "python-dateutil" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/28/79/2f4be1896db3db7ccf44504253a175d56b6bd6b669619edc5147d1aa21ea/botocore-1.43.0.tar.gz", hash = "sha256:e933b31a2d644253e1d029d7d39e99ba41b87e29300534f189744cc438cdf928", size = 15286817, upload-time = "2026-04-29T22:07:31.723Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6d/a2/1285a22bf157f9e97a8fd236daea95d9b14cc8425ae5f8a616badf948408/botocore-1.43.5.tar.gz", hash = "sha256:5c7207816ab5e48382adcb2a64db388fa4abe9ee1d23f72c82ae62c51a0bc84e", size = 15321290, upload-time = "2026-05-06T19:56:35.658Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bf/4b/afc1fef8a43bafb139f57f73bbd70df82807af5934321e8112ae50668827/botocore-1.43.0-py3-none-any.whl", hash = "sha256:cc5b15eaec3c6eac05d8012cb5ef17ebe891beb88a16ca13c374bfaece1241e6", size = 14970102, upload-time = "2026-04-29T22:07:27Z" }, + { url = "https://files.pythonhosted.org/packages/82/d2/99f1741b12e3cdba2e5370f6dafaab743a373c6f83592601ec75ff2cc47f/botocore-1.43.5-py3-none-any.whl", hash = "sha256:a1df6e0c6346735936f42e6b99f3b28f1e9397731c0bc2563c617df7965a0dc0", size = 15002116, upload-time = "2026-05-06T19:56:29.993Z" }, ] [[package]] @@ -451,62 +509,87 @@ toml = [ [[package]] name = "cryptography" -version = "47.0.0" +version = "48.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ef/b2/7ffa7fe8207a8c42147ffe70c3e360b228160c1d85dc3faff16aaa3244c0/cryptography-47.0.0.tar.gz", hash = "sha256:9f8e55fe4e63613a5e1cc5819030f27b97742d720203a087802ce4ce9ceb52bb", size = 830863, upload-time = "2026-04-24T19:54:57.056Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a4/98/40dfe932134bdcae4f6ab5927c87488754bf9eb79297d7e0070b78dd58e9/cryptography-47.0.0-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:160ad728f128972d362e714054f6ba0067cab7fb350c5202a9ae8ae4ce3ef1a0", size = 7912214, upload-time = "2026-04-24T19:53:03.864Z" }, - { url = "https://files.pythonhosted.org/packages/34/c6/2733531243fba725f58611b918056b277692f1033373dcc8bd01af1c05d4/cryptography-47.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b9a8943e359b7615db1a3ba587994618e094ff3d6fa5a390c73d079ce18b3973", size = 4644617, upload-time = "2026-04-24T19:53:06.909Z" }, - { url = "https://files.pythonhosted.org/packages/00/e3/b27be1a670a9b87f855d211cf0e1174a5d721216b7616bd52d8581d912ed/cryptography-47.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f5c15764f261394b22aef6b00252f5195f46f2ca300bec57149474e2538b31f8", size = 4668186, upload-time = "2026-04-24T19:53:09.053Z" }, - { url = "https://files.pythonhosted.org/packages/81/b9/8443cfe5d17d482d348cee7048acf502bb89a51b6382f06240fd290d4ca3/cryptography-47.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:9c59ab0e0fa3a180a5a9c59f3a5abe3ef90d474bc56d7fadfbe80359491b615b", size = 4651244, upload-time = "2026-04-24T19:53:11.217Z" }, - { url = "https://files.pythonhosted.org/packages/5d/5e/13ed0cdd0eb88ba159d6dd5ebfece8cb901dbcf1ae5ac4072e28b55d3153/cryptography-47.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:34b4358b925a5ea3e14384ca781a2c0ef7ac219b57bb9eacc4457078e2b19f92", size = 5252906, upload-time = "2026-04-24T19:53:13.532Z" }, - { url = "https://files.pythonhosted.org/packages/64/16/ed058e1df0f33d440217cd120d41d5dda9dd215a80b8187f68483185af82/cryptography-47.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:0024b87d47ae2399165a6bfb20d24888881eeab83ae2566d62467c5ff0030ce7", size = 4701842, upload-time = "2026-04-24T19:53:15.618Z" }, - { url = "https://files.pythonhosted.org/packages/02/e0/3d30986b30fdbd9e969abbdf8ba00ed0618615144341faeb57f395a084fe/cryptography-47.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:1e47422b5557bb82d3fff997e8d92cff4e28b9789576984f08c248d2b3535d93", size = 4289313, upload-time = "2026-04-24T19:53:17.755Z" }, - { url = "https://files.pythonhosted.org/packages/df/fd/32db38e3ad0cb331f0691cb4c7a8a6f176f679124dee746b3af6633db4d9/cryptography-47.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:6f29f36582e6151d9686235e586dd35bb67491f024767d10b842e520dc6a07ac", size = 4650964, upload-time = "2026-04-24T19:53:20.062Z" }, - { url = "https://files.pythonhosted.org/packages/86/53/5395d944dfd48cb1f67917f533c609c34347185ef15eb4308024c876f274/cryptography-47.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:a9b761f012a943b7de0e828843c5688d0de94a0578d44d6c85a1bae32f87791f", size = 5207817, upload-time = "2026-04-24T19:53:22.498Z" }, - { url = "https://files.pythonhosted.org/packages/34/4f/e5711b28e1901f7d480a2b1b688b645aa4c77c73f10731ed17e7f7db3f0d/cryptography-47.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:4e1de79e047e25d6e9f8cea71c86b4a53aced64134f0f003bbcbf3655fd172c8", size = 4701544, upload-time = "2026-04-24T19:53:24.356Z" }, - { url = "https://files.pythonhosted.org/packages/22/22/c8ddc25de3010fc8da447648f5a092c40e7a8fadf01dd6d255d9c0b9373d/cryptography-47.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ef6b3634087f18d2155b1e8ce264e5345a753da2c5fa9815e7d41315c90f8318", size = 4783536, upload-time = "2026-04-24T19:53:26.665Z" }, - { url = "https://files.pythonhosted.org/packages/66/b6/d4a68f4ea999c6d89e8498579cba1c5fcba4276284de7773b17e4fa69293/cryptography-47.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:11dbb9f50a0f1bb9757b3d8c27c1101780efb8f0bdecfb12439c22a74d64c001", size = 4926106, upload-time = "2026-04-24T19:53:28.686Z" }, - { url = "https://files.pythonhosted.org/packages/54/ed/5f524db1fade9c013aa618e1c99c6ed05e8ffc9ceee6cda22fed22dda3f4/cryptography-47.0.0-cp311-abi3-win32.whl", hash = "sha256:7fda2f02c9015db3f42bb8a22324a454516ed10a8c29ca6ece6cdbb5efe2a203", size = 3258581, upload-time = "2026-04-24T19:53:31.058Z" }, - { url = "https://files.pythonhosted.org/packages/b2/dc/1b901990b174786569029f67542b3edf72ac068b6c3c8683c17e6a2f5363/cryptography-47.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:f5c3296dab66202f1b18a91fa266be93d6aa0c2806ea3d67762c69f60adc71aa", size = 3775309, upload-time = "2026-04-24T19:53:33.054Z" }, - { url = "https://files.pythonhosted.org/packages/14/88/7aa18ad9c11bc87689affa5ce4368d884b517502d75739d475fc6f4a03c7/cryptography-47.0.0-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:be12cb6a204f77ed968bcefe68086eb061695b540a3dd05edac507a3111b25f0", size = 7904299, upload-time = "2026-04-24T19:53:35.003Z" }, - { url = "https://files.pythonhosted.org/packages/07/55/c18f75724544872f234678fdedc871391722cb34a2aee19faa9f63100bb2/cryptography-47.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2ebd84adf0728c039a3be2700289378e1c164afc6748df1a5ed456767bef9ba7", size = 4631180, upload-time = "2026-04-24T19:53:37.517Z" }, - { url = "https://files.pythonhosted.org/packages/ee/65/31a5cc0eaca99cec5bafffe155d407115d96136bb161e8b49e0ef73f09a7/cryptography-47.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7f68d6fbc7fbbcfb0939fea72c3b96a9f9a6edfc0e1b1d29778a2066030418b1", size = 4653529, upload-time = "2026-04-24T19:53:39.775Z" }, - { url = "https://files.pythonhosted.org/packages/e5/bc/641c0519a495f3bfd0421b48d7cd325c4336578523ccd76ea322b6c29c7a/cryptography-47.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:6651d32eff255423503aa276739da98c30f26c40cbeffcc6048e0d54ef704c0c", size = 4638570, upload-time = "2026-04-24T19:53:42.129Z" }, - { url = "https://files.pythonhosted.org/packages/2b/f2/300327b0a47f6dc94dd8b71b57052aefe178bb51745073d73d80604f11ab/cryptography-47.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:3fb8fa48075fad7193f2e5496135c6a76ac4b2aa5a38433df0a539296b377829", size = 5238019, upload-time = "2026-04-24T19:53:44.577Z" }, - { url = "https://files.pythonhosted.org/packages/e9/5a/5b5cf994391d4bf9d9c7efd4c66aabe4d95227256627f8fea6cff7dfadbd/cryptography-47.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:11438c7518132d95f354fa01a4aa2f806d172a061a7bed18cf18cbdacdb204d7", size = 4686832, upload-time = "2026-04-24T19:53:47.015Z" }, - { url = "https://files.pythonhosted.org/packages/dc/2c/ae950e28fd6475c852fc21a44db3e6b5bcc1261d1e370f2b6e42fa800fef/cryptography-47.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:8c1a736bbb3288005796c3f7ccb9453360d7fed483b13b9f468aea5171432923", size = 4269301, upload-time = "2026-04-24T19:53:48.97Z" }, - { url = "https://files.pythonhosted.org/packages/67/fb/6a39782e150ffe5cc1b0018cb6ddc48bf7ca62b498d7539ffc8a758e977d/cryptography-47.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:f1557695e5c2b86e204f6ce9470497848634100787935ab7adc5397c54abd7ab", size = 4638110, upload-time = "2026-04-24T19:53:51.011Z" }, - { url = "https://files.pythonhosted.org/packages/8e/d7/0b3c71090a76e5c203164a47688b697635ece006dcd2499ab3a4dbd3f0bd/cryptography-47.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:f9a034b642b960767fb343766ae5ba6ad653f2e890ddd82955aef288ffea8736", size = 5194988, upload-time = "2026-04-24T19:53:52.962Z" }, - { url = "https://files.pythonhosted.org/packages/63/33/63a961498a9df51721ab578c5a2622661411fc520e00bd83b0cc64eb20c4/cryptography-47.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:b1c76fca783aa7698eb21eb14f9c4aa09452248ee54a627d125025a43f83e7a7", size = 4686563, upload-time = "2026-04-24T19:53:55.274Z" }, - { url = "https://files.pythonhosted.org/packages/b7/bf/5ee5b145248f92250de86145d1c1d6edebbd57a7fe7caa4dedb5d4cf06a1/cryptography-47.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4f7722c97826770bab8ae92959a2e7b20a5e9e9bf4deae68fd86c3ca457bab52", size = 4770094, upload-time = "2026-04-24T19:53:57.753Z" }, - { url = "https://files.pythonhosted.org/packages/92/43/21d220b2da5d517773894dacdcdb5c682c28d3fffce65548cb06e87d5501/cryptography-47.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:09f6d7bf6724f8db8b32f11eccf23efc8e759924bc5603800335cf8859a3ddbd", size = 4913811, upload-time = "2026-04-24T19:54:00.236Z" }, - { url = "https://files.pythonhosted.org/packages/31/98/dc4ad376ac5f1a1a7d4a83f7b0c6f2bcad36b5d2d8f30aeb482d3a7d9582/cryptography-47.0.0-cp314-cp314t-win32.whl", hash = "sha256:6eebcaf0df1d21ce1f90605c9b432dd2c4f4ab665ac29a40d5e3fc68f51b5e63", size = 3237158, upload-time = "2026-04-24T19:54:02.606Z" }, - { url = "https://files.pythonhosted.org/packages/bc/da/97f62d18306b5133468bc3f8cc73a3111e8cdc8cf8d3e69474d6e5fd2d1b/cryptography-47.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:51c9313e90bd1690ec5a75ed047c27c0b8e6c570029712943d6116ef9a90620b", size = 3758706, upload-time = "2026-04-24T19:54:04.433Z" }, - { url = "https://files.pythonhosted.org/packages/e0/34/a4fae8ae7c3bc227460c9ae43f56abf1b911da0ec29e0ebac53bb0a4b6b7/cryptography-47.0.0-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:14432c8a9bcb37009784f9594a62fae211a2ae9543e96c92b2a8e4c3cd5cd0c4", size = 7904072, upload-time = "2026-04-24T19:54:06.411Z" }, - { url = "https://files.pythonhosted.org/packages/01/64/d7b1e54fdb69f22d24a64bb3e88dc718b31c7fb10ef0b9691a3cf7eeea6e/cryptography-47.0.0-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:07efe86201817e7d3c18781ca9770bc0db04e1e48c994be384e4602bc38f8f27", size = 4635767, upload-time = "2026-04-24T19:54:08.519Z" }, - { url = "https://files.pythonhosted.org/packages/8b/7b/cca826391fb2a94efdcdfe4631eb69306ee1cff0b22f664a412c90713877/cryptography-47.0.0-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2b45761c6ec22b7c726d6a829558777e32d0f1c8be7c3f3480f9c912d5ee8a10", size = 4654350, upload-time = "2026-04-24T19:54:10.795Z" }, - { url = "https://files.pythonhosted.org/packages/4c/65/4b57bcc823f42a991627c51c2f68c9fd6eb1393c1756aac876cba2accae2/cryptography-47.0.0-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:edd4da498015da5b9f26d38d3bfc2e90257bfa9cbed1f6767c282a0025ae649b", size = 4643394, upload-time = "2026-04-24T19:54:13.275Z" }, - { url = "https://files.pythonhosted.org/packages/f4/c4/2c5fbeea70adbbca2bbae865e1d605d6a4a7f8dbd9d33eaf69645087f06c/cryptography-47.0.0-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:9af828c0d5a65c70ec729cd7495a4bf1a67ecb66417b8f02ff125ab8a6326a74", size = 5225777, upload-time = "2026-04-24T19:54:15.18Z" }, - { url = "https://files.pythonhosted.org/packages/7e/b8/ac57107ef32749d2b244e36069bb688792a363aaaa3acc9e3cf84c130315/cryptography-47.0.0-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:256d07c78a04d6b276f5df935a9923275f53bd1522f214447fdf365494e2d515", size = 4688771, upload-time = "2026-04-24T19:54:17.835Z" }, - { url = "https://files.pythonhosted.org/packages/56/fc/9f1de22ff8be99d991f240a46863c52d475404c408886c5a38d2b5c3bb26/cryptography-47.0.0-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:5d0e362ff51041b0c0d219cc7d6924d7b8996f57ce5712bdcef71eb3c65a59cc", size = 4270753, upload-time = "2026-04-24T19:54:19.963Z" }, - { url = "https://files.pythonhosted.org/packages/00/68/d70c852797aa68e8e48d12e5a87170c43f67bb4a59403627259dd57d15de/cryptography-47.0.0-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:1581aef4219f7ca2849d0250edaa3866212fb74bf5667284f46aa92f9e65c1ca", size = 4642911, upload-time = "2026-04-24T19:54:21.818Z" }, - { url = "https://files.pythonhosted.org/packages/a5/51/661cbee74f594c5d97ff82d34f10d5551c085ca4668645f4606ebd22bd5d/cryptography-47.0.0-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:a49a3eb5341b9503fa3000a9a0db033161db90d47285291f53c2a9d2cd1b7f76", size = 5181411, upload-time = "2026-04-24T19:54:24.376Z" }, - { url = "https://files.pythonhosted.org/packages/94/87/f2b6c374a82cf076cfa1416992ac8e8ec94d79facc37aec87c1a5cb72352/cryptography-47.0.0-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:2207a498b03275d0051589e326b79d4cf59985c99031b05bb292ac52631c37fe", size = 4688262, upload-time = "2026-04-24T19:54:26.946Z" }, - { url = "https://files.pythonhosted.org/packages/14/e2/8b7462f4acf21ec509616f0245018bb197194ab0b65c2ea21a0bdd53c0eb/cryptography-47.0.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7a02675e2fabd0c0fc04c868b8781863cbf1967691543c22f5470500ff840b31", size = 4775506, upload-time = "2026-04-24T19:54:28.926Z" }, - { url = "https://files.pythonhosted.org/packages/70/75/158e494e4c08dc05e039da5bb48553826bd26c23930cf8d3cd5f21fa8921/cryptography-47.0.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:80887c5cbd1774683cb126f0ab4184567f080071d5acf62205acb354b4b753b7", size = 4912060, upload-time = "2026-04-24T19:54:30.869Z" }, - { url = "https://files.pythonhosted.org/packages/06/bd/0a9d3edbf5eadbac926d7b9b3cd0c4be584eeeae4a003d24d9eda4affbbd/cryptography-47.0.0-cp38-abi3-win32.whl", hash = "sha256:ed67ea4e0cfb5faa5bc7ecb6e2b8838f3807a03758eec239d6c21c8769355310", size = 3248487, upload-time = "2026-04-24T19:54:33.494Z" }, - { url = "https://files.pythonhosted.org/packages/60/80/5681af756d0da3a599b7bdb586fac5a1540f1bcefd2717a20e611ddade45/cryptography-47.0.0-cp38-abi3-win_amd64.whl", hash = "sha256:835d2d7f47cdc53b3224e90810fb1d36ca94ea29cc1801fb4c1bc43876735769", size = 3755737, upload-time = "2026-04-24T19:54:35.408Z" }, - { url = "https://files.pythonhosted.org/packages/1b/a0/928c9ce0d120a40a81aa99e3ba383e87337b9ac9ef9f6db02e4d7822424d/cryptography-47.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:7f1207974a904e005f762869996cf620e9bf79ecb4622f148550bb48e0eb35a7", size = 3909893, upload-time = "2026-04-24T19:54:38.334Z" }, - { url = "https://files.pythonhosted.org/packages/81/75/d691e284750df5d9569f2b1ce4a00a71e1d79566da83b2b3e5549c84917f/cryptography-47.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:1a405c08857258c11016777e11c02bacbe7ef596faf259305d282272a3a05cbe", size = 4587867, upload-time = "2026-04-24T19:54:40.619Z" }, - { url = "https://files.pythonhosted.org/packages/07/d6/1b90f1a4e453009730b4545286f0b39bb348d805c11181fc31544e4f9a65/cryptography-47.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:20fdbe3e38fb67c385d233c89371fa27f9909f6ebca1cecc20c13518dae65475", size = 4627192, upload-time = "2026-04-24T19:54:42.849Z" }, - { url = "https://files.pythonhosted.org/packages/dc/53/cb358a80e9e359529f496870dd08c102aa8a4b5b9f9064f00f0d6ed5b527/cryptography-47.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:f7db373287273d8af1414cf95dc4118b13ffdc62be521997b0f2b270771fef50", size = 4587486, upload-time = "2026-04-24T19:54:44.908Z" }, - { url = "https://files.pythonhosted.org/packages/8b/57/aaa3d53876467a226f9a7a82fd14dd48058ad2de1948493442dfa16e2ffd/cryptography-47.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:9fe6b7c64926c765f9dff301f9c1b867febcda5768868ca084e18589113732ab", size = 4626327, upload-time = "2026-04-24T19:54:47.813Z" }, - { url = "https://files.pythonhosted.org/packages/ab/9c/51f28c3550276bcf35660703ba0ab829a90b88be8cd98a71ef23c2413913/cryptography-47.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:cffbba3392df0fa8629bb7f43454ee2925059ee158e23c54620b9063912b86c8", size = 3698916, upload-time = "2026-04-24T19:54:49.782Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/9f/a9/db8f313fdcd85d767d4973515e1db101f9c71f95fced83233de224673757/cryptography-48.0.0.tar.gz", hash = "sha256:5c3932f4436d1cccb036cb0eaef46e6e2db91035166f1ad6505c3c9d5a635920", size = 832984, upload-time = "2026-05-04T22:59:38.133Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/3d/01f6dd9190170a5a241e0e98c2d04be3664a9e6f5b9b872cde63aff1c3dd/cryptography-48.0.0-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:0c558d2cdffd8f4bbb30fc7134c74d2ca9a476f830bb053074498fbc86f41ed6", size = 8001587, upload-time = "2026-05-04T22:57:36.803Z" }, + { url = "https://files.pythonhosted.org/packages/b2/6e/e90527eef33f309beb811cf7c982c3aeffcce8e3edb178baa4ca3ae4a6fa/cryptography-48.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f5333311663ea94f75dd408665686aaf426563556bb5283554a3539177e03b8c", size = 4690433, upload-time = "2026-05-04T22:57:40.373Z" }, + { url = "https://files.pythonhosted.org/packages/90/04/673510ed51ddff56575f306cf1617d80411ee76831ccd3097599140efdfe/cryptography-48.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7995ef305d7165c3f11ae07f2517e5a4f1d5c18da1376a0a9ed496336b69e5f3", size = 4710620, upload-time = "2026-05-04T22:57:42.935Z" }, + { url = "https://files.pythonhosted.org/packages/14/d5/e9c4ef932c8d800490c34d8bd589d64a31d5890e27ec9e9ad532be893294/cryptography-48.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:40ba1f85eaa6959837b1d51c9767e230e14612eea4ef110ee8854ada22da1bf5", size = 4696283, upload-time = "2026-05-04T22:57:45.294Z" }, + { url = "https://files.pythonhosted.org/packages/0c/29/174b9dfb60b12d59ecfc6cfa04bc88c21b42a54f01b8aae09bb6e51e4c7f/cryptography-48.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:369a6348999f94bbd53435c894377b20ab95f25a9065c283570e70150d8abc3c", size = 5296573, upload-time = "2026-05-04T22:57:47.933Z" }, + { url = "https://files.pythonhosted.org/packages/95/38/0d29a6fd7d0d1373f0c0c88a04ba20e359b257753ac497564cd660fc1d55/cryptography-48.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a0e692c683f4df67815a2d258b324e66f4738bd7a96a218c826dce4f4bd05d8f", size = 4743677, upload-time = "2026-05-04T22:57:50.067Z" }, + { url = "https://files.pythonhosted.org/packages/30/be/eef653013d5c63b6a490529e0316f9ac14a37602965d4903efed1399f32b/cryptography-48.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:18349bbc56f4743c8b12dc32e2bccb2cf83ee8b69a3bba74ef8ae857e26b3d25", size = 4330808, upload-time = "2026-05-04T22:57:52.301Z" }, + { url = "https://files.pythonhosted.org/packages/84/9e/500463e87abb7a0a0f9f256ec21123ecde0a7b5541a15e840ea54551fd81/cryptography-48.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:7e8eac43dfca5c4cccc6dad9a80504436fca53bb9bc3100a2386d730fbe6b602", size = 4695941, upload-time = "2026-05-04T22:57:54.603Z" }, + { url = "https://files.pythonhosted.org/packages/e3/dc/7303087450c2ec9e7fbb750e17c2abfbc658f23cbd0e54009509b7cc4091/cryptography-48.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9ccdac7d40688ecb5a3b4a604b8a88c8002e3442d6c60aead1db2a89a041560c", size = 5252579, upload-time = "2026-05-04T22:57:57.207Z" }, + { url = "https://files.pythonhosted.org/packages/d0/c0/7101d3b7215edcdc90c45da544961fd8ed2d6448f77577460fa75a8443f7/cryptography-48.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:bd72e68b06bb1e96913f97dd4901119bc17f39d4586a5adf2d3e47bc2b9d58b5", size = 4743326, upload-time = "2026-05-04T22:57:59.535Z" }, + { url = "https://files.pythonhosted.org/packages/ac/d8/5b833bad13016f562ab9d063d68199a4bd121d18458e439515601d3357ec/cryptography-48.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:59baa2cb386c4f0b9905bd6eb4c2a79a69a128408fd31d32ca4d7102d4156321", size = 4826672, upload-time = "2026-05-04T22:58:01.996Z" }, + { url = "https://files.pythonhosted.org/packages/98/e1/7074eb8bf3c135558c73fc2bcf0f5633f912e6fb87e868a55c454080ef09/cryptography-48.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9249e3cd978541d665967ac2cb2787fd6a62bddf1e75b3e347a594d7dacf4f74", size = 4972574, upload-time = "2026-05-04T22:58:03.968Z" }, + { url = "https://files.pythonhosted.org/packages/04/70/e5a1b41d325f797f39427aa44ef8baf0be500065ab6d8e10369d850d4a4f/cryptography-48.0.0-cp311-abi3-win32.whl", hash = "sha256:9c459db21422be75e2809370b829a87eb37f74cd785fc4aa9ea1e5f43b47cda4", size = 3294868, upload-time = "2026-05-04T22:58:06.467Z" }, + { url = "https://files.pythonhosted.org/packages/f4/ac/8ac51b4a5fc5932eb7ee5c517ba7dc8cd834f0048962b6b352f00f41ebf9/cryptography-48.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:5b012212e08b8dd5edc78ef54da83dd9892fd9105323b3993eff6bea65dc21d7", size = 3817107, upload-time = "2026-05-04T22:58:08.845Z" }, + { url = "https://files.pythonhosted.org/packages/6b/84/70e3feea9feea87fd7cbe77efb2712ae1e3e6edf10749dc6e95f4e60e455/cryptography-48.0.0-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:3cb07a3ed6431663cd321ea8a000a1314c74211f823e4177fefa2255e057d1ec", size = 7986556, upload-time = "2026-05-04T22:58:11.172Z" }, + { url = "https://files.pythonhosted.org/packages/89/6e/18e07a618bb5442ba10cf4df16e99c071365528aa570dfcb8c02e25a303b/cryptography-48.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8c7378637d7d88016fa6791c159f698b3d3eed28ebf844ac36b9dc04a14dae18", size = 4684776, upload-time = "2026-05-04T22:58:13.712Z" }, + { url = "https://files.pythonhosted.org/packages/be/6a/4ea3b4c6c6759794d5ee2103c304a5076dc4b19ae1f9fe47dba439e159e9/cryptography-48.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc90c0b39b2e3c65ef52c804b72e3c58f8a04ab2a1871272798e5f9572c17d20", size = 4698121, upload-time = "2026-05-04T22:58:16.448Z" }, + { url = "https://files.pythonhosted.org/packages/2f/59/6ff6ad6cae03bb887da2a5860b2c9805f8dac969ef01ce563336c49bd1d1/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:76341972e1eff8b4bea859f09c0d3e64b96ce931b084f9b9b7db8ef364c30eff", size = 4690042, upload-time = "2026-05-04T22:58:18.544Z" }, + { url = "https://files.pythonhosted.org/packages/ca/b4/fc334ed8cfd705aca282fe4d8f5ae64a8e0f74932e9feecb344610cf6e4d/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:55b7718303bf06a5753dcdccf2f3945cf18ad7bffde41b61226e4db31ab89a9c", size = 5282526, upload-time = "2026-05-04T22:58:20.75Z" }, + { url = "https://files.pythonhosted.org/packages/11/08/9f8c5386cc4cd90d8255c7cdd0f5baf459a08502a09de30dc51f553d38dc/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:a64697c641c7b1b2178e573cbc31c7c6684cd56883a478d75143dbb7118036db", size = 4733116, upload-time = "2026-05-04T22:58:23.627Z" }, + { url = "https://files.pythonhosted.org/packages/b8/77/99307d7574045699f8805aa500fa0fb83422d115b5400a064ddd306d7750/cryptography-48.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:561215ea3879cb1cbbf272867e2efda62476f240fb58c64de6b393ae19246741", size = 4316030, upload-time = "2026-05-04T22:58:25.581Z" }, + { url = "https://files.pythonhosted.org/packages/fd/36/a608b98337af3cb2aff4818e406649d30572b7031918b04c87d979495348/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:ad64688338ed4bc1a6618076ba75fd7194a5f1797ac60b47afe926285adb3166", size = 4689640, upload-time = "2026-05-04T22:58:27.747Z" }, + { url = "https://files.pythonhosted.org/packages/dd/a6/825010a291b4438aecc1f568bc428189fc1175515223632477c07dc0a6df/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:906cbf0670286c6e0044156bc7d4af9cbb0ef6db9f73e52c3ec56ba6bdde5336", size = 5237657, upload-time = "2026-05-04T22:58:29.848Z" }, + { url = "https://files.pythonhosted.org/packages/b9/09/4e76a09b4caa29aad535ddc806f5d4c5d01885bd978bd984fbc6ca032cae/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:ea8990436d914540a40ab24b6a77c0969695ed52f4a4874c5137ccf7045a7057", size = 4732362, upload-time = "2026-05-04T22:58:32.009Z" }, + { url = "https://files.pythonhosted.org/packages/18/78/444fa04a77d0cb95f417dda20d450e13c56ba8e5220fc892a1658f44f882/cryptography-48.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c18684a7f0cc9a3cb60328f496b8e3372def7c5d2df39ac267878b05565aaaae", size = 4819580, upload-time = "2026-05-04T22:58:34.254Z" }, + { url = "https://files.pythonhosted.org/packages/38/85/ea67067c70a1fd4be2c63d35eeed82658023021affccc7b17705f8527dd2/cryptography-48.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9be5aafa5736574f8f15f262adc81b2a9869e2cfe9014d52a44633905b40d52c", size = 4963283, upload-time = "2026-05-04T22:58:36.376Z" }, + { url = "https://files.pythonhosted.org/packages/75/54/cc6d0f3deac3e81c7f847e8a189a12b6cdd65059b43dad25d4316abd849a/cryptography-48.0.0-cp314-cp314t-win32.whl", hash = "sha256:c17dfe85494deaeddc5ce251aebd1d60bbe6afc8b62071bb0b469431a000124f", size = 3270954, upload-time = "2026-05-04T22:58:38.791Z" }, + { url = "https://files.pythonhosted.org/packages/49/67/cc947e288c0758a4e5473d1dcb743037ab7785541265a969240b8885441a/cryptography-48.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27241b1dc9962e056062a8eef1991d02c3a24569c95975bd2322a8a52c6e5e12", size = 3797313, upload-time = "2026-05-04T22:58:40.746Z" }, + { url = "https://files.pythonhosted.org/packages/f2/63/61d4a4e1c6b6bab6ce1e213cd36a24c415d90e76d78c5eb8577c5541d2e8/cryptography-48.0.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:58d00498e8933e4a194f3076aee1b4a97dfec1a6da444535755822fe5d8b0b86", size = 7983482, upload-time = "2026-05-04T22:58:43.769Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ac/f5b5995b87770c693e2596559ffafe195b4033a57f14a82268a2842953f3/cryptography-48.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:614d0949f4790582d2cc25553abd09dd723025f0c0e7c67376a1d77196743d6e", size = 4683266, upload-time = "2026-05-04T22:58:46.064Z" }, + { url = "https://files.pythonhosted.org/packages/ec/c6/8b14f67e18338fbc4adb76f66c001f5c3610b3e2d1837f268f47a347dbbb/cryptography-48.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7ce4bfae76319a532a2dc68f82cc32f5676ee792a983187dac07183690e5c66f", size = 4696228, upload-time = "2026-05-04T22:58:48.22Z" }, + { url = "https://files.pythonhosted.org/packages/ea/73/f808fbae9514bd91b47875b003f13e284c8c6bdfd904b7944e803937eec1/cryptography-48.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:2eb992bbd4661238c5a397594c83f5b4dc2bc5b848c365c8f991b6780efcc5c7", size = 4689097, upload-time = "2026-05-04T22:58:50.9Z" }, + { url = "https://files.pythonhosted.org/packages/93/01/d86632d7d28db8ae83221995752eeb6639ffb374c2d22955648cf8d52797/cryptography-48.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:22a5cb272895dce158b2cacdfdc3debd299019659f42947dbdac6f32d68fe832", size = 5283582, upload-time = "2026-05-04T22:58:53.017Z" }, + { url = "https://files.pythonhosted.org/packages/02/e1/50edc7a50334807cc4791fc4a0ce7468b4a1416d9138eab358bfc9a3d70b/cryptography-48.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2b4d59804e8408e2fea7d1fbaf218e5ec984325221db76e6a241a9abd6cdd95c", size = 4730479, upload-time = "2026-05-04T22:58:55.611Z" }, + { url = "https://files.pythonhosted.org/packages/6f/af/99a582b1b1641ff5911ac559beb45097cf79efd4ead4657f578ef1af2d47/cryptography-48.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:984a20b0f62a26f48a3396c72e4bc34c66e356d356bf370053066b3b6d54634a", size = 4326481, upload-time = "2026-05-04T22:58:57.607Z" }, + { url = "https://files.pythonhosted.org/packages/90/ee/89aa26a06ef0a7d7611788ffd571a7c50e368cc6a4d5eef8b4884e866edb/cryptography-48.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:5a5ed8fde7a1d09376ca0b40e68cd59c69fe23b1f9768bd5824f54681626032a", size = 4688713, upload-time = "2026-05-04T22:59:00.077Z" }, + { url = "https://files.pythonhosted.org/packages/70/ba/bcb1b0bb7a33d4c7c0c4d4c7874b4a62ae4f56113a5f4baefa362dfb1f0f/cryptography-48.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:8cd666227ef7af430aa5914a9910e0ddd703e75f039cef0825cd0da71b6b711a", size = 5238165, upload-time = "2026-05-04T22:59:02.317Z" }, + { url = "https://files.pythonhosted.org/packages/c9/70/ca4003b1ce5ca3dc3186ada51908c8a9b9ff7d5cab83cc0d43ee14ec144f/cryptography-48.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9071196d81abc88b3516ac8cdfad32e2b66dd4a5393a8e68a961e9161ddc6239", size = 4729947, upload-time = "2026-05-04T22:59:05.255Z" }, + { url = "https://files.pythonhosted.org/packages/44/a0/4ec7cf774207905aef1a8d11c3750d5a1db805eb380ee4e16df317870128/cryptography-48.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1e2d54c8be6152856a36f0882ab231e70f8ec7f14e93cf87db8a2ed056bf160c", size = 4822059, upload-time = "2026-05-04T22:59:07.802Z" }, + { url = "https://files.pythonhosted.org/packages/1e/75/a2e55f99c16fcac7b5d6c1eb19ad8e00799854d6be5ca845f9259eae1681/cryptography-48.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a5da777e32ffed6f85a7b2b3f7c5cbc88c146bfcd0a1d7baf5fcc6c52ee35dd4", size = 4960575, upload-time = "2026-05-04T22:59:09.851Z" }, + { url = "https://files.pythonhosted.org/packages/b8/23/6e6f32143ab5d8b36ca848a502c4bcd477ae75b9e1677e3530d669062578/cryptography-48.0.0-cp39-abi3-win32.whl", hash = "sha256:77a2ccbbe917f6710e05ba9adaa25fb5075620bf3ea6fb751997875aff4ae4bd", size = 3279117, upload-time = "2026-05-04T22:59:12.019Z" }, + { url = "https://files.pythonhosted.org/packages/9d/9a/0fea98a70cf1749d41d738836f6349d97945f7c89433a259a6c2642eefeb/cryptography-48.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:16cd65b9330583e4619939b3a3843eec1e6e789744bb01e7c7e2e62e33c239c8", size = 3792100, upload-time = "2026-05-04T22:59:14.884Z" }, + { url = "https://files.pythonhosted.org/packages/be/d2/024b5e06be9d44cb021fb0e1a03d34d63989cf56a0fe62f3dfbab695b9b4/cryptography-48.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:84cf79f0dc8b36ac5da873481716e87aef31fcfa0444f9e1d8b4b2cece142855", size = 3950391, upload-time = "2026-05-04T22:59:17.415Z" }, + { url = "https://files.pythonhosted.org/packages/bc/17/3861e17c56fa0fd37491a14a8673fdb77c57fc5693cafe745ea8b06dba75/cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:fdfef35d751d510fcef5252703621574364fec16418c4a1e5e1055248401054b", size = 4637126, upload-time = "2026-05-04T22:59:20.197Z" }, + { url = "https://files.pythonhosted.org/packages/f0/0a/7e226dbff530f21480727eb764973a7bff2b912f8e15cd4f129e71b56d1d/cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:0890f502ddf7d9c6426129c3f49f5c0a39278ed7cd6322c8755ffca6ee675a13", size = 4667270, upload-time = "2026-05-04T22:59:22.647Z" }, + { url = "https://files.pythonhosted.org/packages/3b/f2/5a72274ca9f1b2a8b44a662ee0bf1b435909deb473d6f97bcd035bcdbc71/cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:ecde28a596bead48b0cfd2a1b4416c3d43074c2d785e3a398d7ec1fc4d0f7fbb", size = 4636797, upload-time = "2026-05-04T22:59:24.912Z" }, + { url = "https://files.pythonhosted.org/packages/b4/e1/48cedb2fe63626e91ded1edad159e2a4fb8b6906c4425eb7749673077ce7/cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:4defde8685ae324a9eb9d818717e93b4638ef67070ac9bc15b8ca85f63048355", size = 4666800, upload-time = "2026-05-04T22:59:27.474Z" }, + { url = "https://files.pythonhosted.org/packages/a2/ca/7e8365deec19afb2b2c7be7c1c0aa8f99633b54e90c570999acda93260fc/cryptography-48.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:db63bf618e5dea46c07de12e900fe1cdd2541e6dc9dbae772a70b7d4d4765f6a", size = 3739536, upload-time = "2026-05-04T22:59:29.61Z" }, +] + +[[package]] +name = "datamodel-code-generator" +version = "0.57.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "argcomplete" }, + { name = "black" }, + { name = "genson" }, + { name = "inflect" }, + { name = "isort" }, + { name = "jinja2" }, + { name = "pydantic" }, + { name = "pyyaml" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5d/44/87d5980f813a1e323c5d726b3ac5fec8c915ce8a77fcdceaf9c00457dbae/datamodel_code_generator-0.57.0.tar.gz", hash = "sha256:0eda778ea06eaa476e542a5f1fe1d14cc3bbf686edb33a0ad6151c7d19089906", size = 932941, upload-time = "2026-05-07T16:21:55.819Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/c1/4fb9a44bb4a305b860c5a5b1866dcccfac3b76f5f170a9e68fc7733e16d2/datamodel_code_generator-0.57.0-py3-none-any.whl", hash = "sha256:d26bf5defe5154493d0aa5a822b7725332b9e9dd2abccc2f8856052286aa83b5", size = 259343, upload-time = "2026-05-07T16:21:53.823Z" }, +] + +[package.optional-dependencies] +ruff = [ + { name = "ruff" }, ] [[package]] @@ -637,6 +720,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/81/47/dd9a212ef6e343a6857485ffe25bba537304f1913bdbed446a23f7f592e1/filelock-3.29.0-py3-none-any.whl", hash = "sha256:96f5f6344709aa1572bbf631c640e4ebeeb519e08da902c39a001882f30ac258", size = 39812, upload-time = "2026-04-19T15:39:08.752Z" }, ] +[[package]] +name = "genson" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c5/cf/2303c8ad276dcf5ee2ad6cf69c4338fd86ef0f471a5207b069adf7a393cf/genson-1.3.0.tar.gz", hash = "sha256:e02db9ac2e3fd29e65b5286f7135762e2cd8a986537c075b06fc5f1517308e37", size = 34919, upload-time = "2024-05-15T22:08:49.123Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/5c/e226de133afd8bb267ec27eead9ae3d784b95b39a287ed404caab39a5f50/genson-1.3.0-py3-none-any.whl", hash = "sha256:468feccd00274cc7e4c09e84b08704270ba8d95232aa280f65b986139cec67f7", size = 21470, upload-time = "2024-05-15T22:08:47.056Z" }, +] + [[package]] name = "gitdb" version = "4.0.12" @@ -651,14 +743,14 @@ wheels = [ [[package]] name = "gitpython" -version = "3.1.49" +version = "3.1.50" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "gitdb" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e1/63/210aaa302d6a0a78daa67c5c15bbac2cad361722841278b0209b6da20855/gitpython-3.1.49.tar.gz", hash = "sha256:42f9399c9eb33fc581014bedd76049dfbaf6375aa2a5754575966387280315e1", size = 219367, upload-time = "2026-04-29T00:31:20.478Z" } +sdist = { url = "https://files.pythonhosted.org/packages/33/f6/354ae6491228b5eb40e10d89c4d13c651fe1cf7556e35ebdded50cff57ce/gitpython-3.1.50.tar.gz", hash = "sha256:80da2d12504d52e1f998772dc5baf6e553f8d2fcfe1fcc226c9d9a2ee3372dcc", size = 219798, upload-time = "2026-05-06T04:01:26.571Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fd/6f/b842bfa6f21d6f87c57f9abf7194225e55279d96d869775e19e9f7236fc5/gitpython-3.1.49-py3-none-any.whl", hash = "sha256:024b0422d7f84d15cd794844e029ffebd4c5d42a7eb9b936b458697ef550a02c", size = 212190, upload-time = "2026-04-29T00:31:18.412Z" }, + { url = "https://files.pythonhosted.org/packages/20/7a/1c6e3562dfd8950adbb11ffbc65d21e7c89d01a6e4f137fa981056de25c5/gitpython-3.1.50-py3-none-any.whl", hash = "sha256:d352abe2908d07355014abdd21ddf798c2a961469239afec4962e9da884858f9", size = 212507, upload-time = "2026-05-06T04:01:23.799Z" }, ] [[package]] @@ -685,15 +777,15 @@ grpc = [ [[package]] name = "google-auth" -version = "2.49.2" +version = "2.52.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cryptography" }, { name = "pyasn1-modules" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c6/fc/e925290a1ad95c975c459e2df070fac2b90954e13a0370ac505dff78cb99/google_auth-2.49.2.tar.gz", hash = "sha256:c1ae38500e73065dcae57355adb6278cf8b5c8e391994ae9cbadbcb9631ab409", size = 333958, upload-time = "2026-04-10T00:41:21.888Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d4/f8/80d2493cbedece1c623dc3e3cb1883300871af0dcdae254409522985ac23/google_auth-2.52.0.tar.gz", hash = "sha256:01f30e1a9e3638698d89464f5e603ce29d18e1c0e63ec31ac570aba4e164aaf5", size = 335027, upload-time = "2026-05-07T19:45:24.033Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/73/76/d241a5c927433420507215df6cac1b1fa4ac0ba7a794df42a84326c68da8/google_auth-2.49.2-py3-none-any.whl", hash = "sha256:c2720924dfc82dedb962c9f52cabb2ab16714fd0a6a707e40561d217574ed6d5", size = 240638, upload-time = "2026-04-10T00:41:14.501Z" }, + { url = "https://files.pythonhosted.org/packages/ee/fc/2cdc74252746f547f81ff3f02d4d4234a3f411b5de5b61af97e633a060b9/google_auth-2.52.0-py3-none-any.whl", hash = "sha256:aee92803ba0ff93a70a3b8a35c7b4797837751cd6380b63ff38372b98f3ed627", size = 245614, upload-time = "2026-05-07T19:45:21.914Z" }, ] [[package]] @@ -716,15 +808,15 @@ wheels = [ [[package]] name = "google-cloud-core" -version = "2.5.1" +version = "2.6.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "google-api-core" }, { name = "google-auth" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/dc/24/6ca08b0a03c7b0c620427503ab00353a4ae806b848b93bcea18b6b76fde6/google_cloud_core-2.5.1.tar.gz", hash = "sha256:3dc94bdec9d05a31d9f355045ed0f369fbc0d8c665076c734f065d729800f811", size = 36078, upload-time = "2026-03-30T22:50:08.057Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a8/dd/1eef226e470369b26824a505c34482c0b493bc35fe8e0c6b003b5feca21a/google_cloud_core-2.6.0.tar.gz", hash = "sha256:e76149739f90fac1fc6757c09f47eaccb3145b54adbd7759b0f7c4b235f46c83", size = 36001, upload-time = "2026-05-07T08:04:04.124Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/73/d9/5bb050cb32826466aa9b25f79e2ca2879fe66cb76782d4ed798dd7506151/google_cloud_core-2.5.1-py3-none-any.whl", hash = "sha256:ea62cdf502c20e3e14be8a32c05ed02113d7bef454e40ff3fab6fe1ec9f1f4e7", size = 29452, upload-time = "2026-03-30T22:48:31.567Z" }, + { url = "https://files.pythonhosted.org/packages/84/4a/98da8930ab109c73d9a5d13782a9ebb81ea8c111f6d534a567b71d23e52b/google_cloud_core-2.6.0-py3-none-any.whl", hash = "sha256:6d63ac8e5eca6d9e4319d0a1e2265fadcd7f1049904378caecfa01cf52dd869e", size = 29390, upload-time = "2026-05-07T08:02:34.672Z" }, ] [[package]] @@ -764,26 +856,26 @@ wheels = [ [[package]] name = "google-resumable-media" -version = "2.8.2" +version = "2.9.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "google-crc32c" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3f/d1/b1ea14b93b6b78f57fc580125de44e9f593ab88dd2460f1a8a8d18f74754/google_resumable_media-2.8.2.tar.gz", hash = "sha256:f3354a182ebd193ae3f42e3ef95e6c9b10f128320de23ac7637236713b1acd70", size = 2164510, upload-time = "2026-03-30T23:34:25.369Z" } +sdist = { url = "https://files.pythonhosted.org/packages/00/4b/0b235beccc310d0a48adbc7246b719d173cca6c88c572dfa4b090e39143c/google_resumable_media-2.9.0.tar.gz", hash = "sha256:f7cfb224846a9dd444d125115dfbe8ef02a2b893e78f087762fe716a255a734b", size = 2164534, upload-time = "2026-05-07T08:04:44.236Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5e/f8/50bfaf4658431ff9de45c5c3935af7ab01157a4903c603cd0eee6e78e087/google_resumable_media-2.8.2-py3-none-any.whl", hash = "sha256:82b6d8ccd11765268cdd2a2123f417ec806b8eef3000a9a38dfe3033da5fb220", size = 81511, upload-time = "2026-03-30T23:34:09.671Z" }, + { url = "https://files.pythonhosted.org/packages/07/73/3518e63deb1667c5409a4579e28daf5e84479a87a72c547e0487f7883dcd/google_resumable_media-2.9.0-py3-none-any.whl", hash = "sha256:c8901e88e389af8bed64d9696c74d8bad961865eb2236e13e0bfca9bb0a65ca3", size = 81507, upload-time = "2026-05-07T08:03:23.809Z" }, ] [[package]] name = "googleapis-common-protos" -version = "1.74.0" +version = "1.75.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/20/18/a746c8344152d368a5aac738d4c857012f2c5d1fd2eac7e17b647a7861bd/googleapis_common_protos-1.74.0.tar.gz", hash = "sha256:57971e4eeeba6aad1163c1f0fc88543f965bb49129b8bb55b2b7b26ecab084f1", size = 151254, upload-time = "2026-04-02T21:23:26.679Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/c8/f439cffde755cffa462bfbb156278fa6f9d09119719af9814b858fd4f81f/googleapis_common_protos-1.75.0.tar.gz", hash = "sha256:53a062ff3c32552fbd62c11fe23768b78e4ddf0494d5e5fd97d3f4689c75fbbd", size = 151035, upload-time = "2026-05-07T08:04:49.423Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b6/b0/be5d3329badb9230b765de6eea66b73abd5944bdeb5afb3562ddcd80ae84/googleapis_common_protos-1.74.0-py3-none-any.whl", hash = "sha256:702216f78610bb510e3f12ac3cafd281b7ac45cc5d86e90ad87e4d301a3426b5", size = 300743, upload-time = "2026-04-02T21:22:49.108Z" }, + { url = "https://files.pythonhosted.org/packages/e7/c8/e2645aa8ed02fd4c7a2f59d68783b65b1f3cbdfe39a6308e156509d1fee8/googleapis_common_protos-1.75.0-py3-none-any.whl", hash = "sha256:961ed60399c457ceb0ee8f285a84c870aabc9c6a832b9d37bb281b5bebde43ed", size = 300631, upload-time = "2026-05-07T08:03:30.345Z" }, ] [[package]] @@ -970,6 +1062,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5d/13/ad7d7ca3808a898b4612b6fe93cde56b53f3034dcde235acb1f0e1df24c6/idna-3.13-py3-none-any.whl", hash = "sha256:892ea0cde124a99ce773decba204c5552b69c3c67ffd5f232eb7696135bc8bb3", size = 68629, upload-time = "2026-04-22T16:42:40.909Z" }, ] +[[package]] +name = "inflect" +version = "7.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "more-itertools" }, + { name = "typeguard" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/78/c6/943357d44a21fd995723d07ccaddd78023eace03c1846049a2645d4324a3/inflect-7.5.0.tar.gz", hash = "sha256:faf19801c3742ed5a05a8ce388e0d8fe1a07f8d095c82201eb904f5d27ad571f", size = 73751, upload-time = "2024-12-28T17:11:18.897Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/eb/427ed2b20a38a4ee29f24dbe4ae2dafab198674fe9a85e3d6adf9e5f5f41/inflect-7.5.0-py3-none-any.whl", hash = "sha256:2aea70e5e70c35d8350b8097396ec155ffd68def678c7ff97f51aa69c1d92344", size = 35197, upload-time = "2024-12-28T17:11:15.931Z" }, +] + [[package]] name = "iniconfig" version = "2.3.0" @@ -1034,10 +1139,15 @@ name = "ipython" version = "9.13.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.14'", - "python_full_version == '3.13.*'", - "python_full_version == '3.12.*'", - "python_full_version == '3.11.*'", + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'win32'", + "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'emscripten'", + "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform == 'emscripten'", + "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] dependencies = [ { name = "colorama", marker = "python_full_version >= '3.11' and sys_platform == 'win32'" }, @@ -1070,16 +1180,37 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl", hash = "sha256:a9462224a505ade19a605f71f8fa63c2048833ce50abc86768a0d81d876dc81c", size = 8074, upload-time = "2025-01-17T11:24:33.271Z" }, ] +[[package]] +name = "isort" +version = "8.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ef/7c/ec4ab396d31b3b395e2e999c8f46dec78c5e29209fac49d1f4dace04041d/isort-8.0.1.tar.gz", hash = "sha256:171ac4ff559cdc060bcfff550bc8404a486fee0caab245679c2abe7cb253c78d", size = 769592, upload-time = "2026-02-28T10:08:20.685Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3e/95/c7c34aa53c16353c56d0b802fba48d5f5caa2cdee7958acbcb795c830416/isort-8.0.1-py3-none-any.whl", hash = "sha256:28b89bc70f751b559aeca209e6120393d43fbe2490de0559662be7a9787e3d75", size = 89733, upload-time = "2026-02-28T10:08:19.466Z" }, +] + [[package]] name = "jedi" -version = "0.19.2" +version = "0.20.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "parso" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/72/3a/79a912fbd4d8dd6fbb02bf69afd3bb72cf0c729bb3063c6f4498603db17a/jedi-0.19.2.tar.gz", hash = "sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0", size = 1231287, upload-time = "2024-11-11T01:41:42.873Z" } +sdist = { url = "https://files.pythonhosted.org/packages/46/b7/a3635f6a2d7cf5b5dd98064fc1d5fbbafcb25477bcea204a3a92145d158b/jedi-0.20.0.tar.gz", hash = "sha256:c3f4ccbd276696f4b19c54618d4fb18f9fc24b0aef02acf704b23f487daa1011", size = 3119416, upload-time = "2026-05-01T23:38:47.814Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/93/242e2eab5fe682ffcb8b0084bde703a41d51e17ee0f3a31ff0d9d813620a/jedi-0.20.0-py2.py3-none-any.whl", hash = "sha256:7bdd9c2634f56713299976f4cbd59cb3fa92165cc5e05ea811fb253480728b67", size = 4884812, upload-time = "2026-05-01T23:38:43.919Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9", size = 1572278, upload-time = "2024-11-11T01:41:40.175Z" }, + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, ] [[package]] @@ -1125,7 +1256,7 @@ name = "macholib" version = "1.16.4" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "altgraph" }, + { name = "altgraph", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'emscripten' and sys_platform != 'win32')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/10/2f/97589876ea967487978071c9042518d28b958d87b17dceb7cdc1d881f963/macholib-1.16.4.tar.gz", hash = "sha256:f408c93ab2e995cd2c46e34fe328b130404be143469e41bc366c807448979362", size = 59427, upload-time = "2025-11-22T08:28:38.373Z" } wheels = [ @@ -1134,14 +1265,99 @@ wheels = [ [[package]] name = "markdown-it-py" -version = "4.0.0" +version = "4.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mdurl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/4b/3541d44f3937ba468b75da9eebcae497dcf67adb65caa16760b0a6807ebb/markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559", size = 11631, upload-time = "2025-09-27T18:36:05.558Z" }, + { url = "https://files.pythonhosted.org/packages/98/1b/fbd8eed11021cabd9226c37342fa6ca4e8a98d8188a8d9b66740494960e4/markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419", size = 12057, upload-time = "2025-09-27T18:36:07.165Z" }, + { url = "https://files.pythonhosted.org/packages/40/01/e560d658dc0bb8ab762670ece35281dec7b6c1b33f5fbc09ebb57a185519/markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695", size = 22050, upload-time = "2025-09-27T18:36:08.005Z" }, + { url = "https://files.pythonhosted.org/packages/af/cd/ce6e848bbf2c32314c9b237839119c5a564a59725b53157c856e90937b7a/markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591", size = 20681, upload-time = "2025-09-27T18:36:08.881Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2a/b5c12c809f1c3045c4d580b035a743d12fcde53cf685dbc44660826308da/markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c", size = 20705, upload-time = "2025-09-27T18:36:10.131Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e3/9427a68c82728d0a88c50f890d0fc072a1484de2f3ac1ad0bfc1a7214fd5/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f", size = 21524, upload-time = "2025-09-27T18:36:11.324Z" }, + { url = "https://files.pythonhosted.org/packages/bc/36/23578f29e9e582a4d0278e009b38081dbe363c5e7165113fad546918a232/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6", size = 20282, upload-time = "2025-09-27T18:36:12.573Z" }, + { url = "https://files.pythonhosted.org/packages/56/21/dca11354e756ebd03e036bd8ad58d6d7168c80ce1fe5e75218e4945cbab7/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1", size = 20745, upload-time = "2025-09-27T18:36:13.504Z" }, + { url = "https://files.pythonhosted.org/packages/87/99/faba9369a7ad6e4d10b6a5fbf71fa2a188fe4a593b15f0963b73859a1bbd/markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa", size = 14571, upload-time = "2025-09-27T18:36:14.779Z" }, + { url = "https://files.pythonhosted.org/packages/d6/25/55dc3ab959917602c96985cb1253efaa4ff42f71194bddeb61eb7278b8be/markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8", size = 15056, upload-time = "2025-09-27T18:36:16.125Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9e/0a02226640c255d1da0b8d12e24ac2aa6734da68bff14c05dd53b94a0fc3/markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1", size = 13932, upload-time = "2025-09-27T18:36:17.311Z" }, + { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, + { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, + { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, + { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, + { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, + { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, + { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, + { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" }, + { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" }, + { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" }, + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, ] [[package]] @@ -1165,6 +1381,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, ] +[[package]] +name = "more-itertools" +version = "11.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/f7/139d22fef48ac78127d18e01d80cf1be40236ae489769d17f35c3d425293/more_itertools-11.0.2.tar.gz", hash = "sha256:392a9e1e362cbc106a2457d37cabf9b36e5e12efd4ebff1654630e76597df804", size = 144659, upload-time = "2026-04-09T15:01:33.297Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/98/6af411189d9413534c3eb691182bff1f5c6d44ed2f93f2edfe52a1bbceb8/more_itertools-11.0.2-py3-none-any.whl", hash = "sha256:6e35b35f818b01f691643c6c611bc0902f2e92b46c18fffa77ae1e7c46e912e4", size = 71939, upload-time = "2026-04-09T15:01:32.21Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + [[package]] name = "nest-asyncio" version = "1.6.0" @@ -1191,10 +1425,15 @@ name = "networkx" version = "3.6.1" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.14'", - "python_full_version == '3.13.*'", - "python_full_version == '3.12.*'", - "python_full_version == '3.11.*'", + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'win32'", + "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'emscripten'", + "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform == 'emscripten'", + "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] sdist = { url = "https://files.pythonhosted.org/packages/6a/51/63fe664f3908c97be9d2e4f1158eb633317598cfa6e1fc14af5383f17512/networkx-3.6.1.tar.gz", hash = "sha256:26b7c357accc0c8cde558ad486283728b65b6a95d85ee1cd66bafab4c8168509", size = 2517025, upload-time = "2025-12-08T17:02:39.908Z" } wheels = [ @@ -1280,10 +1519,15 @@ name = "numpy" version = "2.4.4" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.14'", - "python_full_version == '3.13.*'", - "python_full_version == '3.12.*'", - "python_full_version == '3.11.*'", + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'win32'", + "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'emscripten'", + "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform == 'emscripten'", + "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] sdist = { url = "https://files.pythonhosted.org/packages/d7/9f/b8cef5bffa569759033adda9481211426f12f53299629b410340795c2514/numpy-2.4.4.tar.gz", hash = "sha256:2d390634c5182175533585cc89f3608a4682ccb173cc9bb940b2881c8d6f8fa0", size = 20731587, upload-time = "2026-03-29T13:22:01.298Z" } wheels = [ @@ -1373,12 +1617,14 @@ wheels = [ name = "pandas" version = "2.3.3" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] dependencies = [ { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "python-dateutil" }, - { name = "pytz" }, - { name = "tzdata" }, + { name = "python-dateutil", marker = "python_full_version < '3.11'" }, + { name = "pytz", marker = "python_full_version < '3.11'" }, + { name = "tzdata", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/33/01/d40b85317f86cf08d853a4f495195c73815fdf205eef3993821720274518/pandas-2.3.3.tar.gz", hash = "sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b", size = 4495223, upload-time = "2025-09-29T23:34:51.853Z" } wheels = [ @@ -1431,6 +1677,77 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/70/44/5191d2e4026f86a2a109053e194d3ba7a31a2d10a9c2348368c63ed4e85a/pandas-2.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3869faf4bd07b3b66a9f462417d0ca3a9df29a9f6abd5d0d0dbab15dac7abe87", size = 13202175, upload-time = "2025-09-29T23:31:59.173Z" }, ] +[[package]] +name = "pandas" +version = "3.0.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'win32'", + "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'emscripten'", + "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform == 'emscripten'", + "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] +dependencies = [ + { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "python-dateutil", marker = "python_full_version >= '3.11'" }, + { name = "tzdata", marker = "(python_full_version >= '3.11' and sys_platform == 'emscripten') or (python_full_version >= '3.11' and sys_platform == 'win32')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/da/99/b342345300f13440fe9fe385c3c481e2d9a595ee3bab4d3219247ac94e9a/pandas-3.0.2.tar.gz", hash = "sha256:f4753e73e34c8d83221ba58f232433fca2748be8b18dbca02d242ed153945043", size = 4645855, upload-time = "2026-03-31T06:48:30.816Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/97/35/6411db530c618e0e0005187e35aa02ce60ae4c4c4d206964a2f978217c27/pandas-3.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a727a73cbdba2f7458dc82449e2315899d5140b449015d822f515749a46cbbe0", size = 10326926, upload-time = "2026-03-31T06:46:08.29Z" }, + { url = "https://files.pythonhosted.org/packages/c4/d3/b7da1d5d7dbdc5ef52ed7debd2b484313b832982266905315dad5a0bf0b1/pandas-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dbbd4aa20ca51e63b53bbde6a0fa4254b1aaabb74d2f542df7a7959feb1d760c", size = 9926987, upload-time = "2026-03-31T06:46:11.724Z" }, + { url = "https://files.pythonhosted.org/packages/52/77/9b1c2d6070b5dbe239a7bc889e21bfa58720793fb902d1e070695d87c6d0/pandas-3.0.2-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:339dda302bd8369dedeae979cb750e484d549b563c3f54f3922cb8ff4978c5eb", size = 10757067, upload-time = "2026-03-31T06:46:14.903Z" }, + { url = "https://files.pythonhosted.org/packages/20/17/ec40d981705654853726e7ac9aea9ddbb4a5d9cf54d8472222f4f3de06c2/pandas-3.0.2-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:61c2fd96d72b983a9891b2598f286befd4ad262161a609c92dc1652544b46b76", size = 11258787, upload-time = "2026-03-31T06:46:17.683Z" }, + { url = "https://files.pythonhosted.org/packages/90/e3/3f1126d43d3702ca8773871a81c9f15122a1f412342cc56284ffda5b1f70/pandas-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c934008c733b8bbea273ea308b73b3156f0181e5b72960790b09c18a2794fe1e", size = 11771616, upload-time = "2026-03-31T06:46:20.532Z" }, + { url = "https://files.pythonhosted.org/packages/2e/cf/0f4e268e1f5062e44a6bda9f925806721cd4c95c2b808a4c82ebe914f96b/pandas-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:60a80bb4feacbef5e1447a3f82c33209c8b7e07f28d805cfd1fb951e5cb443aa", size = 12337623, upload-time = "2026-03-31T06:46:23.754Z" }, + { url = "https://files.pythonhosted.org/packages/44/a0/97a6339859d4acb2536efb24feb6708e82f7d33b2ed7e036f2983fcced82/pandas-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:ed72cb3f45190874eb579c64fa92d9df74e98fd63e2be7f62bce5ace0ade61df", size = 9897372, upload-time = "2026-03-31T06:46:26.703Z" }, + { url = "https://files.pythonhosted.org/packages/8f/eb/781516b808a99ddf288143cec46b342b3016c3414d137da1fdc3290d8860/pandas-3.0.2-cp311-cp311-win_arm64.whl", hash = "sha256:f12b1a9e332c01e09510586f8ca9b108fd631fd656af82e452d7315ef6df5f9f", size = 9154922, upload-time = "2026-03-31T06:46:30.284Z" }, + { url = "https://files.pythonhosted.org/packages/f3/b0/c20bd4d6d3f736e6bd6b55794e9cd0a617b858eaad27c8f410ea05d953b7/pandas-3.0.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:232a70ebb568c0c4d2db4584f338c1577d81e3af63292208d615907b698a0f18", size = 10347921, upload-time = "2026-03-31T06:46:33.36Z" }, + { url = "https://files.pythonhosted.org/packages/35/d0/4831af68ce30cc2d03c697bea8450e3225a835ef497d0d70f31b8cdde965/pandas-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:970762605cff1ca0d3f71ed4f3a769ea8f85fc8e6348f6e110b8fea7e6eb5a14", size = 9888127, upload-time = "2026-03-31T06:46:36.253Z" }, + { url = "https://files.pythonhosted.org/packages/61/a9/16ea9346e1fc4a96e2896242d9bc674764fb9049b0044c0132502f7a771e/pandas-3.0.2-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aff4e6f4d722e0652707d7bcb190c445fe58428500c6d16005b02401764b1b3d", size = 10399577, upload-time = "2026-03-31T06:46:39.224Z" }, + { url = "https://files.pythonhosted.org/packages/c4/a8/3a61a721472959ab0ce865ef05d10b0d6bfe27ce8801c99f33d4fa996e65/pandas-3.0.2-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ef8b27695c3d3dc78403c9a7d5e59a62d5464a7e1123b4e0042763f7104dc74f", size = 10880030, upload-time = "2026-03-31T06:46:42.412Z" }, + { url = "https://files.pythonhosted.org/packages/da/65/7225c0ea4d6ce9cb2160a7fb7f39804871049f016e74782e5dade4d14109/pandas-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f8d68083e49e16b84734eb1a4dcae4259a75c90fb6e2251ab9a00b61120c06ab", size = 11409468, upload-time = "2026-03-31T06:46:45.2Z" }, + { url = "https://files.pythonhosted.org/packages/fa/5b/46e7c76032639f2132359b5cf4c785dd8cf9aea5ea64699eac752f02b9db/pandas-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:32cc41f310ebd4a296d93515fcac312216adfedb1894e879303987b8f1e2b97d", size = 11936381, upload-time = "2026-03-31T06:46:48.293Z" }, + { url = "https://files.pythonhosted.org/packages/7b/8b/721a9cff6fa6a91b162eb51019c6243b82b3226c71bb6c8ef4a9bd65cbc6/pandas-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:a4785e1d6547d8427c5208b748ae2efb64659a21bd82bf440d4262d02bfa02a4", size = 9744993, upload-time = "2026-03-31T06:46:51.488Z" }, + { url = "https://files.pythonhosted.org/packages/d5/18/7f0bd34ae27b28159aa80f2a6799f47fda34f7fb938a76e20c7b7fe3b200/pandas-3.0.2-cp312-cp312-win_arm64.whl", hash = "sha256:08504503f7101300107ecdc8df73658e4347586db5cfdadabc1592e9d7e7a0fd", size = 9056118, upload-time = "2026-03-31T06:46:54.548Z" }, + { url = "https://files.pythonhosted.org/packages/bf/ca/3e639a1ea6fcd0617ca4e8ca45f62a74de33a56ae6cd552735470b22c8d3/pandas-3.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b5918ba197c951dec132b0c5929a00c0bf05d5942f590d3c10a807f6e15a57d3", size = 10321105, upload-time = "2026-03-31T06:46:57.327Z" }, + { url = "https://files.pythonhosted.org/packages/0b/77/dbc82ff2fb0e63c6564356682bf201edff0ba16c98630d21a1fb312a8182/pandas-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d606a041c89c0a474a4702d532ab7e73a14fe35c8d427b972a625c8e46373668", size = 9864088, upload-time = "2026-03-31T06:46:59.935Z" }, + { url = "https://files.pythonhosted.org/packages/5c/2b/341f1b04bbca2e17e13cd3f08c215b70ef2c60c5356ef1e8c6857449edc7/pandas-3.0.2-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:710246ba0616e86891b58ab95f2495143bb2bc83ab6b06747c74216f583a6ac9", size = 10369066, upload-time = "2026-03-31T06:47:02.792Z" }, + { url = "https://files.pythonhosted.org/packages/12/c5/cbb1ffefb20a93d3f0e1fdcda699fb84976210d411b008f97f48bf6ce27e/pandas-3.0.2-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5d3cfe227c725b1f3dff4278b43d8c784656a42a9325b63af6b1492a8232209e", size = 10876780, upload-time = "2026-03-31T06:47:06.205Z" }, + { url = "https://files.pythonhosted.org/packages/98/fe/2249ae5e0a69bd0ddf17353d0a5d26611d70970111f5b3600cdc8be883e7/pandas-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c3b723df9087a9a9a840e263ebd9f88b64a12075d1bf2ea401a5a42f254f084d", size = 11375181, upload-time = "2026-03-31T06:47:09.383Z" }, + { url = "https://files.pythonhosted.org/packages/de/64/77a38b09e70b6464883b8d7584ab543e748e42c1b5d337a2ee088e0df741/pandas-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a3096110bf9eac0070b7208465f2740e2d8a670d5cb6530b5bb884eca495fd39", size = 11928899, upload-time = "2026-03-31T06:47:12.686Z" }, + { url = "https://files.pythonhosted.org/packages/5e/52/42855bf626868413f761addd574acc6195880ae247a5346477a4361c3acb/pandas-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:07a10f5c36512eead51bc578eb3354ad17578b22c013d89a796ab5eee90cd991", size = 9746574, upload-time = "2026-03-31T06:47:15.64Z" }, + { url = "https://files.pythonhosted.org/packages/88/39/21304ae06a25e8bf9fc820d69b29b2c495b2ae580d1e143146c309941760/pandas-3.0.2-cp313-cp313-win_arm64.whl", hash = "sha256:5fdbfa05931071aba28b408e59226186b01eb5e92bea2ab78b65863ca3228d84", size = 9047156, upload-time = "2026-03-31T06:47:18.595Z" }, + { url = "https://files.pythonhosted.org/packages/72/20/7defa8b27d4f330a903bb68eea33be07d839c5ea6bdda54174efcec0e1d2/pandas-3.0.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:dbc20dea3b9e27d0e66d74c42b2d0c1bed9c2ffe92adea33633e3bedeb5ac235", size = 10756238, upload-time = "2026-03-31T06:47:22.012Z" }, + { url = "https://files.pythonhosted.org/packages/e9/95/49433c14862c636afc0e9b2db83ff16b3ad92959364e52b2955e44c8e94c/pandas-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b75c347eff42497452116ce05ef461822d97ce5b9ff8df6edacb8076092c855d", size = 10408520, upload-time = "2026-03-31T06:47:25.197Z" }, + { url = "https://files.pythonhosted.org/packages/3b/f8/462ad2b5881d6b8ec8e5f7ed2ea1893faa02290d13870a1600fe72ad8efc/pandas-3.0.2-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1478075142e83a5571782ad007fb201ed074bdeac7ebcc8890c71442e96adf7", size = 10324154, upload-time = "2026-03-31T06:47:28.097Z" }, + { url = "https://files.pythonhosted.org/packages/0a/65/d1e69b649cbcddda23ad6e4c40ef935340f6f652a006e5cbc3555ac8adb3/pandas-3.0.2-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5880314e69e763d4c8b27937090de570f1fb8d027059a7ada3f7f8e98bdcb677", size = 10714449, upload-time = "2026-03-31T06:47:30.85Z" }, + { url = "https://files.pythonhosted.org/packages/47/a4/85b59bc65b8190ea3689882db6cdf32a5003c0ccd5a586c30fdcc3ffc4fc/pandas-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b5329e26898896f06035241a626d7c335daa479b9bbc82be7c2742d048e41172", size = 11338475, upload-time = "2026-03-31T06:47:34.026Z" }, + { url = "https://files.pythonhosted.org/packages/1e/c4/bc6966c6e38e5d9478b935272d124d80a589511ed1612a5d21d36f664c68/pandas-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:81526c4afd31971f8b62671442a4b2b51e0aa9acc3819c9f0f12a28b6fcf85f1", size = 11786568, upload-time = "2026-03-31T06:47:36.941Z" }, + { url = "https://files.pythonhosted.org/packages/e8/74/09298ca9740beed1d3504e073d67e128aa07e5ca5ca2824b0c674c0b8676/pandas-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:7cadd7e9a44ec13b621aec60f9150e744cfc7a3dd32924a7e2f45edff31823b0", size = 10488652, upload-time = "2026-03-31T06:47:40.612Z" }, + { url = "https://files.pythonhosted.org/packages/bb/40/c6ea527147c73b24fc15c891c3fcffe9c019793119c5742b8784a062c7db/pandas-3.0.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:db0dbfd2a6cdf3770aa60464d50333d8f3d9165b2f2671bcc299b72de5a6677b", size = 10326084, upload-time = "2026-03-31T06:47:43.834Z" }, + { url = "https://files.pythonhosted.org/packages/95/25/bdb9326c3b5455f8d4d3549fce7abcf967259de146fe2cf7a82368141948/pandas-3.0.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0555c5882688a39317179ab4a0ed41d3ebc8812ab14c69364bbee8fb7a3f6288", size = 9914146, upload-time = "2026-03-31T06:47:46.67Z" }, + { url = "https://files.pythonhosted.org/packages/8d/77/3a227ff3337aa376c60d288e1d61c5d097131d0ac71f954d90a8f369e422/pandas-3.0.2-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:01f31a546acd5574ef77fe199bc90b55527c225c20ccda6601cf6b0fd5ed597c", size = 10444081, upload-time = "2026-03-31T06:47:49.681Z" }, + { url = "https://files.pythonhosted.org/packages/15/88/3cdd54fa279341afa10acf8d2b503556b1375245dccc9315659f795dd2e9/pandas-3.0.2-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:deeca1b5a931fdf0c2212c8a659ade6d3b1edc21f0914ce71ef24456ca7a6535", size = 10897535, upload-time = "2026-03-31T06:47:53.033Z" }, + { url = "https://files.pythonhosted.org/packages/06/9d/98cc7a7624f7932e40f434299260e2917b090a579d75937cb8a57b9d2de3/pandas-3.0.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0f48afd9bb13300ffb5a3316973324c787054ba6665cda0da3fbd67f451995db", size = 11446992, upload-time = "2026-03-31T06:47:56.193Z" }, + { url = "https://files.pythonhosted.org/packages/9a/cd/19ff605cc3760e80602e6826ddef2824d8e7050ed80f2e11c4b079741dc3/pandas-3.0.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6c4d8458b97a35717b62469a4ea0e85abd5ed8687277f5ccfc67f8a5126f8c53", size = 11968257, upload-time = "2026-03-31T06:47:59.137Z" }, + { url = "https://files.pythonhosted.org/packages/db/60/aba6a38de456e7341285102bede27514795c1eaa353bc0e7638b6b785356/pandas-3.0.2-cp314-cp314-win_amd64.whl", hash = "sha256:b35d14bb5d8285d9494fe93815a9e9307c0876e10f1e8e89ac5b88f728ec8dcf", size = 9865893, upload-time = "2026-03-31T06:48:02.038Z" }, + { url = "https://files.pythonhosted.org/packages/08/71/e5ec979dd2e8a093dacb8864598c0ff59a0cee0bbcdc0bfec16a51684d4f/pandas-3.0.2-cp314-cp314-win_arm64.whl", hash = "sha256:63d141b56ef686f7f0d714cfb8de4e320475b86bf4b620aa0b7da89af8cbdbbb", size = 9188644, upload-time = "2026-03-31T06:48:05.045Z" }, + { url = "https://files.pythonhosted.org/packages/f1/6c/7b45d85db19cae1eb524f2418ceaa9d85965dcf7b764ed151386b7c540f0/pandas-3.0.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:140f0cffb1fa2524e874dde5b477d9defe10780d8e9e220d259b2c0874c89d9d", size = 10776246, upload-time = "2026-03-31T06:48:07.789Z" }, + { url = "https://files.pythonhosted.org/packages/a8/3e/7b00648b086c106e81766f25322b48aa8dfa95b55e621dbdf2fdd413a117/pandas-3.0.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ae37e833ff4fed0ba352f6bdd8b73ba3ab3256a85e54edfd1ab51ae40cca0af8", size = 10424801, upload-time = "2026-03-31T06:48:10.897Z" }, + { url = "https://files.pythonhosted.org/packages/da/6e/558dd09a71b53b4008e7fc8a98ec6d447e9bfb63cdaeea10e5eb9b2dabe8/pandas-3.0.2-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4d888a5c678a419a5bb41a2a93818e8ed9fd3172246555c0b37b7cc27027effd", size = 10345643, upload-time = "2026-03-31T06:48:13.7Z" }, + { url = "https://files.pythonhosted.org/packages/be/e3/921c93b4d9a280409451dc8d07b062b503bbec0531d2627e73a756e99a82/pandas-3.0.2-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b444dc64c079e84df91baa8bf613d58405645461cabca929d9178f2cd392398d", size = 10743641, upload-time = "2026-03-31T06:48:16.659Z" }, + { url = "https://files.pythonhosted.org/packages/56/ca/fd17286f24fa3b4d067965d8d5d7e14fe557dd4f979a0b068ac0deaf8228/pandas-3.0.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4544c7a54920de8eeacaa1466a6b7268ecfbc9bc64ab4dbb89c6bbe94d5e0660", size = 11361993, upload-time = "2026-03-31T06:48:19.475Z" }, + { url = "https://files.pythonhosted.org/packages/e4/a5/2f6ed612056819de445a433ca1f2821ac3dab7f150d569a59e9cc105de1d/pandas-3.0.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:734be7551687c00fbd760dc0522ed974f82ad230d4a10f54bf51b80d44a08702", size = 11815274, upload-time = "2026-03-31T06:48:22.695Z" }, + { url = "https://files.pythonhosted.org/packages/00/2f/b622683e99ec3ce00b0854bac9e80868592c5b051733f2cf3a868e5fea26/pandas-3.0.2-cp314-cp314t-win_amd64.whl", hash = "sha256:57a07209bebcbcf768d2d13c9b78b852f9a15978dac41b9e6421a81ad4cdd276", size = 10888530, upload-time = "2026-03-31T06:48:25.806Z" }, + { url = "https://files.pythonhosted.org/packages/cb/2b/f8434233fab2bd66a02ec014febe4e5adced20e2693e0e90a07d118ed30e/pandas-3.0.2-cp314-cp314t-win_arm64.whl", hash = "sha256:5371b72c2d4d415d08765f32d689217a43227484e81b2305b52076e328f6f482", size = 9455341, upload-time = "2026-03-31T06:48:28.418Z" }, +] + [[package]] name = "parallel-web" version = "0.6.0" @@ -1483,9 +1800,11 @@ cli = [ { name = "questionary" }, ] dev = [ + { name = "datamodel-code-generator", extra = ["ruff"] }, { name = "duckdb" }, { name = "nest-asyncio" }, - { name = "pandas" }, + { name = "pandas", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "pandas", version = "3.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "polars" }, { name = "pre-commit" }, { name = "pyarrow" }, @@ -1510,7 +1829,8 @@ duckdb = [ { name = "questionary" }, ] pandas = [ - { name = "pandas" }, + { name = "pandas", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "pandas", version = "3.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, ] polars = [ { name = "polars" }, @@ -1520,7 +1840,8 @@ snowflake = [ { name = "snowflake-connector-python" }, ] spark = [ - { name = "pandas" }, + { name = "pandas", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "pandas", version = "3.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "pyspark" }, ] @@ -1535,6 +1856,7 @@ dev = [ [package.metadata] requires-dist = [ { name = "click", specifier = ">=8.1.0" }, + { name = "datamodel-code-generator", extras = ["ruff"], marker = "extra == 'dev'", specifier = ">=0.26.0" }, { name = "duckdb", marker = "extra == 'duckdb'", specifier = ">=1.0.0" }, { name = "httpx", specifier = ">=0.25.0" }, { name = "nest-asyncio", marker = "extra == 'duckdb'", specifier = ">=1.6.0" }, @@ -1574,11 +1896,20 @@ dev = [ [[package]] name = "parso" -version = "0.8.6" +version = "0.8.7" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/81/76/a1e769043c0c0c9fe391b702539d594731a4362334cdf4dc25d0c09761e7/parso-0.8.6.tar.gz", hash = "sha256:2b9a0332696df97d454fa67b81618fd69c35a7b90327cbe6ba5c92d2c68a7bfd", size = 401621, upload-time = "2026-02-09T15:45:24.425Z" } +sdist = { url = "https://files.pythonhosted.org/packages/30/4b/90c937815137d43ce71ba043cd3566221e9df6b9c805f24b5d138c9d40a7/parso-0.8.7.tar.gz", hash = "sha256:eaaac4c9fdd5e9e8852dc778d2d7405897ec510f2a298071453e5e3a07914bb1", size = 401824, upload-time = "2026-05-01T23:13:02.138Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b6/61/fae042894f4296ec49e3f193aff5d7c18440da9e48102c3315e1bc4519a7/parso-0.8.6-py2.py3-none-any.whl", hash = "sha256:2c549f800b70a5c4952197248825584cb00f033b29c692671d3bf08bf380baff", size = 106894, upload-time = "2026-02-09T15:45:21.391Z" }, + { url = "https://files.pythonhosted.org/packages/99/5d/8268b644392ee874ee82a635cd0df1773de230bde356c38de28e298392cc/parso-0.8.7-py2.py3-none-any.whl", hash = "sha256:a8926eb2a1b915486941fdbd31e86a4baf88fe8c210f25f2f35ecec5b574ca1c", size = 107025, upload-time = "2026-05-01T23:12:58.867Z" }, +] + +[[package]] +name = "pathspec" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/82/42f767fc1c1143d6fd36efb827202a2d997a375e160a71eb2888a925aac1/pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a", size = 135180, upload-time = "2026-04-27T01:46:08.907Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328, upload-time = "2026-04-27T01:46:07.06Z" }, ] [[package]] @@ -1595,7 +1926,7 @@ name = "pexpect" version = "4.9.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "ptyprocess" }, + { name = "ptyprocess", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'emscripten' and sys_platform != 'win32')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450, upload-time = "2023-11-25T09:07:26.339Z" } wheels = [ @@ -1678,14 +2009,14 @@ wheels = [ [[package]] name = "proto-plus" -version = "1.27.2" +version = "1.28.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/81/0d/94dfe80193e79d55258345901acd2917523d56e8381bc4dee7fd38e3868a/proto_plus-1.27.2.tar.gz", hash = "sha256:b2adde53adadf75737c44d3dcb0104fde65250dfc83ad59168b4aa3e574b6a24", size = 57204, upload-time = "2026-03-26T22:18:57.174Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c9/56/e647b0c675392d2da368da7b6f158f7368b18542fd6f7d7400a2f39de000/proto_plus-1.28.0.tar.gz", hash = "sha256:38e5696342835b08fc116f30a25665b29531cda9d5d5643e9b81fc312385abd9", size = 57221, upload-time = "2026-05-07T08:04:50.811Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/84/f3/1fba73eeffafc998a25d59703b63f8be4fe8a5cb12eaff7386a0ba0f7125/proto_plus-1.27.2-py3-none-any.whl", hash = "sha256:6432f75893d3b9e70b9c412f1d2f03f65b11fb164b793d14ae2ca01821d22718", size = 50450, upload-time = "2026-03-26T22:13:42.927Z" }, + { url = "https://files.pythonhosted.org/packages/7c/20/b122d4626976acb81132036d2ad1bb35a1a8775fceb837ec30964622516a/proto_plus-1.28.0-py3-none-any.whl", hash = "sha256:a630604310899e73c59ec302e5765c058d412b2f090b9c79c8822589f14955b8", size = 50410, upload-time = "2026-05-07T08:03:31.962Z" }, ] [[package]] @@ -1847,7 +2178,7 @@ wheels = [ [[package]] name = "pydantic" -version = "2.13.3" +version = "2.13.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-types" }, @@ -1855,125 +2186,125 @@ dependencies = [ { name = "typing-extensions" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d9/e4/40d09941a2cebcb20609b86a559817d5b9291c49dd6f8c87e5feffbe703a/pydantic-2.13.3.tar.gz", hash = "sha256:af09e9d1d09f4e7fe37145c1f577e1d61ceb9a41924bf0094a36506285d0a84d", size = 844068, upload-time = "2026-04-20T14:46:43.632Z" } +sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f3/0a/fd7d723f8f8153418fb40cf9c940e82004fce7e987026b08a68a36dd3fe7/pydantic-2.13.3-py3-none-any.whl", hash = "sha256:6db14ac8dfc9a1e57f87ea2c0de670c251240f43cb0c30a5130e9720dc612927", size = 471981, upload-time = "2026-04-20T14:46:41.402Z" }, + { url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" }, ] [[package]] name = "pydantic-core" -version = "2.46.3" +version = "2.46.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2a/ef/f7abb56c49382a246fd2ce9c799691e3c3e7175ec74b14d99e798bcddb1a/pydantic_core-2.46.3.tar.gz", hash = "sha256:41c178f65b8c29807239d47e6050262eb6bf84eb695e41101e62e38df4a5bc2c", size = 471412, upload-time = "2026-04-20T14:40:56.672Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/22/98/b50eb9a411e87483b5c65dba4fa430a06bac4234d3403a40e5a9905ebcd0/pydantic_core-2.46.3-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:1da3786b8018e60349680720158cc19161cc3b4bdd815beb0a321cd5ce1ad5b1", size = 2108971, upload-time = "2026-04-20T14:43:51.945Z" }, - { url = "https://files.pythonhosted.org/packages/08/4b/f364b9d161718ff2217160a4b5d41ce38de60aed91c3689ebffa1c939d23/pydantic_core-2.46.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cc0988cb29d21bf4a9d5cf2ef970b5c0e38d8d8e107a493278c05dc6c1dda69f", size = 1949588, upload-time = "2026-04-20T14:44:10.386Z" }, - { url = "https://files.pythonhosted.org/packages/8f/8b/30bd03ee83b2f5e29f5ba8e647ab3c456bf56f2ec72fdbcc0215484a0854/pydantic_core-2.46.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27f9067c3bfadd04c55484b89c0d267981b2f3512850f6f66e1e74204a4e4ce3", size = 1975986, upload-time = "2026-04-20T14:43:57.106Z" }, - { url = "https://files.pythonhosted.org/packages/3c/54/13ccf954d84ec275d5d023d5786e4aa48840bc9f161f2838dc98e1153518/pydantic_core-2.46.3-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a642ac886ecf6402d9882d10c405dcf4b902abeb2972cd5fb4a48c83cd59279a", size = 2055830, upload-time = "2026-04-20T14:44:15.499Z" }, - { url = "https://files.pythonhosted.org/packages/be/0e/65f38125e660fdbd72aa858e7dfae893645cfa0e7b13d333e174a367cd23/pydantic_core-2.46.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:79f561438481f28681584b89e2effb22855e2179880314bcddbf5968e935e807", size = 2222340, upload-time = "2026-04-20T14:41:51.353Z" }, - { url = "https://files.pythonhosted.org/packages/d1/88/f3ab7739efe0e7e80777dbb84c59eb98518e3f57ea433206194c2e425272/pydantic_core-2.46.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:57a973eae4665352a47cf1a99b4ee864620f2fe663a217d7a8da68a1f3a5bfda", size = 2280727, upload-time = "2026-04-20T14:41:30.461Z" }, - { url = "https://files.pythonhosted.org/packages/2a/6d/c228219080817bec4982f9531cadb18da6aaa770fdeb114f49c237ac2c9f/pydantic_core-2.46.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:83d002b97072a53ea150d63e0a3adfae5670cef5aa8a6e490240e482d3b22e57", size = 2092158, upload-time = "2026-04-20T14:44:07.305Z" }, - { url = "https://files.pythonhosted.org/packages/0f/b1/525a16711e7c6d61635fac3b0bd54600b5c5d9f60c6fc5aaab26b64a2297/pydantic_core-2.46.3-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:b40ddd51e7c44b28cfaef746c9d3c506d658885e0a46f9eeef2ee815cbf8e045", size = 2116626, upload-time = "2026-04-20T14:42:34.118Z" }, - { url = "https://files.pythonhosted.org/packages/ef/7c/17d30673351439a6951bf54f564cf2443ab00ae264ec9df00e2efd710eb5/pydantic_core-2.46.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ac5ec7fb9b87f04ee839af2d53bcadea57ded7d229719f56c0ed895bff987943", size = 2160691, upload-time = "2026-04-20T14:41:14.023Z" }, - { url = "https://files.pythonhosted.org/packages/86/66/af8adbcbc0886ead7f1a116606a534d75a307e71e6e08226000d51b880d2/pydantic_core-2.46.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:a3b11c812f61b3129c4905781a2601dfdfdea5fe1e6c1cfb696b55d14e9c054f", size = 2182543, upload-time = "2026-04-20T14:40:48.886Z" }, - { url = "https://files.pythonhosted.org/packages/b0/37/6de71e0f54c54a4190010f57deb749e1ddf75c568ada3b1320b70067f121/pydantic_core-2.46.3-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:1108da631e602e5b3c38d6d04fe5bb3bfa54349e6918e3ca6cf570b2e2b2f9d4", size = 2324513, upload-time = "2026-04-20T14:42:36.121Z" }, - { url = "https://files.pythonhosted.org/packages/51/b1/9fc74ce94f603d5ef59ff258ca9c2c8fb902fb548d340a96f77f4d1c3b7f/pydantic_core-2.46.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:de885175515bcfa98ae618c1df7a072f13d179f81376c8007112af20567fd08a", size = 2361853, upload-time = "2026-04-20T14:43:24.886Z" }, - { url = "https://files.pythonhosted.org/packages/40/d0/4c652fc592db35f100279ee751d5a145aca1b9a7984b9684ba7c1b5b0535/pydantic_core-2.46.3-cp310-cp310-win32.whl", hash = "sha256:d11058e3201527d41bc6b545c79187c9e4bf85e15a236a6007f0e991518882b7", size = 1980465, upload-time = "2026-04-20T14:44:46.239Z" }, - { url = "https://files.pythonhosted.org/packages/27/b8/a920453c38afbe1f355e1ea0b0d94a0a3e0b0879d32d793108755fa171d5/pydantic_core-2.46.3-cp310-cp310-win_amd64.whl", hash = "sha256:3612edf65c8ea67ac13616c4d23af12faef1ae435a8a93e5934c2a0cbbdd1fd6", size = 2073884, upload-time = "2026-04-20T14:43:01.201Z" }, - { url = "https://files.pythonhosted.org/packages/22/a2/1ba90a83e85a3f94c796b184f3efde9c72f2830dcda493eea8d59ba78e6d/pydantic_core-2.46.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:ab124d49d0459b2373ecf54118a45c28a1e6d4192a533fbc915e70f556feb8e5", size = 2106740, upload-time = "2026-04-20T14:41:20.932Z" }, - { url = "https://files.pythonhosted.org/packages/b6/f6/99ae893c89a0b9d3daec9f95487aa676709aa83f67643b3f0abaf4ab628a/pydantic_core-2.46.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cca67d52a5c7a16aed2b3999e719c4bcf644074eac304a5d3d62dd70ae7d4b2c", size = 1948293, upload-time = "2026-04-20T14:43:42.115Z" }, - { url = "https://files.pythonhosted.org/packages/3e/b8/2e8e636dc9e3f16c2e16bf0849e24be82c5ee82c603c65fc0326666328fc/pydantic_core-2.46.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c024e08c0ba23e6fd68c771a521e9d6a792f2ebb0fa734296b36394dc30390e", size = 1973222, upload-time = "2026-04-20T14:41:57.841Z" }, - { url = "https://files.pythonhosted.org/packages/34/36/0e730beec4d83c5306f417afbd82ff237d9a21e83c5edf675f31ed84c1fe/pydantic_core-2.46.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6645ce7eec4928e29a1e3b3d5c946621d105d3e79f0c9cddf07c2a9770949287", size = 2053852, upload-time = "2026-04-20T14:40:43.077Z" }, - { url = "https://files.pythonhosted.org/packages/4b/f0/3071131f47e39136a17814576e0fada9168569f7f8c0e6ac4d1ede6a4958/pydantic_core-2.46.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a712c7118e6c5ea96562f7b488435172abb94a3c53c22c9efc1412264a45cbbe", size = 2221134, upload-time = "2026-04-20T14:43:03.349Z" }, - { url = "https://files.pythonhosted.org/packages/2f/a9/a2dc023eec5aa4b02a467874bad32e2446957d2adcab14e107eab502e978/pydantic_core-2.46.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:69a868ef3ff206343579021c40faf3b1edc64b1cc508ff243a28b0a514ccb050", size = 2279785, upload-time = "2026-04-20T14:41:19.285Z" }, - { url = "https://files.pythonhosted.org/packages/0a/44/93f489d16fb63fbd41c670441536541f6e8cfa1e5a69f40bc9c5d30d8c90/pydantic_core-2.46.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc7e8c32db809aa0f6ea1d6869ebc8518a65d5150fdfad8bcae6a49ae32a22e2", size = 2089404, upload-time = "2026-04-20T14:43:10.108Z" }, - { url = "https://files.pythonhosted.org/packages/2a/78/8692e3aa72b2d004f7a5d937f1dfdc8552ba26caf0bec75f342c40f00dec/pydantic_core-2.46.3-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:3481bd1341dc85779ee506bc8e1196a277ace359d89d28588a9468c3ecbe63fa", size = 2114898, upload-time = "2026-04-20T14:44:51.475Z" }, - { url = "https://files.pythonhosted.org/packages/6a/62/e83133f2e7832532060175cebf1f13748f4c7e7e7165cdd1f611f174494b/pydantic_core-2.46.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8690eba565c6d68ffd3a8655525cbdd5246510b44a637ee2c6c03a7ebfe64d3c", size = 2157856, upload-time = "2026-04-20T14:43:46.64Z" }, - { url = "https://files.pythonhosted.org/packages/6d/ec/6a500e3ad7718ee50583fae79c8651f5d37e3abce1fa9ae177ae65842c53/pydantic_core-2.46.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4de88889d7e88d50d40ee5b39d5dac0bcaef9ba91f7e536ac064e6b2834ecccf", size = 2180168, upload-time = "2026-04-20T14:42:00.302Z" }, - { url = "https://files.pythonhosted.org/packages/d8/53/8267811054b1aa7fc1dc7ded93812372ef79a839f5e23558136a6afbfde1/pydantic_core-2.46.3-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:e480080975c1ef7f780b8f99ed72337e7cc5efea2e518a20a692e8e7b278eb8b", size = 2322885, upload-time = "2026-04-20T14:41:05.253Z" }, - { url = "https://files.pythonhosted.org/packages/c8/c1/1c0acdb3aa0856ddc4ecc55214578f896f2de16f400cf51627eb3c26c1c4/pydantic_core-2.46.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:de3a5c376f8cd94da9a1b8fd3dd1c16c7a7b216ed31dc8ce9fd7a22bf13b836e", size = 2360328, upload-time = "2026-04-20T14:41:43.991Z" }, - { url = "https://files.pythonhosted.org/packages/f0/d0/ef39cd0f4a926814f360e71c1adeab48ad214d9727e4deb48eedfb5bce1a/pydantic_core-2.46.3-cp311-cp311-win32.whl", hash = "sha256:fc331a5314ffddd5385b9ee9d0d2fee0b13c27e0e02dad71b1ae5d6561f51eeb", size = 1979464, upload-time = "2026-04-20T14:43:12.215Z" }, - { url = "https://files.pythonhosted.org/packages/18/9c/f41951b0d858e343f1cf09398b2a7b3014013799744f2c4a8ad6a3eec4f2/pydantic_core-2.46.3-cp311-cp311-win_amd64.whl", hash = "sha256:b5b9c6cf08a8a5e502698f5e153056d12c34b8fb30317e0c5fd06f45162a6346", size = 2070837, upload-time = "2026-04-20T14:41:47.707Z" }, - { url = "https://files.pythonhosted.org/packages/9f/1e/264a17cd582f6ed50950d4d03dd5fefd84e570e238afe1cb3e25cf238769/pydantic_core-2.46.3-cp311-cp311-win_arm64.whl", hash = "sha256:5dfd51cf457482f04ec49491811a2b8fd5b843b64b11eecd2d7a1ee596ea78a6", size = 2053647, upload-time = "2026-04-20T14:42:27.535Z" }, - { url = "https://files.pythonhosted.org/packages/4b/cb/5b47425556ecc1f3fe18ed2a0083188aa46e1dd812b06e406475b3a5d536/pydantic_core-2.46.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:b11b59b3eee90a80a36701ddb4576d9ae31f93f05cb9e277ceaa09e6bf074a67", size = 2101946, upload-time = "2026-04-20T14:40:52.581Z" }, - { url = "https://files.pythonhosted.org/packages/a1/4f/2fb62c2267cae99b815bbf4a7b9283812c88ca3153ef29f7707200f1d4e5/pydantic_core-2.46.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:af8653713055ea18a3abc1537fe2ebc42f5b0bbb768d1eb79fd74eb47c0ac089", size = 1951612, upload-time = "2026-04-20T14:42:42.996Z" }, - { url = "https://files.pythonhosted.org/packages/50/6e/b7348fd30d6556d132cddd5bd79f37f96f2601fe0608afac4f5fb01ec0b3/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:75a519dab6d63c514f3a81053e5266c549679e4aa88f6ec57f2b7b854aceb1b0", size = 1977027, upload-time = "2026-04-20T14:42:02.001Z" }, - { url = "https://files.pythonhosted.org/packages/82/11/31d60ee2b45540d3fb0b29302a393dbc01cd771c473f5b5147bcd353e593/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a6cd87cb1575b1ad05ba98894c5b5c96411ef678fa2f6ed2576607095b8d9789", size = 2063008, upload-time = "2026-04-20T14:44:17.952Z" }, - { url = "https://files.pythonhosted.org/packages/8a/db/3a9d1957181b59258f44a2300ab0f0be9d1e12d662a4f57bb31250455c52/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f80a55484b8d843c8ada81ebf70a682f3f00a3d40e378c06cf17ecb44d280d7d", size = 2233082, upload-time = "2026-04-20T14:40:57.934Z" }, - { url = "https://files.pythonhosted.org/packages/9c/e1/3277c38792aeb5cfb18c2f0c5785a221d9ff4e149abbe1184d53d5f72273/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3861f1731b90c50a3266316b9044f5c9b405eecb8e299b0a7120596334e4fe9c", size = 2304615, upload-time = "2026-04-20T14:42:12.584Z" }, - { url = "https://files.pythonhosted.org/packages/5e/d5/e3d9717c9eba10855325650afd2a9cba8e607321697f18953af9d562da2f/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb528e295ed31570ac3dcc9bfdd6e0150bc11ce6168ac87a8082055cf1a67395", size = 2094380, upload-time = "2026-04-20T14:43:05.522Z" }, - { url = "https://files.pythonhosted.org/packages/a1/20/abac35dedcbfd66c6f0b03e4e3564511771d6c9b7ede10a362d03e110d9b/pydantic_core-2.46.3-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:367508faa4973b992b271ba1494acaab36eb7e8739d1e47be5035fb1ea225396", size = 2135429, upload-time = "2026-04-20T14:41:55.549Z" }, - { url = "https://files.pythonhosted.org/packages/6c/a5/41bfd1df69afad71b5cf0535055bccc73022715ad362edbc124bc1e021d7/pydantic_core-2.46.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5ad3c826fe523e4becf4fe39baa44286cff85ef137c729a2c5e269afbfd0905d", size = 2174582, upload-time = "2026-04-20T14:41:45.96Z" }, - { url = "https://files.pythonhosted.org/packages/79/65/38d86ea056b29b2b10734eb23329b7a7672ca604df4f2b6e9c02d4ee22fe/pydantic_core-2.46.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ec638c5d194ef8af27db69f16c954a09797c0dc25015ad6123eb2c73a4d271ca", size = 2187533, upload-time = "2026-04-20T14:40:55.367Z" }, - { url = "https://files.pythonhosted.org/packages/b6/55/a1129141678a2026badc539ad1dee0a71d06f54c2f06a4bd68c030ac781b/pydantic_core-2.46.3-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:28ed528c45446062ee66edb1d33df5d88828ae167de76e773a3c7f64bd14e976", size = 2332985, upload-time = "2026-04-20T14:44:13.05Z" }, - { url = "https://files.pythonhosted.org/packages/d7/60/cb26f4077719f709e54819f4e8e1d43f4091f94e285eb6bd21e1190a7b7c/pydantic_core-2.46.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:aed19d0c783886d5bd86d80ae5030006b45e28464218747dcf83dabfdd092c7b", size = 2373670, upload-time = "2026-04-20T14:41:53.421Z" }, - { url = "https://files.pythonhosted.org/packages/6b/7e/c3f21882bdf1d8d086876f81b5e296206c69c6082551d776895de7801fa0/pydantic_core-2.46.3-cp312-cp312-win32.whl", hash = "sha256:06d5d8820cbbdb4147578c1fe7ffcd5b83f34508cb9f9ab76e807be7db6ff0a4", size = 1966722, upload-time = "2026-04-20T14:44:30.588Z" }, - { url = "https://files.pythonhosted.org/packages/57/be/6b5e757b859013ebfbd7adba02f23b428f37c86dcbf78b5bb0b4ffd36e99/pydantic_core-2.46.3-cp312-cp312-win_amd64.whl", hash = "sha256:c3212fda0ee959c1dd04c60b601ec31097aaa893573a3a1abd0a47bcac2968c1", size = 2072970, upload-time = "2026-04-20T14:42:54.248Z" }, - { url = "https://files.pythonhosted.org/packages/bf/f8/a989b21cc75e9a32d24192ef700eea606521221a89faa40c919ce884f2b1/pydantic_core-2.46.3-cp312-cp312-win_arm64.whl", hash = "sha256:f1f8338dd7a7f31761f1f1a3c47503a9a3b34eea3c8b01fa6ee96408affb5e72", size = 2035963, upload-time = "2026-04-20T14:44:20.4Z" }, - { url = "https://files.pythonhosted.org/packages/9b/3c/9b5e8eb9821936d065439c3b0fb1490ffa64163bfe7e1595985a47896073/pydantic_core-2.46.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:12bc98de041458b80c86c56b24df1d23832f3e166cbaff011f25d187f5c62c37", size = 2102109, upload-time = "2026-04-20T14:41:24.219Z" }, - { url = "https://files.pythonhosted.org/packages/91/97/1c41d1f5a19f241d8069f1e249853bcce378cdb76eec8ab636d7bc426280/pydantic_core-2.46.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:85348b8f89d2c3508b65b16c3c33a4da22b8215138d8b996912bb1532868885f", size = 1951820, upload-time = "2026-04-20T14:42:14.236Z" }, - { url = "https://files.pythonhosted.org/packages/30/b4/d03a7ae14571bc2b6b3c7b122441154720619afe9a336fa3a95434df5e2f/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1105677a6df914b1fb71a81b96c8cce7726857e1717d86001f29be06a25ee6f8", size = 1977785, upload-time = "2026-04-20T14:42:31.648Z" }, - { url = "https://files.pythonhosted.org/packages/ae/0c/4086f808834b59e3c8f1aa26df8f4b6d998cdcf354a143d18ef41529d1fe/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:87082cd65669a33adeba5470769e9704c7cf026cc30afb9cc77fd865578ebaad", size = 2062761, upload-time = "2026-04-20T14:40:37.093Z" }, - { url = "https://files.pythonhosted.org/packages/fa/71/a649be5a5064c2df0db06e0a512c2281134ed2fcc981f52a657936a7527c/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:60e5f66e12c4f5212d08522963380eaaeac5ebd795826cfd19b2dfb0c7a52b9c", size = 2232989, upload-time = "2026-04-20T14:42:59.254Z" }, - { url = "https://files.pythonhosted.org/packages/a2/84/7756e75763e810b3a710f4724441d1ecc5883b94aacb07ca71c5fb5cfb69/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b6cdf19bf84128d5e7c37e8a73a0c5c10d51103a650ac585d42dd6ae233f2b7f", size = 2303975, upload-time = "2026-04-20T14:41:32.287Z" }, - { url = "https://files.pythonhosted.org/packages/6c/35/68a762e0c1e31f35fa0dac733cbd9f5b118042853698de9509c8e5bf128b/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:031bb17f4885a43773c8c763089499f242aee2ea85cf17154168775dccdecf35", size = 2095325, upload-time = "2026-04-20T14:42:47.685Z" }, - { url = "https://files.pythonhosted.org/packages/77/bf/1bf8c9a8e91836c926eae5e3e51dce009bf495a60ca56060689d3df3f340/pydantic_core-2.46.3-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:bcf2a8b2982a6673693eae7348ef3d8cf3979c1d63b54fca7c397a635cc68687", size = 2133368, upload-time = "2026-04-20T14:41:22.766Z" }, - { url = "https://files.pythonhosted.org/packages/e5/50/87d818d6bab915984995157ceb2380f5aac4e563dddbed6b56f0ed057aba/pydantic_core-2.46.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28e8cf2f52d72ced402a137145923a762cbb5081e48b34312f7a0c8f55928ec3", size = 2173908, upload-time = "2026-04-20T14:42:52.044Z" }, - { url = "https://files.pythonhosted.org/packages/91/88/a311fb306d0bd6185db41fa14ae888fb81d0baf648a761ae760d30819d33/pydantic_core-2.46.3-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:17eaface65d9fc5abb940003020309c1bf7a211f5f608d7870297c367e6f9022", size = 2186422, upload-time = "2026-04-20T14:43:29.55Z" }, - { url = "https://files.pythonhosted.org/packages/8f/79/28fd0d81508525ab2054fef7c77a638c8b5b0afcbbaeee493cf7c3fef7e1/pydantic_core-2.46.3-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:93fd339f23408a07e98950a89644f92c54d8729719a40b30c0a30bb9ebc55d23", size = 2332709, upload-time = "2026-04-20T14:42:16.134Z" }, - { url = "https://files.pythonhosted.org/packages/b3/21/795bf5fe5c0f379308b8ef19c50dedab2e7711dbc8d0c2acf08f1c7daa05/pydantic_core-2.46.3-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:23cbdb3aaa74dfe0837975dbf69b469753bbde8eacace524519ffdb6b6e89eb7", size = 2372428, upload-time = "2026-04-20T14:41:10.974Z" }, - { url = "https://files.pythonhosted.org/packages/45/b3/ed14c659cbe7605e3ef063077680a64680aec81eb1a04763a05190d49b7f/pydantic_core-2.46.3-cp313-cp313-win32.whl", hash = "sha256:610eda2e3838f401105e6326ca304f5da1e15393ae25dacae5c5c63f2c275b13", size = 1965601, upload-time = "2026-04-20T14:41:42.128Z" }, - { url = "https://files.pythonhosted.org/packages/ef/bb/adb70d9a762ddd002d723fbf1bd492244d37da41e3af7b74ad212609027e/pydantic_core-2.46.3-cp313-cp313-win_amd64.whl", hash = "sha256:68cc7866ed863db34351294187f9b729964c371ba33e31c26f478471c52e1ed0", size = 2071517, upload-time = "2026-04-20T14:43:36.096Z" }, - { url = "https://files.pythonhosted.org/packages/52/eb/66faefabebfe68bd7788339c9c9127231e680b11906368c67ce112fdb47f/pydantic_core-2.46.3-cp313-cp313-win_arm64.whl", hash = "sha256:f64b5537ac62b231572879cd08ec05600308636a5d63bcbdb15063a466977bec", size = 2035802, upload-time = "2026-04-20T14:43:38.507Z" }, - { url = "https://files.pythonhosted.org/packages/7f/db/a7bcb4940183fda36022cd18ba8dd12f2dff40740ec7b58ce7457befa416/pydantic_core-2.46.3-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:afa3aa644f74e290cdede48a7b0bee37d1c35e71b05105f6b340d484af536d9b", size = 2097614, upload-time = "2026-04-20T14:44:38.374Z" }, - { url = "https://files.pythonhosted.org/packages/24/35/e4066358a22e3e99519db370494c7528f5a2aa1367370e80e27e20283543/pydantic_core-2.46.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ced3310e51aa425f7f77da8bbbb5212616655bedbe82c70944320bc1dbe5e018", size = 1951896, upload-time = "2026-04-20T14:40:53.996Z" }, - { url = "https://files.pythonhosted.org/packages/87/92/37cf4049d1636996e4b888c05a501f40a43ff218983a551d57f9d5e14f0d/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e29908922ce9da1a30b4da490bd1d3d82c01dcfdf864d2a74aacee674d0bfa34", size = 1979314, upload-time = "2026-04-20T14:41:49.446Z" }, - { url = "https://files.pythonhosted.org/packages/d8/36/9ff4d676dfbdfb2d591cf43f3d90ded01e15b1404fd101180ed2d62a2fd3/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0c9ff69140423eea8ed2d5477df3ba037f671f5e897d206d921bc9fdc39613e7", size = 2056133, upload-time = "2026-04-20T14:42:23.574Z" }, - { url = "https://files.pythonhosted.org/packages/bc/f0/405b442a4d7ba855b06eec8b2bf9c617d43b8432d099dfdc7bf999293495/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b675ab0a0d5b1c8fdb81195dc5bcefea3f3c240871cdd7ff9a2de8aa50772eb2", size = 2228726, upload-time = "2026-04-20T14:44:22.816Z" }, - { url = "https://files.pythonhosted.org/packages/e7/f8/65cd92dd5a0bd89ba277a98ecbfaf6fc36bbd3300973c7a4b826d6ab1391/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0087084960f209a9a4af50ecd1fb063d9ad3658c07bb81a7a53f452dacbfb2ba", size = 2301214, upload-time = "2026-04-20T14:44:48.792Z" }, - { url = "https://files.pythonhosted.org/packages/fd/86/ef96a4c6e79e7a2d0410826a68fbc0eccc0fd44aa733be199d5fcac3bb87/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed42e6cc8e1b0e2b9b96e2276bad70ae625d10d6d524aed0c93de974ae029f9f", size = 2099927, upload-time = "2026-04-20T14:41:40.196Z" }, - { url = "https://files.pythonhosted.org/packages/6d/53/269caf30e0096e0a8a8f929d1982a27b3879872cca2d917d17c2f9fdf4fe/pydantic_core-2.46.3-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:f1771ce258afb3e4201e67d154edbbae712a76a6081079fe247c2f53c6322c22", size = 2128789, upload-time = "2026-04-20T14:41:15.868Z" }, - { url = "https://files.pythonhosted.org/packages/00/b0/1a6d9b6a587e118482910c244a1c5acf4d192604174132efd12bf0ac486f/pydantic_core-2.46.3-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a7610b6a5242a6c736d8ad47fd5fff87fcfe8f833b281b1c409c3d6835d9227f", size = 2173815, upload-time = "2026-04-20T14:44:25.152Z" }, - { url = "https://files.pythonhosted.org/packages/87/56/e7e00d4041a7e62b5a40815590114db3b535bf3ca0bf4dca9f16cef25246/pydantic_core-2.46.3-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:ff5e7783bcc5476e1db448bf268f11cb257b1c276d3e89f00b5727be86dd0127", size = 2181608, upload-time = "2026-04-20T14:41:28.933Z" }, - { url = "https://files.pythonhosted.org/packages/e8/22/4bd23c3d41f7c185d60808a1de83c76cf5aeabf792f6c636a55c3b1ec7f9/pydantic_core-2.46.3-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:9d2e32edcc143bc01e95300671915d9ca052d4f745aa0a49c48d4803f8a85f2c", size = 2326968, upload-time = "2026-04-20T14:42:03.962Z" }, - { url = "https://files.pythonhosted.org/packages/24/ac/66cd45129e3915e5ade3b292cb3bc7fd537f58f8f8dbdaba6170f7cabb74/pydantic_core-2.46.3-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:6e42d83d1c6b87fa56b521479cff237e626a292f3b31b6345c15a99121b454c1", size = 2369842, upload-time = "2026-04-20T14:41:35.52Z" }, - { url = "https://files.pythonhosted.org/packages/a2/51/dd4248abb84113615473aa20d5545b7c4cd73c8644003b5259686f93996c/pydantic_core-2.46.3-cp314-cp314-win32.whl", hash = "sha256:07bc6d2a28c3adb4f7c6ae46aa4f2d2929af127f587ed44057af50bf1ce0f505", size = 1959661, upload-time = "2026-04-20T14:41:00.042Z" }, - { url = "https://files.pythonhosted.org/packages/20/eb/59980e5f1ae54a3b86372bd9f0fa373ea2d402e8cdcd3459334430f91e91/pydantic_core-2.46.3-cp314-cp314-win_amd64.whl", hash = "sha256:8940562319bc621da30714617e6a7eaa6b98c84e8c685bcdc02d7ed5e7c7c44e", size = 2071686, upload-time = "2026-04-20T14:43:16.471Z" }, - { url = "https://files.pythonhosted.org/packages/8c/db/1cf77e5247047dfee34bc01fa9bca134854f528c8eb053e144298893d370/pydantic_core-2.46.3-cp314-cp314-win_arm64.whl", hash = "sha256:5dcbbcf4d22210ced8f837c96db941bdb078f419543472aca5d9a0bb7cddc7df", size = 2026907, upload-time = "2026-04-20T14:43:31.732Z" }, - { url = "https://files.pythonhosted.org/packages/57/c0/b3df9f6a543276eadba0a48487b082ca1f201745329d97dbfa287034a230/pydantic_core-2.46.3-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:d0fe3dce1e836e418f912c1ad91c73357d03e556a4d286f441bf34fed2dbeecf", size = 2095047, upload-time = "2026-04-20T14:42:37.982Z" }, - { url = "https://files.pythonhosted.org/packages/66/57/886a938073b97556c168fd99e1a7305bb363cd30a6d2c76086bf0587b32a/pydantic_core-2.46.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9ce92e58abc722dac1bf835a6798a60b294e48eb0e625ec9fd994b932ac5feee", size = 1934329, upload-time = "2026-04-20T14:43:49.655Z" }, - { url = "https://files.pythonhosted.org/packages/0b/7c/b42eaa5c34b13b07ecb51da21761297a9b8eb43044c864a035999998f328/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a03e6467f0f5ab796a486146d1b887b2dc5e5f9b3288898c1b1c3ad974e53e4a", size = 1974847, upload-time = "2026-04-20T14:42:10.737Z" }, - { url = "https://files.pythonhosted.org/packages/e6/9b/92b42db6543e7de4f99ae977101a2967b63122d4b6cf7773812da2d7d5b5/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2798b6ba041b9d70acfb9071a2ea13c8456dd1e6a5555798e41ba7b0790e329c", size = 2041742, upload-time = "2026-04-20T14:40:44.262Z" }, - { url = "https://files.pythonhosted.org/packages/0f/19/46fbe1efabb5aa2834b43b9454e70f9a83ad9c338c1291e48bdc4fecf167/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9be3e221bdc6d69abf294dcf7aff6af19c31a5cdcc8f0aa3b14be29df4bd03b1", size = 2236235, upload-time = "2026-04-20T14:41:27.307Z" }, - { url = "https://files.pythonhosted.org/packages/77/da/b3f95bc009ad60ec53120f5d16c6faa8cabdbe8a20d83849a1f2b8728148/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f13936129ce841f2a5ddf6f126fea3c43cd128807b5a59588c37cf10178c2e64", size = 2282633, upload-time = "2026-04-20T14:44:33.271Z" }, - { url = "https://files.pythonhosted.org/packages/cc/6e/401336117722e28f32fb8220df676769d28ebdf08f2f4469646d404c43a3/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28b5f2ef03416facccb1c6ef744c69793175fd27e44ef15669201601cf423acb", size = 2109679, upload-time = "2026-04-20T14:44:41.065Z" }, - { url = "https://files.pythonhosted.org/packages/fc/53/b289f9bc8756a32fe718c46f55afaeaf8d489ee18d1a1e7be1db73f42cc4/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:830d1247d77ad23852314f069e9d7ddafeec5f684baf9d7e7065ed46a049c4e6", size = 2108342, upload-time = "2026-04-20T14:42:50.144Z" }, - { url = "https://files.pythonhosted.org/packages/10/5b/8292fc7c1f9111f1b2b7c1b0dcf1179edcd014fc3ea4517499f50b829d71/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0793c90c1a3c74966e7975eaef3ed30ebdff3260a0f815a62a22adc17e4c01c", size = 2157208, upload-time = "2026-04-20T14:42:08.133Z" }, - { url = "https://files.pythonhosted.org/packages/2b/9e/f80044e9ec07580f057a89fc131f78dda7a58751ddf52bbe05eaf31db50f/pydantic_core-2.46.3-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:d2d0aead851b66f5245ec0c4fb2612ef457f8bbafefdf65a2bf9d6bac6140f47", size = 2167237, upload-time = "2026-04-20T14:42:25.412Z" }, - { url = "https://files.pythonhosted.org/packages/f8/84/6781a1b037f3b96be9227edbd1101f6d3946746056231bf4ac48cdff1a8d/pydantic_core-2.46.3-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:2f40e4246676beb31c5ce77c38a55ca4e465c6b38d11ea1bd935420568e0b1ab", size = 2312540, upload-time = "2026-04-20T14:40:40.313Z" }, - { url = "https://files.pythonhosted.org/packages/3e/db/19c0839feeb728e7df03255581f198dfdf1c2aeb1e174a8420b63c5252e5/pydantic_core-2.46.3-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:cf489cf8986c543939aeee17a09c04d6ffb43bfef8ca16fcbcc5cfdcbed24dba", size = 2369556, upload-time = "2026-04-20T14:41:09.427Z" }, - { url = "https://files.pythonhosted.org/packages/e0/15/3228774cb7cd45f5f721ddf1b2242747f4eb834d0c491f0c02d606f09fed/pydantic_core-2.46.3-cp314-cp314t-win32.whl", hash = "sha256:ffe0883b56cfc05798bf994164d2b2ff03efe2d22022a2bb080f3b626176dd56", size = 1949756, upload-time = "2026-04-20T14:41:25.717Z" }, - { url = "https://files.pythonhosted.org/packages/b8/2a/c79cf53fd91e5a87e30d481809f52f9a60dd221e39de66455cf04deaad37/pydantic_core-2.46.3-cp314-cp314t-win_amd64.whl", hash = "sha256:706d9d0ce9cf4593d07270d8e9f53b161f90c57d315aeec4fb4fd7a8b10240d8", size = 2051305, upload-time = "2026-04-20T14:43:18.627Z" }, - { url = "https://files.pythonhosted.org/packages/0b/db/d8182a7f1d9343a032265aae186eb063fe26ca4c40f256b21e8da4498e89/pydantic_core-2.46.3-cp314-cp314t-win_arm64.whl", hash = "sha256:77706aeb41df6a76568434701e0917da10692da28cb69d5fb6919ce5fdb07374", size = 2026310, upload-time = "2026-04-20T14:41:01.778Z" }, - { url = "https://files.pythonhosted.org/packages/66/7f/03dbad45cd3aa9083fbc93c210ae8b005af67e4136a14186950a747c6874/pydantic_core-2.46.3-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:9715525891ed524a0a1eb6d053c74d4d4ad5017677fb00af0b7c2644a31bae46", size = 2105683, upload-time = "2026-04-20T14:42:19.779Z" }, - { url = "https://files.pythonhosted.org/packages/26/22/4dc186ac8ea6b257e9855031f51b62a9637beac4d68ac06bee02f046f836/pydantic_core-2.46.3-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:9d2f400712a99a013aff420ef1eb9be077f8189a36c1e3ef87660b4e1088a874", size = 1940052, upload-time = "2026-04-20T14:43:59.274Z" }, - { url = "https://files.pythonhosted.org/packages/0d/ca/d376391a5aff1f2e8188960d7873543608130a870961c2b6b5236627c116/pydantic_core-2.46.3-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd2aab0e2e9dc2daf36bd2686c982535d5e7b1d930a1344a7bb6e82baab42a76", size = 1988172, upload-time = "2026-04-20T14:41:17.469Z" }, - { url = "https://files.pythonhosted.org/packages/0e/6b/523b9f85c23788755d6ab949329de692a2e3a584bc6beb67fef5e035aa9d/pydantic_core-2.46.3-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e9d76736da5f362fabfeea6a69b13b7f2be405c6d6966f06b2f6bfff7e64531", size = 2128596, upload-time = "2026-04-20T14:40:41.707Z" }, - { url = "https://files.pythonhosted.org/packages/34/42/f426db557e8ab2791bc7562052299944a118655496fbff99914e564c0a94/pydantic_core-2.46.3-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:b12dd51f1187c2eb489af8e20f880362db98e954b54ab792fa5d92e8bcc6b803", size = 2091877, upload-time = "2026-04-20T14:43:27.091Z" }, - { url = "https://files.pythonhosted.org/packages/5c/4f/86a832a9d14df58e663bfdf4627dc00d3317c2bd583c4fb23390b0f04b8e/pydantic_core-2.46.3-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:f00a0961b125f1a47af7bcc17f00782e12f4cd056f83416006b30111d941dfa3", size = 1932428, upload-time = "2026-04-20T14:40:45.781Z" }, - { url = "https://files.pythonhosted.org/packages/11/1a/fe857968954d93fb78e0d4b6df5c988c74c4aaa67181c60be7cfe327c0ca/pydantic_core-2.46.3-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:57697d7c056aca4bbb680200f96563e841a6386ac1129370a0102592f4dddff5", size = 1997550, upload-time = "2026-04-20T14:44:02.425Z" }, - { url = "https://files.pythonhosted.org/packages/17/eb/9d89ad2d9b0ba8cd65393d434471621b98912abb10fbe1df08e480ba57b5/pydantic_core-2.46.3-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd35aa21299def8db7ef4fe5c4ff862941a9a158ca7b63d61e66fe67d30416b4", size = 2137657, upload-time = "2026-04-20T14:42:45.149Z" }, - { url = "https://files.pythonhosted.org/packages/1f/da/99d40830684f81dec901cac521b5b91c095394cc1084b9433393cde1c2df/pydantic_core-2.46.3-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:13afdd885f3d71280cf286b13b310ee0f7ccfefd1dbbb661514a474b726e2f25", size = 2107973, upload-time = "2026-04-20T14:42:06.175Z" }, - { url = "https://files.pythonhosted.org/packages/99/a5/87024121818d75bbb2a98ddbaf638e40e7a18b5e0f5492c9ca4b1b316107/pydantic_core-2.46.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:f91c0aff3e3ee0928edd1232c57f643a7a003e6edf1860bc3afcdc749cb513f3", size = 1947191, upload-time = "2026-04-20T14:43:14.319Z" }, - { url = "https://files.pythonhosted.org/packages/60/62/0c1acfe10945b83a6a59d19fbaa92f48825381509e5701b855c08f13db76/pydantic_core-2.46.3-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6529d1d128321a58d30afcc97b49e98836542f68dd41b33c2e972bb9e5290536", size = 2123791, upload-time = "2026-04-20T14:43:22.766Z" }, - { url = "https://files.pythonhosted.org/packages/75/3e/3b2393b4c8f44285561dc30b00cf307a56a2eff7c483a824db3b8221ca51/pydantic_core-2.46.3-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:975c267cff4f7e7272eacbe50f6cc03ca9a3da4c4fbd66fffd89c94c1e311aa1", size = 2153197, upload-time = "2026-04-20T14:44:27.932Z" }, - { url = "https://files.pythonhosted.org/packages/ba/75/5af02fb35505051eee727c061f2881c555ab4f8ddb2d42da715a42c9731b/pydantic_core-2.46.3-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:2b8e4f2bbdf71415c544b4b1138b8060db7b6611bc927e8064c769f64bed651c", size = 2181073, upload-time = "2026-04-20T14:43:20.729Z" }, - { url = "https://files.pythonhosted.org/packages/10/92/7e0e1bd9ca3c68305db037560ca2876f89b2647deb2f8b6319005de37505/pydantic_core-2.46.3-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:e61ea8e9fff9606d09178f577ff8ccdd7206ff73d6552bcec18e1033c4254b85", size = 2315886, upload-time = "2026-04-20T14:44:04.826Z" }, - { url = "https://files.pythonhosted.org/packages/b8/d8/101655f27eaf3e44558ead736b2795d12500598beed4683f279396fa186e/pydantic_core-2.46.3-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:b504bda01bafc69b6d3c7a0c7f039dcf60f47fab70e06fe23f57b5c75bdc82b8", size = 2360528, upload-time = "2026-04-20T14:40:47.431Z" }, - { url = "https://files.pythonhosted.org/packages/07/0f/1c34a74c8d07136f0d729ffe5e1fdab04fbdaa7684f61a92f92511a84a15/pydantic_core-2.46.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:b00b76f7142fc60c762ce579bd29c8fa44aaa56592dd3c54fab3928d0d4ca6ff", size = 2184144, upload-time = "2026-04-20T14:42:57Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/08/f1ba952f1c8ae5581c70fa9c6da89f247b83e3dd8c09c035d5d7931fc23d/pydantic_core-2.46.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:a396dcc17e5a0b164dbe026896245a4fa9ff402edca1dff0be3d53a517f74de4", size = 2113146, upload-time = "2026-05-06T13:37:36.537Z" }, + { url = "https://files.pythonhosted.org/packages/56/c6/65f646c7ff09bd257f660434adb45c4dfcbbcebcc030562fecf6f5bf887d/pydantic_core-2.46.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:da4b951fe36dc7c3a1ccb4e3cd1747c3542b8c9ceede8fc86cae054e764485f5", size = 1949769, upload-time = "2026-05-06T13:37:46.365Z" }, + { url = "https://files.pythonhosted.org/packages/64/ba/bfb1d928fd5b49e1258935ff104ae356e9fd89384a55bf9f847e9193ad40/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb63e0198ca18aad131c089b9204c23079c3afa95487e561f4c522d519e55aba", size = 1974958, upload-time = "2026-05-06T13:37:28.611Z" }, + { url = "https://files.pythonhosted.org/packages/4e/74/76223bfb117b64af743c9b6670d1364516f5c0604f96b48f3272f6af6cc6/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f47286a97f0bc9b8859519809077b91b2cefe4ae47fcbf5e466a009c1c5d742b", size = 2042118, upload-time = "2026-05-06T13:36:55.216Z" }, + { url = "https://files.pythonhosted.org/packages/cb/7b/848732968bc8f48f3187542f08358b9d842db564147b256669426ebb1652/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:905a0ed8ea6f2d61c1738835f99b699348d7857379083e5fc497fa0c967a407c", size = 2222876, upload-time = "2026-05-06T13:38:25.455Z" }, + { url = "https://files.pythonhosted.org/packages/b5/2f/e90b63ee2e14bd8d3db8f705a6d75d64e6ee1b7c2c8833747ce706e1e0ce/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea793e075b70290d89d8142074262885d3f7da19634845135751bd6344f73b50", size = 2286703, upload-time = "2026-05-06T13:37:53.304Z" }, + { url = "https://files.pythonhosted.org/packages/ba/1e/acc4d70f88a0a277e4a1fa77ebb985ceabaf900430f875bf9338e11c9420/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:395aebd9183f9d112f569aeb5b2214d1a10a33bec8456447f7fbdfa51d38d4cd", size = 2092042, upload-time = "2026-05-06T13:38:46.981Z" }, + { url = "https://files.pythonhosted.org/packages/a9/da/0a422b57bf8504102bf3c4ccea9c41bab5a5cee6a54650acf8faf67f5a24/pydantic_core-2.46.4-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:b078afbc25f3a1436c7a1d2cd3e322497ee99615ba97c563566fdf46aff1ee01", size = 2117231, upload-time = "2026-05-06T13:39:23.146Z" }, + { url = "https://files.pythonhosted.org/packages/bd/2a/2ac13c3af305843e23c5078c53d135656b3f05a2fd78cb7bbbb12e97b473/pydantic_core-2.46.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f747929cf940cddb5b3668a390056ddd5ba2e5010615ea2dcf4f9c4f3ab8791d", size = 2168388, upload-time = "2026-05-06T13:40:08.06Z" }, + { url = "https://files.pythonhosted.org/packages/72/04/2beacf7e1607e93eefe4aed1b4709f079b905fb77530179d4f7c71745f22/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:daa27d92c36f24388fe3ad306b174781c747627f134452e4f128ea00ce1fe8c4", size = 2184769, upload-time = "2026-05-06T13:38:13.901Z" }, + { url = "https://files.pythonhosted.org/packages/9e/29/d2b9fd9f539133548eaf622c06a4ce176cb46ac59f32d0359c4abc0de047/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:19e51f073cd3df251856a8a4189fbdf1de4012c3ebacfb1884f94f1eb406079f", size = 2319312, upload-time = "2026-05-06T13:39:08.24Z" }, + { url = "https://files.pythonhosted.org/packages/7c/af/0f7a5b85fec6075bea96e3ef9187de38fccced0de92c1e7feda8d5cc7bb9/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c1747f85cee84c26985853c6f3d9bd3e75da5212912443fa111c113b9c246f39", size = 2361817, upload-time = "2026-05-06T13:38:43.2Z" }, + { url = "https://files.pythonhosted.org/packages/25/a4/73363fec545fd3ec025490bdda2743c56d0dd5b6266b1a53bbe9e4265375/pydantic_core-2.46.4-cp310-cp310-win32.whl", hash = "sha256:2f84c03c8607173d16b5a854ec68a2f9079ae03237a54fb506d13af47e1d018d", size = 1987085, upload-time = "2026-05-06T13:39:25.497Z" }, + { url = "https://files.pythonhosted.org/packages/01/aa/62f082da2c91fac1c234bc9ee0066257ce83f0604abd72e4c9d5991f2d84/pydantic_core-2.46.4-cp310-cp310-win_amd64.whl", hash = "sha256:8358a950c8909158e3df31538a7e4edc2d7265a7c54b47f0864d9e5bae9dcebf", size = 2074311, upload-time = "2026-05-06T13:39:59.922Z" }, + { url = "https://files.pythonhosted.org/packages/5c/fa/6d7708d2cfc1a832acb6aeb0cd16e801902df8a0f583bb3b4b527fde022e/pydantic_core-2.46.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0e96592440881c74a213e5ad528e2b24d3d4f940de2766bed9010ab1d9e51594", size = 2111872, upload-time = "2026-05-06T13:40:27.596Z" }, + { url = "https://files.pythonhosted.org/packages/ae/6f/aa064a3e74b5745afbdf250594f38e7ead05e2d651bcb35994b9417a0d4d/pydantic_core-2.46.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0d65b8c354be7fb5f720c3caa8bc940bc2d20ce749c8e06135f07f8ed95dd7c", size = 1948255, upload-time = "2026-05-06T13:39:12.574Z" }, + { url = "https://files.pythonhosted.org/packages/43/3a/41114a9f7569b84b4d84e7a018c57c56347dac30c0d4a872946ec4e36c46/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826", size = 1972827, upload-time = "2026-05-06T13:38:19.841Z" }, + { url = "https://files.pythonhosted.org/packages/ef/25/1ab42e8048fe551934d9884e8d64daa7e990ad386f310a15981aeb6a5b08/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9037063db01f09b09e237c282b6792bd4da634b5402c4e7f0c61effed7701a04", size = 2041051, upload-time = "2026-05-06T13:38:10.447Z" }, + { url = "https://files.pythonhosted.org/packages/94/c2/1a934597ddf08da410385b3b7aae91956a5a76c635effef456074fad7e88/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc010ab034c8c7452522748bf937df58020d256ccae0874463d1f4d01758af8e", size = 2221314, upload-time = "2026-05-06T13:40:13.089Z" }, + { url = "https://files.pythonhosted.org/packages/02/6d/9e8ad178c9c4df27ad3c8f25d1fe2a7ab0d2ba0559fad4aee5d3d1f16771/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c5dac79fa1614d1e06ca695109c6105923bd9c7d1d6c918d4e637b7e6b32fd3", size = 2285146, upload-time = "2026-05-06T13:38:59.224Z" }, + { url = "https://files.pythonhosted.org/packages/80/50/540cd3aeefc041beb111125c4bff779831a2111fc6b15a9138cda277d32c/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9fa868638bf362d3d138ea55829cefb3d5f4b0d7f142234382a15e2485dbec4", size = 2089685, upload-time = "2026-05-06T13:38:17.762Z" }, + { url = "https://files.pythonhosted.org/packages/6b/a4/b440ad35f05f6a38f89fa0f149accb3f0e02be94ca5e15f3c449a61b4bc9/pydantic_core-2.46.4-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:17299feefe090f2caa5b8e37222bb5f663e4935a8bfa6931d4102e5df1a9f398", size = 2115420, upload-time = "2026-05-06T13:37:58.195Z" }, + { url = "https://files.pythonhosted.org/packages/99/61/de4f55db8dfd57bfdfa9a12ec90fe1b57c4f41062f7ca86f08586b3e0ac0/pydantic_core-2.46.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4c63ebc82684aa89d9a3bcbd13d515b3be44250dc68dd3bd81526c1cb31286c3", size = 2165122, upload-time = "2026-05-06T13:37:01.167Z" }, + { url = "https://files.pythonhosted.org/packages/f7/52/7c529d7bdb2d1068bd52f51fe32572c8301f9a4febf1948f10639f1436f5/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:aaa2a54443eff1950ba5ddc6b6ccda0d9c84a364276a62f969bdf2a390650848", size = 2182573, upload-time = "2026-05-06T13:38:45.04Z" }, + { url = "https://files.pythonhosted.org/packages/37/b3/7c40325848ba78247f2812dcf9c7274e38cd801820ca6dd9fe63bcfb0eb4/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:18e5ceec2ab67e6d5f1a9085e5a24c9c4e2ac4545730bfe668680bca05e555f3", size = 2317139, upload-time = "2026-05-06T13:37:15.539Z" }, + { url = "https://files.pythonhosted.org/packages/d9/37/f913f81a657c865b75da6c0dbed79876073c2a43b5bd9edbe8da785e4d49/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a0f62d0a58f4e7da165457e995725421e0064f2255d8eccebc49f41bbc23b109", size = 2360433, upload-time = "2026-05-06T13:37:30.099Z" }, + { url = "https://files.pythonhosted.org/packages/c4/67/6acaa1be2567f9256b056d8477158cac7240813956ce86e49deae8e173b4/pydantic_core-2.46.4-cp311-cp311-win32.whl", hash = "sha256:041bde0a48fd37cf71cab1c9d56d3e8625a3793fef1f7dd232b3ff37e978ecda", size = 1985513, upload-time = "2026-05-06T13:38:15.669Z" }, + { url = "https://files.pythonhosted.org/packages/aa/e6/c505f83dfeda9a2e5c995cfd872949e4d05e12f7feb3dca72f633daefa94/pydantic_core-2.46.4-cp311-cp311-win_amd64.whl", hash = "sha256:6f2eeda33a839975441c86a4119e1383c50b47faf0cbb5176985565c6bb02c33", size = 2071114, upload-time = "2026-05-06T13:40:35.416Z" }, + { url = "https://files.pythonhosted.org/packages/0f/da/7a263a96d965d9d0df5e8de8a475f33495451117035b09acb110288c381f/pydantic_core-2.46.4-cp311-cp311-win_arm64.whl", hash = "sha256:14f4c5d6db102bd796a627bbb3a17b4cf4574b9ae861d8b7c9a9661c6dd3362d", size = 2044298, upload-time = "2026-05-06T13:38:29.754Z" }, + { url = "https://files.pythonhosted.org/packages/ce/8c/af022f0af448d7747c5154288d46b5f2bc5f17366eaa0e23e9aa04d59f3b/pydantic_core-2.46.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2", size = 2106158, upload-time = "2026-05-06T13:38:57.215Z" }, + { url = "https://files.pythonhosted.org/packages/19/95/6195171e385007300f0f5574592e467c568becce2d937a0b6804f218bc49/pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f", size = 1951724, upload-time = "2026-05-06T13:37:02.697Z" }, + { url = "https://files.pythonhosted.org/packages/8e/bc/f47d1ff9cbb1620e1b5b697eef06010035735f07820180e74178226b27b3/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7", size = 1975742, upload-time = "2026-05-06T13:37:09.448Z" }, + { url = "https://files.pythonhosted.org/packages/5b/11/9b9a5b0306345664a2da6410877af6e8082481b5884b3ddd78d47c6013ce/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7", size = 2052418, upload-time = "2026-05-06T13:37:38.234Z" }, + { url = "https://files.pythonhosted.org/packages/f1/b7/a65fec226f5d78fc39f4a13c4cc0c768c22b113438f60c14adc9d2865038/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712", size = 2232274, upload-time = "2026-05-06T13:38:27.753Z" }, + { url = "https://files.pythonhosted.org/packages/68/f0/92039db98b907ef49269a8271f67db9cb78ae2fc68062ef7e4e77adb5f61/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4", size = 2309940, upload-time = "2026-05-06T13:38:05.353Z" }, + { url = "https://files.pythonhosted.org/packages/5f/97/2aab507d3d00ca626e8e57c1eac6a79e4e5fbcc63eb99733ff55d1717f65/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce", size = 2094516, upload-time = "2026-05-06T13:39:10.577Z" }, + { url = "https://files.pythonhosted.org/packages/22/37/a8aca44d40d737dde2bc05b3c6c07dff0de07ce6f82e9f3167aeaf4d5dea/pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987", size = 2136854, upload-time = "2026-05-06T13:40:22.59Z" }, + { url = "https://files.pythonhosted.org/packages/24/99/fcef1b79238c06a8cbec70819ac722ba76e02bc8ada9b0fd66eba40da01b/pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b", size = 2180306, upload-time = "2026-05-06T13:40:10.666Z" }, + { url = "https://files.pythonhosted.org/packages/ae/6c/fc44000918855b42779d007ae63b0532794739027b2f417321cddbc44f6a/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458", size = 2190044, upload-time = "2026-05-06T13:40:43.231Z" }, + { url = "https://files.pythonhosted.org/packages/6b/65/d9cadc9f1920d7a127ad2edba16c1db7916e59719285cd6c94600b0080ba/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b", size = 2329133, upload-time = "2026-05-06T13:39:57.365Z" }, + { url = "https://files.pythonhosted.org/packages/d0/cf/c873d91679f3a30bcf5e7ac280ce5573483e72295307685120d0d5ad3416/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c", size = 2374464, upload-time = "2026-05-06T13:38:06.976Z" }, + { url = "https://files.pythonhosted.org/packages/47/bd/6f2fc8188f31bf10590f1e98e7b306336161fac930a8c514cd7bd828c7dc/pydantic_core-2.46.4-cp312-cp312-win32.whl", hash = "sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894", size = 1974823, upload-time = "2026-05-06T13:40:47.985Z" }, + { url = "https://files.pythonhosted.org/packages/40/8c/985c1d41ea1107c2534abd9870e4ed5c8e7669b5c308297835c001e7a1c4/pydantic_core-2.46.4-cp312-cp312-win_amd64.whl", hash = "sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89", size = 2072919, upload-time = "2026-05-06T13:39:21.153Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ba/f463d006e0c47373ca7ec5e1a261c59dc01ef4d62b2657af925fb0deee3a/pydantic_core-2.46.4-cp312-cp312-win_arm64.whl", hash = "sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a", size = 2027604, upload-time = "2026-05-06T13:39:03.753Z" }, + { url = "https://files.pythonhosted.org/packages/51/a2/5d30b469c5267a17b39dec53208222f76a8d351dfac4af661888c5aee77d/pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008", size = 2106306, upload-time = "2026-05-06T13:37:48.029Z" }, + { url = "https://files.pythonhosted.org/packages/c1/81/4fa520eaffa8bd7d1525e644cd6d39e7d60b1592bc5b516693c7340b50f1/pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4", size = 1951906, upload-time = "2026-05-06T13:37:17.012Z" }, + { url = "https://files.pythonhosted.org/packages/03/d5/fd02da45b659668b05923b17ba3a0100a0a3d5541e3bd8fcc4ecb711309e/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76", size = 1976802, upload-time = "2026-05-06T13:37:35.113Z" }, + { url = "https://files.pythonhosted.org/packages/21/f2/95727e1368be3d3ed485eaab7adbd7dda408f33f7a36e8b48e0144002b91/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3", size = 2052446, upload-time = "2026-05-06T13:37:12.313Z" }, + { url = "https://files.pythonhosted.org/packages/9c/86/5d99feea3f77c7234b8718075b23db11532773c1a0dbd9b9490215dc2eeb/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76", size = 2232757, upload-time = "2026-05-06T13:39:01.149Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3a/508ac615935ef7588cf6d9e9b91309fdc2da751af865e02a9098de88258c/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4", size = 2309275, upload-time = "2026-05-06T13:37:41.406Z" }, + { url = "https://files.pythonhosted.org/packages/07/f8/41db9de19d7987d6b04715a02b3b40aea467000275d9d758ffaa31af7d50/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a", size = 2094467, upload-time = "2026-05-06T13:39:18.847Z" }, + { url = "https://files.pythonhosted.org/packages/2c/e2/f35033184cb11d0052daf4416e8e10a502ea2ac006fc4f459aee872727d1/pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262", size = 2134417, upload-time = "2026-05-06T13:40:17.944Z" }, + { url = "https://files.pythonhosted.org/packages/7e/7b/6ceeb1cc90e193862f444ebe373d8fdf613f0a82572dde03fb10734c6c71/pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e", size = 2179782, upload-time = "2026-05-06T13:40:32.618Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f2/c8d7773ede6af08036423a00ae0ceffce266c3c52a096c435d68c896083f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd", size = 2188782, upload-time = "2026-05-06T13:36:51.018Z" }, + { url = "https://files.pythonhosted.org/packages/59/31/0c864784e31f09f05cdd87606f08923b9c9e7f6e51dd27f20f62f975ce9f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be", size = 2328334, upload-time = "2026-05-06T13:40:37.764Z" }, + { url = "https://files.pythonhosted.org/packages/c2/eb/4f6c8a41efa30baa755590f4141abf3a8c370fab610915733e74134a7270/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d", size = 2372986, upload-time = "2026-05-06T13:39:34.152Z" }, + { url = "https://files.pythonhosted.org/packages/5b/24/b375a480d53113860c299764bfe9f349a3dc9108b3adc0d7f0d786492ebf/pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb", size = 1973693, upload-time = "2026-05-06T13:37:55.072Z" }, + { url = "https://files.pythonhosted.org/packages/7e/e8/cff247591966f2d22ec8c003cd7587e27b7ba7b81ab2fb888e3ab75dc285/pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292", size = 2071819, upload-time = "2026-05-06T13:38:49.139Z" }, + { url = "https://files.pythonhosted.org/packages/c6/1a/f4aee670d5670e9e148e0c82c7db98d780be566c6e6a97ee8035528ca0b3/pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d", size = 2027411, upload-time = "2026-05-06T13:40:45.796Z" }, + { url = "https://files.pythonhosted.org/packages/8d/74/228a26ddad29c6672b805d9fd78e8d251cd04004fa7eed0e622096cd0250/pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb", size = 2102079, upload-time = "2026-05-06T13:38:41.019Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462", size = 1952179, upload-time = "2026-05-06T13:36:59.812Z" }, + { url = "https://files.pythonhosted.org/packages/95/30/5211a831ae054928054b2f79731661087a2bc5c01e825c672b3a4a8f1b3e/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9", size = 1978926, upload-time = "2026-05-06T13:37:39.933Z" }, + { url = "https://files.pythonhosted.org/packages/57/e9/689668733b1eb67adeef047db3c2e8788fcf65a7fd9c9e2b46b7744fe245/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4", size = 2046785, upload-time = "2026-05-06T13:38:01.995Z" }, + { url = "https://files.pythonhosted.org/packages/60/d9/6715260422ff50a2109878fd24d948a6c3446bb2664f34ee78cd972b3acd/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914", size = 2228733, upload-time = "2026-05-06T13:40:50.371Z" }, + { url = "https://files.pythonhosted.org/packages/18/ae/fdb2f64316afca925640f8e70bb1a564b0ec2721c1389e25b8eb4bf9a299/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28", size = 2307534, upload-time = "2026-05-06T13:37:21.531Z" }, + { url = "https://files.pythonhosted.org/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b", size = 2099732, upload-time = "2026-05-06T13:39:31.942Z" }, + { url = "https://files.pythonhosted.org/packages/06/d5/ee5a3366637fee41dee51a1fc91562dcf12ddbc68fda34e6b253da2324bb/pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c", size = 2129627, upload-time = "2026-05-06T13:37:25.033Z" }, + { url = "https://files.pythonhosted.org/packages/94/33/2414be571d2c6a6c4d08be21f9292b6d3fdb08949a97b6dfe985017821db/pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb", size = 2179141, upload-time = "2026-05-06T13:37:14.046Z" }, + { url = "https://files.pythonhosted.org/packages/7b/79/7daa95be995be0eecc4cf75064cb33f9bbbfe3fe0158caf2f0d4a996a5c7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898", size = 2184325, upload-time = "2026-05-06T13:36:53.615Z" }, + { url = "https://files.pythonhosted.org/packages/9f/cb/d0a382f5c0de8a222dc61c65348e0ce831b1f68e0a018450d31c2cace3a5/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e", size = 2323990, upload-time = "2026-05-06T13:40:29.971Z" }, + { url = "https://files.pythonhosted.org/packages/05/db/d9ba624cc4a5aced1598e88c04fdbd8310c8a69b9d38b9a3d39ce3a61ed7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519", size = 2369978, upload-time = "2026-05-06T13:37:23.027Z" }, + { url = "https://files.pythonhosted.org/packages/f2/20/d15df15ba918c423461905802bfd2981c3af0bfa0e40d05e13edbfa48bc3/pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4", size = 1966354, upload-time = "2026-05-06T13:38:03.499Z" }, + { url = "https://files.pythonhosted.org/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac", size = 2072238, upload-time = "2026-05-06T13:39:40.807Z" }, + { url = "https://files.pythonhosted.org/packages/32/36/51eb763beec1f4cf59b1db243a7dcc39cbb41230f050a09b9d69faaf0a48/pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a", size = 2018251, upload-time = "2026-05-06T13:37:26.72Z" }, + { url = "https://files.pythonhosted.org/packages/e8/91/855af51d625b23aa987116a19e231d2aaef9c4a415273ddc189b79a45fee/pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0", size = 2099593, upload-time = "2026-05-06T13:39:47.682Z" }, + { url = "https://files.pythonhosted.org/packages/fb/1b/8784a54c65edb5f49f0a14d6977cf1b209bba85a4c77445b255c2de58ab3/pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d", size = 1935226, upload-time = "2026-05-06T13:40:40.428Z" }, + { url = "https://files.pythonhosted.org/packages/e8/e7/1955d28d1afc56dd4b3ad7cc0cf39df1b9852964cf16e5d13912756d6d6b/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b", size = 1974605, upload-time = "2026-05-06T13:37:32.029Z" }, + { url = "https://files.pythonhosted.org/packages/93/e2/3fedbf0ba7a22850e6e9fd78117f1c0f10f950182344d8a6c535d468fdd8/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000", size = 2030777, upload-time = "2026-05-06T13:38:55.239Z" }, + { url = "https://files.pythonhosted.org/packages/f8/61/46be275fcaaba0b4f5b9669dd852267ce1ff616592dccf7a7845588df091/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e", size = 2236641, upload-time = "2026-05-06T13:37:08.096Z" }, + { url = "https://files.pythonhosted.org/packages/60/db/12e93e46a8bac9988be3c016860f83293daea8c716c029c9ace279036f2f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd", size = 2286404, upload-time = "2026-05-06T13:40:20.221Z" }, + { url = "https://files.pythonhosted.org/packages/e2/4a/4d8b19008f38d31c53b8219cfedc2e3d5de5fe99d90076b7e767de29274f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3", size = 2109219, upload-time = "2026-05-06T13:38:12.153Z" }, + { url = "https://files.pythonhosted.org/packages/88/70/3cbc40978fefb7bb09c6708d40d4ad1a5d70fd7213c3d17f971de868ec1f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7", size = 2110594, upload-time = "2026-05-06T13:40:02.971Z" }, + { url = "https://files.pythonhosted.org/packages/9d/20/b8d36736216e29491125531685b2f9e61aa5b4b2599893f8268551da3338/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff", size = 2159542, upload-time = "2026-05-06T13:39:27.506Z" }, + { url = "https://files.pythonhosted.org/packages/1d/a2/367df868eb584dacf6bf82a389272406d7178e301c4ac82545ab98bc2dd9/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424", size = 2168146, upload-time = "2026-05-06T13:38:31.93Z" }, + { url = "https://files.pythonhosted.org/packages/c1/b8/4460f77f7e201893f649a29ab355dddd3beee8a97bcb1a320db414f9a06e/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6", size = 2306309, upload-time = "2026-05-06T13:37:44.717Z" }, + { url = "https://files.pythonhosted.org/packages/64/c4/be2639293acd87dc8ddbcec41a73cee9b2ebf996fe6d892a1a74e88ad3f7/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565", size = 2369736, upload-time = "2026-05-06T13:37:05.645Z" }, + { url = "https://files.pythonhosted.org/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02", size = 1955575, upload-time = "2026-05-06T13:38:51.116Z" }, + { url = "https://files.pythonhosted.org/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5", size = 2051624, upload-time = "2026-05-06T13:38:21.672Z" }, + { url = "https://files.pythonhosted.org/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325, upload-time = "2026-05-06T13:40:52.723Z" }, + { url = "https://files.pythonhosted.org/packages/ee/a4/73995fd4ebbb46ba0ee51e6fa049b8f02c40daebb762208feda8a6b7894d/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:14d4edf427bdcf950a8a02d7cb44a08614388dd6e1bdcbf4f67504fa7887da9c", size = 2111589, upload-time = "2026-05-06T13:37:10.817Z" }, + { url = "https://files.pythonhosted.org/packages/fb/7f/f37d3a5e8bfcc2e403f5c57a730f2d815693fb42119e8ea48b3789335af1/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0ce40cd7b21210e99342afafbd4d0f76d784eb5b1d60f3bdc566be4983c6c73b", size = 1944552, upload-time = "2026-05-06T13:36:56.717Z" }, + { url = "https://files.pythonhosted.org/packages/15/3c/d7eb777b3ff43e8433a4efb39a17aa8fd98a4ee8561a24a67ef5db07b2d6/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90884113d8b48f760e9587002789ddd741e76ab9f89518cd1e43b1f1a52ec44b", size = 1982984, upload-time = "2026-05-06T13:39:06.207Z" }, + { url = "https://files.pythonhosted.org/packages/63/87/70b9f40170a81afd55ca26c9b2acb25c20d64bcfbf888fafecb3ba077d4c/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66ce7632c22d837c95301830e111ad0128a32b8207533b60896a96c4915192ea", size = 2138417, upload-time = "2026-05-06T13:39:45.476Z" }, + { url = "https://files.pythonhosted.org/packages/9d/1d/8987ad40f65ae1432753072f214fb5c74fe47ffbd0698bb9cbbb585664f8/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7", size = 2095527, upload-time = "2026-05-06T13:39:52.283Z" }, + { url = "https://files.pythonhosted.org/packages/64/d3/84c282a7eee1d3ac4c0377546ef5a1ea436ce26840d9ac3b7ed54a377507/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df", size = 1936024, upload-time = "2026-05-06T13:40:15.671Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ca/eac61596cdeb4d7e174d3dc0bd8a6238f14f75f97a24e7b7db4c7e7340a0/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526", size = 1990696, upload-time = "2026-05-06T13:38:34.717Z" }, + { url = "https://files.pythonhosted.org/packages/fa/c3/7c8b240552251faf6b3a957db200fcfbbcec36763c050428b601e0c9b83b/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0", size = 2147590, upload-time = "2026-05-06T13:39:29.883Z" }, + { url = "https://files.pythonhosted.org/packages/11/cb/428de0385b6c8d44b716feba566abfacfbd23ee3c4439faa789a1456242f/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:0c563b08bca408dc7f65f700633d8442fffb2421fc47b8101377e9fd65051ff0", size = 2112782, upload-time = "2026-05-06T13:37:04.016Z" }, + { url = "https://files.pythonhosted.org/packages/0b/b5/6a17bdadd0fc1f170adfd05a20d37c832f52b117b4d9131da1f41bb097ce/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:db06ffe51636ffe9ca531fe9023dd64bdd794be8754cb5df57c5498ae5b518a7", size = 1952146, upload-time = "2026-05-06T13:39:43.092Z" }, + { url = "https://files.pythonhosted.org/packages/2a/dc/03734d80e362cd43ef65428e9de77c730ce7f2f11c60d2b1e1b39f0fbf99/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:133878133d271ade3d41d1bfb2a45ec38dbdbda40bc065921c6b04e4630127e2", size = 2134492, upload-time = "2026-05-06T13:36:58.124Z" }, + { url = "https://files.pythonhosted.org/packages/de/df/5e5ffc085ed07cc22d298134d3d911c63e91f6a0eb91fe646750a3209910/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9bc519fbf2b7578398853d815009ae5e4d4603d12f4e3f91da8c06852d3da3e9", size = 2156604, upload-time = "2026-05-06T13:37:49.88Z" }, + { url = "https://files.pythonhosted.org/packages/81/44/6e112a4253e56f5705467cbab7ab5e91ee7398ba3d56d358635958893d3e/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c7a7bd4e39e8e4c12c39cd480356842b6a8a06e41b23a55a5e3e191718838ddf", size = 2183828, upload-time = "2026-05-06T13:37:43.053Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ad/5565071e937d8e752842ac241463944c9eb14c87e2d269f2658a5bd05e98/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:d396ec2b979760aaf3218e76c24e65bd0aca24983298653b3a9d7a45f9e47b30", size = 2310000, upload-time = "2026-05-06T13:37:56.694Z" }, + { url = "https://files.pythonhosted.org/packages/4f/c3/66883a5cec183e7fba4d024b4cbbe61851a63750ef606b0afecc46d1f2bf/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:86e1a4418c6cd97d60c95c71164158eaf7324fae7b0923264016baa993eba6fc", size = 2361286, upload-time = "2026-05-06T13:40:05.667Z" }, + { url = "https://files.pythonhosted.org/packages/4b/2d/69abac8f838090bbecd5df894befb2c2619e7996a98ddb949db9f3b93225/pydantic_core-2.46.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983", size = 2193071, upload-time = "2026-05-06T13:38:08.682Z" }, ] [[package]] @@ -2027,15 +2358,15 @@ wheels = [ [[package]] name = "pyinstaller-hooks-contrib" -version = "2026.4" +version = "2026.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "packaging" }, { name = "setuptools" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c7/fe/9278c29394bf69169febc21f96b4252c3ee7c8ec22c2fc545004bed47e71/pyinstaller_hooks_contrib-2026.4.tar.gz", hash = "sha256:766c281acb1ecc32e21c8c667056d7ebf5da0aabd5e30c219f9c2a283620eeaa", size = 173050, upload-time = "2026-03-31T14:10:51.188Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1a/67/f4452d68793fb15beba4f19ef39a38a8822f0da7452b503c400d5a21f5c1/pyinstaller_hooks_contrib-2026.5.tar.gz", hash = "sha256:f066dfca8f7c45ff6336c9cf9fe25b4e48bfeb322a1aa24faaedfb8a8d1b0b08", size = 173689, upload-time = "2026-05-04T22:36:55.124Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/88/f4/035fb8c06deff827f540a9a4ed9122c54e5376fca3e42eddf0c263730775/pyinstaller_hooks_contrib-2026.4-py3-none-any.whl", hash = "sha256:1de1a5e49a878122010b88c7e295502bc69776c157c4a4dc78741a4e6178b00f", size = 455496, upload-time = "2026-03-31T14:10:49.867Z" }, + { url = "https://files.pythonhosted.org/packages/f6/5c/fd465d11da4d12b50d7eb5d2ee2ceb780d8d049dbb489f3828d131e387af/pyinstaller_hooks_contrib-2026.5-py3-none-any.whl", hash = "sha256:ea1535783fbdac4626351709e83f3ea80b681d3a4745763ebb407b5e27342eb9", size = 457314, upload-time = "2026-05-04T22:36:53.598Z" }, ] [[package]] @@ -2052,15 +2383,15 @@ wheels = [ [[package]] name = "pyopenssl" -version = "26.1.0" +version = "26.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cryptography" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8c/a8/26d36401e3ab8eed9030ad33f381da7856fcfad5691780fccd1b019718fc/pyopenssl-26.1.0.tar.gz", hash = "sha256:737f0a2275c5bc54f3b02137687e1a765931fb3949b9a92a825e4d33b9eec08b", size = 186181, upload-time = "2026-04-24T20:23:48.115Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1a/51/27a5ad5f939d08f690a326ef9582cda7140555180db71695f6fb747d6a36/pyopenssl-26.2.0.tar.gz", hash = "sha256:8c6fcecd1183a7fc897548dfe388b0cdb7f37e018200d8409cf33959dbe35387", size = 182195, upload-time = "2026-05-04T23:06:09.72Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a8/41/52f3a3e812b816a91e89aa504199d8bf989a1f873192b10762be66cf2009/pyopenssl-26.1.0-py3-none-any.whl", hash = "sha256:115563879b2c8ccb207975705d3e491434d8c9d7c79667c902ecbf5f3bbd2ece", size = 58109, upload-time = "2026-04-24T20:23:46.273Z" }, + { url = "https://files.pythonhosted.org/packages/73/b8/a0e2790ae249d6f38c9f66de7a211621a7ab2650217bcd04e1262f578a56/pyopenssl-26.2.0-py3-none-any.whl", hash = "sha256:4f9d971bc5298b8bc1fab282803da04bf000c755d4ad9d99b52de2569ca19a70", size = 55823, upload-time = "2026-05-04T23:06:08.395Z" }, ] [[package]] @@ -2127,15 +2458,15 @@ wheels = [ [[package]] name = "python-discovery" -version = "1.2.2" +version = "1.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "filelock" }, { name = "platformdirs" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/de/ef/3bae0e537cfe91e8431efcba4434463d2c5a65f5a89edd47c6cf2f03c55f/python_discovery-1.2.2.tar.gz", hash = "sha256:876e9c57139eb757cb5878cbdd9ae5379e5d96266c99ef731119e04fffe533bb", size = 58872, upload-time = "2026-04-07T17:28:49.249Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/e0/cc5a8653e9a24f6cf84768f05064aa8ed5a83dcefd5e2a043db14a1c5f44/python_discovery-1.3.0.tar.gz", hash = "sha256:d098f1e86be5d45fe4d14bf1029294aabbd332f4321179dec85e76cddce834b0", size = 63925, upload-time = "2026-05-05T14:38:39.769Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d8/db/795879cc3ddfe338599bddea6388cc5100b088db0a4caf6e6c1af1c27e04/python_discovery-1.2.2-py3-none-any.whl", hash = "sha256:e1ae95d9af875e78f15e19aed0c6137ab1bb49c200f21f5061786490c9585c7a", size = 31894, upload-time = "2026-04-07T17:28:48.09Z" }, + { url = "https://files.pythonhosted.org/packages/30/d4/24d543ab8b8158b7f5a97113c831205f5c900c92c8762b1e7f44b7ea0405/python_discovery-1.3.0-py3-none-any.whl", hash = "sha256:441d9ced3dfce36e113beb35ca302c71c7ef06f3c0f9c227a0b9bb3bd49b9e9f", size = 33124, upload-time = "2026-05-05T14:38:38.539Z" }, ] [[package]] @@ -2147,13 +2478,52 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, ] +[[package]] +name = "pytokens" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b6/34/b4e015b99031667a7b960f888889c5bd34ef585c85e1cb56a594b92836ac/pytokens-0.4.1.tar.gz", hash = "sha256:292052fe80923aae2260c073f822ceba21f3872ced9a68bb7953b348e561179a", size = 23015, upload-time = "2026-01-30T01:03:45.924Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/24/f206113e05cb8ef51b3850e7ef88f20da6f4bf932190ceb48bd3da103e10/pytokens-0.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2a44ed93ea23415c54f3face3b65ef2b844d96aeb3455b8a69b3df6beab6acc5", size = 161522, upload-time = "2026-01-30T01:02:50.393Z" }, + { url = "https://files.pythonhosted.org/packages/d4/e9/06a6bf1b90c2ed81a9c7d2544232fe5d2891d1cd480e8a1809ca354a8eb2/pytokens-0.4.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:add8bf86b71a5d9fb5b89f023a80b791e04fba57960aa790cc6125f7f1d39dfe", size = 246945, upload-time = "2026-01-30T01:02:52.399Z" }, + { url = "https://files.pythonhosted.org/packages/69/66/f6fb1007a4c3d8b682d5d65b7c1fb33257587a5f782647091e3408abe0b8/pytokens-0.4.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:670d286910b531c7b7e3c0b453fd8156f250adb140146d234a82219459b9640c", size = 259525, upload-time = "2026-01-30T01:02:53.737Z" }, + { url = "https://files.pythonhosted.org/packages/04/92/086f89b4d622a18418bac74ab5db7f68cf0c21cf7cc92de6c7b919d76c88/pytokens-0.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:4e691d7f5186bd2842c14813f79f8884bb03f5995f0575272009982c5ac6c0f7", size = 262693, upload-time = "2026-01-30T01:02:54.871Z" }, + { url = "https://files.pythonhosted.org/packages/b4/7b/8b31c347cf94a3f900bdde750b2e9131575a61fdb620d3d3c75832262137/pytokens-0.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:27b83ad28825978742beef057bfe406ad6ed524b2d28c252c5de7b4a6dd48fa2", size = 103567, upload-time = "2026-01-30T01:02:56.414Z" }, + { url = "https://files.pythonhosted.org/packages/3d/92/790ebe03f07b57e53b10884c329b9a1a308648fc083a6d4a39a10a28c8fc/pytokens-0.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d70e77c55ae8380c91c0c18dea05951482e263982911fc7410b1ffd1dadd3440", size = 160864, upload-time = "2026-01-30T01:02:57.882Z" }, + { url = "https://files.pythonhosted.org/packages/13/25/a4f555281d975bfdd1eba731450e2fe3a95870274da73fb12c40aeae7625/pytokens-0.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a58d057208cb9075c144950d789511220b07636dd2e4708d5645d24de666bdc", size = 248565, upload-time = "2026-01-30T01:02:59.912Z" }, + { url = "https://files.pythonhosted.org/packages/17/50/bc0394b4ad5b1601be22fa43652173d47e4c9efbf0044c62e9a59b747c56/pytokens-0.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b49750419d300e2b5a3813cf229d4e5a4c728dae470bcc89867a9ad6f25a722d", size = 260824, upload-time = "2026-01-30T01:03:01.471Z" }, + { url = "https://files.pythonhosted.org/packages/4e/54/3e04f9d92a4be4fc6c80016bc396b923d2a6933ae94b5f557c939c460ee0/pytokens-0.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d9907d61f15bf7261d7e775bd5d7ee4d2930e04424bab1972591918497623a16", size = 264075, upload-time = "2026-01-30T01:03:04.143Z" }, + { url = "https://files.pythonhosted.org/packages/d1/1b/44b0326cb5470a4375f37988aea5d61b5cc52407143303015ebee94abfd6/pytokens-0.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:ee44d0f85b803321710f9239f335aafe16553b39106384cef8e6de40cb4ef2f6", size = 103323, upload-time = "2026-01-30T01:03:05.412Z" }, + { url = "https://files.pythonhosted.org/packages/41/5d/e44573011401fb82e9d51e97f1290ceb377800fb4eed650b96f4753b499c/pytokens-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:140709331e846b728475786df8aeb27d24f48cbcf7bcd449f8de75cae7a45083", size = 160663, upload-time = "2026-01-30T01:03:06.473Z" }, + { url = "https://files.pythonhosted.org/packages/f0/e6/5bbc3019f8e6f21d09c41f8b8654536117e5e211a85d89212d59cbdab381/pytokens-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d6c4268598f762bc8e91f5dbf2ab2f61f7b95bdc07953b602db879b3c8c18e1", size = 255626, upload-time = "2026-01-30T01:03:08.177Z" }, + { url = "https://files.pythonhosted.org/packages/bf/3c/2d5297d82286f6f3d92770289fd439956b201c0a4fc7e72efb9b2293758e/pytokens-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:24afde1f53d95348b5a0eb19488661147285ca4dd7ed752bbc3e1c6242a304d1", size = 269779, upload-time = "2026-01-30T01:03:09.756Z" }, + { url = "https://files.pythonhosted.org/packages/20/01/7436e9ad693cebda0551203e0bf28f7669976c60ad07d6402098208476de/pytokens-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5ad948d085ed6c16413eb5fec6b3e02fa00dc29a2534f088d3302c47eb59adf9", size = 268076, upload-time = "2026-01-30T01:03:10.957Z" }, + { url = "https://files.pythonhosted.org/packages/2e/df/533c82a3c752ba13ae7ef238b7f8cdd272cf1475f03c63ac6cf3fcfb00b6/pytokens-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:3f901fe783e06e48e8cbdc82d631fca8f118333798193e026a50ce1b3757ea68", size = 103552, upload-time = "2026-01-30T01:03:12.066Z" }, + { url = "https://files.pythonhosted.org/packages/cb/dc/08b1a080372afda3cceb4f3c0a7ba2bde9d6a5241f1edb02a22a019ee147/pytokens-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8bdb9d0ce90cbf99c525e75a2fa415144fd570a1ba987380190e8b786bc6ef9b", size = 160720, upload-time = "2026-01-30T01:03:13.843Z" }, + { url = "https://files.pythonhosted.org/packages/64/0c/41ea22205da480837a700e395507e6a24425151dfb7ead73343d6e2d7ffe/pytokens-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5502408cab1cb18e128570f8d598981c68a50d0cbd7c61312a90507cd3a1276f", size = 254204, upload-time = "2026-01-30T01:03:14.886Z" }, + { url = "https://files.pythonhosted.org/packages/e0/d2/afe5c7f8607018beb99971489dbb846508f1b8f351fcefc225fcf4b2adc0/pytokens-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29d1d8fb1030af4d231789959f21821ab6325e463f0503a61d204343c9b355d1", size = 268423, upload-time = "2026-01-30T01:03:15.936Z" }, + { url = "https://files.pythonhosted.org/packages/68/d4/00ffdbd370410c04e9591da9220a68dc1693ef7499173eb3e30d06e05ed1/pytokens-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:970b08dd6b86058b6dc07efe9e98414f5102974716232d10f32ff39701e841c4", size = 266859, upload-time = "2026-01-30T01:03:17.458Z" }, + { url = "https://files.pythonhosted.org/packages/a7/c9/c3161313b4ca0c601eeefabd3d3b576edaa9afdefd32da97210700e47652/pytokens-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:9bd7d7f544d362576be74f9d5901a22f317efc20046efe2034dced238cbbfe78", size = 103520, upload-time = "2026-01-30T01:03:18.652Z" }, + { url = "https://files.pythonhosted.org/packages/8f/a7/b470f672e6fc5fee0a01d9e75005a0e617e162381974213a945fcd274843/pytokens-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4a14d5f5fc78ce85e426aa159489e2d5961acf0e47575e08f35584009178e321", size = 160821, upload-time = "2026-01-30T01:03:19.684Z" }, + { url = "https://files.pythonhosted.org/packages/80/98/e83a36fe8d170c911f864bfded690d2542bfcfacb9c649d11a9e6eb9dc41/pytokens-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f50fd18543be72da51dd505e2ed20d2228c74e0464e4262e4899797803d7fa", size = 254263, upload-time = "2026-01-30T01:03:20.834Z" }, + { url = "https://files.pythonhosted.org/packages/0f/95/70d7041273890f9f97a24234c00b746e8da86df462620194cef1d411ddeb/pytokens-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc74c035f9bfca0255c1af77ddd2d6ae8419012805453e4b0e7513e17904545d", size = 268071, upload-time = "2026-01-30T01:03:21.888Z" }, + { url = "https://files.pythonhosted.org/packages/da/79/76e6d09ae19c99404656d7db9c35dfd20f2086f3eb6ecb496b5b31163bad/pytokens-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f66a6bbe741bd431f6d741e617e0f39ec7257ca1f89089593479347cc4d13324", size = 271716, upload-time = "2026-01-30T01:03:23.633Z" }, + { url = "https://files.pythonhosted.org/packages/79/37/482e55fa1602e0a7ff012661d8c946bafdc05e480ea5a32f4f7e336d4aa9/pytokens-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:b35d7e5ad269804f6697727702da3c517bb8a5228afa450ab0fa787732055fc9", size = 104539, upload-time = "2026-01-30T01:03:24.788Z" }, + { url = "https://files.pythonhosted.org/packages/30/e8/20e7db907c23f3d63b0be3b8a4fd1927f6da2395f5bcc7f72242bb963dfe/pytokens-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8fcb9ba3709ff77e77f1c7022ff11d13553f3c30299a9fe246a166903e9091eb", size = 168474, upload-time = "2026-01-30T01:03:26.428Z" }, + { url = "https://files.pythonhosted.org/packages/d6/81/88a95ee9fafdd8f5f3452107748fd04c24930d500b9aba9738f3ade642cc/pytokens-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79fc6b8699564e1f9b521582c35435f1bd32dd06822322ec44afdeba666d8cb3", size = 290473, upload-time = "2026-01-30T01:03:27.415Z" }, + { url = "https://files.pythonhosted.org/packages/cf/35/3aa899645e29b6375b4aed9f8d21df219e7c958c4c186b465e42ee0a06bf/pytokens-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d31b97b3de0f61571a124a00ffe9a81fb9939146c122c11060725bd5aea79975", size = 303485, upload-time = "2026-01-30T01:03:28.558Z" }, + { url = "https://files.pythonhosted.org/packages/52/a0/07907b6ff512674d9b201859f7d212298c44933633c946703a20c25e9d81/pytokens-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:967cf6e3fd4adf7de8fc73cd3043754ae79c36475c1c11d514fc72cf5490094a", size = 306698, upload-time = "2026-01-30T01:03:29.653Z" }, + { url = "https://files.pythonhosted.org/packages/39/2a/cbbf9250020a4a8dd53ba83a46c097b69e5eb49dd14e708f496f548c6612/pytokens-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:584c80c24b078eec1e227079d56dc22ff755e0ba8654d8383b2c549107528918", size = 116287, upload-time = "2026-01-30T01:03:30.912Z" }, + { url = "https://files.pythonhosted.org/packages/c6/78/397db326746f0a342855b81216ae1f0a32965deccfd7c830a2dbc66d2483/pytokens-0.4.1-py3-none-any.whl", hash = "sha256:26cef14744a8385f35d0e095dc8b3a7583f6c953c2e3d269c7f82484bf5ad2de", size = 13729, upload-time = "2026-01-30T01:03:45.029Z" }, +] + [[package]] name = "pytz" -version = "2026.1.post1" +version = "2026.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/56/db/b8721d71d945e6a8ac63c0fc900b2067181dbb50805958d4d4661cf7d277/pytz-2026.1.post1.tar.gz", hash = "sha256:3378dde6a0c3d26719182142c56e60c7f9af7e968076f31aae569d72a0358ee1", size = 321088, upload-time = "2026-03-03T07:47:50.683Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ff/46/dd499ec9038423421951e4fad73051febaa13d2df82b4064f87af8b8c0c3/pytz-2026.2.tar.gz", hash = "sha256:0e60b47b29f21574376f218fe21abc009894a2321ea16c6754f3cad6eb7cdd6a", size = 320861, upload-time = "2026-05-04T01:35:29.667Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/10/99/781fe0c827be2742bcc775efefccb3b048a3a9c6ce9aec0cbf4a101677e5/pytz-2026.1.post1-py2.py3-none-any.whl", hash = "sha256:f2fd16142fda348286a75e1a524be810bb05d444e5a081f37f7affc635035f7a", size = 510489, upload-time = "2026-03-03T07:47:49.167Z" }, + { url = "https://files.pythonhosted.org/packages/ec/dd/96da98f892250475bdf2328112d7468abdd4acc7b902b6af23f4ed958ea0/pytz-2026.2-py2.py3-none-any.whl", hash = "sha256:04156e608bee23d3792fd45c94ae47fae1036688e75032eea2e3bf0323d1f126", size = 510141, upload-time = "2026-05-04T01:35:27.408Z" }, ] [[package]] @@ -2533,7 +2903,7 @@ wheels = [ [[package]] name = "sqlalchemy-bigquery" -version = "1.16.0" +version = "1.17.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "google-api-core" }, @@ -2542,9 +2912,9 @@ dependencies = [ { name = "packaging" }, { name = "sqlalchemy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7e/6a/c49932b3d9c44cab9202b1866c5b36b7f0d0455d4653fbc0af4466aeaa76/sqlalchemy_bigquery-1.16.0.tar.gz", hash = "sha256:fe937a0d1f4cf7219fcf5d4995c6718805b38d4df43e29398dec5dc7b6d1987e", size = 119632, upload-time = "2025-11-06T01:35:40.373Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/94/6fd01b23a92a2372a71cd1670302a6c11b138ad80906914433e6ddbc1e1a/sqlalchemy_bigquery-1.17.0.tar.gz", hash = "sha256:472284546a0c79cbf99b1bb0f5f99c5131fa888ea25d2d53208e6863e5094e2f", size = 119746, upload-time = "2026-05-07T08:04:51.805Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c0/87/11e6de00ef7949bb8ea06b55304a1a4911c329fdf0d9882b464db240c2c5/sqlalchemy_bigquery-1.16.0-py3-none-any.whl", hash = "sha256:0fe7634cd954f3e74f5e2db6d159f9e5ee87a47fbe8d52eac3cd3bb3dadb3a77", size = 40615, upload-time = "2025-11-06T01:35:39.358Z" }, + { url = "https://files.pythonhosted.org/packages/a3/bf/64ae26c6b58665b76abee9f7e536cef0e886c37e1da0b18f75133ff2fa4d/sqlalchemy_bigquery-1.17.0-py3-none-any.whl", hash = "sha256:89c1d4fc9f045ce762c93bf4b73a6c51a203dcf0dbe2d9ade540c7c5e3ed01dd", size = 39802, upload-time = "2026-05-07T08:03:33.787Z" }, ] [[package]] @@ -2688,35 +3058,47 @@ wheels = [ [[package]] name = "traitlets" -version = "5.14.3" +version = "5.15.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/eb/79/72064e6a701c2183016abbbfedaba506d81e30e232a68c9f0d6f6fcd1574/traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7", size = 161621, upload-time = "2024-04-19T11:11:49.746Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/22/40f55b26baeab80c2d7b3f1db0682f8954e4617fee7d90ce634022ef05c6/traitlets-5.15.0.tar.gz", hash = "sha256:4fead733f81cf1c4c938e06f8ca4633896833c9d89eff878159457f4d4392971", size = 163197, upload-time = "2026-05-06T08:05:58.016Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f", size = 85359, upload-time = "2024-04-19T11:11:46.763Z" }, + { url = "https://files.pythonhosted.org/packages/da/98/a9937a969d018a23badfea0b381f66783649d48e0ea6c41923265c3cbeb3/traitlets-5.15.0-py3-none-any.whl", hash = "sha256:fb36a18867a6803deab09f3c5e0fa81bb7b26a5c9e82501c9933f759166eff40", size = 85877, upload-time = "2026-05-06T08:05:55.853Z" }, ] [[package]] name = "ty" -version = "0.0.33" +version = "0.0.34" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c4/69/e24eefe2c35c0fdbdec9b60e162727af669bb76d64d993d982eb67b24c38/ty-0.0.34.tar.gz", hash = "sha256:a6efe66b0f13c03a65e6c72ec9abfe2792e2fd063c74fa67e2c4930e29d661be", size = 5585933, upload-time = "2026-05-01T23:06:46.388Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/7b/8b85003d6639ef17a97dcbb31f4511cfe78f1c81a964470db100c8c883e7/ty-0.0.34-py3-none-linux_armv6l.whl", hash = "sha256:9ecc3d14f07a95a6ceb88e07f8e62358dbd37325d3d5bd56da7217ff1fef7fb8", size = 11067094, upload-time = "2026-05-01T23:06:21.133Z" }, + { url = "https://files.pythonhosted.org/packages/d7/25/b0098f65b020b015c40567c763fc66fffbec88b2ba6f584bca1e92f05ebb/ty-0.0.34-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:0dccffd8a9d02321cd2dee3249df205e26d62694e741f4eeca36b157fd8b419f", size = 10840909, upload-time = "2026-05-01T23:06:18.409Z" }, + { url = "https://files.pythonhosted.org/packages/e4/55/5e4adcf7d2a1006b844903b27cb81244a9b748d850433a46a6c21776c401/ty-0.0.34-py3-none-macosx_11_0_arm64.whl", hash = "sha256:b0ea47a2998e167ab3b21d2f4b5309a9cf33c297809f6d7e3e753252223174d0", size = 10279378, upload-time = "2026-05-01T23:06:37.962Z" }, + { url = "https://files.pythonhosted.org/packages/4d/91/f537dca0db8fe2558e8ab04d8941d687b384fcc1df5eb9023b2db75ac26c/ty-0.0.34-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b37da00b41a118a459ae56d8947e70651073fb33ebfbceb820e4a10b22d5023", size = 10817423, upload-time = "2026-05-01T23:06:26.247Z" }, + { url = "https://files.pythonhosted.org/packages/2c/c4/55a3ad1da2815af1009bdc1b8c90dc11a364cd314e4b48c5128ba9d38859/ty-0.0.34-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:81cbbb93c2342fe3de43e625d3a9eb149633e9f485e816ebf6395d08685355d8", size = 10851826, upload-time = "2026-05-01T23:06:24.198Z" }, + { url = "https://files.pythonhosted.org/packages/ce/8c/9c7606af22d73fb43ea4369472d9c66ece11231be73b0efe8e3c61655559/ty-0.0.34-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c5b4dea1594a021289e172582df9cde7089dce14b276fc650e7b212b1772e12", size = 11356318, upload-time = "2026-05-01T23:06:51.139Z" }, + { url = "https://files.pythonhosted.org/packages/20/54/bb423f663721ab4138b216425c6b55eaefd3a068243b24d6d8fe988f4e13/ty-0.0.34-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:030fb00aa2d2a5b5ae9d9183d574e0c82dae80566700a7490c43669d8ece40cd", size = 11902968, upload-time = "2026-05-01T23:06:35.82Z" }, + { url = "https://files.pythonhosted.org/packages/b6/22/01122b21ab6b534a2f618c6bbe5f1f7f49fd56f4b2ec8887cd6d40d08fb3/ty-0.0.34-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ae9555e24e36c63a8218e037a5a63f15579eb6aa94f41017e57cd41d335cfb5", size = 11548860, upload-time = "2026-05-01T23:06:42.155Z" }, + { url = "https://files.pythonhosted.org/packages/d1/50/86008b1392ec64bed1957bbcc7aaa43b466b50dfc91bb131841c21d7c5c3/ty-0.0.34-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99eb23df9ed129fc26d1ab00d6f0b8dfe5253b09c2ac6abdb11523fa70d67f10", size = 11457097, upload-time = "2026-05-01T23:06:53.477Z" }, + { url = "https://files.pythonhosted.org/packages/92/3e/4558b2296963ba99c58d8409c57d7db4f3061b656c3613cb21c02c1ef4c2/ty-0.0.34-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:85de45382016eceae69e104815eb2cfa200787df104002e262a86cbd43ed2c02", size = 10798192, upload-time = "2026-05-01T23:06:40.004Z" }, + { url = "https://files.pythonhosted.org/packages/76/bf/650d24402be2ef678528d60caac1d9477a40fc37e3792ecef07834fd7a4a/ty-0.0.34-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:14cb575fb8fa5131f5129d100cfe23c1575d23faf5dfc5158432749a3e38c9b5", size = 10890390, upload-time = "2026-05-01T23:06:33.076Z" }, + { url = "https://files.pythonhosted.org/packages/5c/ef/ccd2ca13906079f7935fd7e067661b24233017f57d987d51d6a121d85bb5/ty-0.0.34-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c6fc0b69d8450e6910ba9db34572b959b81329a97ae273c391f70e9fb6c1aade", size = 11031564, upload-time = "2026-05-01T23:06:55.812Z" }, + { url = "https://files.pythonhosted.org/packages/ba/2d/d27b72005b6f43599e3bcabab0d7135ac0c230b7a307bb99f9eea02c1cda/ty-0.0.34-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:30dfcec2f0fde3993f4f912ed0e057dcbebc8615299f610a4c2ddb7b5a3e1e06", size = 11553430, upload-time = "2026-05-01T23:06:31.096Z" }, + { url = "https://files.pythonhosted.org/packages/a7/12/20812e1ad930b8d4af70eebf19ad23cff6e31efcfa613ef884531fcdbaa1/ty-0.0.34-py3-none-win32.whl", hash = "sha256:97b77ddf007271b812a313a8f0a14929bc5590958433e1fb83ef585676f53342", size = 10436048, upload-time = "2026-05-01T23:06:49.108Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6a/afa095c5987868fbda27c0f731146ac8e3d07b357adfa83daccaee5b1a16/ty-0.0.34-py3-none-win_amd64.whl", hash = "sha256:1f543968accb952705134028d1fda8656882787dbbc667ad4d6c3ba23791d604", size = 11462526, upload-time = "2026-05-01T23:06:28.514Z" }, + { url = "https://files.pythonhosted.org/packages/63/8f/bf041a06260d77662c0605e56dacfe90b786bf824cbe1aed238d15fe5e84/ty-0.0.34-py3-none-win_arm64.whl", hash = "sha256:ea09108cbcb16b6b06d7596312b433bf49681e78d30e4dc7fb3c1b248a95e09a", size = 10846945, upload-time = "2026-05-01T23:06:44.428Z" }, +] + +[[package]] +name = "typeguard" +version = "4.5.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/84/44/9478c50c266826c1bf30d1692e589755bffa8f1c0a3eb7af8a346c255991/ty-0.0.33.tar.gz", hash = "sha256:46d63bda07403322cb6c28ccfdd5536be916e13df725c29f7ccd0a21f06bd9e8", size = 5559373, upload-time = "2026-04-28T10:45:13.18Z" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2b/e8/66e25efcc18542d58706ce4e50415710593721aae26e794ab1dec34fb66f/typeguard-4.5.1.tar.gz", hash = "sha256:f6f8ecbbc819c9bc749983cc67c02391e16a9b43b8b27f15dc70ed7c4a007274", size = 80121, upload-time = "2026-02-19T16:09:03.392Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e9/24/e287388c63a19191be26b32ff4dbd06029834068150ebe2532939bc4c851/ty-0.0.33-py3-none-linux_armv6l.whl", hash = "sha256:94d0a9d2234261a8911396d59e506b5923fe0971dbda43b9dcea287936887fcc", size = 11021308, upload-time = "2026-04-28T10:45:43.34Z" }, - { url = "https://files.pythonhosted.org/packages/00/ca/ba1eed819895bd239fba8ee35dfcd5fcb266c203b0914a17a59579096bb5/ty-0.0.33-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e4a2b5ba078f90de342f56b5f7979bb77c9b9b1d8625a041352ffc6ee93c4073", size = 10777272, upload-time = "2026-04-28T10:45:32.905Z" }, - { url = "https://files.pythonhosted.org/packages/25/a8/c3131d37b44b3fea1d6654a1c929a0cd0873822f77a90482b8ec28f6fbbd/ty-0.0.33-py3-none-macosx_11_0_arm64.whl", hash = "sha256:84ff5707825e9af9668d2bcf66975f93e520a63b524ab494e3a8265735be2563", size = 10201078, upload-time = "2026-04-28T10:45:23.374Z" }, - { url = "https://files.pythonhosted.org/packages/7b/db/d8e37ff0045810cc65e1ff36aa0da0a2253c05659787ac987df8a16c7897/ty-0.0.33-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e375285736f57886868e7af0b11c7b0ec5b6543fa15e7ad2a714fed9f077d4e0", size = 10732347, upload-time = "2026-04-28T10:45:21.444Z" }, - { url = "https://files.pythonhosted.org/packages/e0/1a/20e83a412506a918e4684fc67b567cf7cc13b105470b3428cb23c3d5aa13/ty-0.0.33-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5680f6350c3b4e46b8bff6d7bb132366ea239463d6cad4892725d06046e65464", size = 10808238, upload-time = "2026-04-28T10:45:38.565Z" }, - { url = "https://files.pythonhosted.org/packages/5d/4b/d0a39f4464dc6cb4cc2c159473ce216bd1846bfb684c0323a3cb36dce5c6/ty-0.0.33-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c5535538bad8d0f7e62bcdff02197cdb30e41451d80b35d27e17d128f2e1dc5d", size = 11288348, upload-time = "2026-04-28T10:45:08.419Z" }, - { url = "https://files.pythonhosted.org/packages/35/7e/f1745e0f9583363d7a83d9a4990fc244f76ecc30840ddad83dc16a33c52d/ty-0.0.33-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:da196c42bbbc069e1e21e3e52107c061aa9660352dae57a41930690b56e2c02d", size = 11789907, upload-time = "2026-04-28T10:45:19.064Z" }, - { url = "https://files.pythonhosted.org/packages/a5/71/25f39f46a12d662859d45bc648555d0661044eb43db6b5648c9947487da9/ty-0.0.33-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9281672921ef6d4460e03146b5e6c18cb1a3e3a3b8a1a88f6f33226d05a469b7", size = 11500774, upload-time = "2026-04-28T10:45:48.012Z" }, - { url = "https://files.pythonhosted.org/packages/94/ec/136959ecbb7c71cb90537f5aea441c73f4ab24612868a6ecdc9d7444d32d/ty-0.0.33-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82c1b8f303f82da64e878108e764be3ecbcd7c9903ac0a7f7031614ed00b97ab", size = 11360314, upload-time = "2026-04-28T10:45:05.402Z" }, - { url = "https://files.pythonhosted.org/packages/cf/95/32809575c222f00beed498cb728e9290a0f5009f930025381bb7253b2206/ty-0.0.33-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:efe3af412c9ff67bce5fa37d0a2b0d8555c24072b145a5bac6c79637f1c83abe", size = 10707785, upload-time = "2026-04-28T10:45:10.836Z" }, - { url = "https://files.pythonhosted.org/packages/13/89/c8e9531f7aa4a093359e15fa32c8e1277fbbe90d16894d7c6032d29f4b34/ty-0.0.33-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:aeec29c91ea768601747da546c3efc20b72c2fb1bd52bcc786a5c6eeff51d27b", size = 10834987, upload-time = "2026-04-28T10:45:40.738Z" }, - { url = "https://files.pythonhosted.org/packages/31/16/9835fbcf5338af1a1917bd28fdb8a7193c210b83f243aa286fa9f79cb3ad/ty-0.0.33-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a535977c52bbb5f7e96b8b70a6ad375ad077f4a9ff2492508ea3816a2b403819", size = 10968968, upload-time = "2026-04-28T10:45:30.26Z" }, - { url = "https://files.pythonhosted.org/packages/36/69/64c76aabc1bc70c7f24b686cd93c3407f8ea430905e395f59bf9603ef571/ty-0.0.33-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1d732facf39fcb221ba279d469c5040d37883e964f123b1563888efd34818180", size = 11458077, upload-time = "2026-04-28T10:45:45.971Z" }, - { url = "https://files.pythonhosted.org/packages/91/84/fae27b0c4718776a298690d31ca4cc1995f2e3e1c63a7b59e84c41498e9a/ty-0.0.33-py3-none-win32.whl", hash = "sha256:d90960b574428dc252f85e8598ec5fcb7f619794196b2fc95a90da075ed4681c", size = 10345364, upload-time = "2026-04-28T10:45:16.836Z" }, - { url = "https://files.pythonhosted.org/packages/3c/a0/a2938b23ae3e1a09a2d7c189e2ac5f7113676bae4e0e23948b568e18e5f8/ty-0.0.33-py3-none-win_amd64.whl", hash = "sha256:c1c3aec62c44de610c6e95f0a4e97ac3dbc07934bfdbf1fd90d758c9ff72f48e", size = 11342470, upload-time = "2026-04-28T10:45:26.455Z" }, - { url = "https://files.pythonhosted.org/packages/ab/62/7fb948aace38d2f6329261bb33c035a8484549c74f1db28649c7a4c6fed9/ty-0.0.33-py3-none-win_arm64.whl", hash = "sha256:0d44f99ba1b441e55e2aa301b2ac0a21112784931b46a5f66f4ea9efe5620d97", size = 10742673, upload-time = "2026-04-28T10:45:35.555Z" }, + { url = "https://files.pythonhosted.org/packages/91/88/b55b3117287a8540b76dbdd87733808d4d01c8067a3b339408c250bb3600/typeguard-4.5.1-py3-none-any.whl", hash = "sha256:44d2bf329d49a244110a090b55f5f91aa82d9a9834ebfd30bcc73651e4a8cc40", size = 36745, upload-time = "2026-02-19T16:09:01.6Z" }, ] [[package]] @@ -2751,16 +3133,16 @@ wheels = [ [[package]] name = "urllib3" -version = "2.6.3" +version = "2.7.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } +sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, + { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, ] [[package]] name = "virtualenv" -version = "21.3.0" +version = "21.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "distlib" }, @@ -2769,16 +3151,16 @@ dependencies = [ { name = "python-discovery" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3f/8b/6331f7a7fe70131c301106ec1e7cf23e2501bf7d4ca3636805801ca191bb/virtualenv-21.3.0.tar.gz", hash = "sha256:733750db978ec95c2d8eb4feadaa57091002bce404cb39ba69899cf7bd28944e", size = 7614069, upload-time = "2026-04-27T17:05:58.927Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/0d/915c02c94d207b85580eb09bffab54438a709e7288524094fe781da526c2/virtualenv-21.3.1.tar.gz", hash = "sha256:c2305bc1fddeec40699b8370d13f8d431b0701f00ce895061ce493aeded4426b", size = 7613791, upload-time = "2026-05-05T01:34:31.402Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4b/eb/03bfb1299d4c4510329e470f13f9a4ce793df7fcb5a2fd3510f911066f61/virtualenv-21.3.0-py3-none-any.whl", hash = "sha256:4d28ee41f6d9ec8f1f00cd472b9ffbcedda1b3d3b9a575b5c94a2d004fd51bd7", size = 7594690, upload-time = "2026-04-27T17:05:55.468Z" }, + { url = "https://files.pythonhosted.org/packages/b1/4f/f71e641e504111a5a74e3a20bc52d01bd86788b22699dd3fee1c63253cf6/virtualenv-21.3.1-py3-none-any.whl", hash = "sha256:d1a71cf58f2f9228fff23a1f6ec15d39785c6b32e03658d104974247145edd35", size = 7594539, upload-time = "2026-05-05T01:34:28.98Z" }, ] [[package]] name = "wcwidth" -version = "0.6.0" +version = "0.7.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/35/a2/8e3becb46433538a38726c948d3399905a4c7cabd0df578ede5dc51f0ec2/wcwidth-0.6.0.tar.gz", hash = "sha256:cdc4e4262d6ef9a1a57e018384cbeb1208d8abbc64176027e2c2455c81313159", size = 159684, upload-time = "2026-02-06T19:19:40.919Z" } +sdist = { url = "https://files.pythonhosted.org/packages/2c/ee/afaf0f85a9a18fe47a67f1e4422ed6cf1fe642f0ae0a2f81166231303c52/wcwidth-0.7.0.tar.gz", hash = "sha256:90e3a7ea092341c44b99562e75d09e4d5160fe7a3974c6fb842a101a95e7eed0", size = 182132, upload-time = "2026-05-02T16:04:12.653Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/68/5a/199c59e0a824a3db2b89c5d2dade7ab5f9624dbf6448dc291b46d5ec94d3/wcwidth-0.6.0-py3-none-any.whl", hash = "sha256:1a3a1e510b553315f8e146c54764f4fb6264ffad731b3d78088cdb1478ffbdad", size = 94189, upload-time = "2026-02-06T19:19:39.646Z" }, + { url = "https://files.pythonhosted.org/packages/41/52/e465037f5375f43533d1a80b6923955201596a99142ed524d77b571a1418/wcwidth-0.7.0-py3-none-any.whl", hash = "sha256:5d69154c429a82910e241c738cd0e2976fac8a2dd47a1a805f4afed1c0f136f2", size = 110825, upload-time = "2026-05-02T16:04:11.033Z" }, ] From 57a77296227ec57397ad5f63c4ce3620ccfb5576 Mon Sep 17 00:00:00 2001 From: Serj Babayan Date: Fri, 8 May 2026 12:40:02 -0700 Subject: [PATCH 2/2] try reverting duckdb changes --- parallel_web_tools/cli/commands.py | 70 +- parallel_web_tools/core/auth.py | 112 ++-- parallel_web_tools/core/credentials.py | 7 + parallel_web_tools/core/service_types.py | 44 +- .../integrations/duckdb/_relation.py | 22 - .../integrations/duckdb/batch.py | 7 +- .../integrations/duckdb/findall.py | 7 +- parallel_web_tools/integrations/spark/udf.py | 4 +- tests/test_auth.py | 109 ++- tests/test_cli.py | 62 +- tests/test_credentials.py | 5 +- tests/test_enrichment.py | 4 +- tests/test_service.py | 8 +- uv.lock | 626 ++++++++---------- 14 files changed, 529 insertions(+), 558 deletions(-) delete mode 100644 parallel_web_tools/integrations/duckdb/_relation.py diff --git a/parallel_web_tools/cli/commands.py b/parallel_web_tools/cli/commands.py index 3107993..a83a000 100644 --- a/parallel_web_tools/cli/commands.py +++ b/parallel_web_tools/cli/commands.py @@ -484,22 +484,42 @@ def auth(output_json: bool): print(json.dumps(status, indent=2)) return - if status["authenticated"]: - if status["method"] == "environment": - console.print("[green]Authenticated via PARALLEL_API_KEY environment variable[/green]") - else: - console.print("[green]Authenticated via OAuth[/green]") - console.print(f" Credentials: {status['token_file']}") - if status.get("selected_org_name"): - console.print(f" Organization: {status['selected_org_name']} ({status['selected_org_id']})") - elif status.get("selected_org_id"): - console.print(f" Organization ID: {status['selected_org_id']}") - else: + if not status["authenticated"]: console.print("[yellow]Not authenticated[/yellow]") console.print("\n[cyan]To get started:[/cyan]") console.print(" 1. Create an account at [link=https://parallel.ai]parallel.ai[/link]") console.print(" 2. Run: parallel-cli login") console.print(" Or set PARALLEL_API_KEY environment variable") + return + + # Active source — whichever resolve_api_key would return — comes first. + if status["method"] == "environment": + console.print("[green]Active: PARALLEL_API_KEY environment variable[/green]") + console.print(" [dim]This key is used for all API calls.[/dim]") + else: + console.print("[green]Active: stored credentials (OAuth)[/green]") + console.print(f" Credentials: {status['token_file']}") + if status.get("selected_org_name"): + console.print(f" Organization: {status['selected_org_name']} ({status['selected_org_id']})") + elif status.get("selected_org_id"): + console.print(f" Organization ID: {status['selected_org_id']}") + + # If the env var is overriding stored creds, make that loud and impossible to miss. + if status.get("stored_overridden_by_env"): + console.print() + console.print("[bold yellow]⚠ PARALLEL_API_KEY is set and OVERRIDES your stored login.[/bold yellow]") + console.print(f" Stored credentials: {status['token_file']}") + if status.get("selected_org_name"): + console.print( + f" Stored organization: {status['selected_org_name']} ({status['selected_org_id']}) " + "[dim](inactive)[/dim]" + ) + elif status.get("selected_org_id"): + console.print(f" Stored organization ID: {status['selected_org_id']} [dim](inactive)[/dim]") + console.print(" [dim]Unset PARALLEL_API_KEY to use the stored login instead (`unset PARALLEL_API_KEY`).[/dim]") + elif status["method"] == "environment" and not status.get("has_stored_credentials"): + # Env var set, no stored creds — informational only. + console.print(" [dim]No stored credentials. Run `parallel-cli login` to add an OAuth login.[/dim]") def _build_login_hint(login_method: str | None, email: str | None) -> str | None: @@ -552,15 +572,16 @@ def _run_login(output_json: bool, email: str | None, login_method: str | None) - magic-link failure. - ``"google"`` → append ``login_hint=login=google`` to the URL and open the browser. - - ``"sso"`` → append ``login_hint=login=sso,e=`` to the - URL and open the browser. + - ``"sso"`` → append ``login_hint=login=sso&email=`` to + the URL (two separate query params) and open + the browser. """ import webbrowser from parallel_web_tools.core.auth import ( - _build_verification_uri, - _ensure_client_id, - _is_headless, + build_verification_uri, + ensure_client_id, + is_headless, send_magic_link, ) @@ -575,12 +596,12 @@ def _on_device_code(info): magic_link_error: str | None = None if login_method == "email" and email: try: - send_magic_link(client_id=_ensure_client_id(), email=email, user_code=info.user_code) + send_magic_link(client_id=ensure_client_id(), email=email, user_code=info.user_code) magic_link_sent = True except Exception as e: magic_link_error = str(e) - enriched_uri = _build_verification_uri(info.verification_uri_complete, login_hint, extra_params=extra_params) + enriched_uri = build_verification_uri(info.verification_uri_complete, login_hint, extra_params=extra_params) if output_json: payload = { @@ -623,7 +644,7 @@ def _on_device_code(info): # Providing an on_device_code callback suppresses auth.py's default # browser-launch branch, so open it here for interactive CLI use. - if not _is_headless(): + if not is_headless(): try: webbrowser.open(enriched_uri) except Exception: @@ -691,9 +712,10 @@ def login_google(ctx: click.Context): def login_sso(ctx: click.Context, user_email: str): """Authenticate via enterprise SSO for USER_EMAIL. - Opens the browser on a verification URL that hints ``login=sso,e=`` - so the landing page resolves the right SSO tenant for the email domain - and pre-fills the address. + Opens the browser on a verification URL with ``login_hint=login=sso`` + plus a separate ``email=`` query param so the landing page + resolves the right SSO tenant for the email domain and pre-fills + the address. """ output_json = ctx.obj.get("output_json", False) if ctx.obj else False _run_login(output_json=output_json, email=user_email, login_method="sso") @@ -725,9 +747,9 @@ def _derive_idempotency_key(amount_cents: int) -> str: 300 seconds. Identical repeat requests inside the same 5-minute window reuse the same key, so Stripe's idempotency dedupes them server-side. """ - from parallel_web_tools.core.auth import _ensure_client_id + from parallel_web_tools.core.auth import ensure_client_id - client_id = _ensure_client_id() + client_id = ensure_client_id() five_min_bucket = int(time.time() // 300) * 300 return f"{client_id}-{amount_cents}-{five_min_bucket}" diff --git a/parallel_web_tools/core/auth.py b/parallel_web_tools/core/auth.py index a29154d..0558b59 100644 --- a/parallel_web_tools/core/auth.py +++ b/parallel_web_tools/core/auth.py @@ -89,7 +89,7 @@ def _platform_path(path: str) -> str: return f"{get_platform_url()}{path}" -def _is_headless() -> bool: +def is_headless() -> bool: """Detect if the environment cannot open a browser.""" if os.environ.get("SSH_CLIENT") or os.environ.get("SSH_TTY"): return True @@ -212,7 +212,7 @@ def send_magic_link(client_id: str, email: str, user_code: str, email_type: str raise Exception(f"Magic link send failed: {e.code} - {err_body}") from e -def _ensure_client_id() -> str: +def ensure_client_id() -> str: """Return a registered ``client_id``, registering if none is stored yet. - If the credentials file already has a ``client_id``, returns it. @@ -254,7 +254,7 @@ def _reregister_client_id() -> str: return client_id -def _build_verification_uri( +def build_verification_uri( base: str, login_hint: str | None, extra_params: dict[str, str] | None = None, @@ -419,7 +419,7 @@ def _do_device_flow( else: raise - enriched_uri = _build_verification_uri(info.verification_uri_complete, login_hint) + enriched_uri = build_verification_uri(info.verification_uri_complete, login_hint) if on_device_code: on_device_code(info) @@ -429,7 +429,7 @@ def _do_device_flow( print(f"Or open: {enriched_uri}\n", file=sys.stderr) print(f"Waiting for authorization (expires in {info.expires_in // 60} minutes)...", file=sys.stderr) - if not _is_headless(): + if not is_headless(): try: webbrowser.open(enriched_uri) except Exception: @@ -496,7 +496,7 @@ def get_control_api_access_token() -> str: if tokens.refresh_token_expires_at is not None and now >= tokens.refresh_token_expires_at: raise ReauthenticationRequired("refresh token has expired; run 'parallel-cli login'") - new_tokens = refresh_access_token(refresh_token_value, client_id=_ensure_client_id()) + new_tokens = refresh_access_token(refresh_token_value, client_id=ensure_client_id()) _persist_token_response(new_tokens) return new_tokens.access_token @@ -508,9 +508,9 @@ def login_flow( """Run the full CLI login: register client → device flow → persist tokens → auto-mint data API key. ``login_hint`` is forwarded to the device flow's URL enrichment (see - :func:`_build_verification_uri`). Returns the newly-minted data API key. + :func:`build_verification_uri`). Returns the newly-minted data API key. """ - client_id = _ensure_client_id() + client_id = ensure_client_id() token_resp = _do_device_flow(login_hint=login_hint, on_device_code=on_device_code, client_id=client_id) _persist_token_response(token_resp) @@ -533,22 +533,25 @@ def login_flow( def resolve_api_key(api_key: str | None = None) -> str: - """Resolve API key from parameter, stored credentials, or environment. + """Resolve API key from parameter, environment, or stored credentials. - Priority: explicit ``api_key`` argument → stored credentials → ``PARALLEL_API_KEY``. + Priority: explicit ``api_key`` argument → ``PARALLEL_API_KEY`` env var → + stored credentials. Env beats stored creds so operators can override a + developer's local ``parallel-cli login`` session by exporting the env var + (matches the convention used by AWS, GCP, Anthropic, Stripe SDKs). Raises ``ValueError`` if no key is available. """ if api_key: return api_key - stored = credentials.get_selected_api_key() - if stored: - return stored env_key = os.environ.get("PARALLEL_API_KEY") if env_key: return env_key + stored = credentials.get_selected_api_key() + if stored: + return stored raise ValueError( - "Parallel API key required. Run 'parallel-cli login', set the " - "PARALLEL_API_KEY environment variable, or pass api_key explicitly." + "Parallel API key required. Set the PARALLEL_API_KEY environment " + "variable, run 'parallel-cli login', or pass api_key explicitly." ) @@ -559,23 +562,28 @@ def get_api_key( ) -> str: """Get API key, triggering device-flow login + auto-mint as a fallback. - Priority (when not ``force_login``): stored credentials → service-API key - provisioning from stored control-API tokens → ``PARALLEL_API_KEY``. + Priority (when not ``force_login``): ``PARALLEL_API_KEY`` env var → stored + credentials → service-API key provisioning from stored control-API tokens + → interactive device flow. ``login_hint`` is forwarded to :func:`login_flow` — see - :func:`_build_verification_uri` for the supported hint format. + :func:`build_verification_uri` for the supported hint format. """ if not force_login: + env_key = os.environ.get("PARALLEL_API_KEY") + if env_key: + return env_key + stored = credentials.get_selected_api_key() if stored: return stored # If we still have valid control-API auth but no data API key saved, # mint a new data key via service API before forcing an interactive - # device-authorization flow or falling back to the environment. + # device-authorization flow. try: access_token = get_control_api_access_token() - client_id = _ensure_client_id() + client_id = ensure_client_id() minted_api_key, _ = service.provision_cli_api_key(access_token, client_id=client_id) creds = credentials.load() if creds is not None: @@ -589,10 +597,6 @@ def get_api_key( except service.ServiceApiError: pass - env_key = os.environ.get("PARALLEL_API_KEY") - if env_key: - return env_key - if not on_device_code: print("Starting device authorization...", file=sys.stderr) return login_flow(login_hint=login_hint, on_device_code=on_device_code) @@ -648,26 +652,56 @@ def logout() -> bool: def get_auth_status() -> dict: """Get current authentication status. - Priority matches :func:`resolve_api_key`: stored credentials beat the - ``PARALLEL_API_KEY`` env var. + Reports BOTH sources independently so callers can show that ``PARALLEL_API_KEY`` + is overriding a stored login. ``method`` names the source that + :func:`resolve_api_key` would actually return — env beats stored creds. + + Returned fields: + + - ``authenticated``: at least one source has a key. + - ``method``: ``"environment"`` | ``"oauth"`` | ``None`` — which source wins. + - ``env_var_set``: whether ``PARALLEL_API_KEY`` is set (regardless of winner). + - ``has_stored_credentials``: whether ``auth.json`` has a usable key. + - ``stored_overridden_by_env``: True when the env var is set AND stored creds + exist; the stored creds will not be used until the env var is unset. + - ``token_file`` / ``version`` / ``selected_org_id`` / ``selected_org_name`` + / ``has_control_api_tokens``: stored-credential metadata when present. """ + env_key = os.environ.get("PARALLEL_API_KEY") + env_var_set = bool(env_key) + creds = credentials.load() - if creds is not None: - org = creds.selected_org() - if org and org.api_key: - token_file = credentials.get_active_credentials_file() or credentials.CREDENTIALS_FILE - return { - "authenticated": True, - "method": "oauth", + stored_org = creds.selected_org() if creds is not None else None + has_stored_credentials = bool(stored_org and stored_org.api_key) + + status: dict = { + "authenticated": env_var_set or has_stored_credentials, + "method": None, + "env_var_set": env_var_set, + "has_stored_credentials": has_stored_credentials, + "stored_overridden_by_env": env_var_set and has_stored_credentials, + "token_file": None, + "version": None, + "selected_org_id": None, + "selected_org_name": None, + "has_control_api_tokens": False, + } + + if has_stored_credentials and creds is not None and stored_org is not None: + token_file = credentials.get_active_credentials_file() or credentials.CREDENTIALS_FILE + status.update( + { "token_file": str(token_file), "version": creds.version, "selected_org_id": creds.selected_org_id, - "selected_org_name": org.org_name, - "has_control_api_tokens": bool(org.control_api.refresh_token), + "selected_org_name": stored_org.org_name, + "has_control_api_tokens": bool(stored_org.control_api.refresh_token), } + ) - api_key = os.environ.get("PARALLEL_API_KEY") - if api_key: - return {"authenticated": True, "method": "environment", "token_file": None} + if env_var_set: + status["method"] = "environment" + elif has_stored_credentials: + status["method"] = "oauth" - return {"authenticated": False, "method": None, "token_file": None} + return status diff --git a/parallel_web_tools/core/credentials.py b/parallel_web_tools/core/credentials.py index b539bab..817a060 100644 --- a/parallel_web_tools/core/credentials.py +++ b/parallel_web_tools/core/credentials.py @@ -153,6 +153,13 @@ def load() -> Credentials | None: return None creds = _credentials_from_dict(_migrate_v0(legacy_raw)) save(creds) + # Remove the legacy file only after the new one is durably on disk, so a + # crash mid-migration leaves the user with the original credentials rather + # than nothing. + try: + LEGACY_CREDENTIALS_FILE.unlink() + except OSError: + pass return creds diff --git a/parallel_web_tools/core/service_types.py b/parallel_web_tools/core/service_types.py index e5154bf..30c778d 100644 --- a/parallel_web_tools/core/service_types.py +++ b/parallel_web_tools/core/service_types.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: http://localhost:8090/service/openapi.json -# timestamp: 2026-04-22T20:07:38+00:00 +# timestamp: 2026-05-08T19:39:42+00:00 from __future__ import annotations @@ -9,6 +9,14 @@ from pydantic import BaseModel, Field +class CreateAppResponseModel(BaseModel): + app_id: Annotated[str, Field(description="App ID", title="App Id")] + + +class CreateAppRequestModel(BaseModel): + app_name: Annotated[str, Field(description="App name", title="App Name")] + + class BalanceResponse(BaseModel): org_id: Annotated[str, Field(description="Organization ID", title="Org Id")] credit_balance_cents: Annotated[ @@ -34,21 +42,20 @@ class BalanceResponse(BaseModel): ] = False -class ValidationError(BaseModel): - loc: Annotated[list[str | int], Field(title="Location")] - msg: Annotated[str, Field(title="Message")] - type: Annotated[str, Field(title="Error Type")] - - -class CreateAppResponseModel(BaseModel): +class AppItem(BaseModel): + app_name: Annotated[str, Field(description="App name", title="App Name")] + org_name: Annotated[str | None, Field(description="Organization name", title="Org Name")] app_id: Annotated[str, Field(description="App ID", title="App Id")] + org_id: Annotated[str, Field(description="Organization ID", title="Org Id")] class AddBalanceRequest(BaseModel): amount_cents: Annotated[ int, Field( - description="Amount in cents to charge and add to the balance.", + description="Amount in cents to charge and add to the balance. Must be between 1 and 10000 cents ($100.00).", + gt=0, + le=10000, title="Amount Cents", ), ] @@ -57,7 +64,7 @@ class AddBalanceRequest(BaseModel): Field( description="Required idempotency key. Stripe dedupes the charge server-side for at least 24h when the same key is submitted again for the same org (see https://docs.stripe.com/api/idempotent_requests). Pick a high-entropy value (e.g. a UUID) so distinct agent attempts do not collide.", max_length=128, - min_length=1, + min_length=8, title="Idempotency Key", ), ] @@ -79,20 +86,15 @@ class CreateApiKeyRequestModel(BaseModel): api_key_name: Annotated[str, Field(description="API Key Name", title="Api Key Name")] -class AppItem(BaseModel): - app_name: Annotated[str, Field(description="App name", title="App Name")] - org_name: Annotated[str | None, Field(description="Organization name", title="Org Name")] - app_id: Annotated[str, Field(description="App ID", title="App Id")] - org_id: Annotated[str, Field(description="Organization ID", title="Org Id")] +class ValidationError(BaseModel): + loc: Annotated[list[str | int], Field(title="Location")] + msg: Annotated[str, Field(title="Message")] + type: Annotated[str, Field(title="Error Type")] -class CreateAppRequestModel(BaseModel): - app_name: Annotated[str, Field(description="App name", title="App Name")] +class GetAppsForOrgResponseModel(BaseModel): + apps: Annotated[list[AppItem] | None, Field(description="List of apps", title="Apps")] = None class HTTPValidationError(BaseModel): detail: Annotated[list[ValidationError] | None, Field(title="Detail")] = None - - -class GetAppsForOrgResponseModel(BaseModel): - apps: Annotated[list[AppItem] | None, Field(description="List of apps", title="Apps")] = None diff --git a/parallel_web_tools/integrations/duckdb/_relation.py b/parallel_web_tools/integrations/duckdb/_relation.py deleted file mode 100644 index 2045295..0000000 --- a/parallel_web_tools/integrations/duckdb/_relation.py +++ /dev/null @@ -1,22 +0,0 @@ -from __future__ import annotations - -import pandas as pd - - -class DuckDBResultRelation: - """Thin wrapper that preserves `None` values in `fetchdf()` output.""" - - def __init__(self, relation): - self._relation = relation - - def fetchdf(self): - df = self._relation.fetchdf() - return df.astype(object).where(pd.notna(df), None) - - def __getattr__(self, name): - return getattr(self._relation, name) - - -def wrap_relation(relation): - """Wrap a DuckDB relation so pandas materialization keeps nulls as `None`.""" - return DuckDBResultRelation(relation) diff --git a/parallel_web_tools/integrations/duckdb/batch.py b/parallel_web_tools/integrations/duckdb/batch.py index f7d57f6..1ff0b55 100644 --- a/parallel_web_tools/integrations/duckdb/batch.py +++ b/parallel_web_tools/integrations/duckdb/batch.py @@ -31,7 +31,6 @@ from parallel_web_tools.core import EnrichmentResult, build_output_schema, enrich_batch from parallel_web_tools.core.sql_utils import quote_identifier -from parallel_web_tools.integrations.duckdb._relation import wrap_relation if TYPE_CHECKING: DuckDBEnrichmentResult = EnrichmentResult[duckdb.DuckDBPyRelation] @@ -120,7 +119,7 @@ def enrich_table( if include_basis: empty_cols += ", NULL::VARCHAR AS _basis" empty_query = f"SELECT {select_cols}, {empty_cols} FROM {quote_identifier(source_table)} WHERE 1=0" - rel = wrap_relation(conn.sql(empty_query)) + rel = conn.sql(empty_query) return EnrichmentResult( result=rel, @@ -210,9 +209,9 @@ def enrich_table( if result_table: result_quoted = quote_identifier(result_table) conn.execute(f"CREATE TABLE {result_quoted} AS SELECT * FROM {temp_quoted}") - rel = wrap_relation(conn.sql(f"SELECT * FROM {result_quoted}")) + rel = conn.sql(f"SELECT * FROM {result_quoted}") else: - rel = wrap_relation(conn.sql(f"SELECT * FROM {temp_quoted}")) + rel = conn.sql(f"SELECT * FROM {temp_quoted}") elapsed = time.time() - start_time diff --git a/parallel_web_tools/integrations/duckdb/findall.py b/parallel_web_tools/integrations/duckdb/findall.py index 1eefbd6..5d7ed24 100644 --- a/parallel_web_tools/integrations/duckdb/findall.py +++ b/parallel_web_tools/integrations/duckdb/findall.py @@ -29,7 +29,6 @@ from parallel_web_tools.core.findall import run_findall from parallel_web_tools.core.result import EnrichmentResult from parallel_web_tools.core.sql_utils import quote_identifier -from parallel_web_tools.integrations.duckdb._relation import wrap_relation def _unpack_output(candidate: dict[str, Any]) -> dict[str, Any]: @@ -166,7 +165,7 @@ def findall_table( if not col_names: # No results — return an empty relation - rel = wrap_relation(conn.sql("SELECT 1 WHERE 1=0")) + rel = conn.sql("SELECT 1 WHERE 1=0") return EnrichmentResult( result=rel, success_count=0, @@ -194,9 +193,9 @@ def findall_table( if result_table: result_quoted = quote_identifier(result_table) conn.execute(f"CREATE OR REPLACE TABLE {result_quoted} AS SELECT * FROM {temp_quoted}") - rel = wrap_relation(conn.sql(f"SELECT * FROM {result_quoted}")) + rel = conn.sql(f"SELECT * FROM {result_quoted}") else: - rel = wrap_relation(conn.sql(f"SELECT * FROM {temp_quoted}")) + rel = conn.sql(f"SELECT * FROM {temp_quoted}") return EnrichmentResult( result=rel, diff --git a/parallel_web_tools/integrations/spark/udf.py b/parallel_web_tools/integrations/spark/udf.py index 1959509..5971e5a 100644 --- a/parallel_web_tools/integrations/spark/udf.py +++ b/parallel_web_tools/integrations/spark/udf.py @@ -68,7 +68,7 @@ def _parallel_enrich_partition( valid_indices.append(i) if not valid_items: - return pd.Series([None] * len(items), dtype=object) + return pd.Series([None] * len(items)) # Process valid items in chunks of _MAX_CHUNK_SIZE via enrich_batch all_results: list[dict] = [] @@ -100,7 +100,7 @@ def _parallel_enrich_partition( for i, result in zip(valid_indices, json_results, strict=True): output[i] = result - return pd.Series(output, dtype=object) + return pd.Series(output) def create_parallel_enrich_udf( diff --git a/tests/test_auth.py b/tests/test_auth.py index 1368b6f..9172f2c 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -17,15 +17,15 @@ DeviceCodeInfo, ReauthenticationRequired, TokenResponse, - _build_verification_uri, _do_device_flow, - _ensure_client_id, - _is_headless, _persist_token_response, + build_verification_uri, create_client, + ensure_client_id, get_api_key, get_auth_status, get_control_api_access_token, + is_headless, login_flow, logout, poll_device_token, @@ -65,12 +65,12 @@ def no_sleep(monkeypatch): @pytest.fixture def mock_ensure_client_id(monkeypatch): - """Stub out _ensure_client_id to avoid real /getServiceKeys/register calls. + """Stub out ensure_client_id to avoid real /getServiceKeys/register calls. Returns the value the stub will produce so tests can assert on it. """ value = "cid_test" - monkeypatch.setattr("parallel_web_tools.core.auth._ensure_client_id", lambda: value) + monkeypatch.setattr("parallel_web_tools.core.auth.ensure_client_id", lambda: value) return value @@ -171,17 +171,17 @@ def _token_response(**overrides) -> TokenResponse: # --------------------------------------------------------------------------- -# _build_verification_uri +# build_verification_uri # --------------------------------------------------------------------------- class TestBuildVerificationUri: def test_appends_agent_true(self): - url = _build_verification_uri("http://localhost:3000/getServiceKeys/device?user_code=ABCD", None) + url = build_verification_uri("http://localhost:3000/getServiceKeys/device?user_code=ABCD", None) assert "user_code=ABCD" in url def test_passes_login_hint_through_url_encoded(self): - url = _build_verification_uri( + url = build_verification_uri( "http://localhost:3000/getServiceKeys/device?user_code=ABCD", "login=email,e=user@example.com", ) @@ -190,19 +190,19 @@ def test_passes_login_hint_through_url_encoded(self): def test_supports_non_email_methods(self): # google: no email needed - google_url = _build_verification_uri("http://localhost:3000/d", "login=google") + google_url = build_verification_uri("http://localhost:3000/d", "login=google") assert "login_hint=login%3Dgoogle" in google_url # sso: email carried as ,e=… - sso_url = _build_verification_uri("http://localhost:3000/d", "login=sso,e=u@example.com") + sso_url = build_verification_uri("http://localhost:3000/d", "login=sso,e=u@example.com") assert "login_hint=login%3Dsso%2Ce%3Du%40example.com" in sso_url def test_no_hint_omits_login_hint(self): - url = _build_verification_uri("http://localhost:3000/getServiceKeys/device", None) + url = build_verification_uri("http://localhost:3000/getServiceKeys/device", None) assert "login_hint" not in url # --------------------------------------------------------------------------- -# register_client / _ensure_client_id +# register_client / ensure_client_id # --------------------------------------------------------------------------- @@ -245,12 +245,12 @@ class TestEnsureClientId: def test_returns_stored_client_id_without_registering(self, creds_file): credentials.save(credentials.Credentials(client_id="cid_stored")) with mock.patch("parallel_web_tools.core.auth.register_client") as mock_reg: - assert _ensure_client_id() == "cid_stored" + assert ensure_client_id() == "cid_stored" mock_reg.assert_not_called() def test_registers_and_persists_when_missing(self, creds_file): with mock.patch("parallel_web_tools.core.auth.register_client", return_value="cid_fresh"): - assert _ensure_client_id() == "cid_fresh" + assert ensure_client_id() == "cid_fresh" creds = credentials.load() assert creds is not None @@ -260,7 +260,7 @@ def test_registers_again_when_stored_client_id_is_none(self, creds_file): # Simulate a prior registration failure: file exists but client_id is None. credentials.save(credentials.Credentials(selected_org_id="x", orgs={"x": credentials.OrgCredentials()})) with mock.patch("parallel_web_tools.core.auth.register_client", return_value="cid_new") as mock_reg: - assert _ensure_client_id() == "cid_new" + assert ensure_client_id() == "cid_new" mock_reg.assert_called_once() creds = credentials.load() @@ -272,7 +272,7 @@ def test_falls_back_to_hardcoded_on_registration_failure(self, creds_file, capsy "parallel_web_tools.core.auth.register_client", side_effect=Exception("server down"), ): - assert _ensure_client_id() == "parallel-cli" + assert ensure_client_id() == "parallel-cli" # Failure leaves client_id unset so the next call retries. creds = credentials.load() @@ -468,7 +468,7 @@ def test_raises_on_http_error(self): class TestDoDeviceFlow: @mock.patch("parallel_web_tools.core.auth.webbrowser.open") - @mock.patch("parallel_web_tools.core.auth._is_headless", return_value=False) + @mock.patch("parallel_web_tools.core.auth.is_headless", return_value=False) def test_opens_browser_when_not_headless(self, _headless, mock_browser_open, no_sleep): with _patch_auth_urlopen([DEVICE_RESPONSE, TOKEN_RESPONSE_JSON]): resp = _do_device_flow() @@ -476,14 +476,14 @@ def test_opens_browser_when_not_headless(self, _headless, mock_browser_open, no_ mock_browser_open.assert_called_once() @mock.patch("parallel_web_tools.core.auth.webbrowser.open") - @mock.patch("parallel_web_tools.core.auth._is_headless", return_value=True) + @mock.patch("parallel_web_tools.core.auth.is_headless", return_value=True) def test_skips_browser_when_headless(self, _headless, mock_browser_open, no_sleep): with _patch_auth_urlopen([DEVICE_RESPONSE, TOKEN_RESPONSE_JSON]): _do_device_flow() mock_browser_open.assert_not_called() @mock.patch("parallel_web_tools.core.auth.webbrowser.open") - @mock.patch("parallel_web_tools.core.auth._is_headless", return_value=False) + @mock.patch("parallel_web_tools.core.auth.is_headless", return_value=False) def test_opens_browser_with_login_hint(self, _headless, mock_browser_open, no_sleep): with _patch_auth_urlopen([DEVICE_RESPONSE, TOKEN_RESPONSE_JSON]): _do_device_flow(login_hint="login=email,e=user@example.com") @@ -645,17 +645,19 @@ def test_skips_registration_when_client_id_already_stored(self, creds_file): class TestGetApiKey: - def test_stored_token_first_priority(self, creds_file, monkeypatch): + def test_env_var_first_priority(self, creds_file, monkeypatch): monkeypatch.setenv("PARALLEL_API_KEY", "env_key") credentials.set_api_key_for_org("org_a", "stored_key") - # Stored credentials must win over the env var. - assert get_api_key() == "stored_key" + # Env var must win over stored credentials (operator override). + assert get_api_key() == "env_key" def test_env_var_used_when_no_stored_key(self, creds_file, monkeypatch): monkeypatch.setenv("PARALLEL_API_KEY", "env_key") assert get_api_key() == "env_key" - def test_service_api_mint_beats_env_var_when_auth_json_exists(self, creds_file, monkeypatch): + def test_env_var_short_circuits_service_api_mint(self, creds_file, monkeypatch): + # When the env var is set, control-API minting must NOT be triggered; + # the env var is authoritative and the mint path is unnecessary work. monkeypatch.setenv("PARALLEL_API_KEY", "env_key") credentials.save( credentials.Credentials( @@ -665,20 +667,13 @@ def test_service_api_mint_beats_env_var_when_auth_json_exists(self, creds_file, ) with ( - mock.patch( - "parallel_web_tools.core.auth.get_control_api_access_token", return_value="at_existing" - ) as mock_at, - mock.patch("parallel_web_tools.core.auth._ensure_client_id", return_value="cid_existing") as mock_client_id, - mock.patch( - "parallel_web_tools.core.auth.service.provision_cli_api_key", - return_value=("sk_minted", "cid_existing-2026-04-23-1212"), - ) as mock_provision, + mock.patch("parallel_web_tools.core.auth.get_control_api_access_token") as mock_at, + mock.patch("parallel_web_tools.core.auth.service.provision_cli_api_key") as mock_provision, ): - assert get_api_key() == "sk_minted" + assert get_api_key() == "env_key" - mock_at.assert_called_once_with() - mock_client_id.assert_called_once_with() - mock_provision.assert_called_once_with("at_existing", client_id="cid_existing") + mock_at.assert_not_called() + mock_provision.assert_not_called() def test_stored_only_without_env(self, creds_file, monkeypatch): monkeypatch.delenv("PARALLEL_API_KEY", raising=False) @@ -705,7 +700,7 @@ def test_provisions_via_service_api_when_stored_api_key_missing(self, creds_file mock.patch( "parallel_web_tools.core.auth.get_control_api_access_token", return_value="at_existing" ) as mock_at, - mock.patch("parallel_web_tools.core.auth._ensure_client_id", return_value="cid_existing") as mock_client_id, + mock.patch("parallel_web_tools.core.auth.ensure_client_id", return_value="cid_existing") as mock_client_id, mock.patch( "parallel_web_tools.core.auth.service.provision_cli_api_key", return_value=("sk_minted", "cid_existing-2026-04-23-1212"), @@ -736,7 +731,9 @@ def test_falls_back_to_login_when_control_api_requires_reauth(self, creds_file, assert mock_login.call_args.kwargs.get("login_hint") == "user@example.com" - def test_falls_back_to_env_when_service_api_path_cannot_mint(self, creds_file, monkeypatch): + def test_env_var_returned_without_touching_control_api(self, creds_file, monkeypatch): + # With the new env-first priority, control-API state is irrelevant + # when PARALLEL_API_KEY is set: no reauth check, no login flow. monkeypatch.setenv("PARALLEL_API_KEY", "env_key") credentials.save( credentials.Credentials( @@ -746,14 +743,12 @@ def test_falls_back_to_env_when_service_api_path_cannot_mint(self, creds_file, m ) with ( - mock.patch( - "parallel_web_tools.core.auth.get_control_api_access_token", - side_effect=ReauthenticationRequired("not logged in; run 'parallel-cli login'"), - ), + mock.patch("parallel_web_tools.core.auth.get_control_api_access_token") as mock_at, mock.patch("parallel_web_tools.core.auth.login_flow") as mock_login, ): assert get_api_key() == "env_key" + mock_at.assert_not_called() mock_login.assert_not_called() @@ -769,7 +764,7 @@ def test_status_with_env_var(self, creds_file, monkeypatch): assert status["authenticated"] is True assert status["method"] == "environment" - def test_stored_beats_env_var_in_status(self, creds_file, monkeypatch): + def test_env_var_overrides_stored_in_status(self, creds_file, monkeypatch): credentials.save( credentials.Credentials( selected_org_id="org_a", @@ -779,7 +774,11 @@ def test_stored_beats_env_var_in_status(self, creds_file, monkeypatch): monkeypatch.setenv("PARALLEL_API_KEY", "env_key") status = get_auth_status() assert status["authenticated"] is True - assert status["method"] == "oauth" # stored credentials win + # Env var wins; stored creds are reported but flagged as overridden. + assert status["method"] == "environment" + assert status["env_var_set"] is True + assert status["has_stored_credentials"] is True + assert status["stored_overridden_by_env"] is True assert status["selected_org_name"] == "Acme Org" def test_status_with_stored_token(self, creds_file, legacy_file, monkeypatch): @@ -815,7 +814,7 @@ def test_logout_removes_token_no_revoke_when_missing(self, creds_file, legacy_fi mock_revoke.assert_not_called() assert not legacy_file.exists() - def test_login_flow_writes_structured_auth_without_touching_legacy_file( + def test_login_flow_writes_structured_auth_and_removes_legacy_file( self, creds_file, legacy_file, mock_ensure_client_id ): legacy_file.write_text(json.dumps({"access_token": "legacy_key"})) @@ -831,10 +830,10 @@ def test_login_flow_writes_structured_auth_without_touching_legacy_file( login_flow() auth_disk = json.loads(creds_file.read_text()) - legacy_disk = json.loads(legacy_file.read_text()) assert auth_disk["version"] == 1 assert auth_disk["selected_org_id"] == "org_real" - assert legacy_disk == {"access_token": "legacy_key"} + # Migration removes the legacy file once auth.json is durably written. + assert not legacy_file.exists() def test_logout_revokes_refresh_token_when_present(self, creds_file): credentials.save( @@ -969,45 +968,45 @@ def test_stored_token_used_as_fallback(self, creds_file, legacy_file, monkeypatc assert resolve_api_key() == "stored-token" assert creds_file.exists() - def test_stored_beats_env_var(self, creds_file, monkeypatch): + def test_env_var_beats_stored(self, creds_file, monkeypatch): credentials.set_api_key_for_org("org_a", "stored-key") monkeypatch.setenv("PARALLEL_API_KEY", "env-key") - assert resolve_api_key() == "stored-key" + assert resolve_api_key() == "env-key" # --------------------------------------------------------------------------- -# _is_headless +# is_headless # --------------------------------------------------------------------------- class TestIsHeadless: def test_ssh_client_detected(self): with mock.patch.dict(os.environ, {"SSH_CLIENT": "1.2.3.4 54321 22"}): - assert _is_headless() is True + assert is_headless() is True def test_ssh_tty_detected(self): with mock.patch.dict(os.environ, {"SSH_TTY": "/dev/pts/0"}): - assert _is_headless() is True + assert is_headless() is True def test_ci_detected(self): with mock.patch.dict(os.environ, {"CI": "true"}): - assert _is_headless() is True + assert is_headless() is True def test_docker_detected(self): with mock.patch("os.path.exists", return_value=True): - assert _is_headless() is True + assert is_headless() is True def test_container_env_detected(self): with mock.patch.dict(os.environ, {"container": "podman"}): with mock.patch("os.path.exists", return_value=False): - assert _is_headless() is True + assert is_headless() is True def test_normal_env_not_headless(self): env = {k: v for k, v in os.environ.items() if k not in ("SSH_CLIENT", "SSH_TTY", "CI", "container")} env["DISPLAY"] = ":0" with mock.patch.dict(os.environ, env, clear=True): with mock.patch("os.path.exists", return_value=False): - assert _is_headless() is False + assert is_headless() is False # --------------------------------------------------------------------------- diff --git a/tests/test_cli.py b/tests/test_cli.py index 18d89b8..03b992c 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -278,6 +278,32 @@ def test_auth_not_authenticated(self, runner, tmp_path): assert result.exit_code == 0 assert "Not authenticated" in result.output or "not" in result.output.lower() + def test_auth_warns_when_env_var_overrides_stored(self, runner): + """When PARALLEL_API_KEY is set AND stored creds exist, the override must be obvious.""" + status = { + "authenticated": True, + "method": "environment", + "env_var_set": True, + "has_stored_credentials": True, + "stored_overridden_by_env": True, + "token_file": "/tmp/auth.json", + "version": 1, + "selected_org_id": "org_123", + "selected_org_name": "Acme Org", + "has_control_api_tokens": True, + } + with mock.patch("parallel_web_tools.cli.commands.get_auth_status", return_value=status): + result = runner.invoke(main, ["auth"]) + + assert result.exit_code == 0 + # Active source labelled. + assert "PARALLEL_API_KEY" in result.output + # Override is loud. + assert "OVERRIDES" in result.output + # Stored org is shown so the user knows what's being shadowed. + assert "Acme Org" in result.output + assert "inactive" in result.output + def test_auth_json_includes_selected_org_name(self, runner): """Should include selected org name in JSON output for OAuth auth.""" status = { @@ -2628,7 +2654,7 @@ def test_sends_magic_link_and_skips_browser(self, runner): side_effect=_fake_get_api_key(info), ), mock.patch("parallel_web_tools.core.auth.send_magic_link") as mock_send, - mock.patch("parallel_web_tools.core.auth._ensure_client_id", return_value="cid_xyz"), + mock.patch("parallel_web_tools.core.auth.ensure_client_id", return_value="cid_xyz"), mock.patch("webbrowser.open") as mock_browser, ): result = runner.invoke(main, ["login", "email", "u@example.com"]) @@ -2648,7 +2674,7 @@ def test_json_mode_reports_magic_link_sent(self, runner): side_effect=_fake_get_api_key(info), ), mock.patch("parallel_web_tools.core.auth.send_magic_link"), - mock.patch("parallel_web_tools.core.auth._ensure_client_id", return_value="cid_xyz"), + mock.patch("parallel_web_tools.core.auth.ensure_client_id", return_value="cid_xyz"), mock.patch("webbrowser.open") as mock_browser, ): result = runner.invoke(main, ["login", "--json", "email", "u@example.com"]) @@ -2674,9 +2700,9 @@ def test_falls_back_when_magic_link_fails(self, runner): "parallel_web_tools.core.auth.send_magic_link", side_effect=Exception("SMTP unavailable"), ), - mock.patch("parallel_web_tools.core.auth._ensure_client_id", return_value="cid_xyz"), + mock.patch("parallel_web_tools.core.auth.ensure_client_id", return_value="cid_xyz"), mock.patch( - "parallel_web_tools.core.auth._is_headless", + "parallel_web_tools.core.auth.is_headless", return_value=True, # keep the test hermetic: don't attempt real browser open ), mock.patch("webbrowser.open") as mock_browser, @@ -2702,7 +2728,7 @@ def test_json_mode_reports_magic_link_error(self, runner): "parallel_web_tools.core.auth.send_magic_link", side_effect=Exception("SMTP unavailable"), ), - mock.patch("parallel_web_tools.core.auth._ensure_client_id", return_value="cid_xyz"), + mock.patch("parallel_web_tools.core.auth.ensure_client_id", return_value="cid_xyz"), ): result = runner.invoke(main, ["login", "--json", "email", "u@example.com"]) @@ -2722,7 +2748,7 @@ def test_no_email_still_opens_browser(self, runner): side_effect=_fake_get_api_key(info), ), mock.patch("parallel_web_tools.core.auth.send_magic_link") as mock_send, - mock.patch("parallel_web_tools.core.auth._is_headless", return_value=False), + mock.patch("parallel_web_tools.core.auth.is_headless", return_value=False), mock.patch("webbrowser.open") as mock_browser, ): result = runner.invoke(main, ["login"]) @@ -2743,7 +2769,7 @@ def test_opens_browser_with_google_login_hint(self, runner): side_effect=_fake_get_api_key(info), ), mock.patch("parallel_web_tools.core.auth.send_magic_link") as mock_send, - mock.patch("parallel_web_tools.core.auth._is_headless", return_value=False), + mock.patch("parallel_web_tools.core.auth.is_headless", return_value=False), mock.patch("webbrowser.open") as mock_browser, ): result = runner.invoke(main, ["login", "google"]) @@ -2766,7 +2792,7 @@ def test_opens_browser_with_sso_hint_and_separate_email_param(self, runner): side_effect=_fake_get_api_key(info), ), mock.patch("parallel_web_tools.core.auth.send_magic_link") as mock_send, - mock.patch("parallel_web_tools.core.auth._is_headless", return_value=False), + mock.patch("parallel_web_tools.core.auth.is_headless", return_value=False), mock.patch("webbrowser.open") as mock_browser, ): result = runner.invoke(main, ["login", "sso", "u@example.com"]) @@ -2951,7 +2977,7 @@ def fake_add(token, amount_cents, idempotency_key): with ( mock.patch("parallel_web_tools.cli.commands.get_control_api_access_token", return_value="atk"), mock.patch("parallel_web_tools.core.service.add_balance", side_effect=fake_add), - mock.patch("parallel_web_tools.core.auth._ensure_client_id", return_value="cid_xyz"), + mock.patch("parallel_web_tools.core.auth.ensure_client_id", return_value="cid_xyz"), mock.patch("parallel_web_tools.cli.commands.time.time", return_value=1_700_000_123.0), ): result = runner.invoke(main, ["balance", "--json", "add", "100"]) @@ -2967,7 +2993,7 @@ def test_console_output_shows_charge_and_new_balance(self, runner): with ( mock.patch("parallel_web_tools.cli.commands.get_control_api_access_token", return_value="atk"), mock.patch("parallel_web_tools.core.service.add_balance", return_value=balance), - mock.patch("parallel_web_tools.core.auth._ensure_client_id", return_value="cid_xyz"), + mock.patch("parallel_web_tools.core.auth.ensure_client_id", return_value="cid_xyz"), ): result = runner.invoke(main, ["balance", "add", "100"]) @@ -2980,12 +3006,12 @@ def test_explicit_idempotency_key_overrides_derivation(self, runner): with ( mock.patch("parallel_web_tools.cli.commands.get_control_api_access_token", return_value="atk"), mock.patch("parallel_web_tools.core.service.add_balance", return_value=balance) as mock_add, - mock.patch("parallel_web_tools.core.auth._ensure_client_id") as mock_ensure, + mock.patch("parallel_web_tools.core.auth.ensure_client_id") as mock_ensure, ): result = runner.invoke(main, ["balance", "add", "100", "--idempotency-key", "fixed-key"]) assert result.exit_code == 0 - # _ensure_client_id must NOT be called when an explicit key was provided. + # ensure_client_id must NOT be called when an explicit key was provided. mock_ensure.assert_not_called() assert mock_add.call_args.args[2] == "fixed-key" @@ -3002,7 +3028,7 @@ def capture_key(token, amount_cents, idempotency_key): with ( mock.patch("parallel_web_tools.cli.commands.get_control_api_access_token", return_value="atk"), mock.patch("parallel_web_tools.core.service.add_balance", side_effect=capture_key), - mock.patch("parallel_web_tools.core.auth._ensure_client_id", return_value="cid_xyz"), + mock.patch("parallel_web_tools.core.auth.ensure_client_id", return_value="cid_xyz"), mock.patch("parallel_web_tools.cli.commands.time.time", side_effect=[1_700_000_100, 1_700_000_399]), ): assert runner.invoke(main, ["balance", "add", "100"]).exit_code == 0 @@ -3020,7 +3046,7 @@ def capture_key(token, amount_cents, idempotency_key): with ( mock.patch("parallel_web_tools.cli.commands.get_control_api_access_token", return_value="atk"), mock.patch("parallel_web_tools.core.service.add_balance", side_effect=capture_key), - mock.patch("parallel_web_tools.core.auth._ensure_client_id", return_value="cid_xyz"), + mock.patch("parallel_web_tools.core.auth.ensure_client_id", return_value="cid_xyz"), mock.patch("parallel_web_tools.cli.commands.time.time", side_effect=[1_700_000_100, 1_700_000_400]), ): runner.invoke(main, ["balance", "add", "100"]) @@ -3034,7 +3060,7 @@ def test_zero_amount_passes_through_to_service(self, runner): with ( mock.patch("parallel_web_tools.cli.commands.get_control_api_access_token", return_value="atk"), mock.patch("parallel_web_tools.core.service.add_balance", return_value=balance) as mock_add, - mock.patch("parallel_web_tools.core.auth._ensure_client_id", return_value="cid_xyz"), + mock.patch("parallel_web_tools.core.auth.ensure_client_id", return_value="cid_xyz"), ): result = runner.invoke(main, ["balance", "add", "0"]) @@ -3046,7 +3072,7 @@ def test_large_amount_passes_through_to_service(self, runner): with ( mock.patch("parallel_web_tools.cli.commands.get_control_api_access_token", return_value="atk"), mock.patch("parallel_web_tools.core.service.add_balance", return_value=balance) as mock_add, - mock.patch("parallel_web_tools.core.auth._ensure_client_id", return_value="cid_xyz"), + mock.patch("parallel_web_tools.core.auth.ensure_client_id", return_value="cid_xyz"), ): result = runner.invoke(main, ["balance", "add", "1001"]) @@ -3061,7 +3087,7 @@ def test_reauth_required_exits_auth_error(self, runner): "parallel_web_tools.cli.commands.get_control_api_access_token", side_effect=ReauthenticationRequired("not logged in"), ), - mock.patch("parallel_web_tools.core.auth._ensure_client_id", return_value="cid_xyz"), + mock.patch("parallel_web_tools.core.auth.ensure_client_id", return_value="cid_xyz"), ): result = runner.invoke(main, ["balance", "add", "100"]) @@ -3074,7 +3100,7 @@ def test_service_api_error_exits_api_error(self, runner): with ( mock.patch("parallel_web_tools.cli.commands.get_control_api_access_token", return_value="atk"), mock.patch("parallel_web_tools.core.service.add_balance", side_effect=ServiceApiError("card declined")), - mock.patch("parallel_web_tools.core.auth._ensure_client_id", return_value="cid_xyz"), + mock.patch("parallel_web_tools.core.auth.ensure_client_id", return_value="cid_xyz"), ): result = runner.invoke(main, ["balance", "add", "100"]) diff --git a/tests/test_credentials.py b/tests/test_credentials.py index f21e42d..097b532 100644 --- a/tests/test_credentials.py +++ b/tests/test_credentials.py @@ -66,7 +66,7 @@ def test_load_non_dict_returns_none(self, creds_file): creds_file.write_text(json.dumps(["a", "b"])) assert load() is None - def test_load_v0_migrates_forward_to_auth_without_rewriting_legacy_file(self, creds_file, legacy_file): + def test_load_v0_migrates_forward_and_removes_legacy_file(self, creds_file, legacy_file): legacy_file.parent.mkdir(parents=True, exist_ok=True) legacy_file.write_text(json.dumps({"access_token": "tok_v0"})) @@ -82,7 +82,8 @@ def test_load_v0_migrates_forward_to_auth_without_rewriting_legacy_file(self, cr assert auth_disk["version"] == CURRENT_VERSION assert auth_disk["selected_org_id"] == LEGACY_ORG_ID assert auth_disk["orgs"][LEGACY_ORG_ID]["api_key"] == "tok_v0" - assert json.loads(legacy_file.read_text()) == {"access_token": "tok_v0"} + # The legacy file should have been removed once migration succeeded. + assert not legacy_file.exists() def test_load_v1_roundtrip(self, creds_file): original = Credentials( diff --git a/tests/test_enrichment.py b/tests/test_enrichment.py index 2c88729..32da38a 100644 --- a/tests/test_enrichment.py +++ b/tests/test_enrichment.py @@ -29,9 +29,9 @@ def test_explicit_api_key(self): assert result == "test-key-123" def test_env_var_fallback(self): - """Should use PARALLEL_API_KEY env var when no stored credentials.""" + """Should use PARALLEL_API_KEY env var when no explicit key. Env beats stored creds.""" with mock.patch.dict(os.environ, {"PARALLEL_API_KEY": "env-key-456"}): - with mock.patch("parallel_web_tools.core.credentials.get_selected_api_key", return_value=None): + with mock.patch("parallel_web_tools.core.credentials.get_selected_api_key", return_value="stored-key"): result = resolve_api_key() assert result == "env-key-456" diff --git a/tests/test_service.py b/tests/test_service.py index 786d249..e7f4bd3 100644 --- a/tests/test_service.py +++ b/tests/test_service.py @@ -318,20 +318,20 @@ class TestAddBalance: def test_posts_expected_body_and_parses_response(self): captured: dict = {} with _patch_urlopen(_balance_response(credit_balance_cents=1600), capture=captured): - resp = add_balance("at_xyz", amount_cents=100, idempotency_key="key_123") + resp = add_balance("at_xyz", amount_cents=100, idempotency_key="key_1234") assert captured["method"] == "POST" assert captured["url"].endswith("/service/v1/balance/add") - assert json.loads(captured["body"]) == {"amount_cents": 100, "idempotency_key": "key_123"} + assert json.loads(captured["body"]) == {"amount_cents": 100, "idempotency_key": "key_1234"} assert any(v == "Bearer at_xyz" for v in captured["headers"].values()) assert resp.credit_balance_cents == 1600 def test_raises_on_http_error(self): with _patch_urlopen(_http_error(402, {"error": "card_declined"})): with pytest.raises(ServiceApiError, match="failed: 402"): - add_balance("at_xyz", amount_cents=100, idempotency_key="k") + add_balance("at_xyz", amount_cents=100, idempotency_key="test-key-1") def test_raises_on_malformed_response(self): with _patch_urlopen({"credit_balance_cents": 10}): with pytest.raises(ServiceApiError, match="Unexpected /service/v1/balance/add response"): - add_balance("at_xyz", amount_cents=100, idempotency_key="k") + add_balance("at_xyz", amount_cents=100, idempotency_key="test-key-2") diff --git a/uv.lock b/uv.lock index 6b61a31..3dab8b0 100644 --- a/uv.lock +++ b/uv.lock @@ -2,15 +2,10 @@ version = 1 revision = 3 requires-python = ">=3.10" resolution-markers = [ - "python_full_version >= '3.14' and sys_platform == 'win32'", - "python_full_version >= '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version == '3.13.*' and sys_platform == 'win32'", - "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform == 'win32'", - "python_full_version == '3.13.*' and sys_platform == 'emscripten'", - "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform == 'emscripten'", - "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.14'", + "python_full_version == '3.13.*'", + "python_full_version == '3.12.*'", + "python_full_version == '3.11.*'", "python_full_version < '3.11'", ] @@ -128,30 +123,30 @@ wheels = [ [[package]] name = "boto3" -version = "1.43.5" +version = "1.43.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "botocore" }, { name = "jmespath" }, { name = "s3transfer" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/71/b0/90ba01763dd483bb040d0815dc0ba893421e3f5926672ceab9acbb73b23f/boto3-1.43.5.tar.gz", hash = "sha256:414be7868f25c3b6a0232301c8ab40347911b6b191926b61f00a63f89b97b2bc", size = 113150, upload-time = "2026-05-06T19:56:49.629Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b7/65/47670987f2f9e181397872c7ee6415b7b95156d711b7eab6c55f66e575bc/boto3-1.43.0.tar.gz", hash = "sha256:80d44a943ef90aba7958ab31d30c155c198acc8a9581b5846b3878b2c8951086", size = 113143, upload-time = "2026-04-29T22:07:49.084Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/10/bb/347307758c2003783df1d9a9b07596928d05a6ca0e17790cea3b18105244/boto3-1.43.5-py3-none-any.whl", hash = "sha256:aa8a296c8db55d812767b282cfe4c7977f0b0eeaa709abdaeb368b9c738e901f", size = 140502, upload-time = "2026-05-06T19:56:46.626Z" }, + { url = "https://files.pythonhosted.org/packages/b3/a0/3e6a0b1c1ea6bec76f71473727ef27abf3cd40e9709b3ebcbfbcfaae6f79/boto3-1.43.0-py3-none-any.whl", hash = "sha256:8ebe03754a4b73a5cb6ec2f14cca03ac33bd4760d0adea53da4724845130258b", size = 140497, upload-time = "2026-04-29T22:07:46.216Z" }, ] [[package]] name = "botocore" -version = "1.43.5" +version = "1.43.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jmespath" }, { name = "python-dateutil" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6d/a2/1285a22bf157f9e97a8fd236daea95d9b14cc8425ae5f8a616badf948408/botocore-1.43.5.tar.gz", hash = "sha256:5c7207816ab5e48382adcb2a64db388fa4abe9ee1d23f72c82ae62c51a0bc84e", size = 15321290, upload-time = "2026-05-06T19:56:35.658Z" } +sdist = { url = "https://files.pythonhosted.org/packages/28/79/2f4be1896db3db7ccf44504253a175d56b6bd6b669619edc5147d1aa21ea/botocore-1.43.0.tar.gz", hash = "sha256:e933b31a2d644253e1d029d7d39e99ba41b87e29300534f189744cc438cdf928", size = 15286817, upload-time = "2026-04-29T22:07:31.723Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/82/d2/99f1741b12e3cdba2e5370f6dafaab743a373c6f83592601ec75ff2cc47f/botocore-1.43.5-py3-none-any.whl", hash = "sha256:a1df6e0c6346735936f42e6b99f3b28f1e9397731c0bc2563c617df7965a0dc0", size = 15002116, upload-time = "2026-05-06T19:56:29.993Z" }, + { url = "https://files.pythonhosted.org/packages/bf/4b/afc1fef8a43bafb139f57f73bbd70df82807af5934321e8112ae50668827/botocore-1.43.0-py3-none-any.whl", hash = "sha256:cc5b15eaec3c6eac05d8012cb5ef17ebe891beb88a16ca13c374bfaece1241e6", size = 14970102, upload-time = "2026-04-29T22:07:27Z" }, ] [[package]] @@ -509,62 +504,62 @@ toml = [ [[package]] name = "cryptography" -version = "48.0.0" +version = "47.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9f/a9/db8f313fdcd85d767d4973515e1db101f9c71f95fced83233de224673757/cryptography-48.0.0.tar.gz", hash = "sha256:5c3932f4436d1cccb036cb0eaef46e6e2db91035166f1ad6505c3c9d5a635920", size = 832984, upload-time = "2026-05-04T22:59:38.133Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/df/3d/01f6dd9190170a5a241e0e98c2d04be3664a9e6f5b9b872cde63aff1c3dd/cryptography-48.0.0-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:0c558d2cdffd8f4bbb30fc7134c74d2ca9a476f830bb053074498fbc86f41ed6", size = 8001587, upload-time = "2026-05-04T22:57:36.803Z" }, - { url = "https://files.pythonhosted.org/packages/b2/6e/e90527eef33f309beb811cf7c982c3aeffcce8e3edb178baa4ca3ae4a6fa/cryptography-48.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f5333311663ea94f75dd408665686aaf426563556bb5283554a3539177e03b8c", size = 4690433, upload-time = "2026-05-04T22:57:40.373Z" }, - { url = "https://files.pythonhosted.org/packages/90/04/673510ed51ddff56575f306cf1617d80411ee76831ccd3097599140efdfe/cryptography-48.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7995ef305d7165c3f11ae07f2517e5a4f1d5c18da1376a0a9ed496336b69e5f3", size = 4710620, upload-time = "2026-05-04T22:57:42.935Z" }, - { url = "https://files.pythonhosted.org/packages/14/d5/e9c4ef932c8d800490c34d8bd589d64a31d5890e27ec9e9ad532be893294/cryptography-48.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:40ba1f85eaa6959837b1d51c9767e230e14612eea4ef110ee8854ada22da1bf5", size = 4696283, upload-time = "2026-05-04T22:57:45.294Z" }, - { url = "https://files.pythonhosted.org/packages/0c/29/174b9dfb60b12d59ecfc6cfa04bc88c21b42a54f01b8aae09bb6e51e4c7f/cryptography-48.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:369a6348999f94bbd53435c894377b20ab95f25a9065c283570e70150d8abc3c", size = 5296573, upload-time = "2026-05-04T22:57:47.933Z" }, - { url = "https://files.pythonhosted.org/packages/95/38/0d29a6fd7d0d1373f0c0c88a04ba20e359b257753ac497564cd660fc1d55/cryptography-48.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a0e692c683f4df67815a2d258b324e66f4738bd7a96a218c826dce4f4bd05d8f", size = 4743677, upload-time = "2026-05-04T22:57:50.067Z" }, - { url = "https://files.pythonhosted.org/packages/30/be/eef653013d5c63b6a490529e0316f9ac14a37602965d4903efed1399f32b/cryptography-48.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:18349bbc56f4743c8b12dc32e2bccb2cf83ee8b69a3bba74ef8ae857e26b3d25", size = 4330808, upload-time = "2026-05-04T22:57:52.301Z" }, - { url = "https://files.pythonhosted.org/packages/84/9e/500463e87abb7a0a0f9f256ec21123ecde0a7b5541a15e840ea54551fd81/cryptography-48.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:7e8eac43dfca5c4cccc6dad9a80504436fca53bb9bc3100a2386d730fbe6b602", size = 4695941, upload-time = "2026-05-04T22:57:54.603Z" }, - { url = "https://files.pythonhosted.org/packages/e3/dc/7303087450c2ec9e7fbb750e17c2abfbc658f23cbd0e54009509b7cc4091/cryptography-48.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9ccdac7d40688ecb5a3b4a604b8a88c8002e3442d6c60aead1db2a89a041560c", size = 5252579, upload-time = "2026-05-04T22:57:57.207Z" }, - { url = "https://files.pythonhosted.org/packages/d0/c0/7101d3b7215edcdc90c45da544961fd8ed2d6448f77577460fa75a8443f7/cryptography-48.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:bd72e68b06bb1e96913f97dd4901119bc17f39d4586a5adf2d3e47bc2b9d58b5", size = 4743326, upload-time = "2026-05-04T22:57:59.535Z" }, - { url = "https://files.pythonhosted.org/packages/ac/d8/5b833bad13016f562ab9d063d68199a4bd121d18458e439515601d3357ec/cryptography-48.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:59baa2cb386c4f0b9905bd6eb4c2a79a69a128408fd31d32ca4d7102d4156321", size = 4826672, upload-time = "2026-05-04T22:58:01.996Z" }, - { url = "https://files.pythonhosted.org/packages/98/e1/7074eb8bf3c135558c73fc2bcf0f5633f912e6fb87e868a55c454080ef09/cryptography-48.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9249e3cd978541d665967ac2cb2787fd6a62bddf1e75b3e347a594d7dacf4f74", size = 4972574, upload-time = "2026-05-04T22:58:03.968Z" }, - { url = "https://files.pythonhosted.org/packages/04/70/e5a1b41d325f797f39427aa44ef8baf0be500065ab6d8e10369d850d4a4f/cryptography-48.0.0-cp311-abi3-win32.whl", hash = "sha256:9c459db21422be75e2809370b829a87eb37f74cd785fc4aa9ea1e5f43b47cda4", size = 3294868, upload-time = "2026-05-04T22:58:06.467Z" }, - { url = "https://files.pythonhosted.org/packages/f4/ac/8ac51b4a5fc5932eb7ee5c517ba7dc8cd834f0048962b6b352f00f41ebf9/cryptography-48.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:5b012212e08b8dd5edc78ef54da83dd9892fd9105323b3993eff6bea65dc21d7", size = 3817107, upload-time = "2026-05-04T22:58:08.845Z" }, - { url = "https://files.pythonhosted.org/packages/6b/84/70e3feea9feea87fd7cbe77efb2712ae1e3e6edf10749dc6e95f4e60e455/cryptography-48.0.0-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:3cb07a3ed6431663cd321ea8a000a1314c74211f823e4177fefa2255e057d1ec", size = 7986556, upload-time = "2026-05-04T22:58:11.172Z" }, - { url = "https://files.pythonhosted.org/packages/89/6e/18e07a618bb5442ba10cf4df16e99c071365528aa570dfcb8c02e25a303b/cryptography-48.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8c7378637d7d88016fa6791c159f698b3d3eed28ebf844ac36b9dc04a14dae18", size = 4684776, upload-time = "2026-05-04T22:58:13.712Z" }, - { url = "https://files.pythonhosted.org/packages/be/6a/4ea3b4c6c6759794d5ee2103c304a5076dc4b19ae1f9fe47dba439e159e9/cryptography-48.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc90c0b39b2e3c65ef52c804b72e3c58f8a04ab2a1871272798e5f9572c17d20", size = 4698121, upload-time = "2026-05-04T22:58:16.448Z" }, - { url = "https://files.pythonhosted.org/packages/2f/59/6ff6ad6cae03bb887da2a5860b2c9805f8dac969ef01ce563336c49bd1d1/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:76341972e1eff8b4bea859f09c0d3e64b96ce931b084f9b9b7db8ef364c30eff", size = 4690042, upload-time = "2026-05-04T22:58:18.544Z" }, - { url = "https://files.pythonhosted.org/packages/ca/b4/fc334ed8cfd705aca282fe4d8f5ae64a8e0f74932e9feecb344610cf6e4d/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:55b7718303bf06a5753dcdccf2f3945cf18ad7bffde41b61226e4db31ab89a9c", size = 5282526, upload-time = "2026-05-04T22:58:20.75Z" }, - { url = "https://files.pythonhosted.org/packages/11/08/9f8c5386cc4cd90d8255c7cdd0f5baf459a08502a09de30dc51f553d38dc/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:a64697c641c7b1b2178e573cbc31c7c6684cd56883a478d75143dbb7118036db", size = 4733116, upload-time = "2026-05-04T22:58:23.627Z" }, - { url = "https://files.pythonhosted.org/packages/b8/77/99307d7574045699f8805aa500fa0fb83422d115b5400a064ddd306d7750/cryptography-48.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:561215ea3879cb1cbbf272867e2efda62476f240fb58c64de6b393ae19246741", size = 4316030, upload-time = "2026-05-04T22:58:25.581Z" }, - { url = "https://files.pythonhosted.org/packages/fd/36/a608b98337af3cb2aff4818e406649d30572b7031918b04c87d979495348/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:ad64688338ed4bc1a6618076ba75fd7194a5f1797ac60b47afe926285adb3166", size = 4689640, upload-time = "2026-05-04T22:58:27.747Z" }, - { url = "https://files.pythonhosted.org/packages/dd/a6/825010a291b4438aecc1f568bc428189fc1175515223632477c07dc0a6df/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:906cbf0670286c6e0044156bc7d4af9cbb0ef6db9f73e52c3ec56ba6bdde5336", size = 5237657, upload-time = "2026-05-04T22:58:29.848Z" }, - { url = "https://files.pythonhosted.org/packages/b9/09/4e76a09b4caa29aad535ddc806f5d4c5d01885bd978bd984fbc6ca032cae/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:ea8990436d914540a40ab24b6a77c0969695ed52f4a4874c5137ccf7045a7057", size = 4732362, upload-time = "2026-05-04T22:58:32.009Z" }, - { url = "https://files.pythonhosted.org/packages/18/78/444fa04a77d0cb95f417dda20d450e13c56ba8e5220fc892a1658f44f882/cryptography-48.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c18684a7f0cc9a3cb60328f496b8e3372def7c5d2df39ac267878b05565aaaae", size = 4819580, upload-time = "2026-05-04T22:58:34.254Z" }, - { url = "https://files.pythonhosted.org/packages/38/85/ea67067c70a1fd4be2c63d35eeed82658023021affccc7b17705f8527dd2/cryptography-48.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9be5aafa5736574f8f15f262adc81b2a9869e2cfe9014d52a44633905b40d52c", size = 4963283, upload-time = "2026-05-04T22:58:36.376Z" }, - { url = "https://files.pythonhosted.org/packages/75/54/cc6d0f3deac3e81c7f847e8a189a12b6cdd65059b43dad25d4316abd849a/cryptography-48.0.0-cp314-cp314t-win32.whl", hash = "sha256:c17dfe85494deaeddc5ce251aebd1d60bbe6afc8b62071bb0b469431a000124f", size = 3270954, upload-time = "2026-05-04T22:58:38.791Z" }, - { url = "https://files.pythonhosted.org/packages/49/67/cc947e288c0758a4e5473d1dcb743037ab7785541265a969240b8885441a/cryptography-48.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27241b1dc9962e056062a8eef1991d02c3a24569c95975bd2322a8a52c6e5e12", size = 3797313, upload-time = "2026-05-04T22:58:40.746Z" }, - { url = "https://files.pythonhosted.org/packages/f2/63/61d4a4e1c6b6bab6ce1e213cd36a24c415d90e76d78c5eb8577c5541d2e8/cryptography-48.0.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:58d00498e8933e4a194f3076aee1b4a97dfec1a6da444535755822fe5d8b0b86", size = 7983482, upload-time = "2026-05-04T22:58:43.769Z" }, - { url = "https://files.pythonhosted.org/packages/d5/ac/f5b5995b87770c693e2596559ffafe195b4033a57f14a82268a2842953f3/cryptography-48.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:614d0949f4790582d2cc25553abd09dd723025f0c0e7c67376a1d77196743d6e", size = 4683266, upload-time = "2026-05-04T22:58:46.064Z" }, - { url = "https://files.pythonhosted.org/packages/ec/c6/8b14f67e18338fbc4adb76f66c001f5c3610b3e2d1837f268f47a347dbbb/cryptography-48.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7ce4bfae76319a532a2dc68f82cc32f5676ee792a983187dac07183690e5c66f", size = 4696228, upload-time = "2026-05-04T22:58:48.22Z" }, - { url = "https://files.pythonhosted.org/packages/ea/73/f808fbae9514bd91b47875b003f13e284c8c6bdfd904b7944e803937eec1/cryptography-48.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:2eb992bbd4661238c5a397594c83f5b4dc2bc5b848c365c8f991b6780efcc5c7", size = 4689097, upload-time = "2026-05-04T22:58:50.9Z" }, - { url = "https://files.pythonhosted.org/packages/93/01/d86632d7d28db8ae83221995752eeb6639ffb374c2d22955648cf8d52797/cryptography-48.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:22a5cb272895dce158b2cacdfdc3debd299019659f42947dbdac6f32d68fe832", size = 5283582, upload-time = "2026-05-04T22:58:53.017Z" }, - { url = "https://files.pythonhosted.org/packages/02/e1/50edc7a50334807cc4791fc4a0ce7468b4a1416d9138eab358bfc9a3d70b/cryptography-48.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2b4d59804e8408e2fea7d1fbaf218e5ec984325221db76e6a241a9abd6cdd95c", size = 4730479, upload-time = "2026-05-04T22:58:55.611Z" }, - { url = "https://files.pythonhosted.org/packages/6f/af/99a582b1b1641ff5911ac559beb45097cf79efd4ead4657f578ef1af2d47/cryptography-48.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:984a20b0f62a26f48a3396c72e4bc34c66e356d356bf370053066b3b6d54634a", size = 4326481, upload-time = "2026-05-04T22:58:57.607Z" }, - { url = "https://files.pythonhosted.org/packages/90/ee/89aa26a06ef0a7d7611788ffd571a7c50e368cc6a4d5eef8b4884e866edb/cryptography-48.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:5a5ed8fde7a1d09376ca0b40e68cd59c69fe23b1f9768bd5824f54681626032a", size = 4688713, upload-time = "2026-05-04T22:59:00.077Z" }, - { url = "https://files.pythonhosted.org/packages/70/ba/bcb1b0bb7a33d4c7c0c4d4c7874b4a62ae4f56113a5f4baefa362dfb1f0f/cryptography-48.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:8cd666227ef7af430aa5914a9910e0ddd703e75f039cef0825cd0da71b6b711a", size = 5238165, upload-time = "2026-05-04T22:59:02.317Z" }, - { url = "https://files.pythonhosted.org/packages/c9/70/ca4003b1ce5ca3dc3186ada51908c8a9b9ff7d5cab83cc0d43ee14ec144f/cryptography-48.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9071196d81abc88b3516ac8cdfad32e2b66dd4a5393a8e68a961e9161ddc6239", size = 4729947, upload-time = "2026-05-04T22:59:05.255Z" }, - { url = "https://files.pythonhosted.org/packages/44/a0/4ec7cf774207905aef1a8d11c3750d5a1db805eb380ee4e16df317870128/cryptography-48.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1e2d54c8be6152856a36f0882ab231e70f8ec7f14e93cf87db8a2ed056bf160c", size = 4822059, upload-time = "2026-05-04T22:59:07.802Z" }, - { url = "https://files.pythonhosted.org/packages/1e/75/a2e55f99c16fcac7b5d6c1eb19ad8e00799854d6be5ca845f9259eae1681/cryptography-48.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a5da777e32ffed6f85a7b2b3f7c5cbc88c146bfcd0a1d7baf5fcc6c52ee35dd4", size = 4960575, upload-time = "2026-05-04T22:59:09.851Z" }, - { url = "https://files.pythonhosted.org/packages/b8/23/6e6f32143ab5d8b36ca848a502c4bcd477ae75b9e1677e3530d669062578/cryptography-48.0.0-cp39-abi3-win32.whl", hash = "sha256:77a2ccbbe917f6710e05ba9adaa25fb5075620bf3ea6fb751997875aff4ae4bd", size = 3279117, upload-time = "2026-05-04T22:59:12.019Z" }, - { url = "https://files.pythonhosted.org/packages/9d/9a/0fea98a70cf1749d41d738836f6349d97945f7c89433a259a6c2642eefeb/cryptography-48.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:16cd65b9330583e4619939b3a3843eec1e6e789744bb01e7c7e2e62e33c239c8", size = 3792100, upload-time = "2026-05-04T22:59:14.884Z" }, - { url = "https://files.pythonhosted.org/packages/be/d2/024b5e06be9d44cb021fb0e1a03d34d63989cf56a0fe62f3dfbab695b9b4/cryptography-48.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:84cf79f0dc8b36ac5da873481716e87aef31fcfa0444f9e1d8b4b2cece142855", size = 3950391, upload-time = "2026-05-04T22:59:17.415Z" }, - { url = "https://files.pythonhosted.org/packages/bc/17/3861e17c56fa0fd37491a14a8673fdb77c57fc5693cafe745ea8b06dba75/cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:fdfef35d751d510fcef5252703621574364fec16418c4a1e5e1055248401054b", size = 4637126, upload-time = "2026-05-04T22:59:20.197Z" }, - { url = "https://files.pythonhosted.org/packages/f0/0a/7e226dbff530f21480727eb764973a7bff2b912f8e15cd4f129e71b56d1d/cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:0890f502ddf7d9c6426129c3f49f5c0a39278ed7cd6322c8755ffca6ee675a13", size = 4667270, upload-time = "2026-05-04T22:59:22.647Z" }, - { url = "https://files.pythonhosted.org/packages/3b/f2/5a72274ca9f1b2a8b44a662ee0bf1b435909deb473d6f97bcd035bcdbc71/cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:ecde28a596bead48b0cfd2a1b4416c3d43074c2d785e3a398d7ec1fc4d0f7fbb", size = 4636797, upload-time = "2026-05-04T22:59:24.912Z" }, - { url = "https://files.pythonhosted.org/packages/b4/e1/48cedb2fe63626e91ded1edad159e2a4fb8b6906c4425eb7749673077ce7/cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:4defde8685ae324a9eb9d818717e93b4638ef67070ac9bc15b8ca85f63048355", size = 4666800, upload-time = "2026-05-04T22:59:27.474Z" }, - { url = "https://files.pythonhosted.org/packages/a2/ca/7e8365deec19afb2b2c7be7c1c0aa8f99633b54e90c570999acda93260fc/cryptography-48.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:db63bf618e5dea46c07de12e900fe1cdd2541e6dc9dbae772a70b7d4d4765f6a", size = 3739536, upload-time = "2026-05-04T22:59:29.61Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/ef/b2/7ffa7fe8207a8c42147ffe70c3e360b228160c1d85dc3faff16aaa3244c0/cryptography-47.0.0.tar.gz", hash = "sha256:9f8e55fe4e63613a5e1cc5819030f27b97742d720203a087802ce4ce9ceb52bb", size = 830863, upload-time = "2026-04-24T19:54:57.056Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/98/40dfe932134bdcae4f6ab5927c87488754bf9eb79297d7e0070b78dd58e9/cryptography-47.0.0-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:160ad728f128972d362e714054f6ba0067cab7fb350c5202a9ae8ae4ce3ef1a0", size = 7912214, upload-time = "2026-04-24T19:53:03.864Z" }, + { url = "https://files.pythonhosted.org/packages/34/c6/2733531243fba725f58611b918056b277692f1033373dcc8bd01af1c05d4/cryptography-47.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b9a8943e359b7615db1a3ba587994618e094ff3d6fa5a390c73d079ce18b3973", size = 4644617, upload-time = "2026-04-24T19:53:06.909Z" }, + { url = "https://files.pythonhosted.org/packages/00/e3/b27be1a670a9b87f855d211cf0e1174a5d721216b7616bd52d8581d912ed/cryptography-47.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f5c15764f261394b22aef6b00252f5195f46f2ca300bec57149474e2538b31f8", size = 4668186, upload-time = "2026-04-24T19:53:09.053Z" }, + { url = "https://files.pythonhosted.org/packages/81/b9/8443cfe5d17d482d348cee7048acf502bb89a51b6382f06240fd290d4ca3/cryptography-47.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:9c59ab0e0fa3a180a5a9c59f3a5abe3ef90d474bc56d7fadfbe80359491b615b", size = 4651244, upload-time = "2026-04-24T19:53:11.217Z" }, + { url = "https://files.pythonhosted.org/packages/5d/5e/13ed0cdd0eb88ba159d6dd5ebfece8cb901dbcf1ae5ac4072e28b55d3153/cryptography-47.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:34b4358b925a5ea3e14384ca781a2c0ef7ac219b57bb9eacc4457078e2b19f92", size = 5252906, upload-time = "2026-04-24T19:53:13.532Z" }, + { url = "https://files.pythonhosted.org/packages/64/16/ed058e1df0f33d440217cd120d41d5dda9dd215a80b8187f68483185af82/cryptography-47.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:0024b87d47ae2399165a6bfb20d24888881eeab83ae2566d62467c5ff0030ce7", size = 4701842, upload-time = "2026-04-24T19:53:15.618Z" }, + { url = "https://files.pythonhosted.org/packages/02/e0/3d30986b30fdbd9e969abbdf8ba00ed0618615144341faeb57f395a084fe/cryptography-47.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:1e47422b5557bb82d3fff997e8d92cff4e28b9789576984f08c248d2b3535d93", size = 4289313, upload-time = "2026-04-24T19:53:17.755Z" }, + { url = "https://files.pythonhosted.org/packages/df/fd/32db38e3ad0cb331f0691cb4c7a8a6f176f679124dee746b3af6633db4d9/cryptography-47.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:6f29f36582e6151d9686235e586dd35bb67491f024767d10b842e520dc6a07ac", size = 4650964, upload-time = "2026-04-24T19:53:20.062Z" }, + { url = "https://files.pythonhosted.org/packages/86/53/5395d944dfd48cb1f67917f533c609c34347185ef15eb4308024c876f274/cryptography-47.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:a9b761f012a943b7de0e828843c5688d0de94a0578d44d6c85a1bae32f87791f", size = 5207817, upload-time = "2026-04-24T19:53:22.498Z" }, + { url = "https://files.pythonhosted.org/packages/34/4f/e5711b28e1901f7d480a2b1b688b645aa4c77c73f10731ed17e7f7db3f0d/cryptography-47.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:4e1de79e047e25d6e9f8cea71c86b4a53aced64134f0f003bbcbf3655fd172c8", size = 4701544, upload-time = "2026-04-24T19:53:24.356Z" }, + { url = "https://files.pythonhosted.org/packages/22/22/c8ddc25de3010fc8da447648f5a092c40e7a8fadf01dd6d255d9c0b9373d/cryptography-47.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ef6b3634087f18d2155b1e8ce264e5345a753da2c5fa9815e7d41315c90f8318", size = 4783536, upload-time = "2026-04-24T19:53:26.665Z" }, + { url = "https://files.pythonhosted.org/packages/66/b6/d4a68f4ea999c6d89e8498579cba1c5fcba4276284de7773b17e4fa69293/cryptography-47.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:11dbb9f50a0f1bb9757b3d8c27c1101780efb8f0bdecfb12439c22a74d64c001", size = 4926106, upload-time = "2026-04-24T19:53:28.686Z" }, + { url = "https://files.pythonhosted.org/packages/54/ed/5f524db1fade9c013aa618e1c99c6ed05e8ffc9ceee6cda22fed22dda3f4/cryptography-47.0.0-cp311-abi3-win32.whl", hash = "sha256:7fda2f02c9015db3f42bb8a22324a454516ed10a8c29ca6ece6cdbb5efe2a203", size = 3258581, upload-time = "2026-04-24T19:53:31.058Z" }, + { url = "https://files.pythonhosted.org/packages/b2/dc/1b901990b174786569029f67542b3edf72ac068b6c3c8683c17e6a2f5363/cryptography-47.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:f5c3296dab66202f1b18a91fa266be93d6aa0c2806ea3d67762c69f60adc71aa", size = 3775309, upload-time = "2026-04-24T19:53:33.054Z" }, + { url = "https://files.pythonhosted.org/packages/14/88/7aa18ad9c11bc87689affa5ce4368d884b517502d75739d475fc6f4a03c7/cryptography-47.0.0-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:be12cb6a204f77ed968bcefe68086eb061695b540a3dd05edac507a3111b25f0", size = 7904299, upload-time = "2026-04-24T19:53:35.003Z" }, + { url = "https://files.pythonhosted.org/packages/07/55/c18f75724544872f234678fdedc871391722cb34a2aee19faa9f63100bb2/cryptography-47.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2ebd84adf0728c039a3be2700289378e1c164afc6748df1a5ed456767bef9ba7", size = 4631180, upload-time = "2026-04-24T19:53:37.517Z" }, + { url = "https://files.pythonhosted.org/packages/ee/65/31a5cc0eaca99cec5bafffe155d407115d96136bb161e8b49e0ef73f09a7/cryptography-47.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7f68d6fbc7fbbcfb0939fea72c3b96a9f9a6edfc0e1b1d29778a2066030418b1", size = 4653529, upload-time = "2026-04-24T19:53:39.775Z" }, + { url = "https://files.pythonhosted.org/packages/e5/bc/641c0519a495f3bfd0421b48d7cd325c4336578523ccd76ea322b6c29c7a/cryptography-47.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:6651d32eff255423503aa276739da98c30f26c40cbeffcc6048e0d54ef704c0c", size = 4638570, upload-time = "2026-04-24T19:53:42.129Z" }, + { url = "https://files.pythonhosted.org/packages/2b/f2/300327b0a47f6dc94dd8b71b57052aefe178bb51745073d73d80604f11ab/cryptography-47.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:3fb8fa48075fad7193f2e5496135c6a76ac4b2aa5a38433df0a539296b377829", size = 5238019, upload-time = "2026-04-24T19:53:44.577Z" }, + { url = "https://files.pythonhosted.org/packages/e9/5a/5b5cf994391d4bf9d9c7efd4c66aabe4d95227256627f8fea6cff7dfadbd/cryptography-47.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:11438c7518132d95f354fa01a4aa2f806d172a061a7bed18cf18cbdacdb204d7", size = 4686832, upload-time = "2026-04-24T19:53:47.015Z" }, + { url = "https://files.pythonhosted.org/packages/dc/2c/ae950e28fd6475c852fc21a44db3e6b5bcc1261d1e370f2b6e42fa800fef/cryptography-47.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:8c1a736bbb3288005796c3f7ccb9453360d7fed483b13b9f468aea5171432923", size = 4269301, upload-time = "2026-04-24T19:53:48.97Z" }, + { url = "https://files.pythonhosted.org/packages/67/fb/6a39782e150ffe5cc1b0018cb6ddc48bf7ca62b498d7539ffc8a758e977d/cryptography-47.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:f1557695e5c2b86e204f6ce9470497848634100787935ab7adc5397c54abd7ab", size = 4638110, upload-time = "2026-04-24T19:53:51.011Z" }, + { url = "https://files.pythonhosted.org/packages/8e/d7/0b3c71090a76e5c203164a47688b697635ece006dcd2499ab3a4dbd3f0bd/cryptography-47.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:f9a034b642b960767fb343766ae5ba6ad653f2e890ddd82955aef288ffea8736", size = 5194988, upload-time = "2026-04-24T19:53:52.962Z" }, + { url = "https://files.pythonhosted.org/packages/63/33/63a961498a9df51721ab578c5a2622661411fc520e00bd83b0cc64eb20c4/cryptography-47.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:b1c76fca783aa7698eb21eb14f9c4aa09452248ee54a627d125025a43f83e7a7", size = 4686563, upload-time = "2026-04-24T19:53:55.274Z" }, + { url = "https://files.pythonhosted.org/packages/b7/bf/5ee5b145248f92250de86145d1c1d6edebbd57a7fe7caa4dedb5d4cf06a1/cryptography-47.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4f7722c97826770bab8ae92959a2e7b20a5e9e9bf4deae68fd86c3ca457bab52", size = 4770094, upload-time = "2026-04-24T19:53:57.753Z" }, + { url = "https://files.pythonhosted.org/packages/92/43/21d220b2da5d517773894dacdcdb5c682c28d3fffce65548cb06e87d5501/cryptography-47.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:09f6d7bf6724f8db8b32f11eccf23efc8e759924bc5603800335cf8859a3ddbd", size = 4913811, upload-time = "2026-04-24T19:54:00.236Z" }, + { url = "https://files.pythonhosted.org/packages/31/98/dc4ad376ac5f1a1a7d4a83f7b0c6f2bcad36b5d2d8f30aeb482d3a7d9582/cryptography-47.0.0-cp314-cp314t-win32.whl", hash = "sha256:6eebcaf0df1d21ce1f90605c9b432dd2c4f4ab665ac29a40d5e3fc68f51b5e63", size = 3237158, upload-time = "2026-04-24T19:54:02.606Z" }, + { url = "https://files.pythonhosted.org/packages/bc/da/97f62d18306b5133468bc3f8cc73a3111e8cdc8cf8d3e69474d6e5fd2d1b/cryptography-47.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:51c9313e90bd1690ec5a75ed047c27c0b8e6c570029712943d6116ef9a90620b", size = 3758706, upload-time = "2026-04-24T19:54:04.433Z" }, + { url = "https://files.pythonhosted.org/packages/e0/34/a4fae8ae7c3bc227460c9ae43f56abf1b911da0ec29e0ebac53bb0a4b6b7/cryptography-47.0.0-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:14432c8a9bcb37009784f9594a62fae211a2ae9543e96c92b2a8e4c3cd5cd0c4", size = 7904072, upload-time = "2026-04-24T19:54:06.411Z" }, + { url = "https://files.pythonhosted.org/packages/01/64/d7b1e54fdb69f22d24a64bb3e88dc718b31c7fb10ef0b9691a3cf7eeea6e/cryptography-47.0.0-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:07efe86201817e7d3c18781ca9770bc0db04e1e48c994be384e4602bc38f8f27", size = 4635767, upload-time = "2026-04-24T19:54:08.519Z" }, + { url = "https://files.pythonhosted.org/packages/8b/7b/cca826391fb2a94efdcdfe4631eb69306ee1cff0b22f664a412c90713877/cryptography-47.0.0-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2b45761c6ec22b7c726d6a829558777e32d0f1c8be7c3f3480f9c912d5ee8a10", size = 4654350, upload-time = "2026-04-24T19:54:10.795Z" }, + { url = "https://files.pythonhosted.org/packages/4c/65/4b57bcc823f42a991627c51c2f68c9fd6eb1393c1756aac876cba2accae2/cryptography-47.0.0-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:edd4da498015da5b9f26d38d3bfc2e90257bfa9cbed1f6767c282a0025ae649b", size = 4643394, upload-time = "2026-04-24T19:54:13.275Z" }, + { url = "https://files.pythonhosted.org/packages/f4/c4/2c5fbeea70adbbca2bbae865e1d605d6a4a7f8dbd9d33eaf69645087f06c/cryptography-47.0.0-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:9af828c0d5a65c70ec729cd7495a4bf1a67ecb66417b8f02ff125ab8a6326a74", size = 5225777, upload-time = "2026-04-24T19:54:15.18Z" }, + { url = "https://files.pythonhosted.org/packages/7e/b8/ac57107ef32749d2b244e36069bb688792a363aaaa3acc9e3cf84c130315/cryptography-47.0.0-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:256d07c78a04d6b276f5df935a9923275f53bd1522f214447fdf365494e2d515", size = 4688771, upload-time = "2026-04-24T19:54:17.835Z" }, + { url = "https://files.pythonhosted.org/packages/56/fc/9f1de22ff8be99d991f240a46863c52d475404c408886c5a38d2b5c3bb26/cryptography-47.0.0-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:5d0e362ff51041b0c0d219cc7d6924d7b8996f57ce5712bdcef71eb3c65a59cc", size = 4270753, upload-time = "2026-04-24T19:54:19.963Z" }, + { url = "https://files.pythonhosted.org/packages/00/68/d70c852797aa68e8e48d12e5a87170c43f67bb4a59403627259dd57d15de/cryptography-47.0.0-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:1581aef4219f7ca2849d0250edaa3866212fb74bf5667284f46aa92f9e65c1ca", size = 4642911, upload-time = "2026-04-24T19:54:21.818Z" }, + { url = "https://files.pythonhosted.org/packages/a5/51/661cbee74f594c5d97ff82d34f10d5551c085ca4668645f4606ebd22bd5d/cryptography-47.0.0-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:a49a3eb5341b9503fa3000a9a0db033161db90d47285291f53c2a9d2cd1b7f76", size = 5181411, upload-time = "2026-04-24T19:54:24.376Z" }, + { url = "https://files.pythonhosted.org/packages/94/87/f2b6c374a82cf076cfa1416992ac8e8ec94d79facc37aec87c1a5cb72352/cryptography-47.0.0-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:2207a498b03275d0051589e326b79d4cf59985c99031b05bb292ac52631c37fe", size = 4688262, upload-time = "2026-04-24T19:54:26.946Z" }, + { url = "https://files.pythonhosted.org/packages/14/e2/8b7462f4acf21ec509616f0245018bb197194ab0b65c2ea21a0bdd53c0eb/cryptography-47.0.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7a02675e2fabd0c0fc04c868b8781863cbf1967691543c22f5470500ff840b31", size = 4775506, upload-time = "2026-04-24T19:54:28.926Z" }, + { url = "https://files.pythonhosted.org/packages/70/75/158e494e4c08dc05e039da5bb48553826bd26c23930cf8d3cd5f21fa8921/cryptography-47.0.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:80887c5cbd1774683cb126f0ab4184567f080071d5acf62205acb354b4b753b7", size = 4912060, upload-time = "2026-04-24T19:54:30.869Z" }, + { url = "https://files.pythonhosted.org/packages/06/bd/0a9d3edbf5eadbac926d7b9b3cd0c4be584eeeae4a003d24d9eda4affbbd/cryptography-47.0.0-cp38-abi3-win32.whl", hash = "sha256:ed67ea4e0cfb5faa5bc7ecb6e2b8838f3807a03758eec239d6c21c8769355310", size = 3248487, upload-time = "2026-04-24T19:54:33.494Z" }, + { url = "https://files.pythonhosted.org/packages/60/80/5681af756d0da3a599b7bdb586fac5a1540f1bcefd2717a20e611ddade45/cryptography-47.0.0-cp38-abi3-win_amd64.whl", hash = "sha256:835d2d7f47cdc53b3224e90810fb1d36ca94ea29cc1801fb4c1bc43876735769", size = 3755737, upload-time = "2026-04-24T19:54:35.408Z" }, + { url = "https://files.pythonhosted.org/packages/1b/a0/928c9ce0d120a40a81aa99e3ba383e87337b9ac9ef9f6db02e4d7822424d/cryptography-47.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:7f1207974a904e005f762869996cf620e9bf79ecb4622f148550bb48e0eb35a7", size = 3909893, upload-time = "2026-04-24T19:54:38.334Z" }, + { url = "https://files.pythonhosted.org/packages/81/75/d691e284750df5d9569f2b1ce4a00a71e1d79566da83b2b3e5549c84917f/cryptography-47.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:1a405c08857258c11016777e11c02bacbe7ef596faf259305d282272a3a05cbe", size = 4587867, upload-time = "2026-04-24T19:54:40.619Z" }, + { url = "https://files.pythonhosted.org/packages/07/d6/1b90f1a4e453009730b4545286f0b39bb348d805c11181fc31544e4f9a65/cryptography-47.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:20fdbe3e38fb67c385d233c89371fa27f9909f6ebca1cecc20c13518dae65475", size = 4627192, upload-time = "2026-04-24T19:54:42.849Z" }, + { url = "https://files.pythonhosted.org/packages/dc/53/cb358a80e9e359529f496870dd08c102aa8a4b5b9f9064f00f0d6ed5b527/cryptography-47.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:f7db373287273d8af1414cf95dc4118b13ffdc62be521997b0f2b270771fef50", size = 4587486, upload-time = "2026-04-24T19:54:44.908Z" }, + { url = "https://files.pythonhosted.org/packages/8b/57/aaa3d53876467a226f9a7a82fd14dd48058ad2de1948493442dfa16e2ffd/cryptography-47.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:9fe6b7c64926c765f9dff301f9c1b867febcda5768868ca084e18589113732ab", size = 4626327, upload-time = "2026-04-24T19:54:47.813Z" }, + { url = "https://files.pythonhosted.org/packages/ab/9c/51f28c3550276bcf35660703ba0ab829a90b88be8cd98a71ef23c2413913/cryptography-47.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:cffbba3392df0fa8629bb7f43454ee2925059ee158e23c54620b9063912b86c8", size = 3698916, upload-time = "2026-04-24T19:54:49.782Z" }, ] [[package]] @@ -743,14 +738,14 @@ wheels = [ [[package]] name = "gitpython" -version = "3.1.50" +version = "3.1.49" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "gitdb" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/33/f6/354ae6491228b5eb40e10d89c4d13c651fe1cf7556e35ebdded50cff57ce/gitpython-3.1.50.tar.gz", hash = "sha256:80da2d12504d52e1f998772dc5baf6e553f8d2fcfe1fcc226c9d9a2ee3372dcc", size = 219798, upload-time = "2026-05-06T04:01:26.571Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e1/63/210aaa302d6a0a78daa67c5c15bbac2cad361722841278b0209b6da20855/gitpython-3.1.49.tar.gz", hash = "sha256:42f9399c9eb33fc581014bedd76049dfbaf6375aa2a5754575966387280315e1", size = 219367, upload-time = "2026-04-29T00:31:20.478Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/20/7a/1c6e3562dfd8950adbb11ffbc65d21e7c89d01a6e4f137fa981056de25c5/gitpython-3.1.50-py3-none-any.whl", hash = "sha256:d352abe2908d07355014abdd21ddf798c2a961469239afec4962e9da884858f9", size = 212507, upload-time = "2026-05-06T04:01:23.799Z" }, + { url = "https://files.pythonhosted.org/packages/fd/6f/b842bfa6f21d6f87c57f9abf7194225e55279d96d869775e19e9f7236fc5/gitpython-3.1.49-py3-none-any.whl", hash = "sha256:024b0422d7f84d15cd794844e029ffebd4c5d42a7eb9b936b458697ef550a02c", size = 212190, upload-time = "2026-04-29T00:31:18.412Z" }, ] [[package]] @@ -777,15 +772,15 @@ grpc = [ [[package]] name = "google-auth" -version = "2.52.0" +version = "2.49.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cryptography" }, { name = "pyasn1-modules" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d4/f8/80d2493cbedece1c623dc3e3cb1883300871af0dcdae254409522985ac23/google_auth-2.52.0.tar.gz", hash = "sha256:01f30e1a9e3638698d89464f5e603ce29d18e1c0e63ec31ac570aba4e164aaf5", size = 335027, upload-time = "2026-05-07T19:45:24.033Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c6/fc/e925290a1ad95c975c459e2df070fac2b90954e13a0370ac505dff78cb99/google_auth-2.49.2.tar.gz", hash = "sha256:c1ae38500e73065dcae57355adb6278cf8b5c8e391994ae9cbadbcb9631ab409", size = 333958, upload-time = "2026-04-10T00:41:21.888Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ee/fc/2cdc74252746f547f81ff3f02d4d4234a3f411b5de5b61af97e633a060b9/google_auth-2.52.0-py3-none-any.whl", hash = "sha256:aee92803ba0ff93a70a3b8a35c7b4797837751cd6380b63ff38372b98f3ed627", size = 245614, upload-time = "2026-05-07T19:45:21.914Z" }, + { url = "https://files.pythonhosted.org/packages/73/76/d241a5c927433420507215df6cac1b1fa4ac0ba7a794df42a84326c68da8/google_auth-2.49.2-py3-none-any.whl", hash = "sha256:c2720924dfc82dedb962c9f52cabb2ab16714fd0a6a707e40561d217574ed6d5", size = 240638, upload-time = "2026-04-10T00:41:14.501Z" }, ] [[package]] @@ -808,15 +803,15 @@ wheels = [ [[package]] name = "google-cloud-core" -version = "2.6.0" +version = "2.5.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "google-api-core" }, { name = "google-auth" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a8/dd/1eef226e470369b26824a505c34482c0b493bc35fe8e0c6b003b5feca21a/google_cloud_core-2.6.0.tar.gz", hash = "sha256:e76149739f90fac1fc6757c09f47eaccb3145b54adbd7759b0f7c4b235f46c83", size = 36001, upload-time = "2026-05-07T08:04:04.124Z" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/24/6ca08b0a03c7b0c620427503ab00353a4ae806b848b93bcea18b6b76fde6/google_cloud_core-2.5.1.tar.gz", hash = "sha256:3dc94bdec9d05a31d9f355045ed0f369fbc0d8c665076c734f065d729800f811", size = 36078, upload-time = "2026-03-30T22:50:08.057Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/84/4a/98da8930ab109c73d9a5d13782a9ebb81ea8c111f6d534a567b71d23e52b/google_cloud_core-2.6.0-py3-none-any.whl", hash = "sha256:6d63ac8e5eca6d9e4319d0a1e2265fadcd7f1049904378caecfa01cf52dd869e", size = 29390, upload-time = "2026-05-07T08:02:34.672Z" }, + { url = "https://files.pythonhosted.org/packages/73/d9/5bb050cb32826466aa9b25f79e2ca2879fe66cb76782d4ed798dd7506151/google_cloud_core-2.5.1-py3-none-any.whl", hash = "sha256:ea62cdf502c20e3e14be8a32c05ed02113d7bef454e40ff3fab6fe1ec9f1f4e7", size = 29452, upload-time = "2026-03-30T22:48:31.567Z" }, ] [[package]] @@ -856,26 +851,26 @@ wheels = [ [[package]] name = "google-resumable-media" -version = "2.9.0" +version = "2.8.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "google-crc32c" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/00/4b/0b235beccc310d0a48adbc7246b719d173cca6c88c572dfa4b090e39143c/google_resumable_media-2.9.0.tar.gz", hash = "sha256:f7cfb224846a9dd444d125115dfbe8ef02a2b893e78f087762fe716a255a734b", size = 2164534, upload-time = "2026-05-07T08:04:44.236Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3f/d1/b1ea14b93b6b78f57fc580125de44e9f593ab88dd2460f1a8a8d18f74754/google_resumable_media-2.8.2.tar.gz", hash = "sha256:f3354a182ebd193ae3f42e3ef95e6c9b10f128320de23ac7637236713b1acd70", size = 2164510, upload-time = "2026-03-30T23:34:25.369Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/07/73/3518e63deb1667c5409a4579e28daf5e84479a87a72c547e0487f7883dcd/google_resumable_media-2.9.0-py3-none-any.whl", hash = "sha256:c8901e88e389af8bed64d9696c74d8bad961865eb2236e13e0bfca9bb0a65ca3", size = 81507, upload-time = "2026-05-07T08:03:23.809Z" }, + { url = "https://files.pythonhosted.org/packages/5e/f8/50bfaf4658431ff9de45c5c3935af7ab01157a4903c603cd0eee6e78e087/google_resumable_media-2.8.2-py3-none-any.whl", hash = "sha256:82b6d8ccd11765268cdd2a2123f417ec806b8eef3000a9a38dfe3033da5fb220", size = 81511, upload-time = "2026-03-30T23:34:09.671Z" }, ] [[package]] name = "googleapis-common-protos" -version = "1.75.0" +version = "1.74.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b5/c8/f439cffde755cffa462bfbb156278fa6f9d09119719af9814b858fd4f81f/googleapis_common_protos-1.75.0.tar.gz", hash = "sha256:53a062ff3c32552fbd62c11fe23768b78e4ddf0494d5e5fd97d3f4689c75fbbd", size = 151035, upload-time = "2026-05-07T08:04:49.423Z" } +sdist = { url = "https://files.pythonhosted.org/packages/20/18/a746c8344152d368a5aac738d4c857012f2c5d1fd2eac7e17b647a7861bd/googleapis_common_protos-1.74.0.tar.gz", hash = "sha256:57971e4eeeba6aad1163c1f0fc88543f965bb49129b8bb55b2b7b26ecab084f1", size = 151254, upload-time = "2026-04-02T21:23:26.679Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e7/c8/e2645aa8ed02fd4c7a2f59d68783b65b1f3cbdfe39a6308e156509d1fee8/googleapis_common_protos-1.75.0-py3-none-any.whl", hash = "sha256:961ed60399c457ceb0ee8f285a84c870aabc9c6a832b9d37bb281b5bebde43ed", size = 300631, upload-time = "2026-05-07T08:03:30.345Z" }, + { url = "https://files.pythonhosted.org/packages/b6/b0/be5d3329badb9230b765de6eea66b73abd5944bdeb5afb3562ddcd80ae84/googleapis_common_protos-1.74.0-py3-none-any.whl", hash = "sha256:702216f78610bb510e3f12ac3cafd281b7ac45cc5d86e90ad87e4d301a3426b5", size = 300743, upload-time = "2026-04-02T21:22:49.108Z" }, ] [[package]] @@ -1139,15 +1134,10 @@ name = "ipython" version = "9.13.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.14' and sys_platform == 'win32'", - "python_full_version >= '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version == '3.13.*' and sys_platform == 'win32'", - "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform == 'win32'", - "python_full_version == '3.13.*' and sys_platform == 'emscripten'", - "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform == 'emscripten'", - "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.14'", + "python_full_version == '3.13.*'", + "python_full_version == '3.12.*'", + "python_full_version == '3.11.*'", ] dependencies = [ { name = "colorama", marker = "python_full_version >= '3.11' and sys_platform == 'win32'" }, @@ -1191,14 +1181,14 @@ wheels = [ [[package]] name = "jedi" -version = "0.20.0" +version = "0.19.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "parso" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/46/b7/a3635f6a2d7cf5b5dd98064fc1d5fbbafcb25477bcea204a3a92145d158b/jedi-0.20.0.tar.gz", hash = "sha256:c3f4ccbd276696f4b19c54618d4fb18f9fc24b0aef02acf704b23f487daa1011", size = 3119416, upload-time = "2026-05-01T23:38:47.814Z" } +sdist = { url = "https://files.pythonhosted.org/packages/72/3a/79a912fbd4d8dd6fbb02bf69afd3bb72cf0c729bb3063c6f4498603db17a/jedi-0.19.2.tar.gz", hash = "sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0", size = 1231287, upload-time = "2024-11-11T01:41:42.873Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9a/93/242e2eab5fe682ffcb8b0084bde703a41d51e17ee0f3a31ff0d9d813620a/jedi-0.20.0-py2.py3-none-any.whl", hash = "sha256:7bdd9c2634f56713299976f4cbd59cb3fa92165cc5e05ea811fb253480728b67", size = 4884812, upload-time = "2026-05-01T23:38:43.919Z" }, + { url = "https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9", size = 1572278, upload-time = "2024-11-11T01:41:40.175Z" }, ] [[package]] @@ -1256,7 +1246,7 @@ name = "macholib" version = "1.16.4" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "altgraph", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "altgraph" }, ] sdist = { url = "https://files.pythonhosted.org/packages/10/2f/97589876ea967487978071c9042518d28b958d87b17dceb7cdc1d881f963/macholib-1.16.4.tar.gz", hash = "sha256:f408c93ab2e995cd2c46e34fe328b130404be143469e41bc366c807448979362", size = 59427, upload-time = "2025-11-22T08:28:38.373Z" } wheels = [ @@ -1265,14 +1255,14 @@ wheels = [ [[package]] name = "markdown-it-py" -version = "4.2.0" +version = "4.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mdurl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" }, + { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, ] [[package]] @@ -1425,15 +1415,10 @@ name = "networkx" version = "3.6.1" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.14' and sys_platform == 'win32'", - "python_full_version >= '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version == '3.13.*' and sys_platform == 'win32'", - "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform == 'win32'", - "python_full_version == '3.13.*' and sys_platform == 'emscripten'", - "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform == 'emscripten'", - "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.14'", + "python_full_version == '3.13.*'", + "python_full_version == '3.12.*'", + "python_full_version == '3.11.*'", ] sdist = { url = "https://files.pythonhosted.org/packages/6a/51/63fe664f3908c97be9d2e4f1158eb633317598cfa6e1fc14af5383f17512/networkx-3.6.1.tar.gz", hash = "sha256:26b7c357accc0c8cde558ad486283728b65b6a95d85ee1cd66bafab4c8168509", size = 2517025, upload-time = "2025-12-08T17:02:39.908Z" } wheels = [ @@ -1519,15 +1504,10 @@ name = "numpy" version = "2.4.4" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.14' and sys_platform == 'win32'", - "python_full_version >= '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version == '3.13.*' and sys_platform == 'win32'", - "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform == 'win32'", - "python_full_version == '3.13.*' and sys_platform == 'emscripten'", - "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform == 'emscripten'", - "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.14'", + "python_full_version == '3.13.*'", + "python_full_version == '3.12.*'", + "python_full_version == '3.11.*'", ] sdist = { url = "https://files.pythonhosted.org/packages/d7/9f/b8cef5bffa569759033adda9481211426f12f53299629b410340795c2514/numpy-2.4.4.tar.gz", hash = "sha256:2d390634c5182175533585cc89f3608a4682ccb173cc9bb940b2881c8d6f8fa0", size = 20731587, upload-time = "2026-03-29T13:22:01.298Z" } wheels = [ @@ -1617,14 +1597,12 @@ wheels = [ name = "pandas" version = "2.3.3" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.11'", -] dependencies = [ { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "python-dateutil", marker = "python_full_version < '3.11'" }, - { name = "pytz", marker = "python_full_version < '3.11'" }, - { name = "tzdata", marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "python-dateutil" }, + { name = "pytz" }, + { name = "tzdata" }, ] sdist = { url = "https://files.pythonhosted.org/packages/33/01/d40b85317f86cf08d853a4f495195c73815fdf205eef3993821720274518/pandas-2.3.3.tar.gz", hash = "sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b", size = 4495223, upload-time = "2025-09-29T23:34:51.853Z" } wheels = [ @@ -1677,77 +1655,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/70/44/5191d2e4026f86a2a109053e194d3ba7a31a2d10a9c2348368c63ed4e85a/pandas-2.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3869faf4bd07b3b66a9f462417d0ca3a9df29a9f6abd5d0d0dbab15dac7abe87", size = 13202175, upload-time = "2025-09-29T23:31:59.173Z" }, ] -[[package]] -name = "pandas" -version = "3.0.2" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.14' and sys_platform == 'win32'", - "python_full_version >= '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version == '3.13.*' and sys_platform == 'win32'", - "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform == 'win32'", - "python_full_version == '3.13.*' and sys_platform == 'emscripten'", - "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform == 'emscripten'", - "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform != 'emscripten' and sys_platform != 'win32'", -] -dependencies = [ - { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "python-dateutil", marker = "python_full_version >= '3.11'" }, - { name = "tzdata", marker = "(python_full_version >= '3.11' and sys_platform == 'emscripten') or (python_full_version >= '3.11' and sys_platform == 'win32')" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/da/99/b342345300f13440fe9fe385c3c481e2d9a595ee3bab4d3219247ac94e9a/pandas-3.0.2.tar.gz", hash = "sha256:f4753e73e34c8d83221ba58f232433fca2748be8b18dbca02d242ed153945043", size = 4645855, upload-time = "2026-03-31T06:48:30.816Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/97/35/6411db530c618e0e0005187e35aa02ce60ae4c4c4d206964a2f978217c27/pandas-3.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a727a73cbdba2f7458dc82449e2315899d5140b449015d822f515749a46cbbe0", size = 10326926, upload-time = "2026-03-31T06:46:08.29Z" }, - { url = "https://files.pythonhosted.org/packages/c4/d3/b7da1d5d7dbdc5ef52ed7debd2b484313b832982266905315dad5a0bf0b1/pandas-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dbbd4aa20ca51e63b53bbde6a0fa4254b1aaabb74d2f542df7a7959feb1d760c", size = 9926987, upload-time = "2026-03-31T06:46:11.724Z" }, - { url = "https://files.pythonhosted.org/packages/52/77/9b1c2d6070b5dbe239a7bc889e21bfa58720793fb902d1e070695d87c6d0/pandas-3.0.2-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:339dda302bd8369dedeae979cb750e484d549b563c3f54f3922cb8ff4978c5eb", size = 10757067, upload-time = "2026-03-31T06:46:14.903Z" }, - { url = "https://files.pythonhosted.org/packages/20/17/ec40d981705654853726e7ac9aea9ddbb4a5d9cf54d8472222f4f3de06c2/pandas-3.0.2-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:61c2fd96d72b983a9891b2598f286befd4ad262161a609c92dc1652544b46b76", size = 11258787, upload-time = "2026-03-31T06:46:17.683Z" }, - { url = "https://files.pythonhosted.org/packages/90/e3/3f1126d43d3702ca8773871a81c9f15122a1f412342cc56284ffda5b1f70/pandas-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c934008c733b8bbea273ea308b73b3156f0181e5b72960790b09c18a2794fe1e", size = 11771616, upload-time = "2026-03-31T06:46:20.532Z" }, - { url = "https://files.pythonhosted.org/packages/2e/cf/0f4e268e1f5062e44a6bda9f925806721cd4c95c2b808a4c82ebe914f96b/pandas-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:60a80bb4feacbef5e1447a3f82c33209c8b7e07f28d805cfd1fb951e5cb443aa", size = 12337623, upload-time = "2026-03-31T06:46:23.754Z" }, - { url = "https://files.pythonhosted.org/packages/44/a0/97a6339859d4acb2536efb24feb6708e82f7d33b2ed7e036f2983fcced82/pandas-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:ed72cb3f45190874eb579c64fa92d9df74e98fd63e2be7f62bce5ace0ade61df", size = 9897372, upload-time = "2026-03-31T06:46:26.703Z" }, - { url = "https://files.pythonhosted.org/packages/8f/eb/781516b808a99ddf288143cec46b342b3016c3414d137da1fdc3290d8860/pandas-3.0.2-cp311-cp311-win_arm64.whl", hash = "sha256:f12b1a9e332c01e09510586f8ca9b108fd631fd656af82e452d7315ef6df5f9f", size = 9154922, upload-time = "2026-03-31T06:46:30.284Z" }, - { url = "https://files.pythonhosted.org/packages/f3/b0/c20bd4d6d3f736e6bd6b55794e9cd0a617b858eaad27c8f410ea05d953b7/pandas-3.0.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:232a70ebb568c0c4d2db4584f338c1577d81e3af63292208d615907b698a0f18", size = 10347921, upload-time = "2026-03-31T06:46:33.36Z" }, - { url = "https://files.pythonhosted.org/packages/35/d0/4831af68ce30cc2d03c697bea8450e3225a835ef497d0d70f31b8cdde965/pandas-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:970762605cff1ca0d3f71ed4f3a769ea8f85fc8e6348f6e110b8fea7e6eb5a14", size = 9888127, upload-time = "2026-03-31T06:46:36.253Z" }, - { url = "https://files.pythonhosted.org/packages/61/a9/16ea9346e1fc4a96e2896242d9bc674764fb9049b0044c0132502f7a771e/pandas-3.0.2-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aff4e6f4d722e0652707d7bcb190c445fe58428500c6d16005b02401764b1b3d", size = 10399577, upload-time = "2026-03-31T06:46:39.224Z" }, - { url = "https://files.pythonhosted.org/packages/c4/a8/3a61a721472959ab0ce865ef05d10b0d6bfe27ce8801c99f33d4fa996e65/pandas-3.0.2-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ef8b27695c3d3dc78403c9a7d5e59a62d5464a7e1123b4e0042763f7104dc74f", size = 10880030, upload-time = "2026-03-31T06:46:42.412Z" }, - { url = "https://files.pythonhosted.org/packages/da/65/7225c0ea4d6ce9cb2160a7fb7f39804871049f016e74782e5dade4d14109/pandas-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f8d68083e49e16b84734eb1a4dcae4259a75c90fb6e2251ab9a00b61120c06ab", size = 11409468, upload-time = "2026-03-31T06:46:45.2Z" }, - { url = "https://files.pythonhosted.org/packages/fa/5b/46e7c76032639f2132359b5cf4c785dd8cf9aea5ea64699eac752f02b9db/pandas-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:32cc41f310ebd4a296d93515fcac312216adfedb1894e879303987b8f1e2b97d", size = 11936381, upload-time = "2026-03-31T06:46:48.293Z" }, - { url = "https://files.pythonhosted.org/packages/7b/8b/721a9cff6fa6a91b162eb51019c6243b82b3226c71bb6c8ef4a9bd65cbc6/pandas-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:a4785e1d6547d8427c5208b748ae2efb64659a21bd82bf440d4262d02bfa02a4", size = 9744993, upload-time = "2026-03-31T06:46:51.488Z" }, - { url = "https://files.pythonhosted.org/packages/d5/18/7f0bd34ae27b28159aa80f2a6799f47fda34f7fb938a76e20c7b7fe3b200/pandas-3.0.2-cp312-cp312-win_arm64.whl", hash = "sha256:08504503f7101300107ecdc8df73658e4347586db5cfdadabc1592e9d7e7a0fd", size = 9056118, upload-time = "2026-03-31T06:46:54.548Z" }, - { url = "https://files.pythonhosted.org/packages/bf/ca/3e639a1ea6fcd0617ca4e8ca45f62a74de33a56ae6cd552735470b22c8d3/pandas-3.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b5918ba197c951dec132b0c5929a00c0bf05d5942f590d3c10a807f6e15a57d3", size = 10321105, upload-time = "2026-03-31T06:46:57.327Z" }, - { url = "https://files.pythonhosted.org/packages/0b/77/dbc82ff2fb0e63c6564356682bf201edff0ba16c98630d21a1fb312a8182/pandas-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d606a041c89c0a474a4702d532ab7e73a14fe35c8d427b972a625c8e46373668", size = 9864088, upload-time = "2026-03-31T06:46:59.935Z" }, - { url = "https://files.pythonhosted.org/packages/5c/2b/341f1b04bbca2e17e13cd3f08c215b70ef2c60c5356ef1e8c6857449edc7/pandas-3.0.2-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:710246ba0616e86891b58ab95f2495143bb2bc83ab6b06747c74216f583a6ac9", size = 10369066, upload-time = "2026-03-31T06:47:02.792Z" }, - { url = "https://files.pythonhosted.org/packages/12/c5/cbb1ffefb20a93d3f0e1fdcda699fb84976210d411b008f97f48bf6ce27e/pandas-3.0.2-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5d3cfe227c725b1f3dff4278b43d8c784656a42a9325b63af6b1492a8232209e", size = 10876780, upload-time = "2026-03-31T06:47:06.205Z" }, - { url = "https://files.pythonhosted.org/packages/98/fe/2249ae5e0a69bd0ddf17353d0a5d26611d70970111f5b3600cdc8be883e7/pandas-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c3b723df9087a9a9a840e263ebd9f88b64a12075d1bf2ea401a5a42f254f084d", size = 11375181, upload-time = "2026-03-31T06:47:09.383Z" }, - { url = "https://files.pythonhosted.org/packages/de/64/77a38b09e70b6464883b8d7584ab543e748e42c1b5d337a2ee088e0df741/pandas-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a3096110bf9eac0070b7208465f2740e2d8a670d5cb6530b5bb884eca495fd39", size = 11928899, upload-time = "2026-03-31T06:47:12.686Z" }, - { url = "https://files.pythonhosted.org/packages/5e/52/42855bf626868413f761addd574acc6195880ae247a5346477a4361c3acb/pandas-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:07a10f5c36512eead51bc578eb3354ad17578b22c013d89a796ab5eee90cd991", size = 9746574, upload-time = "2026-03-31T06:47:15.64Z" }, - { url = "https://files.pythonhosted.org/packages/88/39/21304ae06a25e8bf9fc820d69b29b2c495b2ae580d1e143146c309941760/pandas-3.0.2-cp313-cp313-win_arm64.whl", hash = "sha256:5fdbfa05931071aba28b408e59226186b01eb5e92bea2ab78b65863ca3228d84", size = 9047156, upload-time = "2026-03-31T06:47:18.595Z" }, - { url = "https://files.pythonhosted.org/packages/72/20/7defa8b27d4f330a903bb68eea33be07d839c5ea6bdda54174efcec0e1d2/pandas-3.0.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:dbc20dea3b9e27d0e66d74c42b2d0c1bed9c2ffe92adea33633e3bedeb5ac235", size = 10756238, upload-time = "2026-03-31T06:47:22.012Z" }, - { url = "https://files.pythonhosted.org/packages/e9/95/49433c14862c636afc0e9b2db83ff16b3ad92959364e52b2955e44c8e94c/pandas-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b75c347eff42497452116ce05ef461822d97ce5b9ff8df6edacb8076092c855d", size = 10408520, upload-time = "2026-03-31T06:47:25.197Z" }, - { url = "https://files.pythonhosted.org/packages/3b/f8/462ad2b5881d6b8ec8e5f7ed2ea1893faa02290d13870a1600fe72ad8efc/pandas-3.0.2-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1478075142e83a5571782ad007fb201ed074bdeac7ebcc8890c71442e96adf7", size = 10324154, upload-time = "2026-03-31T06:47:28.097Z" }, - { url = "https://files.pythonhosted.org/packages/0a/65/d1e69b649cbcddda23ad6e4c40ef935340f6f652a006e5cbc3555ac8adb3/pandas-3.0.2-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5880314e69e763d4c8b27937090de570f1fb8d027059a7ada3f7f8e98bdcb677", size = 10714449, upload-time = "2026-03-31T06:47:30.85Z" }, - { url = "https://files.pythonhosted.org/packages/47/a4/85b59bc65b8190ea3689882db6cdf32a5003c0ccd5a586c30fdcc3ffc4fc/pandas-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b5329e26898896f06035241a626d7c335daa479b9bbc82be7c2742d048e41172", size = 11338475, upload-time = "2026-03-31T06:47:34.026Z" }, - { url = "https://files.pythonhosted.org/packages/1e/c4/bc6966c6e38e5d9478b935272d124d80a589511ed1612a5d21d36f664c68/pandas-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:81526c4afd31971f8b62671442a4b2b51e0aa9acc3819c9f0f12a28b6fcf85f1", size = 11786568, upload-time = "2026-03-31T06:47:36.941Z" }, - { url = "https://files.pythonhosted.org/packages/e8/74/09298ca9740beed1d3504e073d67e128aa07e5ca5ca2824b0c674c0b8676/pandas-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:7cadd7e9a44ec13b621aec60f9150e744cfc7a3dd32924a7e2f45edff31823b0", size = 10488652, upload-time = "2026-03-31T06:47:40.612Z" }, - { url = "https://files.pythonhosted.org/packages/bb/40/c6ea527147c73b24fc15c891c3fcffe9c019793119c5742b8784a062c7db/pandas-3.0.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:db0dbfd2a6cdf3770aa60464d50333d8f3d9165b2f2671bcc299b72de5a6677b", size = 10326084, upload-time = "2026-03-31T06:47:43.834Z" }, - { url = "https://files.pythonhosted.org/packages/95/25/bdb9326c3b5455f8d4d3549fce7abcf967259de146fe2cf7a82368141948/pandas-3.0.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0555c5882688a39317179ab4a0ed41d3ebc8812ab14c69364bbee8fb7a3f6288", size = 9914146, upload-time = "2026-03-31T06:47:46.67Z" }, - { url = "https://files.pythonhosted.org/packages/8d/77/3a227ff3337aa376c60d288e1d61c5d097131d0ac71f954d90a8f369e422/pandas-3.0.2-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:01f31a546acd5574ef77fe199bc90b55527c225c20ccda6601cf6b0fd5ed597c", size = 10444081, upload-time = "2026-03-31T06:47:49.681Z" }, - { url = "https://files.pythonhosted.org/packages/15/88/3cdd54fa279341afa10acf8d2b503556b1375245dccc9315659f795dd2e9/pandas-3.0.2-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:deeca1b5a931fdf0c2212c8a659ade6d3b1edc21f0914ce71ef24456ca7a6535", size = 10897535, upload-time = "2026-03-31T06:47:53.033Z" }, - { url = "https://files.pythonhosted.org/packages/06/9d/98cc7a7624f7932e40f434299260e2917b090a579d75937cb8a57b9d2de3/pandas-3.0.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0f48afd9bb13300ffb5a3316973324c787054ba6665cda0da3fbd67f451995db", size = 11446992, upload-time = "2026-03-31T06:47:56.193Z" }, - { url = "https://files.pythonhosted.org/packages/9a/cd/19ff605cc3760e80602e6826ddef2824d8e7050ed80f2e11c4b079741dc3/pandas-3.0.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6c4d8458b97a35717b62469a4ea0e85abd5ed8687277f5ccfc67f8a5126f8c53", size = 11968257, upload-time = "2026-03-31T06:47:59.137Z" }, - { url = "https://files.pythonhosted.org/packages/db/60/aba6a38de456e7341285102bede27514795c1eaa353bc0e7638b6b785356/pandas-3.0.2-cp314-cp314-win_amd64.whl", hash = "sha256:b35d14bb5d8285d9494fe93815a9e9307c0876e10f1e8e89ac5b88f728ec8dcf", size = 9865893, upload-time = "2026-03-31T06:48:02.038Z" }, - { url = "https://files.pythonhosted.org/packages/08/71/e5ec979dd2e8a093dacb8864598c0ff59a0cee0bbcdc0bfec16a51684d4f/pandas-3.0.2-cp314-cp314-win_arm64.whl", hash = "sha256:63d141b56ef686f7f0d714cfb8de4e320475b86bf4b620aa0b7da89af8cbdbbb", size = 9188644, upload-time = "2026-03-31T06:48:05.045Z" }, - { url = "https://files.pythonhosted.org/packages/f1/6c/7b45d85db19cae1eb524f2418ceaa9d85965dcf7b764ed151386b7c540f0/pandas-3.0.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:140f0cffb1fa2524e874dde5b477d9defe10780d8e9e220d259b2c0874c89d9d", size = 10776246, upload-time = "2026-03-31T06:48:07.789Z" }, - { url = "https://files.pythonhosted.org/packages/a8/3e/7b00648b086c106e81766f25322b48aa8dfa95b55e621dbdf2fdd413a117/pandas-3.0.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ae37e833ff4fed0ba352f6bdd8b73ba3ab3256a85e54edfd1ab51ae40cca0af8", size = 10424801, upload-time = "2026-03-31T06:48:10.897Z" }, - { url = "https://files.pythonhosted.org/packages/da/6e/558dd09a71b53b4008e7fc8a98ec6d447e9bfb63cdaeea10e5eb9b2dabe8/pandas-3.0.2-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4d888a5c678a419a5bb41a2a93818e8ed9fd3172246555c0b37b7cc27027effd", size = 10345643, upload-time = "2026-03-31T06:48:13.7Z" }, - { url = "https://files.pythonhosted.org/packages/be/e3/921c93b4d9a280409451dc8d07b062b503bbec0531d2627e73a756e99a82/pandas-3.0.2-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b444dc64c079e84df91baa8bf613d58405645461cabca929d9178f2cd392398d", size = 10743641, upload-time = "2026-03-31T06:48:16.659Z" }, - { url = "https://files.pythonhosted.org/packages/56/ca/fd17286f24fa3b4d067965d8d5d7e14fe557dd4f979a0b068ac0deaf8228/pandas-3.0.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4544c7a54920de8eeacaa1466a6b7268ecfbc9bc64ab4dbb89c6bbe94d5e0660", size = 11361993, upload-time = "2026-03-31T06:48:19.475Z" }, - { url = "https://files.pythonhosted.org/packages/e4/a5/2f6ed612056819de445a433ca1f2821ac3dab7f150d569a59e9cc105de1d/pandas-3.0.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:734be7551687c00fbd760dc0522ed974f82ad230d4a10f54bf51b80d44a08702", size = 11815274, upload-time = "2026-03-31T06:48:22.695Z" }, - { url = "https://files.pythonhosted.org/packages/00/2f/b622683e99ec3ce00b0854bac9e80868592c5b051733f2cf3a868e5fea26/pandas-3.0.2-cp314-cp314t-win_amd64.whl", hash = "sha256:57a07209bebcbcf768d2d13c9b78b852f9a15978dac41b9e6421a81ad4cdd276", size = 10888530, upload-time = "2026-03-31T06:48:25.806Z" }, - { url = "https://files.pythonhosted.org/packages/cb/2b/f8434233fab2bd66a02ec014febe4e5adced20e2693e0e90a07d118ed30e/pandas-3.0.2-cp314-cp314t-win_arm64.whl", hash = "sha256:5371b72c2d4d415d08765f32d689217a43227484e81b2305b52076e328f6f482", size = 9455341, upload-time = "2026-03-31T06:48:28.418Z" }, -] - [[package]] name = "parallel-web" version = "0.6.0" @@ -1803,8 +1710,7 @@ dev = [ { name = "datamodel-code-generator", extra = ["ruff"] }, { name = "duckdb" }, { name = "nest-asyncio" }, - { name = "pandas", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "pandas", version = "3.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "pandas" }, { name = "polars" }, { name = "pre-commit" }, { name = "pyarrow" }, @@ -1829,8 +1735,7 @@ duckdb = [ { name = "questionary" }, ] pandas = [ - { name = "pandas", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "pandas", version = "3.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "pandas" }, ] polars = [ { name = "polars" }, @@ -1840,8 +1745,7 @@ snowflake = [ { name = "snowflake-connector-python" }, ] spark = [ - { name = "pandas", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "pandas", version = "3.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "pandas" }, { name = "pyspark" }, ] @@ -1896,11 +1800,11 @@ dev = [ [[package]] name = "parso" -version = "0.8.7" +version = "0.8.6" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/30/4b/90c937815137d43ce71ba043cd3566221e9df6b9c805f24b5d138c9d40a7/parso-0.8.7.tar.gz", hash = "sha256:eaaac4c9fdd5e9e8852dc778d2d7405897ec510f2a298071453e5e3a07914bb1", size = 401824, upload-time = "2026-05-01T23:13:02.138Z" } +sdist = { url = "https://files.pythonhosted.org/packages/81/76/a1e769043c0c0c9fe391b702539d594731a4362334cdf4dc25d0c09761e7/parso-0.8.6.tar.gz", hash = "sha256:2b9a0332696df97d454fa67b81618fd69c35a7b90327cbe6ba5c92d2c68a7bfd", size = 401621, upload-time = "2026-02-09T15:45:24.425Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/99/5d/8268b644392ee874ee82a635cd0df1773de230bde356c38de28e298392cc/parso-0.8.7-py2.py3-none-any.whl", hash = "sha256:a8926eb2a1b915486941fdbd31e86a4baf88fe8c210f25f2f35ecec5b574ca1c", size = 107025, upload-time = "2026-05-01T23:12:58.867Z" }, + { url = "https://files.pythonhosted.org/packages/b6/61/fae042894f4296ec49e3f193aff5d7c18440da9e48102c3315e1bc4519a7/parso-0.8.6-py2.py3-none-any.whl", hash = "sha256:2c549f800b70a5c4952197248825584cb00f033b29c692671d3bf08bf380baff", size = 106894, upload-time = "2026-02-09T15:45:21.391Z" }, ] [[package]] @@ -1926,7 +1830,7 @@ name = "pexpect" version = "4.9.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "ptyprocess", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "ptyprocess" }, ] sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450, upload-time = "2023-11-25T09:07:26.339Z" } wheels = [ @@ -2009,14 +1913,14 @@ wheels = [ [[package]] name = "proto-plus" -version = "1.28.0" +version = "1.27.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c9/56/e647b0c675392d2da368da7b6f158f7368b18542fd6f7d7400a2f39de000/proto_plus-1.28.0.tar.gz", hash = "sha256:38e5696342835b08fc116f30a25665b29531cda9d5d5643e9b81fc312385abd9", size = 57221, upload-time = "2026-05-07T08:04:50.811Z" } +sdist = { url = "https://files.pythonhosted.org/packages/81/0d/94dfe80193e79d55258345901acd2917523d56e8381bc4dee7fd38e3868a/proto_plus-1.27.2.tar.gz", hash = "sha256:b2adde53adadf75737c44d3dcb0104fde65250dfc83ad59168b4aa3e574b6a24", size = 57204, upload-time = "2026-03-26T22:18:57.174Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7c/20/b122d4626976acb81132036d2ad1bb35a1a8775fceb837ec30964622516a/proto_plus-1.28.0-py3-none-any.whl", hash = "sha256:a630604310899e73c59ec302e5765c058d412b2f090b9c79c8822589f14955b8", size = 50410, upload-time = "2026-05-07T08:03:31.962Z" }, + { url = "https://files.pythonhosted.org/packages/84/f3/1fba73eeffafc998a25d59703b63f8be4fe8a5cb12eaff7386a0ba0f7125/proto_plus-1.27.2-py3-none-any.whl", hash = "sha256:6432f75893d3b9e70b9c412f1d2f03f65b11fb164b793d14ae2ca01821d22718", size = 50450, upload-time = "2026-03-26T22:13:42.927Z" }, ] [[package]] @@ -2178,7 +2082,7 @@ wheels = [ [[package]] name = "pydantic" -version = "2.13.4" +version = "2.13.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-types" }, @@ -2186,125 +2090,125 @@ dependencies = [ { name = "typing-extensions" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d9/e4/40d09941a2cebcb20609b86a559817d5b9291c49dd6f8c87e5feffbe703a/pydantic-2.13.3.tar.gz", hash = "sha256:af09e9d1d09f4e7fe37145c1f577e1d61ceb9a41924bf0094a36506285d0a84d", size = 844068, upload-time = "2026-04-20T14:46:43.632Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" }, + { url = "https://files.pythonhosted.org/packages/f3/0a/fd7d723f8f8153418fb40cf9c940e82004fce7e987026b08a68a36dd3fe7/pydantic-2.13.3-py3-none-any.whl", hash = "sha256:6db14ac8dfc9a1e57f87ea2c0de670c251240f43cb0c30a5130e9720dc612927", size = 471981, upload-time = "2026-04-20T14:46:41.402Z" }, ] [[package]] name = "pydantic-core" -version = "2.46.4" +version = "2.46.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e7/08/f1ba952f1c8ae5581c70fa9c6da89f247b83e3dd8c09c035d5d7931fc23d/pydantic_core-2.46.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:a396dcc17e5a0b164dbe026896245a4fa9ff402edca1dff0be3d53a517f74de4", size = 2113146, upload-time = "2026-05-06T13:37:36.537Z" }, - { url = "https://files.pythonhosted.org/packages/56/c6/65f646c7ff09bd257f660434adb45c4dfcbbcebcc030562fecf6f5bf887d/pydantic_core-2.46.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:da4b951fe36dc7c3a1ccb4e3cd1747c3542b8c9ceede8fc86cae054e764485f5", size = 1949769, upload-time = "2026-05-06T13:37:46.365Z" }, - { url = "https://files.pythonhosted.org/packages/64/ba/bfb1d928fd5b49e1258935ff104ae356e9fd89384a55bf9f847e9193ad40/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb63e0198ca18aad131c089b9204c23079c3afa95487e561f4c522d519e55aba", size = 1974958, upload-time = "2026-05-06T13:37:28.611Z" }, - { url = "https://files.pythonhosted.org/packages/4e/74/76223bfb117b64af743c9b6670d1364516f5c0604f96b48f3272f6af6cc6/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f47286a97f0bc9b8859519809077b91b2cefe4ae47fcbf5e466a009c1c5d742b", size = 2042118, upload-time = "2026-05-06T13:36:55.216Z" }, - { url = "https://files.pythonhosted.org/packages/cb/7b/848732968bc8f48f3187542f08358b9d842db564147b256669426ebb1652/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:905a0ed8ea6f2d61c1738835f99b699348d7857379083e5fc497fa0c967a407c", size = 2222876, upload-time = "2026-05-06T13:38:25.455Z" }, - { url = "https://files.pythonhosted.org/packages/b5/2f/e90b63ee2e14bd8d3db8f705a6d75d64e6ee1b7c2c8833747ce706e1e0ce/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea793e075b70290d89d8142074262885d3f7da19634845135751bd6344f73b50", size = 2286703, upload-time = "2026-05-06T13:37:53.304Z" }, - { url = "https://files.pythonhosted.org/packages/ba/1e/acc4d70f88a0a277e4a1fa77ebb985ceabaf900430f875bf9338e11c9420/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:395aebd9183f9d112f569aeb5b2214d1a10a33bec8456447f7fbdfa51d38d4cd", size = 2092042, upload-time = "2026-05-06T13:38:46.981Z" }, - { url = "https://files.pythonhosted.org/packages/a9/da/0a422b57bf8504102bf3c4ccea9c41bab5a5cee6a54650acf8faf67f5a24/pydantic_core-2.46.4-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:b078afbc25f3a1436c7a1d2cd3e322497ee99615ba97c563566fdf46aff1ee01", size = 2117231, upload-time = "2026-05-06T13:39:23.146Z" }, - { url = "https://files.pythonhosted.org/packages/bd/2a/2ac13c3af305843e23c5078c53d135656b3f05a2fd78cb7bbbb12e97b473/pydantic_core-2.46.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f747929cf940cddb5b3668a390056ddd5ba2e5010615ea2dcf4f9c4f3ab8791d", size = 2168388, upload-time = "2026-05-06T13:40:08.06Z" }, - { url = "https://files.pythonhosted.org/packages/72/04/2beacf7e1607e93eefe4aed1b4709f079b905fb77530179d4f7c71745f22/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:daa27d92c36f24388fe3ad306b174781c747627f134452e4f128ea00ce1fe8c4", size = 2184769, upload-time = "2026-05-06T13:38:13.901Z" }, - { url = "https://files.pythonhosted.org/packages/9e/29/d2b9fd9f539133548eaf622c06a4ce176cb46ac59f32d0359c4abc0de047/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:19e51f073cd3df251856a8a4189fbdf1de4012c3ebacfb1884f94f1eb406079f", size = 2319312, upload-time = "2026-05-06T13:39:08.24Z" }, - { url = "https://files.pythonhosted.org/packages/7c/af/0f7a5b85fec6075bea96e3ef9187de38fccced0de92c1e7feda8d5cc7bb9/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c1747f85cee84c26985853c6f3d9bd3e75da5212912443fa111c113b9c246f39", size = 2361817, upload-time = "2026-05-06T13:38:43.2Z" }, - { url = "https://files.pythonhosted.org/packages/25/a4/73363fec545fd3ec025490bdda2743c56d0dd5b6266b1a53bbe9e4265375/pydantic_core-2.46.4-cp310-cp310-win32.whl", hash = "sha256:2f84c03c8607173d16b5a854ec68a2f9079ae03237a54fb506d13af47e1d018d", size = 1987085, upload-time = "2026-05-06T13:39:25.497Z" }, - { url = "https://files.pythonhosted.org/packages/01/aa/62f082da2c91fac1c234bc9ee0066257ce83f0604abd72e4c9d5991f2d84/pydantic_core-2.46.4-cp310-cp310-win_amd64.whl", hash = "sha256:8358a950c8909158e3df31538a7e4edc2d7265a7c54b47f0864d9e5bae9dcebf", size = 2074311, upload-time = "2026-05-06T13:39:59.922Z" }, - { url = "https://files.pythonhosted.org/packages/5c/fa/6d7708d2cfc1a832acb6aeb0cd16e801902df8a0f583bb3b4b527fde022e/pydantic_core-2.46.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0e96592440881c74a213e5ad528e2b24d3d4f940de2766bed9010ab1d9e51594", size = 2111872, upload-time = "2026-05-06T13:40:27.596Z" }, - { url = "https://files.pythonhosted.org/packages/ae/6f/aa064a3e74b5745afbdf250594f38e7ead05e2d651bcb35994b9417a0d4d/pydantic_core-2.46.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0d65b8c354be7fb5f720c3caa8bc940bc2d20ce749c8e06135f07f8ed95dd7c", size = 1948255, upload-time = "2026-05-06T13:39:12.574Z" }, - { url = "https://files.pythonhosted.org/packages/43/3a/41114a9f7569b84b4d84e7a018c57c56347dac30c0d4a872946ec4e36c46/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826", size = 1972827, upload-time = "2026-05-06T13:38:19.841Z" }, - { url = "https://files.pythonhosted.org/packages/ef/25/1ab42e8048fe551934d9884e8d64daa7e990ad386f310a15981aeb6a5b08/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9037063db01f09b09e237c282b6792bd4da634b5402c4e7f0c61effed7701a04", size = 2041051, upload-time = "2026-05-06T13:38:10.447Z" }, - { url = "https://files.pythonhosted.org/packages/94/c2/1a934597ddf08da410385b3b7aae91956a5a76c635effef456074fad7e88/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc010ab034c8c7452522748bf937df58020d256ccae0874463d1f4d01758af8e", size = 2221314, upload-time = "2026-05-06T13:40:13.089Z" }, - { url = "https://files.pythonhosted.org/packages/02/6d/9e8ad178c9c4df27ad3c8f25d1fe2a7ab0d2ba0559fad4aee5d3d1f16771/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c5dac79fa1614d1e06ca695109c6105923bd9c7d1d6c918d4e637b7e6b32fd3", size = 2285146, upload-time = "2026-05-06T13:38:59.224Z" }, - { url = "https://files.pythonhosted.org/packages/80/50/540cd3aeefc041beb111125c4bff779831a2111fc6b15a9138cda277d32c/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9fa868638bf362d3d138ea55829cefb3d5f4b0d7f142234382a15e2485dbec4", size = 2089685, upload-time = "2026-05-06T13:38:17.762Z" }, - { url = "https://files.pythonhosted.org/packages/6b/a4/b440ad35f05f6a38f89fa0f149accb3f0e02be94ca5e15f3c449a61b4bc9/pydantic_core-2.46.4-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:17299feefe090f2caa5b8e37222bb5f663e4935a8bfa6931d4102e5df1a9f398", size = 2115420, upload-time = "2026-05-06T13:37:58.195Z" }, - { url = "https://files.pythonhosted.org/packages/99/61/de4f55db8dfd57bfdfa9a12ec90fe1b57c4f41062f7ca86f08586b3e0ac0/pydantic_core-2.46.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4c63ebc82684aa89d9a3bcbd13d515b3be44250dc68dd3bd81526c1cb31286c3", size = 2165122, upload-time = "2026-05-06T13:37:01.167Z" }, - { url = "https://files.pythonhosted.org/packages/f7/52/7c529d7bdb2d1068bd52f51fe32572c8301f9a4febf1948f10639f1436f5/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:aaa2a54443eff1950ba5ddc6b6ccda0d9c84a364276a62f969bdf2a390650848", size = 2182573, upload-time = "2026-05-06T13:38:45.04Z" }, - { url = "https://files.pythonhosted.org/packages/37/b3/7c40325848ba78247f2812dcf9c7274e38cd801820ca6dd9fe63bcfb0eb4/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:18e5ceec2ab67e6d5f1a9085e5a24c9c4e2ac4545730bfe668680bca05e555f3", size = 2317139, upload-time = "2026-05-06T13:37:15.539Z" }, - { url = "https://files.pythonhosted.org/packages/d9/37/f913f81a657c865b75da6c0dbed79876073c2a43b5bd9edbe8da785e4d49/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a0f62d0a58f4e7da165457e995725421e0064f2255d8eccebc49f41bbc23b109", size = 2360433, upload-time = "2026-05-06T13:37:30.099Z" }, - { url = "https://files.pythonhosted.org/packages/c4/67/6acaa1be2567f9256b056d8477158cac7240813956ce86e49deae8e173b4/pydantic_core-2.46.4-cp311-cp311-win32.whl", hash = "sha256:041bde0a48fd37cf71cab1c9d56d3e8625a3793fef1f7dd232b3ff37e978ecda", size = 1985513, upload-time = "2026-05-06T13:38:15.669Z" }, - { url = "https://files.pythonhosted.org/packages/aa/e6/c505f83dfeda9a2e5c995cfd872949e4d05e12f7feb3dca72f633daefa94/pydantic_core-2.46.4-cp311-cp311-win_amd64.whl", hash = "sha256:6f2eeda33a839975441c86a4119e1383c50b47faf0cbb5176985565c6bb02c33", size = 2071114, upload-time = "2026-05-06T13:40:35.416Z" }, - { url = "https://files.pythonhosted.org/packages/0f/da/7a263a96d965d9d0df5e8de8a475f33495451117035b09acb110288c381f/pydantic_core-2.46.4-cp311-cp311-win_arm64.whl", hash = "sha256:14f4c5d6db102bd796a627bbb3a17b4cf4574b9ae861d8b7c9a9661c6dd3362d", size = 2044298, upload-time = "2026-05-06T13:38:29.754Z" }, - { url = "https://files.pythonhosted.org/packages/ce/8c/af022f0af448d7747c5154288d46b5f2bc5f17366eaa0e23e9aa04d59f3b/pydantic_core-2.46.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2", size = 2106158, upload-time = "2026-05-06T13:38:57.215Z" }, - { url = "https://files.pythonhosted.org/packages/19/95/6195171e385007300f0f5574592e467c568becce2d937a0b6804f218bc49/pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f", size = 1951724, upload-time = "2026-05-06T13:37:02.697Z" }, - { url = "https://files.pythonhosted.org/packages/8e/bc/f47d1ff9cbb1620e1b5b697eef06010035735f07820180e74178226b27b3/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7", size = 1975742, upload-time = "2026-05-06T13:37:09.448Z" }, - { url = "https://files.pythonhosted.org/packages/5b/11/9b9a5b0306345664a2da6410877af6e8082481b5884b3ddd78d47c6013ce/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7", size = 2052418, upload-time = "2026-05-06T13:37:38.234Z" }, - { url = "https://files.pythonhosted.org/packages/f1/b7/a65fec226f5d78fc39f4a13c4cc0c768c22b113438f60c14adc9d2865038/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712", size = 2232274, upload-time = "2026-05-06T13:38:27.753Z" }, - { url = "https://files.pythonhosted.org/packages/68/f0/92039db98b907ef49269a8271f67db9cb78ae2fc68062ef7e4e77adb5f61/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4", size = 2309940, upload-time = "2026-05-06T13:38:05.353Z" }, - { url = "https://files.pythonhosted.org/packages/5f/97/2aab507d3d00ca626e8e57c1eac6a79e4e5fbcc63eb99733ff55d1717f65/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce", size = 2094516, upload-time = "2026-05-06T13:39:10.577Z" }, - { url = "https://files.pythonhosted.org/packages/22/37/a8aca44d40d737dde2bc05b3c6c07dff0de07ce6f82e9f3167aeaf4d5dea/pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987", size = 2136854, upload-time = "2026-05-06T13:40:22.59Z" }, - { url = "https://files.pythonhosted.org/packages/24/99/fcef1b79238c06a8cbec70819ac722ba76e02bc8ada9b0fd66eba40da01b/pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b", size = 2180306, upload-time = "2026-05-06T13:40:10.666Z" }, - { url = "https://files.pythonhosted.org/packages/ae/6c/fc44000918855b42779d007ae63b0532794739027b2f417321cddbc44f6a/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458", size = 2190044, upload-time = "2026-05-06T13:40:43.231Z" }, - { url = "https://files.pythonhosted.org/packages/6b/65/d9cadc9f1920d7a127ad2edba16c1db7916e59719285cd6c94600b0080ba/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b", size = 2329133, upload-time = "2026-05-06T13:39:57.365Z" }, - { url = "https://files.pythonhosted.org/packages/d0/cf/c873d91679f3a30bcf5e7ac280ce5573483e72295307685120d0d5ad3416/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c", size = 2374464, upload-time = "2026-05-06T13:38:06.976Z" }, - { url = "https://files.pythonhosted.org/packages/47/bd/6f2fc8188f31bf10590f1e98e7b306336161fac930a8c514cd7bd828c7dc/pydantic_core-2.46.4-cp312-cp312-win32.whl", hash = "sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894", size = 1974823, upload-time = "2026-05-06T13:40:47.985Z" }, - { url = "https://files.pythonhosted.org/packages/40/8c/985c1d41ea1107c2534abd9870e4ed5c8e7669b5c308297835c001e7a1c4/pydantic_core-2.46.4-cp312-cp312-win_amd64.whl", hash = "sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89", size = 2072919, upload-time = "2026-05-06T13:39:21.153Z" }, - { url = "https://files.pythonhosted.org/packages/c4/ba/f463d006e0c47373ca7ec5e1a261c59dc01ef4d62b2657af925fb0deee3a/pydantic_core-2.46.4-cp312-cp312-win_arm64.whl", hash = "sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a", size = 2027604, upload-time = "2026-05-06T13:39:03.753Z" }, - { url = "https://files.pythonhosted.org/packages/51/a2/5d30b469c5267a17b39dec53208222f76a8d351dfac4af661888c5aee77d/pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008", size = 2106306, upload-time = "2026-05-06T13:37:48.029Z" }, - { url = "https://files.pythonhosted.org/packages/c1/81/4fa520eaffa8bd7d1525e644cd6d39e7d60b1592bc5b516693c7340b50f1/pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4", size = 1951906, upload-time = "2026-05-06T13:37:17.012Z" }, - { url = "https://files.pythonhosted.org/packages/03/d5/fd02da45b659668b05923b17ba3a0100a0a3d5541e3bd8fcc4ecb711309e/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76", size = 1976802, upload-time = "2026-05-06T13:37:35.113Z" }, - { url = "https://files.pythonhosted.org/packages/21/f2/95727e1368be3d3ed485eaab7adbd7dda408f33f7a36e8b48e0144002b91/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3", size = 2052446, upload-time = "2026-05-06T13:37:12.313Z" }, - { url = "https://files.pythonhosted.org/packages/9c/86/5d99feea3f77c7234b8718075b23db11532773c1a0dbd9b9490215dc2eeb/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76", size = 2232757, upload-time = "2026-05-06T13:39:01.149Z" }, - { url = "https://files.pythonhosted.org/packages/d2/3a/508ac615935ef7588cf6d9e9b91309fdc2da751af865e02a9098de88258c/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4", size = 2309275, upload-time = "2026-05-06T13:37:41.406Z" }, - { url = "https://files.pythonhosted.org/packages/07/f8/41db9de19d7987d6b04715a02b3b40aea467000275d9d758ffaa31af7d50/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a", size = 2094467, upload-time = "2026-05-06T13:39:18.847Z" }, - { url = "https://files.pythonhosted.org/packages/2c/e2/f35033184cb11d0052daf4416e8e10a502ea2ac006fc4f459aee872727d1/pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262", size = 2134417, upload-time = "2026-05-06T13:40:17.944Z" }, - { url = "https://files.pythonhosted.org/packages/7e/7b/6ceeb1cc90e193862f444ebe373d8fdf613f0a82572dde03fb10734c6c71/pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e", size = 2179782, upload-time = "2026-05-06T13:40:32.618Z" }, - { url = "https://files.pythonhosted.org/packages/5a/f2/c8d7773ede6af08036423a00ae0ceffce266c3c52a096c435d68c896083f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd", size = 2188782, upload-time = "2026-05-06T13:36:51.018Z" }, - { url = "https://files.pythonhosted.org/packages/59/31/0c864784e31f09f05cdd87606f08923b9c9e7f6e51dd27f20f62f975ce9f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be", size = 2328334, upload-time = "2026-05-06T13:40:37.764Z" }, - { url = "https://files.pythonhosted.org/packages/c2/eb/4f6c8a41efa30baa755590f4141abf3a8c370fab610915733e74134a7270/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d", size = 2372986, upload-time = "2026-05-06T13:39:34.152Z" }, - { url = "https://files.pythonhosted.org/packages/5b/24/b375a480d53113860c299764bfe9f349a3dc9108b3adc0d7f0d786492ebf/pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb", size = 1973693, upload-time = "2026-05-06T13:37:55.072Z" }, - { url = "https://files.pythonhosted.org/packages/7e/e8/cff247591966f2d22ec8c003cd7587e27b7ba7b81ab2fb888e3ab75dc285/pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292", size = 2071819, upload-time = "2026-05-06T13:38:49.139Z" }, - { url = "https://files.pythonhosted.org/packages/c6/1a/f4aee670d5670e9e148e0c82c7db98d780be566c6e6a97ee8035528ca0b3/pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d", size = 2027411, upload-time = "2026-05-06T13:40:45.796Z" }, - { url = "https://files.pythonhosted.org/packages/8d/74/228a26ddad29c6672b805d9fd78e8d251cd04004fa7eed0e622096cd0250/pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb", size = 2102079, upload-time = "2026-05-06T13:38:41.019Z" }, - { url = "https://files.pythonhosted.org/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462", size = 1952179, upload-time = "2026-05-06T13:36:59.812Z" }, - { url = "https://files.pythonhosted.org/packages/95/30/5211a831ae054928054b2f79731661087a2bc5c01e825c672b3a4a8f1b3e/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9", size = 1978926, upload-time = "2026-05-06T13:37:39.933Z" }, - { url = "https://files.pythonhosted.org/packages/57/e9/689668733b1eb67adeef047db3c2e8788fcf65a7fd9c9e2b46b7744fe245/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4", size = 2046785, upload-time = "2026-05-06T13:38:01.995Z" }, - { url = "https://files.pythonhosted.org/packages/60/d9/6715260422ff50a2109878fd24d948a6c3446bb2664f34ee78cd972b3acd/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914", size = 2228733, upload-time = "2026-05-06T13:40:50.371Z" }, - { url = "https://files.pythonhosted.org/packages/18/ae/fdb2f64316afca925640f8e70bb1a564b0ec2721c1389e25b8eb4bf9a299/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28", size = 2307534, upload-time = "2026-05-06T13:37:21.531Z" }, - { url = "https://files.pythonhosted.org/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b", size = 2099732, upload-time = "2026-05-06T13:39:31.942Z" }, - { url = "https://files.pythonhosted.org/packages/06/d5/ee5a3366637fee41dee51a1fc91562dcf12ddbc68fda34e6b253da2324bb/pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c", size = 2129627, upload-time = "2026-05-06T13:37:25.033Z" }, - { url = "https://files.pythonhosted.org/packages/94/33/2414be571d2c6a6c4d08be21f9292b6d3fdb08949a97b6dfe985017821db/pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb", size = 2179141, upload-time = "2026-05-06T13:37:14.046Z" }, - { url = "https://files.pythonhosted.org/packages/7b/79/7daa95be995be0eecc4cf75064cb33f9bbbfe3fe0158caf2f0d4a996a5c7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898", size = 2184325, upload-time = "2026-05-06T13:36:53.615Z" }, - { url = "https://files.pythonhosted.org/packages/9f/cb/d0a382f5c0de8a222dc61c65348e0ce831b1f68e0a018450d31c2cace3a5/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e", size = 2323990, upload-time = "2026-05-06T13:40:29.971Z" }, - { url = "https://files.pythonhosted.org/packages/05/db/d9ba624cc4a5aced1598e88c04fdbd8310c8a69b9d38b9a3d39ce3a61ed7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519", size = 2369978, upload-time = "2026-05-06T13:37:23.027Z" }, - { url = "https://files.pythonhosted.org/packages/f2/20/d15df15ba918c423461905802bfd2981c3af0bfa0e40d05e13edbfa48bc3/pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4", size = 1966354, upload-time = "2026-05-06T13:38:03.499Z" }, - { url = "https://files.pythonhosted.org/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac", size = 2072238, upload-time = "2026-05-06T13:39:40.807Z" }, - { url = "https://files.pythonhosted.org/packages/32/36/51eb763beec1f4cf59b1db243a7dcc39cbb41230f050a09b9d69faaf0a48/pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a", size = 2018251, upload-time = "2026-05-06T13:37:26.72Z" }, - { url = "https://files.pythonhosted.org/packages/e8/91/855af51d625b23aa987116a19e231d2aaef9c4a415273ddc189b79a45fee/pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0", size = 2099593, upload-time = "2026-05-06T13:39:47.682Z" }, - { url = "https://files.pythonhosted.org/packages/fb/1b/8784a54c65edb5f49f0a14d6977cf1b209bba85a4c77445b255c2de58ab3/pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d", size = 1935226, upload-time = "2026-05-06T13:40:40.428Z" }, - { url = "https://files.pythonhosted.org/packages/e8/e7/1955d28d1afc56dd4b3ad7cc0cf39df1b9852964cf16e5d13912756d6d6b/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b", size = 1974605, upload-time = "2026-05-06T13:37:32.029Z" }, - { url = "https://files.pythonhosted.org/packages/93/e2/3fedbf0ba7a22850e6e9fd78117f1c0f10f950182344d8a6c535d468fdd8/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000", size = 2030777, upload-time = "2026-05-06T13:38:55.239Z" }, - { url = "https://files.pythonhosted.org/packages/f8/61/46be275fcaaba0b4f5b9669dd852267ce1ff616592dccf7a7845588df091/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e", size = 2236641, upload-time = "2026-05-06T13:37:08.096Z" }, - { url = "https://files.pythonhosted.org/packages/60/db/12e93e46a8bac9988be3c016860f83293daea8c716c029c9ace279036f2f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd", size = 2286404, upload-time = "2026-05-06T13:40:20.221Z" }, - { url = "https://files.pythonhosted.org/packages/e2/4a/4d8b19008f38d31c53b8219cfedc2e3d5de5fe99d90076b7e767de29274f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3", size = 2109219, upload-time = "2026-05-06T13:38:12.153Z" }, - { url = "https://files.pythonhosted.org/packages/88/70/3cbc40978fefb7bb09c6708d40d4ad1a5d70fd7213c3d17f971de868ec1f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7", size = 2110594, upload-time = "2026-05-06T13:40:02.971Z" }, - { url = "https://files.pythonhosted.org/packages/9d/20/b8d36736216e29491125531685b2f9e61aa5b4b2599893f8268551da3338/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff", size = 2159542, upload-time = "2026-05-06T13:39:27.506Z" }, - { url = "https://files.pythonhosted.org/packages/1d/a2/367df868eb584dacf6bf82a389272406d7178e301c4ac82545ab98bc2dd9/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424", size = 2168146, upload-time = "2026-05-06T13:38:31.93Z" }, - { url = "https://files.pythonhosted.org/packages/c1/b8/4460f77f7e201893f649a29ab355dddd3beee8a97bcb1a320db414f9a06e/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6", size = 2306309, upload-time = "2026-05-06T13:37:44.717Z" }, - { url = "https://files.pythonhosted.org/packages/64/c4/be2639293acd87dc8ddbcec41a73cee9b2ebf996fe6d892a1a74e88ad3f7/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565", size = 2369736, upload-time = "2026-05-06T13:37:05.645Z" }, - { url = "https://files.pythonhosted.org/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02", size = 1955575, upload-time = "2026-05-06T13:38:51.116Z" }, - { url = "https://files.pythonhosted.org/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5", size = 2051624, upload-time = "2026-05-06T13:38:21.672Z" }, - { url = "https://files.pythonhosted.org/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325, upload-time = "2026-05-06T13:40:52.723Z" }, - { url = "https://files.pythonhosted.org/packages/ee/a4/73995fd4ebbb46ba0ee51e6fa049b8f02c40daebb762208feda8a6b7894d/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:14d4edf427bdcf950a8a02d7cb44a08614388dd6e1bdcbf4f67504fa7887da9c", size = 2111589, upload-time = "2026-05-06T13:37:10.817Z" }, - { url = "https://files.pythonhosted.org/packages/fb/7f/f37d3a5e8bfcc2e403f5c57a730f2d815693fb42119e8ea48b3789335af1/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0ce40cd7b21210e99342afafbd4d0f76d784eb5b1d60f3bdc566be4983c6c73b", size = 1944552, upload-time = "2026-05-06T13:36:56.717Z" }, - { url = "https://files.pythonhosted.org/packages/15/3c/d7eb777b3ff43e8433a4efb39a17aa8fd98a4ee8561a24a67ef5db07b2d6/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90884113d8b48f760e9587002789ddd741e76ab9f89518cd1e43b1f1a52ec44b", size = 1982984, upload-time = "2026-05-06T13:39:06.207Z" }, - { url = "https://files.pythonhosted.org/packages/63/87/70b9f40170a81afd55ca26c9b2acb25c20d64bcfbf888fafecb3ba077d4c/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66ce7632c22d837c95301830e111ad0128a32b8207533b60896a96c4915192ea", size = 2138417, upload-time = "2026-05-06T13:39:45.476Z" }, - { url = "https://files.pythonhosted.org/packages/9d/1d/8987ad40f65ae1432753072f214fb5c74fe47ffbd0698bb9cbbb585664f8/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7", size = 2095527, upload-time = "2026-05-06T13:39:52.283Z" }, - { url = "https://files.pythonhosted.org/packages/64/d3/84c282a7eee1d3ac4c0377546ef5a1ea436ce26840d9ac3b7ed54a377507/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df", size = 1936024, upload-time = "2026-05-06T13:40:15.671Z" }, - { url = "https://files.pythonhosted.org/packages/d7/ca/eac61596cdeb4d7e174d3dc0bd8a6238f14f75f97a24e7b7db4c7e7340a0/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526", size = 1990696, upload-time = "2026-05-06T13:38:34.717Z" }, - { url = "https://files.pythonhosted.org/packages/fa/c3/7c8b240552251faf6b3a957db200fcfbbcec36763c050428b601e0c9b83b/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0", size = 2147590, upload-time = "2026-05-06T13:39:29.883Z" }, - { url = "https://files.pythonhosted.org/packages/11/cb/428de0385b6c8d44b716feba566abfacfbd23ee3c4439faa789a1456242f/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:0c563b08bca408dc7f65f700633d8442fffb2421fc47b8101377e9fd65051ff0", size = 2112782, upload-time = "2026-05-06T13:37:04.016Z" }, - { url = "https://files.pythonhosted.org/packages/0b/b5/6a17bdadd0fc1f170adfd05a20d37c832f52b117b4d9131da1f41bb097ce/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:db06ffe51636ffe9ca531fe9023dd64bdd794be8754cb5df57c5498ae5b518a7", size = 1952146, upload-time = "2026-05-06T13:39:43.092Z" }, - { url = "https://files.pythonhosted.org/packages/2a/dc/03734d80e362cd43ef65428e9de77c730ce7f2f11c60d2b1e1b39f0fbf99/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:133878133d271ade3d41d1bfb2a45ec38dbdbda40bc065921c6b04e4630127e2", size = 2134492, upload-time = "2026-05-06T13:36:58.124Z" }, - { url = "https://files.pythonhosted.org/packages/de/df/5e5ffc085ed07cc22d298134d3d911c63e91f6a0eb91fe646750a3209910/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9bc519fbf2b7578398853d815009ae5e4d4603d12f4e3f91da8c06852d3da3e9", size = 2156604, upload-time = "2026-05-06T13:37:49.88Z" }, - { url = "https://files.pythonhosted.org/packages/81/44/6e112a4253e56f5705467cbab7ab5e91ee7398ba3d56d358635958893d3e/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c7a7bd4e39e8e4c12c39cd480356842b6a8a06e41b23a55a5e3e191718838ddf", size = 2183828, upload-time = "2026-05-06T13:37:43.053Z" }, - { url = "https://files.pythonhosted.org/packages/ac/ad/5565071e937d8e752842ac241463944c9eb14c87e2d269f2658a5bd05e98/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:d396ec2b979760aaf3218e76c24e65bd0aca24983298653b3a9d7a45f9e47b30", size = 2310000, upload-time = "2026-05-06T13:37:56.694Z" }, - { url = "https://files.pythonhosted.org/packages/4f/c3/66883a5cec183e7fba4d024b4cbbe61851a63750ef606b0afecc46d1f2bf/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:86e1a4418c6cd97d60c95c71164158eaf7324fae7b0923264016baa993eba6fc", size = 2361286, upload-time = "2026-05-06T13:40:05.667Z" }, - { url = "https://files.pythonhosted.org/packages/4b/2d/69abac8f838090bbecd5df894befb2c2619e7996a98ddb949db9f3b93225/pydantic_core-2.46.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983", size = 2193071, upload-time = "2026-05-06T13:38:08.682Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/2a/ef/f7abb56c49382a246fd2ce9c799691e3c3e7175ec74b14d99e798bcddb1a/pydantic_core-2.46.3.tar.gz", hash = "sha256:41c178f65b8c29807239d47e6050262eb6bf84eb695e41101e62e38df4a5bc2c", size = 471412, upload-time = "2026-04-20T14:40:56.672Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/98/b50eb9a411e87483b5c65dba4fa430a06bac4234d3403a40e5a9905ebcd0/pydantic_core-2.46.3-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:1da3786b8018e60349680720158cc19161cc3b4bdd815beb0a321cd5ce1ad5b1", size = 2108971, upload-time = "2026-04-20T14:43:51.945Z" }, + { url = "https://files.pythonhosted.org/packages/08/4b/f364b9d161718ff2217160a4b5d41ce38de60aed91c3689ebffa1c939d23/pydantic_core-2.46.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cc0988cb29d21bf4a9d5cf2ef970b5c0e38d8d8e107a493278c05dc6c1dda69f", size = 1949588, upload-time = "2026-04-20T14:44:10.386Z" }, + { url = "https://files.pythonhosted.org/packages/8f/8b/30bd03ee83b2f5e29f5ba8e647ab3c456bf56f2ec72fdbcc0215484a0854/pydantic_core-2.46.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27f9067c3bfadd04c55484b89c0d267981b2f3512850f6f66e1e74204a4e4ce3", size = 1975986, upload-time = "2026-04-20T14:43:57.106Z" }, + { url = "https://files.pythonhosted.org/packages/3c/54/13ccf954d84ec275d5d023d5786e4aa48840bc9f161f2838dc98e1153518/pydantic_core-2.46.3-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a642ac886ecf6402d9882d10c405dcf4b902abeb2972cd5fb4a48c83cd59279a", size = 2055830, upload-time = "2026-04-20T14:44:15.499Z" }, + { url = "https://files.pythonhosted.org/packages/be/0e/65f38125e660fdbd72aa858e7dfae893645cfa0e7b13d333e174a367cd23/pydantic_core-2.46.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:79f561438481f28681584b89e2effb22855e2179880314bcddbf5968e935e807", size = 2222340, upload-time = "2026-04-20T14:41:51.353Z" }, + { url = "https://files.pythonhosted.org/packages/d1/88/f3ab7739efe0e7e80777dbb84c59eb98518e3f57ea433206194c2e425272/pydantic_core-2.46.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:57a973eae4665352a47cf1a99b4ee864620f2fe663a217d7a8da68a1f3a5bfda", size = 2280727, upload-time = "2026-04-20T14:41:30.461Z" }, + { url = "https://files.pythonhosted.org/packages/2a/6d/c228219080817bec4982f9531cadb18da6aaa770fdeb114f49c237ac2c9f/pydantic_core-2.46.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:83d002b97072a53ea150d63e0a3adfae5670cef5aa8a6e490240e482d3b22e57", size = 2092158, upload-time = "2026-04-20T14:44:07.305Z" }, + { url = "https://files.pythonhosted.org/packages/0f/b1/525a16711e7c6d61635fac3b0bd54600b5c5d9f60c6fc5aaab26b64a2297/pydantic_core-2.46.3-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:b40ddd51e7c44b28cfaef746c9d3c506d658885e0a46f9eeef2ee815cbf8e045", size = 2116626, upload-time = "2026-04-20T14:42:34.118Z" }, + { url = "https://files.pythonhosted.org/packages/ef/7c/17d30673351439a6951bf54f564cf2443ab00ae264ec9df00e2efd710eb5/pydantic_core-2.46.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ac5ec7fb9b87f04ee839af2d53bcadea57ded7d229719f56c0ed895bff987943", size = 2160691, upload-time = "2026-04-20T14:41:14.023Z" }, + { url = "https://files.pythonhosted.org/packages/86/66/af8adbcbc0886ead7f1a116606a534d75a307e71e6e08226000d51b880d2/pydantic_core-2.46.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:a3b11c812f61b3129c4905781a2601dfdfdea5fe1e6c1cfb696b55d14e9c054f", size = 2182543, upload-time = "2026-04-20T14:40:48.886Z" }, + { url = "https://files.pythonhosted.org/packages/b0/37/6de71e0f54c54a4190010f57deb749e1ddf75c568ada3b1320b70067f121/pydantic_core-2.46.3-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:1108da631e602e5b3c38d6d04fe5bb3bfa54349e6918e3ca6cf570b2e2b2f9d4", size = 2324513, upload-time = "2026-04-20T14:42:36.121Z" }, + { url = "https://files.pythonhosted.org/packages/51/b1/9fc74ce94f603d5ef59ff258ca9c2c8fb902fb548d340a96f77f4d1c3b7f/pydantic_core-2.46.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:de885175515bcfa98ae618c1df7a072f13d179f81376c8007112af20567fd08a", size = 2361853, upload-time = "2026-04-20T14:43:24.886Z" }, + { url = "https://files.pythonhosted.org/packages/40/d0/4c652fc592db35f100279ee751d5a145aca1b9a7984b9684ba7c1b5b0535/pydantic_core-2.46.3-cp310-cp310-win32.whl", hash = "sha256:d11058e3201527d41bc6b545c79187c9e4bf85e15a236a6007f0e991518882b7", size = 1980465, upload-time = "2026-04-20T14:44:46.239Z" }, + { url = "https://files.pythonhosted.org/packages/27/b8/a920453c38afbe1f355e1ea0b0d94a0a3e0b0879d32d793108755fa171d5/pydantic_core-2.46.3-cp310-cp310-win_amd64.whl", hash = "sha256:3612edf65c8ea67ac13616c4d23af12faef1ae435a8a93e5934c2a0cbbdd1fd6", size = 2073884, upload-time = "2026-04-20T14:43:01.201Z" }, + { url = "https://files.pythonhosted.org/packages/22/a2/1ba90a83e85a3f94c796b184f3efde9c72f2830dcda493eea8d59ba78e6d/pydantic_core-2.46.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:ab124d49d0459b2373ecf54118a45c28a1e6d4192a533fbc915e70f556feb8e5", size = 2106740, upload-time = "2026-04-20T14:41:20.932Z" }, + { url = "https://files.pythonhosted.org/packages/b6/f6/99ae893c89a0b9d3daec9f95487aa676709aa83f67643b3f0abaf4ab628a/pydantic_core-2.46.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cca67d52a5c7a16aed2b3999e719c4bcf644074eac304a5d3d62dd70ae7d4b2c", size = 1948293, upload-time = "2026-04-20T14:43:42.115Z" }, + { url = "https://files.pythonhosted.org/packages/3e/b8/2e8e636dc9e3f16c2e16bf0849e24be82c5ee82c603c65fc0326666328fc/pydantic_core-2.46.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c024e08c0ba23e6fd68c771a521e9d6a792f2ebb0fa734296b36394dc30390e", size = 1973222, upload-time = "2026-04-20T14:41:57.841Z" }, + { url = "https://files.pythonhosted.org/packages/34/36/0e730beec4d83c5306f417afbd82ff237d9a21e83c5edf675f31ed84c1fe/pydantic_core-2.46.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6645ce7eec4928e29a1e3b3d5c946621d105d3e79f0c9cddf07c2a9770949287", size = 2053852, upload-time = "2026-04-20T14:40:43.077Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f0/3071131f47e39136a17814576e0fada9168569f7f8c0e6ac4d1ede6a4958/pydantic_core-2.46.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a712c7118e6c5ea96562f7b488435172abb94a3c53c22c9efc1412264a45cbbe", size = 2221134, upload-time = "2026-04-20T14:43:03.349Z" }, + { url = "https://files.pythonhosted.org/packages/2f/a9/a2dc023eec5aa4b02a467874bad32e2446957d2adcab14e107eab502e978/pydantic_core-2.46.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:69a868ef3ff206343579021c40faf3b1edc64b1cc508ff243a28b0a514ccb050", size = 2279785, upload-time = "2026-04-20T14:41:19.285Z" }, + { url = "https://files.pythonhosted.org/packages/0a/44/93f489d16fb63fbd41c670441536541f6e8cfa1e5a69f40bc9c5d30d8c90/pydantic_core-2.46.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc7e8c32db809aa0f6ea1d6869ebc8518a65d5150fdfad8bcae6a49ae32a22e2", size = 2089404, upload-time = "2026-04-20T14:43:10.108Z" }, + { url = "https://files.pythonhosted.org/packages/2a/78/8692e3aa72b2d004f7a5d937f1dfdc8552ba26caf0bec75f342c40f00dec/pydantic_core-2.46.3-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:3481bd1341dc85779ee506bc8e1196a277ace359d89d28588a9468c3ecbe63fa", size = 2114898, upload-time = "2026-04-20T14:44:51.475Z" }, + { url = "https://files.pythonhosted.org/packages/6a/62/e83133f2e7832532060175cebf1f13748f4c7e7e7165cdd1f611f174494b/pydantic_core-2.46.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8690eba565c6d68ffd3a8655525cbdd5246510b44a637ee2c6c03a7ebfe64d3c", size = 2157856, upload-time = "2026-04-20T14:43:46.64Z" }, + { url = "https://files.pythonhosted.org/packages/6d/ec/6a500e3ad7718ee50583fae79c8651f5d37e3abce1fa9ae177ae65842c53/pydantic_core-2.46.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4de88889d7e88d50d40ee5b39d5dac0bcaef9ba91f7e536ac064e6b2834ecccf", size = 2180168, upload-time = "2026-04-20T14:42:00.302Z" }, + { url = "https://files.pythonhosted.org/packages/d8/53/8267811054b1aa7fc1dc7ded93812372ef79a839f5e23558136a6afbfde1/pydantic_core-2.46.3-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:e480080975c1ef7f780b8f99ed72337e7cc5efea2e518a20a692e8e7b278eb8b", size = 2322885, upload-time = "2026-04-20T14:41:05.253Z" }, + { url = "https://files.pythonhosted.org/packages/c8/c1/1c0acdb3aa0856ddc4ecc55214578f896f2de16f400cf51627eb3c26c1c4/pydantic_core-2.46.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:de3a5c376f8cd94da9a1b8fd3dd1c16c7a7b216ed31dc8ce9fd7a22bf13b836e", size = 2360328, upload-time = "2026-04-20T14:41:43.991Z" }, + { url = "https://files.pythonhosted.org/packages/f0/d0/ef39cd0f4a926814f360e71c1adeab48ad214d9727e4deb48eedfb5bce1a/pydantic_core-2.46.3-cp311-cp311-win32.whl", hash = "sha256:fc331a5314ffddd5385b9ee9d0d2fee0b13c27e0e02dad71b1ae5d6561f51eeb", size = 1979464, upload-time = "2026-04-20T14:43:12.215Z" }, + { url = "https://files.pythonhosted.org/packages/18/9c/f41951b0d858e343f1cf09398b2a7b3014013799744f2c4a8ad6a3eec4f2/pydantic_core-2.46.3-cp311-cp311-win_amd64.whl", hash = "sha256:b5b9c6cf08a8a5e502698f5e153056d12c34b8fb30317e0c5fd06f45162a6346", size = 2070837, upload-time = "2026-04-20T14:41:47.707Z" }, + { url = "https://files.pythonhosted.org/packages/9f/1e/264a17cd582f6ed50950d4d03dd5fefd84e570e238afe1cb3e25cf238769/pydantic_core-2.46.3-cp311-cp311-win_arm64.whl", hash = "sha256:5dfd51cf457482f04ec49491811a2b8fd5b843b64b11eecd2d7a1ee596ea78a6", size = 2053647, upload-time = "2026-04-20T14:42:27.535Z" }, + { url = "https://files.pythonhosted.org/packages/4b/cb/5b47425556ecc1f3fe18ed2a0083188aa46e1dd812b06e406475b3a5d536/pydantic_core-2.46.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:b11b59b3eee90a80a36701ddb4576d9ae31f93f05cb9e277ceaa09e6bf074a67", size = 2101946, upload-time = "2026-04-20T14:40:52.581Z" }, + { url = "https://files.pythonhosted.org/packages/a1/4f/2fb62c2267cae99b815bbf4a7b9283812c88ca3153ef29f7707200f1d4e5/pydantic_core-2.46.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:af8653713055ea18a3abc1537fe2ebc42f5b0bbb768d1eb79fd74eb47c0ac089", size = 1951612, upload-time = "2026-04-20T14:42:42.996Z" }, + { url = "https://files.pythonhosted.org/packages/50/6e/b7348fd30d6556d132cddd5bd79f37f96f2601fe0608afac4f5fb01ec0b3/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:75a519dab6d63c514f3a81053e5266c549679e4aa88f6ec57f2b7b854aceb1b0", size = 1977027, upload-time = "2026-04-20T14:42:02.001Z" }, + { url = "https://files.pythonhosted.org/packages/82/11/31d60ee2b45540d3fb0b29302a393dbc01cd771c473f5b5147bcd353e593/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a6cd87cb1575b1ad05ba98894c5b5c96411ef678fa2f6ed2576607095b8d9789", size = 2063008, upload-time = "2026-04-20T14:44:17.952Z" }, + { url = "https://files.pythonhosted.org/packages/8a/db/3a9d1957181b59258f44a2300ab0f0be9d1e12d662a4f57bb31250455c52/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f80a55484b8d843c8ada81ebf70a682f3f00a3d40e378c06cf17ecb44d280d7d", size = 2233082, upload-time = "2026-04-20T14:40:57.934Z" }, + { url = "https://files.pythonhosted.org/packages/9c/e1/3277c38792aeb5cfb18c2f0c5785a221d9ff4e149abbe1184d53d5f72273/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3861f1731b90c50a3266316b9044f5c9b405eecb8e299b0a7120596334e4fe9c", size = 2304615, upload-time = "2026-04-20T14:42:12.584Z" }, + { url = "https://files.pythonhosted.org/packages/5e/d5/e3d9717c9eba10855325650afd2a9cba8e607321697f18953af9d562da2f/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb528e295ed31570ac3dcc9bfdd6e0150bc11ce6168ac87a8082055cf1a67395", size = 2094380, upload-time = "2026-04-20T14:43:05.522Z" }, + { url = "https://files.pythonhosted.org/packages/a1/20/abac35dedcbfd66c6f0b03e4e3564511771d6c9b7ede10a362d03e110d9b/pydantic_core-2.46.3-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:367508faa4973b992b271ba1494acaab36eb7e8739d1e47be5035fb1ea225396", size = 2135429, upload-time = "2026-04-20T14:41:55.549Z" }, + { url = "https://files.pythonhosted.org/packages/6c/a5/41bfd1df69afad71b5cf0535055bccc73022715ad362edbc124bc1e021d7/pydantic_core-2.46.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5ad3c826fe523e4becf4fe39baa44286cff85ef137c729a2c5e269afbfd0905d", size = 2174582, upload-time = "2026-04-20T14:41:45.96Z" }, + { url = "https://files.pythonhosted.org/packages/79/65/38d86ea056b29b2b10734eb23329b7a7672ca604df4f2b6e9c02d4ee22fe/pydantic_core-2.46.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ec638c5d194ef8af27db69f16c954a09797c0dc25015ad6123eb2c73a4d271ca", size = 2187533, upload-time = "2026-04-20T14:40:55.367Z" }, + { url = "https://files.pythonhosted.org/packages/b6/55/a1129141678a2026badc539ad1dee0a71d06f54c2f06a4bd68c030ac781b/pydantic_core-2.46.3-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:28ed528c45446062ee66edb1d33df5d88828ae167de76e773a3c7f64bd14e976", size = 2332985, upload-time = "2026-04-20T14:44:13.05Z" }, + { url = "https://files.pythonhosted.org/packages/d7/60/cb26f4077719f709e54819f4e8e1d43f4091f94e285eb6bd21e1190a7b7c/pydantic_core-2.46.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:aed19d0c783886d5bd86d80ae5030006b45e28464218747dcf83dabfdd092c7b", size = 2373670, upload-time = "2026-04-20T14:41:53.421Z" }, + { url = "https://files.pythonhosted.org/packages/6b/7e/c3f21882bdf1d8d086876f81b5e296206c69c6082551d776895de7801fa0/pydantic_core-2.46.3-cp312-cp312-win32.whl", hash = "sha256:06d5d8820cbbdb4147578c1fe7ffcd5b83f34508cb9f9ab76e807be7db6ff0a4", size = 1966722, upload-time = "2026-04-20T14:44:30.588Z" }, + { url = "https://files.pythonhosted.org/packages/57/be/6b5e757b859013ebfbd7adba02f23b428f37c86dcbf78b5bb0b4ffd36e99/pydantic_core-2.46.3-cp312-cp312-win_amd64.whl", hash = "sha256:c3212fda0ee959c1dd04c60b601ec31097aaa893573a3a1abd0a47bcac2968c1", size = 2072970, upload-time = "2026-04-20T14:42:54.248Z" }, + { url = "https://files.pythonhosted.org/packages/bf/f8/a989b21cc75e9a32d24192ef700eea606521221a89faa40c919ce884f2b1/pydantic_core-2.46.3-cp312-cp312-win_arm64.whl", hash = "sha256:f1f8338dd7a7f31761f1f1a3c47503a9a3b34eea3c8b01fa6ee96408affb5e72", size = 2035963, upload-time = "2026-04-20T14:44:20.4Z" }, + { url = "https://files.pythonhosted.org/packages/9b/3c/9b5e8eb9821936d065439c3b0fb1490ffa64163bfe7e1595985a47896073/pydantic_core-2.46.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:12bc98de041458b80c86c56b24df1d23832f3e166cbaff011f25d187f5c62c37", size = 2102109, upload-time = "2026-04-20T14:41:24.219Z" }, + { url = "https://files.pythonhosted.org/packages/91/97/1c41d1f5a19f241d8069f1e249853bcce378cdb76eec8ab636d7bc426280/pydantic_core-2.46.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:85348b8f89d2c3508b65b16c3c33a4da22b8215138d8b996912bb1532868885f", size = 1951820, upload-time = "2026-04-20T14:42:14.236Z" }, + { url = "https://files.pythonhosted.org/packages/30/b4/d03a7ae14571bc2b6b3c7b122441154720619afe9a336fa3a95434df5e2f/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1105677a6df914b1fb71a81b96c8cce7726857e1717d86001f29be06a25ee6f8", size = 1977785, upload-time = "2026-04-20T14:42:31.648Z" }, + { url = "https://files.pythonhosted.org/packages/ae/0c/4086f808834b59e3c8f1aa26df8f4b6d998cdcf354a143d18ef41529d1fe/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:87082cd65669a33adeba5470769e9704c7cf026cc30afb9cc77fd865578ebaad", size = 2062761, upload-time = "2026-04-20T14:40:37.093Z" }, + { url = "https://files.pythonhosted.org/packages/fa/71/a649be5a5064c2df0db06e0a512c2281134ed2fcc981f52a657936a7527c/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:60e5f66e12c4f5212d08522963380eaaeac5ebd795826cfd19b2dfb0c7a52b9c", size = 2232989, upload-time = "2026-04-20T14:42:59.254Z" }, + { url = "https://files.pythonhosted.org/packages/a2/84/7756e75763e810b3a710f4724441d1ecc5883b94aacb07ca71c5fb5cfb69/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b6cdf19bf84128d5e7c37e8a73a0c5c10d51103a650ac585d42dd6ae233f2b7f", size = 2303975, upload-time = "2026-04-20T14:41:32.287Z" }, + { url = "https://files.pythonhosted.org/packages/6c/35/68a762e0c1e31f35fa0dac733cbd9f5b118042853698de9509c8e5bf128b/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:031bb17f4885a43773c8c763089499f242aee2ea85cf17154168775dccdecf35", size = 2095325, upload-time = "2026-04-20T14:42:47.685Z" }, + { url = "https://files.pythonhosted.org/packages/77/bf/1bf8c9a8e91836c926eae5e3e51dce009bf495a60ca56060689d3df3f340/pydantic_core-2.46.3-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:bcf2a8b2982a6673693eae7348ef3d8cf3979c1d63b54fca7c397a635cc68687", size = 2133368, upload-time = "2026-04-20T14:41:22.766Z" }, + { url = "https://files.pythonhosted.org/packages/e5/50/87d818d6bab915984995157ceb2380f5aac4e563dddbed6b56f0ed057aba/pydantic_core-2.46.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28e8cf2f52d72ced402a137145923a762cbb5081e48b34312f7a0c8f55928ec3", size = 2173908, upload-time = "2026-04-20T14:42:52.044Z" }, + { url = "https://files.pythonhosted.org/packages/91/88/a311fb306d0bd6185db41fa14ae888fb81d0baf648a761ae760d30819d33/pydantic_core-2.46.3-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:17eaface65d9fc5abb940003020309c1bf7a211f5f608d7870297c367e6f9022", size = 2186422, upload-time = "2026-04-20T14:43:29.55Z" }, + { url = "https://files.pythonhosted.org/packages/8f/79/28fd0d81508525ab2054fef7c77a638c8b5b0afcbbaeee493cf7c3fef7e1/pydantic_core-2.46.3-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:93fd339f23408a07e98950a89644f92c54d8729719a40b30c0a30bb9ebc55d23", size = 2332709, upload-time = "2026-04-20T14:42:16.134Z" }, + { url = "https://files.pythonhosted.org/packages/b3/21/795bf5fe5c0f379308b8ef19c50dedab2e7711dbc8d0c2acf08f1c7daa05/pydantic_core-2.46.3-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:23cbdb3aaa74dfe0837975dbf69b469753bbde8eacace524519ffdb6b6e89eb7", size = 2372428, upload-time = "2026-04-20T14:41:10.974Z" }, + { url = "https://files.pythonhosted.org/packages/45/b3/ed14c659cbe7605e3ef063077680a64680aec81eb1a04763a05190d49b7f/pydantic_core-2.46.3-cp313-cp313-win32.whl", hash = "sha256:610eda2e3838f401105e6326ca304f5da1e15393ae25dacae5c5c63f2c275b13", size = 1965601, upload-time = "2026-04-20T14:41:42.128Z" }, + { url = "https://files.pythonhosted.org/packages/ef/bb/adb70d9a762ddd002d723fbf1bd492244d37da41e3af7b74ad212609027e/pydantic_core-2.46.3-cp313-cp313-win_amd64.whl", hash = "sha256:68cc7866ed863db34351294187f9b729964c371ba33e31c26f478471c52e1ed0", size = 2071517, upload-time = "2026-04-20T14:43:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/52/eb/66faefabebfe68bd7788339c9c9127231e680b11906368c67ce112fdb47f/pydantic_core-2.46.3-cp313-cp313-win_arm64.whl", hash = "sha256:f64b5537ac62b231572879cd08ec05600308636a5d63bcbdb15063a466977bec", size = 2035802, upload-time = "2026-04-20T14:43:38.507Z" }, + { url = "https://files.pythonhosted.org/packages/7f/db/a7bcb4940183fda36022cd18ba8dd12f2dff40740ec7b58ce7457befa416/pydantic_core-2.46.3-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:afa3aa644f74e290cdede48a7b0bee37d1c35e71b05105f6b340d484af536d9b", size = 2097614, upload-time = "2026-04-20T14:44:38.374Z" }, + { url = "https://files.pythonhosted.org/packages/24/35/e4066358a22e3e99519db370494c7528f5a2aa1367370e80e27e20283543/pydantic_core-2.46.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ced3310e51aa425f7f77da8bbbb5212616655bedbe82c70944320bc1dbe5e018", size = 1951896, upload-time = "2026-04-20T14:40:53.996Z" }, + { url = "https://files.pythonhosted.org/packages/87/92/37cf4049d1636996e4b888c05a501f40a43ff218983a551d57f9d5e14f0d/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e29908922ce9da1a30b4da490bd1d3d82c01dcfdf864d2a74aacee674d0bfa34", size = 1979314, upload-time = "2026-04-20T14:41:49.446Z" }, + { url = "https://files.pythonhosted.org/packages/d8/36/9ff4d676dfbdfb2d591cf43f3d90ded01e15b1404fd101180ed2d62a2fd3/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0c9ff69140423eea8ed2d5477df3ba037f671f5e897d206d921bc9fdc39613e7", size = 2056133, upload-time = "2026-04-20T14:42:23.574Z" }, + { url = "https://files.pythonhosted.org/packages/bc/f0/405b442a4d7ba855b06eec8b2bf9c617d43b8432d099dfdc7bf999293495/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b675ab0a0d5b1c8fdb81195dc5bcefea3f3c240871cdd7ff9a2de8aa50772eb2", size = 2228726, upload-time = "2026-04-20T14:44:22.816Z" }, + { url = "https://files.pythonhosted.org/packages/e7/f8/65cd92dd5a0bd89ba277a98ecbfaf6fc36bbd3300973c7a4b826d6ab1391/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0087084960f209a9a4af50ecd1fb063d9ad3658c07bb81a7a53f452dacbfb2ba", size = 2301214, upload-time = "2026-04-20T14:44:48.792Z" }, + { url = "https://files.pythonhosted.org/packages/fd/86/ef96a4c6e79e7a2d0410826a68fbc0eccc0fd44aa733be199d5fcac3bb87/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed42e6cc8e1b0e2b9b96e2276bad70ae625d10d6d524aed0c93de974ae029f9f", size = 2099927, upload-time = "2026-04-20T14:41:40.196Z" }, + { url = "https://files.pythonhosted.org/packages/6d/53/269caf30e0096e0a8a8f929d1982a27b3879872cca2d917d17c2f9fdf4fe/pydantic_core-2.46.3-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:f1771ce258afb3e4201e67d154edbbae712a76a6081079fe247c2f53c6322c22", size = 2128789, upload-time = "2026-04-20T14:41:15.868Z" }, + { url = "https://files.pythonhosted.org/packages/00/b0/1a6d9b6a587e118482910c244a1c5acf4d192604174132efd12bf0ac486f/pydantic_core-2.46.3-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a7610b6a5242a6c736d8ad47fd5fff87fcfe8f833b281b1c409c3d6835d9227f", size = 2173815, upload-time = "2026-04-20T14:44:25.152Z" }, + { url = "https://files.pythonhosted.org/packages/87/56/e7e00d4041a7e62b5a40815590114db3b535bf3ca0bf4dca9f16cef25246/pydantic_core-2.46.3-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:ff5e7783bcc5476e1db448bf268f11cb257b1c276d3e89f00b5727be86dd0127", size = 2181608, upload-time = "2026-04-20T14:41:28.933Z" }, + { url = "https://files.pythonhosted.org/packages/e8/22/4bd23c3d41f7c185d60808a1de83c76cf5aeabf792f6c636a55c3b1ec7f9/pydantic_core-2.46.3-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:9d2e32edcc143bc01e95300671915d9ca052d4f745aa0a49c48d4803f8a85f2c", size = 2326968, upload-time = "2026-04-20T14:42:03.962Z" }, + { url = "https://files.pythonhosted.org/packages/24/ac/66cd45129e3915e5ade3b292cb3bc7fd537f58f8f8dbdaba6170f7cabb74/pydantic_core-2.46.3-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:6e42d83d1c6b87fa56b521479cff237e626a292f3b31b6345c15a99121b454c1", size = 2369842, upload-time = "2026-04-20T14:41:35.52Z" }, + { url = "https://files.pythonhosted.org/packages/a2/51/dd4248abb84113615473aa20d5545b7c4cd73c8644003b5259686f93996c/pydantic_core-2.46.3-cp314-cp314-win32.whl", hash = "sha256:07bc6d2a28c3adb4f7c6ae46aa4f2d2929af127f587ed44057af50bf1ce0f505", size = 1959661, upload-time = "2026-04-20T14:41:00.042Z" }, + { url = "https://files.pythonhosted.org/packages/20/eb/59980e5f1ae54a3b86372bd9f0fa373ea2d402e8cdcd3459334430f91e91/pydantic_core-2.46.3-cp314-cp314-win_amd64.whl", hash = "sha256:8940562319bc621da30714617e6a7eaa6b98c84e8c685bcdc02d7ed5e7c7c44e", size = 2071686, upload-time = "2026-04-20T14:43:16.471Z" }, + { url = "https://files.pythonhosted.org/packages/8c/db/1cf77e5247047dfee34bc01fa9bca134854f528c8eb053e144298893d370/pydantic_core-2.46.3-cp314-cp314-win_arm64.whl", hash = "sha256:5dcbbcf4d22210ced8f837c96db941bdb078f419543472aca5d9a0bb7cddc7df", size = 2026907, upload-time = "2026-04-20T14:43:31.732Z" }, + { url = "https://files.pythonhosted.org/packages/57/c0/b3df9f6a543276eadba0a48487b082ca1f201745329d97dbfa287034a230/pydantic_core-2.46.3-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:d0fe3dce1e836e418f912c1ad91c73357d03e556a4d286f441bf34fed2dbeecf", size = 2095047, upload-time = "2026-04-20T14:42:37.982Z" }, + { url = "https://files.pythonhosted.org/packages/66/57/886a938073b97556c168fd99e1a7305bb363cd30a6d2c76086bf0587b32a/pydantic_core-2.46.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9ce92e58abc722dac1bf835a6798a60b294e48eb0e625ec9fd994b932ac5feee", size = 1934329, upload-time = "2026-04-20T14:43:49.655Z" }, + { url = "https://files.pythonhosted.org/packages/0b/7c/b42eaa5c34b13b07ecb51da21761297a9b8eb43044c864a035999998f328/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a03e6467f0f5ab796a486146d1b887b2dc5e5f9b3288898c1b1c3ad974e53e4a", size = 1974847, upload-time = "2026-04-20T14:42:10.737Z" }, + { url = "https://files.pythonhosted.org/packages/e6/9b/92b42db6543e7de4f99ae977101a2967b63122d4b6cf7773812da2d7d5b5/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2798b6ba041b9d70acfb9071a2ea13c8456dd1e6a5555798e41ba7b0790e329c", size = 2041742, upload-time = "2026-04-20T14:40:44.262Z" }, + { url = "https://files.pythonhosted.org/packages/0f/19/46fbe1efabb5aa2834b43b9454e70f9a83ad9c338c1291e48bdc4fecf167/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9be3e221bdc6d69abf294dcf7aff6af19c31a5cdcc8f0aa3b14be29df4bd03b1", size = 2236235, upload-time = "2026-04-20T14:41:27.307Z" }, + { url = "https://files.pythonhosted.org/packages/77/da/b3f95bc009ad60ec53120f5d16c6faa8cabdbe8a20d83849a1f2b8728148/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f13936129ce841f2a5ddf6f126fea3c43cd128807b5a59588c37cf10178c2e64", size = 2282633, upload-time = "2026-04-20T14:44:33.271Z" }, + { url = "https://files.pythonhosted.org/packages/cc/6e/401336117722e28f32fb8220df676769d28ebdf08f2f4469646d404c43a3/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28b5f2ef03416facccb1c6ef744c69793175fd27e44ef15669201601cf423acb", size = 2109679, upload-time = "2026-04-20T14:44:41.065Z" }, + { url = "https://files.pythonhosted.org/packages/fc/53/b289f9bc8756a32fe718c46f55afaeaf8d489ee18d1a1e7be1db73f42cc4/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:830d1247d77ad23852314f069e9d7ddafeec5f684baf9d7e7065ed46a049c4e6", size = 2108342, upload-time = "2026-04-20T14:42:50.144Z" }, + { url = "https://files.pythonhosted.org/packages/10/5b/8292fc7c1f9111f1b2b7c1b0dcf1179edcd014fc3ea4517499f50b829d71/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0793c90c1a3c74966e7975eaef3ed30ebdff3260a0f815a62a22adc17e4c01c", size = 2157208, upload-time = "2026-04-20T14:42:08.133Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9e/f80044e9ec07580f057a89fc131f78dda7a58751ddf52bbe05eaf31db50f/pydantic_core-2.46.3-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:d2d0aead851b66f5245ec0c4fb2612ef457f8bbafefdf65a2bf9d6bac6140f47", size = 2167237, upload-time = "2026-04-20T14:42:25.412Z" }, + { url = "https://files.pythonhosted.org/packages/f8/84/6781a1b037f3b96be9227edbd1101f6d3946746056231bf4ac48cdff1a8d/pydantic_core-2.46.3-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:2f40e4246676beb31c5ce77c38a55ca4e465c6b38d11ea1bd935420568e0b1ab", size = 2312540, upload-time = "2026-04-20T14:40:40.313Z" }, + { url = "https://files.pythonhosted.org/packages/3e/db/19c0839feeb728e7df03255581f198dfdf1c2aeb1e174a8420b63c5252e5/pydantic_core-2.46.3-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:cf489cf8986c543939aeee17a09c04d6ffb43bfef8ca16fcbcc5cfdcbed24dba", size = 2369556, upload-time = "2026-04-20T14:41:09.427Z" }, + { url = "https://files.pythonhosted.org/packages/e0/15/3228774cb7cd45f5f721ddf1b2242747f4eb834d0c491f0c02d606f09fed/pydantic_core-2.46.3-cp314-cp314t-win32.whl", hash = "sha256:ffe0883b56cfc05798bf994164d2b2ff03efe2d22022a2bb080f3b626176dd56", size = 1949756, upload-time = "2026-04-20T14:41:25.717Z" }, + { url = "https://files.pythonhosted.org/packages/b8/2a/c79cf53fd91e5a87e30d481809f52f9a60dd221e39de66455cf04deaad37/pydantic_core-2.46.3-cp314-cp314t-win_amd64.whl", hash = "sha256:706d9d0ce9cf4593d07270d8e9f53b161f90c57d315aeec4fb4fd7a8b10240d8", size = 2051305, upload-time = "2026-04-20T14:43:18.627Z" }, + { url = "https://files.pythonhosted.org/packages/0b/db/d8182a7f1d9343a032265aae186eb063fe26ca4c40f256b21e8da4498e89/pydantic_core-2.46.3-cp314-cp314t-win_arm64.whl", hash = "sha256:77706aeb41df6a76568434701e0917da10692da28cb69d5fb6919ce5fdb07374", size = 2026310, upload-time = "2026-04-20T14:41:01.778Z" }, + { url = "https://files.pythonhosted.org/packages/66/7f/03dbad45cd3aa9083fbc93c210ae8b005af67e4136a14186950a747c6874/pydantic_core-2.46.3-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:9715525891ed524a0a1eb6d053c74d4d4ad5017677fb00af0b7c2644a31bae46", size = 2105683, upload-time = "2026-04-20T14:42:19.779Z" }, + { url = "https://files.pythonhosted.org/packages/26/22/4dc186ac8ea6b257e9855031f51b62a9637beac4d68ac06bee02f046f836/pydantic_core-2.46.3-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:9d2f400712a99a013aff420ef1eb9be077f8189a36c1e3ef87660b4e1088a874", size = 1940052, upload-time = "2026-04-20T14:43:59.274Z" }, + { url = "https://files.pythonhosted.org/packages/0d/ca/d376391a5aff1f2e8188960d7873543608130a870961c2b6b5236627c116/pydantic_core-2.46.3-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd2aab0e2e9dc2daf36bd2686c982535d5e7b1d930a1344a7bb6e82baab42a76", size = 1988172, upload-time = "2026-04-20T14:41:17.469Z" }, + { url = "https://files.pythonhosted.org/packages/0e/6b/523b9f85c23788755d6ab949329de692a2e3a584bc6beb67fef5e035aa9d/pydantic_core-2.46.3-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e9d76736da5f362fabfeea6a69b13b7f2be405c6d6966f06b2f6bfff7e64531", size = 2128596, upload-time = "2026-04-20T14:40:41.707Z" }, + { url = "https://files.pythonhosted.org/packages/34/42/f426db557e8ab2791bc7562052299944a118655496fbff99914e564c0a94/pydantic_core-2.46.3-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:b12dd51f1187c2eb489af8e20f880362db98e954b54ab792fa5d92e8bcc6b803", size = 2091877, upload-time = "2026-04-20T14:43:27.091Z" }, + { url = "https://files.pythonhosted.org/packages/5c/4f/86a832a9d14df58e663bfdf4627dc00d3317c2bd583c4fb23390b0f04b8e/pydantic_core-2.46.3-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:f00a0961b125f1a47af7bcc17f00782e12f4cd056f83416006b30111d941dfa3", size = 1932428, upload-time = "2026-04-20T14:40:45.781Z" }, + { url = "https://files.pythonhosted.org/packages/11/1a/fe857968954d93fb78e0d4b6df5c988c74c4aaa67181c60be7cfe327c0ca/pydantic_core-2.46.3-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:57697d7c056aca4bbb680200f96563e841a6386ac1129370a0102592f4dddff5", size = 1997550, upload-time = "2026-04-20T14:44:02.425Z" }, + { url = "https://files.pythonhosted.org/packages/17/eb/9d89ad2d9b0ba8cd65393d434471621b98912abb10fbe1df08e480ba57b5/pydantic_core-2.46.3-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd35aa21299def8db7ef4fe5c4ff862941a9a158ca7b63d61e66fe67d30416b4", size = 2137657, upload-time = "2026-04-20T14:42:45.149Z" }, + { url = "https://files.pythonhosted.org/packages/1f/da/99d40830684f81dec901cac521b5b91c095394cc1084b9433393cde1c2df/pydantic_core-2.46.3-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:13afdd885f3d71280cf286b13b310ee0f7ccfefd1dbbb661514a474b726e2f25", size = 2107973, upload-time = "2026-04-20T14:42:06.175Z" }, + { url = "https://files.pythonhosted.org/packages/99/a5/87024121818d75bbb2a98ddbaf638e40e7a18b5e0f5492c9ca4b1b316107/pydantic_core-2.46.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:f91c0aff3e3ee0928edd1232c57f643a7a003e6edf1860bc3afcdc749cb513f3", size = 1947191, upload-time = "2026-04-20T14:43:14.319Z" }, + { url = "https://files.pythonhosted.org/packages/60/62/0c1acfe10945b83a6a59d19fbaa92f48825381509e5701b855c08f13db76/pydantic_core-2.46.3-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6529d1d128321a58d30afcc97b49e98836542f68dd41b33c2e972bb9e5290536", size = 2123791, upload-time = "2026-04-20T14:43:22.766Z" }, + { url = "https://files.pythonhosted.org/packages/75/3e/3b2393b4c8f44285561dc30b00cf307a56a2eff7c483a824db3b8221ca51/pydantic_core-2.46.3-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:975c267cff4f7e7272eacbe50f6cc03ca9a3da4c4fbd66fffd89c94c1e311aa1", size = 2153197, upload-time = "2026-04-20T14:44:27.932Z" }, + { url = "https://files.pythonhosted.org/packages/ba/75/5af02fb35505051eee727c061f2881c555ab4f8ddb2d42da715a42c9731b/pydantic_core-2.46.3-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:2b8e4f2bbdf71415c544b4b1138b8060db7b6611bc927e8064c769f64bed651c", size = 2181073, upload-time = "2026-04-20T14:43:20.729Z" }, + { url = "https://files.pythonhosted.org/packages/10/92/7e0e1bd9ca3c68305db037560ca2876f89b2647deb2f8b6319005de37505/pydantic_core-2.46.3-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:e61ea8e9fff9606d09178f577ff8ccdd7206ff73d6552bcec18e1033c4254b85", size = 2315886, upload-time = "2026-04-20T14:44:04.826Z" }, + { url = "https://files.pythonhosted.org/packages/b8/d8/101655f27eaf3e44558ead736b2795d12500598beed4683f279396fa186e/pydantic_core-2.46.3-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:b504bda01bafc69b6d3c7a0c7f039dcf60f47fab70e06fe23f57b5c75bdc82b8", size = 2360528, upload-time = "2026-04-20T14:40:47.431Z" }, + { url = "https://files.pythonhosted.org/packages/07/0f/1c34a74c8d07136f0d729ffe5e1fdab04fbdaa7684f61a92f92511a84a15/pydantic_core-2.46.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:b00b76f7142fc60c762ce579bd29c8fa44aaa56592dd3c54fab3928d0d4ca6ff", size = 2184144, upload-time = "2026-04-20T14:42:57Z" }, ] [[package]] @@ -2358,15 +2262,15 @@ wheels = [ [[package]] name = "pyinstaller-hooks-contrib" -version = "2026.5" +version = "2026.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "packaging" }, { name = "setuptools" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1a/67/f4452d68793fb15beba4f19ef39a38a8822f0da7452b503c400d5a21f5c1/pyinstaller_hooks_contrib-2026.5.tar.gz", hash = "sha256:f066dfca8f7c45ff6336c9cf9fe25b4e48bfeb322a1aa24faaedfb8a8d1b0b08", size = 173689, upload-time = "2026-05-04T22:36:55.124Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/fe/9278c29394bf69169febc21f96b4252c3ee7c8ec22c2fc545004bed47e71/pyinstaller_hooks_contrib-2026.4.tar.gz", hash = "sha256:766c281acb1ecc32e21c8c667056d7ebf5da0aabd5e30c219f9c2a283620eeaa", size = 173050, upload-time = "2026-03-31T14:10:51.188Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f6/5c/fd465d11da4d12b50d7eb5d2ee2ceb780d8d049dbb489f3828d131e387af/pyinstaller_hooks_contrib-2026.5-py3-none-any.whl", hash = "sha256:ea1535783fbdac4626351709e83f3ea80b681d3a4745763ebb407b5e27342eb9", size = 457314, upload-time = "2026-05-04T22:36:53.598Z" }, + { url = "https://files.pythonhosted.org/packages/88/f4/035fb8c06deff827f540a9a4ed9122c54e5376fca3e42eddf0c263730775/pyinstaller_hooks_contrib-2026.4-py3-none-any.whl", hash = "sha256:1de1a5e49a878122010b88c7e295502bc69776c157c4a4dc78741a4e6178b00f", size = 455496, upload-time = "2026-03-31T14:10:49.867Z" }, ] [[package]] @@ -2383,15 +2287,15 @@ wheels = [ [[package]] name = "pyopenssl" -version = "26.2.0" +version = "26.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cryptography" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1a/51/27a5ad5f939d08f690a326ef9582cda7140555180db71695f6fb747d6a36/pyopenssl-26.2.0.tar.gz", hash = "sha256:8c6fcecd1183a7fc897548dfe388b0cdb7f37e018200d8409cf33959dbe35387", size = 182195, upload-time = "2026-05-04T23:06:09.72Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/a8/26d36401e3ab8eed9030ad33f381da7856fcfad5691780fccd1b019718fc/pyopenssl-26.1.0.tar.gz", hash = "sha256:737f0a2275c5bc54f3b02137687e1a765931fb3949b9a92a825e4d33b9eec08b", size = 186181, upload-time = "2026-04-24T20:23:48.115Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/73/b8/a0e2790ae249d6f38c9f66de7a211621a7ab2650217bcd04e1262f578a56/pyopenssl-26.2.0-py3-none-any.whl", hash = "sha256:4f9d971bc5298b8bc1fab282803da04bf000c755d4ad9d99b52de2569ca19a70", size = 55823, upload-time = "2026-05-04T23:06:08.395Z" }, + { url = "https://files.pythonhosted.org/packages/a8/41/52f3a3e812b816a91e89aa504199d8bf989a1f873192b10762be66cf2009/pyopenssl-26.1.0-py3-none-any.whl", hash = "sha256:115563879b2c8ccb207975705d3e491434d8c9d7c79667c902ecbf5f3bbd2ece", size = 58109, upload-time = "2026-04-24T20:23:46.273Z" }, ] [[package]] @@ -2458,15 +2362,15 @@ wheels = [ [[package]] name = "python-discovery" -version = "1.3.0" +version = "1.2.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "filelock" }, { name = "platformdirs" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ae/e0/cc5a8653e9a24f6cf84768f05064aa8ed5a83dcefd5e2a043db14a1c5f44/python_discovery-1.3.0.tar.gz", hash = "sha256:d098f1e86be5d45fe4d14bf1029294aabbd332f4321179dec85e76cddce834b0", size = 63925, upload-time = "2026-05-05T14:38:39.769Z" } +sdist = { url = "https://files.pythonhosted.org/packages/de/ef/3bae0e537cfe91e8431efcba4434463d2c5a65f5a89edd47c6cf2f03c55f/python_discovery-1.2.2.tar.gz", hash = "sha256:876e9c57139eb757cb5878cbdd9ae5379e5d96266c99ef731119e04fffe533bb", size = 58872, upload-time = "2026-04-07T17:28:49.249Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/30/d4/24d543ab8b8158b7f5a97113c831205f5c900c92c8762b1e7f44b7ea0405/python_discovery-1.3.0-py3-none-any.whl", hash = "sha256:441d9ced3dfce36e113beb35ca302c71c7ef06f3c0f9c227a0b9bb3bd49b9e9f", size = 33124, upload-time = "2026-05-05T14:38:38.539Z" }, + { url = "https://files.pythonhosted.org/packages/d8/db/795879cc3ddfe338599bddea6388cc5100b088db0a4caf6e6c1af1c27e04/python_discovery-1.2.2-py3-none-any.whl", hash = "sha256:e1ae95d9af875e78f15e19aed0c6137ab1bb49c200f21f5061786490c9585c7a", size = 31894, upload-time = "2026-04-07T17:28:48.09Z" }, ] [[package]] @@ -2519,11 +2423,11 @@ wheels = [ [[package]] name = "pytz" -version = "2026.2" +version = "2026.1.post1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ff/46/dd499ec9038423421951e4fad73051febaa13d2df82b4064f87af8b8c0c3/pytz-2026.2.tar.gz", hash = "sha256:0e60b47b29f21574376f218fe21abc009894a2321ea16c6754f3cad6eb7cdd6a", size = 320861, upload-time = "2026-05-04T01:35:29.667Z" } +sdist = { url = "https://files.pythonhosted.org/packages/56/db/b8721d71d945e6a8ac63c0fc900b2067181dbb50805958d4d4661cf7d277/pytz-2026.1.post1.tar.gz", hash = "sha256:3378dde6a0c3d26719182142c56e60c7f9af7e968076f31aae569d72a0358ee1", size = 321088, upload-time = "2026-03-03T07:47:50.683Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/dd/96da98f892250475bdf2328112d7468abdd4acc7b902b6af23f4ed958ea0/pytz-2026.2-py2.py3-none-any.whl", hash = "sha256:04156e608bee23d3792fd45c94ae47fae1036688e75032eea2e3bf0323d1f126", size = 510141, upload-time = "2026-05-04T01:35:27.408Z" }, + { url = "https://files.pythonhosted.org/packages/10/99/781fe0c827be2742bcc775efefccb3b048a3a9c6ce9aec0cbf4a101677e5/pytz-2026.1.post1-py2.py3-none-any.whl", hash = "sha256:f2fd16142fda348286a75e1a524be810bb05d444e5a081f37f7affc635035f7a", size = 510489, upload-time = "2026-03-03T07:47:49.167Z" }, ] [[package]] @@ -2903,7 +2807,7 @@ wheels = [ [[package]] name = "sqlalchemy-bigquery" -version = "1.17.0" +version = "1.16.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "google-api-core" }, @@ -2912,9 +2816,9 @@ dependencies = [ { name = "packaging" }, { name = "sqlalchemy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1d/94/6fd01b23a92a2372a71cd1670302a6c11b138ad80906914433e6ddbc1e1a/sqlalchemy_bigquery-1.17.0.tar.gz", hash = "sha256:472284546a0c79cbf99b1bb0f5f99c5131fa888ea25d2d53208e6863e5094e2f", size = 119746, upload-time = "2026-05-07T08:04:51.805Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/6a/c49932b3d9c44cab9202b1866c5b36b7f0d0455d4653fbc0af4466aeaa76/sqlalchemy_bigquery-1.16.0.tar.gz", hash = "sha256:fe937a0d1f4cf7219fcf5d4995c6718805b38d4df43e29398dec5dc7b6d1987e", size = 119632, upload-time = "2025-11-06T01:35:40.373Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a3/bf/64ae26c6b58665b76abee9f7e536cef0e886c37e1da0b18f75133ff2fa4d/sqlalchemy_bigquery-1.17.0-py3-none-any.whl", hash = "sha256:89c1d4fc9f045ce762c93bf4b73a6c51a203dcf0dbe2d9ade540c7c5e3ed01dd", size = 39802, upload-time = "2026-05-07T08:03:33.787Z" }, + { url = "https://files.pythonhosted.org/packages/c0/87/11e6de00ef7949bb8ea06b55304a1a4911c329fdf0d9882b464db240c2c5/sqlalchemy_bigquery-1.16.0-py3-none-any.whl", hash = "sha256:0fe7634cd954f3e74f5e2db6d159f9e5ee87a47fbe8d52eac3cd3bb3dadb3a77", size = 40615, upload-time = "2025-11-06T01:35:39.358Z" }, ] [[package]] @@ -3058,35 +2962,35 @@ wheels = [ [[package]] name = "traitlets" -version = "5.15.0" +version = "5.14.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1b/22/40f55b26baeab80c2d7b3f1db0682f8954e4617fee7d90ce634022ef05c6/traitlets-5.15.0.tar.gz", hash = "sha256:4fead733f81cf1c4c938e06f8ca4633896833c9d89eff878159457f4d4392971", size = 163197, upload-time = "2026-05-06T08:05:58.016Z" } +sdist = { url = "https://files.pythonhosted.org/packages/eb/79/72064e6a701c2183016abbbfedaba506d81e30e232a68c9f0d6f6fcd1574/traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7", size = 161621, upload-time = "2024-04-19T11:11:49.746Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/da/98/a9937a969d018a23badfea0b381f66783649d48e0ea6c41923265c3cbeb3/traitlets-5.15.0-py3-none-any.whl", hash = "sha256:fb36a18867a6803deab09f3c5e0fa81bb7b26a5c9e82501c9933f759166eff40", size = 85877, upload-time = "2026-05-06T08:05:55.853Z" }, + { url = "https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f", size = 85359, upload-time = "2024-04-19T11:11:46.763Z" }, ] [[package]] name = "ty" -version = "0.0.34" +version = "0.0.33" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c4/69/e24eefe2c35c0fdbdec9b60e162727af669bb76d64d993d982eb67b24c38/ty-0.0.34.tar.gz", hash = "sha256:a6efe66b0f13c03a65e6c72ec9abfe2792e2fd063c74fa67e2c4930e29d661be", size = 5585933, upload-time = "2026-05-01T23:06:46.388Z" } +sdist = { url = "https://files.pythonhosted.org/packages/84/44/9478c50c266826c1bf30d1692e589755bffa8f1c0a3eb7af8a346c255991/ty-0.0.33.tar.gz", hash = "sha256:46d63bda07403322cb6c28ccfdd5536be916e13df725c29f7ccd0a21f06bd9e8", size = 5559373, upload-time = "2026-04-28T10:45:13.18Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/83/7b/8b85003d6639ef17a97dcbb31f4511cfe78f1c81a964470db100c8c883e7/ty-0.0.34-py3-none-linux_armv6l.whl", hash = "sha256:9ecc3d14f07a95a6ceb88e07f8e62358dbd37325d3d5bd56da7217ff1fef7fb8", size = 11067094, upload-time = "2026-05-01T23:06:21.133Z" }, - { url = "https://files.pythonhosted.org/packages/d7/25/b0098f65b020b015c40567c763fc66fffbec88b2ba6f584bca1e92f05ebb/ty-0.0.34-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:0dccffd8a9d02321cd2dee3249df205e26d62694e741f4eeca36b157fd8b419f", size = 10840909, upload-time = "2026-05-01T23:06:18.409Z" }, - { url = "https://files.pythonhosted.org/packages/e4/55/5e4adcf7d2a1006b844903b27cb81244a9b748d850433a46a6c21776c401/ty-0.0.34-py3-none-macosx_11_0_arm64.whl", hash = "sha256:b0ea47a2998e167ab3b21d2f4b5309a9cf33c297809f6d7e3e753252223174d0", size = 10279378, upload-time = "2026-05-01T23:06:37.962Z" }, - { url = "https://files.pythonhosted.org/packages/4d/91/f537dca0db8fe2558e8ab04d8941d687b384fcc1df5eb9023b2db75ac26c/ty-0.0.34-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b37da00b41a118a459ae56d8947e70651073fb33ebfbceb820e4a10b22d5023", size = 10817423, upload-time = "2026-05-01T23:06:26.247Z" }, - { url = "https://files.pythonhosted.org/packages/2c/c4/55a3ad1da2815af1009bdc1b8c90dc11a364cd314e4b48c5128ba9d38859/ty-0.0.34-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:81cbbb93c2342fe3de43e625d3a9eb149633e9f485e816ebf6395d08685355d8", size = 10851826, upload-time = "2026-05-01T23:06:24.198Z" }, - { url = "https://files.pythonhosted.org/packages/ce/8c/9c7606af22d73fb43ea4369472d9c66ece11231be73b0efe8e3c61655559/ty-0.0.34-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c5b4dea1594a021289e172582df9cde7089dce14b276fc650e7b212b1772e12", size = 11356318, upload-time = "2026-05-01T23:06:51.139Z" }, - { url = "https://files.pythonhosted.org/packages/20/54/bb423f663721ab4138b216425c6b55eaefd3a068243b24d6d8fe988f4e13/ty-0.0.34-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:030fb00aa2d2a5b5ae9d9183d574e0c82dae80566700a7490c43669d8ece40cd", size = 11902968, upload-time = "2026-05-01T23:06:35.82Z" }, - { url = "https://files.pythonhosted.org/packages/b6/22/01122b21ab6b534a2f618c6bbe5f1f7f49fd56f4b2ec8887cd6d40d08fb3/ty-0.0.34-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ae9555e24e36c63a8218e037a5a63f15579eb6aa94f41017e57cd41d335cfb5", size = 11548860, upload-time = "2026-05-01T23:06:42.155Z" }, - { url = "https://files.pythonhosted.org/packages/d1/50/86008b1392ec64bed1957bbcc7aaa43b466b50dfc91bb131841c21d7c5c3/ty-0.0.34-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99eb23df9ed129fc26d1ab00d6f0b8dfe5253b09c2ac6abdb11523fa70d67f10", size = 11457097, upload-time = "2026-05-01T23:06:53.477Z" }, - { url = "https://files.pythonhosted.org/packages/92/3e/4558b2296963ba99c58d8409c57d7db4f3061b656c3613cb21c02c1ef4c2/ty-0.0.34-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:85de45382016eceae69e104815eb2cfa200787df104002e262a86cbd43ed2c02", size = 10798192, upload-time = "2026-05-01T23:06:40.004Z" }, - { url = "https://files.pythonhosted.org/packages/76/bf/650d24402be2ef678528d60caac1d9477a40fc37e3792ecef07834fd7a4a/ty-0.0.34-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:14cb575fb8fa5131f5129d100cfe23c1575d23faf5dfc5158432749a3e38c9b5", size = 10890390, upload-time = "2026-05-01T23:06:33.076Z" }, - { url = "https://files.pythonhosted.org/packages/5c/ef/ccd2ca13906079f7935fd7e067661b24233017f57d987d51d6a121d85bb5/ty-0.0.34-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c6fc0b69d8450e6910ba9db34572b959b81329a97ae273c391f70e9fb6c1aade", size = 11031564, upload-time = "2026-05-01T23:06:55.812Z" }, - { url = "https://files.pythonhosted.org/packages/ba/2d/d27b72005b6f43599e3bcabab0d7135ac0c230b7a307bb99f9eea02c1cda/ty-0.0.34-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:30dfcec2f0fde3993f4f912ed0e057dcbebc8615299f610a4c2ddb7b5a3e1e06", size = 11553430, upload-time = "2026-05-01T23:06:31.096Z" }, - { url = "https://files.pythonhosted.org/packages/a7/12/20812e1ad930b8d4af70eebf19ad23cff6e31efcfa613ef884531fcdbaa1/ty-0.0.34-py3-none-win32.whl", hash = "sha256:97b77ddf007271b812a313a8f0a14929bc5590958433e1fb83ef585676f53342", size = 10436048, upload-time = "2026-05-01T23:06:49.108Z" }, - { url = "https://files.pythonhosted.org/packages/b0/6a/afa095c5987868fbda27c0f731146ac8e3d07b357adfa83daccaee5b1a16/ty-0.0.34-py3-none-win_amd64.whl", hash = "sha256:1f543968accb952705134028d1fda8656882787dbbc667ad4d6c3ba23791d604", size = 11462526, upload-time = "2026-05-01T23:06:28.514Z" }, - { url = "https://files.pythonhosted.org/packages/63/8f/bf041a06260d77662c0605e56dacfe90b786bf824cbe1aed238d15fe5e84/ty-0.0.34-py3-none-win_arm64.whl", hash = "sha256:ea09108cbcb16b6b06d7596312b433bf49681e78d30e4dc7fb3c1b248a95e09a", size = 10846945, upload-time = "2026-05-01T23:06:44.428Z" }, + { url = "https://files.pythonhosted.org/packages/e9/24/e287388c63a19191be26b32ff4dbd06029834068150ebe2532939bc4c851/ty-0.0.33-py3-none-linux_armv6l.whl", hash = "sha256:94d0a9d2234261a8911396d59e506b5923fe0971dbda43b9dcea287936887fcc", size = 11021308, upload-time = "2026-04-28T10:45:43.34Z" }, + { url = "https://files.pythonhosted.org/packages/00/ca/ba1eed819895bd239fba8ee35dfcd5fcb266c203b0914a17a59579096bb5/ty-0.0.33-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e4a2b5ba078f90de342f56b5f7979bb77c9b9b1d8625a041352ffc6ee93c4073", size = 10777272, upload-time = "2026-04-28T10:45:32.905Z" }, + { url = "https://files.pythonhosted.org/packages/25/a8/c3131d37b44b3fea1d6654a1c929a0cd0873822f77a90482b8ec28f6fbbd/ty-0.0.33-py3-none-macosx_11_0_arm64.whl", hash = "sha256:84ff5707825e9af9668d2bcf66975f93e520a63b524ab494e3a8265735be2563", size = 10201078, upload-time = "2026-04-28T10:45:23.374Z" }, + { url = "https://files.pythonhosted.org/packages/7b/db/d8e37ff0045810cc65e1ff36aa0da0a2253c05659787ac987df8a16c7897/ty-0.0.33-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e375285736f57886868e7af0b11c7b0ec5b6543fa15e7ad2a714fed9f077d4e0", size = 10732347, upload-time = "2026-04-28T10:45:21.444Z" }, + { url = "https://files.pythonhosted.org/packages/e0/1a/20e83a412506a918e4684fc67b567cf7cc13b105470b3428cb23c3d5aa13/ty-0.0.33-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5680f6350c3b4e46b8bff6d7bb132366ea239463d6cad4892725d06046e65464", size = 10808238, upload-time = "2026-04-28T10:45:38.565Z" }, + { url = "https://files.pythonhosted.org/packages/5d/4b/d0a39f4464dc6cb4cc2c159473ce216bd1846bfb684c0323a3cb36dce5c6/ty-0.0.33-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c5535538bad8d0f7e62bcdff02197cdb30e41451d80b35d27e17d128f2e1dc5d", size = 11288348, upload-time = "2026-04-28T10:45:08.419Z" }, + { url = "https://files.pythonhosted.org/packages/35/7e/f1745e0f9583363d7a83d9a4990fc244f76ecc30840ddad83dc16a33c52d/ty-0.0.33-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:da196c42bbbc069e1e21e3e52107c061aa9660352dae57a41930690b56e2c02d", size = 11789907, upload-time = "2026-04-28T10:45:19.064Z" }, + { url = "https://files.pythonhosted.org/packages/a5/71/25f39f46a12d662859d45bc648555d0661044eb43db6b5648c9947487da9/ty-0.0.33-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9281672921ef6d4460e03146b5e6c18cb1a3e3a3b8a1a88f6f33226d05a469b7", size = 11500774, upload-time = "2026-04-28T10:45:48.012Z" }, + { url = "https://files.pythonhosted.org/packages/94/ec/136959ecbb7c71cb90537f5aea441c73f4ab24612868a6ecdc9d7444d32d/ty-0.0.33-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82c1b8f303f82da64e878108e764be3ecbcd7c9903ac0a7f7031614ed00b97ab", size = 11360314, upload-time = "2026-04-28T10:45:05.402Z" }, + { url = "https://files.pythonhosted.org/packages/cf/95/32809575c222f00beed498cb728e9290a0f5009f930025381bb7253b2206/ty-0.0.33-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:efe3af412c9ff67bce5fa37d0a2b0d8555c24072b145a5bac6c79637f1c83abe", size = 10707785, upload-time = "2026-04-28T10:45:10.836Z" }, + { url = "https://files.pythonhosted.org/packages/13/89/c8e9531f7aa4a093359e15fa32c8e1277fbbe90d16894d7c6032d29f4b34/ty-0.0.33-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:aeec29c91ea768601747da546c3efc20b72c2fb1bd52bcc786a5c6eeff51d27b", size = 10834987, upload-time = "2026-04-28T10:45:40.738Z" }, + { url = "https://files.pythonhosted.org/packages/31/16/9835fbcf5338af1a1917bd28fdb8a7193c210b83f243aa286fa9f79cb3ad/ty-0.0.33-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a535977c52bbb5f7e96b8b70a6ad375ad077f4a9ff2492508ea3816a2b403819", size = 10968968, upload-time = "2026-04-28T10:45:30.26Z" }, + { url = "https://files.pythonhosted.org/packages/36/69/64c76aabc1bc70c7f24b686cd93c3407f8ea430905e395f59bf9603ef571/ty-0.0.33-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1d732facf39fcb221ba279d469c5040d37883e964f123b1563888efd34818180", size = 11458077, upload-time = "2026-04-28T10:45:45.971Z" }, + { url = "https://files.pythonhosted.org/packages/91/84/fae27b0c4718776a298690d31ca4cc1995f2e3e1c63a7b59e84c41498e9a/ty-0.0.33-py3-none-win32.whl", hash = "sha256:d90960b574428dc252f85e8598ec5fcb7f619794196b2fc95a90da075ed4681c", size = 10345364, upload-time = "2026-04-28T10:45:16.836Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a0/a2938b23ae3e1a09a2d7c189e2ac5f7113676bae4e0e23948b568e18e5f8/ty-0.0.33-py3-none-win_amd64.whl", hash = "sha256:c1c3aec62c44de610c6e95f0a4e97ac3dbc07934bfdbf1fd90d758c9ff72f48e", size = 11342470, upload-time = "2026-04-28T10:45:26.455Z" }, + { url = "https://files.pythonhosted.org/packages/ab/62/7fb948aace38d2f6329261bb33c035a8484549c74f1db28649c7a4c6fed9/ty-0.0.33-py3-none-win_arm64.whl", hash = "sha256:0d44f99ba1b441e55e2aa301b2ac0a21112784931b46a5f66f4ea9efe5620d97", size = 10742673, upload-time = "2026-04-28T10:45:35.555Z" }, ] [[package]] @@ -3133,16 +3037,16 @@ wheels = [ [[package]] name = "urllib3" -version = "2.7.0" +version = "2.6.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, + { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, ] [[package]] name = "virtualenv" -version = "21.3.1" +version = "21.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "distlib" }, @@ -3151,16 +3055,16 @@ dependencies = [ { name = "python-discovery" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ec/0d/915c02c94d207b85580eb09bffab54438a709e7288524094fe781da526c2/virtualenv-21.3.1.tar.gz", hash = "sha256:c2305bc1fddeec40699b8370d13f8d431b0701f00ce895061ce493aeded4426b", size = 7613791, upload-time = "2026-05-05T01:34:31.402Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3f/8b/6331f7a7fe70131c301106ec1e7cf23e2501bf7d4ca3636805801ca191bb/virtualenv-21.3.0.tar.gz", hash = "sha256:733750db978ec95c2d8eb4feadaa57091002bce404cb39ba69899cf7bd28944e", size = 7614069, upload-time = "2026-04-27T17:05:58.927Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b1/4f/f71e641e504111a5a74e3a20bc52d01bd86788b22699dd3fee1c63253cf6/virtualenv-21.3.1-py3-none-any.whl", hash = "sha256:d1a71cf58f2f9228fff23a1f6ec15d39785c6b32e03658d104974247145edd35", size = 7594539, upload-time = "2026-05-05T01:34:28.98Z" }, + { url = "https://files.pythonhosted.org/packages/4b/eb/03bfb1299d4c4510329e470f13f9a4ce793df7fcb5a2fd3510f911066f61/virtualenv-21.3.0-py3-none-any.whl", hash = "sha256:4d28ee41f6d9ec8f1f00cd472b9ffbcedda1b3d3b9a575b5c94a2d004fd51bd7", size = 7594690, upload-time = "2026-04-27T17:05:55.468Z" }, ] [[package]] name = "wcwidth" -version = "0.7.0" +version = "0.6.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2c/ee/afaf0f85a9a18fe47a67f1e4422ed6cf1fe642f0ae0a2f81166231303c52/wcwidth-0.7.0.tar.gz", hash = "sha256:90e3a7ea092341c44b99562e75d09e4d5160fe7a3974c6fb842a101a95e7eed0", size = 182132, upload-time = "2026-05-02T16:04:12.653Z" } +sdist = { url = "https://files.pythonhosted.org/packages/35/a2/8e3becb46433538a38726c948d3399905a4c7cabd0df578ede5dc51f0ec2/wcwidth-0.6.0.tar.gz", hash = "sha256:cdc4e4262d6ef9a1a57e018384cbeb1208d8abbc64176027e2c2455c81313159", size = 159684, upload-time = "2026-02-06T19:19:40.919Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/41/52/e465037f5375f43533d1a80b6923955201596a99142ed524d77b571a1418/wcwidth-0.7.0-py3-none-any.whl", hash = "sha256:5d69154c429a82910e241c738cd0e2976fac8a2dd47a1a805f4afed1c0f136f2", size = 110825, upload-time = "2026-05-02T16:04:11.033Z" }, + { url = "https://files.pythonhosted.org/packages/68/5a/199c59e0a824a3db2b89c5d2dade7ab5f9624dbf6448dc291b46d5ec94d3/wcwidth-0.6.0-py3-none-any.whl", hash = "sha256:1a3a1e510b553315f8e146c54764f4fb6264ffad731b3d78088cdb1478ffbdad", size = 94189, upload-time = "2026-02-06T19:19:39.646Z" }, ]