diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index eb6c36425a..60a1e5601c 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -175,6 +175,11 @@ jobs: needs: detect uses: ./.github/workflows/profile-artifact-check.yml + icons-freshness-check: + name: Icon assets freshness + needs: detect + uses: ./.github/workflows/icons-freshness-check.yml + case-collision-check: name: Check no case-colliding filenames needs: detect @@ -247,6 +252,7 @@ jobs: - lockfile-diff - docker-lint - profile-artifact-check + - icons-freshness-check - supply-chain - review-labels - osv-scanner diff --git a/.github/workflows/icons-freshness-check.yml b/.github/workflows/icons-freshness-check.yml new file mode 100644 index 0000000000..6e497e864e --- /dev/null +++ b/.github/workflows/icons-freshness-check.yml @@ -0,0 +1,45 @@ +name: Icon Assets Freshness + +# Guard for the single-master icon pipeline. assets/icon-master.svg is the +# source of truth for all 23 generated icons (see scripts/generate_icons.py); +# this check regenerates every target and byte-compares against the committed +# files. The repo can never hold a hand-edited or stale generated icon, and a +# master change can never land without its regenerated outputs. +# +# Byte-determinism note: resvg renders the same SVG to identical bytes on any +# host for pure-shape artwork. If the master ever contains , font +# rendering can differ across OSes — pin fonts via resvg's font_files/font_dirs +# and commit a known font before relying on this check for that master. + +on: + workflow_call: + +permissions: + contents: read + +jobs: + check: + name: Generated icons match master + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Install uv + uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # 8.2.0 + with: + # Pinned: unpinned setup-uv fetches a 'latest' manifest from + # raw.githubusercontent.com every job; transient fetch failures + # fail the job (2026-07-28 incident). Keep in sync with tests.yml. + version: "0.9.28" + enable-cache: true + cache-dependency-glob: | + pyproject.toml + uv.lock + + # Pillow is a core dependency; resvg-py rides the dev extra. + - name: Install locked deps + run: uv sync --locked --python 3.11 --extra dev + + - name: Verify generated icons are in sync with the master + run: uv run python scripts/generate_icons.py --check diff --git a/assets/icon-master.svg b/assets/icon-master.svg new file mode 100644 index 0000000000..da1c367d2b --- /dev/null +++ b/assets/icon-master.svg @@ -0,0 +1,4 @@ + + + + diff --git a/pyproject.toml b/pyproject.toml index 4d98fe6ba5..0610129e2e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -199,7 +199,7 @@ modal = ["modal==1.3.4"] daytona = ["daytona==0.155.0"] vercel = ["vercel==0.7.2"] hindsight = ["hindsight-client==0.6.1"] -dev = ["debugpy==1.8.20", "pytest==9.1.1", "pytest-asyncio==1.3.0", "mcp==2.0.0", "httpx2==2.7.0", "starlette==1.3.1", "ty==0.0.21", "ruff==0.15.10", "setuptools==83.0.0"] # starlette: CVE-2026-48710; setuptools: 83 (torch >=2.13 requires setuptools 83) +dev = ["debugpy==1.8.20", "pytest==9.1.1", "pytest-asyncio==1.3.0", "mcp==2.0.0", "httpx2==2.7.0", "starlette==1.3.1", "ty==0.0.21", "ruff==0.15.10", "setuptools==83.0.0", "resvg-py==0.4.0"] # starlette: CVE-2026-48710; setuptools: 83 (torch >=2.13 requires setuptools 83) messaging = ["python-telegram-bot[webhooks]==22.8", "discord.py[voice]==2.7.1", "aiohttp==3.14.3", "brotlicffi==1.2.0.1", "slack-bolt==1.30.0", "slack-sdk==3.43.0", "qrcode==7.4.2"] # aiohttp 3.14.3: prior CVEs + GHSA-cq5v-8q36-5273/GHSA-mfx4-hv73-q22v/GHSA-mq44-7p77-q5h7 cron = [] # croniter is now a core dependency; this extra kept for back-compat slack = ["slack-bolt==1.30.0", "slack-sdk==3.43.0", "aiohttp==3.14.3"] diff --git a/scripts/generate_icons.py b/scripts/generate_icons.py new file mode 100644 index 0000000000..4010502993 --- /dev/null +++ b/scripts/generate_icons.py @@ -0,0 +1,272 @@ +#!/usr/bin/env python3 +"""Generate every app icon in the repo from a single master SVG. + +Usage (from repo root): + .venv/Scripts/python.exe scripts/generate_icons.py # write + .venv/Scripts/python.exe scripts/generate_icons.py --check # verify only + +Master: assets/icon-master.svg + A 1024x1024 SVG. The background must be a WHITE ROUNDED SQUARE + (rx ~ 245, spanning the full canvas) with the artwork on top — + the transparent corners outside the squircle are what give the + icons their app-icon shape everywhere downstream. If the master + is missing, a red-circle placeholder is written automatically; + drop in the real artwork and re-run. + +--check mode regenerates every target in memory and byte-compares it +against the committed file, exiting nonzero on any drift. This is what +CI runs (see .github/workflows/icons-freshness-check.yml) so the repo +can never hold a hand-edited or stale generated icon. + +Rendering: resvg (resvg-py) for SVG -> PNG fidelity at every size. +Containers: Pillow for multi-size .ico and .icns. + +Deps: + Pillow (core dependency), resvg-py (dev extra): + uv sync --extra dev + +Outputs (23 files, all measured against the originals): + apps/desktop/assets/icon.png 1024x1024 squircle + apps/desktop/assets/icon.ico 16,24,32,48,64,128,256 + apps/desktop/assets/icon.icns 16..1024 (real ICNS) + apps/desktop/assets/appx/Wide310x150Logo.png 310x150, squircle 100 centered + apps/desktop/assets/appx/StoreLogo.png 50x50 squircle + apps/desktop/assets/appx/Square44x44Logo.png 44x44 squircle + apps/desktop/assets/appx/Square150x150Logo.png 150x150 squircle + apps/desktop/public/apple-touch-icon.png 1024x1024 squircle + apps/desktop/public/nous-girl.jpg 256x256 JPEG on white + apps/bootstrap-installer/src-tauri/icons/32x32.png 32x32 + apps/bootstrap-installer/src-tauri/icons/128x128.png 128x128 + apps/bootstrap-installer/src-tauri/icons/128x128@2x.png 256x256 + apps/bootstrap-installer/src-tauri/icons/icon.ico 16,32,64,128,256 + apps/bootstrap-installer/src-tauri/icons/icon.icns 16..1024 + apps/bootstrap-installer/public/nous-girl.jpg 256x256 JPEG on white + website/static/img/logo.png 1772x1799 black-frame wordmark + website/static/img/nous-logo.png 150x150 on white (opaque) + website/static/img/favicon-16x16.png 16x16 + website/static/img/favicon-32x32.png 32x32 + website/static/img/apple-touch-icon.png 180x180 + website/static/img/favicon.ico 16,32,48 + website/static/img/favicon.svg copy of the master + web/public/favicon.ico 16,32,48 +""" + +from __future__ import annotations + +import io +import shutil +import sys +from pathlib import Path + +from PIL import Image + +try: + import resvg_py +except ImportError: + sys.exit( + "resvg-py is missing. Install it with:\n" + " uv sync --extra dev" + ) + +ROOT = Path(__file__).resolve().parent.parent +MASTER = ROOT / "assets" / "icon-master.svg" + +# The squircle corner radius measured on the real 1024 icon (~245px). +SQUIRCLE_RADIUS = 245 + +PLACEHOLDER = f''' + + + +''' + +# (relpath, kind, arg) +TARGETS: list[tuple[str, str, object]] = [ + ("apps/desktop/assets/icon.png", "png", 1024), + ("apps/desktop/assets/icon.ico", "ico", [16, 24, 32, 48, 64, 128, 256]), + ("apps/desktop/assets/icon.icns", "icns", None), + ("apps/desktop/assets/appx/Wide310x150Logo.png", "wide", (310, 150)), + ("apps/desktop/assets/appx/StoreLogo.png", "png", 50), + ("apps/desktop/assets/appx/Square44x44Logo.png", "png", 44), + ("apps/desktop/assets/appx/Square150x150Logo.png", "png", 150), + ("apps/desktop/public/apple-touch-icon.png", "png", 1024), + ("apps/desktop/public/nous-girl.jpg", "jpg_white", 256), + ("apps/bootstrap-installer/src-tauri/icons/32x32.png", "png", 32), + ("apps/bootstrap-installer/src-tauri/icons/128x128.png", "png", 128), + ("apps/bootstrap-installer/src-tauri/icons/128x128@2x.png", "png", 256), + ("apps/bootstrap-installer/src-tauri/icons/icon.ico", "ico", [16, 32, 64, 128, 256]), + ("apps/bootstrap-installer/src-tauri/icons/icon.icns", "icns", None), + ("apps/bootstrap-installer/public/nous-girl.jpg", "jpg_white", 256), + ("website/static/img/logo.png", "logo", None), + ("website/static/img/nous-logo.png", "png_white", 150), + ("website/static/img/favicon-16x16.png", "png", 16), + ("website/static/img/favicon-32x32.png", "png", 32), + ("website/static/img/apple-touch-icon.png", "png", 180), + ("website/static/img/favicon.ico", "ico", [16, 32, 48]), + ("website/static/img/favicon.svg", "svg_copy", None), + ("web/public/favicon.ico", "ico", [16, 32, 48]), +] + + +def ensure_master(check: bool = False) -> Path: + if not MASTER.exists(): + if check: + sys.exit( + f"[check] {MASTER.relative_to(ROOT)} is missing — " + "cannot verify generated icons without a master." + ) + MASTER.write_text(PLACEHOLDER, encoding="utf-8") + print(f"[master] wrote placeholder: {MASTER.relative_to(ROOT)}") + return MASTER + + +def render(size: int, *, background: str | None = None) -> Image.Image: + """Render the master to an RGBA PNG of `size`x`size`.""" + data = resvg_py.svg_to_bytes( + svg_path=str(MASTER), width=size, height=size, background=background + ) + return Image.open(io.BytesIO(data)).convert("RGBA") + + +def paste_centered(canvas: Image.Image, img: Image.Image) -> None: + x = (canvas.width - img.width) // 2 + y = (canvas.height - img.height) // 2 + canvas.alpha_composite(img, (x, y)) + + +def build_logo_image() -> Image.Image: + """1772x1799: black frame (~14-17px) + white interior + art at ~1.7x. + + Measured from the real logo.png: border L/R 14, T 17, B 16; white + interior spans (15,18)..(1758,1784) = 1743x1766. + """ + from PIL import ImageDraw + + W, H = 1772, 1799 + art_w, art_h = 1743, 1766 + border = 14 + top, bottom = 17, 16 + canvas = Image.new("RGBA", (W, H), (0, 0, 0, 0)) + + draw = ImageDraw.Draw(canvas) + draw.rounded_rectangle((0, 0, W - 1, H - 1), radius=2, fill=(0, 0, 0, 255)) + draw.rectangle( + (border, top, W - 1 - border, H - 1 - bottom), + fill=(255, 255, 255, 255), + ) + + # Art: render the master square at ~1.7x, tiny vertical stretch to fill. + art = render(art_w).resize((art_w, art_h), Image.LANCZOS) + canvas.alpha_composite(art, (border, top)) + return canvas + + +def target_bytes(kind: str, arg: object) -> bytes: + """Produce the exact bytes for one target. Shared by write + check.""" + if kind == "svg_copy": + return MASTER.read_bytes() + + buf = io.BytesIO() + if kind == "png": + render(arg).save(buf, "PNG") + elif kind == "png_white": + render(arg, background="#ffffff").save(buf, "PNG") + elif kind == "jpg_white": + # JPEG has no alpha: flatten the squircle onto white. + render(arg, background="#ffffff").convert("RGB").save(buf, "JPEG", quality=90) + elif kind == "ico": + img = render(max(arg)) + img.save(buf, format="ICO", sizes=[(s, s) for s in arg]) + elif kind == "icns": + img = render(1024) + frames = [img.resize((s, s), Image.LANCZOS) for s in (16, 32, 64, 128, 256, 512, 1024)] + img.save(buf, format="ICNS", append_images=frames[1:]) + elif kind == "wide": + w, h = arg + canvas = Image.new("RGBA", (w, h), (0, 0, 0, 0)) + paste_centered(canvas, render(100)) + canvas.save(buf, "PNG") + elif kind == "logo": + build_logo_image().save(buf, "PNG") + else: + raise ValueError(f"unknown kind {kind!r}") + return buf.getvalue() + + +def cmd_write() -> None: + ensure_master() + written = 0 + for rel, kind, arg in TARGETS: + path = ROOT / rel + path.parent.mkdir(parents=True, exist_ok=True) + try: + path.write_bytes(target_bytes(kind, arg)) + written += 1 + except Exception as exc: # noqa: BLE001 - report and keep going + print(f" !! {rel}: FAILED ({exc})") + print(f"[ok] wrote {written}/{len(TARGETS)} files from {MASTER.name}") + + print("\n[verify]") + for rel, kind, arg in TARGETS: + path = ROOT / rel + try: + if kind == "svg_copy": + print(f" {rel}: {path.stat().st_size} bytes SVG") + continue + im = Image.open(path) + if kind == "ico": + sizes = [] + try: + for i in range(im.n_frames): + im.seek(i) + sizes.append(im.size) + except Exception: + sizes = [im.size] + print(f" {rel}: ICO {sorted(set(sizes))}") + elif kind == "icns": + print(f" {rel}: ICNS {im.size} (container)") + else: + print(f" {rel}: {im.format} {im.size}") + except Exception as exc: + print(f" {rel}: VERIFY FAILED ({exc})") + + +def cmd_check() -> int: + ensure_master(check=True) + drifted: list[str] = [] + for rel, kind, arg in TARGETS: + path = ROOT / rel + if not path.exists(): + drifted.append(f"{rel}: MISSING (expected generated file)") + continue + try: + expected = target_bytes(kind, arg) + except Exception as exc: # noqa: BLE001 + drifted.append(f"{rel}: REGENERATE FAILED ({exc})") + continue + actual = path.read_bytes() + if actual != expected: + drifted.append(f"{rel}: drift ({len(actual)} bytes on disk vs {len(expected)} generated)") + + if not drifted: + print(f"[ok] all {len(TARGETS)} generated icons match {MASTER.name}") + return 0 + + print(f"[check] {len(drifted)} file(s) out of sync with {MASTER.name}:") + for line in drifted: + print(f" - {line}") + print( + "\nFix: run `.venv/Scripts/python.exe scripts/generate_icons.py` " + "and commit the regenerated files." + ) + return 1 + + +def main() -> None: + if "--check" in sys.argv: + sys.exit(cmd_check()) + cmd_write() + + +if __name__ == "__main__": + main() diff --git a/uv.lock b/uv.lock index 5f663dffb0..48a2b9618e 100644 --- a/uv.lock +++ b/uv.lock @@ -12,105 +12,105 @@ exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for exclude-newer-span = "P14D" [options.exclude-newer-package] -agent-client-protocol = false -ai-edge-litert = false -aiohttp = false -aiohttp-socks = false -aiosqlite = false -alibabacloud-dingtalk = false -anthropic = false -asyncpg = false -azure-identity = false -boto3 = false -brotlicffi = false -certifi = false -concurrent-log-handler = false -croniter = false -cryptography = false -daytona = false -debugpy = false -defusedxml = false -dingtalk-stream = false -discord-py = false -edge-tts = false elevenlabs = false -exa-py = false -fal-client = false -fastapi = false -faster-whisper = false -fire = false -firecrawl-anydoc = false -firecrawl-py = false -google-api-python-client = false +setuptools = false +modal = false +pyyaml = false +maturin = false +numpy = false +pvporcupine = false google-auth = false -google-auth-httplib2 = false -google-auth-oauthlib = false +aiohttp = false +starlette = false +mistralai = false +firecrawl-py = false +parallel-web = false +markdown = false +opentelemetry-sdk = false +prompt-toolkit = false +fastapi = false +anthropic = false +pathspec = false +openwakeword = false +defusedxml = false +ruff = false +exa-py = false +mcp = false +python-olm = false +wheel = false +websockets = false +pytest = false +pillow = false +setuptools-rust = false +tenacity = false +pyjwt = false +slack-bolt = false +fal-client = false +asyncpg = false +boto3 = false +ruamel-yaml = false +sherpa-onnx = false +huggingface-hub = false +pytest-asyncio = false google-cloud-pubsub = false -h2 = false -hindsight-client = false +slack-sdk = false +discord-py = false honcho-ai = false +faster-whisper = false +certifi = false +debugpy = false +packaging = false +rich = false +supermemory = false +unpaddedbase64 = false +h2 = false +youtube-transcript-api = false +edge-tts = false +microsoft-teams-apps = false +aiohttp-socks = false +ai-edge-litert = false +onnxruntime = false +croniter = false +opentelemetry-exporter-otlp-proto-http = false +azure-identity = false +lark-oapi = false +sentencepiece = false +firecrawl-anydoc = false +google-api-python-client = false +brotlicffi = false +mem0ai = false +qrcode = false +vercel = false +aiosqlite = false +tzdata = false +python-dotenv = false +daytona = false +uvicorn = false +nemo-relay = false +requests = false httplib2 = false httpx = false -httpx2 = false -huggingface_hub = false -jinja2 = false -lark-oapi = false -markdown = false -maturin = false -mautrix = false -mcp = false -mem0ai = false -microsoft-teams-apps = false -mistralai = false -modal = false -nemo-relay = false -numpy = false -onnxruntime = false -openai = false -opentelemetry-exporter-otlp-proto-http = false -opentelemetry-sdk = false -openwakeword = false -packaging = false -parallel-web = false -pathspec = false -pillow = false -prompt-toolkit = false -psutil = false -pvporcupine = false pyasn1 = false -pydantic = false -pyjwt = false -pytest = false -pytest-asyncio = false -python-dotenv = false -python-multipart = false -python-olm = false -python-telegram-bot = false -pyyaml = false -qrcode = false -requests = false -rich = false -ruamel-yaml = false -ruff = false -sentencepiece = false -setuptools = false -setuptools-rust = false -sherpa-onnx = false -slack-bolt = false -slack-sdk = false snowballstemmer = false -sounddevice = false -starlette = false -supermemory = false -tenacity = false +concurrent-log-handler = false +cryptography = false +pydantic = false +agent-client-protocol = false +fire = false +httpx2 = false +google-auth-oauthlib = false +python-multipart = false ty = false -tzdata = false -unpaddedbase64 = false -uvicorn = false -vercel = false -websockets = false -wheel = false -youtube-transcript-api = false +openai = false +mautrix = false +google-auth-httplib2 = false +python-telegram-bot = false +psutil = false +jinja2 = false +hindsight-client = false +sounddevice = false +alibabacloud-dingtalk = false +dingtalk-stream = false [manifest] overrides = [ @@ -1805,6 +1805,7 @@ dev = [ { name = "mcp" }, { name = "pytest" }, { name = "pytest-asyncio" }, + { name = "resvg-py" }, { name = "ruff" }, { name = "setuptools" }, { name = "starlette" }, @@ -2121,6 +2122,7 @@ requires-dist = [ { name = "qrcode", marker = "extra == 'feishu'", specifier = "==7.4.2" }, { name = "qrcode", marker = "extra == 'messaging'", specifier = "==7.4.2" }, { name = "requests", specifier = "==2.33.0" }, + { name = "resvg-py", marker = "extra == 'dev'", specifier = "==0.4.0" }, { name = "rich", specifier = "==14.3.3" }, { name = "ruamel-yaml", specifier = "==0.18.17" }, { name = "ruff", marker = "extra == 'dev'", specifier = "==0.15.10" }, @@ -4046,6 +4048,56 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06", size = 54481, upload-time = "2023-05-01T04:11:28.427Z" }, ] +[[package]] +name = "resvg-py" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2e/4e/26c3b88f8d2c2ef96c2d9bf48823f484a91275011c58cbeddbc98b32a0cd/resvg_py-0.4.0.tar.gz", hash = "sha256:7b93183f2179bb23411e8451dc6201ad3bceed9b7e35dca2107faf6d8b157c43", size = 1747412, upload-time = "2026-08-12T16:01:33.882Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/49/df4bd6106527281ef72b3916cb949ca760e170b7a01b84a36ecf1c85eac5/resvg_py-0.4.0-cp310-abi3-android_24_arm64_v8a.whl", hash = "sha256:7302bb18376c27b055f692c97b9e73f716bfb0e628715b8eb49b78bdf8489e8b", size = 1307910, upload-time = "2026-08-12T16:00:24.957Z" }, + { url = "https://files.pythonhosted.org/packages/7e/74/ef6cfea97f5774dfe3dc189134932e43bf6c0f0f34036145e847e97be7b9/resvg_py-0.4.0-cp310-abi3-android_24_x86_64.whl", hash = "sha256:22dc18024ec4b3dbeec4c144c299a3ea829726ca1ff666b1a0bb79c674c92ffd", size = 1313463, upload-time = "2026-08-12T16:00:26.663Z" }, + { url = "https://files.pythonhosted.org/packages/63/ba/46e9eb4da8b85b4a3615025414022773d0b65abf886be8fc69e8b0e7ab31/resvg_py-0.4.0-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:facd4639edf0cbd800adee3c0f20583331f47f1bf5921953c8564c74081b79bb", size = 1202090, upload-time = "2026-08-12T16:00:27.805Z" }, + { url = "https://files.pythonhosted.org/packages/ec/b1/0456a51ee69d40ffdbd49f4a953b7eb93f09eabbd64c1ff4124464270fb6/resvg_py-0.4.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:663f1e77fef38354afa28214cbe2f9512a317b66aae1b5571624f6a9c820f070", size = 1148361, upload-time = "2026-08-12T16:00:28.863Z" }, + { url = "https://files.pythonhosted.org/packages/c1/e1/49f08214fa4df8b24e009c70b373067d702d5b5f9cb5b03757e6b0ca5f65/resvg_py-0.4.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b1675fef0c5c28e3981082d51e7402bc7d47358b75e8e65f8be1f533d891b15", size = 1344871, upload-time = "2026-08-12T16:00:29.921Z" }, + { url = "https://files.pythonhosted.org/packages/0e/e3/2fa8f7373d9fc08ee296522d553e72bf18d41f0813be6a6b12d637d7e711/resvg_py-0.4.0-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0da2aa6e580ff6d9a984da96faeab3e08d7e5328a0e0a93a9eb1563e5143ab1d", size = 1268440, upload-time = "2026-08-12T16:00:31.406Z" }, + { url = "https://files.pythonhosted.org/packages/98/dc/e76e0b870bcb543ed109dfba1893757d1ef690150c3a57928720c244f14f/resvg_py-0.4.0-cp310-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:47b8b3e65d8abb48bdd3a4a1442abd1b7ba29a3e2783a7b772fe118c10b5f994", size = 1443392, upload-time = "2026-08-12T16:00:32.483Z" }, + { url = "https://files.pythonhosted.org/packages/bd/c5/4a0f0e954faf36e31f7abab2420ef82c7b0ff3c7165e5772403f6a593937/resvg_py-0.4.0-cp310-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:055dc6e2da744af6cc977f78cbf8ac1cdb544ef1fc9f638dd3da86100ff6f04e", size = 1398857, upload-time = "2026-08-12T16:00:33.713Z" }, + { url = "https://files.pythonhosted.org/packages/ca/99/058b1188f980116787b9c189f1bc15f4000b8148665f22fbd4c619d62ffb/resvg_py-0.4.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa319c50a1cf11ec868faff88b06233cc4e6959251657cbc809a90e459aabb05", size = 1345682, upload-time = "2026-08-12T16:00:34.853Z" }, + { url = "https://files.pythonhosted.org/packages/19/45/5f68775c5e9d603d417f1753fe076a44b4cfe4774bf7b31911671ba56e13/resvg_py-0.4.0-cp310-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7675c10e7fcb282186b9e167ee29d4fbfa75af17ba3892df185fd387c6216878", size = 1426219, upload-time = "2026-08-12T16:00:36.494Z" }, + { url = "https://files.pythonhosted.org/packages/a5/eb/e8e824c32c9257b70745a160569f56fa964c75df55f45ecf4e8e8e66f3dd/resvg_py-0.4.0-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c50ed47c2d16cd8cee9f2692523f501d24b431a0ec654ad9137c66805738d564", size = 1523681, upload-time = "2026-08-12T16:00:37.82Z" }, + { url = "https://files.pythonhosted.org/packages/26/54/9eeda766ac87452524d7f6b2bccef8b0e245277e95c9501280aa7df61fc6/resvg_py-0.4.0-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:4c6456670ea732611e5ff1019544f09e1f09fa0e90a6fc35b4e96b3c22943b4c", size = 1545125, upload-time = "2026-08-12T16:00:38.949Z" }, + { url = "https://files.pythonhosted.org/packages/e0/e9/04c66e1fbd7fb75e2c28e504652632ae2d8d1880c96d52b9811163b13a8b/resvg_py-0.4.0-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:74c1ceea6cfa29b79992834aa3a238600d399f00aeb5df5d02289fd1f4560b96", size = 1588426, upload-time = "2026-08-12T16:00:40.227Z" }, + { url = "https://files.pythonhosted.org/packages/45/5f/6b5a57f796b83a45b97d8f6ebdf0ac7e200e8d33851a48ddf9a5eb07e46f/resvg_py-0.4.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3274d9e168b22dc5c3241619965a64c2d1451685871002329b707b2aeb397b9b", size = 1562330, upload-time = "2026-08-12T16:00:41.3Z" }, + { url = "https://files.pythonhosted.org/packages/60/57/5863b4d6a257f9033f1d187fd921553c4ed2e7f0e71c0d4d0ac21f48b82c/resvg_py-0.4.0-cp310-abi3-win32.whl", hash = "sha256:fee3af4e045049a7da46e7e40ca3cd97e5ea2e5dc66aa9f9769d73d69d300904", size = 1125848, upload-time = "2026-08-12T16:00:42.306Z" }, + { url = "https://files.pythonhosted.org/packages/43/7f/b8f825cab25a132c0119b5ad2110dd0dfa6e7e110afae3affb6cfb3b5a0e/resvg_py-0.4.0-cp310-abi3-win_amd64.whl", hash = "sha256:e889d57d51ac913c23e8d1843a29f0356a473519983959b34c6177698e4a3aef", size = 1144555, upload-time = "2026-08-12T16:00:43.574Z" }, + { url = "https://files.pythonhosted.org/packages/66/80/ad5b1afeea46401c36c6a016dba7e8b05d0a38e7a1a2ddf3129416bc05d1/resvg_py-0.4.0-cp310-abi3-win_arm64.whl", hash = "sha256:81b837f1915b352aa9b2663730331c7e7c675880b5fdad341d14046ba9a3b4af", size = 1063229, upload-time = "2026-08-12T16:00:44.841Z" }, + { url = "https://files.pythonhosted.org/packages/92/c5/4b82e20ba90d310bee2657ee283cf3d7e3e59b42f28b42343c5049ec2485/resvg_py-0.4.0-cp315-abi3.abi3t-macosx_10_12_x86_64.whl", hash = "sha256:f28aef8ebb639a668402c8be3ca4b818f2a2b105a509682a231e0c95d3eaa2bf", size = 1201497, upload-time = "2026-08-12T16:01:03.949Z" }, + { url = "https://files.pythonhosted.org/packages/39/7a/00ee34bafff95fea7e8858ea7ceedee797d3ee7512a56e7673262dfd9057/resvg_py-0.4.0-cp315-abi3.abi3t-macosx_11_0_arm64.whl", hash = "sha256:f48e42cfea42aea01edb83125499e17059ef3539b1f6ab950d4d00480ad7011b", size = 1147730, upload-time = "2026-08-12T16:01:05.015Z" }, + { url = "https://files.pythonhosted.org/packages/64/a9/c22f254b165b8f7a7b36e494d4e60f75ff93c0eceeeab199c6d7f836f6ff/resvg_py-0.4.0-cp315-abi3.abi3t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bff774a5cf4ff0d5554bfb785d864cb38fa4531759672527af13e50bd6065c88", size = 1344020, upload-time = "2026-08-12T16:01:06.106Z" }, + { url = "https://files.pythonhosted.org/packages/0f/5d/152241a069573f389a60aeb07757fe256ec2f0aa91936b69d352bb0148e0/resvg_py-0.4.0-cp315-abi3.abi3t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ec75269c60761d4d2cccb8f80649a2ee9015806bb3b339421ae71b22e3a3c8d8", size = 1267463, upload-time = "2026-08-12T16:01:07.435Z" }, + { url = "https://files.pythonhosted.org/packages/14/7a/05a223f6e7a7535c234728b52138a35711831e02a4982533bb404917bb98/resvg_py-0.4.0-cp315-abi3.abi3t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:830d87b2a512c0355219e4deff5086da6299e01591fe068aeaa5d1a06c7b2450", size = 1441631, upload-time = "2026-08-12T16:01:08.756Z" }, + { url = "https://files.pythonhosted.org/packages/a0/35/930ee65c2a1f7fe0ec17d4ba2707efddfd770b1f5769f78c3cdb5a8b07f0/resvg_py-0.4.0-cp315-abi3.abi3t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8fbd0846f594d208f8b28df5adfe0cf5a1d2a0e365d79fa9abd694a7b0e16fe9", size = 1398417, upload-time = "2026-08-12T16:01:09.961Z" }, + { url = "https://files.pythonhosted.org/packages/c2/af/b04b9bbc8195f5c81d4e79fc14a69acd4da9ef38dbb496d42d86cd4c8b71/resvg_py-0.4.0-cp315-abi3.abi3t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:586e6ce8e6d368e34e00e705daca3780d506334432940a5fb3695c4e3215cbef", size = 1344689, upload-time = "2026-08-12T16:01:11.095Z" }, + { url = "https://files.pythonhosted.org/packages/8f/d4/f2a15ac2c4324cab29234e6da98fe54c2c2229f5e55a2fda6fa0e502caee/resvg_py-0.4.0-cp315-abi3.abi3t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4256a7283bf827442dab20b3614dd9f98c4faf9321389d68478f51f81940d278", size = 1426312, upload-time = "2026-08-12T16:01:12.231Z" }, + { url = "https://files.pythonhosted.org/packages/0f/6a/9068dbffee179f1b378ec5b4906edbc5c8feb6afcd5215886640d445b676/resvg_py-0.4.0-cp315-abi3.abi3t-musllinux_1_2_aarch64.whl", hash = "sha256:407de28dc272b48942fd7820f3dc9c64266ac7a32603763114767196a3ca8a27", size = 1522267, upload-time = "2026-08-12T16:01:13.546Z" }, + { url = "https://files.pythonhosted.org/packages/75/2e/6c4662168aad28b13398da4d1292c86aa34e4fe814dabf5eeb00bb3229b7/resvg_py-0.4.0-cp315-abi3.abi3t-musllinux_1_2_armv7l.whl", hash = "sha256:3614654205efbf516f3c5eaab619f931dce6a08bd41beb8bb9a786ff15bcc1c3", size = 1544578, upload-time = "2026-08-12T16:01:15.357Z" }, + { url = "https://files.pythonhosted.org/packages/61/93/1b9b50f3a72d36b2f8d454874fef1707c60495fa9a605a518feba2da122e/resvg_py-0.4.0-cp315-abi3.abi3t-musllinux_1_2_i686.whl", hash = "sha256:ea9f555f0b5212c7c773b6fdb0597d5ab95983213d93c1753b1f7929c371d542", size = 1588383, upload-time = "2026-08-12T16:01:16.457Z" }, + { url = "https://files.pythonhosted.org/packages/20/a8/0681fe9835bc7c788b237d39c5172ef51ce4efb3f78ee91fef93116d6e09/resvg_py-0.4.0-cp315-abi3.abi3t-musllinux_1_2_x86_64.whl", hash = "sha256:a811975def9a8a643170ae2271b67232a99a438d74d08a6e7e0665f6eb753159", size = 1561365, upload-time = "2026-08-12T16:01:17.588Z" }, + { url = "https://files.pythonhosted.org/packages/0d/05/3c327408a93ccb76cadccb02eee5afdd1b89988bd5dffe0c4bc53f47a1b9/resvg_py-0.4.0-cp315-abi3.abi3t-win32.whl", hash = "sha256:dcd45cb27bbe955cf9b3c7b6d3a4d2a4ba43b9761f7248daba04f42d3f88d49f", size = 1125887, upload-time = "2026-08-12T16:01:18.719Z" }, + { url = "https://files.pythonhosted.org/packages/52/f6/6f1e358cb266ab9867733d905223c0f419d7db63e980576ab0e9853b466d/resvg_py-0.4.0-cp315-abi3.abi3t-win_amd64.whl", hash = "sha256:a83beeebc7ed1b707bcebb4113c247ecb944551203c910aa2b1709e4498b68d3", size = 1143340, upload-time = "2026-08-12T16:01:20.074Z" }, + { url = "https://files.pythonhosted.org/packages/c1/cd/0ee87a5ddc6f320dc827bd5e2749dfa081e8c8ddda22ea7bbb1f561e1716/resvg_py-0.4.0-cp315-abi3.abi3t-win_arm64.whl", hash = "sha256:25f81ea0c2b0a5edf90eec5b662933d97b89abeb62019bd5eaee37aca327debe", size = 1061705, upload-time = "2026-08-12T16:01:21.151Z" }, + { url = "https://files.pythonhosted.org/packages/d5/e9/9c90d2488f9e579a2ffc743704bc35223a451c48bae912e4b527475a3756/resvg_py-0.4.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d389cc4b21ca81113b8fe828a630d66f11ffa1dc559af2ef41798de88a443c4a", size = 1345594, upload-time = "2026-08-12T16:01:22.309Z" }, + { url = "https://files.pythonhosted.org/packages/7c/0b/f746632146d954626e1fecdc3f4028f91586a4f73a3d5b6339f5563e1000/resvg_py-0.4.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f5f7c60ac7c43eb46645e28f5f9a64b5795ce59be362d719119d843808761969", size = 1268619, upload-time = "2026-08-12T16:01:23.362Z" }, + { url = "https://files.pythonhosted.org/packages/07/41/b70597b88597343f522600a911c13cb9a20c5f8a8e415f175d1ac4f2b85a/resvg_py-0.4.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:82425e89f8ff18d077f7dfb92bc86b469e6671ac48aebd5736f9b01c77ee0808", size = 1444001, upload-time = "2026-08-12T16:01:24.476Z" }, + { url = "https://files.pythonhosted.org/packages/15/7f/6741146cd45e421fdff04f736e0cb2b79c63845b717654fe636d9193c1e4/resvg_py-0.4.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ba433618a078685e7d7058a9f1db631a5608666e5d9d558a94f5292a5f561548", size = 1399164, upload-time = "2026-08-12T16:01:25.544Z" }, + { url = "https://files.pythonhosted.org/packages/83/e1/49eec6a6faadc9f5ac5dc9efc0cdfc82028ae92e156b964a9c98b5fbf8c5/resvg_py-0.4.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:006123008d602f20948ae4048b4ace5665bf71d48333d4d3f9e6fac21d7fd277", size = 1345949, upload-time = "2026-08-12T16:01:26.661Z" }, + { url = "https://files.pythonhosted.org/packages/05/a6/bcc4db3e6e22a2e6351b35be9ad4e33db968ee2a4ba173597151fb538a53/resvg_py-0.4.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e414b09cc4ebf418b7ceb52ffb777296f6f9f62ecf85b8beb36e9a70dc709f4f", size = 1426578, upload-time = "2026-08-12T16:01:27.744Z" }, + { url = "https://files.pythonhosted.org/packages/eb/dc/e05fbb2861a036926d126024cb888cff4892831689d981e00a742d631bfd/resvg_py-0.4.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:69ffc6fd35ac0db183331d8a401d2ed60ba9edceb44ecda1757c3a455f9af3bd", size = 1523976, upload-time = "2026-08-12T16:01:29.046Z" }, + { url = "https://files.pythonhosted.org/packages/ff/5e/2c42980324e999b94cc689c7445848715cfe1b8d8d7af194bea1d166dd2e/resvg_py-0.4.0-pp311-pypy311_pp73-musllinux_1_2_armv7l.whl", hash = "sha256:0647b6bea989da4f282ebc2b06fadfdc29045e35a5761810aa876bb1ce520146", size = 1545404, upload-time = "2026-08-12T16:01:30.201Z" }, + { url = "https://files.pythonhosted.org/packages/5d/b4/79365305c62797adc79d614d0db22d5773500eab9d0634cf7794e6a57a46/resvg_py-0.4.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:eba4f2393869392e0e5d86820ab246427899b1098f2f71c83f4f83ac3f0bc422", size = 1588538, upload-time = "2026-08-12T16:01:31.485Z" }, + { url = "https://files.pythonhosted.org/packages/3e/6e/8854bae4907280edec643c0bb2d125acbd41f24ce04383ac0bf3e668d35e/resvg_py-0.4.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:19ce20db44718eef646401039a39d9ec72bda1298ca8401c8a913ffaa6573fc4", size = 1562622, upload-time = "2026-08-12T16:01:32.559Z" }, +] + [[package]] name = "rich" version = "14.3.3" @@ -4267,7 +4319,7 @@ resolution-markers = [ "python_full_version < '3.12'", ] dependencies = [ - { name = "numpy", marker = "python_full_version < '3.12'" }, + { name = "numpy" }, ] sdist = { url = "https://files.pythonhosted.org/packages/7a/97/5a3609c4f8d58b039179648e62dd220f89864f56f7357f5d4f45c29eb2cc/scipy-1.17.1.tar.gz", hash = "sha256:95d8e012d8cb8816c226aef832200b1d45109ed4464303e997c5b13122b297c0", size = 30573822, upload-time = "2026-02-23T00:26:24.851Z" } wheels = [ @@ -4322,7 +4374,7 @@ resolution-markers = [ "python_full_version == '3.12.*'", ] dependencies = [ - { name = "numpy", marker = "python_full_version >= '3.12'" }, + { name = "numpy" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a7/25/c2700dfaf6442b4effaa91af24ebce5dc9d31bb4a69706313aae70d72cd0/scipy-1.18.0.tar.gz", hash = "sha256:67b2ad2ad54c72ca6d04975a9b2df8c3638c34ddd5b28738e94fc2b57929d378", size = 30774447, upload-time = "2026-06-19T15:01:43.456Z" } wheels = [ @@ -4970,11 +5022,11 @@ name = "vercel-workers" version = "0.0.25" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "anyio", marker = "python_full_version >= '3.12'" }, - { name = "httpx", marker = "python_full_version >= '3.12'" }, - { name = "pydantic", marker = "python_full_version >= '3.12'" }, - { name = "python-dotenv", marker = "python_full_version >= '3.12'" }, - { name = "vercel", marker = "python_full_version >= '3.12'" }, + { name = "anyio" }, + { name = "httpx" }, + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "vercel" }, ] sdist = { url = "https://files.pythonhosted.org/packages/30/df/04d37021ad7ca53b7599c313e411d91623c7a005c741f491d1eefb7a9f0c/vercel_workers-0.0.25.tar.gz", hash = "sha256:212ded01400b524be51d251df49f801caf115ad7d48cca7eb168cbeceda3def3", size = 64149, upload-time = "2026-06-20T19:26:27.177Z" } wheels = [