Skip to content

Commit 17ace51

Browse files
committed
add commands to list and fetch jumpstart examples
1 parent acdbf28 commit 17ace51

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

@@ -497,3 +498,23 @@ def _order_content_results(
497498
result = sorted(result, key=lambda c: c["created_time"], reverse=True)
498499

499500
return list(result)
501+
502+
503+
def list_examples(connect_server: RSConnectServer):
504+
with RSConnectClient(connect_server) as client:
505+
connect_version = client.server_settings()["version"]
506+
has_public_examples = compare_semvers(connect_version, "2024.05.0")
507+
result = client.examples_list() if has_public_examples in [0, 1] else client.examples_list_legacy()
508+
return result
509+
510+
511+
def download_example(connect_server: RSConnectServer, example_name: str):
512+
with RSConnectClient(connect_server) as client:
513+
connect_version = client.server_settings()["version"]
514+
has_public_examples = compare_semvers(connect_version, "2024.05.0")
515+
result = (
516+
client.examples_download(example_name)
517+
if has_public_examples in [0, 1]
518+
else client.examples_download_legacy(example_name)
519+
)
520+
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,
@@ -384,6 +385,28 @@ def content_build(self, content_guid: str, bundle_id: Optional[str] = None) -> B
384385
response = self._server.handle_bad_response(response)
385386
return response
386387

388+
def examples_list(self) -> list[Examples]:
389+
response = cast(Union[List[Examples], HTTPResponse], self.get("v1/examples"))
390+
response = self._server.handle_bad_response(response)
391+
return response
392+
393+
# todo: delete me in October of 2025
394+
def examples_list_legacy(self) -> list[Examples]:
395+
response = cast(Union[List[Examples], HTTPResponse], self.get("v1/experimental/examples"))
396+
response = self._server.handle_bad_response(response)
397+
return response
398+
399+
def examples_download(self, example_name: str) -> HTTPResponse:
400+
response = cast(HTTPResponse, self.get("v1/examples/%s/zip" % example_name, decode_response=False))
401+
response = self._server.handle_bad_response(response, is_httpresponse=True)
402+
return response
403+
404+
# todo: delete me in October of 2025
405+
def examples_download_legacy(self, example_name: str) -> HTTPResponse:
406+
response = cast(HTTPResponse, self.get("v1/experimental/examples/%s/zip" % example_name, decode_response=False))
407+
response = self._server.handle_bad_response(response, is_httpresponse=True)
408+
return response
409+
387410
def system_caches_runtime_list(self) -> list[ListEntryOutputDTO]:
388411
response = cast(Union[List[ListEntryOutputDTO], HTTPResponse], self.get("v1/system/caches/runtime"))
389412
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,
@@ -2872,6 +2874,88 @@ def system_caches_delete(
28722874
ce.delete_runtime_cache(language, version, image_name, dry_run)
28732875

28742876

2877+
@cli.group(no_args_is_help=True, help="Fetch Posit Connect jumpstart examples.")
2878+
def examples():
2879+
pass
2880+
2881+
2882+
@examples.command(
2883+
name="list",
2884+
short_help="List jumpstart examples on a Posit Connect server.",
2885+
)
2886+
@server_args
2887+
@click.pass_context
2888+
def examples_list(
2889+
ctx: click.Context,
2890+
name: str,
2891+
server: Optional[str],
2892+
api_key: Optional[str],
2893+
insecure: bool,
2894+
cacert: Optional[str],
2895+
verbose: int,
2896+
):
2897+
set_verbosity(verbose)
2898+
output_params(ctx, locals().items())
2899+
with cli_feedback("", stderr=True):
2900+
ce = RSConnectExecutor(ctx, name, server, api_key, insecure, cacert, logger=None).validate_server()
2901+
if not isinstance(ce.remote_server, RSConnectServer):
2902+
raise RSConnectException("rsconnect examples list` requires a Posit Connect server.")
2903+
examples = list_examples(ce.remote_server)
2904+
result = [{"name": ex["name"], "description": ex["description"]} for ex in examples]
2905+
json.dump(result, sys.stdout, indent=2)
2906+
2907+
2908+
@examples.command(
2909+
name="download",
2910+
short_help="Download a jumpstart example from a Posit Connect server.",
2911+
)
2912+
@server_args
2913+
@click.option(
2914+
"--example",
2915+
required=True,
2916+
help="The name of the example to download.",
2917+
)
2918+
@click.option(
2919+
"--output",
2920+
"-o",
2921+
type=click.Path(),
2922+
required=True,
2923+
help="Defines the output location for the download.",
2924+
)
2925+
@click.option(
2926+
"--overwrite",
2927+
is_flag=True,
2928+
help="Overwrite the output file if it already exists.",
2929+
)
2930+
@click.pass_context
2931+
def examples_download(
2932+
ctx: click.Context,
2933+
name: Optional[str],
2934+
server: Optional[str],
2935+
api_key: Optional[str],
2936+
insecure: bool,
2937+
cacert: Optional[str],
2938+
example: str,
2939+
output: str,
2940+
overwrite: bool,
2941+
verbose: int,
2942+
):
2943+
set_verbosity(verbose)
2944+
output_params(ctx, locals().items())
2945+
with cli_feedback("", stderr=True):
2946+
ce = RSConnectExecutor(ctx, name, server, api_key, insecure, cacert, logger=None).validate_server()
2947+
if not isinstance(ce.remote_server, RSConnectServer):
2948+
raise RSConnectException("`rsconnect examples download` requires a Posit Connect server.")
2949+
if exists(output) and not overwrite:
2950+
raise RSConnectException("The output file already exists: %s" % output)
2951+
2952+
result = download_example(ce.remote_server, example)
2953+
if not isinstance(result.response_body, bytes):
2954+
raise RSConnectException("The response body must be bytes (not string or None).")
2955+
with open(output, "wb") as f:
2956+
f.write(result.response_body)
2957+
2958+
28752959
if __name__ == "__main__":
28762960
cli()
28772961
click.echo()

rsconnect/models.py

+10
Original file line numberDiff line numberDiff line change
@@ -624,3 +624,13 @@ class UserRecord(TypedDict):
624624
locked: bool
625625
guid: str
626626
preferences: dict[str, object]
627+
628+
629+
class Examples(TypedDict):
630+
name: str
631+
type: str
632+
title: str
633+
description: str
634+
files: list[str]
635+
requirements: list[str]
636+
links: list[dict[str, str]]

0 commit comments

Comments
 (0)