#!/usr/bin/env python3 """Text or image search against the nyris Find API. Usage: NYRIS_API_KEY=... python3 find.py text "hydraulic pump" NYRIS_API_KEY=... python3 find.py image ./query.jpg NYRIS_API_KEY=... python3 find.py image-json ./query.jpg # JSON body, image as base64 NYRIS_API_KEY=... python3 find.py regions ./query.jpg python3 find.py resize-check 3000x2000 # no network """ import base64 import json import os import sys import urllib.error import urllib.request import uuid BASE = os.environ.get("NYRIS_BASE_URL", "https://api.nyris.io") def api_key() -> str: key = os.environ.get("NYRIS_API_KEY", "").strip() if not key: sys.exit("Set NYRIS_API_KEY") return key def multipart(fields: list[tuple[str, bytes, str | None]]) -> tuple[bytes, str]: boundary = "----nyris" + uuid.uuid4().hex chunks: list[bytes] = [] for name, data, filename in fields: chunks.append(f"--{boundary}\r\n".encode()) if filename: chunks.append( f'Content-Disposition: form-data; name="{name}"; filename="{filename}"\r\n' f"Content-Type: image/jpeg\r\n\r\n".encode() ) else: chunks.append(f'Content-Disposition: form-data; name="{name}"\r\n\r\n'.encode()) chunks.append(data) chunks.append(b"\r\n") chunks.append(f"--{boundary}--\r\n".encode()) return b"".join(chunks), boundary def post(path: str, body: bytes, content_type: str) -> dict: request = urllib.request.Request( BASE + path, data=body, method="POST", headers={ "X-Api-Key": api_key(), "Accept": "application/items.complete+json", "Content-Type": content_type, }, ) try: with urllib.request.urlopen(request, timeout=60) as response: return json.loads(response.read().decode()) except urllib.error.HTTPError as error: detail = error.read().decode(errors="replace")[:300] sys.exit(f"HTTP {error.code}: {detail}") def area_size(width: int, height: int, max_side: int = 1024) -> tuple[int, int]: """Match getThumbSizeArea: target pixel area, aspect ratio kept.""" if height == 0: return width, height aspect = width / height area = max_side * max_side out_w = (area * aspect) ** 0.5 out_h = out_w / aspect return max(1, round(out_w)), max(1, round(out_h)) def main() -> None: if len(sys.argv) < 3: sys.exit(__doc__.strip()) mode, arg = sys.argv[1], sys.argv[2] if mode == "text": body, boundary = multipart([("text", arg.encode(), None)]) payload = post("/find/v1.1", body, f"multipart/form-data; boundary={boundary}") elif mode == "image": data = open(arg, "rb").read() body, boundary = multipart([("image", data, "query.jpg")]) payload = post("/find/v1.1", body, f"multipart/form-data; boundary={boundary}") elif mode == "image-json": data = open(arg, "rb").read() body = json.dumps({"image": base64.b64encode(data).decode("ascii")}).encode() payload = post("/find/v1.1", body, "application/json") elif mode == "regions": data = open(arg, "rb").read() payload = post("/find/v2/regions", data, "image/jpeg") elif mode == "resize-check": width, height = (int(part) for part in arg.split("x")) print(area_size(width, height)) return else: sys.exit(__doc__.strip()) results = payload.get("results", payload.get("regions")) count = len(results) if isinstance(results, list) else 0 print(json.dumps({"status": 200, "count": count, "id": payload.get("id")}, indent=2)) if __name__ == "__main__": main()