From 373f639ed81336f2a1393b9726d2a2a1dc48a0b9 Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Sat, 6 Jan 2024 15:16:31 -0500 Subject: [PATCH] separate bundle creation from URL encoding --- shinylive/__init__.py | 11 ++++- shinylive/_main.py | 30 +++++++++--- shinylive/_url.py | 106 +++++++++++++++++++++++++++++------------- 3 files changed, 107 insertions(+), 40 deletions(-) diff --git a/shinylive/__init__.py b/shinylive/__init__.py index d75b595..75952d8 100644 --- a/shinylive/__init__.py +++ b/shinylive/__init__.py @@ -1,11 +1,18 @@ """A package for packaging Shiny applications that run on Python in the browser.""" from . import _version -from ._url import decode_shinylive_url, encode_shinylive_url +from ._url import ( + create_shinylive_bundle_file, + create_shinylive_bundle_text, + create_shinylive_url, + decode_shinylive_url, +) __version__ = _version.SHINYLIVE_PACKAGE_VERSION __all__ = ( "decode_shinylive_url", - "encode_shinylive_url", + "create_shinylive_url", + "create_shinylive_bundle_text", + "create_shinylive_bundle_file", ) diff --git a/shinylive/_main.py b/shinylive/_main.py index f25cb89..b457699 100644 --- a/shinylive/_main.py +++ b/shinylive/_main.py @@ -8,7 +8,12 @@ import click from . import _assets, _deps, _export, _version -from ._url import decode_shinylive_url, encode_shinylive_url +from ._url import ( + create_shinylive_bundle_file, + create_shinylive_bundle_text, + create_shinylive_url, + decode_shinylive_url, +) from ._utils import print_as_json @@ -519,6 +524,9 @@ def url() -> None: @click.option( "-v", "--view", is_flag=True, default=False, help="Open the link in a browser." ) +@click.option( + "--json", is_flag=True, default=False, help="Print the bundle as JSON to stdout." +) @click.option( "--no-header", is_flag=True, default=False, help="Hide the Shinylive header." ) @@ -529,6 +537,7 @@ def encode( files: Optional[tuple[str, ...]] = None, mode: Literal["editor", "app"] = "editor", language: Optional[str] = None, + json: bool = False, no_header: bool = False, view: bool = False, ) -> None: @@ -549,15 +558,24 @@ def encode( else: lang = None - url = encode_shinylive_url( - app=app_in, - files=files, + if "\n" in app_in: + bundle = create_shinylive_bundle_text(app_in, files, lang) + else: + bundle = create_shinylive_bundle_file(app_in, files, lang) + + if json: + print_as_json(bundle["files"]) + if not view: + return + + url = create_shinylive_url( + bundle, mode=mode, - language=lang, header=not no_header, ) - print(url) + if not json: + print(url) if view: import webbrowser diff --git a/shinylive/_url.py b/shinylive/_url.py index 9ee34bf..eaebb16 100644 --- a/shinylive/_url.py +++ b/shinylive/_url.py @@ -23,13 +23,55 @@ class FileContentJson(TypedDict): type: NotRequired[Literal["text", "binary"]] -def encode_shinylive_url( - app: str | Path, - files: Optional[str | Path | Sequence[str | Path]] = None, +class AppBundle(TypedDict): + language: Literal["py", "r"] + files: list[FileContentJson] + + +def create_shinylive_url( + bundle: AppBundle, mode: Literal["editor", "app"] = "editor", - language: Optional[Literal["py", "r"]] = None, header: bool = True, ) -> str: + """ """ + + file_lz = lzstring_file_bundle(bundle["files"]) + + base = "https://shinylive.io" + h = "h=0&" if not header and mode == "app" else "" + + return f"{base}/{bundle['language']}/{mode}/#{h}code={file_lz}" + + +def create_shinylive_bundle_text( + app: str, + files: Optional[str | Path | Sequence[str | Path]] = None, + language: Optional[Literal["py", "r"]] = None, + root_dir: str | Path = ".", +) -> AppBundle: + if language is None: + language = detect_app_language(app) + elif language not in ["py", "r"]: + raise ValueError( + f"Language '{language}' is not supported. Please specify one of 'py' or 'r'." + ) + + app_fc: FileContentJson = { + "name": f"app.{'py' if language == 'py' else 'R'}", + "content": app, + } + + return { + "language": language, + "files": add_supporting_files_to_bundle(app_fc, files, root_dir), + } + + +def create_shinylive_bundle_file( + app: str | Path, + files: Optional[str | Path | Sequence[str | Path]] = None, + language: Optional[Literal["py", "r"]] = None, +) -> AppBundle: """ Generate a URL for a [ShinyLive application](https://shinylive.io). @@ -57,20 +99,34 @@ def encode_shinylive_url( if language is None: language = detect_app_language(app) + elif language not in ["py", "r"]: + raise ValueError( + f"Language '{language}' is not supported. Please specify one of 'py' or 'r'." + ) - # if app has a newline, then it's app content, not a path - if isinstance(app, str) and "\n" in app: - app_path = "" - root_dir = Path(".") - app_fc: FileContentJson = { - "name": f"app.{'py' if language == 'py' else 'R'}", - "content": app, - } - file_bundle = [app_fc] - else: - app_path = Path(app) - root_dir = app_path.parent - file_bundle = [read_file(app, root_dir)] + app_path = Path(app) + root_dir = app_path.parent + app_fc = read_file(app, root_dir) + + # if the app is not named either `ui.R` or `server.R`, then make it app.py or app.R + if app_fc["name"] not in ["ui.R", "server.R"]: + app_fc["name"] = f"app.{'py' if language == 'py' else 'R'}" + + return { + "language": language, + "files": add_supporting_files_to_bundle(app_fc, files, root_dir, app_path), + } + + +def add_supporting_files_to_bundle( + app: FileContentJson, + files: Optional[str | Path | Sequence[str | Path]] = None, + root_dir: str | Path = ".", + app_path: str | Path = "", +) -> list[FileContentJson]: + app_path = Path(app_path) + + file_bundle = [app] if isinstance(files, (str, Path)): files = [files] @@ -88,21 +144,7 @@ def encode_shinylive_url( read_file(file, root_dir) for file in file_list if Path(file) != app_path ] - if language not in ["py", "r"]: - raise ValueError( - f"Language '{language}' is not supported. Please specify one of 'py' or 'r'." - ) - - # if first file is not named either `ui.R` or `server.R`, then make it app.{language} - if file_bundle[0]["name"] not in ["ui.R", "server.R"]: - file_bundle[0]["name"] = f"app.{'py' if language == 'py' else 'R'}" - - file_lz = lzstring_file_bundle(file_bundle) - - base = "https://shinylive.io" - h = "h=0&" if not header and mode == "app" else "" - - return f"{base}/{language}/{mode}/#{h}code={file_lz}" + return file_bundle def detect_app_language(app: str | Path) -> Literal["py", "r"]: