From 9bb8c192836538cf1465d05828714772c495ae99 Mon Sep 17 00:00:00 2001 From: Neeraj Kashyap Date: Tue, 21 Nov 2023 11:00:38 -0800 Subject: [PATCH] Added the `client` directory It contains a lightweight Python client for the Engine API. --- engineapi/client/__init__.py | 0 engineapi/client/leaderboards.py | 234 +++++++++++++++++++++++++++++ engineapi/client/sample-score.json | 10 ++ 3 files changed, 244 insertions(+) create mode 100644 engineapi/client/__init__.py create mode 100644 engineapi/client/leaderboards.py create mode 100644 engineapi/client/sample-score.json diff --git a/engineapi/client/__init__.py b/engineapi/client/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/engineapi/client/leaderboards.py b/engineapi/client/leaderboards.py new file mode 100644 index 00000000..59e439ed --- /dev/null +++ b/engineapi/client/leaderboards.py @@ -0,0 +1,234 @@ +import argparse +import json +import os +import sys +from typing import Optional +import uuid + +import requests + +LEADERBOARD_API_URL = os.environ.get( + "LEADERBOARD_API_URL", "http://localhost:7191/leaderboard/" +) + + +def moonstream_access_token(value: Optional[str]) -> uuid.UUID: + if value is None: + value = os.environ.get("MOONSTREAM_ACCESS_TOKEN") + + if value is None: + raise ValueError( + "Moonstream access token is required: either via -A/--authorization, or via the MOONSTREAM_ACCESS_TOKEN environment variable" + ) + + try: + value_uuid = uuid.UUID(value) + except Exception: + raise ValueError("Moonstream access token must be a valid UUID") + + return value_uuid + + +def requires_authorization(parser: argparse.ArgumentParser) -> None: + parser.add_argument( + "-A", + "--authorization", + type=moonstream_access_token, + required=False, + default=os.environ.get("MOONSTREAM_ACCESS_TOKEN"), + help="Moonstream API access token (if not provided, must be specified using the MOONSTREAM_ACCESS_TOKEN environment variable)", + ) + + +def handle_get(args: argparse.Namespace) -> None: + url = LEADERBOARD_API_URL + params = { + "leaderboard_id": str(args.id), + "limit": str(args.limit), + "offset": str(args.offset), + } + if args.version is not None: + params["version"] = str(args.version) + + response = requests.get(url, params=params) + response.raise_for_status() + + print(json.dumps(response.json())) + + +def handle_create(args: argparse.Namespace) -> None: + url = LEADERBOARD_API_URL + + headers = { + "Content-Type": "application/json", + "Authorization": f"Bearer {str(args.authorization)}", + } + + body = { + "title": args.title, + "description": args.description, + } + + response = requests.post(url, headers=headers, json=body) + response.raise_for_status() + print(json.dumps(response.json())) + + +def handle_versions(args: argparse.Namespace) -> None: + url = f"{LEADERBOARD_API_URL}{args.id}/versions" + + headers = { + "Authorization": f"Bearer {str(args.authorization)}", + } + + response = requests.get(url, headers=headers) + response.raise_for_status() + print(json.dumps(response.json())) + + +def handle_create_version(args: argparse.Namespace) -> None: + url = f"{LEADERBOARD_API_URL}{args.id}/versions" + + headers = { + "Authorization": f"Bearer {str(args.authorization)}", + "Content-Type": "application/json", + } + + body = { + "publish": args.publish, + } + + response = requests.post(url, headers=headers, json=body) + response.raise_for_status() + print(json.dumps(response.json())) + + +def handle_publish(args: argparse.Namespace) -> None: + url = f"{LEADERBOARD_API_URL}{args.id}/versions/{args.version}" + + headers = { + "Authorization": f"Bearer {str(args.authorization)}", + "Content-Type": "application/json", + } + + body = { + "publish": args.publish, + } + + response = requests.put(url, headers=headers, json=body) + response.raise_for_status() + print(json.dumps(response.json())) + + +def handle_upload_scores(args: argparse.Namespace) -> None: + url = f"{LEADERBOARD_API_URL}{args.id}/scores" + if args.version is not None: + url = f"{LEADERBOARD_API_URL}{args.id}/versions/{args.version}/scores" + + params = { + "overwrite": "true", + "normalize_addresses": "false", + } + + headers = { + "Content-Type": "application/json", + "Authorization": f"Bearer {str(args.authorization)}", + } + + if args.scores is None: + args.scores = sys.stdin + + with args.scores as ifp: + body = json.load(ifp) + + response = requests.put(url, headers=headers, params=params, json=body) + response.raise_for_status() + print(json.dumps(response.json())) + + +def generate_cli() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser(description="HTTP client for Leaderboard API") + parser.set_defaults(func=lambda _: parser.print_help()) + + subparsers = parser.add_subparsers() + + # GET /leaderboard/?leaderboard_id=&limit=&offset=&version= + get_parser = subparsers.add_parser("get") + get_parser.add_argument("-i", "--id", type=uuid.UUID, required=True) + get_parser.add_argument("-l", "--limit", type=int, default=10) + get_parser.add_argument("-o", "--offset", type=int, default=0) + get_parser.add_argument("-v", "--version", type=int, default=None) + get_parser.set_defaults(func=handle_get) + + # POST /leaderboard/ + create_parser = subparsers.add_parser("create") + create_parser.add_argument( + "-t", "--title", type=str, required=True, help="Title for leaderboard" + ) + create_parser.add_argument( + "-d", + "--description", + type=str, + required=False, + default="", + help="Description for leaderboard", + ) + requires_authorization(create_parser) + create_parser.set_defaults(func=handle_create) + + # GET /leaderboard//versions + versions_parser = subparsers.add_parser("versions") + versions_parser.add_argument("-i", "--id", type=uuid.UUID, required=True) + requires_authorization(versions_parser) + versions_parser.set_defaults(func=handle_versions) + + # POST /leaderboard//versions + create_version_parser = subparsers.add_parser("create-version") + create_version_parser.add_argument("-i", "--id", type=uuid.UUID, required=True) + create_version_parser.add_argument( + "--publish", + action="store_true", + help="Set this flag to publish the version immediately upon creation", + ) + requires_authorization(create_version_parser) + create_version_parser.set_defaults(func=handle_create_version) + + # PUT /leaderboard//versions/ + publish_parser = subparsers.add_parser("publish") + publish_parser.add_argument("-i", "--id", type=uuid.UUID, required=True) + publish_parser.add_argument("-v", "--version", type=int, required=True) + publish_parser.add_argument( + "--publish", action="store_true", help="Set to publish, leave to unpublish" + ) + requires_authorization(publish_parser) + publish_parser.set_defaults(func=handle_publish) + + # PUT /leaderboard//scores and PUT /leaderboard//versions//scores + upload_scores_parser = subparsers.add_parser("upload-scores") + upload_scores_parser.add_argument("-i", "--id", type=uuid.UUID, required=True) + upload_scores_parser.add_argument( + "-v", + "--version", + type=int, + required=False, + default=None, + help="Specify a version to upload scores to (if not specified a new version is created)", + ) + upload_scores_parser.add_argument( + "-s", + "--scores", + type=argparse.FileType("r"), + required=False, + default=None, + help="Path to scores file. If not provided, reads from stdin.", + ) + upload_scores_parser.set_defaults(func=handle_upload_scores) + requires_authorization(upload_scores_parser) + + return parser + + +if __name__ == "__main__": + parser = generate_cli() + args = parser.parse_args() + args.func(args) diff --git a/engineapi/client/sample-score.json b/engineapi/client/sample-score.json new file mode 100644 index 00000000..3523a324 --- /dev/null +++ b/engineapi/client/sample-score.json @@ -0,0 +1,10 @@ +[ + { + "address": "0x0000000000000000000000000000000000000000", + "score": 19, + "points_data": { + "secondary_score_1": 7, + "secondary_score_2": 29 + } + } +]