Skip to content
This repository was archived by the owner on Jan 23, 2026. It is now read-only.

Commit 6a9eb05

Browse files
committed
Add basic package list functionality to jmp pkg CLI
1 parent c819d5c commit 6a9eb05

16 files changed

Lines changed: 541 additions & 300 deletions

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# Jumpstarter Package Management CLI
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import asyncclick as click
2+
from jumpstarter_cli_common import AliasedGroup
3+
4+
from .commands.list import list
5+
6+
7+
@click.group(cls=AliasedGroup)
8+
def pkg():
9+
"""Jumpstarter package management CLI tool"""
10+
11+
12+
pkg.add_command(list)
13+
14+
if __name__ == "__main__":
15+
pkg()
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
"""Allow running Jumpstarter through `python -m jumpstarter_cli_pkg`."""
2+
3+
from . import pkg
4+
5+
if __name__ == "__main__":
6+
pkg(prog_name="jmp-pkg")
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
import re
2+
from typing import Optional
3+
4+
import asyncclick as click
5+
from jumpstarter_cli_common import opt_output_all
6+
from jumpstarter_cli_common.exceptions import handle_exceptions
7+
from jumpstarter_cli_common.opt import OutputMode, OutputType
8+
from jumpstarter_cli_common.table import make_table
9+
10+
from ..repository import LocalDriverRepository, V1Alpha1DriverPackageList
11+
12+
MAX_SUMMARY_LENGTH = 100
13+
14+
15+
def clean_truncate_summary(summary: Optional[str]):
16+
if summary is None:
17+
return ""
18+
# Get only the first line
19+
first_line = summary.split("\n")[0].strip()
20+
# Strip markdown formatting
21+
cleaned_summary = re.sub(r"[#*_~`\[\]\(\)\{}]", "", first_line) # Remove markdown characters
22+
# Truncate if necessary
23+
truncated_summary = cleaned_summary[:MAX_SUMMARY_LENGTH] + (
24+
"..." if len(cleaned_summary) > MAX_SUMMARY_LENGTH else ""
25+
)
26+
return truncated_summary
27+
28+
29+
def print_packages(local_drivers: V1Alpha1DriverPackageList, is_wide: bool):
30+
if is_wide:
31+
columns = ["NAME", "VERSION", "INSTALLED", "CATEGORIES", "LICENSE", "SUMMARY"]
32+
else:
33+
columns = ["NAME", "VERSION", "INSTALLED", "CATEGORIES"]
34+
driver_rows = []
35+
for package in local_drivers.items:
36+
driver_rows.append(
37+
{
38+
"INSTALLED": "*" if package.installed else "",
39+
"NAME": package.name,
40+
"VERSION": package.version,
41+
"CATEGORIES": ",".join(package.categories),
42+
"LICENSE": package.license if package.license else "Unspecified",
43+
"SUMMARY": clean_truncate_summary(package.summary),
44+
}
45+
)
46+
click.echo(make_table(columns, driver_rows))
47+
48+
49+
@click.command("list")
50+
@opt_output_all
51+
@handle_exceptions
52+
def list(output: OutputType):
53+
"""List Jumpstarter packages"""
54+
local_repo = LocalDriverRepository.from_venv()
55+
local_drivers = local_repo.list_packages()
56+
match output:
57+
case OutputMode.JSON:
58+
click.echo(local_drivers.dump_json())
59+
case OutputMode.YAML:
60+
click.echo(local_drivers.dump_yaml())
61+
case OutputMode.NAME:
62+
for package in local_drivers.items:
63+
for driver in package.drivers.items:
64+
click.echo(f"driver.jumpstarter.dev/{package.name}/{driver.name}")
65+
case _:
66+
click.echo("Fetching local packages for current Python environment")
67+
print_packages(local_drivers, is_wide=output == OutputMode.WIDE)
68+
69+
70+
# def print_drivers(driver_packages: V1Alpha1DriverPackageList, is_wide: bool):
71+
# if is_wide:
72+
# columns = ["NAME", "PACKAGE", "VERSION", "TYPE", "CATEGORIES", "LICENSE"]
73+
# else:
74+
# columns = ["NAME", "TYPE"]
75+
# driver_rows = []
76+
# for package in driver_packages.items:
77+
# for driver in package.drivers:
78+
# driver_rows.append(
79+
# {
80+
# "NAME": driver.name,
81+
# "PACKAGE": package.name,
82+
# "VERSION": package.version,
83+
# "TYPE": driver.type,
84+
# "CATEGORIES": ",".join(package.categories),
85+
# "LICENSE": package.license if package.license else "Unspecified",
86+
# }
87+
# )
88+
# click.echo(make_table(columns, driver_rows))
89+
90+
91+
# @list.command("drivers")
92+
# @opt_output_all
93+
# @handle_exceptions
94+
# async def get_drivers(output: OutputType):
95+
# """
96+
# Display all available drivers.
97+
# """
98+
# local_repo = LocalDriverRepository.from_venv()
99+
# local_drivers = local_repo.list_packages()
100+
# match output:
101+
# case OutputMode.JSON:
102+
# click.echo(local_drivers.dump_json())
103+
# case OutputMode.YAML:
104+
# click.echo(local_drivers.dump_yaml())
105+
# case OutputMode.NAME:
106+
# for package in local_drivers.items:
107+
# for driver in package.drivers:
108+
# click.echo(f"driver.jumpstarter.dev/{package.name}/{driver.name}")
109+
# case _:
110+
# print_drivers(local_drivers, is_wide=output == OutputMode.WIDE)
111+
112+
113+
# def print_driver_clients(driver_packages: V1Alpha1DriverPackageList, is_wide: bool):
114+
# if is_wide:
115+
# columns = ["NAME", "PACKAGE", "VERSION", "TYPE", "CATEGORIES", "LICENSE"]
116+
# else:
117+
# columns = ["NAME", "TYPE"]
118+
# driver_rows = []
119+
# for package in driver_packages.items:
120+
# for client in package.clients:
121+
# driver_rows.append(
122+
# {
123+
# "NAME": client.name,
124+
# "PACKAGE": package.name,
125+
# "VERSION": package.version,
126+
# "TYPE": client.type,
127+
# "CATEGORIES": ",".join(package.categories),
128+
# "LICENSE": package.license if package.license else "Unspecified",
129+
# }
130+
# )
131+
# click.echo(make_table(columns, driver_rows))
132+
133+
134+
# @list.command("driver-clients")
135+
# @opt_output_all
136+
# @handle_exceptions
137+
# async def get_driver_clients(output: OutputType):
138+
# """
139+
# Display all available driver clients.
140+
# """
141+
# local_repo = LocalDriverRepository.from_venv()
142+
# local_drivers = local_repo.list_packages()
143+
# match output:
144+
# case OutputMode.JSON:
145+
# click.echo(local_drivers.dump_json())
146+
# case OutputMode.YAML:
147+
# click.echo(local_drivers.dump_yaml())
148+
# case OutputMode.NAME:
149+
# for package in local_drivers.items:
150+
# for driver in package.drivers:
151+
# click.echo(f"driver-client.jumpstarter.dev/{package.name}/{driver.name}")
152+
# case _:
153+
# print_driver_clients(local_drivers, is_wide=output == OutputMode.WIDE)
154+
155+
156+
# @list.command("packages")
157+
# async def get_packages(output: OutputType):
158+
# """
159+
# Display all available jumpstarter driver packages.
160+
# """
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
from .base import DriverRepository
2+
from .local import LocalDriverRepository
3+
from .package import (
4+
V1Alpha1AdapterEntryPoint,
5+
V1Alpha1AdapterEntryPointList,
6+
V1Alpha1DriverClientEntryPoint,
7+
V1Alpha1DriverClientEntryPointList,
8+
V1Alpha1DriverEntryPoint,
9+
V1Alpha1DriverEntryPointList,
10+
V1Alpha1DriverPackage,
11+
V1Alpha1DriverPackageList,
12+
)
13+
14+
__all__ = [
15+
"DriverRepository",
16+
"LocalDriverRepository",
17+
"V1Alpha1AdapterEntryPoint",
18+
"V1Alpha1AdapterEntryPointList",
19+
"V1Alpha1DriverClientEntryPoint",
20+
"V1Alpha1DriverClientEntryPointList",
21+
"V1Alpha1DriverEntryPoint",
22+
"V1Alpha1DriverEntryPointList",
23+
"V1Alpha1DriverPackage",
24+
"V1Alpha1DriverPackageList",
25+
]
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
from abc import ABC, abstractmethod
2+
3+
from .package import (
4+
V1Alpha1DriverPackageList,
5+
)
6+
7+
8+
class DriverRepository(ABC):
9+
"""
10+
A repository of driver packages.
11+
"""
12+
13+
@abstractmethod
14+
def list_packages(self) -> V1Alpha1DriverPackageList:
15+
"""
16+
List all available driver packages.
17+
"""
18+
pass
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
from importlib.metadata import entry_points
2+
3+
from .base import DriverRepository
4+
from .package import (
5+
V1Alpha1AdapterEntryPoint,
6+
V1Alpha1DriverClientEntryPoint,
7+
V1Alpha1DriverEntryPoint,
8+
V1Alpha1DriverPackage,
9+
V1Alpha1DriverPackageList,
10+
)
11+
12+
13+
class LocalDriverRepository(DriverRepository):
14+
"""
15+
A local repository of driver packages from the current venv.
16+
"""
17+
18+
DRIVER_ENTRY_POINT_GROUP = "jumpstarter.drivers"
19+
DRIVER_CLIENT_ENTRY_POINT_GROUP = "jumpstarter.clients"
20+
ADAPTER_ENTRY_POINT_GROUP = "jumpstarter.adapters"
21+
22+
@staticmethod
23+
def from_venv():
24+
"""
25+
Create a `LocalDriverRepository` from the current venv.
26+
"""
27+
return LocalDriverRepository()
28+
29+
def _get_driver_packages_from_entry_points(self) -> list[V1Alpha1DriverPackage]:
30+
# Create a dict of driver packages to collect entry points
31+
driver_packages: dict[str, V1Alpha1DriverPackage] = {}
32+
33+
# Closure to process entry points for a specific entry point group
34+
def _process_entry_points(group: str):
35+
"""Process entry points for a specific group and add them to driver_packages."""
36+
for entry_point in list(entry_points(group=group)):
37+
package_id = f"{entry_point.dist.name}=={entry_point.dist.version}"
38+
# Check if the package is in the driver packages
39+
if package_id not in driver_packages:
40+
# Create a new package
41+
if entry_point.dist is not None:
42+
# Create the package from the entry point distribution metadata
43+
driver_packages[package_id] = V1Alpha1DriverPackage.from_distribution(entry_point.dist)
44+
else:
45+
# Skip this entry point if the distribution metadata is not available
46+
continue
47+
# Add the driver/client to the package
48+
match group:
49+
case LocalDriverRepository.DRIVER_ENTRY_POINT_GROUP:
50+
driver_packages[package_id].drivers.items.append(
51+
V1Alpha1DriverEntryPoint.from_entry_point(entry_point)
52+
)
53+
case LocalDriverRepository.DRIVER_CLIENT_ENTRY_POINT_GROUP:
54+
driver_packages[package_id].driver_clients.items.append(
55+
V1Alpha1DriverClientEntryPoint.from_entry_point(entry_point)
56+
)
57+
case LocalDriverRepository.ADAPTER_ENTRY_POINT_GROUP:
58+
driver_packages[package_id].adapters.items.append(
59+
V1Alpha1AdapterEntryPoint.from_entry_point(entry_point)
60+
)
61+
62+
# Process driver entry points
63+
_process_entry_points(LocalDriverRepository.DRIVER_ENTRY_POINT_GROUP)
64+
65+
# Process client entry points
66+
_process_entry_points(LocalDriverRepository.DRIVER_CLIENT_ENTRY_POINT_GROUP)
67+
68+
# Process adapter entry points
69+
_process_entry_points(LocalDriverRepository.DRIVER_CLIENT_ENTRY_POINT_GROUP)
70+
71+
# Return the assembled driver packages list
72+
return list(driver_packages.values())
73+
74+
def list_packages(self) -> V1Alpha1DriverPackageList:
75+
# Get the local drivers using the Jumpstarter drivers entry point
76+
driver_packages = self._get_driver_packages_from_entry_points()
77+
return V1Alpha1DriverPackageList(items=driver_packages)

0 commit comments

Comments
 (0)