Skip to content

Commit 32af3cf

Browse files
committed
add commands to list and fetch jumpstart examples
1 parent 8601629 commit 32af3cf

File tree

4 files changed

+138
-0
lines changed

4 files changed

+138
-0
lines changed

rsconnect/actions_content.py

Lines changed: 21 additions & 0 deletions
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

@@ -501,3 +502,23 @@ def _order_content_results(
501502
result = sorted(result, key=lambda c: c["created_time"], reverse=True)
502503

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

rsconnect/api.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@
6969
ContentItemV1,
7070
DeleteInputDTO,
7171
DeleteOutputDTO,
72+
Examples,
7273
ListEntryOutputDTO,
7374
PyInfo,
7475
ServerSettings,
@@ -474,6 +475,28 @@ def content_build(self, content_guid: str, bundle_id: Optional[str] = None) -> B
474475
response = self._server.handle_bad_response(response)
475476
return response
476477

478+
def examples_list(self) -> list[Examples]:
479+
response = cast(Union[List[Examples], HTTPResponse], self.get("v1/examples"))
480+
response = self._server.handle_bad_response(response)
481+
return response
482+
483+
# todo: delete me in October of 2025
484+
def examples_list_legacy(self) -> list[Examples]:
485+
response = cast(Union[List[Examples], HTTPResponse], self.get("v1/experimental/examples"))
486+
response = self._server.handle_bad_response(response)
487+
return response
488+
489+
def examples_download(self, example_name: str) -> HTTPResponse:
490+
response = cast(HTTPResponse, self.get("v1/examples/%s/zip" % example_name, decode_response=False))
491+
response = self._server.handle_bad_response(response, is_httpresponse=True)
492+
return response
493+
494+
# todo: delete me in October of 2025
495+
def examples_download_legacy(self, example_name: str) -> HTTPResponse:
496+
response = cast(HTTPResponse, self.get("v1/experimental/examples/%s/zip" % example_name, decode_response=False))
497+
response = self._server.handle_bad_response(response, is_httpresponse=True)
498+
return response
499+
477500
def system_caches_runtime_list(self) -> list[ListEntryOutputDTO]:
478501
response = cast(Union[List[ListEntryOutputDTO], HTTPResponse], self.get("v1/system/caches/runtime"))
479502
response = self._server.handle_bad_response(response)

rsconnect/main.py

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,10 @@
4545
build_remove_content,
4646
build_start,
4747
download_bundle,
48+
download_example,
4849
emit_build_log,
4950
get_content,
51+
list_examples,
5052
search_content,
5153
)
5254
from .api import (
@@ -3009,6 +3011,88 @@ def system_caches_delete(
30093011
ce.delete_runtime_cache(language, version, image_name, dry_run)
30103012

30113013

3014+
@cli.group(no_args_is_help=True, help="Fetch Posit Connect jumpstart examples.")
3015+
def examples():
3016+
pass
3017+
3018+
3019+
@examples.command(
3020+
name="list",
3021+
short_help="List jumpstart examples on a Posit Connect server.",
3022+
)
3023+
@server_args
3024+
@click.pass_context
3025+
def examples_list(
3026+
ctx: click.Context,
3027+
name: str,
3028+
server: Optional[str],
3029+
api_key: Optional[str],
3030+
insecure: bool,
3031+
cacert: Optional[str],
3032+
verbose: int,
3033+
):
3034+
set_verbosity(verbose)
3035+
output_params(ctx, locals().items())
3036+
with cli_feedback("", stderr=True):
3037+
ce = RSConnectExecutor(ctx, name, server, api_key, insecure, cacert, logger=None).validate_server()
3038+
if not isinstance(ce.remote_server, RSConnectServer):
3039+
raise RSConnectException("rsconnect examples list` requires a Posit Connect server.")
3040+
examples = list_examples(ce.remote_server)
3041+
result = [{"name": ex["name"], "description": ex["description"]} for ex in examples]
3042+
json.dump(result, sys.stdout, indent=2)
3043+
3044+
3045+
@examples.command(
3046+
name="download",
3047+
short_help="Download a jumpstart example from a Posit Connect server.",
3048+
)
3049+
@server_args
3050+
@click.option(
3051+
"--example",
3052+
required=True,
3053+
help="The name of the example to download.",
3054+
)
3055+
@click.option(
3056+
"--output",
3057+
"-o",
3058+
type=click.Path(),
3059+
required=True,
3060+
help="Defines the output location for the download.",
3061+
)
3062+
@click.option(
3063+
"--overwrite",
3064+
is_flag=True,
3065+
help="Overwrite the output file if it already exists.",
3066+
)
3067+
@click.pass_context
3068+
def examples_download(
3069+
ctx: click.Context,
3070+
name: Optional[str],
3071+
server: Optional[str],
3072+
api_key: Optional[str],
3073+
insecure: bool,
3074+
cacert: Optional[str],
3075+
example: str,
3076+
output: str,
3077+
overwrite: bool,
3078+
verbose: int,
3079+
):
3080+
set_verbosity(verbose)
3081+
output_params(ctx, locals().items())
3082+
with cli_feedback("", stderr=True):
3083+
ce = RSConnectExecutor(ctx, name, server, api_key, insecure, cacert, logger=None).validate_server()
3084+
if not isinstance(ce.remote_server, RSConnectServer):
3085+
raise RSConnectException("`rsconnect examples download` requires a Posit Connect server.")
3086+
if exists(output) and not overwrite:
3087+
raise RSConnectException("The output file already exists: %s" % output)
3088+
3089+
result = download_example(ce.remote_server, example)
3090+
if not isinstance(result.response_body, bytes):
3091+
raise RSConnectException("The response body must be bytes (not string or None).")
3092+
with open(output, "wb") as f:
3093+
f.write(result.response_body)
3094+
3095+
30123096
if __name__ == "__main__":
30133097
cli()
30143098
click.echo()

rsconnect/models.py

Lines changed: 10 additions & 0 deletions
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)