Skip to content

Commit 0381c8b

Browse files
committed
add commands to list and fetch jumpstart examples
1 parent 4cddae9 commit 0381c8b

File tree

4 files changed

+139
-1
lines changed

4 files changed

+139
-1
lines changed

rsconnect/actions_content.py

+21
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
ContentItemV1,
2424
VersionSearchFilter,
2525
)
26+
from .utils_package import compare_semvers
2627

2728
_content_build_store: ContentBuildStore | None = None
2829

@@ -503,3 +504,23 @@ def _order_content_results(
503504
result = sorted(result, key=lambda c: c["created_time"], reverse=True)
504505

505506
return list(result)
507+
508+
509+
def list_examples(connect_server: RSConnectServer):
510+
with RSConnectClient(connect_server) as client:
511+
connect_version = client.server_settings()["version"]
512+
has_public_examples = compare_semvers(connect_version, "2024.05.0")
513+
result = client.examples_list() if has_public_examples in [0, 1] else client.examples_list_legacy()
514+
return result
515+
516+
517+
def download_example(connect_server: RSConnectServer, example_name: str):
518+
with RSConnectClient(connect_server) as client:
519+
connect_version = client.server_settings()["version"]
520+
has_public_examples = compare_semvers(connect_version, "2024.05.0")
521+
result = (
522+
client.examples_download(example_name)
523+
if has_public_examples in [0, 1]
524+
else client.examples_download_legacy(example_name)
525+
)
526+
return result

rsconnect/api.py

+23
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@
6868
ContentItemV1,
6969
DeleteInputDTO,
7070
DeleteOutputDTO,
71+
Examples,
7172
ListEntryOutputDTO,
7273
PyInfo,
7374
ServerSettings,
@@ -389,6 +390,28 @@ def content_build(self, content_guid: str, bundle_id: Optional[str] = None) -> B
389390
response = self._server.handle_bad_response(response)
390391
return response
391392

393+
def examples_list(self) -> list[Examples]:
394+
response = cast(Union[List[Examples], HTTPResponse], self.get("v1/examples"))
395+
response = self._server.handle_bad_response(response)
396+
return response
397+
398+
# todo: delete me in October of 2025
399+
def examples_list_legacy(self) -> list[Examples]:
400+
response = cast(Union[List[Examples], HTTPResponse], self.get("v1/experimental/examples"))
401+
response = self._server.handle_bad_response(response)
402+
return response
403+
404+
def examples_download(self, example_name: str) -> HTTPResponse:
405+
response = cast(HTTPResponse, self.get("v1/examples/%s/zip" % example_name, decode_response=False))
406+
response = self._server.handle_bad_response(response, is_httpresponse=True)
407+
return response
408+
409+
# todo: delete me in October of 2025
410+
def examples_download_legacy(self, example_name: str) -> HTTPResponse:
411+
response = cast(HTTPResponse, self.get("v1/experimental/examples/%s/zip" % example_name, decode_response=False))
412+
response = self._server.handle_bad_response(response, is_httpresponse=True)
413+
return response
414+
392415
def system_caches_runtime_list(self) -> list[ListEntryOutputDTO]:
393416
response = cast(Union[List[ListEntryOutputDTO], HTTPResponse], self.get("v1/system/caches/runtime"))
394417
response = self._server.handle_bad_response(response)

rsconnect/main.py

+85-1
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,10 @@
4444
build_remove_content,
4545
build_start,
4646
download_bundle,
47+
download_example,
4748
emit_build_log,
4849
get_content,
50+
list_examples,
4951
search_content,
5052
)
5153
from .api import RSConnectClient, RSConnectExecutor, RSConnectServer
@@ -60,8 +62,8 @@
6062
make_manifest_bundle,
6163
make_notebook_html_bundle,
6264
make_notebook_source_bundle,
63-
make_voila_bundle,
6465
make_tensorflow_bundle,
66+
make_voila_bundle,
6567
read_manifest_app_mode,
6668
validate_entry_point,
6769
validate_extra_files,
@@ -2815,6 +2817,88 @@ def system_caches_delete(
28152817
ce.delete_runtime_cache(language, version, image_name, dry_run)
28162818

28172819

2820+
@cli.group(no_args_is_help=True, help="Fetch Posit Connect jumpstart examples.")
2821+
def examples():
2822+
pass
2823+
2824+
2825+
@examples.command(
2826+
name="list",
2827+
short_help="List jumpstart examples on a Posit Connect server.",
2828+
)
2829+
@server_args
2830+
@click.pass_context
2831+
def examples_list(
2832+
ctx: click.Context,
2833+
name: str,
2834+
server: Optional[str],
2835+
api_key: Optional[str],
2836+
insecure: bool,
2837+
cacert: Optional[str],
2838+
verbose: int,
2839+
):
2840+
set_verbosity(verbose)
2841+
output_params(ctx, locals().items())
2842+
with cli_feedback("", stderr=True):
2843+
ce = RSConnectExecutor(ctx, name, server, api_key, insecure, cacert, logger=None).validate_server()
2844+
if not isinstance(ce.remote_server, RSConnectServer):
2845+
raise RSConnectException("rsconnect examples list` requires a Posit Connect server.")
2846+
examples = list_examples(ce.remote_server)
2847+
result = [{"name": ex["name"], "description": ex["description"]} for ex in examples]
2848+
json.dump(result, sys.stdout, indent=2)
2849+
2850+
2851+
@examples.command(
2852+
name="download",
2853+
short_help="Download a jumpstart example from a Posit Connect server.",
2854+
)
2855+
@server_args
2856+
@click.option(
2857+
"--example",
2858+
required=True,
2859+
help="The name of the example to download.",
2860+
)
2861+
@click.option(
2862+
"--output",
2863+
"-o",
2864+
type=click.Path(),
2865+
required=True,
2866+
help="Defines the output location for the download.",
2867+
)
2868+
@click.option(
2869+
"--overwrite",
2870+
is_flag=True,
2871+
help="Overwrite the output file if it already exists.",
2872+
)
2873+
@click.pass_context
2874+
def examples_download(
2875+
ctx: click.Context,
2876+
name: Optional[str],
2877+
server: Optional[str],
2878+
api_key: Optional[str],
2879+
insecure: bool,
2880+
cacert: Optional[str],
2881+
example: str,
2882+
output: str,
2883+
overwrite: bool,
2884+
verbose: int,
2885+
):
2886+
set_verbosity(verbose)
2887+
output_params(ctx, locals().items())
2888+
with cli_feedback("", stderr=True):
2889+
ce = RSConnectExecutor(ctx, name, server, api_key, insecure, cacert, logger=None).validate_server()
2890+
if not isinstance(ce.remote_server, RSConnectServer):
2891+
raise RSConnectException("`rsconnect examples download` requires a Posit Connect server.")
2892+
if exists(output) and not overwrite:
2893+
raise RSConnectException("The output file already exists: %s" % output)
2894+
2895+
result = download_example(ce.remote_server, example)
2896+
if not isinstance(result.response_body, bytes):
2897+
raise RSConnectException("The response body must be bytes (not string or None).")
2898+
with open(output, "wb") as f:
2899+
f.write(result.response_body)
2900+
2901+
28182902
if __name__ == "__main__":
28192903
cli()
28202904
click.echo()

rsconnect/models.py

+10
Original file line numberDiff line numberDiff line change
@@ -617,3 +617,13 @@ class UserRecord(TypedDict):
617617
locked: bool
618618
guid: str
619619
preferences: dict[str, object]
620+
621+
622+
class Examples(TypedDict):
623+
name: str
624+
type: str
625+
title: str
626+
description: str
627+
files: list[str]
628+
requirements: list[str]
629+
links: list[dict[str, str]]

0 commit comments

Comments
 (0)