chore: move ci.py to ~/.claude/scripts (uv inline script, no project dep)
Script now lives globally as a uv inline-dependency script. Removes per-project scripts/ci.py and the msgspec dev dependency. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -8,7 +8,6 @@ members = [
|
|||||||
[dependency-groups]
|
[dependency-groups]
|
||||||
dev = [
|
dev = [
|
||||||
"hypothesis>=6.151.6",
|
"hypothesis>=6.151.6",
|
||||||
"msgspec>=0.20.0",
|
|
||||||
"niquests>=3.14.0",
|
"niquests>=3.14.0",
|
||||||
"playwright>=1.58.0",
|
"playwright>=1.58.0",
|
||||||
"pytest>=8.0.0",
|
"pytest>=8.0.0",
|
||||||
|
|||||||
356
scripts/ci.py
356
scripts/ci.py
@@ -1,356 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""Query Gitea Actions CI pipeline status.
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
uv run python scripts/ci.py # list last 5 runs
|
|
||||||
uv run python scripts/ci.py --limit 10 # list last 10 runs
|
|
||||||
uv run python scripts/ci.py --branch master # filter by branch
|
|
||||||
uv run python scripts/ci.py --run-id 44 # inspect specific run + logs
|
|
||||||
uv run python scripts/ci.py --logs # list mode with failed job logs
|
|
||||||
uv run python scripts/ci.py --tail-lines 200 # more log context
|
|
||||||
|
|
||||||
Requires: GITEA_TOKEN env var with read:repository + actions scopes.
|
|
||||||
Remote URL is auto-detected from git remote origin (SSH format:
|
|
||||||
ssh://git@HOST:PORT/OWNER/REPO.git).
|
|
||||||
Override with GITEA_BASE_URL + GITEA_REPO (e.g. GITEA_REPO=owner/repo).
|
|
||||||
"""
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
from datetime import datetime
|
|
||||||
from urllib.parse import urlparse
|
|
||||||
|
|
||||||
import msgspec
|
|
||||||
import niquests
|
|
||||||
|
|
||||||
_REQUEST_TIMEOUT_SECONDS = 30
|
|
||||||
_ANSI_ESCAPE = re.compile(r"\x1b\[[0-9;]*[a-zA-Z]")
|
|
||||||
|
|
||||||
|
|
||||||
# ── API boundary types ────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
|
|
||||||
class GiteaRun(msgspec.Struct):
|
|
||||||
id: int
|
|
||||||
display_title: str
|
|
||||||
status: str
|
|
||||||
conclusion: str | None = None
|
|
||||||
head_branch: str = ""
|
|
||||||
head_sha: str = ""
|
|
||||||
html_url: str = ""
|
|
||||||
started_at: str | None = None
|
|
||||||
completed_at: str | None = None
|
|
||||||
|
|
||||||
|
|
||||||
class GiteaRunsResponse(msgspec.Struct):
|
|
||||||
workflow_runs: list[GiteaRun]
|
|
||||||
total_count: int
|
|
||||||
|
|
||||||
|
|
||||||
class GiteaStep(msgspec.Struct):
|
|
||||||
name: str
|
|
||||||
status: str
|
|
||||||
conclusion: str | None = None
|
|
||||||
|
|
||||||
|
|
||||||
class GiteaJob(msgspec.Struct):
|
|
||||||
id: int
|
|
||||||
run_id: int
|
|
||||||
name: str
|
|
||||||
status: str
|
|
||||||
conclusion: str | None = None
|
|
||||||
steps: list[GiteaStep] | None = None # API returns null when job hasn't run yet
|
|
||||||
|
|
||||||
|
|
||||||
class GiteaJobsResponse(msgspec.Struct):
|
|
||||||
jobs: list[GiteaJob]
|
|
||||||
total_count: int
|
|
||||||
|
|
||||||
|
|
||||||
# ── Remote detection ──────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
|
|
||||||
def parse_gitea_remote() -> tuple[str, str, str]:
|
|
||||||
"""Return (api_base_url, owner, repo) from git remote or env vars.
|
|
||||||
|
|
||||||
Parses SSH remote URL: ssh://git@HOST:PORT/OWNER/REPO.git
|
|
||||||
Falls back to GITEA_BASE_URL + GITEA_REPO env vars.
|
|
||||||
"""
|
|
||||||
base_url = os.environ.get("GITEA_BASE_URL", "")
|
|
||||||
repo_slug = os.environ.get("GITEA_REPO", "")
|
|
||||||
if base_url and repo_slug:
|
|
||||||
parts = repo_slug.split("/", 1)
|
|
||||||
assert len(parts) == 2, f"GITEA_REPO must be OWNER/REPO, got: {repo_slug}"
|
|
||||||
return base_url.rstrip("/"), parts[0], parts[1]
|
|
||||||
|
|
||||||
try:
|
|
||||||
raw_url = subprocess.check_output(
|
|
||||||
["git", "remote", "get-url", "origin"],
|
|
||||||
stderr=subprocess.DEVNULL,
|
|
||||||
text=True,
|
|
||||||
).strip()
|
|
||||||
except subprocess.CalledProcessError:
|
|
||||||
print(
|
|
||||||
"Error: could not get git remote URL. Set GITEA_BASE_URL and GITEA_REPO.",
|
|
||||||
file=sys.stderr,
|
|
||||||
)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
parsed = urlparse(raw_url)
|
|
||||||
host = parsed.hostname
|
|
||||||
assert host, f"Could not parse host from remote URL: {raw_url}"
|
|
||||||
|
|
||||||
path_parts = parsed.path.lstrip("/").split("/")
|
|
||||||
assert len(path_parts) >= 2, f"Could not parse owner/repo from remote URL: {raw_url}"
|
|
||||||
|
|
||||||
owner = path_parts[0]
|
|
||||||
repo = path_parts[1].removesuffix(".git")
|
|
||||||
assert owner, f"Empty owner in remote URL: {raw_url}"
|
|
||||||
assert repo, f"Empty repo in remote URL: {raw_url}"
|
|
||||||
|
|
||||||
return f"https://{host}", owner, repo
|
|
||||||
|
|
||||||
|
|
||||||
# ── API calls ─────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
|
|
||||||
def fetch_runs(
|
|
||||||
session: niquests.Session,
|
|
||||||
api_base: str,
|
|
||||||
owner: str,
|
|
||||||
repo: str,
|
|
||||||
*,
|
|
||||||
limit: int,
|
|
||||||
branch: str,
|
|
||||||
) -> list[GiteaRun]:
|
|
||||||
assert limit > 0
|
|
||||||
assert limit <= 20
|
|
||||||
|
|
||||||
# page=1 is required for limit to be respected by Gitea's Actions API
|
|
||||||
params: dict[str, str | int] = {"limit": limit, "page": 1}
|
|
||||||
if branch:
|
|
||||||
params["branch"] = branch
|
|
||||||
|
|
||||||
resp = session.get(
|
|
||||||
f"{api_base}/api/v1/repos/{owner}/{repo}/actions/runs",
|
|
||||||
params=params,
|
|
||||||
timeout=_REQUEST_TIMEOUT_SECONDS,
|
|
||||||
)
|
|
||||||
if resp.status_code == 404:
|
|
||||||
print("Warning: Actions API endpoint not found — check Gitea version/config.", file=sys.stderr)
|
|
||||||
return []
|
|
||||||
_check_response(resp, "fetch runs")
|
|
||||||
return msgspec.json.decode(resp.content, type=GiteaRunsResponse).workflow_runs
|
|
||||||
|
|
||||||
|
|
||||||
def fetch_jobs(
|
|
||||||
session: niquests.Session,
|
|
||||||
api_base: str,
|
|
||||||
owner: str,
|
|
||||||
repo: str,
|
|
||||||
run_id: int,
|
|
||||||
) -> list[GiteaJob]:
|
|
||||||
resp = session.get(
|
|
||||||
f"{api_base}/api/v1/repos/{owner}/{repo}/actions/runs/{run_id}/jobs",
|
|
||||||
timeout=_REQUEST_TIMEOUT_SECONDS,
|
|
||||||
)
|
|
||||||
if resp.status_code == 404:
|
|
||||||
print(f"Warning: run #{run_id} not found.", file=sys.stderr)
|
|
||||||
return []
|
|
||||||
_check_response(resp, f"fetch jobs for run #{run_id}")
|
|
||||||
return msgspec.json.decode(resp.content, type=GiteaJobsResponse).jobs
|
|
||||||
|
|
||||||
|
|
||||||
def fetch_job_log(
|
|
||||||
session: niquests.Session,
|
|
||||||
api_base: str,
|
|
||||||
owner: str,
|
|
||||||
repo: str,
|
|
||||||
job_id: int,
|
|
||||||
tail_lines: int,
|
|
||||||
) -> str:
|
|
||||||
assert tail_lines > 0
|
|
||||||
assert tail_lines <= 500
|
|
||||||
|
|
||||||
resp = session.get(
|
|
||||||
f"{api_base}/api/v1/repos/{owner}/{repo}/actions/jobs/{job_id}/logs",
|
|
||||||
timeout=_REQUEST_TIMEOUT_SECONDS,
|
|
||||||
)
|
|
||||||
if resp.status_code == 403:
|
|
||||||
# Gitea issue #36268: log access may require additional token scopes
|
|
||||||
return "[log unavailable — 403 Forbidden; ensure token has actions read scope (see Gitea issue #36268)]"
|
|
||||||
if resp.status_code == 404:
|
|
||||||
return "[log not found — job may still be running or logs have expired]"
|
|
||||||
_check_response(resp, f"fetch log for job #{job_id}")
|
|
||||||
|
|
||||||
text = _ANSI_ESCAPE.sub("", resp.text)
|
|
||||||
lines = text.splitlines()
|
|
||||||
return "\n".join(lines[-tail_lines:])
|
|
||||||
|
|
||||||
|
|
||||||
def _check_response(resp: niquests.Response, context: str) -> None:
|
|
||||||
if resp.status_code == 401:
|
|
||||||
print(f"Error: 401 Unauthorized ({context}). Check GITEA_TOKEN.", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
if resp.status_code == 403:
|
|
||||||
print(f"Error: 403 Forbidden ({context}). Token may lack required scopes.", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
resp.raise_for_status()
|
|
||||||
|
|
||||||
|
|
||||||
# ── Formatting ────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
|
|
||||||
def format_run_summary(run: GiteaRun) -> str:
|
|
||||||
icon = "[OK] " if run.conclusion == "success" else "[FAIL]"
|
|
||||||
sha = run.head_sha[:7] if run.head_sha else "unknown"
|
|
||||||
duration = _format_duration(run.started_at, run.completed_at)
|
|
||||||
return f"{icon} #{run.id} \"{run.display_title}\" on {run.head_branch} ({sha}) -- {duration}"
|
|
||||||
|
|
||||||
|
|
||||||
def format_job_detail(job: GiteaJob) -> str:
|
|
||||||
icon = " [ok]" if job.conclusion == "success" else "[FAIL]"
|
|
||||||
lines = [f"{icon} {job.name}"]
|
|
||||||
failed_steps = [s.name for s in (job.steps or []) if s.conclusion not in (None, "success", "skipped")]
|
|
||||||
if failed_steps:
|
|
||||||
lines.append(f" Failed steps: {', '.join(failed_steps)}")
|
|
||||||
return "\n".join(lines)
|
|
||||||
|
|
||||||
|
|
||||||
def format_log_block(job_name: str, log_text: str) -> str:
|
|
||||||
return f"--- LOG: {job_name} ---\n{log_text}\n--- END LOG: {job_name} ---"
|
|
||||||
|
|
||||||
|
|
||||||
def _format_duration(started_at: str | None, completed_at: str | None) -> str:
|
|
||||||
if not started_at or not completed_at:
|
|
||||||
return "?"
|
|
||||||
try:
|
|
||||||
start = datetime.fromisoformat(started_at.replace("Z", "+00:00"))
|
|
||||||
end = datetime.fromisoformat(completed_at.replace("Z", "+00:00"))
|
|
||||||
total_seconds = int((end - start).total_seconds())
|
|
||||||
if total_seconds < 0:
|
|
||||||
return "?"
|
|
||||||
minutes, seconds = divmod(total_seconds, 60)
|
|
||||||
return f"{minutes}m{seconds:02d}s"
|
|
||||||
except (ValueError, AttributeError):
|
|
||||||
return "?"
|
|
||||||
|
|
||||||
|
|
||||||
# ── Orchestration ─────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
|
|
||||||
def _list_runs(
|
|
||||||
session: niquests.Session,
|
|
||||||
api_base: str,
|
|
||||||
owner: str,
|
|
||||||
repo: str,
|
|
||||||
limit: int,
|
|
||||||
branch: str,
|
|
||||||
show_logs: bool,
|
|
||||||
tail_lines: int,
|
|
||||||
) -> None:
|
|
||||||
runs = fetch_runs(session, api_base, owner, repo, limit=limit, branch=branch)
|
|
||||||
if not runs:
|
|
||||||
print("No runs found.")
|
|
||||||
return
|
|
||||||
|
|
||||||
print(f"=== CI Pipeline Status (last {len(runs)} runs) ===\n")
|
|
||||||
|
|
||||||
passed = 0
|
|
||||||
failed = 0
|
|
||||||
in_progress = 0
|
|
||||||
|
|
||||||
for run in runs:
|
|
||||||
print(format_run_summary(run))
|
|
||||||
|
|
||||||
if run.conclusion == "success":
|
|
||||||
passed += 1
|
|
||||||
elif run.conclusion is None:
|
|
||||||
in_progress += 1
|
|
||||||
else:
|
|
||||||
failed += 1
|
|
||||||
jobs = fetch_jobs(session, api_base, owner, repo, run.id)
|
|
||||||
failed_jobs = [j for j in jobs if j.conclusion not in ("success", None, "skipped")]
|
|
||||||
if failed_jobs:
|
|
||||||
print(f" Failed jobs: {', '.join(j.name for j in failed_jobs)}")
|
|
||||||
if show_logs:
|
|
||||||
for job in failed_jobs:
|
|
||||||
log = fetch_job_log(session, api_base, owner, repo, job.id, tail_lines)
|
|
||||||
print(f"\n{format_log_block(job.name, log)}")
|
|
||||||
|
|
||||||
parts = [f"{passed} passed", f"{failed} failed"]
|
|
||||||
if in_progress:
|
|
||||||
parts.append(f"{in_progress} in progress")
|
|
||||||
print(f"\nSummary: {', '.join(parts)} out of {len(runs)} runs")
|
|
||||||
|
|
||||||
|
|
||||||
def _inspect_run(
|
|
||||||
session: niquests.Session,
|
|
||||||
api_base: str,
|
|
||||||
owner: str,
|
|
||||||
repo: str,
|
|
||||||
run_id: int,
|
|
||||||
tail_lines: int,
|
|
||||||
) -> None:
|
|
||||||
jobs = fetch_jobs(session, api_base, owner, repo, run_id)
|
|
||||||
if not jobs:
|
|
||||||
print(f"No jobs found for run #{run_id}.")
|
|
||||||
return
|
|
||||||
|
|
||||||
print(f"=== Run #{run_id} — {len(jobs)} job(s) ===\n")
|
|
||||||
for job in jobs:
|
|
||||||
print(format_job_detail(job))
|
|
||||||
|
|
||||||
failed_jobs = [j for j in jobs if j.conclusion not in ("success", None, "skipped")]
|
|
||||||
if not failed_jobs:
|
|
||||||
return
|
|
||||||
|
|
||||||
print(f"\nFetching logs for {len(failed_jobs)} failed job(s)...")
|
|
||||||
for job in failed_jobs:
|
|
||||||
log = fetch_job_log(session, api_base, owner, repo, job.id, tail_lines)
|
|
||||||
print(f"\n{format_log_block(job.name, log)}")
|
|
||||||
|
|
||||||
|
|
||||||
# ── Entry point ───────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
|
||||||
token = os.environ.get("GITEA_TOKEN", "")
|
|
||||||
if not token:
|
|
||||||
print("Error: GITEA_TOKEN environment variable not set.", file=sys.stderr)
|
|
||||||
print("Generate a token: Gitea → Settings → Applications → Access Tokens", file=sys.stderr)
|
|
||||||
print("Required scopes: read:repository + actions read.", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(
|
|
||||||
description="Query Gitea Actions CI pipeline status.",
|
|
||||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
||||||
)
|
|
||||||
parser.add_argument("--limit", type=int, default=5, help="Runs to show (max 20, default 5)")
|
|
||||||
parser.add_argument("--branch", default="", help="Filter by branch name")
|
|
||||||
parser.add_argument("--run-id", type=int, default=0, help="Inspect a specific run (all jobs + logs)")
|
|
||||||
parser.add_argument("--logs", action="store_true", help="Include truncated logs for failed jobs")
|
|
||||||
parser.add_argument("--tail-lines", type=int, default=150, help="Log lines per failed job (max 500, default 150)")
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
assert args.limit > 0, f"--limit must be positive, got {args.limit}"
|
|
||||||
assert args.limit <= 20, f"--limit must be ≤ 20, got {args.limit}"
|
|
||||||
assert args.tail_lines > 0, f"--tail-lines must be positive, got {args.tail_lines}"
|
|
||||||
assert args.tail_lines <= 500, f"--tail-lines must be ≤ 500, got {args.tail_lines}"
|
|
||||||
|
|
||||||
api_base, owner, repo = parse_gitea_remote()
|
|
||||||
|
|
||||||
session = niquests.Session()
|
|
||||||
session.headers["Authorization"] = f"token {token}"
|
|
||||||
|
|
||||||
if args.run_id:
|
|
||||||
_inspect_run(session, api_base, owner, repo, args.run_id, args.tail_lines)
|
|
||||||
else:
|
|
||||||
_list_runs(session, api_base, owner, repo, args.limit, args.branch, args.logs, args.tail_lines)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
49
uv.lock
generated
49
uv.lock
generated
@@ -17,7 +17,6 @@ members = [
|
|||||||
[manifest.dependency-groups]
|
[manifest.dependency-groups]
|
||||||
dev = [
|
dev = [
|
||||||
{ name = "hypothesis", specifier = ">=6.151.6" },
|
{ name = "hypothesis", specifier = ">=6.151.6" },
|
||||||
{ name = "msgspec", specifier = ">=0.20.0" },
|
|
||||||
{ name = "niquests", specifier = ">=3.14.0" },
|
{ name = "niquests", specifier = ">=3.14.0" },
|
||||||
{ name = "playwright", specifier = ">=1.58.0" },
|
{ name = "playwright", specifier = ">=1.58.0" },
|
||||||
{ name = "pytest", specifier = ">=8.0.0" },
|
{ name = "pytest", specifier = ">=8.0.0" },
|
||||||
@@ -1262,54 +1261,6 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/9b/f7/4a5e785ec9fbd65146a27b6b70b6cdc161a66f2024e4b04ac06a67f5578b/mistune-3.2.0-py3-none-any.whl", hash = "sha256:febdc629a3c78616b94393c6580551e0e34cc289987ec6c35ed3f4be42d0eee1", size = 53598, upload-time = "2025-12-23T11:36:33.211Z" },
|
{ url = "https://files.pythonhosted.org/packages/9b/f7/4a5e785ec9fbd65146a27b6b70b6cdc161a66f2024e4b04ac06a67f5578b/mistune-3.2.0-py3-none-any.whl", hash = "sha256:febdc629a3c78616b94393c6580551e0e34cc289987ec6c35ed3f4be42d0eee1", size = 53598, upload-time = "2025-12-23T11:36:33.211Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "msgspec"
|
|
||||||
version = "0.20.0"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/ea/9c/bfbd12955a49180cbd234c5d29ec6f74fe641698f0cd9df154a854fc8a15/msgspec-0.20.0.tar.gz", hash = "sha256:692349e588fde322875f8d3025ac01689fead5901e7fb18d6870a44519d62a29", size = 317862, upload-time = "2025-11-24T03:56:28.934Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/03/59/fdcb3af72f750a8de2bcf39d62ada70b5eb17b06d7f63860e0a679cb656b/msgspec-0.20.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:09e0efbf1ac641fedb1d5496c59507c2f0dc62a052189ee62c763e0aae217520", size = 193345, upload-time = "2025-11-24T03:55:20.613Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/5a/15/3c225610da9f02505d37d69a77f4a2e7daae2a125f99d638df211ba84e59/msgspec-0.20.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:23ee3787142e48f5ee746b2909ce1b76e2949fbe0f97f9f6e70879f06c218b54", size = 186867, upload-time = "2025-11-24T03:55:22.4Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/81/36/13ab0c547e283bf172f45491edfdea0e2cecb26ae61e3a7b1ae6058b326d/msgspec-0.20.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:81f4ac6f0363407ac0465eff5c7d4d18f26870e00674f8fcb336d898a1e36854", size = 215351, upload-time = "2025-11-24T03:55:23.958Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/6b/96/5c095b940de3aa6b43a71ec76275ac3537b21bd45c7499b5a17a429110fa/msgspec-0.20.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bb4d873f24ae18cd1334f4e37a178ed46c9d186437733351267e0a269bdf7e53", size = 219896, upload-time = "2025-11-24T03:55:25.356Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/98/7a/81a7b5f01af300761087b114dafa20fb97aed7184d33aab64d48874eb187/msgspec-0.20.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b92b8334427b8393b520c24ff53b70f326f79acf5f74adb94fd361bcff8a1d4e", size = 220389, upload-time = "2025-11-24T03:55:26.99Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/70/c0/3d0cce27db9a9912421273d49eab79ce01ecd2fed1a2f1b74af9b445f33c/msgspec-0.20.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:562c44b047c05cc0384e006fae7a5e715740215c799429e0d7e3e5adf324285a", size = 223348, upload-time = "2025-11-24T03:55:28.311Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/89/5e/406b7d578926b68790e390d83a1165a9bfc2d95612a1a9c1c4d5c72ea815/msgspec-0.20.0-cp311-cp311-win_amd64.whl", hash = "sha256:d1dcc93a3ce3d3195985bfff18a48274d0b5ffbc96fa1c5b89da6f0d9af81b29", size = 188713, upload-time = "2025-11-24T03:55:29.553Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/47/87/14fe2316624ceedf76a9e94d714d194cbcb699720b210ff189f89ca4efd7/msgspec-0.20.0-cp311-cp311-win_arm64.whl", hash = "sha256:aa387aa330d2e4bd69995f66ea8fdc87099ddeedf6fdb232993c6a67711e7520", size = 174229, upload-time = "2025-11-24T03:55:31.107Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d9/6f/1e25eee957e58e3afb2a44b94fa95e06cebc4c236193ed0de3012fff1e19/msgspec-0.20.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2aba22e2e302e9231e85edc24f27ba1f524d43c223ef5765bd8624c7df9ec0a5", size = 196391, upload-time = "2025-11-24T03:55:32.677Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/7f/ee/af51d090ada641d4b264992a486435ba3ef5b5634bc27e6eb002f71cef7d/msgspec-0.20.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:716284f898ab2547fedd72a93bb940375de9fbfe77538f05779632dc34afdfde", size = 188644, upload-time = "2025-11-24T03:55:33.934Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/49/d6/9709ee093b7742362c2934bfb1bbe791a1e09bed3ea5d8a18ce552fbfd73/msgspec-0.20.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:558ed73315efa51b1538fa8f1d3b22c8c5ff6d9a2a62eff87d25829b94fc5054", size = 218852, upload-time = "2025-11-24T03:55:35.575Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/5c/a2/488517a43ccf5a4b6b6eca6dd4ede0bd82b043d1539dd6bb908a19f8efd3/msgspec-0.20.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:509ac1362a1d53aa66798c9b9fd76872d7faa30fcf89b2fba3bcbfd559d56eb0", size = 224937, upload-time = "2025-11-24T03:55:36.859Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d5/e8/49b832808aa23b85d4f090d1d2e48a4e3834871415031ed7c5fe48723156/msgspec-0.20.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1353c2c93423602e7dea1aa4c92f3391fdfc25ff40e0bacf81d34dbc68adb870", size = 222858, upload-time = "2025-11-24T03:55:38.187Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/9f/56/1dc2fa53685dca9c3f243a6cbecd34e856858354e455b77f47ebd76cf5bf/msgspec-0.20.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cb33b5eb5adb3c33d749684471c6a165468395d7aa02d8867c15103b81e1da3e", size = 227248, upload-time = "2025-11-24T03:55:39.496Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/5a/51/aba940212c23b32eedce752896205912c2668472ed5b205fc33da28a6509/msgspec-0.20.0-cp312-cp312-win_amd64.whl", hash = "sha256:fb1d934e435dd3a2b8cf4bbf47a8757100b4a1cfdc2afdf227541199885cdacb", size = 190024, upload-time = "2025-11-24T03:55:40.829Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/41/ad/3b9f259d94f183daa9764fef33fdc7010f7ecffc29af977044fa47440a83/msgspec-0.20.0-cp312-cp312-win_arm64.whl", hash = "sha256:00648b1e19cf01b2be45444ba9dc961bd4c056ffb15706651e64e5d6ec6197b7", size = 175390, upload-time = "2025-11-24T03:55:42.05Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/8a/d1/b902d38b6e5ba3bdddbec469bba388d647f960aeed7b5b3623a8debe8a76/msgspec-0.20.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9c1ff8db03be7598b50dd4b4a478d6fe93faae3bd54f4f17aa004d0e46c14c46", size = 196463, upload-time = "2025-11-24T03:55:43.405Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/57/b6/eff0305961a1d9447ec2b02f8c73c8946f22564d302a504185b730c9a761/msgspec-0.20.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f6532369ece217fd37c5ebcfd7e981f2615628c21121b7b2df9d3adcf2fd69b8", size = 188650, upload-time = "2025-11-24T03:55:44.761Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/99/93/f2ec1ae1de51d3fdee998a1ede6b2c089453a2ee82b5c1b361ed9095064a/msgspec-0.20.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f9a1697da2f85a751ac3cc6a97fceb8e937fc670947183fb2268edaf4016d1ee", size = 218834, upload-time = "2025-11-24T03:55:46.441Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/28/83/36557b04cfdc317ed8a525c4993b23e43a8fbcddaddd78619112ca07138c/msgspec-0.20.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7fac7e9c92eddcd24c19d9e5f6249760941485dff97802461ae7c995a2450111", size = 224917, upload-time = "2025-11-24T03:55:48.06Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/8f/56/362037a1ed5be0b88aced59272442c4b40065c659700f4b195a7f4d0ac88/msgspec-0.20.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f953a66f2a3eb8d5ea64768445e2bb301d97609db052628c3e1bcb7d87192a9f", size = 222821, upload-time = "2025-11-24T03:55:49.388Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/92/75/fa2370ec341cedf663731ab7042e177b3742645c5dd4f64dc96bd9f18a6b/msgspec-0.20.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:247af0313ae64a066d3aea7ba98840f6681ccbf5c90ba9c7d17f3e39dbba679c", size = 227227, upload-time = "2025-11-24T03:55:51.125Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f1/25/5e8080fe0117f799b1b68008dc29a65862077296b92550632de015128579/msgspec-0.20.0-cp313-cp313-win_amd64.whl", hash = "sha256:67d5e4dfad52832017018d30a462604c80561aa62a9d548fc2bd4e430b66a352", size = 189966, upload-time = "2025-11-24T03:55:52.458Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/79/b6/63363422153937d40e1cb349c5081338401f8529a5a4e216865decd981bf/msgspec-0.20.0-cp313-cp313-win_arm64.whl", hash = "sha256:91a52578226708b63a9a13de287b1ec3ed1123e4a088b198143860c087770458", size = 175378, upload-time = "2025-11-24T03:55:53.721Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/bb/18/62dc13ab0260c7d741dda8dc7f481495b93ac9168cd887dda5929880eef8/msgspec-0.20.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:eead16538db1b3f7ec6e3ed1f6f7c5dec67e90f76e76b610e1ffb5671815633a", size = 196407, upload-time = "2025-11-24T03:55:55.001Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/dd/1d/b9949e4ad6953e9f9a142c7997b2f7390c81e03e93570c7c33caf65d27e1/msgspec-0.20.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:703c3bb47bf47801627fb1438f106adbfa2998fe586696d1324586a375fca238", size = 188889, upload-time = "2025-11-24T03:55:56.311Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/1e/19/f8bb2dc0f1bfe46cc7d2b6b61c5e9b5a46c62298e8f4d03bbe499c926180/msgspec-0.20.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6cdb227dc585fb109305cee0fd304c2896f02af93ecf50a9c84ee54ee67dbb42", size = 219691, upload-time = "2025-11-24T03:55:57.908Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b8/8e/6b17e43f6eb9369d9858ee32c97959fcd515628a1df376af96c11606cf70/msgspec-0.20.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:27d35044dd8818ac1bd0fedb2feb4fbdff4e3508dd7c5d14316a12a2d96a0de0", size = 224918, upload-time = "2025-11-24T03:55:59.322Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/1c/db/0e833a177db1a4484797adba7f429d4242585980b90882cc38709e1b62df/msgspec-0.20.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b4296393a29ee42dd25947981c65506fd4ad39beaf816f614146fa0c5a6c91ae", size = 223436, upload-time = "2025-11-24T03:56:00.716Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c3/30/d2ee787f4c918fd2b123441d49a7707ae9015e0e8e1ab51aa7967a97b90e/msgspec-0.20.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:205fbdadd0d8d861d71c8f3399fe1a82a2caf4467bc8ff9a626df34c12176980", size = 227190, upload-time = "2025-11-24T03:56:02.371Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ff/37/9c4b58ff11d890d788e700b827db2366f4d11b3313bf136780da7017278b/msgspec-0.20.0-cp314-cp314-win_amd64.whl", hash = "sha256:7dfebc94fe7d3feec6bc6c9df4f7e9eccc1160bb5b811fbf3e3a56899e398a6b", size = 193950, upload-time = "2025-11-24T03:56:03.668Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e9/4e/cab707bf2fa57408e2934e5197fc3560079db34a1e3cd2675ff2e47e07de/msgspec-0.20.0-cp314-cp314-win_arm64.whl", hash = "sha256:2ad6ae36e4a602b24b4bf4eaf8ab5a441fec03e1f1b5931beca8ebda68f53fc0", size = 179018, upload-time = "2025-11-24T03:56:05.038Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/4c/06/3da3fc9aaa55618a8f43eb9052453cfe01f82930bca3af8cea63a89f3a11/msgspec-0.20.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:f84703e0e6ef025663dd1de828ca028774797b8155e070e795c548f76dde65d5", size = 200389, upload-time = "2025-11-24T03:56:06.375Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/83/3b/cc4270a5ceab40dfe1d1745856951b0a24fd16ac8539a66ed3004a60c91e/msgspec-0.20.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7c83fc24dd09cf1275934ff300e3951b3adc5573f0657a643515cc16c7dee131", size = 193198, upload-time = "2025-11-24T03:56:07.742Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/cd/ae/4c7905ac53830c8e3c06fdd60e3cdcfedc0bbc993872d1549b84ea21a1bd/msgspec-0.20.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f13ccb1c335a124e80c4562573b9b90f01ea9521a1a87f7576c2e281d547f56", size = 225973, upload-time = "2025-11-24T03:56:09.18Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d9/da/032abac1de4d0678d99eaeadb1323bd9d247f4711c012404ba77ed6f15ca/msgspec-0.20.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:17c2b5ca19f19306fc83c96d85e606d2cc107e0caeea85066b5389f664e04846", size = 229509, upload-time = "2025-11-24T03:56:10.898Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/69/52/fdc7bdb7057a166f309e0b44929e584319e625aaba4771b60912a9321ccd/msgspec-0.20.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d931709355edabf66c2dd1a756b2d658593e79882bc81aae5964969d5a291b63", size = 230434, upload-time = "2025-11-24T03:56:12.48Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/cb/fe/1dfd5f512b26b53043884e4f34710c73e294e7cc54278c3fe28380e42c37/msgspec-0.20.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:565f915d2e540e8a0c93a01ff67f50aebe1f7e22798c6a25873f9fda8d1325f8", size = 231758, upload-time = "2025-11-24T03:56:13.765Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/97/f6/9ba7121b8e0c4e0beee49575d1dbc804e2e72467692f0428cf39ceba1ea5/msgspec-0.20.0-cp314-cp314t-win_amd64.whl", hash = "sha256:726f3e6c3c323f283f6021ebb6c8ccf58d7cd7baa67b93d73bfbe9a15c34ab8d", size = 206540, upload-time = "2025-11-24T03:56:15.029Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c8/3e/c5187de84bb2c2ca334ab163fcacf19a23ebb1d876c837f81a1b324a15bf/msgspec-0.20.0-cp314-cp314t-win_arm64.whl", hash = "sha256:93f23528edc51d9f686808a361728e903d6f2be55c901d6f5c92e44c6d546bfc", size = 183011, upload-time = "2025-11-24T03:56:16.442Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "niquests"
|
name = "niquests"
|
||||||
version = "3.17.0"
|
version = "3.17.0"
|
||||||
|
|||||||
Reference in New Issue
Block a user