"""Dependency-free Python 3 SDK and CLI for Graham & Lane's public catalog.""" import argparse import json import urllib.error import urllib.parse import urllib.request class CatalogError(RuntimeError): def __init__(self, message, status, code, retry_after): super().__init__(message) self.status, self.code, self.retry_after = status, code, retry_after def _get(path): request = urllib.request.Request( "https://grahamandlane.com" + path, headers={"Accept": "application/json", "API-Version": "1"}, method="GET") try: with urllib.request.urlopen(request, timeout=15) as response: return json.load(response) except urllib.error.HTTPError as error: try: detail = json.load(error).get("error", {}) except (ValueError, AttributeError): detail = {} raise CatalogError(detail.get("message", "Catalog request failed"), error.code, detail.get("code"), error.headers.get("Retry-After")) from error def get_studio_information(): return _get("/api/v1/studio.json") def find_arrangements(query="", limit=20, cursor=None): if not isinstance(query, str) or len(query) > 200: raise ValueError("query must be a string of at most 200 characters") if type(limit) is not int or not 1 <= limit <= 100: raise ValueError("limit must be an integer from 1 to 100") if cursor is not None and (not isinstance(cursor, str) or not cursor or len(cursor) > 2048): raise ValueError("cursor must be an opaque string returned by the API") params = {"q": query, "limit": limit} if cursor is not None: params["cursor"] = cursor return _get("/api/v1/arrangements.json?" + urllib.parse.urlencode(params)) def main(): parser = argparse.ArgumentParser(description="Read-only public flower catalog. No account or API key; orders require a call.") commands = parser.add_subparsers(dest="command", required=True) commands.add_parser("studio", help="Public studio information") search = commands.add_parser("find", help="Published arrangements, prices and next-page cursor") search.add_argument("query", nargs="*", help="Optional search phrase") args = parser.parse_args() try: data = get_studio_information() if args.command == "studio" else find_arrangements(" ".join(args.query)) print(json.dumps(data, indent=2)) except (CatalogError, urllib.error.URLError, ValueError) as error: retry = getattr(error, "retry_after", None) parser.exit(1, f"{error}{'; retry after ' + retry + ' seconds' if retry else ''}\n") if __name__ == "__main__": main()