From 63fc8641233a494cf6319f6091f7f617452fff41 Mon Sep 17 00:00:00 2001 From: Ajeya Bhat Date: Tue, 13 Feb 2024 12:08:45 +0530 Subject: [PATCH] changed name --- dev-requirements.txt | 67 +++++- outpostcli/cli.py | 37 +-- outpostcli/config_utils.py | 3 +- outpostcli/core.py | 14 -- outpostcli/{inference.py => endpoint.py} | 49 ++-- outpostcli/endpoints.py | 288 +++++++++++++++++++++++ outpostcli/inferences.py | 109 --------- outpostcli/utils.py | 31 ++- pyproject.toml | 4 +- requirements.txt | 54 ++++- 10 files changed, 476 insertions(+), 180 deletions(-) rename outpostcli/{inference.py => endpoint.py} (56%) create mode 100644 outpostcli/endpoints.py delete mode 100644 outpostcli/inferences.py diff --git a/dev-requirements.txt b/dev-requirements.txt index 6b7ba10..5a3835a 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,2 +1,65 @@ -black -ruff \ No newline at end of file +# +# This file is autogenerated by pip-compile with Python 3.11 +# by the following command: +# +# pip-compile --extra=dev --output-file=dev-requirements.txt pyproject.toml +# +annotated-types==0.6.0 + # via pydantic +anyio==4.2.0 + # via httpx +black==24.1.1 + # via outpostcli (pyproject.toml) +certifi==2024.2.2 + # via + # httpcore + # httpx +click==8.1.7 + # via + # black + # outpostcli (pyproject.toml) +h11==0.14.0 + # via httpcore +httpcore==1.0.2 + # via httpx +httpx==0.26.0 + # via outpostkit +idna==3.6 + # via + # anyio + # httpx +markdown-it-py==3.0.0 + # via rich +mdurl==0.1.2 + # via markdown-it-py +mypy-extensions==1.0.0 + # via black +outpostkit==0.0.30 + # via outpostcli (pyproject.toml) +packaging==23.2 + # via + # black + # outpostkit +pathspec==0.12.1 + # via black +platformdirs==4.2.0 + # via black +pydantic==2.6.1 + # via outpostkit +pydantic-core==2.16.2 + # via pydantic +pygments==2.17.2 + # via rich +rich==13.7.0 + # via outpostcli (pyproject.toml) +ruff==0.2.1 + # via outpostcli (pyproject.toml) +sniffio==1.3.0 + # via + # anyio + # httpx +typing-extensions==4.9.0 + # via + # outpostkit + # pydantic + # pydantic-core diff --git a/outpostcli/cli.py b/outpostcli/cli.py index 9695c40..cbce6f6 100644 --- a/outpostcli/cli.py +++ b/outpostcli/cli.py @@ -1,20 +1,18 @@ -import json - import click import outpostkit from outpostkit import Client +from outpostkit.exceptions import OutpostError, OutpostHTTPException from .config_utils import ( - get_default_api_token_from_config, purge_config_file, remove_details_from_config_file, write_details_to_config_file, ) from .constants import cli_version from .exceptions import NotLoggedInError -from .inference import inference -from .inferences import inferences -from .utils import check_token, click_group +from .endpoint import inference +from .endpoints import endpoints +from .utils import add_options, api_token_opt, check_token, click_group CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"]) @@ -26,7 +24,7 @@ def outpostcli(): # # Add subcommands -outpostcli.add_command(inferences) +outpostcli.add_command(endpoints) outpostcli.add_command(inference) # job.add_command(lep) # kv.add_command(lep) @@ -40,14 +38,7 @@ def outpostcli(): @outpostcli.command() -@click.option( - "--api-token", - "-t", - help="The API token for the outpost user.", - default=None, - prompt=True, - hide_input=True, -) +@add_options([api_token_opt]) def login(api_token: str): """ Login to the outpost. @@ -62,12 +53,13 @@ def login(api_token: str): @outpostcli.command() -@click.option("--api-token", "-t", default=lambda: get_default_api_token_from_config()) +@add_options([api_token_opt]) def user(api_token): """ Get details about the currently logged in user. """ - click.echo(json.dumps(Client(api_token=api_token).user.__dict__, indent=4)) + click.echo(Client(api_token=api_token).user) + # click.echo(json.dumps(Client(api_token=api_token).user, indent=4)) @outpostcli.command(name="sdk-version") @@ -75,7 +67,7 @@ def sdk_version(): """ Get details about the currently logged in user. """ - click.echo(outpostkit.__about__.__version__) + click.echo(outpostkit.__version__) @outpostcli.command() @@ -96,3 +88,12 @@ def logout(purge: bool): click.echo("Logged out successfully.") except NotLoggedInError: click.echo("No logged in user found.") + + +def outpost(): + try: + outpostcli() + except OutpostError as error: + click.echo(f"An error occurred: {error}", err=True) + except OutpostHTTPException as error: + click.echo(f"""APIException occurred - {error}""", err=True) diff --git a/outpostcli/config_utils.py b/outpostcli/config_utils.py index df1e613..a664dba 100644 --- a/outpostcli/config_utils.py +++ b/outpostcli/config_utils.py @@ -1,9 +1,8 @@ -import os import configparser +import os from outpostcli.exceptions import NotLoggedInError - CONFIG_FILE = os.environ.get( "OUTPOSTCLI_CFG_PATH", os.path.join(os.path.expanduser("~"), ".outpostcli.cfg") ) diff --git a/outpostcli/core.py b/outpostcli/core.py index feec88d..c205c35 100644 --- a/outpostcli/core.py +++ b/outpostcli/core.py @@ -2,17 +2,3 @@ get_default_api_token_from_config, get_default_entity_from_config, ) - - -def get_api_token(ctx, param, value): - if value is None: - return get_default_api_token_from_config() - else: - return value - - -def get_entity(ctx, param, value): - if value is None: - return get_default_entity_from_config() - else: - return value diff --git a/outpostcli/inference.py b/outpostcli/endpoint.py similarity index 56% rename from outpostcli/inference.py rename to outpostcli/endpoint.py index c279b7d..19e2bb0 100644 --- a/outpostcli/inference.py +++ b/outpostcli/endpoint.py @@ -1,54 +1,51 @@ import json import click -from outpostkit import Client -from outpostkit.inference import Inference +from outpostkit import Client, Endpoint from outpostkit.utils import convert_outpost_date_str_to_date from rich.table import Table -from outpostcli.config_utils import ( - get_default_api_token_from_config, - get_default_entity_from_config, +from outpostcli.utils import ( + add_options, + api_token_opt, + click_group, + console, + entity_opt, ) -from outpostcli.exceptions import SourceNotSupportedError -from outpostcli.utils import click_group, combine_inf_load_source_model, console @click_group() def inference(): """ - Manage Inferences + Manage an Inference service """ pass @inference.command(name="get") -@click.option("--api-token", "-t", default=lambda: get_default_api_token_from_config()) -@click.option("--entity", "-e", default=lambda: get_default_entity_from_config()) +@add_options([api_token_opt, entity_opt]) @click.argument("name", type=str, nargs=1) def get_inference(api_token, entity, name): client = Client(api_token=api_token) - inf_data = Inference(client=client, name=name, entity=entity).get() - click.echo(json.dumps(inf_data.__dict__, indent=2)) + inf_data = Endpoint(client=client, name=name, entity=entity).get() + click.echo(inf_data.__dict__) @inference.command(name="deploy") @click.argument("name", type=str, nargs=1) -@click.option("--api-token", "-t", default=lambda: get_default_api_token_from_config()) -@click.option("--entity", "-e", default=lambda: get_default_entity_from_config()) +@add_options([api_token_opt, entity_opt]) def deploy_inference(api_token, entity, name): client = Client(api_token=api_token) - deploy_data = Inference(client=client, name=name, entity=entity).deploy({}) + deploy_data = Endpoint(client=client, name=name, entity=entity).deploy({}) click.echo(f"Deployment successful. id: {deploy_data.id}") @inference.command(name="deployments") +@add_options([api_token_opt, entity_opt]) @click.argument("name", type=str, nargs=1) -@click.option("--api-token", "-t", default=lambda: get_default_api_token_from_config()) -@click.option("--entity", "-e", default=lambda: get_default_entity_from_config()) def list_inference_deployments(api_token, entity, name): client = Client(api_token=api_token) - deployments_resp = Inference( + deployments_resp = Endpoint( client=client, name=name, entity=entity ).list_deploymets(params={}) @@ -77,15 +74,14 @@ def list_inference_deployments(api_token, entity, name): @inference.command(name="delete") @click.argument("name", type=str, nargs=1) -@click.option("--api-token", "-t", default=lambda: get_default_api_token_from_config()) -@click.option("--entity", "-e", default=lambda: get_default_entity_from_config()) +@add_options([api_token_opt, entity_opt]) def delete_inference(api_token, entity, name): fullName = f"{entity}/{name}" if click.confirm( f"do you really want to delete this endpoint: {fullName} ?", abort=True ): client = Client(api_token=api_token) - delete_resp = Inference(client=client, name=name, entity=entity).delete() + delete_resp = Endpoint(client=client, name=name, entity=entity).delete() return "Inference endpoint deleted." return "Aborted" @@ -93,12 +89,11 @@ def delete_inference(api_token, entity, name): @inference.command(name="dep-status") @click.argument("name", type=str, nargs=1) -@click.option("--api-token", "-t", default=lambda: get_default_api_token_from_config()) -@click.option("--entity", "-e", default=lambda: get_default_entity_from_config()) +@add_options([api_token_opt, entity_opt]) @click.option("--verbose", "-v", is_flag=True, help="Verbose") def inf_dep_status(api_token, entity, name): client = Client(api_token=api_token) - # status_data = Inference( - # client=client, api_token=api_token, name=name, entity=entity - # ).status() - # click.echo() + status_data = Endpoint( + client=client, api_token=api_token, name=name, entity=entity + ).status() + click.echo(status_data) diff --git a/outpostcli/endpoints.py b/outpostcli/endpoints.py new file mode 100644 index 0000000..becf0dc --- /dev/null +++ b/outpostcli/endpoints.py @@ -0,0 +1,288 @@ +import json +import os +from typing import Optional +from urllib.parse import urlparse + +import click +from outpostkit import Client, Endpoints +from outpostkit.utils import convert_outpost_date_str_to_date +from rich.table import Table + +from outpostcli.exceptions import SourceNotSupportedError +from outpostcli.utils import ( + add_options, + api_token_opt, + click_group, + combine_inf_load_source_model, + console, + entity_opt, +) + + +@click_group() +def endpoints(): + """ + Manage Endpoints + """ + pass + + +@endpoints.command(name="list") +@add_options([api_token_opt, entity_opt]) +def list_endpoints(api_token, entity): + client = Client(api_token=api_token) + infs_resp = Endpoints(client=client, entity=entity).list() + inf_table = Table( + title=f"Inference Services ({infs_resp.total})", + ) + # "primary_endpoint", + inf_table.add_column("name") + inf_table.add_column("model") + inf_table.add_column("status") + inf_table.add_column("hardware_instance") + inf_table.add_column("visibility") + inf_table.add_column("updated_at", justify="right") + for inf in infs_resp.inferences: + inf_table.add_row( + inf.name, + combine_inf_load_source_model( + inf.loadModelWeightsFrom, inf.outpostModel, inf.huggingfaceModel + ), + inf.status, + inf.hardwareInstance.name, + inf.visibility, + convert_outpost_date_str_to_date(inf.updatedAt).isoformat(), + ) + + console.print(inf_table) + + +@endpoints.command(name="create") +@click.argument("model-data", type=str, nargs=1, required=False) +@click.option( + "--name", + "-n", + type=str, + default=None, + required=False, + help="name of the inference endpoint to create.", +) +@click.option( + "--huggingface-token-id", + type=str, + default=None, + required=False, + help="revision of the model to use.", +) +@click.option( + "--huggingface-token-id", + type=str, + default=None, + required=False, + help="revision of the model to use.", +) +@click.option( + "--hardware-instance", + "-h", + type=str, + required=True, + help="hardware instance type to use", +) +@click.option( + "--template-path", + "-p", + type=str, + help="template path", + required=False, +) +@click.option( + "--task-type", + "-t", + type=str, + default="custom", + help="task type", + required=False, +) +@click.option( + "--base-image", + type=click.Choice( + [ + "transformers-pt", + "transformers-tf", + "python", + "diffusers-pt", + "diffusers-tf", + "tensorflow", + "diffusers", + ] + ), + # type=str, + help="base image", + required=False, +) +@click.option( + "--visibility", + type=click.Choice(["private", "public", "internal"]), + # type=str, + help="visibility of the endpoint", + required=False, +) +@click.option( + "--replica-scaling-min", + type=int, + default=0, + help="minimum number of replicas", + required=False, +) +@click.option( + "--replica-scaling-max", + type=int, + default=1, + help="maximum number of replicas", + required=False, +) +@click.option( + "--replica-scaling-scaledown-period", + type=int, + default=900, + help="number of seconds to wait before scaling down.", + required=False, +) +@click.option( + "--replica-scaling-target-pending-req", + type=int, + default=20, + help="threshold of number of requests in pending before scaling up.", + required=False, +) +@add_options([api_token_opt, entity_opt]) +def create_endpoint( + api_token: str, + entity: str, + model_data: Optional[str], + hardware_instance: str, + huggingface_token_id, + base_image: Optional[str], + name: Optional[str], + template_path: Optional[str], + task_type: str, + replica_scaling_min: int, + replica_scaling_max: int, + visibility: str, + replica_scaling_scaledowm_period: int, + replica_scaling_target_pending_req: int, +): + client = Client(api_token=api_token) + if template_path: + [actual_path, class_name] = template_path.rsplit(":", 1) + click.echo(f"{actual_path},{class_name}") + if not actual_path or not class_name: + click.echo( + "Please specify the template classname along with the path.", err=True + ) + return + if not base_image: + click.echo("Please specify the base image you want to use.", err=True) + return + try: + result = urlparse(actual_path) + if all([result.scheme, result.netloc]): + click.echo("url") + data = { + "templateType": "custom", + "customTemplateConfig": { + "className": class_name, + "url": actual_path, + }, + "hardwareInstance": hardware_instance, + "taskType": task_type, + "name": name, + "containerType": "prebuilt", + "visibility": visibility, + "prebuiltImageName": base_image, + "replicaScalingConfig": { + "min": replica_scaling_min, + "max": replica_scaling_max, + "scaledownPeriod": replica_scaling_scaledowm_period, + "targetPendingRequests": replica_scaling_target_pending_req, + }, + } + create_resp = Endpoints(client=client, entity=entity).create(json=data) + else: + raise ValueError("Not an url.") + except ValueError: + if os.path.exists(actual_path) and os.path.isfile(actual_path): + click.echo("file") + # do something + data = { + "templateType": "custom", + "customTemplateConfig": { + "className": class_name, + }, + "taskType": task_type, + "name": name, + "containerType": "prebuilt", + "visibility": visibility, + "hardwareInstance": hardware_instance, + "prebuiltImageName": base_image, + "replicaScalingConfig": { + "min": replica_scaling_min, + "max": replica_scaling_max, + "scaledownPeriod": replica_scaling_scaledowm_period, + "targetPendingRequests": replica_scaling_target_pending_req, + }, + } + create_resp = Endpoints(client=client, entity=entity).create( + data={"metadata": json.dumps(data)}, + files={"template": open(actual_path, mode="rb")}, + ) + else: + click.echo("Invalid template file path.", err=True) + return + except Exception as e: + click.echo(f"could not parse the template, error: {e}", err=True) + return + else: + # do the other thing + if not model_data: + click.echo("Please provided the model name.", err=True) + return + m_splits = model_data.split(":", 1) + model_details: dict[str] = None + if len(m_splits) == 1: + [model_name, revision] = model_data.split("@", 1) + model_details = { + "modelSource": "outpost", + "outpostModel": {"fullName": model_name, "commit": revision}, + } + else: + if m_splits[0] == "hf" or m_splits[0] == "huggingface": + [model_name, revision] = model_data.split("@", 1) + model_details = { + "modelSource": "huggingface", + "huggingfaceModel": { + "id": model_name, + "revision": revision, + "keyId": huggingface_token_id, + }, + } + else: + raise SourceNotSupportedError(f"source {m_splits[0]} not supported.") + + create_body = { + "templateType": "autogenerated", + "autogeneratedTemplateConfig": model_details, + "hardwareInstance": hardware_instance, + "visibility": visibility, + "name": name, + "replicaScalingConfig": { + "min": replica_scaling_min, + "max": replica_scaling_max, + "scaledownPeriod": replica_scaling_scaledowm_period, + "targetPendingRequests": replica_scaling_target_pending_req, + }, + } + create_resp = Endpoints(client=client, entity=entity).create(json=create_body) + click.echo("Inference created...") + click.echo(f"name: {create_resp.name}") + click.echo(f"id: {create_resp.id}") diff --git a/outpostcli/inferences.py b/outpostcli/inferences.py deleted file mode 100644 index 5d7ab63..0000000 --- a/outpostcli/inferences.py +++ /dev/null @@ -1,109 +0,0 @@ -import click -from outpostkit import Client -from outpostkit.inference import Inferences -from outpostkit.utils import convert_outpost_date_str_to_date -from rich.table import Table - -from outpostcli.config_utils import ( - get_default_api_token_from_config, - get_default_entity_from_config, -) -from outpostcli.exceptions import SourceNotSupportedError -from outpostcli.utils import click_group, combine_inf_load_source_model, console - - -@click_group() -def inferences(): - """ - Manage Inferences - """ - pass - - -@inferences.command(name="list") -@click.option("--api-token", "-t", default=lambda: get_default_api_token_from_config()) -@click.option("--entity", "-e", default=lambda: get_default_entity_from_config()) -def list_inferences(api_token, entity): - client = Client(api_token=api_token) - infs_resp = Inferences(client=client, entity=entity).list() - inf_table = Table( - title=f"Inference Services ({infs_resp.total})", - ) - # "primary_endpoint", - inf_table.add_column("name") - inf_table.add_column("model") - inf_table.add_column("status") - inf_table.add_column("hardware_instance") - inf_table.add_column("visibility") - inf_table.add_column("updated_at", justify="right") - for inf in infs_resp.inferences: - inf_table.add_row( - inf.name, - combine_inf_load_source_model( - inf.loadModelWeightsFrom, inf.outpostModel, inf.huggingfaceModel - ), - inf.status, - inf.hardwareInstance.name, - inf.visibility, - convert_outpost_date_str_to_date(inf.updatedAt).isoformat(), - ) - - console.print(inf_table) - - -@inferences.command(name="create") -@click.argument("model", type=str, nargs=1) -@click.option("--api-token", "-t", default=lambda: get_default_api_token_from_config()) -@click.option("--entity", "-e", default=lambda: get_default_entity_from_config()) -@click.option( - "--revision", "-r", type=str, default=None, help="revision of the model to use." -) -@click.option( - "--name", - "-n", - type=str, - default=None, - help="name of the inference endpoint to create.", -) -@click.option( - "--huggingface-token-id", - type=str, - default=None, - help="revision of the model to use.", -) -@click.option( - "--hardware-instance", - "-h", - type=str, - help="hardware instance type to use", -) -def create_inference( - api_token, entity, model, revision, hardware_instance, huggingface_token_id, name -): - client = Client(api_token=api_token) - m_splits = model.split(":", 1) - model_details: dict[str] = None - if len(m_splits) == 1: - model_details = { - "loadModelWeightsFrom": "outpost", - "outpostModel": {"fullName": model, "commit": revision}, - } - else: - if m_splits[0] == "hf" or m_splits[0] == "huggingface": - model_details = { - "loadModelWeightsFrom": "huggingface", - "huggingfaceModel": {"id": m_splits[1], "revision": revision}, - "keyId": huggingface_token_id, - } - else: - raise SourceNotSupportedError(f"source {m_splits[0]} not supported.") - create_body = { - **model_details, - "hardwareInstance": hardware_instance, - "name": name, - } - click.echo(create_body) - create_resp = Inferences(client=client, entity=entity).create(data=create_body) - click.echo("Inference created...") - click.echo(f"name: {create_resp.name}") - click.echo(f"id: {create_resp.id}") diff --git a/outpostcli/utils.py b/outpostcli/utils.py index 3e9bc1c..0374d65 100644 --- a/outpostcli/utils.py +++ b/outpostcli/utils.py @@ -1,8 +1,16 @@ from typing import Optional + import click +from outpostkit._types.inference import ( + InferenceAutogeneratedHFModelDetails, + InferenceAutogeneratedOutpostModelDetails, +) from rich.console import Console -from outpostkit.inference import InferenceOutpostModel, InferenceHuggingfaceModel +from outpostcli.config_utils import ( + get_default_api_token_from_config, + get_default_entity_from_config, +) console = Console(highlight=False) @@ -52,8 +60,8 @@ def check_token(token: str): def combine_inf_load_source_model( load_source, - outpost_model: Optional[InferenceOutpostModel], - hf_model: Optional[InferenceHuggingfaceModel], + outpost_model: Optional[InferenceAutogeneratedOutpostModelDetails], + hf_model: Optional[InferenceAutogeneratedHFModelDetails], ): if load_source == "hugginface" and hf_model: return f"hf:{hf_model.id}" @@ -61,3 +69,20 @@ def combine_inf_load_source_model( return f"{outpost_model.model.fullName}" else: return "custom" + + +def add_options(options): + def _add_options(func): + for option in reversed(options): + func = option(func) + return func + + return _add_options + + +api_token_opt = click.option( + "--api-token", default=lambda: get_default_api_token_from_config() +) +entity_opt = click.option( + "--entity", "-e", default=lambda: get_default_entity_from_config() +) diff --git a/pyproject.toml b/pyproject.toml index 0a2e2c3..9620cdb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,7 @@ license = { file = "LICENSE" } authors = [{ name = "Outpost Innovations, Inc." }] requires-python = ">=3.8" dependencies = [ - "outpostkit>=0.0.26", + "outpostkit>=0.0.31", "click", "rich", ] @@ -31,7 +31,7 @@ testpaths = "tests/" packages = ["outpostcli"] [project.scripts] -outpostcli = "outpostcli.cli:outpostcli" +outpostcli = "outpostcli.cli:outpost" [tool.setuptools.package-data] "outpostcli" = ["py.typed"] diff --git a/requirements.txt b/requirements.txt index 60ee0ed..de75d0a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,51 @@ -outpostkit==0.0.26 -click -rich \ No newline at end of file +# +# This file is autogenerated by pip-compile with Python 3.11 +# by the following command: +# +# pip-compile --output-file=requirements.txt pyproject.toml +# +annotated-types==0.6.0 + # via pydantic +anyio==4.2.0 + # via httpx +certifi==2024.2.2 + # via + # httpcore + # httpx +click==8.1.7 + # via outpostcli (pyproject.toml) +h11==0.14.0 + # via httpcore +httpcore==1.0.2 + # via httpx +httpx==0.26.0 + # via outpostkit +idna==3.6 + # via + # anyio + # httpx +markdown-it-py==3.0.0 + # via rich +mdurl==0.1.2 + # via markdown-it-py +outpostkit==0.0.31 + # via outpostcli (pyproject.toml) +packaging==23.2 + # via outpostkit +pydantic==2.6.1 + # via outpostkit +pydantic-core==2.16.2 + # via pydantic +pygments==2.17.2 + # via rich +rich==13.7.0 + # via outpostcli (pyproject.toml) +sniffio==1.3.0 + # via + # anyio + # httpx +typing-extensions==4.9.0 + # via + # outpostkit + # pydantic + # pydantic-core