From dced8ce5a3eeec7674476584110d0e01fae5005a Mon Sep 17 00:00:00 2001 From: Nick Cao Date: Mon, 31 Mar 2025 13:27:05 -0400 Subject: [PATCH 01/16] Refactor dump_json/dump_yaml into SerializableBaseModel base class --- .../jumpstarter_cli_common/version.py | 12 ++------ .../jumpstarter/jumpstarter/client/grpc.py | 30 ++++--------------- .../jumpstarter/common/pydantic.py | 10 +++++++ .../jumpstarter/jumpstarter/config/client.py | 9 ++---- .../jumpstarter/config/exporter.py | 9 ++---- 5 files changed, 23 insertions(+), 47 deletions(-) create mode 100644 packages/jumpstarter/jumpstarter/common/pydantic.py diff --git a/packages/jumpstarter-cli-common/jumpstarter_cli_common/version.py b/packages/jumpstarter-cli-common/jumpstarter_cli_common/version.py index a2d6bdf0b..94dcb9585 100644 --- a/packages/jumpstarter-cli-common/jumpstarter_cli_common/version.py +++ b/packages/jumpstarter-cli-common/jumpstarter_cli_common/version.py @@ -3,10 +3,10 @@ import sys import asyncclick as click -import yaml -from pydantic import BaseModel, ConfigDict, Field +from pydantic import ConfigDict, Field from .opt import OutputMode, OutputType, opt_output_all +from jumpstarter.common.pydantic import SerializableBaseModel def get_client_version(): @@ -27,18 +27,12 @@ def version_msg(): return f"Jumpstarter v{jumpstarter_version} from {location} (Python {python_version})" -class JumpstarterVersion(BaseModel): +class JumpstarterVersion(SerializableBaseModel): git_version: str = Field(alias="gitVersion") python_version: str = Field(alias="pythonVersion") model_config = ConfigDict(populate_by_name=True) - def dump_json(self): - return self.model_dump_json(indent=4, by_alias=True) - - def dump_yaml(self): - return yaml.safe_dump(self.model_dump(mode="json", by_alias=True), indent=2) - def version_obj(): return JumpstarterVersion(git_version=importlib.metadata.version("jumpstarter"), python_version=sys.version) diff --git a/packages/jumpstarter/jumpstarter/client/grpc.py b/packages/jumpstarter/jumpstarter/client/grpc.py index 3ea50856c..f842d08ac 100644 --- a/packages/jumpstarter/jumpstarter/client/grpc.py +++ b/packages/jumpstarter/jumpstarter/client/grpc.py @@ -3,13 +3,13 @@ from dataclasses import dataclass, field from datetime import datetime, timedelta -import yaml from google.protobuf import duration_pb2, field_mask_pb2, json_format from grpc.aio import Channel from jumpstarter_protocol import client_pb2, client_pb2_grpc, kubernetes_pb2 -from pydantic import BaseModel, ConfigDict, Field, field_serializer +from pydantic import ConfigDict, Field, field_serializer from jumpstarter.common.grpc import translate_grpc_exceptions +from jumpstarter.common.pydantic import SerializableBaseModel def parse_identifier(identifier: str, kind: str) -> (str, str): @@ -35,7 +35,7 @@ def parse_lease_identifier(identifier: str) -> (str, str): return parse_identifier(identifier, "leases") -class Exporter(BaseModel): +class Exporter(SerializableBaseModel): namespace: str name: str labels: dict[str, str] @@ -46,7 +46,7 @@ def from_protobuf(cls, data: client_pb2.Exporter) -> Exporter: return cls(namespace=namespace, name=name, labels=data.labels) -class Lease(BaseModel): +class Lease(SerializableBaseModel): namespace: str name: str selector: str @@ -92,14 +92,8 @@ def from_protobuf(cls, data: client_pb2.Lease) -> Lease: conditions=data.conditions, ) - def dump_json(self): - return self.model_dump_json(indent=4, by_alias=True) - def dump_yaml(self): - return yaml.safe_dump(self.model_dump(mode="json", by_alias=True), indent=2) - - -class ExporterList(BaseModel): +class ExporterList(SerializableBaseModel): exporters: list[Exporter] next_page_token: str | None = Field(exclude=True) @@ -110,14 +104,8 @@ def from_protobuf(cls, data: client_pb2.ListExportersResponse) -> ExporterList: next_page_token=data.next_page_token, ) - def dump_json(self): - return self.model_dump_json(indent=4, by_alias=True) - - def dump_yaml(self): - return yaml.safe_dump(self.model_dump(mode="json", by_alias=True), indent=2) - -class LeaseList(BaseModel): +class LeaseList(SerializableBaseModel): leases: list[Lease] next_page_token: str | None = Field(exclude=True) @@ -128,12 +116,6 @@ def from_protobuf(cls, data: client_pb2.ListLeasesResponse) -> LeaseList: next_page_token=data.next_page_token, ) - def dump_json(self): - return self.model_dump_json(indent=4, by_alias=True) - - def dump_yaml(self): - return yaml.safe_dump(self.model_dump(mode="json", by_alias=True), indent=2) - @dataclass(kw_only=True, slots=True) class ClientService: diff --git a/packages/jumpstarter/jumpstarter/common/pydantic.py b/packages/jumpstarter/jumpstarter/common/pydantic.py new file mode 100644 index 000000000..0fc1c2706 --- /dev/null +++ b/packages/jumpstarter/jumpstarter/common/pydantic.py @@ -0,0 +1,10 @@ +import yaml +from pydantic import BaseModel + + +class SerializableBaseModel(BaseModel): + def dump_json(self): + return self.model_dump_json(indent=4, by_alias=True) + + def dump_yaml(self): + return yaml.safe_dump(self.model_dump(mode="json", by_alias=True), indent=2) diff --git a/packages/jumpstarter/jumpstarter/config/client.py b/packages/jumpstarter/jumpstarter/config/client.py index 6c1841d28..d1c9daa85 100644 --- a/packages/jumpstarter/jumpstarter/config/client.py +++ b/packages/jumpstarter/jumpstarter/config/client.py @@ -16,6 +16,7 @@ from jumpstarter.client.grpc import ClientService from jumpstarter.common.exceptions import FileNotFoundError from jumpstarter.common.grpc import aio_secure_channel, ssl_channel_credentials +from jumpstarter.common.pydantic import SerializableBaseModel def _allow_from_env(): @@ -274,16 +275,10 @@ def delete(cls, alias: str) -> Path: return path -class ClientConfigListV1Alpha1(BaseModel): +class ClientConfigListV1Alpha1(SerializableBaseModel): api_version: Literal["jumpstarter.dev/v1alpha1"] = Field(alias="apiVersion", default="jumpstarter.dev/v1alpha1") current_config: Optional[str] = Field(alias="currentConfig") items: list[ClientConfigV1Alpha1] kind: Literal["ClientConfigList"] = Field(default="ClientConfigList") - def dump_json(self): - return self.model_dump_json(indent=4, by_alias=True) - - def dump_yaml(self): - return yaml.safe_dump(self.model_dump(mode="json", by_alias=True), indent=2) - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) diff --git a/packages/jumpstarter/jumpstarter/config/exporter.py b/packages/jumpstarter/jumpstarter/config/exporter.py index d8e2f45f0..13eebaf87 100644 --- a/packages/jumpstarter/jumpstarter/config/exporter.py +++ b/packages/jumpstarter/jumpstarter/config/exporter.py @@ -14,6 +14,7 @@ from .tls import TLSConfigV1Alpha1 from jumpstarter.common.grpc import aio_secure_channel, ssl_channel_credentials from jumpstarter.common.importlib import import_class +from jumpstarter.common.pydantic import SerializableBaseModel from jumpstarter.driver import Driver @@ -175,15 +176,9 @@ async def channel_factory(): await exporter.serve() -class ExporterConfigListV1Alpha1(BaseModel): +class ExporterConfigListV1Alpha1(SerializableBaseModel): api_version: Literal["jumpstarter.dev/v1alpha1"] = Field(alias="apiVersion", default="jumpstarter.dev/v1alpha1") items: list[ExporterConfigV1Alpha1] kind: Literal["ExporterConfigList"] = Field(default="ExporterConfigList") - def dump_json(self): - return self.model_dump_json(indent=4, by_alias=True) - - def dump_yaml(self): - return yaml.safe_dump(self.model_dump(mode="json", by_alias=True), indent=2) - model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) From 87f3842a12632617e06df086aca4b67e41f3c5a2 Mon Sep 17 00:00:00 2001 From: Nick Cao Date: Mon, 31 Mar 2025 13:34:45 -0400 Subject: [PATCH 02/16] Dedup output mode handling --- .../jumpstarter_cli_common/opt.py | 11 ++--------- .../jumpstarter-cli/jumpstarter_cli/create.py | 6 ++---- .../jumpstarter-cli/jumpstarter_cli/get.py | 12 ++++-------- .../jumpstarter-cli/jumpstarter_cli/update.py | 6 ++---- .../jumpstarter/common/pydantic.py | 19 +++++++++++++++++++ 5 files changed, 29 insertions(+), 25 deletions(-) diff --git a/packages/jumpstarter-cli-common/jumpstarter_cli_common/opt.py b/packages/jumpstarter-cli-common/jumpstarter_cli_common/opt.py index cd9937bd7..7b25f7b09 100644 --- a/packages/jumpstarter-cli-common/jumpstarter_cli_common/opt.py +++ b/packages/jumpstarter-cli-common/jumpstarter_cli_common/opt.py @@ -2,6 +2,8 @@ import asyncclick as click +from jumpstarter.common.pydantic import OutputMode + opt_log_level = click.option( "--log-level", "log_level", @@ -20,15 +22,6 @@ opt_labels = click.option("-l", "--label", "labels", type=(str, str), multiple=True, help="Labels") -class OutputMode(str): - JSON = "json" - YAML = "yaml" - NAME = "name" - PATH = "path" - - -OutputType = Optional[OutputMode] - opt_output_all = click.option( "-o", "--output", diff --git a/packages/jumpstarter-cli/jumpstarter_cli/create.py b/packages/jumpstarter-cli/jumpstarter_cli/create.py index 43fba15ec..586ce69d9 100644 --- a/packages/jumpstarter-cli/jumpstarter_cli/create.py +++ b/packages/jumpstarter-cli/jumpstarter_cli/create.py @@ -55,10 +55,8 @@ async def create_lease(config, selector: str, duration: timedelta, output: Outpu lease = config.create_lease(selector=selector, duration=duration) match output: - case OutputMode.JSON: - click.echo(lease.dump_json()) - case OutputMode.YAML: - click.echo(lease.dump_yaml()) + case OutputMode.JSON | OutputMode.YAML: + click.echo(lease.dump(output)) case OutputMode.NAME: click.echo(lease.name) case _: diff --git a/packages/jumpstarter-cli/jumpstarter_cli/get.py b/packages/jumpstarter-cli/jumpstarter_cli/get.py index 570f8fd4e..d209f5614 100644 --- a/packages/jumpstarter-cli/jumpstarter_cli/get.py +++ b/packages/jumpstarter-cli/jumpstarter_cli/get.py @@ -25,10 +25,8 @@ def get_exporters(config, selector: str | None, output: OutputType): exporters = config.list_exporters(filter=selector) match output: - case OutputMode.JSON: - click.echo(exporters.dump_json()) - case OutputMode.YAML: - click.echo(exporters.dump_yaml()) + case OutputMode.JSON | OutputMode.YAML: + click.echo(exporters.dump(output)) case OutputMode.NAME: for exporter in exporters.exporters: click.echo(exporter.name) @@ -57,10 +55,8 @@ def get_leases(config, selector: str | None, output: OutputType): leases = config.list_leases(filter=selector) match output: - case OutputMode.JSON: - click.echo(leases.dump_json()) - case OutputMode.YAML: - click.echo(leases.dump_yaml()) + case OutputMode.JSON | OutputMode.YAML: + click.echo(leases.dump(output)) case OutputMode.NAME: for lease in leases.leases: click.echo(lease.name) diff --git a/packages/jumpstarter-cli/jumpstarter_cli/update.py b/packages/jumpstarter-cli/jumpstarter_cli/update.py index f4f3badbf..15693cc02 100644 --- a/packages/jumpstarter-cli/jumpstarter_cli/update.py +++ b/packages/jumpstarter-cli/jumpstarter_cli/update.py @@ -28,10 +28,8 @@ async def update_lease(config, name: str, duration: timedelta, output: OutputTyp lease = config.update_lease(name, duration) match output: - case OutputMode.JSON: - click.echo(lease.dump_json()) - case OutputMode.YAML: - click.echo(lease.dump_yaml()) + case OutputMode.JSON | OutputMode.YAML: + click.echo(lease.dump(output)) case OutputMode.NAME: click.echo(lease.name) case _: diff --git a/packages/jumpstarter/jumpstarter/common/pydantic.py b/packages/jumpstarter/jumpstarter/common/pydantic.py index 0fc1c2706..367808eeb 100644 --- a/packages/jumpstarter/jumpstarter/common/pydantic.py +++ b/packages/jumpstarter/jumpstarter/common/pydantic.py @@ -2,7 +2,26 @@ from pydantic import BaseModel +class OutputMode(str): + JSON = "json" + YAML = "yaml" + NAME = "name" + PATH = "path" + + +OutputType = OutputMode | None + + class SerializableBaseModel(BaseModel): + def dump(self, mode: OutputType = None): + match mode: + case OutputMode.JSON: + return self.dump_json() + case OutputMode.YAML: + return self.dump_yaml() + case _: + raise NotImplementedError("unimplemented output mode: {}".format(mode)) + def dump_json(self): return self.model_dump_json(indent=4, by_alias=True) From c2ffe01307811d5a82ba25ce35126d0d7d1c247f Mon Sep 17 00:00:00 2001 From: Nick Cao Date: Mon, 31 Mar 2025 13:43:00 -0400 Subject: [PATCH 03/16] fixup! Dedup output mode handling --- packages/jumpstarter-cli-common/jumpstarter_cli_common/opt.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/jumpstarter-cli-common/jumpstarter_cli_common/opt.py b/packages/jumpstarter-cli-common/jumpstarter_cli_common/opt.py index 7b25f7b09..a6562649d 100644 --- a/packages/jumpstarter-cli-common/jumpstarter_cli_common/opt.py +++ b/packages/jumpstarter-cli-common/jumpstarter_cli_common/opt.py @@ -2,7 +2,9 @@ import asyncclick as click -from jumpstarter.common.pydantic import OutputMode +from jumpstarter.common.pydantic import OutputMode, OutputType + +__all__ = ["OutputType"] opt_log_level = click.option( "--log-level", From 7a6b7a0e0f43436af15ce77c09ec9ff5753ec37d Mon Sep 17 00:00:00 2001 From: Nick Cao Date: Mon, 31 Mar 2025 14:03:04 -0400 Subject: [PATCH 04/16] Make JsonBaseModel inherit from SerializableBaseModel --- .../jumpstarter_kubernetes/json.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/json.py b/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/json.py index 872c6696e..9b8fd36a9 100644 --- a/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/json.py +++ b/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/json.py @@ -1,14 +1,9 @@ -import yaml -from pydantic import BaseModel, ConfigDict +from pydantic import ConfigDict +from jumpstarter.common.pydantic import SerializableBaseModel -class JsonBaseModel(BaseModel): - """A Pydantic BaseModel with additional Jumpstarter JSON options applied.""" - - def dump_json(self): - return self.model_dump_json(indent=4, by_alias=True) - def dump_yaml(self): - return yaml.safe_dump(self.model_dump(mode="json", by_alias=True), indent=2) +class JsonBaseModel(SerializableBaseModel): + """A Pydantic BaseModel with additional Jumpstarter JSON options applied.""" model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) From 18ba00b4e2b65596bb460f55949398f65adb367f Mon Sep 17 00:00:00 2001 From: Nick Cao Date: Mon, 31 Mar 2025 14:16:09 -0400 Subject: [PATCH 05/16] Cleanup output handling in admin cli --- .../jumpstarter_cli_admin/create.py | 22 ++-- .../jumpstarter_cli_admin/print.py | 120 +++++++++--------- 2 files changed, 68 insertions(+), 74 deletions(-) diff --git a/packages/jumpstarter-cli-admin/jumpstarter_cli_admin/create.py b/packages/jumpstarter-cli-admin/jumpstarter_cli_admin/create.py index cbd95667b..3ceae1f2c 100644 --- a/packages/jumpstarter-cli-admin/jumpstarter_cli_admin/create.py +++ b/packages/jumpstarter-cli-admin/jumpstarter_cli_admin/create.py @@ -38,12 +38,11 @@ def create(log_level: Optional[str]): def print_created_client(client: V1Alpha1Client, output: OutputType): - if output == OutputMode.JSON: - click.echo(client.dump_json()) - elif output == OutputMode.YAML: - click.echo(client.dump_yaml()) - elif output == OutputMode.NAME: - click.echo(f"client.jumpstarter.dev/{client.metadata.name}") + match output: + case OutputMode.JSON | OutputMode.YAML: + click.echo(client.dump(output)) + case OutputMode.NAME: + click.echo(f"client.jumpstarter.dev/{client.metadata.name}") @create.command("client") @@ -127,12 +126,11 @@ async def create_client( def print_created_exporter(exporter: V1Alpha1Exporter, output: OutputType): - if output == OutputMode.JSON: - click.echo(exporter.dump_json()) - elif output == OutputMode.YAML: - click.echo(exporter.dump_yaml()) - elif output == OutputMode.NAME: - click.echo(f"exporter.jumpstarter.dev/{exporter.metadata.name}") + match output: + case OutputMode.JSON | OutputMode.YAML: + click.echo(exporter.dump(output)) + case OutputMode.NAME: + click.echo(f"exporter.jumpstarter.dev/{exporter.metadata.name}") @create.command("exporter") diff --git a/packages/jumpstarter-cli-admin/jumpstarter_cli_admin/print.py b/packages/jumpstarter-cli-admin/jumpstarter_cli_admin/print.py index fc0a4cb38..9dc2cdf21 100644 --- a/packages/jumpstarter-cli-admin/jumpstarter_cli_admin/print.py +++ b/packages/jumpstarter-cli-admin/jumpstarter_cli_admin/print.py @@ -24,28 +24,26 @@ def make_client_row(client: V1Alpha1Client): def print_client(client: V1Alpha1Client, output: OutputType): - if output == OutputMode.JSON: - click.echo(client.dump_json()) - elif output == OutputMode.YAML: - click.echo(client.dump_yaml()) - elif output == OutputMode.NAME: - click.echo(f"client.jumpstarter.dev/{client.metadata.name}") - else: - click.echo(make_table(CLIENT_COLUMNS, [make_client_row(client)])) + match output: + case OutputMode.JSON | OutputMode.YAML: + click.echo(client.dump(output)) + case OutputMode.NAME: + click.echo(f"client.jumpstarter.dev/{client.metadata.name}") + case _: + click.echo(make_table(CLIENT_COLUMNS, [make_client_row(client)])) def print_clients(clients: V1Alpha1List[V1Alpha1Client], namespace: str, output: OutputType): - if output == OutputMode.JSON: - click.echo(clients.dump_json()) - elif output == OutputMode.YAML: - click.echo(clients.dump_yaml()) - elif output == OutputMode.NAME: - for item in clients.items: - click.echo(f"client.jumpstarter.dev/{item.metadata.name}") - elif len(clients.items) == 0: - raise click.ClickException(f'No resources found in "{namespace}" namespace') - else: - click.echo(make_table(CLIENT_COLUMNS, list(map(make_client_row, clients.items)))) + match output: + case OutputMode.JSON | OutputMode.YAML: + click.echo(clients.dump(output)) + case OutputMode.NAME: + for item in clients.items: + click.echo(f"client.jumpstarter.dev/{item.metadata.name}") + case _: + if len(clients.items) == 0: + raise click.ClickException(f'No resources found in "{namespace}" namespace') + click.echo(make_table(CLIENT_COLUMNS, list(map(make_client_row, clients.items)))) EXPORTER_COLUMNS = ["NAME", "ENDPOINT", "DEVICES", "AGE"] @@ -85,33 +83,33 @@ def get_device_rows(exporters: list[V1Alpha1Exporter]): def print_exporter(exporter: V1Alpha1Exporter, devices: bool, output: OutputType): - if output == OutputMode.JSON: - click.echo(exporter.dump_json()) - elif output == OutputMode.YAML: - click.echo(exporter.dump_yaml()) - elif output == OutputMode.NAME: - click.echo(f"exporter.jumpstarter.dev/{exporter.metadata.name}") - elif devices: - # Print the devices for the exporter - click.echo(make_table(DEVICE_COLUMNS, get_device_rows([exporter]))) - else: - click.echo(make_table(EXPORTER_COLUMNS, [make_exporter_row(exporter)])) + match output: + case OutputMode.JSON | OutputMode.YAML: + click.echo(exporter.dump(output)) + case OutputMode.NAME: + click.echo(f"exporter.jumpstarter.dev/{exporter.metadata.name}") + case _: + if devices: + # Print the devices for the exporter + click.echo(make_table(DEVICE_COLUMNS, get_device_rows([exporter]))) + else: + click.echo(make_table(EXPORTER_COLUMNS, [make_exporter_row(exporter)])) def print_exporters(exporters: V1Alpha1List[V1Alpha1Exporter], namespace: str, devices: bool, output: OutputType): - if output == OutputMode.JSON: - click.echo(exporters.dump_json()) - elif output == OutputMode.YAML: - click.echo(exporters.dump_yaml()) - elif output == OutputMode.NAME: - for item in exporters.items: - click.echo(f"exporter.jumpstarter.dev/{item.metadata.name}") - elif len(exporters.items) == 0: - raise click.ClickException(f'No resources found in "{namespace}" namespace') - elif devices: - click.echo(make_table(DEVICE_COLUMNS, get_device_rows(exporters.items))) - else: - click.echo(make_table(EXPORTER_COLUMNS, list(map(make_exporter_row, exporters.items)))) + match output: + case OutputMode.JSON | OutputMode.YAML: + click.echo(exporters.dump(output)) + case OutputMode.NAME: + for item in exporters.items: + click.echo(f"exporter.jumpstarter.dev/{item.metadata.name}") + case _: + if len(exporters.items) == 0: + raise click.ClickException(f'No resources found in "{namespace}" namespace') + if devices: + click.echo(make_table(DEVICE_COLUMNS, get_device_rows(exporters.items))) + else: + click.echo(make_table(EXPORTER_COLUMNS, list(map(make_exporter_row, exporters.items)))) LEASE_COLUMNS = ["NAME", "CLIENT", "SELECTOR", "EXPORTER", "STATUS", "REASON", "BEGIN", "END", "DURATION", "AGE"] @@ -154,25 +152,23 @@ def make_lease_row(lease: V1Alpha1Lease): def print_lease(lease: V1Alpha1Lease, output: OutputType): - if output == OutputMode.JSON: - click.echo(lease.dump_json()) - elif output == OutputMode.YAML: - click.echo(lease.dump_yaml()) - elif output == OutputMode.NAME: - click.echo(f"lease.jumpstarter.dev/{lease.metadata.name}") - else: - click.echo(make_table(LEASE_COLUMNS, [make_lease_row(lease)])) + match output: + case OutputMode.JSON | OutputMode.YAML: + click.echo(lease.dump(output)) + case OutputMode.NAME: + click.echo(f"lease.jumpstarter.dev/{lease.metadata.name}") + case _: + click.echo(make_table(LEASE_COLUMNS, [make_lease_row(lease)])) def print_leases(leases: V1Alpha1List[V1Alpha1Lease], namespace: str, output: OutputType): - if output == OutputMode.JSON: - click.echo(leases.dump_json()) - elif output == OutputMode.YAML: - click.echo(leases.dump_yaml()) - elif output == OutputMode.NAME: - for item in leases.items: - click.echo(f"lease.jumpstarter.dev/{item.metadata.name}") - elif len(leases.items) == 0: - raise click.ClickException(f'No resources found in "{namespace}" namespace') - else: - click.echo(make_table(LEASE_COLUMNS, list(map(make_lease_row, leases.items)))) + match output: + case OutputMode.JSON | OutputMode.YAML: + click.echo(leases.dump(output)) + case OutputMode.NAME: + for item in leases.items: + click.echo(f"lease.jumpstarter.dev/{item.metadata.name}") + case _: + if len(leases.items) == 0: + raise click.ClickException(f'No resources found in "{namespace}" namespace') + click.echo(make_table(LEASE_COLUMNS, list(map(make_lease_row, leases.items)))) From 4ce830b589d881472900c7b6c1ed7df2ed7f2e20 Mon Sep 17 00:00:00 2001 From: Nick Cao Date: Mon, 31 Mar 2025 14:22:01 -0400 Subject: [PATCH 06/16] Implment dump_name for singular k8s object --- .../jumpstarter_cli_admin/create.py | 15 ++++----------- .../jumpstarter_cli_admin/print.py | 8 ++------ .../jumpstarter_kubernetes/json.py | 12 +++++++++++- 3 files changed, 17 insertions(+), 18 deletions(-) diff --git a/packages/jumpstarter-cli-admin/jumpstarter_cli_admin/create.py b/packages/jumpstarter-cli-admin/jumpstarter_cli_admin/create.py index 3ceae1f2c..c8f5aaa12 100644 --- a/packages/jumpstarter-cli-admin/jumpstarter_cli_admin/create.py +++ b/packages/jumpstarter-cli-admin/jumpstarter_cli_admin/create.py @@ -4,7 +4,6 @@ import asyncclick as click from jumpstarter_cli_common import ( AliasedGroup, - OutputMode, OutputType, opt_context, opt_kubeconfig, @@ -38,11 +37,8 @@ def create(log_level: Optional[str]): def print_created_client(client: V1Alpha1Client, output: OutputType): - match output: - case OutputMode.JSON | OutputMode.YAML: - click.echo(client.dump(output)) - case OutputMode.NAME: - click.echo(f"client.jumpstarter.dev/{client.metadata.name}") + if output is not None: + click.echo(client.dump(output)) @create.command("client") @@ -126,11 +122,8 @@ async def create_client( def print_created_exporter(exporter: V1Alpha1Exporter, output: OutputType): - match output: - case OutputMode.JSON | OutputMode.YAML: - click.echo(exporter.dump(output)) - case OutputMode.NAME: - click.echo(f"exporter.jumpstarter.dev/{exporter.metadata.name}") + if output is not None: + click.echo(exporter.dump(output)) @create.command("exporter") diff --git a/packages/jumpstarter-cli-admin/jumpstarter_cli_admin/print.py b/packages/jumpstarter-cli-admin/jumpstarter_cli_admin/print.py index 9dc2cdf21..d915cb909 100644 --- a/packages/jumpstarter-cli-admin/jumpstarter_cli_admin/print.py +++ b/packages/jumpstarter-cli-admin/jumpstarter_cli_admin/print.py @@ -25,10 +25,8 @@ def make_client_row(client: V1Alpha1Client): def print_client(client: V1Alpha1Client, output: OutputType): match output: - case OutputMode.JSON | OutputMode.YAML: + case OutputMode.JSON | OutputMode.YAML | OutputMode.NAME: click.echo(client.dump(output)) - case OutputMode.NAME: - click.echo(f"client.jumpstarter.dev/{client.metadata.name}") case _: click.echo(make_table(CLIENT_COLUMNS, [make_client_row(client)])) @@ -84,10 +82,8 @@ def get_device_rows(exporters: list[V1Alpha1Exporter]): def print_exporter(exporter: V1Alpha1Exporter, devices: bool, output: OutputType): match output: - case OutputMode.JSON | OutputMode.YAML: + case OutputMode.JSON | OutputMode.YAML | OutputMode.NAME: click.echo(exporter.dump(output)) - case OutputMode.NAME: - click.echo(f"exporter.jumpstarter.dev/{exporter.metadata.name}") case _: if devices: # Print the devices for the exporter diff --git a/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/json.py b/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/json.py index 9b8fd36a9..fd6656ce7 100644 --- a/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/json.py +++ b/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/json.py @@ -1,9 +1,19 @@ from pydantic import ConfigDict -from jumpstarter.common.pydantic import SerializableBaseModel +from jumpstarter.common.pydantic import OutputMode, OutputType, SerializableBaseModel class JsonBaseModel(SerializableBaseModel): """A Pydantic BaseModel with additional Jumpstarter JSON options applied.""" + def dump(self, mode: OutputType = None): + match mode: + case OutputMode.NAME: + return self.dump_name() + case _: + return super().dump(mode) + + def dump_name(self): + return "{}.jumpstarter.dev/{}".format(self.kind.lower(), self.metadata.name) + model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) From d72c633f1d8359e6cca40b973187d78c43690d4b Mon Sep 17 00:00:00 2001 From: Nick Cao Date: Mon, 31 Mar 2025 14:30:34 -0400 Subject: [PATCH 07/16] Handle dumping names in lists --- .../jumpstarter_cli_admin/print.py | 19 ++++--------------- .../jumpstarter_kubernetes/list.py | 3 +++ 2 files changed, 7 insertions(+), 15 deletions(-) diff --git a/packages/jumpstarter-cli-admin/jumpstarter_cli_admin/print.py b/packages/jumpstarter-cli-admin/jumpstarter_cli_admin/print.py index d915cb909..d10574725 100644 --- a/packages/jumpstarter-cli-admin/jumpstarter_cli_admin/print.py +++ b/packages/jumpstarter-cli-admin/jumpstarter_cli_admin/print.py @@ -33,11 +33,8 @@ def print_client(client: V1Alpha1Client, output: OutputType): def print_clients(clients: V1Alpha1List[V1Alpha1Client], namespace: str, output: OutputType): match output: - case OutputMode.JSON | OutputMode.YAML: + case OutputMode.JSON | OutputMode.YAML | OutputMode.NAME: click.echo(clients.dump(output)) - case OutputMode.NAME: - for item in clients.items: - click.echo(f"client.jumpstarter.dev/{item.metadata.name}") case _: if len(clients.items) == 0: raise click.ClickException(f'No resources found in "{namespace}" namespace') @@ -94,11 +91,8 @@ def print_exporter(exporter: V1Alpha1Exporter, devices: bool, output: OutputType def print_exporters(exporters: V1Alpha1List[V1Alpha1Exporter], namespace: str, devices: bool, output: OutputType): match output: - case OutputMode.JSON | OutputMode.YAML: + case OutputMode.JSON | OutputMode.YAML | OutputMode.NAME: click.echo(exporters.dump(output)) - case OutputMode.NAME: - for item in exporters.items: - click.echo(f"exporter.jumpstarter.dev/{item.metadata.name}") case _: if len(exporters.items) == 0: raise click.ClickException(f'No resources found in "{namespace}" namespace') @@ -149,21 +143,16 @@ def make_lease_row(lease: V1Alpha1Lease): def print_lease(lease: V1Alpha1Lease, output: OutputType): match output: - case OutputMode.JSON | OutputMode.YAML: + case OutputMode.JSON | OutputMode.YAML | OutputMode.NAME: click.echo(lease.dump(output)) - case OutputMode.NAME: - click.echo(f"lease.jumpstarter.dev/{lease.metadata.name}") case _: click.echo(make_table(LEASE_COLUMNS, [make_lease_row(lease)])) def print_leases(leases: V1Alpha1List[V1Alpha1Lease], namespace: str, output: OutputType): match output: - case OutputMode.JSON | OutputMode.YAML: + case OutputMode.JSON | OutputMode.YAML | OutputMode.NAME: click.echo(leases.dump(output)) - case OutputMode.NAME: - for item in leases.items: - click.echo(f"lease.jumpstarter.dev/{item.metadata.name}") case _: if len(leases.items) == 0: raise click.ClickException(f'No resources found in "{namespace}" namespace') diff --git a/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/list.py b/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/list.py index 3c8dc41d1..85ac4719d 100644 --- a/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/list.py +++ b/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/list.py @@ -13,3 +13,6 @@ class V1Alpha1List(JsonBaseModel, Generic[T]): api_version: Literal["jumpstarter.dev/v1alpha1"] = Field(alias="apiVersion", default="jumpstarter.dev/v1alpha1") items: list[T] kind: Literal["List"] = Field(default="List") + + def dump_name(self): + return "\n".join(item.dump_name() for item in self.items) From b89ceac625cf10850d04cbecd8e5529be8d50788 Mon Sep 17 00:00:00 2001 From: Nick Cao Date: Mon, 31 Mar 2025 14:53:14 -0400 Subject: [PATCH 08/16] Suppress newline from click.echo --- .../jumpstarter_cli_admin/create.py | 5 ++-- .../jumpstarter_cli_admin/create_test.py | 2 -- .../jumpstarter_cli_admin/get_test.py | 9 ------ .../jumpstarter_cli_admin/print.py | 29 ++++++++++--------- .../jumpstarter_cli_common/__init__.py | 2 ++ .../jumpstarter_cli_common/echo.py | 5 ++++ .../jumpstarter_kubernetes/json.py | 2 +- .../jumpstarter_kubernetes/list.py | 2 +- .../jumpstarter/common/pydantic.py | 2 +- 9 files changed, 28 insertions(+), 30 deletions(-) create mode 100644 packages/jumpstarter-cli-common/jumpstarter_cli_common/echo.py diff --git a/packages/jumpstarter-cli-admin/jumpstarter_cli_admin/create.py b/packages/jumpstarter-cli-admin/jumpstarter_cli_admin/create.py index c8f5aaa12..938618f84 100644 --- a/packages/jumpstarter-cli-admin/jumpstarter_cli_admin/create.py +++ b/packages/jumpstarter-cli-admin/jumpstarter_cli_admin/create.py @@ -5,6 +5,7 @@ from jumpstarter_cli_common import ( AliasedGroup, OutputType, + echo, opt_context, opt_kubeconfig, opt_labels, @@ -38,7 +39,7 @@ def create(log_level: Optional[str]): def print_created_client(client: V1Alpha1Client, output: OutputType): if output is not None: - click.echo(client.dump(output)) + echo(client.dump(output)) @create.command("client") @@ -123,7 +124,7 @@ async def create_client( def print_created_exporter(exporter: V1Alpha1Exporter, output: OutputType): if output is not None: - click.echo(exporter.dump(output)) + echo(exporter.dump(output)) @create.command("exporter") diff --git a/packages/jumpstarter-cli-admin/jumpstarter_cli_admin/create_test.py b/packages/jumpstarter-cli-admin/jumpstarter_cli_admin/create_test.py index 5781321d2..b9570d790 100644 --- a/packages/jumpstarter-cli-admin/jumpstarter_cli_admin/create_test.py +++ b/packages/jumpstarter-cli-admin/jumpstarter_cli_admin/create_test.py @@ -66,7 +66,6 @@ credential: name: {name}-credential endpoint: {endpoint} - """.format(name=CLIENT_NAME, endpoint=CLIENT_ENDPOINT) UNSAFE_CLIENT_CONFIG = ClientConfigV1Alpha1( @@ -214,7 +213,6 @@ async def test_create_client( name: {name}-credential devices: [] endpoint: {endpoint} - """.format(name=EXPORTER_NAME, endpoint=EXPORTER_ENDPOINT) EXPORTER_CONFIG = ExporterConfigV1Alpha1( diff --git a/packages/jumpstarter-cli-admin/jumpstarter_cli_admin/get_test.py b/packages/jumpstarter-cli-admin/jumpstarter_cli_admin/get_test.py index fa7521d12..d133074ed 100644 --- a/packages/jumpstarter-cli-admin/jumpstarter_cli_admin/get_test.py +++ b/packages/jumpstarter-cli-admin/jumpstarter_cli_admin/get_test.py @@ -75,7 +75,6 @@ def getheaders(self): credential: name: test-credential endpoint: grpc://example.com:443 - """ @@ -217,7 +216,6 @@ async def test_get_client(_load_kube_config_mock, get_client_mock: AsyncMock): name: another-credential endpoint: grpc://example.com:443 kind: ClientList - """ CLIENTS_LIST_NAME = """client.jumpstarter.dev/test @@ -227,7 +225,6 @@ async def test_get_client(_load_kube_config_mock, get_client_mock: AsyncMock): CLIENTS_LIST_EMPTY_YAML = """apiVersion: jumpstarter.dev/v1alpha1 items: [] kind: ClientList - """ @@ -334,7 +331,6 @@ async def test_get_clients(_load_kube_config_mock, list_clients_mock: AsyncMock) name: test-credential devices: [] endpoint: grpc://example.com:443 - """ @@ -448,7 +444,6 @@ async def test_get_exporter(_load_kube_config_mock, get_exporter_mock: AsyncMock hardware: rpi4 uuid: f7cd30ac-64a3-42c6-ba31-b25f033b97c1 endpoint: grpc://example.com:443 - """ @@ -592,7 +587,6 @@ async def test_get_exporter_devices(_load_kube_config_mock, get_exporter_mock: A devices: [] endpoint: grpc://example.com:443 kind: ExporterList - """ EXPORTERS_LIST_NAME = """exporter.jumpstarter.dev/test @@ -760,7 +754,6 @@ async def test_get_exporters(_load_kube_config_mock, list_exporters_mock: AsyncM uuid: f7cd30ac-64a3-42c6-ba31-b25f033b97c1 endpoint: grpc://example.com:443 kind: ExporterList - """ EXPORTERS_DEVICES_LIST_NAME = EXPORTERS_LIST_NAME @@ -936,7 +929,6 @@ async def test_get_exporters_devices(_load_kube_config_mock, list_exporters_mock ended: true exporter: name: test_exporter - """ @@ -1145,7 +1137,6 @@ async def test_get_lease(_load_kube_config_mock, get_lease_mock: AsyncMock): exporter: name: test_exporter kind: LeaseList - """ LEASES_LIST_NAME = """lease.jumpstarter.dev/82a8ac0d-d7ff-4009-8948-18a3c5c607b1 diff --git a/packages/jumpstarter-cli-admin/jumpstarter_cli_admin/print.py b/packages/jumpstarter-cli-admin/jumpstarter_cli_admin/print.py index d10574725..1978390fb 100644 --- a/packages/jumpstarter-cli-admin/jumpstarter_cli_admin/print.py +++ b/packages/jumpstarter-cli-admin/jumpstarter_cli_admin/print.py @@ -2,6 +2,7 @@ from jumpstarter_cli_common import ( OutputMode, OutputType, + echo, make_table, time_since, ) @@ -26,19 +27,19 @@ def make_client_row(client: V1Alpha1Client): def print_client(client: V1Alpha1Client, output: OutputType): match output: case OutputMode.JSON | OutputMode.YAML | OutputMode.NAME: - click.echo(client.dump(output)) + echo(client.dump(output)) case _: - click.echo(make_table(CLIENT_COLUMNS, [make_client_row(client)])) + echo(make_table(CLIENT_COLUMNS, [make_client_row(client)])) def print_clients(clients: V1Alpha1List[V1Alpha1Client], namespace: str, output: OutputType): match output: case OutputMode.JSON | OutputMode.YAML | OutputMode.NAME: - click.echo(clients.dump(output)) + echo(clients.dump(output)) case _: if len(clients.items) == 0: raise click.ClickException(f'No resources found in "{namespace}" namespace') - click.echo(make_table(CLIENT_COLUMNS, list(map(make_client_row, clients.items)))) + echo(make_table(CLIENT_COLUMNS, list(map(make_client_row, clients.items)))) EXPORTER_COLUMNS = ["NAME", "ENDPOINT", "DEVICES", "AGE"] @@ -80,26 +81,26 @@ def get_device_rows(exporters: list[V1Alpha1Exporter]): def print_exporter(exporter: V1Alpha1Exporter, devices: bool, output: OutputType): match output: case OutputMode.JSON | OutputMode.YAML | OutputMode.NAME: - click.echo(exporter.dump(output)) + echo(exporter.dump(output)) case _: if devices: # Print the devices for the exporter - click.echo(make_table(DEVICE_COLUMNS, get_device_rows([exporter]))) + echo(make_table(DEVICE_COLUMNS, get_device_rows([exporter]))) else: - click.echo(make_table(EXPORTER_COLUMNS, [make_exporter_row(exporter)])) + echo(make_table(EXPORTER_COLUMNS, [make_exporter_row(exporter)])) def print_exporters(exporters: V1Alpha1List[V1Alpha1Exporter], namespace: str, devices: bool, output: OutputType): match output: case OutputMode.JSON | OutputMode.YAML | OutputMode.NAME: - click.echo(exporters.dump(output)) + echo(exporters.dump(output)) case _: if len(exporters.items) == 0: raise click.ClickException(f'No resources found in "{namespace}" namespace') if devices: - click.echo(make_table(DEVICE_COLUMNS, get_device_rows(exporters.items))) + echo(make_table(DEVICE_COLUMNS, get_device_rows(exporters.items))) else: - click.echo(make_table(EXPORTER_COLUMNS, list(map(make_exporter_row, exporters.items)))) + echo(make_table(EXPORTER_COLUMNS, list(map(make_exporter_row, exporters.items)))) LEASE_COLUMNS = ["NAME", "CLIENT", "SELECTOR", "EXPORTER", "STATUS", "REASON", "BEGIN", "END", "DURATION", "AGE"] @@ -144,16 +145,16 @@ def make_lease_row(lease: V1Alpha1Lease): def print_lease(lease: V1Alpha1Lease, output: OutputType): match output: case OutputMode.JSON | OutputMode.YAML | OutputMode.NAME: - click.echo(lease.dump(output)) + echo(lease.dump(output)) case _: - click.echo(make_table(LEASE_COLUMNS, [make_lease_row(lease)])) + echo(make_table(LEASE_COLUMNS, [make_lease_row(lease)])) def print_leases(leases: V1Alpha1List[V1Alpha1Lease], namespace: str, output: OutputType): match output: case OutputMode.JSON | OutputMode.YAML | OutputMode.NAME: - click.echo(leases.dump(output)) + echo(leases.dump(output)) case _: if len(leases.items) == 0: raise click.ClickException(f'No resources found in "{namespace}" namespace') - click.echo(make_table(LEASE_COLUMNS, list(map(make_lease_row, leases.items)))) + echo(make_table(LEASE_COLUMNS, list(map(make_lease_row, leases.items)))) diff --git a/packages/jumpstarter-cli-common/jumpstarter_cli_common/__init__.py b/packages/jumpstarter-cli-common/jumpstarter_cli_common/__init__.py index afd6343c8..33d4d4704 100644 --- a/packages/jumpstarter-cli-common/jumpstarter_cli_common/__init__.py +++ b/packages/jumpstarter-cli-common/jumpstarter_cli_common/__init__.py @@ -1,5 +1,6 @@ from .alias import AliasedGroup from .config import opt_config +from .echo import echo from .opt import ( NameOutputType, OutputMode, @@ -21,6 +22,7 @@ __all__ = [ "AliasedGroup", + "echo", "make_table", "opt_config", "opt_context", diff --git a/packages/jumpstarter-cli-common/jumpstarter_cli_common/echo.py b/packages/jumpstarter-cli-common/jumpstarter_cli_common/echo.py new file mode 100644 index 000000000..e78e8ed19 --- /dev/null +++ b/packages/jumpstarter-cli-common/jumpstarter_cli_common/echo.py @@ -0,0 +1,5 @@ +from functools import partial + +import asyncclick as click + +echo = partial(click.echo, nl=False) diff --git a/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/json.py b/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/json.py index fd6656ce7..8ccfbd0ff 100644 --- a/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/json.py +++ b/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/json.py @@ -14,6 +14,6 @@ def dump(self, mode: OutputType = None): return super().dump(mode) def dump_name(self): - return "{}.jumpstarter.dev/{}".format(self.kind.lower(), self.metadata.name) + return "{}.jumpstarter.dev/{}\n".format(self.kind.lower(), self.metadata.name) model_config = ConfigDict(arbitrary_types_allowed=True, populate_by_name=True) diff --git a/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/list.py b/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/list.py index 85ac4719d..1424fa758 100644 --- a/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/list.py +++ b/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/list.py @@ -15,4 +15,4 @@ class V1Alpha1List(JsonBaseModel, Generic[T]): kind: Literal["List"] = Field(default="List") def dump_name(self): - return "\n".join(item.dump_name() for item in self.items) + return "".join(item.dump_name() for item in self.items) diff --git a/packages/jumpstarter/jumpstarter/common/pydantic.py b/packages/jumpstarter/jumpstarter/common/pydantic.py index 367808eeb..aa2372573 100644 --- a/packages/jumpstarter/jumpstarter/common/pydantic.py +++ b/packages/jumpstarter/jumpstarter/common/pydantic.py @@ -23,7 +23,7 @@ def dump(self, mode: OutputType = None): raise NotImplementedError("unimplemented output mode: {}".format(mode)) def dump_json(self): - return self.model_dump_json(indent=4, by_alias=True) + return self.model_dump_json(indent=4, by_alias=True) + "\n" def dump_yaml(self): return yaml.safe_dump(self.model_dump(mode="json", by_alias=True), indent=2) From 8d4c69e1ef9738ead61ea30a2648ac10bf8071df Mon Sep 17 00:00:00 2001 From: Nick Cao Date: Mon, 31 Mar 2025 14:57:06 -0400 Subject: [PATCH 09/16] Tables still need the newline --- .../jumpstarter_cli_admin/print.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/jumpstarter-cli-admin/jumpstarter_cli_admin/print.py b/packages/jumpstarter-cli-admin/jumpstarter_cli_admin/print.py index 1978390fb..5ee51e3a6 100644 --- a/packages/jumpstarter-cli-admin/jumpstarter_cli_admin/print.py +++ b/packages/jumpstarter-cli-admin/jumpstarter_cli_admin/print.py @@ -29,7 +29,7 @@ def print_client(client: V1Alpha1Client, output: OutputType): case OutputMode.JSON | OutputMode.YAML | OutputMode.NAME: echo(client.dump(output)) case _: - echo(make_table(CLIENT_COLUMNS, [make_client_row(client)])) + click.echo(make_table(CLIENT_COLUMNS, [make_client_row(client)])) def print_clients(clients: V1Alpha1List[V1Alpha1Client], namespace: str, output: OutputType): @@ -39,7 +39,7 @@ def print_clients(clients: V1Alpha1List[V1Alpha1Client], namespace: str, output: case _: if len(clients.items) == 0: raise click.ClickException(f'No resources found in "{namespace}" namespace') - echo(make_table(CLIENT_COLUMNS, list(map(make_client_row, clients.items)))) + click.echo(make_table(CLIENT_COLUMNS, list(map(make_client_row, clients.items)))) EXPORTER_COLUMNS = ["NAME", "ENDPOINT", "DEVICES", "AGE"] @@ -85,9 +85,9 @@ def print_exporter(exporter: V1Alpha1Exporter, devices: bool, output: OutputType case _: if devices: # Print the devices for the exporter - echo(make_table(DEVICE_COLUMNS, get_device_rows([exporter]))) + click.echo(make_table(DEVICE_COLUMNS, get_device_rows([exporter]))) else: - echo(make_table(EXPORTER_COLUMNS, [make_exporter_row(exporter)])) + click.echo(make_table(EXPORTER_COLUMNS, [make_exporter_row(exporter)])) def print_exporters(exporters: V1Alpha1List[V1Alpha1Exporter], namespace: str, devices: bool, output: OutputType): @@ -98,9 +98,9 @@ def print_exporters(exporters: V1Alpha1List[V1Alpha1Exporter], namespace: str, d if len(exporters.items) == 0: raise click.ClickException(f'No resources found in "{namespace}" namespace') if devices: - echo(make_table(DEVICE_COLUMNS, get_device_rows(exporters.items))) + click.echo(make_table(DEVICE_COLUMNS, get_device_rows(exporters.items))) else: - echo(make_table(EXPORTER_COLUMNS, list(map(make_exporter_row, exporters.items)))) + click.echo(make_table(EXPORTER_COLUMNS, list(map(make_exporter_row, exporters.items)))) LEASE_COLUMNS = ["NAME", "CLIENT", "SELECTOR", "EXPORTER", "STATUS", "REASON", "BEGIN", "END", "DURATION", "AGE"] @@ -147,7 +147,7 @@ def print_lease(lease: V1Alpha1Lease, output: OutputType): case OutputMode.JSON | OutputMode.YAML | OutputMode.NAME: echo(lease.dump(output)) case _: - echo(make_table(LEASE_COLUMNS, [make_lease_row(lease)])) + click.echo(make_table(LEASE_COLUMNS, [make_lease_row(lease)])) def print_leases(leases: V1Alpha1List[V1Alpha1Lease], namespace: str, output: OutputType): @@ -157,4 +157,4 @@ def print_leases(leases: V1Alpha1List[V1Alpha1Lease], namespace: str, output: Ou case _: if len(leases.items) == 0: raise click.ClickException(f'No resources found in "{namespace}" namespace') - echo(make_table(LEASE_COLUMNS, list(map(make_lease_row, leases.items)))) + click.echo(make_table(LEASE_COLUMNS, list(map(make_lease_row, leases.items)))) From 9b7e9c4d8f03823c49df9ad5772d53c9203a850f Mon Sep 17 00:00:00 2001 From: Nick Cao Date: Mon, 31 Mar 2025 15:04:51 -0400 Subject: [PATCH 10/16] Support additional dump modes --- packages/jumpstarter/jumpstarter/common/pydantic.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/jumpstarter/jumpstarter/common/pydantic.py b/packages/jumpstarter/jumpstarter/common/pydantic.py index aa2372573..1a32e9624 100644 --- a/packages/jumpstarter/jumpstarter/common/pydantic.py +++ b/packages/jumpstarter/jumpstarter/common/pydantic.py @@ -19,11 +19,15 @@ def dump(self, mode: OutputType = None): return self.dump_json() case OutputMode.YAML: return self.dump_yaml() - case _: + case OutputMode.NAME: + return self.dump_name() + case OutputMode.PATH: + return self.dump_path() + case None | _: raise NotImplementedError("unimplemented output mode: {}".format(mode)) - def dump_json(self): + def dump_json(self) -> str: return self.model_dump_json(indent=4, by_alias=True) + "\n" - def dump_yaml(self): + def dump_yaml(self) -> str: return yaml.safe_dump(self.model_dump(mode="json", by_alias=True), indent=2) From fe6436f78e7fd8f62e715c3424433a0508283dae Mon Sep 17 00:00:00 2001 From: Nick Cao Date: Mon, 31 Mar 2025 15:17:33 -0400 Subject: [PATCH 11/16] Implement magic opt_output_auto --- .../jumpstarter_cli_admin/create.py | 6 ++--- .../jumpstarter_cli_admin/get.py | 11 +++++--- .../jumpstarter_cli_common/__init__.py | 2 ++ .../jumpstarter_cli_common/opt.py | 27 +++++++++++++++++++ 4 files changed, 39 insertions(+), 7 deletions(-) diff --git a/packages/jumpstarter-cli-admin/jumpstarter_cli_admin/create.py b/packages/jumpstarter-cli-admin/jumpstarter_cli_admin/create.py index 938618f84..0e4829e01 100644 --- a/packages/jumpstarter-cli-admin/jumpstarter_cli_admin/create.py +++ b/packages/jumpstarter-cli-admin/jumpstarter_cli_admin/create.py @@ -12,7 +12,7 @@ opt_log_level, opt_namespace, opt_nointeractive, - opt_output_all, + opt_output_auto, ) from jumpstarter_kubernetes import ClientsV1Alpha1Api, ExportersV1Alpha1Api, V1Alpha1Client, V1Alpha1Exporter from kubernetes_asyncio.client.exceptions import ApiException @@ -71,7 +71,7 @@ def print_created_client(client: V1Alpha1Client, output: OutputType): @opt_context @opt_oidc_username @opt_nointeractive -@opt_output_all +@opt_output_auto(V1Alpha1Client) async def create_client( name: Optional[str], kubeconfig: Optional[str], @@ -148,7 +148,7 @@ def print_created_exporter(exporter: V1Alpha1Exporter, output: OutputType): @opt_context @opt_oidc_username @opt_nointeractive -@opt_output_all +@opt_output_auto(V1Alpha1Exporter) async def create_exporter( name: Optional[str], kubeconfig: Optional[str], diff --git a/packages/jumpstarter-cli-admin/jumpstarter_cli_admin/get.py b/packages/jumpstarter-cli-admin/jumpstarter_cli_admin/get.py index a5358b1e5..0ab0bb5d2 100644 --- a/packages/jumpstarter-cli-admin/jumpstarter_cli_admin/get.py +++ b/packages/jumpstarter-cli-admin/jumpstarter_cli_admin/get.py @@ -9,12 +9,15 @@ opt_kubeconfig, opt_log_level, opt_namespace, - opt_output_all, + opt_output_auto, ) from jumpstarter_kubernetes import ( ClientsV1Alpha1Api, ExportersV1Alpha1Api, LeasesV1Alpha1Api, + V1Alpha1Client, + V1Alpha1Exporter, + V1Alpha1Lease, ) from kubernetes_asyncio.client.exceptions import ApiException from kubernetes_asyncio.config.config_exception import ConfigException @@ -41,7 +44,7 @@ def get(log_level: Optional[str]): @opt_namespace @opt_kubeconfig @opt_context -@opt_output_all +@opt_output_auto(V1Alpha1Client) async def get_client( name: Optional[str], kubeconfig: Optional[str], context: Optional[str], namespace: str, output: OutputType ): @@ -65,7 +68,7 @@ async def get_client( @opt_namespace @opt_kubeconfig @opt_context -@opt_output_all +@opt_output_auto(V1Alpha1Exporter) @click.option("-d", "--devices", is_flag=True, help="Display the devices hosted by the exporter(s)") async def get_exporter( name: Optional[str], @@ -95,7 +98,7 @@ async def get_exporter( @opt_namespace @opt_kubeconfig @opt_context -@opt_output_all +@opt_output_auto(V1Alpha1Lease) async def get_lease( name: Optional[str], kubeconfig: Optional[str], context: Optional[str], namespace: str, output: OutputType ): diff --git a/packages/jumpstarter-cli-common/jumpstarter_cli_common/__init__.py b/packages/jumpstarter-cli-common/jumpstarter_cli_common/__init__.py index 33d4d4704..3eac5e179 100644 --- a/packages/jumpstarter-cli-common/jumpstarter_cli_common/__init__.py +++ b/packages/jumpstarter-cli-common/jumpstarter_cli_common/__init__.py @@ -13,6 +13,7 @@ opt_namespace, opt_nointeractive, opt_output_all, + opt_output_auto, opt_output_name_only, opt_output_path_only, ) @@ -34,6 +35,7 @@ "opt_output_all", "opt_output_name_only", "opt_output_path_only", + "opt_output_auto", "OutputMode", "OutputType", "NameOutputType", diff --git a/packages/jumpstarter-cli-common/jumpstarter_cli_common/opt.py b/packages/jumpstarter-cli-common/jumpstarter_cli_common/opt.py index a6562649d..a1baf5349 100644 --- a/packages/jumpstarter-cli-common/jumpstarter_cli_common/opt.py +++ b/packages/jumpstarter-cli-common/jumpstarter_cli_common/opt.py @@ -55,3 +55,30 @@ opt_nointeractive = click.option( "--nointeractive", is_flag=True, default=False, help="Disable interactive prompts (for use in scripts)." ) + + +def opt_output_auto(cls): + choices = [] + if hasattr(cls, "dump_json"): + choices.append(OutputMode.JSON) + if hasattr(cls, "dump_yaml"): + choices.append(OutputMode.YAML) + if hasattr(cls, "dump_name"): + choices.append(OutputMode.NAME) + if hasattr(cls, "dump_path"): + choices.append(OutputMode.PATH) + + if OutputMode.PATH in choices: + help = 'Output mode. Use "-o path" for shorter output (file/path).' + elif OutputMode.NAME in choices: + help = 'Output mode. Use "-o name" for shorter output (resource/name).' + else: + help = "Output mode." + + return click.option( + "-o", + "--output", + type=click.Choice(choices), + default=None, + help=help, + ) From d5acf3bd381083c54240ee861c57b25ba222dba0 Mon Sep 17 00:00:00 2001 From: Nick Cao Date: Mon, 31 Mar 2025 15:28:24 -0400 Subject: [PATCH 12/16] Drop redundant match clause --- .../jumpstarter_cli_admin/print.py | 81 +++++++++---------- 1 file changed, 37 insertions(+), 44 deletions(-) diff --git a/packages/jumpstarter-cli-admin/jumpstarter_cli_admin/print.py b/packages/jumpstarter-cli-admin/jumpstarter_cli_admin/print.py index 5ee51e3a6..808569e3a 100644 --- a/packages/jumpstarter-cli-admin/jumpstarter_cli_admin/print.py +++ b/packages/jumpstarter-cli-admin/jumpstarter_cli_admin/print.py @@ -1,6 +1,5 @@ import asyncclick as click from jumpstarter_cli_common import ( - OutputMode, OutputType, echo, make_table, @@ -25,21 +24,19 @@ def make_client_row(client: V1Alpha1Client): def print_client(client: V1Alpha1Client, output: OutputType): - match output: - case OutputMode.JSON | OutputMode.YAML | OutputMode.NAME: - echo(client.dump(output)) - case _: - click.echo(make_table(CLIENT_COLUMNS, [make_client_row(client)])) + if output: + echo(client.dump(output)) + else: + click.echo(make_table(CLIENT_COLUMNS, [make_client_row(client)])) def print_clients(clients: V1Alpha1List[V1Alpha1Client], namespace: str, output: OutputType): - match output: - case OutputMode.JSON | OutputMode.YAML | OutputMode.NAME: - echo(clients.dump(output)) - case _: - if len(clients.items) == 0: - raise click.ClickException(f'No resources found in "{namespace}" namespace') - click.echo(make_table(CLIENT_COLUMNS, list(map(make_client_row, clients.items)))) + if output: + echo(clients.dump(output)) + else: + if len(clients.items) == 0: + raise click.ClickException(f'No resources found in "{namespace}" namespace') + click.echo(make_table(CLIENT_COLUMNS, list(map(make_client_row, clients.items)))) EXPORTER_COLUMNS = ["NAME", "ENDPOINT", "DEVICES", "AGE"] @@ -79,28 +76,26 @@ def get_device_rows(exporters: list[V1Alpha1Exporter]): def print_exporter(exporter: V1Alpha1Exporter, devices: bool, output: OutputType): - match output: - case OutputMode.JSON | OutputMode.YAML | OutputMode.NAME: - echo(exporter.dump(output)) - case _: - if devices: - # Print the devices for the exporter - click.echo(make_table(DEVICE_COLUMNS, get_device_rows([exporter]))) - else: - click.echo(make_table(EXPORTER_COLUMNS, [make_exporter_row(exporter)])) + if output: + echo(exporter.dump(output)) + else: + if devices: + # Print the devices for the exporter + click.echo(make_table(DEVICE_COLUMNS, get_device_rows([exporter]))) + else: + click.echo(make_table(EXPORTER_COLUMNS, [make_exporter_row(exporter)])) def print_exporters(exporters: V1Alpha1List[V1Alpha1Exporter], namespace: str, devices: bool, output: OutputType): - match output: - case OutputMode.JSON | OutputMode.YAML | OutputMode.NAME: - echo(exporters.dump(output)) - case _: - if len(exporters.items) == 0: - raise click.ClickException(f'No resources found in "{namespace}" namespace') - if devices: - click.echo(make_table(DEVICE_COLUMNS, get_device_rows(exporters.items))) - else: - click.echo(make_table(EXPORTER_COLUMNS, list(map(make_exporter_row, exporters.items)))) + if output: + echo(exporters.dump(output)) + else: + if len(exporters.items) == 0: + raise click.ClickException(f'No resources found in "{namespace}" namespace') + if devices: + click.echo(make_table(DEVICE_COLUMNS, get_device_rows(exporters.items))) + else: + click.echo(make_table(EXPORTER_COLUMNS, list(map(make_exporter_row, exporters.items)))) LEASE_COLUMNS = ["NAME", "CLIENT", "SELECTOR", "EXPORTER", "STATUS", "REASON", "BEGIN", "END", "DURATION", "AGE"] @@ -143,18 +138,16 @@ def make_lease_row(lease: V1Alpha1Lease): def print_lease(lease: V1Alpha1Lease, output: OutputType): - match output: - case OutputMode.JSON | OutputMode.YAML | OutputMode.NAME: - echo(lease.dump(output)) - case _: - click.echo(make_table(LEASE_COLUMNS, [make_lease_row(lease)])) + if output: + echo(lease.dump(output)) + else: + click.echo(make_table(LEASE_COLUMNS, [make_lease_row(lease)])) def print_leases(leases: V1Alpha1List[V1Alpha1Lease], namespace: str, output: OutputType): - match output: - case OutputMode.JSON | OutputMode.YAML | OutputMode.NAME: - echo(leases.dump(output)) - case _: - if len(leases.items) == 0: - raise click.ClickException(f'No resources found in "{namespace}" namespace') - click.echo(make_table(LEASE_COLUMNS, list(map(make_lease_row, leases.items)))) + if output: + echo(leases.dump(output)) + else: + if len(leases.items) == 0: + raise click.ClickException(f'No resources found in "{namespace}" namespace') + click.echo(make_table(LEASE_COLUMNS, list(map(make_lease_row, leases.items)))) From 2c50e118d3099f257e9e410ff53de876da3ed915 Mon Sep 17 00:00:00 2001 From: Nick Cao Date: Mon, 31 Mar 2025 15:39:35 -0400 Subject: [PATCH 13/16] Use output_mode_auto in more places --- .../jumpstarter-cli/jumpstarter_cli/create.py | 38 +++++------ .../jumpstarter-cli/jumpstarter_cli/get.py | 65 +++++++++---------- .../jumpstarter-cli/jumpstarter_cli/update.py | 36 +++++----- .../jumpstarter/jumpstarter/client/grpc.py | 18 +++-- 4 files changed, 78 insertions(+), 79 deletions(-) diff --git a/packages/jumpstarter-cli/jumpstarter_cli/create.py b/packages/jumpstarter-cli/jumpstarter_cli/create.py index 586ce69d9..762877676 100644 --- a/packages/jumpstarter-cli/jumpstarter_cli/create.py +++ b/packages/jumpstarter-cli/jumpstarter_cli/create.py @@ -2,15 +2,16 @@ import asyncclick as click from jumpstarter_cli_common import ( - OutputMode, OutputType, + echo, make_table, opt_config, - opt_output_all, + opt_output_auto, ) from jumpstarter_cli_common.exceptions import handle_exceptions from .common import opt_duration_partial, opt_selector +from jumpstarter.client.grpc import Lease @click.group() @@ -24,7 +25,7 @@ def create(): @opt_config(exporter=False) @opt_selector @opt_duration_partial(required=True) -@opt_output_all +@opt_output_auto(Lease) @handle_exceptions async def create_lease(config, selector: str, duration: timedelta, output: OutputType): """ @@ -54,20 +55,17 @@ async def create_lease(config, selector: str, duration: timedelta, output: Outpu lease = config.create_lease(selector=selector, duration=duration) - match output: - case OutputMode.JSON | OutputMode.YAML: - click.echo(lease.dump(output)) - case OutputMode.NAME: - click.echo(lease.name) - case _: - columns = ["NAME", "SELECTOR", "DURATION", "CLIENT", "EXPORTER"] - rows = [ - { - "NAME": lease.name, - "SELECTOR": lease.selector, - "DURATION": str(lease.duration.total_seconds()), - "CLIENT": lease.client, - "EXPORTER": lease.exporter, - } - ] - click.echo(make_table(columns, rows)) + if output: + echo(lease.dump(output)) + else: + columns = ["NAME", "SELECTOR", "DURATION", "CLIENT", "EXPORTER"] + rows = [ + { + "NAME": lease.name, + "SELECTOR": lease.selector, + "DURATION": str(lease.duration.total_seconds()), + "CLIENT": lease.client, + "EXPORTER": lease.exporter, + } + ] + click.echo(make_table(columns, rows)) diff --git a/packages/jumpstarter-cli/jumpstarter_cli/get.py b/packages/jumpstarter-cli/jumpstarter_cli/get.py index d209f5614..669077f19 100644 --- a/packages/jumpstarter-cli/jumpstarter_cli/get.py +++ b/packages/jumpstarter-cli/jumpstarter_cli/get.py @@ -1,8 +1,9 @@ import asyncclick as click -from jumpstarter_cli_common import OutputMode, OutputType, make_table, opt_config, opt_output_all +from jumpstarter_cli_common import OutputType, make_table, opt_config, opt_output_auto from jumpstarter_cli_common.exceptions import handle_exceptions from .common import opt_selector +from jumpstarter.client.grpc import ExporterList, LeaseList @click.group() @@ -15,7 +16,7 @@ def get(): @get.command(name="exporters") @opt_config(exporter=False) @opt_selector -@opt_output_all +@opt_output_auto(ExporterList) @handle_exceptions def get_exporters(config, selector: str | None, output: OutputType): """ @@ -24,28 +25,24 @@ def get_exporters(config, selector: str | None, output: OutputType): exporters = config.list_exporters(filter=selector) - match output: - case OutputMode.JSON | OutputMode.YAML: - click.echo(exporters.dump(output)) - case OutputMode.NAME: - for exporter in exporters.exporters: - click.echo(exporter.name) - case _: - columns = ["NAME", "LABELS"] - rows = [ - { - "NAME": exporter.name, - "LABELS": ",".join(("{}={}".format(i[0], i[1]) for i in exporter.labels.items())), - } - for exporter in exporters.exporters - ] - click.echo(make_table(columns, rows)) + if output: + click.echo(exporters.dump(output)) + else: + columns = ["NAME", "LABELS"] + rows = [ + { + "NAME": exporter.name, + "LABELS": ",".join(("{}={}".format(i[0], i[1]) for i in exporter.labels.items())), + } + for exporter in exporters.exporters + ] + click.echo(make_table(columns, rows)) @get.command(name="leases") @opt_config(exporter=False) @opt_selector -@opt_output_all +@opt_output_auto(LeaseList) @handle_exceptions def get_leases(config, selector: str | None, output: OutputType): """ @@ -54,20 +51,16 @@ def get_leases(config, selector: str | None, output: OutputType): leases = config.list_leases(filter=selector) - match output: - case OutputMode.JSON | OutputMode.YAML: - click.echo(leases.dump(output)) - case OutputMode.NAME: - for lease in leases.leases: - click.echo(lease.name) - case _: - columns = ["NAME", "CLIENT", "EXPORTER"] - rows = [ - { - "NAME": lease.name, - "CLIENT": lease.client, - "EXPORTER": lease.exporter, - } - for lease in leases.leases - ] - click.echo(make_table(columns, rows)) + if output: + click.echo(leases.dump(output)) + else: + columns = ["NAME", "CLIENT", "EXPORTER"] + rows = [ + { + "NAME": lease.name, + "CLIENT": lease.client, + "EXPORTER": lease.exporter, + } + for lease in leases.leases + ] + click.echo(make_table(columns, rows)) diff --git a/packages/jumpstarter-cli/jumpstarter_cli/update.py b/packages/jumpstarter-cli/jumpstarter_cli/update.py index 15693cc02..268e63eaa 100644 --- a/packages/jumpstarter-cli/jumpstarter_cli/update.py +++ b/packages/jumpstarter-cli/jumpstarter_cli/update.py @@ -1,10 +1,11 @@ from datetime import timedelta import asyncclick as click -from jumpstarter_cli_common import OutputMode, OutputType, make_table, opt_config, opt_output_all +from jumpstarter_cli_common import OutputType, echo, make_table, opt_config, opt_output_auto from jumpstarter_cli_common.exceptions import handle_exceptions from .common import opt_duration_partial +from jumpstarter.client.grpc import Lease @click.group() @@ -18,7 +19,7 @@ def update(): @opt_config(exporter=False) @click.argument("name") @opt_duration_partial(required=True) -@opt_output_all +@opt_output_auto(Lease) @handle_exceptions async def update_lease(config, name: str, duration: timedelta, output: OutputType): """ @@ -27,20 +28,17 @@ async def update_lease(config, name: str, duration: timedelta, output: OutputTyp lease = config.update_lease(name, duration) - match output: - case OutputMode.JSON | OutputMode.YAML: - click.echo(lease.dump(output)) - case OutputMode.NAME: - click.echo(lease.name) - case _: - columns = ["NAME", "SELECTOR", "DURATION", "CLIENT", "EXPORTER"] - rows = [ - { - "NAME": lease.name, - "SELECTOR": lease.selector, - "DURATION": str(lease.duration.total_seconds()), - "CLIENT": lease.client, - "EXPORTER": lease.exporter, - } - ] - click.echo(make_table(columns, rows)) + if output: + echo(lease.dump(output)) + else: + columns = ["NAME", "SELECTOR", "DURATION", "CLIENT", "EXPORTER"] + rows = [ + { + "NAME": lease.name, + "SELECTOR": lease.selector, + "DURATION": str(lease.duration.total_seconds()), + "CLIENT": lease.client, + "EXPORTER": lease.exporter, + } + ] + click.echo(make_table(columns, rows)) diff --git a/packages/jumpstarter/jumpstarter/client/grpc.py b/packages/jumpstarter/jumpstarter/client/grpc.py index f842d08ac..84f2a2359 100644 --- a/packages/jumpstarter/jumpstarter/client/grpc.py +++ b/packages/jumpstarter/jumpstarter/client/grpc.py @@ -35,9 +35,15 @@ def parse_lease_identifier(identifier: str) -> (str, str): return parse_identifier(identifier, "leases") -class Exporter(SerializableBaseModel): +class GrpcObject(SerializableBaseModel): namespace: str name: str + + def dump_name(self) -> str: + return self.name + "\n" + + +class Exporter(GrpcObject): labels: dict[str, str] @classmethod @@ -46,9 +52,7 @@ def from_protobuf(cls, data: client_pb2.Exporter) -> Exporter: return cls(namespace=namespace, name=name, labels=data.labels) -class Lease(SerializableBaseModel): - namespace: str - name: str +class Lease(GrpcObject): selector: str duration: timedelta client: str @@ -97,6 +101,9 @@ class ExporterList(SerializableBaseModel): exporters: list[Exporter] next_page_token: str | None = Field(exclude=True) + def dump_name(self) -> str: + return "".join(exporter.dump_name() for exporter in self.exporters) + @classmethod def from_protobuf(cls, data: client_pb2.ListExportersResponse) -> ExporterList: return cls( @@ -109,6 +116,9 @@ class LeaseList(SerializableBaseModel): leases: list[Lease] next_page_token: str | None = Field(exclude=True) + def dump_name(self) -> str: + return "".join(lease.dump_name() for lease in self.leases) + @classmethod def from_protobuf(cls, data: client_pb2.ListLeasesResponse) -> LeaseList: return cls( From 48a637ef46362afb30d6c36a86c704dbd75c3468 Mon Sep 17 00:00:00 2001 From: Nick Cao Date: Mon, 31 Mar 2025 15:43:38 -0400 Subject: [PATCH 14/16] Drop more use of output mode --- .../jumpstarter_cli_common/version.py | 11 +++++------ .../jumpstarter_kubernetes/json.py | 9 +-------- 2 files changed, 6 insertions(+), 14 deletions(-) diff --git a/packages/jumpstarter-cli-common/jumpstarter_cli_common/version.py b/packages/jumpstarter-cli-common/jumpstarter_cli_common/version.py index 94dcb9585..8372b6f21 100644 --- a/packages/jumpstarter-cli-common/jumpstarter_cli_common/version.py +++ b/packages/jumpstarter-cli-common/jumpstarter_cli_common/version.py @@ -5,7 +5,8 @@ import asyncclick as click from pydantic import ConfigDict, Field -from .opt import OutputMode, OutputType, opt_output_all +from .echo import echo +from .opt import OutputType, opt_output_auto from jumpstarter.common.pydantic import SerializableBaseModel @@ -39,12 +40,10 @@ def version_obj(): @click.command() -@opt_output_all +@opt_output_auto(JumpstarterVersion) def version(output: OutputType): """Get the current Jumpstarter version""" - if output == OutputMode.JSON: - click.echo(version_obj().dump_json()) - elif output == OutputMode.YAML: - click.echo(version_obj().dump_yaml()) + if output: + echo(version_obj().dump(output)) else: click.echo(version_msg()) diff --git a/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/json.py b/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/json.py index 8ccfbd0ff..9aafefcc9 100644 --- a/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/json.py +++ b/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/json.py @@ -1,18 +1,11 @@ from pydantic import ConfigDict -from jumpstarter.common.pydantic import OutputMode, OutputType, SerializableBaseModel +from jumpstarter.common.pydantic import SerializableBaseModel class JsonBaseModel(SerializableBaseModel): """A Pydantic BaseModel with additional Jumpstarter JSON options applied.""" - def dump(self, mode: OutputType = None): - match mode: - case OutputMode.NAME: - return self.dump_name() - case _: - return super().dump(mode) - def dump_name(self): return "{}.jumpstarter.dev/{}\n".format(self.kind.lower(), self.metadata.name) From e26a246d276f8fcde07377f9ba3bd51d7b8431dd Mon Sep 17 00:00:00 2001 From: Nick Cao Date: Mon, 31 Mar 2025 15:56:07 -0400 Subject: [PATCH 15/16] Fixup tests --- .../jumpstarter_kubernetes/clients_test.py | 3 ++- .../jumpstarter_kubernetes/exporters_test.py | 3 ++- .../jumpstarter_kubernetes/test_leases.py | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/clients_test.py b/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/clients_test.py index d42383294..b9d534c05 100644 --- a/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/clients_test.py +++ b/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/clients_test.py @@ -35,7 +35,8 @@ def test_client_dump_json(): "credential": null, "endpoint": "https://test-client" } -}""" +} +""" ) diff --git a/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/exporters_test.py b/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/exporters_test.py index 78104c0ad..5aef2474c 100644 --- a/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/exporters_test.py +++ b/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/exporters_test.py @@ -49,7 +49,8 @@ def test_exporter_dump_json(): ], "endpoint": "https://test-exporter" } -}""" +} +""" ) diff --git a/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/test_leases.py b/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/test_leases.py index ef523d38c..2cb2d07be 100644 --- a/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/test_leases.py +++ b/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/test_leases.py @@ -76,7 +76,8 @@ def test_lease_dump_json(): "name": "test-exporter" } } -}""" +} +""" ) From ca9347422938a32279a21db2ab0c74ad7467821f Mon Sep 17 00:00:00 2001 From: Nick Cao Date: Tue, 15 Apr 2025 15:05:51 -0400 Subject: [PATCH 16/16] Generalize SerializableBaseModelList --- .../jumpstarter_cli_admin/print.py | 10 ++++++---- .../jumpstarter_kubernetes/__init__.py | 2 -- .../jumpstarter_kubernetes/clients.py | 6 +++--- .../jumpstarter_kubernetes/exporters.py | 6 +++--- .../jumpstarter_kubernetes/leases.py | 6 +++--- .../jumpstarter_kubernetes/list.py | 18 ------------------ .../jumpstarter/jumpstarter/common/pydantic.py | 18 +++++++++++++++++- 7 files changed, 32 insertions(+), 34 deletions(-) delete mode 100644 packages/jumpstarter-kubernetes/jumpstarter_kubernetes/list.py diff --git a/packages/jumpstarter-cli-admin/jumpstarter_cli_admin/print.py b/packages/jumpstarter-cli-admin/jumpstarter_cli_admin/print.py index 808569e3a..3ca3fdbac 100644 --- a/packages/jumpstarter-cli-admin/jumpstarter_cli_admin/print.py +++ b/packages/jumpstarter-cli-admin/jumpstarter_cli_admin/print.py @@ -9,8 +9,8 @@ V1Alpha1Client, V1Alpha1Exporter, V1Alpha1Lease, - V1Alpha1List, ) +from jumpstarter.common.pydantic import SerializableBaseModelList CLIENT_COLUMNS = ["NAME", "ENDPOINT", "AGE"] @@ -30,7 +30,7 @@ def print_client(client: V1Alpha1Client, output: OutputType): click.echo(make_table(CLIENT_COLUMNS, [make_client_row(client)])) -def print_clients(clients: V1Alpha1List[V1Alpha1Client], namespace: str, output: OutputType): +def print_clients(clients: SerializableBaseModelList[V1Alpha1Client], namespace: str, output: OutputType): if output: echo(clients.dump(output)) else: @@ -86,7 +86,9 @@ def print_exporter(exporter: V1Alpha1Exporter, devices: bool, output: OutputType click.echo(make_table(EXPORTER_COLUMNS, [make_exporter_row(exporter)])) -def print_exporters(exporters: V1Alpha1List[V1Alpha1Exporter], namespace: str, devices: bool, output: OutputType): +def print_exporters( + exporters: SerializableBaseModelList[V1Alpha1Exporter], namespace: str, devices: bool, output: OutputType +): if output: echo(exporters.dump(output)) else: @@ -144,7 +146,7 @@ def print_lease(lease: V1Alpha1Lease, output: OutputType): click.echo(make_table(LEASE_COLUMNS, [make_lease_row(lease)])) -def print_leases(leases: V1Alpha1List[V1Alpha1Lease], namespace: str, output: OutputType): +def print_leases(leases: SerializableBaseModelList[V1Alpha1Lease], namespace: str, output: OutputType): if output: echo(leases.dump(output)) else: diff --git a/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/__init__.py b/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/__init__.py index 6a9bf1b76..0e6db65b4 100644 --- a/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/__init__.py +++ b/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/__init__.py @@ -15,7 +15,6 @@ V1Alpha1LeaseSpec, V1Alpha1LeaseStatus, ) -from .list import V1Alpha1List __all__ = [ "ClientsV1Alpha1Api", @@ -33,7 +32,6 @@ "V1Alpha1LeaseList", "V1Alpha1LeaseSelector", "V1Alpha1LeaseSpec", - "V1Alpha1List", "get_ip_address", "helm_installed", "install_helm_chart", diff --git a/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/clients.py b/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/clients.py index 836e44b6d..8ce69005c 100644 --- a/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/clients.py +++ b/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/clients.py @@ -7,9 +7,9 @@ from pydantic import Field from .json import JsonBaseModel -from .list import V1Alpha1List from .serialize import SerializeV1ObjectMeta, SerializeV1ObjectReference from .util import AbstractAsyncCustomObjectApi +from jumpstarter.common.pydantic import SerializableBaseModelList from jumpstarter.config import ClientConfigV1Alpha1, ClientConfigV1Alpha1Drivers, ObjectMeta logger = logging.getLogger(__name__) @@ -53,7 +53,7 @@ def from_dict(dict: dict): ) -class V1Alpha1ClientList(V1Alpha1List[V1Alpha1Client]): +class V1Alpha1ClientList(SerializableBaseModelList[V1Alpha1Client]): kind: Literal["ClientList"] = Field(default="ClientList") @staticmethod @@ -99,7 +99,7 @@ async def create_client( await asyncio.sleep(CREATE_CLIENT_DELAY) raise Exception("Timeout waiting for client credentials") - async def list_clients(self) -> V1Alpha1List[V1Alpha1Client]: + async def list_clients(self) -> SerializableBaseModelList[V1Alpha1Client]: """List the client objects in the cluster async""" res = await self.api.list_namespaced_custom_object( namespace=self.namespace, group="jumpstarter.dev", plural="clients", version="v1alpha1" diff --git a/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/exporters.py b/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/exporters.py index 56cb3af84..95077d996 100644 --- a/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/exporters.py +++ b/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/exporters.py @@ -6,9 +6,9 @@ from pydantic import Field from .json import JsonBaseModel -from .list import V1Alpha1List from .serialize import SerializeV1ObjectMeta, SerializeV1ObjectReference from .util import AbstractAsyncCustomObjectApi +from jumpstarter.common.pydantic import SerializableBaseModelList from jumpstarter.config import ExporterConfigV1Alpha1, ObjectMeta CREATE_EXPORTER_DELAY = 1 @@ -57,7 +57,7 @@ def from_dict(dict: dict): ) -class V1Alpha1ExporterList(V1Alpha1List[V1Alpha1Exporter]): +class V1Alpha1ExporterList(SerializableBaseModelList[V1Alpha1Exporter]): kind: Literal["ExporterList"] = Field(default="ExporterList") @staticmethod @@ -68,7 +68,7 @@ def from_dict(dict: dict): class ExportersV1Alpha1Api(AbstractAsyncCustomObjectApi): """Interact with the exporters custom resource API""" - async def list_exporters(self) -> V1Alpha1List[V1Alpha1Exporter]: + async def list_exporters(self) -> SerializableBaseModelList[V1Alpha1Exporter]: """List the exporter objects in the cluster""" res = await self.api.list_namespaced_custom_object( namespace=self.namespace, group="jumpstarter.dev", plural="exporters", version="v1alpha1" diff --git a/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/leases.py b/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/leases.py index 67361684c..f4143a36f 100644 --- a/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/leases.py +++ b/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/leases.py @@ -4,9 +4,9 @@ from pydantic import Field from .json import JsonBaseModel -from .list import V1Alpha1List from .serialize import SerializeV1Condition, SerializeV1ObjectMeta, SerializeV1ObjectReference from .util import AbstractAsyncCustomObjectApi +from jumpstarter.common.pydantic import SerializableBaseModelList class V1Alpha1LeaseStatus(JsonBaseModel): @@ -77,7 +77,7 @@ def from_dict(dict: dict): ) -class V1Alpha1LeaseList(V1Alpha1List[V1Alpha1Lease]): +class V1Alpha1LeaseList(SerializableBaseModelList[V1Alpha1Lease]): kind: Literal["LeaseList"] = Field(default="LeaseList") @staticmethod @@ -88,7 +88,7 @@ def from_dict(dict: dict): class LeasesV1Alpha1Api(AbstractAsyncCustomObjectApi): """Interact with the leases custom resource API""" - async def list_leases(self) -> V1Alpha1List[V1Alpha1Lease]: + async def list_leases(self) -> SerializableBaseModelList[V1Alpha1Lease]: """List the lease objects in the cluster async""" result = await self.api.list_namespaced_custom_object( namespace=self.namespace, group="jumpstarter.dev", plural="leases", version="v1alpha1" diff --git a/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/list.py b/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/list.py deleted file mode 100644 index 1424fa758..000000000 --- a/packages/jumpstarter-kubernetes/jumpstarter_kubernetes/list.py +++ /dev/null @@ -1,18 +0,0 @@ -from typing import Generic, Literal, TypeVar - -from pydantic import Field - -from .json import JsonBaseModel - -T = TypeVar("T") - - -class V1Alpha1List(JsonBaseModel, Generic[T]): - """A generic list result type.""" - - api_version: Literal["jumpstarter.dev/v1alpha1"] = Field(alias="apiVersion", default="jumpstarter.dev/v1alpha1") - items: list[T] - kind: Literal["List"] = Field(default="List") - - def dump_name(self): - return "".join(item.dump_name() for item in self.items) diff --git a/packages/jumpstarter/jumpstarter/common/pydantic.py b/packages/jumpstarter/jumpstarter/common/pydantic.py index 1a32e9624..2453ffe45 100644 --- a/packages/jumpstarter/jumpstarter/common/pydantic.py +++ b/packages/jumpstarter/jumpstarter/common/pydantic.py @@ -1,5 +1,9 @@ +from typing import Generic, Literal, TypeVar + import yaml -from pydantic import BaseModel +from pydantic import BaseModel, Field + +T = TypeVar("T") class OutputMode(str): @@ -31,3 +35,15 @@ def dump_json(self) -> str: def dump_yaml(self) -> str: return yaml.safe_dump(self.model_dump(mode="json", by_alias=True), indent=2) + + +class SerializableBaseModelList(SerializableBaseModel, Generic[T]): + api_version: Literal["jumpstarter.dev/v1alpha1"] = Field( + alias="apiVersion", + default="jumpstarter.dev/v1alpha1", + ) + kind: Literal["List"] = Field(default="List") + items: list[T] + + def dump_name(self): + return "".join(item.dump_name() for item in self.items)