diff --git a/agents/bug-fix/Dockerfile b/agents/bug-fix/Dockerfile new file mode 100644 index 0000000000..898f209b69 --- /dev/null +++ b/agents/bug-fix/Dockerfile @@ -0,0 +1,45 @@ +FROM python:3.12 AS builder + +COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ + +WORKDIR /app + +# Workspace metadata. +COPY pyproject.toml uv.lock VERSION ./ + +# pyproject.toml of every workspace member uv needs to keep its +# lockfile coherent. +COPY http_service/pyproject.toml ./http_service/pyproject.toml +COPY services/hackbot-api/pyproject.toml ./services/hackbot-api/pyproject.toml + +# Workspace members the agent image actually needs (source included). +COPY agents/bug-fix ./agents/bug-fix +COPY bugbug ./bugbug +COPY libs/hackbot-runtime ./libs/hackbot-runtime + +RUN uv sync --locked --no-dev --package hackbot-agent-bug-fix + +FROM python:3.12 AS base + +COPY --from=builder /app /app +WORKDIR /app/agents/bug-fix + +ENV PYTHONUNBUFFERED=1 +ENV PATH="/app/.venv/bin:$PATH" + +FROM base AS agent + +RUN useradd --create-home --shell /bin/bash agent \ + && mkdir -p /workspace \ + && chown agent:agent /workspace + +ENV PYTHONDONTWRITEBYTECODE=1 +USER agent + +CMD ["python", "-m", "agent_runner"] + +FROM base AS broker + +EXPOSE 8765 + +CMD ["python", "-m", "broker"] diff --git a/agents/bug-fix/agent_runner/__init__.py b/agents/bug-fix/agent_runner/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/agents/bug-fix/agent_runner/__main__.py b/agents/bug-fix/agent_runner/__main__.py new file mode 100644 index 0000000000..611fa110e2 --- /dev/null +++ b/agents/bug-fix/agent_runner/__main__.py @@ -0,0 +1,84 @@ +import logging +import subprocess +import sys +import tempfile +from pathlib import Path + +from pydantic_settings import BaseSettings, SettingsConfigDict + +from hackbot_runtime import AgentResult, Context, run_async + +log = logging.getLogger("bug-fix-agent") + +FIREFOX_REPO_URL = "https://github.com/mozilla-firefox/firefox.git" + + +class AgentInputs(BaseSettings): + bug_id: int + bugzilla_mcp_url: str + source_repo: Path = Path("/workspace/firefox") + model: str | None = None + max_turns: int | None = None + effort: str | None = None + + model_config = SettingsConfigDict(extra="ignore") + + +def ensure_firefox_source(source_repo: Path) -> None: + """Shallow-clone the Firefox source tree if it isn't already present. + + Idempotent: a mounted volume or pre-baked image with an existing + checkout short-circuits the clone. + """ + if (source_repo / ".git").exists(): + log.info("firefox source already present at %s", source_repo) + return + source_repo.mkdir(parents=True, exist_ok=True) + log.info("cloning firefox source (shallow) to %s", source_repo) + subprocess.run( + ["git", "clone", "--depth=1", FIREFOX_REPO_URL, str(source_repo)], + check=True, + stdout=sys.stderr, + stderr=sys.stderr, + ) + log.info("firefox shallow clone complete") + + +async def main(ctx: Context) -> AgentResult: + from bugbug.tools.bug_fix.agent import BugFixTool + + inputs = AgentInputs() + ensure_firefox_source(inputs.source_repo) + + log_path = Path(tempfile.mkdtemp(prefix="bug-fix-log-")) / "agent.log" + + tool = BugFixTool.create() + result = await tool.run( + bugzilla_mcp_server={ + "type": "http", + "url": inputs.bugzilla_mcp_url, + }, + source_repo=inputs.source_repo, + bugs=[inputs.bug_id], + model=inputs.model, + max_turns=inputs.max_turns, + effort=inputs.effort, + log=log_path, + ) + + if log_path.exists() and ctx.uploader is not None: + ctx.uploader.upload_file("logs/agent.log", log_path, "text/plain") + + return AgentResult( + status="ok" if result.exit_code == 0 else "error", + error=None if result.exit_code == 0 else f"exit_code={result.exit_code}", + findings={ + "exit_code": result.exit_code, + "bugs_processed": result.bugs_processed, + }, + exit_code=result.exit_code, + ) + + +if __name__ == "__main__": + raise SystemExit(run_async(main)) diff --git a/agents/bug-fix/broker/__init__.py b/agents/bug-fix/broker/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/agents/bug-fix/broker/__main__.py b/agents/bug-fix/broker/__main__.py new file mode 100644 index 0000000000..9824936126 --- /dev/null +++ b/agents/bug-fix/broker/__main__.py @@ -0,0 +1,73 @@ +"""Bugzilla MCP broker. + +Sidecar container that holds the Bugzilla API key and serves the +bugzilla MCP tools over HTTP. The agent process (in a sibling container +in the same Cloud Run Job task) reaches us at `127.0.0.1:/mcp`. +The agent container itself binds no Bugzilla credentials. +""" + +import logging +from contextlib import asynccontextmanager + +import bugsy +import uvicorn +from mcp.server.streamable_http_manager import StreamableHTTPSessionManager +from pydantic_settings import BaseSettings, SettingsConfigDict +from starlette.applications import Starlette +from starlette.routing import Mount + +from bugbug.tools.bug_fix.bugzilla_mcp import BugzillaContext +from bugbug.tools.bug_fix.bugzilla_mcp import build_server as build_bugzilla_server + +log = logging.getLogger("bugzilla-broker") + + +class BrokerInputs(BaseSettings): + bugzilla_api_url: str + bugzilla_api_key: str + dry_run: bool = False + host: str = "0.0.0.0" + port: int = 8765 + + model_config = SettingsConfigDict(extra="ignore") + + +def build_app(inputs: BrokerInputs) -> Starlette: + client = bugsy.Bugsy( + api_key=inputs.bugzilla_api_key, bugzilla_url=inputs.bugzilla_api_url + ) + ctx = BugzillaContext(client=client, dry_run=inputs.dry_run) + sdk_config = build_bugzilla_server(ctx) + mcp_server = sdk_config["instance"] + + manager = StreamableHTTPSessionManager(app=mcp_server, stateless=True) + + @asynccontextmanager + async def lifespan(app): + async with manager.run(): + log.info( + "bugzilla broker ready on %s:%d (dry_run=%s)", + inputs.host, + inputs.port, + inputs.dry_run, + ) + yield + + async def mcp_handler(scope, receive, send): + await manager.handle_request(scope, receive, send) + + return Starlette(routes=[Mount("/mcp", app=mcp_handler)], lifespan=lifespan) + + +def main() -> None: + logging.basicConfig( + level=logging.INFO, + format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", + ) + inputs = BrokerInputs() + app = build_app(inputs) + uvicorn.run(app, host=inputs.host, port=inputs.port, log_config=None) + + +if __name__ == "__main__": + main() diff --git a/agents/bug-fix/compose.yml b/agents/bug-fix/compose.yml new file mode 100644 index 0000000000..394bd2cdab --- /dev/null +++ b/agents/bug-fix/compose.yml @@ -0,0 +1,32 @@ +services: + bug-fix-broker: + build: + context: ../.. + dockerfile: agents/bug-fix/Dockerfile + target: broker + environment: + BUGZILLA_API_URL: ${BUGZILLA_API_URL} + BUGZILLA_API_KEY: ${BUGZILLA_API_KEY} + DRY_RUN: ${BROKER_DRY_RUN:-true} + expose: + - "8765" + + bug-fix-agent: + build: + context: ../.. + dockerfile: agents/bug-fix/Dockerfile + target: agent + # Only required env vars are listed. Optional ones (MODEL, MAX_TURNS, + # EFFORT, RESULTS_POLICY_URL, RESULTS_POLICY_FIELDS, RESULTS_PREFIX) + # are intentionally omitted — pydantic-settings uses defaults / treats + # absence as "unspecified" rather than parsing "" as int. To override + # at runtime, use `docker compose run -e MODEL=…`. + environment: + RUN_ID: ${RUN_ID:-local-dev} + BUG_ID: ${BUG_ID:?error} + BUGZILLA_MCP_URL: http://bug-fix-broker:8765/mcp + SOURCE_REPO: /workspace/firefox + ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:?error} + depends_on: + bug-fix-broker: + condition: service_started diff --git a/agents/bug-fix/pyproject.toml b/agents/bug-fix/pyproject.toml new file mode 100644 index 0000000000..0dfc185a85 --- /dev/null +++ b/agents/bug-fix/pyproject.toml @@ -0,0 +1,20 @@ +[project] +name = "hackbot-agent-bug-fix" +version = "0.1.0" +description = "Cloud Run Job image that runs the bug-fix agent for hackbot-api" +requires-python = ">=3.12" +dependencies = [ + "bugbug", + "hackbot-runtime", + "bugsy", + "grizzly-framework", + "prefpicker", + "claude-agent-sdk>=0.1.30", + "mcp>=1.0.0", + "starlette>=0.36.0", + "uvicorn>=0.27.0", +] + +[tool.uv.sources] +bugbug = { workspace = true } +hackbot-runtime = { workspace = true } diff --git a/bugbug/tools/bug_fix/agent.py b/bugbug/tools/bug_fix/agent.py index 52509c84e5..9e2aecd15f 100644 --- a/bugbug/tools/bug_fix/agent.py +++ b/bugbug/tools/bug_fix/agent.py @@ -1,23 +1,24 @@ """Bug fix triage tool -- a Bugzilla triage agent. Orchestrates a Claude agent that triages bugs according to rulesets -in the rules/ directory, with access to a source repository and an -in-process Bugzilla MCP server. +in the rules/ directory. The agent has access to a source repository +and reaches Bugzilla via an out-of-process MCP broker (HTTP transport) +that holds the Bugzilla token — the agent process itself never sees it. """ from __future__ import annotations import json import sys -from dataclasses import dataclass, field +from dataclasses import dataclass from pathlib import Path -import bugsy from claude_agent_sdk import ( AgentDefinition, AssistantMessage, ClaudeAgentOptions, ClaudeSDKClient, + McpServerConfig, ResultMessage, SystemMessage, TextBlock, @@ -28,8 +29,6 @@ ) from bugbug.tools.base import GenerativeModelTool -from bugbug.tools.bug_fix.bugzilla_mcp import BugzillaContext -from bugbug.tools.bug_fix.bugzilla_mcp import build_server as build_bugzilla_server from bugbug.tools.bug_fix.config import ( BUGZILLA_READ_TOOLS, BUGZILLA_WRITE_TOOLS, @@ -51,54 +50,6 @@ class BugFixResult: exit_code: int = 0 bugs_processed: int = 0 - simulated_writes: list[dict] = field(default_factory=list) - - -# --------------------------------------------------------------------------- # -# Bug selection -# --------------------------------------------------------------------------- # - - -def fetch_initial_bugs( - bz: bugsy.Bugsy, - bug_ids: list[int] | None, - keywords: list[str], - blocks: int | None, - status: list[str], -) -> tuple[list[int], list[int]]: - """Resolve selectors into a concrete list of bug IDs. - - All selectors intersect: ``bugs=[1,2,3]`` + ``keywords=["sec-low"]`` - returns only those of 1,2,3 that carry sec-low. - - Returns (selected_ids, inaccessible_ids). - """ - params: dict = {"include_fields": "id"} - if bug_ids: - params["id"] = ",".join(str(i) for i in bug_ids) - for kw in keywords: - params.setdefault("keywords", []).append(kw) - if blocks is not None: - params["blocks"] = blocks - if status: - params["status"] = status - - if not bug_ids: - params.setdefault("limit", 200) - - for k, v in list(params.items()): - if isinstance(v, list) and len(v) == 1: - params[k] = v[0] - - result = bz.request("bug", params=params) - returned = [b["id"] for b in result.get("bugs", [])] - returned_set = set(returned) - - inaccessible: list[int] = [] - if bug_ids: - inaccessible = [i for i in bug_ids if i not in returned_set] - - return returned, inaccessible # --------------------------------------------------------------------------- # @@ -261,17 +212,12 @@ def create(cls, **kwargs): async def run( self, *, - base_url: str, - api_key: str, + bugzilla_mcp_server: McpServerConfig, source_repo: Path, - bugs: list[int] | None = None, - keywords: list[str] | None = None, - blocks: int | None = None, - status: list[str] | None = None, + bugs: list[int], instructions: str = "", task: str | None = None, rules_dir: Path | None = None, - dry_run: bool = False, newest_first: bool = False, model: str | None = None, max_turns: int | None = None, @@ -281,49 +227,24 @@ async def run( ) -> BugFixResult: if rules_dir is None: rules_dir = HERE / "rules" - keywords = keywords or [] - status = status or [] - - # --- Bugzilla client & MCP server --------------------------------- # - bz = bugsy.Bugsy(api_key=api_key, bugzilla_url=base_url) - bz_ctx = BugzillaContext(client=bz, dry_run=dry_run) - bugzilla_server = build_bugzilla_server(bz_ctx) - - # --- Firefox build/eval MCP server -------------------------------- # - fx_ctx = FirefoxContext.from_source_repo(source_repo) - firefox_server = build_firefox_server(fx_ctx) - # --- Resolve bug selectors to concrete IDs ------------------------ # - print("[bug_fix] resolving bug set...", file=sys.stderr) - try: - selected, inaccessible = fetch_initial_bugs( - bz, bugs, keywords, blocks, status - ) - except bugsy.BugsyException as e: - print(f"[bug_fix] bug selection failed: {e}", file=sys.stderr) - return BugFixResult(exit_code=2) - - if inaccessible: - print( - f"[bug_fix] {len(inaccessible)} bug(s) inaccessible, skipping: {inaccessible}", - file=sys.stderr, - ) - if not selected: - print( - "[bug_fix] no accessible bugs match the selectors — nothing to do", - file=sys.stderr, - ) + if not bugs: + print("[bug_fix] no bug ids supplied — nothing to do", file=sys.stderr) return BugFixResult(exit_code=0) - selected.sort(reverse=newest_first) + selected = sorted(bugs, reverse=newest_first) print(f"[bug_fix] triaging {len(selected)} bug(s): {selected}", file=sys.stderr) + # --- Firefox build/eval MCP server (in-process; no tokens) -------- # + fx_ctx = FirefoxContext.from_source_repo(source_repo) + firefox_server = build_firefox_server(fx_ctx) + # --- Build agent options ------------------------------------------ # system_prompt = load_system_prompt(rules_dir, instructions) options = ClaudeAgentOptions( system_prompt=system_prompt, - mcp_servers={"bugzilla": bugzilla_server, "firefox": firefox_server}, + mcp_servers={"bugzilla": bugzilla_mcp_server, "firefox": firefox_server}, agents={"investigator": make_investigator()}, cwd=str(source_repo.resolve()), add_dirs=[str(rules_dir.resolve())], @@ -339,7 +260,6 @@ async def run( *BUGZILLA_WRITE_TOOLS, *FIREFOX_TOOLS, ], - disallowed_tools=list(BUGZILLA_WRITE_TOOLS) if dry_run else [], model=model, max_turns=max_turns, **({"effort": effort} if effort else {}), @@ -375,20 +295,7 @@ async def run( if isinstance(msg, ResultMessage) and msg.is_error: exit_code = 1 - # --- Build result ------------------------------------------------- # - result = BugFixResult( + return BugFixResult( exit_code=exit_code, bugs_processed=len(selected), - simulated_writes=list(bz_ctx.simulated) if dry_run else [], ) - - if dry_run and bz_ctx.simulated: - print(f"\n{'=' * 60}", file=sys.stderr) - print( - f"[bug_fix] DRY-RUN SUMMARY: {len(bz_ctx.simulated)} simulated write(s)", - file=sys.stderr, - ) - for i, s in enumerate(bz_ctx.simulated, 1): - print(f" {i}. {s['action']} bug {s['bug_id']}", file=sys.stderr) - - return result diff --git a/bugbug/tools/bug_fix/bugzilla_mcp.py b/bugbug/tools/bug_fix/bugzilla_mcp.py index 35a4785ac3..51f86d92c1 100644 --- a/bugbug/tools/bug_fix/bugzilla_mcp.py +++ b/bugbug/tools/bug_fix/bugzilla_mcp.py @@ -13,7 +13,7 @@ import mimetypes import os import sys -from dataclasses import dataclass, field +from dataclasses import dataclass import bugsy from claude_agent_sdk import create_sdk_mcp_server, tool @@ -35,8 +35,6 @@ class BugzillaContext: client: bugsy.Bugsy dry_run: bool = False confirm: bool = False - # Record of simulated writes during dry-run, for end-of-run summary. - simulated: list[dict] = field(default_factory=list) def _text(content: str) -> dict: @@ -381,7 +379,6 @@ async def update_bug(args): if ctx.dry_run: print(f"\n[DRY-RUN] update_bug {bug_id}", file=sys.stderr) print(json.dumps(action_desc, indent=2, default=str), file=sys.stderr) - ctx.simulated.append({"action": "update_bug", **action_desc}) return _jtext( { "dry_run": True, @@ -451,7 +448,6 @@ async def add_comment(args): if ctx.dry_run: print(f"\n[DRY-RUN] add_comment on bug {bug_id}", file=sys.stderr) print(json.dumps(action_desc, indent=2, default=str), file=sys.stderr) - ctx.simulated.append({"action": "add_comment", **action_desc}) return _jtext( { "dry_run": True, @@ -573,7 +569,6 @@ async def add_attachment(args): if ctx.dry_run: print(f"\n[DRY-RUN] add_attachment on bug {bug_id}", file=sys.stderr) print(json.dumps(action_desc, indent=2, default=str), file=sys.stderr) - ctx.simulated.append({"action": "add_attachment", **action_desc}) return _jtext( { "dry_run": True, @@ -704,7 +699,6 @@ async def create_bug(args): if ctx.dry_run: print("\n[DRY-RUN] create_bug", file=sys.stderr) print(json.dumps(action_desc, indent=2, default=str), file=sys.stderr) - ctx.simulated.append({"action": "create_bug", **action_desc}) return _jtext( { "dry_run": True, diff --git a/docker-compose.yml b/docker-compose.yml index fc48b7e896..2d87fe2950 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,4 +1,6 @@ -version: "3.2" +include: + - path: agents/bug-fix/compose.yml + services: bugbug-base: build: diff --git a/libs/hackbot-runtime/hackbot_runtime/__init__.py b/libs/hackbot-runtime/hackbot_runtime/__init__.py new file mode 100644 index 0000000000..7f97491b3e --- /dev/null +++ b/libs/hackbot-runtime/hackbot_runtime/__init__.py @@ -0,0 +1,12 @@ +from hackbot_runtime.context import Context +from hackbot_runtime.result import AgentResult +from hackbot_runtime.runtime import run, run_async +from hackbot_runtime.uploader import SignedPolicyUploader + +__all__ = [ + "AgentResult", + "Context", + "SignedPolicyUploader", + "run", + "run_async", +] diff --git a/libs/hackbot-runtime/hackbot_runtime/context.py b/libs/hackbot-runtime/hackbot_runtime/context.py new file mode 100644 index 0000000000..0948a7b318 --- /dev/null +++ b/libs/hackbot-runtime/hackbot_runtime/context.py @@ -0,0 +1,32 @@ +from functools import cached_property + +from pydantic_settings import BaseSettings, SettingsConfigDict + +from hackbot_runtime.uploader import SignedPolicyUploader + + +class Context(BaseSettings): + """Platform context handed to every agent's main() by the runtime. + + `run_id` is required. The results-upload fields are optional so + local-dev runs (compose, scripts) can start the agent without a + signed POST policy — in that case the runtime skips the summary + upload and logs a warning rather than failing. + """ + + run_id: str + results_prefix: str = "" + results_policy_url: str | None = None + results_policy_fields: dict[str, str] = {} + + model_config = SettingsConfigDict(extra="ignore") + + @cached_property + def uploader(self) -> SignedPolicyUploader | None: + if not self.results_policy_url: + return None + return SignedPolicyUploader( + url=self.results_policy_url, + fields=self.results_policy_fields, + prefix=self.results_prefix, + ) diff --git a/libs/hackbot-runtime/hackbot_runtime/result.py b/libs/hackbot-runtime/hackbot_runtime/result.py new file mode 100644 index 0000000000..3bf88041ec --- /dev/null +++ b/libs/hackbot-runtime/hackbot_runtime/result.py @@ -0,0 +1,17 @@ +from dataclasses import dataclass, field +from typing import Any, Literal + + +@dataclass +class AgentResult: + """Outcome reported by an agent's main() to the runtime. + + The runtime serialises this into the summary.json artifact the orchestrator + reads. `status` drives the run's terminal state in hackbot-api; `findings` + is opaque to the platform and surfaced verbatim. + """ + + status: Literal["ok", "error"] = "ok" + error: str | None = None + findings: dict[str, Any] = field(default_factory=dict) + exit_code: int = 0 diff --git a/libs/hackbot-runtime/hackbot_runtime/runtime.py b/libs/hackbot-runtime/hackbot_runtime/runtime.py new file mode 100644 index 0000000000..f30cd928e3 --- /dev/null +++ b/libs/hackbot-runtime/hackbot_runtime/runtime.py @@ -0,0 +1,122 @@ +import asyncio +import logging +import sys +import traceback +from collections.abc import Awaitable, Callable + +from pydantic import ValidationError + +from hackbot_runtime.context import Context +from hackbot_runtime.result import AgentResult + +log = logging.getLogger("hackbot_runtime") + +AgentMain = Callable[[Context], AgentResult] +AsyncAgentMain = Callable[[Context], Awaitable[AgentResult]] + +_SUMMARY_NAME = "summary.json" + + +def _configure_logging() -> None: + if not logging.getLogger().handlers: + logging.basicConfig( + level=logging.INFO, + stream=sys.stderr, + format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", + ) + + +def _summary_payload_from_result(result: AgentResult) -> dict: + return { + "status": result.status, + "error": result.error, + "findings": result.findings, + } + + +def _summary_payload_from_exception(exc: BaseException) -> dict: + return { + "status": "error", + "error": f"{type(exc).__name__}: {exc}", + "findings": {"traceback": traceback.format_exc()}, + } + + +def _load_context() -> Context | None: + try: + return Context() + except ValidationError as exc: + log.error( + "Failed to load Context from env; no summary can be written.\n%s", + exc, + ) + return None + + +def _finish(ctx: Context, result_or_exc: AgentResult | BaseException) -> int: + if isinstance(result_or_exc, AgentResult): + payload = _summary_payload_from_result(result_or_exc) + exit_code = result_or_exc.exit_code + else: + payload = _summary_payload_from_exception(result_or_exc) + exit_code = 1 + + if ctx.uploader is None: + log.warning( + "RESULTS_POLICY_URL not configured; skipping summary.json upload. " + "Summary that would have been uploaded: %s", + payload, + ) + return exit_code + + try: + ctx.uploader.upload_json(_SUMMARY_NAME, payload) + except Exception: + log.exception("Failed to upload summary.json") + return 1 if exit_code == 0 else exit_code + + return exit_code + + +def _validate_result(result: object) -> AgentResult | RuntimeError: + if isinstance(result, AgentResult): + return result + return RuntimeError( + f"Agent returned {type(result).__name__}; expected AgentResult" + ) + + +def run(entrypoint: AgentMain) -> int: + _configure_logging() + ctx = _load_context() + if ctx is None: + return 2 + + try: + result = entrypoint(ctx) + except Exception as exc: + log.exception("Agent raised an exception") + return _finish(ctx, exc) + + validated = _validate_result(result) + if isinstance(validated, RuntimeError): + log.error(str(validated)) + return _finish(ctx, validated) + + +def run_async(entrypoint: AsyncAgentMain) -> int: + _configure_logging() + ctx = _load_context() + if ctx is None: + return 2 + + try: + result = asyncio.run(entrypoint(ctx)) + except Exception as exc: + log.exception("Agent raised an exception") + return _finish(ctx, exc) + + validated = _validate_result(result) + if isinstance(validated, RuntimeError): + log.error(str(validated)) + return _finish(ctx, validated) diff --git a/libs/hackbot-runtime/hackbot_runtime/uploader.py b/libs/hackbot-runtime/hackbot_runtime/uploader.py new file mode 100644 index 0000000000..51e4c62e51 --- /dev/null +++ b/libs/hackbot-runtime/hackbot_runtime/uploader.py @@ -0,0 +1,36 @@ +import json +from pathlib import Path + +import requests + + +class SignedPolicyUploader: + """POST artifacts to a GCS V4 signed POST policy. + + The orchestrator passes the policy via env vars consumed by `Context`. + The Job has no GCP identity; this signed policy is its only write + capability. + """ + + def __init__(self, url: str, fields: dict[str, str], prefix: str) -> None: + self.url = url + self.fields = fields + self.prefix = prefix + + def upload_bytes( + self, name: str, data: bytes, content_type: str = "application/octet-stream" + ) -> None: + key = f"{self.prefix}{name}" + form: dict[str, str] = dict(self.fields) + form["key"] = key + files = {"file": (name, data, content_type)} + response = requests.post(self.url, data=form, files=files, timeout=300) + response.raise_for_status() + + def upload_file(self, name: str, path: Path, content_type: str | None = None) -> None: + data = path.read_bytes() + self.upload_bytes(name, data, content_type or "application/octet-stream") + + def upload_json(self, name: str, payload: dict) -> None: + body = json.dumps(payload, default=str).encode("utf-8") + self.upload_bytes(name, body, "application/json") diff --git a/libs/hackbot-runtime/pyproject.toml b/libs/hackbot-runtime/pyproject.toml new file mode 100644 index 0000000000..41e7f8077c --- /dev/null +++ b/libs/hackbot-runtime/pyproject.toml @@ -0,0 +1,9 @@ +[project] +name = "hackbot-runtime" +version = "0.1.0" +description = "Agent-side runtime for hackbot-api: signed-policy upload, summary contract, exception handling" +requires-python = ">=3.12" +dependencies = [ + "requests>=2.32.0", + "pydantic-settings>=2.1.0", +] diff --git a/pyproject.toml b/pyproject.toml index 7bc9df97b2..7302856463 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -132,7 +132,7 @@ artifacts = [ ] [tool.uv.workspace] -members = ["http_service"] +members = ["http_service", "services/hackbot-api", "agents/bug-fix", "libs/hackbot-runtime"] [tool.ruff] extend-exclude = ["data"] diff --git a/scripts/run_bug_fix.py b/scripts/run_bug_fix.py index 47bf18516b..bf47df644b 100644 --- a/scripts/run_bug_fix.py +++ b/scripts/run_bug_fix.py @@ -1,16 +1,23 @@ -"""Run the bug_fix tool locally.""" +"""Run the bug_fix tool locally. + +Uses an in-process Bugzilla MCP server for simplicity — no separate +broker needed. The orchestrated production path is different: the agent +runs with no Bugzilla credentials and talks to a broker sidecar. +""" import asyncio from pathlib import Path +import bugsy from pydantic_settings import BaseSettings, SettingsConfigDict from bugbug.tools.bug_fix.agent import BugFixTool +from bugbug.tools.bug_fix.bugzilla_mcp import BugzillaContext, build_server class Settings(BaseSettings): bug_id: int - bugzilla_api_url: str | None = "https://bugzilla.mozilla.org/rest" + bugzilla_api_url: str = "https://bugzilla.mozilla.org/rest" bugzilla_api_key: str source_repo: Path model: str | None = None @@ -26,21 +33,28 @@ class Settings(BaseSettings): async def main(): settings = Settings() + + bugzilla_mcp_server = build_server( + BugzillaContext( + client=bugsy.Bugsy( + api_key=settings.bugzilla_api_key, + bugzilla_url=settings.bugzilla_api_url, + ), + dry_run=True, + ) + ) + tool = BugFixTool.create() result = await tool.run( - base_url=settings.bugzilla_api_url, - api_key=settings.bugzilla_api_key, + bugzilla_mcp_server=bugzilla_mcp_server, source_repo=settings.source_repo, model=settings.model, max_turns=settings.max_turns, effort=settings.effort, bugs=[settings.bug_id], - dry_run=True, verbose=True, ) print(f"\nexit_code={result.exit_code} bugs_processed={result.bugs_processed}") - if result.simulated_writes: - print(f"simulated writes: {len(result.simulated_writes)}") if __name__ == "__main__": diff --git a/services/hackbot-api/Dockerfile b/services/hackbot-api/Dockerfile new file mode 100644 index 0000000000..8eb3af78b8 --- /dev/null +++ b/services/hackbot-api/Dockerfile @@ -0,0 +1,21 @@ +FROM python:3.12-slim AS builder + +COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ + +WORKDIR /app +COPY pyproject.toml uv.lock* ./ +RUN uv sync --locked --no-dev || uv sync --no-dev + +FROM python:3.12-slim + +WORKDIR /app +COPY --from=builder /app/.venv /app/.venv +COPY app/ ./app/ + +ENV PYTHONUNBUFFERED=1 +ENV PORT=8080 +ENV PATH="/app/.venv/bin:$PATH" + +EXPOSE 8080 + +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8080"] diff --git a/services/hackbot-api/alembic.ini b/services/hackbot-api/alembic.ini new file mode 100644 index 0000000000..bb9020ae94 --- /dev/null +++ b/services/hackbot-api/alembic.ini @@ -0,0 +1,41 @@ +[alembic] +script_location = %(here)s/alembic + +prepend_sys_path = . + +path_separator = os + + +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARNING +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARNING +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/services/hackbot-api/alembic/env.py b/services/hackbot-api/alembic/env.py new file mode 100644 index 0000000000..27ea550a0b --- /dev/null +++ b/services/hackbot-api/alembic/env.py @@ -0,0 +1,56 @@ +import asyncio +from logging.config import fileConfig + +from alembic import context +from sqlalchemy.engine import Connection + +from app.database.connection import close_db, init_db +from app.database.models import Base + +config = context.config + +if config.config_file_name is not None: + fileConfig(config.config_file_name) + +target_metadata = Base.metadata + + +def run_migrations_offline() -> None: + url = config.get_main_option("sqlalchemy.url") + context.configure( + url=url, + target_metadata=target_metadata, + literal_binds=True, + dialect_opts={"paramstyle": "named"}, + ) + + with context.begin_transaction(): + context.run_migrations() + + +def do_run_migrations(connection: Connection) -> None: + context.configure(connection=connection, target_metadata=target_metadata) + + with context.begin_transaction(): + context.run_migrations() + + +async def run_async_migrations() -> None: + """Run migrations using Cloud SQL Connector.""" + engine = await init_db() + + try: + async with engine.connect() as connection: + await connection.run_sync(do_run_migrations) + finally: + await close_db() + + +def run_migrations_online() -> None: + asyncio.run(run_async_migrations()) + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/services/hackbot-api/alembic/script.py.mako b/services/hackbot-api/alembic/script.py.mako new file mode 100644 index 0000000000..11016301e7 --- /dev/null +++ b/services/hackbot-api/alembic/script.py.mako @@ -0,0 +1,28 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision: str = ${repr(up_revision)} +down_revision: Union[str, Sequence[str], None] = ${repr(down_revision)} +branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)} +depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)} + + +def upgrade() -> None: + """Upgrade schema.""" + ${upgrades if upgrades else "pass"} + + +def downgrade() -> None: + """Downgrade schema.""" + ${downgrades if downgrades else "pass"} diff --git a/services/hackbot-api/alembic/versions/0001_initial.py b/services/hackbot-api/alembic/versions/0001_initial.py new file mode 100644 index 0000000000..45322b8f9f --- /dev/null +++ b/services/hackbot-api/alembic/versions/0001_initial.py @@ -0,0 +1,59 @@ +"""initial schema: runs table + +Revision ID: 0001_initial +Revises: +Create Date: 2026-05-16 + +""" +from typing import Sequence, Union + +import sqlalchemy as sa +from alembic import op +from sqlalchemy.dialects.postgresql import JSONB, UUID + +revision: str = "0001_initial" +down_revision: Union[str, Sequence[str], None] = None +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.create_table( + "runs", + sa.Column("run_id", UUID(as_uuid=True), primary_key=True, nullable=False), + sa.Column("agent", sa.String(), nullable=False), + sa.Column("status", sa.String(), nullable=False), + sa.Column("inputs", JSONB(), nullable=False), + sa.Column( + "created_at", + sa.DateTime(timezone=True), + server_default=sa.func.now(), + nullable=False, + ), + sa.Column( + "updated_at", + sa.DateTime(timezone=True), + server_default=sa.func.now(), + nullable=False, + ), + sa.Column("execution_name", sa.String(), nullable=True), + sa.Column("results_prefix", sa.String(), nullable=False), + sa.Column("summary", JSONB(), nullable=True), + sa.Column( + "artifacts", + JSONB(), + nullable=False, + server_default=sa.text("'[]'::jsonb"), + ), + sa.Column("error", sa.Text(), nullable=True), + ) + op.create_index("ix_runs_agent", "runs", ["agent"]) + op.create_index("ix_runs_status", "runs", ["status"]) + op.create_index("ix_runs_created_at", "runs", ["created_at"]) + + +def downgrade() -> None: + op.drop_index("ix_runs_created_at", table_name="runs") + op.drop_index("ix_runs_status", table_name="runs") + op.drop_index("ix_runs_agent", table_name="runs") + op.drop_table("runs") diff --git a/services/hackbot-api/app/__init__.py b/services/hackbot-api/app/__init__.py new file mode 100644 index 0000000000..3dc1f76bc6 --- /dev/null +++ b/services/hackbot-api/app/__init__.py @@ -0,0 +1 @@ +__version__ = "0.1.0" diff --git a/services/hackbot-api/app/agents.py b/services/hackbot-api/app/agents.py new file mode 100644 index 0000000000..ad60d0f882 --- /dev/null +++ b/services/hackbot-api/app/agents.py @@ -0,0 +1,46 @@ +from collections.abc import Callable +from dataclasses import dataclass + +from pydantic import BaseModel + +from app.schemas import BugFixInputs + + +@dataclass(frozen=True) +class AgentSpec: + name: str + description: str + job_name: str + input_schema: type[BaseModel] + build_env: Callable[[BaseModel], dict[str, str]] + + +def _bug_fix_env(inputs: BaseModel) -> dict[str, str]: + assert isinstance(inputs, BugFixInputs) + # The bug-fix agent's Job is multi-container: an `agent` container + # (no tokens) and a `broker` sidecar (holds BZ_API_KEY at deploy time + # via Secret Manager). The orchestrator only overrides the `agent` + # container's env per execution — the broker is fully configured at + # deploy time. The agent reaches the broker on the task's loopback. + env: dict[str, str] = { + "BUG_ID": str(inputs.bug_id), + "BUGZILLA_MCP_URL": "http://127.0.0.1:8765/mcp", + } + if inputs.model is not None: + env["MODEL"] = inputs.model + if inputs.max_turns is not None: + env["MAX_TURNS"] = str(inputs.max_turns) + if inputs.effort is not None: + env["EFFORT"] = inputs.effort + return env + + +AGENT_REGISTRY: dict[str, AgentSpec] = { + "bug-fix": AgentSpec( + name="bug-fix", + description="Investigate a Bugzilla bug and produce a candidate fix patch against the Firefox source tree.", + job_name="hackbot-agent-bug-fix", + input_schema=BugFixInputs, + build_env=_bug_fix_env, + ), +} diff --git a/services/hackbot-api/app/auth.py b/services/hackbot-api/app/auth.py new file mode 100644 index 0000000000..3d782558e2 --- /dev/null +++ b/services/hackbot-api/app/auth.py @@ -0,0 +1,18 @@ +import hmac + +from fastapi import Header, HTTPException, status + +from app.config import settings + + +async def require_api_key(x_api_key: str | None = Header(default=None)) -> None: + if not settings.api_key: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="API key not configured", + ) + if x_api_key is None or not hmac.compare_digest(x_api_key, settings.api_key): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Invalid or missing X-API-Key", + ) diff --git a/services/hackbot-api/app/config.py b/services/hackbot-api/app/config.py new file mode 100644 index 0000000000..89a572dd47 --- /dev/null +++ b/services/hackbot-api/app/config.py @@ -0,0 +1,36 @@ +from pydantic_settings import BaseSettings + + +class Settings(BaseSettings): + # GCP + gcp_project: str = "" + gcp_region: str = "us-central1" + results_bucket: str = "" + + # Cloud SQL Postgres + cloud_sql_instance: str = "" + db_user: str = "" + db_pass: str = "" + db_name: str = "hackbot" + + # Cloud Run Jobs + job_execution_timeout_seconds: int = 8 * 60 * 60 + signed_policy_max_bytes: int = 5 * 1024 * 1024 * 1024 + signed_policy_grace_seconds: int = 60 * 60 + + # API auth + api_key: str = "" + + # Server + port: int = 8080 + environment: str = "development" + sentry_dsn: str | None = None + + model_config = { + "env_file": ".env", + "env_file_encoding": "utf-8", + "extra": "ignore", + } + + +settings = Settings() diff --git a/services/hackbot-api/app/database/__init__.py b/services/hackbot-api/app/database/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/services/hackbot-api/app/database/connection.py b/services/hackbot-api/app/database/connection.py new file mode 100644 index 0000000000..b1cf3f80e6 --- /dev/null +++ b/services/hackbot-api/app/database/connection.py @@ -0,0 +1,69 @@ +import asyncio +import logging +from collections.abc import AsyncGenerator + +from google.cloud.sql.connector import Connector, IPTypes +from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine + +from app.config import settings + +logger = logging.getLogger(__name__) + +connector: Connector | None = None +engine = None +async_session_maker: async_sessionmaker[AsyncSession] | None = None + + +async def init_db(): + """Initialize database connection. Call this on app startup.""" + global connector, engine, async_session_maker + + loop = asyncio.get_running_loop() + connector = Connector(loop=loop) + + async def get_connection(): + return await connector.connect_async( + settings.cloud_sql_instance, + "asyncpg", + user=settings.db_user, + password=settings.db_pass, + db=settings.db_name, + ip_type=IPTypes.PUBLIC, + ) + + engine = create_async_engine( + "postgresql+asyncpg://", + async_creator=get_connection, + pool_pre_ping=True, + pool_size=5, + max_overflow=5, + pool_timeout=30, + pool_recycle=1800, + ) + + async_session_maker = async_sessionmaker( + engine, + class_=AsyncSession, + expire_on_commit=False, + ) + + return engine + + +async def close_db(): + """Close database connection. Call this on app shutdown.""" + global connector, engine + if engine: + await engine.dispose() + if connector: + await connector.close_async() + + +async def get_db() -> AsyncGenerator[AsyncSession, None]: + assert async_session_maker is not None, "init_db() must run first" + async with async_session_maker() as session: + try: + yield session + except Exception: + await session.rollback() + raise diff --git a/services/hackbot-api/app/database/models.py b/services/hackbot-api/app/database/models.py new file mode 100644 index 0000000000..0f566fb035 --- /dev/null +++ b/services/hackbot-api/app/database/models.py @@ -0,0 +1,37 @@ +from datetime import datetime +from uuid import UUID + +from sqlalchemy import DateTime, String, Text +from sqlalchemy.dialects.postgresql import JSONB +from sqlalchemy.dialects.postgresql import UUID as PG_UUID +from sqlalchemy.ext.asyncio import AsyncAttrs +from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column +from sqlalchemy.sql import func + + +class Base(AsyncAttrs, DeclarativeBase): + pass + + +class Run(Base): + __tablename__ = "runs" + + run_id: Mapped[UUID] = mapped_column(PG_UUID(as_uuid=True), primary_key=True) + agent: Mapped[str] = mapped_column(String, nullable=False, index=True) + status: Mapped[str] = mapped_column(String, nullable=False, index=True) + inputs: Mapped[dict] = mapped_column(JSONB, nullable=False) + created_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), + server_default=func.now(), + index=True, + ) + updated_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), + server_default=func.now(), + onupdate=func.now(), + ) + execution_name: Mapped[str | None] = mapped_column(String, nullable=True) + results_prefix: Mapped[str] = mapped_column(String, nullable=False) + summary: Mapped[dict | None] = mapped_column(JSONB, nullable=True) + artifacts: Mapped[list] = mapped_column(JSONB, nullable=False, default=list) + error: Mapped[str | None] = mapped_column(Text, nullable=True) diff --git a/services/hackbot-api/app/gcs.py b/services/hackbot-api/app/gcs.py new file mode 100644 index 0000000000..b73479ea33 --- /dev/null +++ b/services/hackbot-api/app/gcs.py @@ -0,0 +1,92 @@ +import asyncio +import json +import logging +from datetime import timedelta +from functools import lru_cache +from typing import Any + +from google.cloud import storage + +from app.config import settings +from app.schemas import ArtifactRef, RunSummary + +log = logging.getLogger(__name__) + + +def run_prefix(run_id: str) -> str: + return f"runs/{run_id}/" + + +def summary_blob_name(run_id: str) -> str: + return f"{run_prefix(run_id)}summary.json" + + +@lru_cache(maxsize=1) +def _client() -> storage.Client: + return storage.Client(project=settings.gcp_project or None) + + +def _generate_post_policy_sync(run_id: str) -> dict[str, Any]: + bucket_name = settings.results_bucket + if not bucket_name: + raise RuntimeError("results_bucket not configured") + + prefix = run_prefix(run_id) + expiration_seconds = ( + settings.job_execution_timeout_seconds + settings.signed_policy_grace_seconds + ) + + policy = _client().generate_signed_post_policy_v4( + bucket_name=bucket_name, + blob_name=f"{prefix}_placeholder", + expiration=timedelta(seconds=expiration_seconds), + conditions=[ + ["starts-with", "$key", prefix], + ["content-length-range", 0, settings.signed_policy_max_bytes], + ], + ) + return {"url": policy["url"], "fields": policy["fields"]} + + +async def generate_results_policy(run_id: str) -> dict[str, Any]: + return await asyncio.to_thread(_generate_post_policy_sync, run_id) + + +def _read_summary_sync(run_id: str) -> RunSummary | None: + bucket = _client().bucket(settings.results_bucket) + blob = bucket.blob(summary_blob_name(run_id)) + if not blob.exists(): + return None + raw = blob.download_as_bytes() + try: + data = json.loads(raw) + except json.JSONDecodeError: + log.warning("summary.json for run %s is not valid JSON", run_id) + return RunSummary(status="error", error="summary.json is not valid JSON") + return RunSummary.model_validate(data) + + +async def read_summary(run_id: str) -> RunSummary | None: + return await asyncio.to_thread(_read_summary_sync, run_id) + + +def _list_artifacts_sync(run_id: str) -> list[ArtifactRef]: + bucket = _client().bucket(settings.results_bucket) + prefix = run_prefix(run_id) + artifacts: list[ArtifactRef] = [] + for blob in _client().list_blobs(bucket, prefix=prefix): + name = blob.name.removeprefix(prefix) + if not name: + continue + artifacts.append( + ArtifactRef( + name=name, + size=blob.size or 0, + content_type=blob.content_type, + ) + ) + return artifacts + + +async def list_artifacts(run_id: str) -> list[ArtifactRef]: + return await asyncio.to_thread(_list_artifacts_sync, run_id) diff --git a/services/hackbot-api/app/jobs.py b/services/hackbot-api/app/jobs.py new file mode 100644 index 0000000000..abd270bfa2 --- /dev/null +++ b/services/hackbot-api/app/jobs.py @@ -0,0 +1,86 @@ +import asyncio +import logging +from enum import Enum +from functools import lru_cache + +from google.cloud import run_v2 + +from app.config import settings + +log = logging.getLogger(__name__) + + +class ExecutionStatus(str, Enum): + pending = "pending" + running = "running" + succeeded = "succeeded" + failed = "failed" + cancelled = "cancelled" + + +@lru_cache(maxsize=1) +def _jobs_client() -> run_v2.JobsClient: + return run_v2.JobsClient() + + +@lru_cache(maxsize=1) +def _executions_client() -> run_v2.ExecutionsClient: + return run_v2.ExecutionsClient() + + +def _job_resource_name(job_name: str) -> str: + if not settings.gcp_project or not settings.gcp_region: + raise RuntimeError("gcp_project and gcp_region must be configured") + return ( + f"projects/{settings.gcp_project}/locations/{settings.gcp_region}/jobs/{job_name}" + ) + + +_AGENT_CONTAINER_NAME = "agent" + + +def _trigger_sync(job_name: str, env_overrides: dict[str, str]) -> str: + # Each agent's Job manifest declares two containers: `agent` (no + # tokens) and `broker` (holds tokens, fully configured at deploy + # time). Per-execution env overrides target only the agent + # container by name so the broker's env (Secret Manager-backed) is + # untouched. + overrides = run_v2.RunJobRequest.Overrides( + container_overrides=[ + run_v2.RunJobRequest.Overrides.ContainerOverride( + name=_AGENT_CONTAINER_NAME, + env=[ + run_v2.EnvVar(name=k, value=v) for k, v in env_overrides.items() + ], + ) + ], + timeout={"seconds": settings.job_execution_timeout_seconds}, + task_count=1, + ) + request = run_v2.RunJobRequest( + name=_job_resource_name(job_name), + overrides=overrides, + ) + operation = _jobs_client().run_job(request=request) + return operation.metadata.name + + +async def trigger_execution(job_name: str, env_overrides: dict[str, str]) -> str: + return await asyncio.to_thread(_trigger_sync, job_name, env_overrides) + + +def _execution_status_sync(execution_name: str) -> ExecutionStatus: + execution = _executions_client().get_execution(name=execution_name) + if execution.completion_time: + if execution.succeeded_count and not execution.failed_count: + return ExecutionStatus.succeeded + if execution.cancelled_count: + return ExecutionStatus.cancelled + return ExecutionStatus.failed + if execution.running_count or execution.start_time: + return ExecutionStatus.running + return ExecutionStatus.pending + + +async def get_execution_status(execution_name: str) -> ExecutionStatus: + return await asyncio.to_thread(_execution_status_sync, execution_name) diff --git a/services/hackbot-api/app/main.py b/services/hackbot-api/app/main.py new file mode 100644 index 0000000000..8693303a50 --- /dev/null +++ b/services/hackbot-api/app/main.py @@ -0,0 +1,54 @@ +import logging +from contextlib import asynccontextmanager + +import sentry_sdk +from fastapi import FastAPI + +from app import __version__ +from app.config import settings +from app.database.connection import close_db, init_db +from app.routers import runs_router + +if settings.sentry_dsn: + sentry_sdk.init( + dsn=settings.sentry_dsn, + environment=settings.environment, + release=f"hackbot-api@{__version__}", + send_default_pii=True, + ) + +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", +) + + +@asynccontextmanager +async def lifespan(app: FastAPI): + await init_db() + try: + yield + finally: + await close_db() + + +app = FastAPI( + title="Hackbot API", + description="Agent orchestration platform that runs agents as Cloud Run Jobs", + version=__version__, + lifespan=lifespan, +) + +app.include_router(runs_router) + + +@app.get("/health") +async def health_check(): + """Health check endpoint for Cloud Run.""" + return {"message": "Service is healthy", "status": "ok"} + + +if __name__ == "__main__": + import uvicorn + + uvicorn.run(app, host="0.0.0.0", port=settings.port) diff --git a/services/hackbot-api/app/routers/__init__.py b/services/hackbot-api/app/routers/__init__.py new file mode 100644 index 0000000000..185d634d4e --- /dev/null +++ b/services/hackbot-api/app/routers/__init__.py @@ -0,0 +1,3 @@ +from app.routers.runs import router as runs_router + +__all__ = ["runs_router"] diff --git a/services/hackbot-api/app/routers/runs.py b/services/hackbot-api/app/routers/runs.py new file mode 100644 index 0000000000..6f7d0641ea --- /dev/null +++ b/services/hackbot-api/app/routers/runs.py @@ -0,0 +1,159 @@ +import json +import logging +import uuid + +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.ext.asyncio import AsyncSession + +from app import gcs, jobs +from app.agents import AGENT_REGISTRY, AgentSpec +from app.auth import require_api_key +from app.config import settings +from app.database.connection import get_db +from app.database.models import Run +from app.jobs import ExecutionStatus +from app.schemas import ( + TERMINAL_STATUSES, + AgentDescriptor, + RunDoc, + RunRef, + RunStatus, + RunSummary, +) + +log = logging.getLogger(__name__) + +router = APIRouter(dependencies=[Depends(require_api_key)]) + + +def _lookup_agent(name: str) -> AgentSpec: + agent = AGENT_REGISTRY.get(name) + if agent is None: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Unknown agent '{name}'", + ) + return agent + + +@router.get("/agents", response_model=list[AgentDescriptor]) +async def list_agents() -> list[AgentDescriptor]: + return [ + AgentDescriptor( + name=spec.name, + description=spec.description, + input_schema=spec.input_schema.model_json_schema(), + ) + for spec in AGENT_REGISTRY.values() + ] + + +@router.post("/agents/{agent_name}/runs", response_model=RunRef, status_code=201) +async def create_run( + agent_name: str, + payload: dict, + db: AsyncSession = Depends(get_db), +) -> RunRef: + agent = _lookup_agent(agent_name) + try: + inputs = agent.input_schema.model_validate(payload) + except ValueError as exc: + raise HTTPException(status_code=422, detail=str(exc)) from exc + + run_id = uuid.uuid4() + results_prefix = gcs.run_prefix(str(run_id)) + + policy = await gcs.generate_results_policy(str(run_id)) + + run = Run( + run_id=run_id, + agent=agent.name, + status=RunStatus.pending.value, + inputs=inputs.model_dump(mode="json"), + results_prefix=results_prefix, + artifacts=[], + ) + db.add(run) + await db.flush() + + env_overrides: dict[str, str] = { + "RUN_ID": str(run_id), + "RESULTS_BUCKET": settings.results_bucket, + "RESULTS_PREFIX": results_prefix, + "RESULTS_POLICY_URL": policy["url"], + "RESULTS_POLICY_FIELDS": json.dumps(policy["fields"]), + **agent.build_env(inputs), + } + + try: + execution_name = await jobs.trigger_execution(agent.job_name, env_overrides) + except Exception as exc: + log.exception("Failed to trigger Cloud Run Job for run %s", run_id) + run.status = RunStatus.failed.value + run.error = f"Failed to start execution: {exc}" + await db.commit() + raise HTTPException( + status_code=status.HTTP_502_BAD_GATEWAY, + detail="Failed to start agent execution", + ) from exc + + run.execution_name = execution_name + await db.commit() + + return RunRef.model_validate(run) + + +@router.get("/runs/{run_id}", response_model=RunDoc) +async def get_run(run_id: uuid.UUID, db: AsyncSession = Depends(get_db)) -> RunDoc: + run = await db.get(Run, run_id) + if run is None: + raise HTTPException(status_code=404, detail="Run not found") + + if run.status in {s.value for s in TERMINAL_STATUSES} or run.execution_name is None: + return RunDoc.model_validate(run) + + await _reconcile(db, run) + return RunDoc.model_validate(run) + + +async def _reconcile(db: AsyncSession, run: Run) -> None: + assert run.execution_name is not None + try: + exec_status = await jobs.get_execution_status(run.execution_name) + except Exception: + log.exception("Failed to fetch execution status for run %s", run.run_id) + return + + if exec_status in (ExecutionStatus.pending, ExecutionStatus.running): + if run.status == RunStatus.pending.value and exec_status == ExecutionStatus.running: + run.status = RunStatus.running.value + await db.commit() + return + + summary = await gcs.read_summary(str(run.run_id)) + artifacts = await gcs.list_artifacts(str(run.run_id)) + + new_status, error = _terminal_status(exec_status, summary) + + run.status = new_status.value + run.artifacts = [a.model_dump(mode="json") for a in artifacts] + if summary is not None: + run.summary = summary.model_dump(mode="json") + if error is not None: + run.error = error + + await db.commit() + + +def _terminal_status( + exec_status: ExecutionStatus, summary: RunSummary | None +) -> tuple[RunStatus, str | None]: + if exec_status == ExecutionStatus.cancelled: + return RunStatus.timed_out, "Execution was cancelled or timed out" + if summary is None: + return RunStatus.failed, "Execution finished without writing summary.json" + if summary.status != "ok": + return RunStatus.failed, summary.error + if exec_status != ExecutionStatus.succeeded: + return RunStatus.failed, "Execution exited non-zero despite summary status=ok" + return RunStatus.succeeded, None diff --git a/services/hackbot-api/app/schemas.py b/services/hackbot-api/app/schemas.py new file mode 100644 index 0000000000..36ad0f9b17 --- /dev/null +++ b/services/hackbot-api/app/schemas.py @@ -0,0 +1,69 @@ +from datetime import datetime +from enum import Enum +from typing import Any +from uuid import UUID + +from pydantic import BaseModel, ConfigDict, Field + + +class RunStatus(str, Enum): + pending = "pending" + running = "running" + succeeded = "succeeded" + failed = "failed" + timed_out = "timed_out" + + +TERMINAL_STATUSES = {RunStatus.succeeded, RunStatus.failed, RunStatus.timed_out} + + +class ArtifactRef(BaseModel): + name: str + size: int + content_type: str | None = None + + +class RunSummary(BaseModel): + status: str + error: str | None = None + findings: dict[str, Any] = Field(default_factory=dict) + + +class AgentDescriptor(BaseModel): + name: str + description: str + input_schema: dict[str, Any] + + +class RunRef(BaseModel): + model_config = ConfigDict(from_attributes=True) + + run_id: UUID + agent: str + status: RunStatus + + +class RunDoc(BaseModel): + model_config = ConfigDict(from_attributes=True) + + run_id: UUID + agent: str + status: RunStatus + inputs: dict[str, Any] + created_at: datetime + updated_at: datetime + execution_name: str | None = None + results_prefix: str + summary: RunSummary | None = None + artifacts: list[ArtifactRef] = Field(default_factory=list) + error: str | None = None + + +# --- Per-agent input schemas --- + + +class BugFixInputs(BaseModel): + bug_id: int + model: str | None = None + max_turns: int | None = None + effort: str | None = None diff --git a/services/hackbot-api/pyproject.toml b/services/hackbot-api/pyproject.toml new file mode 100644 index 0000000000..14a8a0b5d4 --- /dev/null +++ b/services/hackbot-api/pyproject.toml @@ -0,0 +1,25 @@ +[project] +name = "hackbot-api" +version = "0.1.0" +description = "Agent orchestration platform that runs agents as Cloud Run Jobs" +requires-python = ">=3.12" +dependencies = [ + "fastapi>=0.109.0", + "uvicorn[standard]>=0.27.0", + "pydantic>=2.6.0", + "pydantic-settings>=2.1.0", + "sqlalchemy[asyncio]>=2.0.25", + "asyncpg>=0.29.0", + "cloud-sql-python-connector[asyncpg]>=1.5.0", + "alembic>=1.13.1", + "google-cloud-storage>=2.16.0", + "google-cloud-run>=0.10.0", + "sentry-sdk>=2.51.0", +] + +[project.optional-dependencies] +dev = ["pytest>=8.0.0", "pytest-asyncio>=0.23.0", "httpx>=0.26.0"] + +[tool.pytest.ini_options] +asyncio_mode = "auto" +testpaths = ["tests"] diff --git a/uv.lock b/uv.lock index fd17b0da4b..f038257745 100644 --- a/uv.lock +++ b/uv.lock @@ -20,6 +20,9 @@ resolution-markers = [ members = [ "bugbug", "bugbug-http-service", + "hackbot-agent-bug-fix", + "hackbot-api", + "hackbot-runtime", ] [[package]] @@ -146,6 +149,28 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/62/29/2f8418269e46454a26171bfdd6a055d74febf32234e474930f2f60a17145/aiohttp-3.13.5-cp314-cp314t-win_amd64.whl", hash = "sha256:18a2f6c1182c51baa1d28d68fea51513cb2a76612f038853c0ad3c145423d3d9", size = 505441, upload-time = "2026-03-31T22:00:12.791Z" }, ] +[[package]] +name = "aioquic" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "cryptography" }, + { name = "pylsqpack" }, + { name = "pyopenssl" }, + { name = "service-identity" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4b/1a/bf10b2c57c06c7452b685368cb1ac90565a6e686e84ec6f84465fb8f78f4/aioquic-1.2.0.tar.gz", hash = "sha256:f91263bb3f71948c5c8915b4d50ee370004f20a416f67fab3dcc90556c7e7199", size = 179891, upload-time = "2024-07-06T23:27:09.301Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/19/03/1c385739e504c70ab2a66a4bc0e7cd95cee084b374dcd4dc97896378400b/aioquic-1.2.0-cp38-abi3-macosx_10_9_x86_64.whl", hash = "sha256:3e23964dfb04526ade6e66f5b7cd0c830421b8138303ab60ba6e204015e7cb0b", size = 1753473, upload-time = "2024-07-06T23:26:20.809Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1f/4d1c40714db65be828e1a1e2cce7f8f4b252be67d89f2942f86a1951826c/aioquic-1.2.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:84d733332927b76218a3b246216104116f766f5a9b2308ec306cd017b3049660", size = 2083563, upload-time = "2024-07-06T23:26:24.254Z" }, + { url = "https://files.pythonhosted.org/packages/15/48/56a8c9083d1deea4ccaf1cbf5a91a396b838b4a0f8650f4e9f45c7879a38/aioquic-1.2.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2466499759b31ea4f1d17f4aeb1f8d4297169e05e3c1216d618c9757f4dd740d", size = 2555697, upload-time = "2024-07-06T23:26:26.16Z" }, + { url = "https://files.pythonhosted.org/packages/0f/93/fa4c981a8a8a903648d4cd6e12c0fca7f44e3ef4ba15a8b99a26af05b868/aioquic-1.2.0-cp38-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cd75015462ca5070a888110dc201f35a9f4c7459f9201b77adc3c06013611bb8", size = 2149089, upload-time = "2024-07-06T23:26:28.277Z" }, + { url = "https://files.pythonhosted.org/packages/b0/0f/4a280923313b831892caaa45348abea89e7dd2e4422a86699bb0e506b1dd/aioquic-1.2.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:43ae3b11d43400a620ca0b4b4885d12b76a599c2cbddba755f74bebfa65fe587", size = 2205221, upload-time = "2024-07-06T23:26:30.682Z" }, + { url = "https://files.pythonhosted.org/packages/d2/6b/a6a1d1762ce06f13b68f524bb9c5f4d6ca7cda9b072d7e744626b89b77be/aioquic-1.2.0-cp38-abi3-win32.whl", hash = "sha256:910d8c91da86bba003d491d15deaeac3087d1b9d690b9edc1375905d8867b742", size = 1214037, upload-time = "2024-07-06T23:26:32.651Z" }, + { url = "https://files.pythonhosted.org/packages/dd/aa/e8a8a75c93dee0ab229df3c2d17f63cd44d0ad5ee8540e2ec42779ce3a39/aioquic-1.2.0-cp38-abi3-win_amd64.whl", hash = "sha256:e3dcfb941004333d477225a6689b55fc7f905af5ee6a556eb5083be0354e653a", size = 1530339, upload-time = "2024-07-06T23:26:34.753Z" }, +] + [[package]] name = "aiosignal" version = "1.4.0" @@ -159,6 +184,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" }, ] +[[package]] +name = "alembic" +version = "1.18.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mako" }, + { name = "sqlalchemy" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/94/13/8b084e0f2efb0275a1d534838844926f798bd766566b1375174e2448cd31/alembic-1.18.4.tar.gz", hash = "sha256:cb6e1fd84b6174ab8dbb2329f86d631ba9559dd78df550b57804d607672cedbc", size = 2056725, upload-time = "2026-02-10T16:00:47.195Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/29/6533c317b74f707ea28f8d633734dbda2119bbadfc61b2f3640ba835d0f7/alembic-1.18.4-py3-none-any.whl", hash = "sha256:a5ed4adcf6d8a4cb575f3d759f071b03cd6e5c7618eb796cb52497be25bfe19a", size = 263893, upload-time = "2026-02-10T16:00:49.997Z" }, +] + [[package]] name = "amqp" version = "5.3.1" @@ -299,6 +338,46 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fe/ba/e2081de779ca30d473f21f5b30e0e737c438205440784c7dfc81efc2b029/async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c", size = 6233, upload-time = "2024-11-06T16:41:37.9Z" }, ] +[[package]] +name = "asyncpg" +version = "0.31.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/cc/d18065ce2380d80b1bcce927c24a2642efd38918e33fd724bc4bca904877/asyncpg-0.31.0.tar.gz", hash = "sha256:c989386c83940bfbd787180f2b1519415e2d3d6277a70d9d0f0145ac73500735", size = 993667, upload-time = "2025-11-24T23:27:00.812Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/a6/59d0a146e61d20e18db7396583242e32e0f120693b67a8de43f1557033e2/asyncpg-0.31.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b44c31e1efc1c15188ef183f287c728e2046abb1d26af4d20858215d50d91fad", size = 662042, upload-time = "2025-11-24T23:25:49.578Z" }, + { url = "https://files.pythonhosted.org/packages/36/01/ffaa189dcb63a2471720615e60185c3f6327716fdc0fc04334436fbb7c65/asyncpg-0.31.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0c89ccf741c067614c9b5fc7f1fc6f3b61ab05ae4aaa966e6fd6b93097c7d20d", size = 638504, upload-time = "2025-11-24T23:25:51.501Z" }, + { url = "https://files.pythonhosted.org/packages/9f/62/3f699ba45d8bd24c5d65392190d19656d74ff0185f42e19d0bbd973bb371/asyncpg-0.31.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:12b3b2e39dc5470abd5e98c8d3373e4b1d1234d9fbdedf538798b2c13c64460a", size = 3426241, upload-time = "2025-11-24T23:25:53.278Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d1/a867c2150f9c6e7af6462637f613ba67f78a314b00db220cd26ff559d532/asyncpg-0.31.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:aad7a33913fb8bcb5454313377cc330fbb19a0cd5faa7272407d8a0c4257b671", size = 3520321, upload-time = "2025-11-24T23:25:54.982Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1a/cce4c3f246805ecd285a3591222a2611141f1669d002163abef999b60f98/asyncpg-0.31.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3df118d94f46d85b2e434fd62c84cb66d5834d5a890725fe625f498e72e4d5ec", size = 3316685, upload-time = "2025-11-24T23:25:57.43Z" }, + { url = "https://files.pythonhosted.org/packages/40/ae/0fc961179e78cc579e138fad6eb580448ecae64908f95b8cb8ee2f241f67/asyncpg-0.31.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bd5b6efff3c17c3202d4b37189969acf8927438a238c6257f66be3c426beba20", size = 3471858, upload-time = "2025-11-24T23:25:59.636Z" }, + { url = "https://files.pythonhosted.org/packages/52/b2/b20e09670be031afa4cbfabd645caece7f85ec62d69c312239de568e058e/asyncpg-0.31.0-cp312-cp312-win32.whl", hash = "sha256:027eaa61361ec735926566f995d959ade4796f6a49d3bde17e5134b9964f9ba8", size = 527852, upload-time = "2025-11-24T23:26:01.084Z" }, + { url = "https://files.pythonhosted.org/packages/b5/f0/f2ed1de154e15b107dc692262395b3c17fc34eafe2a78fc2115931561730/asyncpg-0.31.0-cp312-cp312-win_amd64.whl", hash = "sha256:72d6bdcbc93d608a1158f17932de2321f68b1a967a13e014998db87a72ed3186", size = 597175, upload-time = "2025-11-24T23:26:02.564Z" }, + { url = "https://files.pythonhosted.org/packages/95/11/97b5c2af72a5d0b9bc3fa30cd4b9ce22284a9a943a150fdc768763caf035/asyncpg-0.31.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c204fab1b91e08b0f47e90a75d1b3c62174dab21f670ad6c5d0f243a228f015b", size = 661111, upload-time = "2025-11-24T23:26:04.467Z" }, + { url = "https://files.pythonhosted.org/packages/1b/71/157d611c791a5e2d0423f09f027bd499935f0906e0c2a416ce712ba51ef3/asyncpg-0.31.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:54a64f91839ba59008eccf7aad2e93d6e3de688d796f35803235ea1c4898ae1e", size = 636928, upload-time = "2025-11-24T23:26:05.944Z" }, + { url = "https://files.pythonhosted.org/packages/2e/fc/9e3486fb2bbe69d4a867c0b76d68542650a7ff1574ca40e84c3111bb0c6e/asyncpg-0.31.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0e0822b1038dc7253b337b0f3f676cadc4ac31b126c5d42691c39691962e403", size = 3424067, upload-time = "2025-11-24T23:26:07.957Z" }, + { url = "https://files.pythonhosted.org/packages/12/c6/8c9d076f73f07f995013c791e018a1cd5f31823c2a3187fc8581706aa00f/asyncpg-0.31.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bef056aa502ee34204c161c72ca1f3c274917596877f825968368b2c33f585f4", size = 3518156, upload-time = "2025-11-24T23:26:09.591Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3b/60683a0baf50fbc546499cfb53132cb6835b92b529a05f6a81471ab60d0c/asyncpg-0.31.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0bfbcc5b7ffcd9b75ab1558f00db2ae07db9c80637ad1b2469c43df79d7a5ae2", size = 3319636, upload-time = "2025-11-24T23:26:11.168Z" }, + { url = "https://files.pythonhosted.org/packages/50/dc/8487df0f69bd398a61e1792b3cba0e47477f214eff085ba0efa7eac9ce87/asyncpg-0.31.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:22bc525ebbdc24d1261ecbf6f504998244d4e3be1721784b5f64664d61fbe602", size = 3472079, upload-time = "2025-11-24T23:26:13.164Z" }, + { url = "https://files.pythonhosted.org/packages/13/a1/c5bbeeb8531c05c89135cb8b28575ac2fac618bcb60119ee9696c3faf71c/asyncpg-0.31.0-cp313-cp313-win32.whl", hash = "sha256:f890de5e1e4f7e14023619399a471ce4b71f5418cd67a51853b9910fdfa73696", size = 527606, upload-time = "2025-11-24T23:26:14.78Z" }, + { url = "https://files.pythonhosted.org/packages/91/66/b25ccb84a246b470eb943b0107c07edcae51804912b824054b3413995a10/asyncpg-0.31.0-cp313-cp313-win_amd64.whl", hash = "sha256:dc5f2fa9916f292e5c5c8b2ac2813763bcd7f58e130055b4ad8a0531314201ab", size = 596569, upload-time = "2025-11-24T23:26:16.189Z" }, + { url = "https://files.pythonhosted.org/packages/3c/36/e9450d62e84a13aea6580c83a47a437f26c7ca6fa0f0fd40b6670793ea30/asyncpg-0.31.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f6b56b91bb0ffc328c4e3ed113136cddd9deefdf5f79ab448598b9772831df44", size = 660867, upload-time = "2025-11-24T23:26:17.631Z" }, + { url = "https://files.pythonhosted.org/packages/82/4b/1d0a2b33b3102d210439338e1beea616a6122267c0df459ff0265cd5807a/asyncpg-0.31.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:334dec28cf20d7f5bb9e45b39546ddf247f8042a690bff9b9573d00086e69cb5", size = 638349, upload-time = "2025-11-24T23:26:19.689Z" }, + { url = "https://files.pythonhosted.org/packages/41/aa/e7f7ac9a7974f08eff9183e392b2d62516f90412686532d27e196c0f0eeb/asyncpg-0.31.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:98cc158c53f46de7bb677fd20c417e264fc02b36d901cc2a43bd6cb0dc6dbfd2", size = 3410428, upload-time = "2025-11-24T23:26:21.275Z" }, + { url = "https://files.pythonhosted.org/packages/6f/de/bf1b60de3dede5c2731e6788617a512bc0ebd9693eac297ee74086f101d7/asyncpg-0.31.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9322b563e2661a52e3cdbc93eed3be7748b289f792e0011cb2720d278b366ce2", size = 3471678, upload-time = "2025-11-24T23:26:23.627Z" }, + { url = "https://files.pythonhosted.org/packages/46/78/fc3ade003e22d8bd53aaf8f75f4be48f0b460fa73738f0391b9c856a9147/asyncpg-0.31.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19857a358fc811d82227449b7ca40afb46e75b33eb8897240c3839dd8b744218", size = 3313505, upload-time = "2025-11-24T23:26:25.235Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e9/73eb8a6789e927816f4705291be21f2225687bfa97321e40cd23055e903a/asyncpg-0.31.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ba5f8886e850882ff2c2ace5732300e99193823e8107e2c53ef01c1ebfa1e85d", size = 3434744, upload-time = "2025-11-24T23:26:26.944Z" }, + { url = "https://files.pythonhosted.org/packages/08/4b/f10b880534413c65c5b5862f79b8e81553a8f364e5238832ad4c0af71b7f/asyncpg-0.31.0-cp314-cp314-win32.whl", hash = "sha256:cea3a0b2a14f95834cee29432e4ddc399b95700eb1d51bbc5bfee8f31fa07b2b", size = 532251, upload-time = "2025-11-24T23:26:28.404Z" }, + { url = "https://files.pythonhosted.org/packages/d3/2d/7aa40750b7a19efa5d66e67fc06008ca0f27ba1bd082e457ad82f59aba49/asyncpg-0.31.0-cp314-cp314-win_amd64.whl", hash = "sha256:04d19392716af6b029411a0264d92093b6e5e8285ae97a39957b9a9c14ea72be", size = 604901, upload-time = "2025-11-24T23:26:30.34Z" }, + { url = "https://files.pythonhosted.org/packages/ce/fe/b9dfe349b83b9dee28cc42360d2c86b2cdce4cb551a2c2d27e156bcac84d/asyncpg-0.31.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:bdb957706da132e982cc6856bb2f7b740603472b54c3ebc77fe60ea3e57e1bd2", size = 702280, upload-time = "2025-11-24T23:26:32Z" }, + { url = "https://files.pythonhosted.org/packages/6a/81/e6be6e37e560bd91e6c23ea8a6138a04fd057b08cf63d3c5055c98e81c1d/asyncpg-0.31.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6d11b198111a72f47154fa03b85799f9be63701e068b43f84ac25da0bda9cb31", size = 682931, upload-time = "2025-11-24T23:26:33.572Z" }, + { url = "https://files.pythonhosted.org/packages/a6/45/6009040da85a1648dd5bc75b3b0a062081c483e75a1a29041ae63a0bf0dc/asyncpg-0.31.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:18c83b03bc0d1b23e6230f5bf8d4f217dc9bc08644ce0502a9d91dc9e634a9c7", size = 3581608, upload-time = "2025-11-24T23:26:35.638Z" }, + { url = "https://files.pythonhosted.org/packages/7e/06/2e3d4d7608b0b2b3adbee0d0bd6a2d29ca0fc4d8a78f8277df04e2d1fd7b/asyncpg-0.31.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e009abc333464ff18b8f6fd146addffd9aaf63e79aa3bb40ab7a4c332d0c5e9e", size = 3498738, upload-time = "2025-11-24T23:26:37.275Z" }, + { url = "https://files.pythonhosted.org/packages/7d/aa/7d75ede780033141c51d83577ea23236ba7d3a23593929b32b49db8ed36e/asyncpg-0.31.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3b1fbcb0e396a5ca435a8826a87e5c2c2cc0c8c68eb6fadf82168056b0e53a8c", size = 3401026, upload-time = "2025-11-24T23:26:39.423Z" }, + { url = "https://files.pythonhosted.org/packages/ba/7a/15e37d45e7f7c94facc1e9148c0e455e8f33c08f0b8a0b1deb2c5171771b/asyncpg-0.31.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8df714dba348efcc162d2adf02d213e5fab1bd9f557e1305633e851a61814a7a", size = 3429426, upload-time = "2025-11-24T23:26:41.032Z" }, + { url = "https://files.pythonhosted.org/packages/13/d5/71437c5f6ae5f307828710efbe62163974e71237d5d46ebd2869ea052d10/asyncpg-0.31.0-cp314-cp314t-win32.whl", hash = "sha256:1b41f1afb1033f2b44f3234993b15096ddc9cd71b21a42dbd87fc6a57b43d65d", size = 614495, upload-time = "2025-11-24T23:26:42.659Z" }, + { url = "https://files.pythonhosted.org/packages/3c/d7/8fb3044eaef08a310acfe23dae9a8e2e07d305edc29a53497e52bc76eca7/asyncpg-0.31.0-cp314-cp314t-win_amd64.whl", hash = "sha256:bd4107bb7cdd0e9e65fae66a62afd3a249663b844fa34d479f6d5b3bef9c04c3", size = 706062, upload-time = "2025-11-24T23:26:44.086Z" }, +] + [[package]] name = "attrs" version = "26.1.0" @@ -600,6 +679,18 @@ requires-dist = [ { name = "sentry-sdk", extras = ["flask"], specifier = "~=2.55.0" }, ] +[[package]] +name = "bugsy" +version = "0.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e5/d0/610e2f68f515238f98f920d9fe0a9b4bbc56436e6cfc64c5b64dc20b1459/bugsy-0.12.0.tar.gz", hash = "sha256:2d49af1cb4a2841e3d677ac535b054c51f457a5d124ad106c43a8fc3847fb26e", size = 17841, upload-time = "2020-07-13T18:47:24.272Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/50/e6/a99aa6ab1ae7842011ca6d50d159a5e5a500717d13fc2ae42f39131741e3/bugsy-0.12.0-py2.py3-none-any.whl", hash = "sha256:b5df82ff0e708cca73a517bfa80494a43b1b97267d5778a5cba62f7a9d04e9da", size = 27223, upload-time = "2020-07-13T18:47:23.232Z" }, +] + [[package]] name = "cachetools" version = "7.0.5" @@ -862,6 +953,28 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, ] +[[package]] +name = "cloud-sql-python-connector" +version = "1.20.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiofiles" }, + { name = "aiohttp" }, + { name = "cryptography" }, + { name = "dnspython" }, + { name = "google-auth" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/35/b2145359b1d6e6a2447be27c5048dd23703ac7df7303f8bbb88f0cb5d793/cloud_sql_python_connector-1.20.2.tar.gz", hash = "sha256:c1a6d827199c9730e7f9687c074885ce7db447dd7d8f3f3a49c37180a530df95", size = 44245, upload-time = "2026-04-17T19:03:37.631Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/53/e94962c964644f75cae552ed2d8162cf0404d6e7004113777f355717f9ec/cloud_sql_python_connector-1.20.2-py3-none-any.whl", hash = "sha256:cbd55bf58cbd5bea8e44c5768bcecc18d4ab2bc490150d90620b3fdb518d1063", size = 50099, upload-time = "2026-04-17T19:03:35.91Z" }, +] + +[package.optional-dependencies] +asyncpg = [ + { name = "asyncpg" }, +] + [[package]] name = "cloudpathlib" version = "0.23.0" @@ -1122,6 +1235,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d2/f1/00ce3bde3ca542d1acd8f8cfa38e446840945aa6363f9b74746394b14127/cryptography-46.0.7-cp38-abi3-win_amd64.whl", hash = "sha256:506c4ff91eff4f82bdac7633318a526b1d1309fc07ca76a3ad182cb5b686d6d3", size = 3472985, upload-time = "2026-04-08T01:57:36.714Z" }, ] +[[package]] +name = "cssbeautifier" +version = "1.15.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "editorconfig" }, + { name = "jsbeautifier" }, + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f7/01/fdf41c1e5f93d359681976ba10410a04b299d248e28ecce1d4e88588dde4/cssbeautifier-1.15.4.tar.gz", hash = "sha256:9bb08dc3f64c101a01677f128acf01905914cf406baf87434dcde05b74c0acf5", size = 25376, upload-time = "2025-02-27T17:53:51.341Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/51/ef6c5628e46092f0a54c7cee69acc827adc6b6aab57b55d344fefbdf28f1/cssbeautifier-1.15.4-py3-none-any.whl", hash = "sha256:78c84d5e5378df7d08622bbd0477a1abdbd209680e95480bf22f12d5701efc98", size = 123667, upload-time = "2025-02-27T17:53:43.594Z" }, +] + [[package]] name = "cssselect" version = "1.4.0" @@ -1246,6 +1373,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" }, ] +[[package]] +name = "dnspython" +version = "2.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/8b/57666417c0f90f08bcafa776861060426765fdb422eb10212086fb811d26/dnspython-2.8.0.tar.gz", hash = "sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f", size = 368251, upload-time = "2025-09-07T18:58:00.022Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", size = 331094, upload-time = "2025-09-07T18:57:58.071Z" }, +] + [[package]] name = "docstring-parser" version = "0.17.0" @@ -1255,6 +1391,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/55/e2/2537ebcff11c1ee1ff17d8d0b6f4db75873e3b0fb32c2d4a2ee31ecb310a/docstring_parser-0.17.0-py3-none-any.whl", hash = "sha256:cf2569abd23dce8099b300f9b4fa8191e9582dda731fd533daf54c4551658708", size = 36896, upload-time = "2025-07-21T07:35:00.684Z" }, ] +[[package]] +name = "editorconfig" +version = "0.17.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/88/3a/a61d9a1f319a186b05d14df17daea42fcddea63c213bcd61a929fb3a6796/editorconfig-0.17.1.tar.gz", hash = "sha256:23c08b00e8e08cc3adcddb825251c497478df1dada6aefeb01e626ad37303745", size = 14695, upload-time = "2025-06-09T08:21:37.097Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/fd/a40c621ff207f3ce8e484aa0fc8ba4eb6e3ecf52e15b42ba764b457a9550/editorconfig-0.17.1-py3-none-any.whl", hash = "sha256:1eda9c2c0db8c16dbd50111b710572a5e6de934e39772de1959d41f64fc17c82", size = 16360, upload-time = "2025-06-09T08:21:35.654Z" }, +] + [[package]] name = "executing" version = "2.2.1" @@ -1273,6 +1418,44 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/51/37/b3ea9cd5558ff4cb51957caca2193981c6b0ff30bd0d2630ac62505d99d0/fake_useragent-2.2.0-py3-none-any.whl", hash = "sha256:67f35ca4d847b0d298187443aaf020413746e56acd985a611908c73dba2daa24", size = 161695, upload-time = "2025-04-14T15:32:17.732Z" }, ] +[[package]] +name = "fastapi" +version = "0.136.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "pydantic" }, + { name = "starlette" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5d/45/c130091c2dfa061bbfe3150f2a5091ef1adf149f2a8d2ae769ecaf6e99a2/fastapi-0.136.1.tar.gz", hash = "sha256:7af665ad7acfa0a3baf8983d393b6b471b9da10ede59c60045f49fbc89a0fa7f", size = 397448, upload-time = "2026-04-23T16:49:44.046Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/ff/2e4eca3ade2c22fe1dea7043b8ee9dabe47753349eb1b56a202de8af6349/fastapi-0.136.1-py3-none-any.whl", hash = "sha256:a6e9d7eeada96c93a4d69cb03836b44fa34e2854accb7244a1ece36cd4781c3f", size = 117683, upload-time = "2026-04-23T16:49:42.437Z" }, +] + +[[package]] +name = "fasteners" +version = "0.20" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2d/18/7881a99ba5244bfc82f06017316ffe93217dbbbcfa52b887caa1d4f2a6d3/fasteners-0.20.tar.gz", hash = "sha256:55dce8792a41b56f727ba6e123fcaee77fd87e638a6863cec00007bfea84c8d8", size = 25087, upload-time = "2025-08-11T10:19:37.785Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/ac/e5d886f892666d2d1e5cb8c1a41146e1d79ae8896477b1153a21711d3b44/fasteners-0.20-py3-none-any.whl", hash = "sha256:9422c40d1e350e4259f509fb2e608d6bc43c0136f79a00db1b49046029d0b3b7", size = 18702, upload-time = "2025-08-11T10:19:35.716Z" }, +] + +[[package]] +name = "ffpuppet" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "psutil" }, + { name = "xvfbwrapper", marker = "sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1c/c6/53684d79f0ee149667a845240de8c6c2c883414a6ffc8e8c1868b579b147/ffpuppet-1.0.0.tar.gz", hash = "sha256:997b97cd6e988a0d73aa1afa0e41d854a2e816f0e4dcb8db21f7fce24d3eaec6", size = 88955, upload-time = "2026-04-13T15:15:44.499Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d8/b5/99b5da978bcaf1e097de98d0d1983dece37a56d46c37c5f3fadb8f59f4d5/ffpuppet-1.0.0-py3-none-any.whl", hash = "sha256:a5d3db5bb831bd783aa0a865e906b209d0df80b1670096768e489cc4377e24ed", size = 102054, upload-time = "2026-04-13T15:15:43.364Z" }, +] + [[package]] name = "fickling" version = "0.1.10" @@ -1484,6 +1667,47 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d5/1f/5f4a3cd9e4440e9d9bc78ad0a91a1c8d46b4d429d5239ebe6793c9fe5c41/fsspec-2026.3.0-py3-none-any.whl", hash = "sha256:d2ceafaad1b3457968ed14efa28798162f1638dbb5d2a6868a2db002a5ee39a4", size = 202595, upload-time = "2026-03-27T19:11:13.595Z" }, ] +[[package]] +name = "fuzzfetch" +version = "16.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytz" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bf/b5/6740955495488e65b7a82631cf5533f755bb9b90ea811fa5c0b74a0ba0b4/fuzzfetch-16.1.0.tar.gz", hash = "sha256:8c50d21c01f0b72a80b1a1453e6c3650615ce72de2add017ff41e5931fb880b1", size = 569555, upload-time = "2025-12-11T17:06:36.613Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/76/dbfd71c02e726625a9005699b62daaae3d929fe5eba611215593e587830a/fuzzfetch-16.1.0-py3-none-any.whl", hash = "sha256:2cb3b1281c040e568559f59408d23534eb3d52c3bc6e53b63de85bc08c7a1b5e", size = 31161, upload-time = "2025-12-11T17:06:35.285Z" }, +] + +[[package]] +name = "fuzzmanager" +version = "0.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "fasteners" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/99/e6/18b19df176271f4dd84345fd4afecb6fec5c41929d29d79ea6cc64aecd58/fuzzmanager-0.9.1.tar.gz", hash = "sha256:bb5d8b09aa5f0778bbd709b253d346b9fe4b6cfa20d40d2a97d080aec7c0732a", size = 1101098, upload-time = "2025-12-15T18:28:39.156Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/82/13c404190cff33ee76971bb410d0d843e89c30e4b03ad5c04d5a9988a69e/fuzzmanager-0.9.1-py3-none-any.whl", hash = "sha256:a66ea0c59431e6827924470876cdb061b16111c048079b3033bf1ff14672f659", size = 83442, upload-time = "2025-12-15T18:28:37.689Z" }, +] + +[[package]] +name = "fxpoppet" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ffpuppet" }, + { name = "fuzzfetch" }, + { name = "pyyaml" }, + { name = "xvfbwrapper" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a9/f4/eb56d0dbfd68ae70ec6a088589c998d49bfba2d5917c836a33f1d448d576/fxpoppet-0.4.1.tar.gz", hash = "sha256:edb3565f89f65385683661c45cc0b61ae33364406cb9bd52e71ae6d80884ae76", size = 44912, upload-time = "2025-11-19T21:58:40.973Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/70/037e1fed8c40b185c8769e137c3be2ab3c19766471462514cd09d0eb022e/fxpoppet-0.4.1-py3-none-any.whl", hash = "sha256:f8d75e5a3b128aa7e78f6a93c2c60443f163694607027809c1acb279c754aaef", size = 49288, upload-time = "2025-11-19T21:58:39.789Z" }, +] + [[package]] name = "gitdb" version = "4.0.12" @@ -1508,6 +1732,28 @@ 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" }, ] +[[package]] +name = "google-api-core" +version = "2.30.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-auth" }, + { name = "googleapis-common-protos" }, + { name = "proto-plus" }, + { name = "protobuf" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/16/ce/502a57fb0ec752026d24df1280b162294b22a0afb98a326084f9a979138b/google_api_core-2.30.3.tar.gz", hash = "sha256:e601a37f148585319b26db36e219df68c5d07b6382cff2d580e83404e44d641b", size = 177001, upload-time = "2026-04-10T00:41:28.035Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/15/e56f351cf6ef1cfea58e6ac226a7318ed1deb2218c4b3cc9bd9e4b786c5a/google_api_core-2.30.3-py3-none-any.whl", hash = "sha256:a85761ba72c444dad5d611c2220633480b2b6be2521eca69cca2dbb3ffd6bfe8", size = 173274, upload-time = "2026-04-09T22:57:16.198Z" }, +] + +[package.optional-dependencies] +grpc = [ + { name = "grpcio" }, + { name = "grpcio-status" }, +] + [[package]] name = "google-auth" version = "2.49.1" @@ -1526,6 +1772,76 @@ requests = [ { name = "requests" }, ] +[[package]] +name = "google-cloud-core" +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/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/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]] +name = "google-cloud-run" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-api-core", extra = ["grpc"] }, + { name = "google-auth" }, + { name = "grpc-google-iam-v1" }, + { name = "grpcio" }, + { name = "proto-plus" }, + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b7/89/dcaf0dc97e39b41e446456ceb60657ab025de79cfccd39cbd739d1a9849e/google_cloud_run-0.16.0.tar.gz", hash = "sha256:d52cf4e6ad3702ae48caccf6abcab543afee6f61c2a6ec753cc62a31e5b629f1", size = 514452, upload-time = "2026-03-26T22:17:05.589Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/c7/46153dc13713b5e4276d86f28ff4563332f9e4bae5ebc83abc5bfd994801/google_cloud_run-0.16.0-py3-none-any.whl", hash = "sha256:d7d2dd7307130fde2a0ce27e96d580dd23b7b2d973b6484b94d902e6b2618860", size = 459112, upload-time = "2026-03-26T22:16:00.018Z" }, +] + +[[package]] +name = "google-cloud-storage" +version = "3.10.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-api-core" }, + { name = "google-auth" }, + { name = "google-cloud-core" }, + { name = "google-crc32c" }, + { name = "google-resumable-media" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4c/47/205eb8e9a1739b5345843e5a425775cbdc472cc38e7eda082ba5b8d02450/google_cloud_storage-3.10.1.tar.gz", hash = "sha256:97db9aa4460727982040edd2bd13ff3d5e2260b5331ad22895802da1fc2a5286", size = 17309950, upload-time = "2026-03-23T09:35:23.409Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ad/ff/ca9ab2417fa913d75aae38bf40bf856bb2749a604b2e0f701b37cfcd23cc/google_cloud_storage-3.10.1-py3-none-any.whl", hash = "sha256:a72f656759b7b99bda700f901adcb3425a828d4a29f911bc26b3ea79c5b1217f", size = 324453, upload-time = "2026-03-23T09:35:21.368Z" }, +] + +[[package]] +name = "google-crc32c" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/03/41/4b9c02f99e4c5fb477122cd5437403b552873f014616ac1d19ac8221a58d/google_crc32c-1.8.0.tar.gz", hash = "sha256:a428e25fb7691024de47fecfbff7ff957214da51eddded0da0ae0e0f03a2cf79", size = 14192, upload-time = "2025-12-16T00:35:25.142Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/5f/7307325b1198b59324c0fa9807cafb551afb65e831699f2ce211ad5c8240/google_crc32c-1.8.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:4b8286b659c1335172e39563ab0a768b8015e88e08329fa5321f774275fc3113", size = 31300, upload-time = "2025-12-16T00:21:56.723Z" }, + { url = "https://files.pythonhosted.org/packages/21/8e/58c0d5d86e2220e6a37befe7e6a94dd2f6006044b1a33edf1ff6d9f7e319/google_crc32c-1.8.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:2a3dc3318507de089c5384cc74d54318401410f82aa65b2d9cdde9d297aca7cb", size = 30867, upload-time = "2025-12-16T00:38:31.302Z" }, + { url = "https://files.pythonhosted.org/packages/ce/a9/a780cc66f86335a6019f557a8aaca8fbb970728f0efd2430d15ff1beae0e/google_crc32c-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:14f87e04d613dfa218d6135e81b78272c3b904e2a7053b841481b38a7d901411", size = 33364, upload-time = "2025-12-16T00:40:22.96Z" }, + { url = "https://files.pythonhosted.org/packages/21/3f/3457ea803db0198c9aaca2dd373750972ce28a26f00544b6b85088811939/google_crc32c-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cb5c869c2923d56cb0c8e6bcdd73c009c36ae39b652dbe46a05eb4ef0ad01454", size = 33740, upload-time = "2025-12-16T00:40:23.96Z" }, + { url = "https://files.pythonhosted.org/packages/df/c0/87c2073e0c72515bb8733d4eef7b21548e8d189f094b5dad20b0ecaf64f6/google_crc32c-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:3cc0c8912038065eafa603b238abf252e204accab2a704c63b9e14837a854962", size = 34437, upload-time = "2025-12-16T00:35:21.395Z" }, + { url = "https://files.pythonhosted.org/packages/d1/db/000f15b41724589b0e7bc24bc7a8967898d8d3bc8caf64c513d91ef1f6c0/google_crc32c-1.8.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:3ebb04528e83b2634857f43f9bb8ef5b2bbe7f10f140daeb01b58f972d04736b", size = 31297, upload-time = "2025-12-16T00:23:20.709Z" }, + { url = "https://files.pythonhosted.org/packages/d7/0d/8ebed0c39c53a7e838e2a486da8abb0e52de135f1b376ae2f0b160eb4c1a/google_crc32c-1.8.0-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:450dc98429d3e33ed2926fc99ee81001928d63460f8538f21a5d6060912a8e27", size = 30867, upload-time = "2025-12-16T00:43:14.628Z" }, + { url = "https://files.pythonhosted.org/packages/ce/42/b468aec74a0354b34c8cbf748db20d6e350a68a2b0912e128cabee49806c/google_crc32c-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3b9776774b24ba76831609ffbabce8cdf6fa2bd5e9df37b594221c7e333a81fa", size = 33344, upload-time = "2025-12-16T00:40:24.742Z" }, + { url = "https://files.pythonhosted.org/packages/1c/e8/b33784d6fc77fb5062a8a7854e43e1e618b87d5ddf610a88025e4de6226e/google_crc32c-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:89c17d53d75562edfff86679244830599ee0a48efc216200691de8b02ab6b2b8", size = 33694, upload-time = "2025-12-16T00:40:25.505Z" }, + { url = "https://files.pythonhosted.org/packages/92/b1/d3cbd4d988afb3d8e4db94ca953df429ed6db7282ed0e700d25e6c7bfc8d/google_crc32c-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:57a50a9035b75643996fbf224d6661e386c7162d1dfdab9bc4ca790947d1007f", size = 34435, upload-time = "2025-12-16T00:35:22.107Z" }, + { url = "https://files.pythonhosted.org/packages/21/88/8ecf3c2b864a490b9e7010c84fd203ec8cf3b280651106a3a74dd1b0ca72/google_crc32c-1.8.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:e6584b12cb06796d285d09e33f63309a09368b9d806a551d8036a4207ea43697", size = 31301, upload-time = "2025-12-16T00:24:48.527Z" }, + { url = "https://files.pythonhosted.org/packages/36/c6/f7ff6c11f5ca215d9f43d3629163727a272eabc356e5c9b2853df2bfe965/google_crc32c-1.8.0-cp314-cp314-macosx_12_0_x86_64.whl", hash = "sha256:f4b51844ef67d6cf2e9425983274da75f18b1597bb2c998e1c0a0e8d46f8f651", size = 30868, upload-time = "2025-12-16T00:48:12.163Z" }, + { url = "https://files.pythonhosted.org/packages/56/15/c25671c7aad70f8179d858c55a6ae8404902abe0cdcf32a29d581792b491/google_crc32c-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b0d1a7afc6e8e4635564ba8aa5c0548e3173e41b6384d7711a9123165f582de2", size = 33381, upload-time = "2025-12-16T00:40:26.268Z" }, + { url = "https://files.pythonhosted.org/packages/42/fa/f50f51260d7b0ef5d4898af122d8a7ec5a84e2984f676f746445f783705f/google_crc32c-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8b3f68782f3cbd1bce027e48768293072813469af6a61a86f6bb4977a4380f21", size = 33734, upload-time = "2025-12-16T00:40:27.028Z" }, + { url = "https://files.pythonhosted.org/packages/08/a5/7b059810934a09fb3ccb657e0843813c1fee1183d3bc2c8041800374aa2c/google_crc32c-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:d511b3153e7011a27ab6ee6bb3a5404a55b994dc1a7322c0b87b29606d9790e2", size = 34878, upload-time = "2025-12-16T00:35:23.142Z" }, +] + [[package]] name = "google-genai" version = "1.70.0" @@ -1547,6 +1863,35 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/36/a3/d4564c8a9beaf6a3cef8d70fa6354318572cebfee65db4f01af0d41f45ba/google_genai-1.70.0-py3-none-any.whl", hash = "sha256:b74c24549d8b4208f4c736fd11857374788e1ffffc725de45d706e35c97fceee", size = 760584, upload-time = "2026-04-01T10:52:44.349Z" }, ] +[[package]] +name = "google-resumable-media" +version = "2.9.0" +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" } +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" }, +] + +[[package]] +name = "googleapis-common-protos" +version = "1.75.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" } +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" }, +] + +[package.optional-dependencies] +grpc = [ + { name = "grpcio" }, +] + [[package]] name = "gql" version = "4.0.0" @@ -1594,6 +1939,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ea/ab/1608e5a7578e62113506740b88066bf09888322a311cff602105e619bd87/greenlet-3.3.2-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:ac8d61d4343b799d1e526db579833d72f23759c71e07181c2d2944e429eb09cd", size = 280358, upload-time = "2026-02-20T20:17:43.971Z" }, { url = "https://files.pythonhosted.org/packages/a5/23/0eae412a4ade4e6623ff7626e38998cb9b11e9ff1ebacaa021e4e108ec15/greenlet-3.3.2-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ceec72030dae6ac0c8ed7591b96b70410a8be370b6a477b1dbc072856ad02bd", size = 601217, upload-time = "2026-02-20T20:47:31.462Z" }, { url = "https://files.pythonhosted.org/packages/f8/16/5b1678a9c07098ecb9ab2dd159fafaf12e963293e61ee8d10ecb55273e5e/greenlet-3.3.2-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a2a5be83a45ce6188c045bcc44b0ee037d6a518978de9a5d97438548b953a1ac", size = 611792, upload-time = "2026-02-20T20:55:58.423Z" }, + { url = "https://files.pythonhosted.org/packages/5c/c5/cc09412a29e43406eba18d61c70baa936e299bc27e074e2be3806ed29098/greenlet-3.3.2-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ae9e21c84035c490506c17002f5c8ab25f980205c3e61ddb3a2a2a2e6c411fcb", size = 626250, upload-time = "2026-02-20T21:02:46.596Z" }, { url = "https://files.pythonhosted.org/packages/50/1f/5155f55bd71cabd03765a4aac9ac446be129895271f73872c36ebd4b04b6/greenlet-3.3.2-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43e99d1749147ac21dde49b99c9abffcbc1e2d55c67501465ef0930d6e78e070", size = 613875, upload-time = "2026-02-20T20:21:01.102Z" }, { url = "https://files.pythonhosted.org/packages/fc/dd/845f249c3fcd69e32df80cdab059b4be8b766ef5830a3d0aa9d6cad55beb/greenlet-3.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4c956a19350e2c37f2c48b336a3afb4bff120b36076d9d7fb68cb44e05d95b79", size = 1571467, upload-time = "2026-02-20T20:49:33.495Z" }, { url = "https://files.pythonhosted.org/packages/2a/50/2649fe21fcc2b56659a452868e695634722a6655ba245d9f77f5656010bf/greenlet-3.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6c6f8ba97d17a1e7d664151284cb3315fc5f8353e75221ed4324f84eb162b395", size = 1640001, upload-time = "2026-02-20T20:21:09.154Z" }, @@ -1602,6 +1948,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ac/48/f8b875fa7dea7dd9b33245e37f065af59df6a25af2f9561efa8d822fde51/greenlet-3.3.2-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:aa6ac98bdfd716a749b84d4034486863fd81c3abde9aa3cf8eff9127981a4ae4", size = 279120, upload-time = "2026-02-20T20:19:01.9Z" }, { url = "https://files.pythonhosted.org/packages/49/8d/9771d03e7a8b1ee456511961e1b97a6d77ae1dea4a34a5b98eee706689d3/greenlet-3.3.2-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ab0c7e7901a00bc0a7284907273dc165b32e0d109a6713babd04471327ff7986", size = 603238, upload-time = "2026-02-20T20:47:32.873Z" }, { url = "https://files.pythonhosted.org/packages/59/0e/4223c2bbb63cd5c97f28ffb2a8aee71bdfb30b323c35d409450f51b91e3e/greenlet-3.3.2-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d248d8c23c67d2291ffd47af766e2a3aa9fa1c6703155c099feb11f526c63a92", size = 614219, upload-time = "2026-02-20T20:55:59.817Z" }, + { url = "https://files.pythonhosted.org/packages/94/2b/4d012a69759ac9d77210b8bfb128bc621125f5b20fc398bce3940d036b1c/greenlet-3.3.2-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ccd21bb86944ca9be6d967cf7691e658e43417782bce90b5d2faeda0ff78a7dd", size = 628268, upload-time = "2026-02-20T21:02:48.024Z" }, { url = "https://files.pythonhosted.org/packages/7a/34/259b28ea7a2a0c904b11cd36c79b8cef8019b26ee5dbe24e73b469dea347/greenlet-3.3.2-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b6997d360a4e6a4e936c0f9625b1c20416b8a0ea18a8e19cabbefc712e7397ab", size = 616774, upload-time = "2026-02-20T20:21:02.454Z" }, { url = "https://files.pythonhosted.org/packages/0a/03/996c2d1689d486a6e199cb0f1cf9e4aa940c500e01bdf201299d7d61fa69/greenlet-3.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:64970c33a50551c7c50491671265d8954046cb6e8e2999aacdd60e439b70418a", size = 1571277, upload-time = "2026-02-20T20:49:34.795Z" }, { url = "https://files.pythonhosted.org/packages/d9/c4/2570fc07f34a39f2caf0bf9f24b0a1a0a47bc2e8e465b2c2424821389dfc/greenlet-3.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1a9172f5bf6bd88e6ba5a84e0a68afeac9dc7b6b412b245dd64f52d83c81e55b", size = 1640455, upload-time = "2026-02-20T20:21:10.261Z" }, @@ -1610,6 +1957,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3f/ae/8bffcbd373b57a5992cd077cbe8858fff39110480a9d50697091faea6f39/greenlet-3.3.2-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:8d1658d7291f9859beed69a776c10822a0a799bc4bfe1bd4272bb60e62507dab", size = 279650, upload-time = "2026-02-20T20:18:00.783Z" }, { url = "https://files.pythonhosted.org/packages/d1/c0/45f93f348fa49abf32ac8439938726c480bd96b2a3c6f4d949ec0124b69f/greenlet-3.3.2-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:18cb1b7337bca281915b3c5d5ae19f4e76d35e1df80f4ad3c1a7be91fadf1082", size = 650295, upload-time = "2026-02-20T20:47:34.036Z" }, { url = "https://files.pythonhosted.org/packages/b3/de/dd7589b3f2b8372069ab3e4763ea5329940fc7ad9dcd3e272a37516d7c9b/greenlet-3.3.2-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c2e47408e8ce1c6f1ceea0dffcdf6ebb85cc09e55c7af407c99f1112016e45e9", size = 662163, upload-time = "2026-02-20T20:56:01.295Z" }, + { url = "https://files.pythonhosted.org/packages/cd/ac/85804f74f1ccea31ba518dcc8ee6f14c79f73fe36fa1beba38930806df09/greenlet-3.3.2-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e3cb43ce200f59483eb82949bf1835a99cf43d7571e900d7c8d5c62cdf25d2f9", size = 675371, upload-time = "2026-02-20T21:02:49.664Z" }, { url = "https://files.pythonhosted.org/packages/d2/d8/09bfa816572a4d83bccd6750df1926f79158b1c36c5f73786e26dbe4ee38/greenlet-3.3.2-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63d10328839d1973e5ba35e98cccbca71b232b14051fd957b6f8b6e8e80d0506", size = 664160, upload-time = "2026-02-20T20:21:04.015Z" }, { url = "https://files.pythonhosted.org/packages/48/cf/56832f0c8255d27f6c35d41b5ec91168d74ec721d85f01a12131eec6b93c/greenlet-3.3.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8e4ab3cfb02993c8cc248ea73d7dae6cec0253e9afa311c9b37e603ca9fad2ce", size = 1619181, upload-time = "2026-02-20T20:49:36.052Z" }, { url = "https://files.pythonhosted.org/packages/0a/23/b90b60a4aabb4cec0796e55f25ffbfb579a907c3898cd2905c8918acaa16/greenlet-3.3.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:94ad81f0fd3c0c0681a018a976e5c2bd2ca2d9d94895f23e7bb1af4e8af4e2d5", size = 1687713, upload-time = "2026-02-20T20:21:11.684Z" }, @@ -1618,12 +1966,51 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/98/6d/8f2ef704e614bcf58ed43cfb8d87afa1c285e98194ab2cfad351bf04f81e/greenlet-3.3.2-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:e26e72bec7ab387ac80caa7496e0f908ff954f31065b0ffc1f8ecb1338b11b54", size = 286617, upload-time = "2026-02-20T20:19:29.856Z" }, { url = "https://files.pythonhosted.org/packages/5e/0d/93894161d307c6ea237a43988f27eba0947b360b99ac5239ad3fe09f0b47/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b466dff7a4ffda6ca975979bab80bdadde979e29fc947ac3be4451428d8b0e4", size = 655189, upload-time = "2026-02-20T20:47:35.742Z" }, { url = "https://files.pythonhosted.org/packages/f5/2c/d2d506ebd8abcb57386ec4f7ba20f4030cbe56eae541bc6fd6ef399c0b41/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b8bddc5b73c9720bea487b3bffdb1840fe4e3656fba3bd40aa1489e9f37877ff", size = 658225, upload-time = "2026-02-20T20:56:02.527Z" }, + { url = "https://files.pythonhosted.org/packages/d1/67/8197b7e7e602150938049d8e7f30de1660cfb87e4c8ee349b42b67bdb2e1/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:59b3e2c40f6706b05a9cd299c836c6aa2378cabe25d021acd80f13abf81181cf", size = 666581, upload-time = "2026-02-20T21:02:51.526Z" }, { url = "https://files.pythonhosted.org/packages/8e/30/3a09155fbf728673a1dea713572d2d31159f824a37c22da82127056c44e4/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b26b0f4428b871a751968285a1ac9648944cea09807177ac639b030bddebcea4", size = 657907, upload-time = "2026-02-20T20:21:05.259Z" }, { url = "https://files.pythonhosted.org/packages/f3/fd/d05a4b7acd0154ed758797f0a43b4c0962a843bedfe980115e842c5b2d08/greenlet-3.3.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1fb39a11ee2e4d94be9a76671482be9398560955c9e568550de0224e41104727", size = 1618857, upload-time = "2026-02-20T20:49:37.309Z" }, { url = "https://files.pythonhosted.org/packages/6f/e1/50ee92a5db521de8f35075b5eff060dd43d39ebd46c2181a2042f7070385/greenlet-3.3.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:20154044d9085151bc309e7689d6f7ba10027f8f5a8c0676ad398b951913d89e", size = 1680010, upload-time = "2026-02-20T20:21:13.427Z" }, { url = "https://files.pythonhosted.org/packages/29/4b/45d90626aef8e65336bed690106d1382f7a43665e2249017e9527df8823b/greenlet-3.3.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c04c5e06ec3e022cbfe2cd4a846e1d4e50087444f875ff6d2c2ad8445495cf1a", size = 237086, upload-time = "2026-02-20T20:20:45.786Z" }, ] +[[package]] +name = "grizzly-framework" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aioquic" }, + { name = "beautifulsoup4" }, + { name = "bugsy" }, + { name = "cryptography" }, + { name = "cssbeautifier" }, + { name = "fasteners" }, + { name = "ffpuppet" }, + { name = "fuzzmanager" }, + { name = "fxpoppet" }, + { name = "jsbeautifier" }, + { name = "lithium-reducer" }, + { name = "prefpicker" }, + { name = "psutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/32/27a32b180e2bc6b6b7a797228e6b89ff2e4e0b025e5bccd494010e8e7c2b/grizzly_framework-1.0.0.tar.gz", hash = "sha256:511855924f9dd40b6ac63213d0e661b45000737a3d184d3f63edd3e2f52c1839", size = 233691, upload-time = "2026-04-13T15:24:29.043Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/db/ecea1b68e80ce46fe3b3eea9c56798b9a80479b2eb6dddddaef3725e378d/grizzly_framework-1.0.0-py3-none-any.whl", hash = "sha256:86df6e68af72f614d7166804629cf6e78125a765bd008549a14e7aef01e0c986", size = 300711, upload-time = "2026-04-13T15:24:27.906Z" }, +] + +[[package]] +name = "grpc-google-iam-v1" +version = "0.14.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "googleapis-common-protos", extra = ["grpc"] }, + { name = "grpcio" }, + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/44/4f/d098419ad0bfc06c9ce440575f05aa22d8973b6c276e86ac7890093d3c37/grpc_google_iam_v1-0.14.4.tar.gz", hash = "sha256:392b3796947ed6334e61171d9ab06bf7eb357f554e5fc7556ad7aab6d0e17038", size = 23706, upload-time = "2026-04-01T01:57:49.813Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/89/22/c2dd50c09bf679bd38173656cd4402d2511e563b33bc88f90009cf50613c/grpc_google_iam_v1-0.14.4-py3-none-any.whl", hash = "sha256:412facc320fcbd94034b4df3d557662051d4d8adfa86e0ddb4dca70a3f739964", size = 32675, upload-time = "2026-04-01T01:57:47.69Z" }, +] + [[package]] name = "grpcio" version = "1.80.0" @@ -1665,6 +2052,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e5/8c/bbe6baf2557262834f2070cf668515fa308b2d38a4bbf771f8f7872a7036/grpcio-1.80.0-cp314-cp314-win_amd64.whl", hash = "sha256:3b01e1f5464c583d2f567b2e46ff0d516ef979978f72091fd81f5ab7fa6e2e7f", size = 5019457, upload-time = "2026-03-30T08:48:37.308Z" }, ] +[[package]] +name = "grpcio-status" +version = "1.80.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "googleapis-common-protos" }, + { name = "grpcio" }, + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/ed/105f619bdd00cb47a49aa2feea6232ea2bbb04199d52a22cc6a7d603b5cb/grpcio_status-1.80.0.tar.gz", hash = "sha256:df73802a4c89a3ea88aa2aff971e886fccce162bc2e6511408b3d67a144381cd", size = 13901, upload-time = "2026-03-30T08:54:34.784Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/80/58cd2dfc19a07d022abe44bde7c365627f6c7cb6f692ada6c65ca437d09a/grpcio_status-1.80.0-py3-none-any.whl", hash = "sha256:4b56990363af50dbf2c2ebb80f1967185c07d87aa25aa2bea45ddb75fc181dbe", size = 14638, upload-time = "2026-03-30T08:54:01.569Z" }, +] + [[package]] name = "gunicorn" version = "25.1.0" @@ -1699,6 +2100,94 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/69/b2/119f6e6dcbd96f9069ce9a2665e0146588dc9f88f29549711853645e736a/h2-4.3.0-py3-none-any.whl", hash = "sha256:c438f029a25f7945c69e0ccf0fb951dc3f73a5f6412981daee861431b70e2bdd", size = 61779, upload-time = "2025-08-23T18:12:17.779Z" }, ] +[[package]] +name = "hackbot-agent-bug-fix" +version = "0.1.0" +source = { virtual = "agents/bug-fix" } +dependencies = [ + { name = "bugbug" }, + { name = "bugsy" }, + { name = "claude-agent-sdk" }, + { name = "grizzly-framework" }, + { name = "hackbot-runtime" }, + { name = "mcp" }, + { name = "prefpicker" }, + { name = "starlette" }, + { name = "uvicorn" }, +] + +[package.metadata] +requires-dist = [ + { name = "bugbug", editable = "." }, + { name = "bugsy" }, + { name = "claude-agent-sdk", specifier = ">=0.1.30" }, + { name = "grizzly-framework" }, + { name = "hackbot-runtime", editable = "libs/hackbot-runtime" }, + { name = "mcp", specifier = ">=1.0.0" }, + { name = "prefpicker" }, + { name = "starlette", specifier = ">=0.36.0" }, + { name = "uvicorn", specifier = ">=0.27.0" }, +] + +[[package]] +name = "hackbot-api" +version = "0.1.0" +source = { virtual = "services/hackbot-api" } +dependencies = [ + { name = "alembic" }, + { name = "asyncpg" }, + { name = "cloud-sql-python-connector", extra = ["asyncpg"] }, + { name = "fastapi" }, + { name = "google-cloud-run" }, + { name = "google-cloud-storage" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "sentry-sdk" }, + { name = "sqlalchemy", extra = ["asyncio"] }, + { name = "uvicorn", extra = ["standard"] }, +] + +[package.optional-dependencies] +dev = [ + { name = "httpx" }, + { name = "pytest" }, + { name = "pytest-asyncio" }, +] + +[package.metadata] +requires-dist = [ + { name = "alembic", specifier = ">=1.13.1" }, + { name = "asyncpg", specifier = ">=0.29.0" }, + { name = "cloud-sql-python-connector", extras = ["asyncpg"], specifier = ">=1.5.0" }, + { name = "fastapi", specifier = ">=0.109.0" }, + { name = "google-cloud-run", specifier = ">=0.10.0" }, + { name = "google-cloud-storage", specifier = ">=2.16.0" }, + { name = "httpx", marker = "extra == 'dev'", specifier = ">=0.26.0" }, + { name = "pydantic", specifier = ">=2.6.0" }, + { name = "pydantic-settings", specifier = ">=2.1.0" }, + { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0.0" }, + { name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=0.23.0" }, + { name = "sentry-sdk", specifier = ">=2.51.0" }, + { name = "sqlalchemy", extras = ["asyncio"], specifier = ">=2.0.25" }, + { name = "uvicorn", extras = ["standard"], specifier = ">=0.27.0" }, +] +provides-extras = ["dev"] + +[[package]] +name = "hackbot-runtime" +version = "0.1.0" +source = { editable = "libs/hackbot-runtime" } +dependencies = [ + { name = "pydantic-settings" }, + { name = "requests" }, +] + +[package.metadata] +requires-dist = [ + { name = "pydantic-settings", specifier = ">=2.1.0" }, + { name = "requests", specifier = ">=2.32.0" }, +] + [[package]] name = "hf-xet" version = "1.4.3" @@ -1753,6 +2242,35 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, ] +[[package]] +name = "httptools" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/46/120a669232c7bdedb9d52d4aeae7e6c7dfe151e99dc70802e2fc7a5e1993/httptools-0.7.1.tar.gz", hash = "sha256:abd72556974f8e7c74a259655924a717a2365b236c882c3f6f8a45fe94703ac9", size = 258961, upload-time = "2025-10-10T03:55:08.559Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/53/7f/403e5d787dc4942316e515e949b0c8a013d84078a915910e9f391ba9b3ed/httptools-0.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:38e0c83a2ea9746ebbd643bdfb521b9aa4a91703e2cd705c20443405d2fd16a5", size = 206280, upload-time = "2025-10-10T03:54:39.274Z" }, + { url = "https://files.pythonhosted.org/packages/2a/0d/7f3fd28e2ce311ccc998c388dd1c53b18120fda3b70ebb022b135dc9839b/httptools-0.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f25bbaf1235e27704f1a7b86cd3304eabc04f569c828101d94a0e605ef7205a5", size = 110004, upload-time = "2025-10-10T03:54:40.403Z" }, + { url = "https://files.pythonhosted.org/packages/84/a6/b3965e1e146ef5762870bbe76117876ceba51a201e18cc31f5703e454596/httptools-0.7.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2c15f37ef679ab9ecc06bfc4e6e8628c32a8e4b305459de7cf6785acd57e4d03", size = 517655, upload-time = "2025-10-10T03:54:41.347Z" }, + { url = "https://files.pythonhosted.org/packages/11/7d/71fee6f1844e6fa378f2eddde6c3e41ce3a1fb4b2d81118dd544e3441ec0/httptools-0.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7fe6e96090df46b36ccfaf746f03034e5ab723162bc51b0a4cf58305324036f2", size = 511440, upload-time = "2025-10-10T03:54:42.452Z" }, + { url = "https://files.pythonhosted.org/packages/22/a5/079d216712a4f3ffa24af4a0381b108aa9c45b7a5cc6eb141f81726b1823/httptools-0.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f72fdbae2dbc6e68b8239defb48e6a5937b12218e6ffc2c7846cc37befa84362", size = 495186, upload-time = "2025-10-10T03:54:43.937Z" }, + { url = "https://files.pythonhosted.org/packages/e9/9e/025ad7b65278745dee3bd0ebf9314934c4592560878308a6121f7f812084/httptools-0.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e99c7b90a29fd82fea9ef57943d501a16f3404d7b9ee81799d41639bdaae412c", size = 499192, upload-time = "2025-10-10T03:54:45.003Z" }, + { url = "https://files.pythonhosted.org/packages/6d/de/40a8f202b987d43afc4d54689600ff03ce65680ede2f31df348d7f368b8f/httptools-0.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:3e14f530fefa7499334a79b0cf7e7cd2992870eb893526fb097d51b4f2d0f321", size = 86694, upload-time = "2025-10-10T03:54:45.923Z" }, + { url = "https://files.pythonhosted.org/packages/09/8f/c77b1fcbfd262d422f12da02feb0d218fa228d52485b77b953832105bb90/httptools-0.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6babce6cfa2a99545c60bfef8bee0cc0545413cb0018f617c8059a30ad985de3", size = 202889, upload-time = "2025-10-10T03:54:47.089Z" }, + { url = "https://files.pythonhosted.org/packages/0a/1a/22887f53602feaa066354867bc49a68fc295c2293433177ee90870a7d517/httptools-0.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:601b7628de7504077dd3dcb3791c6b8694bbd967148a6d1f01806509254fb1ca", size = 108180, upload-time = "2025-10-10T03:54:48.052Z" }, + { url = "https://files.pythonhosted.org/packages/32/6a/6aaa91937f0010d288d3d124ca2946d48d60c3a5ee7ca62afe870e3ea011/httptools-0.7.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:04c6c0e6c5fb0739c5b8a9eb046d298650a0ff38cf42537fc372b28dc7e4472c", size = 478596, upload-time = "2025-10-10T03:54:48.919Z" }, + { url = "https://files.pythonhosted.org/packages/6d/70/023d7ce117993107be88d2cbca566a7c1323ccbaf0af7eabf2064fe356f6/httptools-0.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69d4f9705c405ae3ee83d6a12283dc9feba8cc6aaec671b412917e644ab4fa66", size = 473268, upload-time = "2025-10-10T03:54:49.993Z" }, + { url = "https://files.pythonhosted.org/packages/32/4d/9dd616c38da088e3f436e9a616e1d0cc66544b8cdac405cc4e81c8679fc7/httptools-0.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:44c8f4347d4b31269c8a9205d8a5ee2df5322b09bbbd30f8f862185bb6b05346", size = 455517, upload-time = "2025-10-10T03:54:51.066Z" }, + { url = "https://files.pythonhosted.org/packages/1d/3a/a6c595c310b7df958e739aae88724e24f9246a514d909547778d776799be/httptools-0.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:465275d76db4d554918aba40bf1cbebe324670f3dfc979eaffaa5d108e2ed650", size = 458337, upload-time = "2025-10-10T03:54:52.196Z" }, + { url = "https://files.pythonhosted.org/packages/fd/82/88e8d6d2c51edc1cc391b6e044c6c435b6aebe97b1abc33db1b0b24cd582/httptools-0.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:322d00c2068d125bd570f7bf78b2d367dad02b919d8581d7476d8b75b294e3e6", size = 85743, upload-time = "2025-10-10T03:54:53.448Z" }, + { url = "https://files.pythonhosted.org/packages/34/50/9d095fcbb6de2d523e027a2f304d4551855c2f46e0b82befd718b8b20056/httptools-0.7.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:c08fe65728b8d70b6923ce31e3956f859d5e1e8548e6f22ec520a962c6757270", size = 203619, upload-time = "2025-10-10T03:54:54.321Z" }, + { url = "https://files.pythonhosted.org/packages/07/f0/89720dc5139ae54b03f861b5e2c55a37dba9a5da7d51e1e824a1f343627f/httptools-0.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7aea2e3c3953521c3c51106ee11487a910d45586e351202474d45472db7d72d3", size = 108714, upload-time = "2025-10-10T03:54:55.163Z" }, + { url = "https://files.pythonhosted.org/packages/b3/cb/eea88506f191fb552c11787c23f9a405f4c7b0c5799bf73f2249cd4f5228/httptools-0.7.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0e68b8582f4ea9166be62926077a3334064d422cf08ab87d8b74664f8e9058e1", size = 472909, upload-time = "2025-10-10T03:54:56.056Z" }, + { url = "https://files.pythonhosted.org/packages/e0/4a/a548bdfae6369c0d078bab5769f7b66f17f1bfaa6fa28f81d6be6959066b/httptools-0.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df091cf961a3be783d6aebae963cc9b71e00d57fa6f149025075217bc6a55a7b", size = 470831, upload-time = "2025-10-10T03:54:57.219Z" }, + { url = "https://files.pythonhosted.org/packages/4d/31/14df99e1c43bd132eec921c2e7e11cda7852f65619bc0fc5bdc2d0cb126c/httptools-0.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f084813239e1eb403ddacd06a30de3d3e09a9b76e7894dcda2b22f8a726e9c60", size = 452631, upload-time = "2025-10-10T03:54:58.219Z" }, + { url = "https://files.pythonhosted.org/packages/22/d2/b7e131f7be8d854d48cb6d048113c30f9a46dca0c9a8b08fcb3fcd588cdc/httptools-0.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7347714368fb2b335e9063bc2b96f2f87a9ceffcd9758ac295f8bbcd3ffbc0ca", size = 452910, upload-time = "2025-10-10T03:54:59.366Z" }, + { url = "https://files.pythonhosted.org/packages/53/cf/878f3b91e4e6e011eff6d1fa9ca39f7eb17d19c9d7971b04873734112f30/httptools-0.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:cfabda2a5bb85aa2a904ce06d974a3f30fb36cc63d7feaddec05d2050acede96", size = 88205, upload-time = "2025-10-10T03:55:00.389Z" }, +] + [[package]] name = "httpx" version = "0.28.1" @@ -2074,6 +2592,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7b/91/984aca2ec129e2757d1e4e3c81c3fcda9d0f85b74670a094cc443d9ee949/joblib-1.5.3-py3-none-any.whl", hash = "sha256:5fc3c5039fc5ca8c0276333a188bbd59d6b7ab37fe6632daa76bc7f9ec18e713", size = 309071, upload-time = "2025-12-15T08:41:44.973Z" }, ] +[[package]] +name = "jsbeautifier" +version = "1.15.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "editorconfig" }, + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ea/98/d6cadf4d5a1c03b2136837a435682418c29fdeb66be137128544cecc5b7a/jsbeautifier-1.15.4.tar.gz", hash = "sha256:5bb18d9efb9331d825735fbc5360ee8f1aac5e52780042803943aa7f854f7592", size = 75257, upload-time = "2025-02-27T17:53:53.252Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2d/14/1c65fccf8413d5f5c6e8425f84675169654395098000d8bddc4e9d3390e1/jsbeautifier-1.15.4-py3-none-any.whl", hash = "sha256:72f65de312a3f10900d7685557f84cb61a9733c50dcc27271a39f5b0051bf528", size = 94707, upload-time = "2025-02-27T17:53:46.152Z" }, +] + [[package]] name = "json-e" version = "4.8.2" @@ -2482,6 +3013,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f1/0d/8c93c11c58338bd6f721fc6ffb48135331e80991dcdaa57ca1492662f53a/libmozdata-0.2.12-py3-none-any.whl", hash = "sha256:d0ccc540d64498e89b5c7dfe83ab224e4abd5c044875eefe4ca44a378ab9a080", size = 73367, upload-time = "2025-10-15T08:52:36.197Z" }, ] +[[package]] +name = "lithium-reducer" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ffpuppet" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/64/30/763b1a7155d0c0e7850b54dd5e7f59b9204e26bc44a05ccdadca10154662/lithium_reducer-4.0.0.tar.gz", hash = "sha256:1af0695f8bc982c76f3df7da7fdf14f5bb4390b09226d61a19680f75405db3ad", size = 55912, upload-time = "2024-11-12T19:19:07.793Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/7a/604829c4f18b2514f91dcbe5648e23af99918af07e75c22ba7042072653b/lithium_reducer-4.0.0-py3-none-any.whl", hash = "sha256:069a693b9bcccd955c907805736a18f01d25e2ad2d157d9602c3d71535cf96ec", size = 50898, upload-time = "2024-11-12T19:19:06.711Z" }, +] + [[package]] name = "llama-cpp-python" version = "0.3.19" @@ -2741,6 +3284,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d9/76/7ffc1d3005cf7749123bc47cb3ea343cd97b0ac2211bab40f57283577d0e/lxml_html_clean-0.4.4-py3-none-any.whl", hash = "sha256:ce2ef506614ecb85ee1c5fe0a2aa45b06a19514ec7949e9c8f34f06925cfabcb", size = 14565, upload-time = "2026-02-27T09:35:51.86Z" }, ] +[[package]] +name = "mako" +version = "1.3.12" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/00/62/791b31e69ae182791ec67f04850f2f062716bbd205483d63a215f3e062d3/mako-1.3.12.tar.gz", hash = "sha256:9f778e93289bd410bb35daadeb4fc66d95a746f0b75777b942088b7fd7af550a", size = 400219, upload-time = "2026-04-28T19:01:08.512Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/b1/a0ec7a5a9db730a08daef1fdfb8090435b82465abbf758a596f0ea88727e/mako-1.3.12-py3-none-any.whl", hash = "sha256:8f61569480282dbf557145ce441e4ba888be453c30989f879f0d652e39f53ea9", size = 78521, upload-time = "2026-04-28T19:01:10.393Z" }, +] + [[package]] name = "markdown-it-py" version = "4.0.0" @@ -3711,6 +4266,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5d/19/fd3ef348460c80af7bb4669ea7926651d1f95c23ff2df18b9d24bab4f3fa/pre_commit-4.5.1-py2.py3-none-any.whl", hash = "sha256:3b3afd891e97337708c1674210f8eba659b52a38ea5f822ff142d10786221f77", size = 226437, upload-time = "2025-12-16T21:14:32.409Z" }, ] +[[package]] +name = "prefpicker" +version = "2.23.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/60/27/f02129eea456b21cf7f369312d370939e3a7de1d87ba29531bdffee18c42/prefpicker-2.23.0.tar.gz", hash = "sha256:d464a9c284dff85cb815fd4c8f450c7e438691fa87b59c3213e9a58a70f6f79f", size = 27434, upload-time = "2026-04-13T15:27:10.617Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3e/cb/40499dd1b226e649f06c9798d611b727b7e7dd6061b3e992144443f78793/prefpicker-2.23.0-py3-none-any.whl", hash = "sha256:c06deb8dad64ae12a10e881968650a926413c011e76fcb3a903f5c6592eaf672", size = 25029, upload-time = "2026-04-13T15:27:09.409Z" }, +] + [[package]] name = "preshed" version = "3.0.13" @@ -3851,6 +4418,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5b/5a/bc7b4a4ef808fa59a816c17b20c4bef6884daebbdf627ff2a161da67da19/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", size = 13305, upload-time = "2025-10-08T19:49:00.792Z" }, ] +[[package]] +name = "proto-plus" +version = "1.28.0" +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" } +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" }, +] + [[package]] name = "protobuf" version = "6.33.6" @@ -4128,6 +4707,25 @@ crypto = [ { name = "cryptography" }, ] +[[package]] +name = "pylsqpack" +version = "0.3.24" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7c/a0/20b34e654b911a9abb736b242cc0a11912bc79ea3e911f139ea756e39ea2/pylsqpack-0.3.24.tar.gz", hash = "sha256:8ec455f44614228f89e38d40c1b1e37895620e20ec6b21e3b562fa8b79a23890", size = 677187, upload-time = "2026-03-29T15:42:40.136Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/88/71b79d334f67dd595fbed5f3a337e2aa997a96e452bb1b64120bccf5679d/pylsqpack-0.3.24-cp310-abi3-macosx_10_9_x86_64.whl", hash = "sha256:8edf48d0a023cd3629b2c4aaccac9b79a46d566c0f61e7416b5678228433763d", size = 162525, upload-time = "2026-03-29T15:42:25.436Z" }, + { url = "https://files.pythonhosted.org/packages/4e/96/f0a7625075394e93db42bd476abb7240ff1a474acd1ad404158baf68dc6a/pylsqpack-0.3.24-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:e7d956dbc8f7d597b237b9157d0a16bc7c655a1b031239763c18dc8582aff8cc", size = 168643, upload-time = "2026-03-29T15:42:26.744Z" }, + { url = "https://files.pythonhosted.org/packages/42/de/49ec59856ea41468ed879ec143fc429729e37e4860b2119959a2a66fb652/pylsqpack-0.3.24-cp310-abi3-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b6a8bb42127d5ece8d301a673c8205df25b73b69f8c46b9f0c3034588de1789a", size = 246930, upload-time = "2026-03-29T15:42:28.136Z" }, + { url = "https://files.pythonhosted.org/packages/cd/d3/3e748fa5317782bfe68a7eaf890524aee48281c59f07e9bdfd7774f158db/pylsqpack-0.3.24-cp310-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e3f977d419c60c1d6c2240e6d7a52df820d37eb8c36b4057113bcd7859f53e2c", size = 249234, upload-time = "2026-03-29T15:42:29.583Z" }, + { url = "https://files.pythonhosted.org/packages/22/5b/06f5e354ed882ce036ed65f2a393c98d0f6c71a23fa64b53251ddeb40a7b/pylsqpack-0.3.24-cp310-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6024854eb16d32803d4890fb90a73b9348c74b61c0770680aefaaa75f8456e8c", size = 250274, upload-time = "2026-03-29T15:42:31.03Z" }, + { url = "https://files.pythonhosted.org/packages/61/0e/c95cae2817a5c272b7a3132376165aa16875efcccbbd3e6608f5082770cc/pylsqpack-0.3.24-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:54978a9879471596d84bbad5e67d727014048926bc5bb2dac0eb3701b48c5ac9", size = 246966, upload-time = "2026-03-29T15:42:32.035Z" }, + { url = "https://files.pythonhosted.org/packages/40/fe/d5e84c3b4b2fa716df9e95aeb40d3bfb4de50c21cccccd66e194cfc084ac/pylsqpack-0.3.24-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:caf63ddc2e581c764d17432893acce02c5c29ff879d77c2abf1e26aa4eeb831b", size = 246546, upload-time = "2026-03-29T15:42:33.105Z" }, + { url = "https://files.pythonhosted.org/packages/65/f5/88e442ced83c0305f50f45bf521bbce3344ef0c29c3442f010086ff0c124/pylsqpack-0.3.24-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e3dc5f146fd456b50b227858aed59faa0ff8445aa426e69bb4e50d46c487aab0", size = 248517, upload-time = "2026-03-29T15:42:34.237Z" }, + { url = "https://files.pythonhosted.org/packages/a6/c2/886348974bba20db2a80cf37e97203d7334223b3c1c1babe4159dd12626d/pylsqpack-0.3.24-cp310-abi3-win32.whl", hash = "sha256:8da12be7b35b7c9a8cf73a4c077f72e5022a311f80a401c79904213376f2d767", size = 153483, upload-time = "2026-03-29T15:42:35.214Z" }, + { url = "https://files.pythonhosted.org/packages/0d/22/adbce7adfb41b8f5f222195f7f4f5e58655aa3e83f525bc5f3882b07d6e8/pylsqpack-0.3.24-cp310-abi3-win_amd64.whl", hash = "sha256:c3e2327af25ee616ce4483a8748f0957cf017cbca82d58ed15efea68f70f94ff", size = 156145, upload-time = "2026-03-29T15:42:36.902Z" }, + { url = "https://files.pythonhosted.org/packages/a5/2e/6fb6d797ce88741a0e18984bbab69160abc0971a41f4478cab6c8255a8dc/pylsqpack-0.3.24-cp310-abi3-win_arm64.whl", hash = "sha256:23b4d8af48836893beac356c10ca268161953de5bf9ed691526a93f5c82433e9", size = 153424, upload-time = "2026-03-29T15:42:38.73Z" }, +] + [[package]] name = "pyopenssl" version = "26.0.0" @@ -4201,6 +4799,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" }, ] +[[package]] +name = "pytest-asyncio" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/90/2c/8af215c0f776415f3590cac4f9086ccefd6fd463befeae41cd4d3f193e5a/pytest_asyncio-1.3.0.tar.gz", hash = "sha256:d7f52f36d231b80ee124cd216ffb19369aa168fc10095013c6b014a34d3ee9e5", size = 50087, upload-time = "2025-11-10T16:07:47.256Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5", size = 15075, upload-time = "2025-11-10T16:07:45.537Z" }, +] + [[package]] name = "pytest-cov" version = "7.1.0" @@ -4912,6 +5523,21 @@ flask = [ { name = "markupsafe" }, ] +[[package]] +name = "service-identity" +version = "24.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "cryptography" }, + { name = "pyasn1" }, + { name = "pyasn1-modules" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/07/a5/dfc752b979067947261dbbf2543470c58efe735c3c1301dd870ef27830ee/service_identity-24.2.0.tar.gz", hash = "sha256:b8683ba13f0d39c6cd5d625d2c5f65421d6d707b013b375c355751557cbe8e09", size = 39245, upload-time = "2024-10-26T07:21:57.736Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/2c/ca6dd598b384bc1ce581e24aaae0f2bed4ccac57749d5c3befbb5e742081/service_identity-24.2.0-py3-none-any.whl", hash = "sha256:6b047fbd8a84fd0bb0d55ebce4031e400562b9196e1e0d3e0fe2b8a59f6d4a85", size = 11364, upload-time = "2024-10-26T07:21:56.302Z" }, +] + [[package]] name = "setuptools" version = "82.0.1" @@ -5187,6 +5813,11 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/46/2c/9664130905f03db57961b8980b05cab624afd114bf2be2576628a9f22da4/sqlalchemy-2.0.48-py3-none-any.whl", hash = "sha256:a66fe406437dd65cacd96a72689a3aaaecaebbcd62d81c5ac1c0fdbeac835096", size = 1940202, upload-time = "2026-03-02T15:52:43.285Z" }, ] +[package.optional-dependencies] +asyncio = [ + { name = "greenlet" }, +] + [[package]] name = "srsly" version = "2.5.3" @@ -5585,14 +6216,57 @@ name = "uvicorn" version = "0.42.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "click", marker = "sys_platform != 'emscripten'" }, - { name = "h11", marker = "sys_platform != 'emscripten'" }, + { name = "click" }, + { name = "h11" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e3/ad/4a96c425be6fb67e0621e62d86c402b4a17ab2be7f7c055d9bd2f638b9e2/uvicorn-0.42.0.tar.gz", hash = "sha256:9b1f190ce15a2dd22e7758651d9b6d12df09a13d51ba5bf4fc33c383a48e1775", size = 85393, upload-time = "2026-03-16T06:19:50.077Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/0a/89/f8827ccff89c1586027a105e5630ff6139a64da2515e24dafe860bd9ae4d/uvicorn-0.42.0-py3-none-any.whl", hash = "sha256:96c30f5c7abe6f74ae8900a70e92b85ad6613b745d4879eb9b16ccad15645359", size = 68830, upload-time = "2026-03-16T06:19:48.325Z" }, ] +[package.optional-dependencies] +standard = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "httptools" }, + { name = "python-dotenv" }, + { name = "pyyaml" }, + { name = "uvloop", marker = "platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'" }, + { name = "watchfiles" }, + { name = "websockets" }, +] + +[[package]] +name = "uvloop" +version = "0.22.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/06/f0/18d39dbd1971d6d62c4629cc7fa67f74821b0dc1f5a77af43719de7936a7/uvloop-0.22.1.tar.gz", hash = "sha256:6c84bae345b9147082b17371e3dd5d42775bddce91f885499017f4607fdaf39f", size = 2443250, upload-time = "2025-10-16T22:17:19.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/ff/7f72e8170be527b4977b033239a83a68d5c881cc4775fca255c677f7ac5d/uvloop-0.22.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fe94b4564e865d968414598eea1a6de60adba0c040ba4ed05ac1300de402cd42", size = 1359936, upload-time = "2025-10-16T22:16:29.436Z" }, + { url = "https://files.pythonhosted.org/packages/c3/c6/e5d433f88fd54d81ef4be58b2b7b0cea13c442454a1db703a1eea0db1a59/uvloop-0.22.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:51eb9bd88391483410daad430813d982010f9c9c89512321f5b60e2cddbdddd6", size = 752769, upload-time = "2025-10-16T22:16:30.493Z" }, + { url = "https://files.pythonhosted.org/packages/24/68/a6ac446820273e71aa762fa21cdcc09861edd3536ff47c5cd3b7afb10eeb/uvloop-0.22.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:700e674a166ca5778255e0e1dc4e9d79ab2acc57b9171b79e65feba7184b3370", size = 4317413, upload-time = "2025-10-16T22:16:31.644Z" }, + { url = "https://files.pythonhosted.org/packages/5f/6f/e62b4dfc7ad6518e7eff2516f680d02a0f6eb62c0c212e152ca708a0085e/uvloop-0.22.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b5b1ac819a3f946d3b2ee07f09149578ae76066d70b44df3fa990add49a82e4", size = 4426307, upload-time = "2025-10-16T22:16:32.917Z" }, + { url = "https://files.pythonhosted.org/packages/90/60/97362554ac21e20e81bcef1150cb2a7e4ffdaf8ea1e5b2e8bf7a053caa18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e047cc068570bac9866237739607d1313b9253c3051ad84738cbb095be0537b2", size = 4131970, upload-time = "2025-10-16T22:16:34.015Z" }, + { url = "https://files.pythonhosted.org/packages/99/39/6b3f7d234ba3964c428a6e40006340f53ba37993f46ed6e111c6e9141d18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:512fec6815e2dd45161054592441ef76c830eddaad55c8aa30952e6fe1ed07c0", size = 4296343, upload-time = "2025-10-16T22:16:35.149Z" }, + { url = "https://files.pythonhosted.org/packages/89/8c/182a2a593195bfd39842ea68ebc084e20c850806117213f5a299dfc513d9/uvloop-0.22.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:561577354eb94200d75aca23fbde86ee11be36b00e52a4eaf8f50fb0c86b7705", size = 1358611, upload-time = "2025-10-16T22:16:36.833Z" }, + { url = "https://files.pythonhosted.org/packages/d2/14/e301ee96a6dc95224b6f1162cd3312f6d1217be3907b79173b06785f2fe7/uvloop-0.22.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cdf5192ab3e674ca26da2eada35b288d2fa49fdd0f357a19f0e7c4e7d5077c8", size = 751811, upload-time = "2025-10-16T22:16:38.275Z" }, + { url = "https://files.pythonhosted.org/packages/b7/02/654426ce265ac19e2980bfd9ea6590ca96a56f10c76e63801a2df01c0486/uvloop-0.22.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e2ea3d6190a2968f4a14a23019d3b16870dd2190cd69c8180f7c632d21de68d", size = 4288562, upload-time = "2025-10-16T22:16:39.375Z" }, + { url = "https://files.pythonhosted.org/packages/15/c0/0be24758891ef825f2065cd5db8741aaddabe3e248ee6acc5e8a80f04005/uvloop-0.22.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0530a5fbad9c9e4ee3f2b33b148c6a64d47bbad8000ea63704fa8260f4cf728e", size = 4366890, upload-time = "2025-10-16T22:16:40.547Z" }, + { url = "https://files.pythonhosted.org/packages/d2/53/8369e5219a5855869bcee5f4d317f6da0e2c669aecf0ef7d371e3d084449/uvloop-0.22.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bc5ef13bbc10b5335792360623cc378d52d7e62c2de64660616478c32cd0598e", size = 4119472, upload-time = "2025-10-16T22:16:41.694Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ba/d69adbe699b768f6b29a5eec7b47dd610bd17a69de51b251126a801369ea/uvloop-0.22.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1f38ec5e3f18c8a10ded09742f7fb8de0108796eb673f30ce7762ce1b8550cad", size = 4239051, upload-time = "2025-10-16T22:16:43.224Z" }, + { url = "https://files.pythonhosted.org/packages/90/cd/b62bdeaa429758aee8de8b00ac0dd26593a9de93d302bff3d21439e9791d/uvloop-0.22.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3879b88423ec7e97cd4eba2a443aa26ed4e59b45e6b76aabf13fe2f27023a142", size = 1362067, upload-time = "2025-10-16T22:16:44.503Z" }, + { url = "https://files.pythonhosted.org/packages/0d/f8/a132124dfda0777e489ca86732e85e69afcd1ff7686647000050ba670689/uvloop-0.22.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4baa86acedf1d62115c1dc6ad1e17134476688f08c6efd8a2ab076e815665c74", size = 752423, upload-time = "2025-10-16T22:16:45.968Z" }, + { url = "https://files.pythonhosted.org/packages/a3/94/94af78c156f88da4b3a733773ad5ba0b164393e357cc4bd0ab2e2677a7d6/uvloop-0.22.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:297c27d8003520596236bdb2335e6b3f649480bd09e00d1e3a99144b691d2a35", size = 4272437, upload-time = "2025-10-16T22:16:47.451Z" }, + { url = "https://files.pythonhosted.org/packages/b5/35/60249e9fd07b32c665192cec7af29e06c7cd96fa1d08b84f012a56a0b38e/uvloop-0.22.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c1955d5a1dd43198244d47664a5858082a3239766a839b2102a269aaff7a4e25", size = 4292101, upload-time = "2025-10-16T22:16:49.318Z" }, + { url = "https://files.pythonhosted.org/packages/02/62/67d382dfcb25d0a98ce73c11ed1a6fba5037a1a1d533dcbb7cab033a2636/uvloop-0.22.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b31dc2fccbd42adc73bc4e7cdbae4fc5086cf378979e53ca5d0301838c5682c6", size = 4114158, upload-time = "2025-10-16T22:16:50.517Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/f1171b4a882a5d13c8b7576f348acfe6074d72eaf52cccef752f748d4a9f/uvloop-0.22.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:93f617675b2d03af4e72a5333ef89450dfaa5321303ede6e67ba9c9d26878079", size = 4177360, upload-time = "2025-10-16T22:16:52.646Z" }, + { url = "https://files.pythonhosted.org/packages/79/7b/b01414f31546caf0919da80ad57cbfe24c56b151d12af68cee1b04922ca8/uvloop-0.22.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:37554f70528f60cad66945b885eb01f1bb514f132d92b6eeed1c90fd54ed6289", size = 1454790, upload-time = "2025-10-16T22:16:54.355Z" }, + { url = "https://files.pythonhosted.org/packages/d4/31/0bb232318dd838cad3fa8fb0c68c8b40e1145b32025581975e18b11fab40/uvloop-0.22.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b76324e2dc033a0b2f435f33eb88ff9913c156ef78e153fb210e03c13da746b3", size = 796783, upload-time = "2025-10-16T22:16:55.906Z" }, + { url = "https://files.pythonhosted.org/packages/42/38/c9b09f3271a7a723a5de69f8e237ab8e7803183131bc57c890db0b6bb872/uvloop-0.22.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:badb4d8e58ee08dad957002027830d5c3b06aea446a6a3744483c2b3b745345c", size = 4647548, upload-time = "2025-10-16T22:16:57.008Z" }, + { url = "https://files.pythonhosted.org/packages/c1/37/945b4ca0ac27e3dc4952642d4c900edd030b3da6c9634875af6e13ae80e5/uvloop-0.22.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b91328c72635f6f9e0282e4a57da7470c7350ab1c9f48546c0f2866205349d21", size = 4467065, upload-time = "2025-10-16T22:16:58.206Z" }, + { url = "https://files.pythonhosted.org/packages/97/cc/48d232f33d60e2e2e0b42f4e73455b146b76ebe216487e862700457fbf3c/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:daf620c2995d193449393d6c62131b3fbd40a63bf7b307a1527856ace637fe88", size = 4328384, upload-time = "2025-10-16T22:16:59.36Z" }, + { url = "https://files.pythonhosted.org/packages/e4/16/c1fd27e9549f3c4baf1dc9c20c456cd2f822dbf8de9f463824b0c0357e06/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6cde23eeda1a25c75b2e07d39970f3374105d5eafbaab2a4482be82f272d5a5e", size = 4296730, upload-time = "2025-10-16T22:17:00.744Z" }, +] + [[package]] name = "validx" version = "0.8.1" @@ -5676,6 +6350,76 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/06/7c/34330a89da55610daa5f245ddce5aab81244321101614751e7537f125133/wasabi-1.1.3-py3-none-any.whl", hash = "sha256:f76e16e8f7e79f8c4c8be49b4024ac725713ab10cd7f19350ad18a8e3f71728c", size = 27880, upload-time = "2024-05-31T16:56:16.699Z" }, ] +[[package]] +name = "watchfiles" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c2/c9/8869df9b2a2d6c59d79220a4db37679e74f807c559ffe5265e08b227a210/watchfiles-1.1.1.tar.gz", hash = "sha256:a173cb5c16c4f40ab19cecf48a534c409f7ea983ab8fed0741304a1c0a31b3f2", size = 94440, upload-time = "2025-10-14T15:06:21.08Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/74/d5/f039e7e3c639d9b1d09b07ea412a6806d38123f0508e5f9b48a87b0a76cc/watchfiles-1.1.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:8c89f9f2f740a6b7dcc753140dd5e1ab9215966f7a3530d0c0705c83b401bd7d", size = 404745, upload-time = "2025-10-14T15:04:46.731Z" }, + { url = "https://files.pythonhosted.org/packages/a5/96/a881a13aa1349827490dab2d363c8039527060cfcc2c92cc6d13d1b1049e/watchfiles-1.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bd404be08018c37350f0d6e34676bd1e2889990117a2b90070b3007f172d0610", size = 391769, upload-time = "2025-10-14T15:04:48.003Z" }, + { url = "https://files.pythonhosted.org/packages/4b/5b/d3b460364aeb8da471c1989238ea0e56bec24b6042a68046adf3d9ddb01c/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8526e8f916bb5b9a0a777c8317c23ce65de259422bba5b31325a6fa6029d33af", size = 449374, upload-time = "2025-10-14T15:04:49.179Z" }, + { url = "https://files.pythonhosted.org/packages/b9/44/5769cb62d4ed055cb17417c0a109a92f007114a4e07f30812a73a4efdb11/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2edc3553362b1c38d9f06242416a5d8e9fe235c204a4072e988ce2e5bb1f69f6", size = 459485, upload-time = "2025-10-14T15:04:50.155Z" }, + { url = "https://files.pythonhosted.org/packages/19/0c/286b6301ded2eccd4ffd0041a1b726afda999926cf720aab63adb68a1e36/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30f7da3fb3f2844259cba4720c3fc7138eb0f7b659c38f3bfa65084c7fc7abce", size = 488813, upload-time = "2025-10-14T15:04:51.059Z" }, + { url = "https://files.pythonhosted.org/packages/c7/2b/8530ed41112dd4a22f4dcfdb5ccf6a1baad1ff6eed8dc5a5f09e7e8c41c7/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8979280bdafff686ba5e4d8f97840f929a87ed9cdf133cbbd42f7766774d2aa", size = 594816, upload-time = "2025-10-14T15:04:52.031Z" }, + { url = "https://files.pythonhosted.org/packages/ce/d2/f5f9fb49489f184f18470d4f99f4e862a4b3e9ac2865688eb2099e3d837a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dcc5c24523771db3a294c77d94771abcfcb82a0e0ee8efd910c37c59ec1b31bb", size = 475186, upload-time = "2025-10-14T15:04:53.064Z" }, + { url = "https://files.pythonhosted.org/packages/cf/68/5707da262a119fb06fbe214d82dd1fe4a6f4af32d2d14de368d0349eb52a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db5d7ae38ff20153d542460752ff397fcf5c96090c1230803713cf3147a6803", size = 456812, upload-time = "2025-10-14T15:04:55.174Z" }, + { url = "https://files.pythonhosted.org/packages/66/ab/3cbb8756323e8f9b6f9acb9ef4ec26d42b2109bce830cc1f3468df20511d/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:28475ddbde92df1874b6c5c8aaeb24ad5be47a11f87cde5a28ef3835932e3e94", size = 630196, upload-time = "2025-10-14T15:04:56.22Z" }, + { url = "https://files.pythonhosted.org/packages/78/46/7152ec29b8335f80167928944a94955015a345440f524d2dfe63fc2f437b/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:36193ed342f5b9842edd3532729a2ad55c4160ffcfa3700e0d54be496b70dd43", size = 622657, upload-time = "2025-10-14T15:04:57.521Z" }, + { url = "https://files.pythonhosted.org/packages/0a/bf/95895e78dd75efe9a7f31733607f384b42eb5feb54bd2eb6ed57cc2e94f4/watchfiles-1.1.1-cp312-cp312-win32.whl", hash = "sha256:859e43a1951717cc8de7f4c77674a6d389b106361585951d9e69572823f311d9", size = 272042, upload-time = "2025-10-14T15:04:59.046Z" }, + { url = "https://files.pythonhosted.org/packages/87/0a/90eb755f568de2688cb220171c4191df932232c20946966c27a59c400850/watchfiles-1.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:91d4c9a823a8c987cce8fa2690923b069966dabb196dd8d137ea2cede885fde9", size = 288410, upload-time = "2025-10-14T15:05:00.081Z" }, + { url = "https://files.pythonhosted.org/packages/36/76/f322701530586922fbd6723c4f91ace21364924822a8772c549483abed13/watchfiles-1.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:a625815d4a2bdca61953dbba5a39d60164451ef34c88d751f6c368c3ea73d404", size = 278209, upload-time = "2025-10-14T15:05:01.168Z" }, + { url = "https://files.pythonhosted.org/packages/bb/f4/f750b29225fe77139f7ae5de89d4949f5a99f934c65a1f1c0b248f26f747/watchfiles-1.1.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:130e4876309e8686a5e37dba7d5e9bc77e6ed908266996ca26572437a5271e18", size = 404321, upload-time = "2025-10-14T15:05:02.063Z" }, + { url = "https://files.pythonhosted.org/packages/2b/f9/f07a295cde762644aa4c4bb0f88921d2d141af45e735b965fb2e87858328/watchfiles-1.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5f3bde70f157f84ece3765b42b4a52c6ac1a50334903c6eaf765362f6ccca88a", size = 391783, upload-time = "2025-10-14T15:05:03.052Z" }, + { url = "https://files.pythonhosted.org/packages/bc/11/fc2502457e0bea39a5c958d86d2cb69e407a4d00b85735ca724bfa6e0d1a/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14e0b1fe858430fc0251737ef3824c54027bedb8c37c38114488b8e131cf8219", size = 449279, upload-time = "2025-10-14T15:05:04.004Z" }, + { url = "https://files.pythonhosted.org/packages/e3/1f/d66bc15ea0b728df3ed96a539c777acfcad0eb78555ad9efcaa1274688f0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f27db948078f3823a6bb3b465180db8ebecf26dd5dae6f6180bd87383b6b4428", size = 459405, upload-time = "2025-10-14T15:05:04.942Z" }, + { url = "https://files.pythonhosted.org/packages/be/90/9f4a65c0aec3ccf032703e6db02d89a157462fbb2cf20dd415128251cac0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:059098c3a429f62fc98e8ec62b982230ef2c8df68c79e826e37b895bc359a9c0", size = 488976, upload-time = "2025-10-14T15:05:05.905Z" }, + { url = "https://files.pythonhosted.org/packages/37/57/ee347af605d867f712be7029bb94c8c071732a4b44792e3176fa3c612d39/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfb5862016acc9b869bb57284e6cb35fdf8e22fe59f7548858e2f971d045f150", size = 595506, upload-time = "2025-10-14T15:05:06.906Z" }, + { url = "https://files.pythonhosted.org/packages/a8/78/cc5ab0b86c122047f75e8fc471c67a04dee395daf847d3e59381996c8707/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:319b27255aacd9923b8a276bb14d21a5f7ff82564c744235fc5eae58d95422ae", size = 474936, upload-time = "2025-10-14T15:05:07.906Z" }, + { url = "https://files.pythonhosted.org/packages/62/da/def65b170a3815af7bd40a3e7010bf6ab53089ef1b75d05dd5385b87cf08/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c755367e51db90e75b19454b680903631d41f9e3607fbd941d296a020c2d752d", size = 456147, upload-time = "2025-10-14T15:05:09.138Z" }, + { url = "https://files.pythonhosted.org/packages/57/99/da6573ba71166e82d288d4df0839128004c67d2778d3b566c138695f5c0b/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c22c776292a23bfc7237a98f791b9ad3144b02116ff10d820829ce62dff46d0b", size = 630007, upload-time = "2025-10-14T15:05:10.117Z" }, + { url = "https://files.pythonhosted.org/packages/a8/51/7439c4dd39511368849eb1e53279cd3454b4a4dbace80bab88feeb83c6b5/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:3a476189be23c3686bc2f4321dd501cb329c0a0469e77b7b534ee10129ae6374", size = 622280, upload-time = "2025-10-14T15:05:11.146Z" }, + { url = "https://files.pythonhosted.org/packages/95/9c/8ed97d4bba5db6fdcdb2b298d3898f2dd5c20f6b73aee04eabe56c59677e/watchfiles-1.1.1-cp313-cp313-win32.whl", hash = "sha256:bf0a91bfb5574a2f7fc223cf95eeea79abfefa404bf1ea5e339c0c1560ae99a0", size = 272056, upload-time = "2025-10-14T15:05:12.156Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f3/c14e28429f744a260d8ceae18bf58c1d5fa56b50d006a7a9f80e1882cb0d/watchfiles-1.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:52e06553899e11e8074503c8e716d574adeeb7e68913115c4b3653c53f9bae42", size = 288162, upload-time = "2025-10-14T15:05:13.208Z" }, + { url = "https://files.pythonhosted.org/packages/dc/61/fe0e56c40d5cd29523e398d31153218718c5786b5e636d9ae8ae79453d27/watchfiles-1.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:ac3cc5759570cd02662b15fbcd9d917f7ecd47efe0d6b40474eafd246f91ea18", size = 277909, upload-time = "2025-10-14T15:05:14.49Z" }, + { url = "https://files.pythonhosted.org/packages/79/42/e0a7d749626f1e28c7108a99fb9bf524b501bbbeb9b261ceecde644d5a07/watchfiles-1.1.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:563b116874a9a7ce6f96f87cd0b94f7faf92d08d0021e837796f0a14318ef8da", size = 403389, upload-time = "2025-10-14T15:05:15.777Z" }, + { url = "https://files.pythonhosted.org/packages/15/49/08732f90ce0fbbc13913f9f215c689cfc9ced345fb1bcd8829a50007cc8d/watchfiles-1.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3ad9fe1dae4ab4212d8c91e80b832425e24f421703b5a42ef2e4a1e215aff051", size = 389964, upload-time = "2025-10-14T15:05:16.85Z" }, + { url = "https://files.pythonhosted.org/packages/27/0d/7c315d4bd5f2538910491a0393c56bf70d333d51bc5b34bee8e68e8cea19/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce70f96a46b894b36eba678f153f052967a0d06d5b5a19b336ab0dbbd029f73e", size = 448114, upload-time = "2025-10-14T15:05:17.876Z" }, + { url = "https://files.pythonhosted.org/packages/c3/24/9e096de47a4d11bc4df41e9d1e61776393eac4cb6eb11b3e23315b78b2cc/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cb467c999c2eff23a6417e58d75e5828716f42ed8289fe6b77a7e5a91036ca70", size = 460264, upload-time = "2025-10-14T15:05:18.962Z" }, + { url = "https://files.pythonhosted.org/packages/cc/0f/e8dea6375f1d3ba5fcb0b3583e2b493e77379834c74fd5a22d66d85d6540/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:836398932192dae4146c8f6f737d74baeac8b70ce14831a239bdb1ca882fc261", size = 487877, upload-time = "2025-10-14T15:05:20.094Z" }, + { url = "https://files.pythonhosted.org/packages/ac/5b/df24cfc6424a12deb41503b64d42fbea6b8cb357ec62ca84a5a3476f654a/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:743185e7372b7bc7c389e1badcc606931a827112fbbd37f14c537320fca08620", size = 595176, upload-time = "2025-10-14T15:05:21.134Z" }, + { url = "https://files.pythonhosted.org/packages/8f/b5/853b6757f7347de4e9b37e8cc3289283fb983cba1ab4d2d7144694871d9c/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:afaeff7696e0ad9f02cbb8f56365ff4686ab205fcf9c4c5b6fdfaaa16549dd04", size = 473577, upload-time = "2025-10-14T15:05:22.306Z" }, + { url = "https://files.pythonhosted.org/packages/e1/f7/0a4467be0a56e80447c8529c9fce5b38eab4f513cb3d9bf82e7392a5696b/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7eb7da0eb23aa2ba036d4f616d46906013a68caf61b7fdbe42fc8b25132e77", size = 455425, upload-time = "2025-10-14T15:05:23.348Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e0/82583485ea00137ddf69bc84a2db88bd92ab4a6e3c405e5fb878ead8d0e7/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:831a62658609f0e5c64178211c942ace999517f5770fe9436be4c2faeba0c0ef", size = 628826, upload-time = "2025-10-14T15:05:24.398Z" }, + { url = "https://files.pythonhosted.org/packages/28/9a/a785356fccf9fae84c0cc90570f11702ae9571036fb25932f1242c82191c/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:f9a2ae5c91cecc9edd47e041a930490c31c3afb1f5e6d71de3dc671bfaca02bf", size = 622208, upload-time = "2025-10-14T15:05:25.45Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f4/0872229324ef69b2c3edec35e84bd57a1289e7d3fe74588048ed8947a323/watchfiles-1.1.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:d1715143123baeeaeadec0528bb7441103979a1d5f6fd0e1f915383fea7ea6d5", size = 404315, upload-time = "2025-10-14T15:05:26.501Z" }, + { url = "https://files.pythonhosted.org/packages/7b/22/16d5331eaed1cb107b873f6ae1b69e9ced582fcf0c59a50cd84f403b1c32/watchfiles-1.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:39574d6370c4579d7f5d0ad940ce5b20db0e4117444e39b6d8f99db5676c52fd", size = 390869, upload-time = "2025-10-14T15:05:27.649Z" }, + { url = "https://files.pythonhosted.org/packages/b2/7e/5643bfff5acb6539b18483128fdc0ef2cccc94a5b8fbda130c823e8ed636/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7365b92c2e69ee952902e8f70f3ba6360d0d596d9299d55d7d386df84b6941fb", size = 449919, upload-time = "2025-10-14T15:05:28.701Z" }, + { url = "https://files.pythonhosted.org/packages/51/2e/c410993ba5025a9f9357c376f48976ef0e1b1aefb73b97a5ae01a5972755/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bfff9740c69c0e4ed32416f013f3c45e2ae42ccedd1167ef2d805c000b6c71a5", size = 460845, upload-time = "2025-10-14T15:05:30.064Z" }, + { url = "https://files.pythonhosted.org/packages/8e/a4/2df3b404469122e8680f0fcd06079317e48db58a2da2950fb45020947734/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b27cf2eb1dda37b2089e3907d8ea92922b673c0c427886d4edc6b94d8dfe5db3", size = 489027, upload-time = "2025-10-14T15:05:31.064Z" }, + { url = "https://files.pythonhosted.org/packages/ea/84/4587ba5b1f267167ee715b7f66e6382cca6938e0a4b870adad93e44747e6/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:526e86aced14a65a5b0ec50827c745597c782ff46b571dbfe46192ab9e0b3c33", size = 595615, upload-time = "2025-10-14T15:05:32.074Z" }, + { url = "https://files.pythonhosted.org/packages/6a/0f/c6988c91d06e93cd0bb3d4a808bcf32375ca1904609835c3031799e3ecae/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04e78dd0b6352db95507fd8cb46f39d185cf8c74e4cf1e4fbad1d3df96faf510", size = 474836, upload-time = "2025-10-14T15:05:33.209Z" }, + { url = "https://files.pythonhosted.org/packages/b4/36/ded8aebea91919485b7bbabbd14f5f359326cb5ec218cd67074d1e426d74/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c85794a4cfa094714fb9c08d4a218375b2b95b8ed1666e8677c349906246c05", size = 455099, upload-time = "2025-10-14T15:05:34.189Z" }, + { url = "https://files.pythonhosted.org/packages/98/e0/8c9bdba88af756a2fce230dd365fab2baf927ba42cd47521ee7498fd5211/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:74d5012b7630714b66be7b7b7a78855ef7ad58e8650c73afc4c076a1f480a8d6", size = 630626, upload-time = "2025-10-14T15:05:35.216Z" }, + { url = "https://files.pythonhosted.org/packages/2a/84/a95db05354bf2d19e438520d92a8ca475e578c647f78f53197f5a2f17aaf/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:8fbe85cb3201c7d380d3d0b90e63d520f15d6afe217165d7f98c9c649654db81", size = 622519, upload-time = "2025-10-14T15:05:36.259Z" }, + { url = "https://files.pythonhosted.org/packages/1d/ce/d8acdc8de545de995c339be67711e474c77d643555a9bb74a9334252bd55/watchfiles-1.1.1-cp314-cp314-win32.whl", hash = "sha256:3fa0b59c92278b5a7800d3ee7733da9d096d4aabcfabb9a928918bd276ef9b9b", size = 272078, upload-time = "2025-10-14T15:05:37.63Z" }, + { url = "https://files.pythonhosted.org/packages/c4/c9/a74487f72d0451524be827e8edec251da0cc1fcf111646a511ae752e1a3d/watchfiles-1.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:c2047d0b6cea13b3316bdbafbfa0c4228ae593d995030fda39089d36e64fc03a", size = 287664, upload-time = "2025-10-14T15:05:38.95Z" }, + { url = "https://files.pythonhosted.org/packages/df/b8/8ac000702cdd496cdce998c6f4ee0ca1f15977bba51bdf07d872ebdfc34c/watchfiles-1.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:842178b126593addc05acf6fce960d28bc5fae7afbaa2c6c1b3a7b9460e5be02", size = 277154, upload-time = "2025-10-14T15:05:39.954Z" }, + { url = "https://files.pythonhosted.org/packages/47/a8/e3af2184707c29f0f14b1963c0aace6529f9d1b8582d5b99f31bbf42f59e/watchfiles-1.1.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:88863fbbc1a7312972f1c511f202eb30866370ebb8493aef2812b9ff28156a21", size = 403820, upload-time = "2025-10-14T15:05:40.932Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ec/e47e307c2f4bd75f9f9e8afbe3876679b18e1bcec449beca132a1c5ffb2d/watchfiles-1.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:55c7475190662e202c08c6c0f4d9e345a29367438cf8e8037f3155e10a88d5a5", size = 390510, upload-time = "2025-10-14T15:05:41.945Z" }, + { url = "https://files.pythonhosted.org/packages/d5/a0/ad235642118090f66e7b2f18fd5c42082418404a79205cdfca50b6309c13/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f53fa183d53a1d7a8852277c92b967ae99c2d4dcee2bfacff8868e6e30b15f7", size = 448408, upload-time = "2025-10-14T15:05:43.385Z" }, + { url = "https://files.pythonhosted.org/packages/df/85/97fa10fd5ff3332ae17e7e40e20784e419e28521549780869f1413742e9d/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6aae418a8b323732fa89721d86f39ec8f092fc2af67f4217a2b07fd3e93c6101", size = 458968, upload-time = "2025-10-14T15:05:44.404Z" }, + { url = "https://files.pythonhosted.org/packages/47/c2/9059c2e8966ea5ce678166617a7f75ecba6164375f3b288e50a40dc6d489/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f096076119da54a6080e8920cbdaac3dbee667eb91dcc5e5b78840b87415bd44", size = 488096, upload-time = "2025-10-14T15:05:45.398Z" }, + { url = "https://files.pythonhosted.org/packages/94/44/d90a9ec8ac309bc26db808a13e7bfc0e4e78b6fc051078a554e132e80160/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00485f441d183717038ed2e887a7c868154f216877653121068107b227a2f64c", size = 596040, upload-time = "2025-10-14T15:05:46.502Z" }, + { url = "https://files.pythonhosted.org/packages/95/68/4e3479b20ca305cfc561db3ed207a8a1c745ee32bf24f2026a129d0ddb6e/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a55f3e9e493158d7bfdb60a1165035f1cf7d320914e7b7ea83fe22c6023b58fc", size = 473847, upload-time = "2025-10-14T15:05:47.484Z" }, + { url = "https://files.pythonhosted.org/packages/4f/55/2af26693fd15165c4ff7857e38330e1b61ab8c37d15dc79118cdba115b7a/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c91ed27800188c2ae96d16e3149f199d62f86c7af5f5f4d2c61a3ed8cd3666c", size = 455072, upload-time = "2025-10-14T15:05:48.928Z" }, + { url = "https://files.pythonhosted.org/packages/66/1d/d0d200b10c9311ec25d2273f8aad8c3ef7cc7ea11808022501811208a750/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:311ff15a0bae3714ffb603e6ba6dbfba4065ab60865d15a6ec544133bdb21099", size = 629104, upload-time = "2025-10-14T15:05:49.908Z" }, + { url = "https://files.pythonhosted.org/packages/e3/bd/fa9bb053192491b3867ba07d2343d9f2252e00811567d30ae8d0f78136fe/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a916a2932da8f8ab582f242c065f5c81bed3462849ca79ee357dd9551b0e9b01", size = 622112, upload-time = "2025-10-14T15:05:50.941Z" }, +] + [[package]] name = "wcwidth" version = "0.6.0" @@ -5898,6 +6642,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1f/3d/1661dd114a914a67e3f7ab66fa1382e7599c2a8c340f314ad30a3e2b4d08/xgboost-3.2.0-py3-none-win_amd64.whl", hash = "sha256:0d169736fd836fc13646c7ab787167b3a8110351c2c6bc770c755ee1618f0442", size = 101681668, upload-time = "2026-02-10T10:59:31.202Z" }, ] +[[package]] +name = "xvfbwrapper" +version = "0.2.23" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/ab/65cf1481f9aff56d761f4864b5c090ed739609ccd23f491b8418e363b1eb/xvfbwrapper-0.2.23.tar.gz", hash = "sha256:ff318c00e74b60fb657843745c3e7de8df67e2ebd01f533b02747bcb9cabb826", size = 13130, upload-time = "2026-03-22T05:49:53.071Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/e0/36ceaecb09ac035c2d1aa15afc0a469b446aa692e2c549aed38c45e8da3a/xvfbwrapper-0.2.23-py3-none-any.whl", hash = "sha256:1ff01cd5f0e66cff683945155f9000ac9d1ae84832bf9642a096b610b523a70a", size = 7114, upload-time = "2026-03-22T05:49:52.027Z" }, +] + [[package]] name = "xxhash" version = "3.6.0"