Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
313debf
feat(url): Add `shinylive url`command and `make_shinylive_url()` func…
gadenbuie Jan 4, 2024
d570bfe
export `make_shinylive_url()`
gadenbuie Jan 4, 2024
a231982
chore: app and files could be Paths
gadenbuie Jan 4, 2024
13a4d9d
require lzstring
gadenbuie Jan 4, 2024
b6944fd
remove `__all__` from `__init__.py`
gadenbuie Jan 4, 2024
18cf274
noqa and pyright ingore
gadenbuie Jan 4, 2024
163f452
feat: shinylive url (encode,decode)
gadenbuie Jan 4, 2024
cccffbc
allow piping into `shinylive url decode`
gadenbuie Jan 5, 2024
281e47a
allow piping into `shinylive url encode` and detect app source code
gadenbuie Jan 5, 2024
637725d
negotiate aggressively with the type checker
gadenbuie Jan 5, 2024
e36fbd7
demote unused f string
gadenbuie Jan 5, 2024
7b02ac9
add comment
gadenbuie Jan 5, 2024
dd3d6f9
document --out option
gadenbuie Jan 5, 2024
294341a
require `--help` so that piping into url encode works
gadenbuie Jan 5, 2024
0dea7a4
include files, recursively
gadenbuie Jan 5, 2024
d725e57
less aggressive type check convincing
gadenbuie Jan 5, 2024
a7cc038
rename --out to --dir
gadenbuie Jan 5, 2024
331f958
automatically detect app language when app is the text content
gadenbuie Jan 5, 2024
2b9d3ce
Apply suggestions from code review
gadenbuie Jan 6, 2024
479f55c
import Literal
gadenbuie Jan 6, 2024
b5da6d8
type narrow language from encode CLI -> internal
gadenbuie Jan 6, 2024
f635f55
don't need to import os
gadenbuie Jan 6, 2024
ef8218f
add note about decode result wrt --dir
gadenbuie Jan 6, 2024
2da9c8a
write base64-decoded binary files
gadenbuie Jan 6, 2024
07c952f
detect_app_language() returns "py" or "r"
gadenbuie Jan 6, 2024
8f9e75a
make FileContentJson.type not required
gadenbuie Jan 6, 2024
e1b0b81
only add header param in app mode
gadenbuie Jan 6, 2024
b87490b
if file is str|Path, promote to list
gadenbuie Jan 6, 2024
246cdad
improve FileContentJson typing throughout
gadenbuie Jan 6, 2024
4a96487
exclude _dev folder from checks
gadenbuie Jan 6, 2024
89f4d27
fix syntax for creating FileContenJson objects
gadenbuie Jan 6, 2024
6d5c22a
require typing-extensions
gadenbuie Jan 6, 2024
373f639
separate bundle creation from URL encoding
gadenbuie Jan 6, 2024
d3a7665
add `encode_shinylive_url()` and make only encode/decode public
gadenbuie Jan 8, 2024
dd75236
simplify types and remove need for AppBundle
gadenbuie Jan 8, 2024
6674c6b
move package version into a subpackage
gadenbuie Jan 8, 2024
a9199b7
wrap decode outputs in helper functions, too
gadenbuie Jan 8, 2024
b6bf76f
rename version to _version
gadenbuie Jan 12, 2024
c80e99c
docs: describe feature in changelog
gadenbuie Jan 12, 2024
5a73146
fix one more _version import
gadenbuie Jan 12, 2024
bc0fad7
bump package version to 0.1.3.9000
gadenbuie Jan 12, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [UNRELEASED]

* Added `shinylive url encode` and `shinylive url decode` commands to encode local apps into a shinylive.io URL or decode a shinylive.io URL into local files. These commands are accompanied by `encode_shinylive_url()` and `decode_shinylive_url()` functions for programmatic use. (#20)

## [0.1.3] - 2024-12-19

* Fixed `shinylive assets install-from-local`.
Expand Down
2 changes: 1 addition & 1 deletion pyrightconfig.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"ignore": ["build", "dist", "typings", "sandbox"],
"ignore": ["build", "dist", "typings", "sandbox", "_dev"],
"typeCheckingMode": "strict",
"reportImportCycles": "none",
"reportUnusedFunction": "none",
Expand Down
6 changes: 4 additions & 2 deletions setup.cfg
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[metadata]
name = shinylive
version = attr: shinylive.__version__
version = attr: shinylive._version.SHINYLIVE_PACKAGE_VERSION
author = Winston Chang
author_email = [email protected]
url = https://github.com/posit-dev/py-shinylive
Expand Down Expand Up @@ -36,6 +36,8 @@ install_requires =
shiny
click>=8.1.7
appdirs>=1.4.4
lzstring>=1.0.4
typing-extensions>=4.0.1
tests_require =
pytest>=3
zip_safe = False
Expand Down Expand Up @@ -69,7 +71,7 @@ console_scripts =
# F405: Name may be undefined, or defined from star imports
# W503: Line break occurred before a binary operator
ignore = E302, E501, F403, F405, W503
exclude = docs, .venv
exclude = docs, .venv, _dev

[isort]
profile=black
7 changes: 5 additions & 2 deletions shinylive/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
"""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 ._version import SHINYLIVE_PACKAGE_VERSION

__version__ = _version.SHINYLIVE_PACKAGE_VERSION
__version__ = SHINYLIVE_PACKAGE_VERSION

__all__ = ("decode_shinylive_url", "encode_shinylive_url")
204 changes: 187 additions & 17 deletions shinylive/_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,22 @@
import collections
import sys
from pathlib import Path
from typing import MutableMapping, Optional
from typing import Literal, MutableMapping, Optional

import click

from . import _assets, _deps, _export, _version
from . import _assets, _deps, _export
from ._url import (
create_shinylive_bundle_file,
create_shinylive_bundle_text,
create_shinylive_chunk_contents,
create_shinylive_url,
decode_shinylive_url,
detect_app_language,
write_files_from_shinylive_io,
)
from ._utils import print_as_json
from ._version import SHINYLIVE_ASSETS_VERSION, SHINYLIVE_PACKAGE_VERSION


# Make sure commands are listed in the order they are added in the code.
Expand All @@ -29,8 +39,8 @@ def list_commands(self, ctx: click.Context) -> list[str]:

version_txt = f"""
\b
shinylive Python package version: {_version.SHINYLIVE_PACKAGE_VERSION}
shinylive web assets version: {_assets.SHINYLIVE_ASSETS_VERSION}
shinylive Python package version: {SHINYLIVE_PACKAGE_VERSION}
shinylive web assets version: {SHINYLIVE_ASSETS_VERSION}
"""

# CLI structure:
Expand Down Expand Up @@ -59,6 +69,7 @@ def list_commands(self, ctx: click.Context) -> list[str]:
# * language-resources
# * app-resources
# * Options: --json-file / stdin (required)
# * url


# #############################################################################
Expand All @@ -74,7 +85,7 @@ def list_commands(self, ctx: click.Context) -> list[str]:
)
# > Add a --version option which immediately prints the version number and exits the
# > program.
@click.version_option(_version.SHINYLIVE_PACKAGE_VERSION, message="%(version)s")
@click.version_option(SHINYLIVE_PACKAGE_VERSION, message="%(version)s")
def main() -> None:
...

Expand Down Expand Up @@ -185,7 +196,7 @@ def assets_info(
@click.option(
"--version",
type=str,
default=_version.SHINYLIVE_ASSETS_VERSION,
default=SHINYLIVE_ASSETS_VERSION,
help="Shinylive version to download.",
show_default=True,
)
Expand All @@ -207,11 +218,11 @@ def download(
url: Optional[str],
) -> None:
if version is None: # pyright: ignore[reportUnnecessaryComparison]
version = _version.SHINYLIVE_ASSETS_VERSION
version = SHINYLIVE_ASSETS_VERSION
_assets.download_shinylive(destdir=upgrade_dir(dir), version=version, url=url)


cleanup_help = f"Remove all versions of local assets except the currently-used version, {_assets.SHINYLIVE_ASSETS_VERSION}."
cleanup_help = f"Remove all versions of local assets except the currently-used version, {SHINYLIVE_ASSETS_VERSION}."


@assets.command(
Expand All @@ -234,7 +245,7 @@ def cleanup(
short_help="Remove a specific version of local copies of assets.",
help=f"""Remove a specific version (`VERSION`) of local copies of assets."

For example, `VERSION` might be `{ _version.SHINYLIVE_ASSETS_VERSION }`.
For example, `VERSION` might be `{ SHINYLIVE_ASSETS_VERSION }`.
""",
no_args_is_help=True,
)
Expand Down Expand Up @@ -270,7 +281,7 @@ def remove(
@click.option(
"--version",
type=str,
default=_version.SHINYLIVE_ASSETS_VERSION,
default=SHINYLIVE_ASSETS_VERSION,
help="Version of the shinylive assets being copied.",
show_default=True,
)
Expand All @@ -292,7 +303,7 @@ def install_from_local(
) -> None:
dir = upgrade_dir(dir)
if version is None: # pyright: ignore[reportUnnecessaryComparison]
version = _version.SHINYLIVE_ASSETS_VERSION
version = SHINYLIVE_ASSETS_VERSION
print(f"Copying shinylive-{version} from {build} to {dir}")
_assets.copy_shinylive_local(source_dir=build, destdir=dir, version=version)

Expand All @@ -313,7 +324,7 @@ def install_from_local(
@click.option(
"--version",
type=str,
default=_version.SHINYLIVE_ASSETS_VERSION,
default=SHINYLIVE_ASSETS_VERSION,
help="Version of shinylive assets being linked.",
show_default=True,
)
Expand All @@ -337,7 +348,7 @@ def link_from_local(
raise click.UsageError("Must specify BUILD")
dir = upgrade_dir(dir)
if version is None: # pyright: ignore[reportUnnecessaryComparison]
version = _version.SHINYLIVE_ASSETS_VERSION
version = SHINYLIVE_ASSETS_VERSION
print(f"Creating symlink for shinylive-{version} from {build} to {dir}")
_assets.link_shinylive_local(source_dir=build, destdir=dir, version=version)

Expand All @@ -346,7 +357,7 @@ def link_from_local(
help="Print the version of the Shinylive assets.",
)
def version() -> None:
print(_assets.SHINYLIVE_ASSETS_VERSION)
print(SHINYLIVE_ASSETS_VERSION)


# #############################################################################
Expand Down Expand Up @@ -385,8 +396,8 @@ def extension() -> None:
def extension_info() -> None:
print_as_json(
{
"version": _version.SHINYLIVE_PACKAGE_VERSION,
"assets_version": _version.SHINYLIVE_ASSETS_VERSION,
"version": SHINYLIVE_PACKAGE_VERSION,
"assets_version": SHINYLIVE_ASSETS_VERSION,
"scripts": {
"codeblock-to-json": _assets.codeblock_to_json_file(),
},
Expand Down Expand Up @@ -459,7 +470,7 @@ def app_resources(
def defunct_help(cmd: str) -> str:
return f"""The shinylive CLI command `{cmd}` is defunct.

You are using a newer version of the Python shinylive package ({ _version.SHINYLIVE_PACKAGE_VERSION }) with an older
You are using a newer version of the Python shinylive package ({ SHINYLIVE_PACKAGE_VERSION }) with an older
version of the Quarto shinylive extension, and these versions are not compatible.

Please update your Quarto shinylive extension by running this command in the top level
Expand All @@ -469,6 +480,165 @@ def defunct_help(cmd: str) -> str:
"""


# #############################################################################
# ## shinylive.io url
# #############################################################################


@main.group(
short_help="Create or decode a shinylive.io URL.",
help="Create or decode a shinylive.io URL.",
no_args_is_help=True,
cls=OrderedGroup,
)
def url() -> None:
pass


@url.command(
short_help="Create a shinylive.io URL from local files.",
help="""
Create a shinylive.io URL for a Shiny app from local files.

APP is the path to the primary Shiny app file.

FILES are additional supporting files or directories for the app.

On macOS, you can copy the URL to the clipboard with:

shinylive url encode app.py | pbcopy
""",
)
@click.option(
"-m",
"--mode",
type=click.Choice(["editor", "app"]),
required=True,
default="editor",
help="The shinylive mode: include the editor or show only the app.",
)
@click.option(
"-l",
"--language",
type=click.Choice(["python", "py", "R", "r"]),
required=False,
default=None,
help="The primary language used to run the app, by default inferred from the app file.",
)
@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."
)
@click.argument("app", type=str, nargs=1, required=True, default="-")
@click.argument("files", type=str, nargs=-1, required=False)
def encode(
app: str,
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:
if app == "-":
app_in = sys.stdin.read()
else:
app_in = app

if language is not None:
if language in ["py", "python"]:
lang = "py"
elif language in ["r", "R"]:
lang = "r"
else:
raise click.UsageError(
f"Invalid language '{language}', must be one of 'py', 'python', 'r', 'R'."
)
else:
lang = detect_app_language(app_in)

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)
if not view:
return

url = create_shinylive_url(
bundle,
lang,
mode=mode,
header=not no_header,
)

if not json:
print(url)

if view:
import webbrowser

webbrowser.open(url)


@url.command(
short_help="Decode a shinylive.io URL.",
help="""
Decode a shinylive.io URL.

URL is the shinylive editor or app URL. If not specified, the URL will be read from
stdin, allowing you to read the URL from a file or the clipboard.

When `--dir` is provided, the decoded files will be written to the specified directory.
Otherwise, the contents of the shinylive app will be printed to stdout.

On macOS, you can read the URL from the clipboard with:

pbpaste | shinylive url decode
""",
)
@click.option(
"--dir",
type=str,
default=None,
help="Output directory into which the app's files will be written. The directory is created if it does not exist. ",
)
@click.option(
"--json",
is_flag=True,
default=False,
help="Prints the decoded shinylive bundle as JSON to stdout, ignoring --dir.",
)
@click.argument("url", type=str, nargs=1, default="-")
def decode(url: str, dir: Optional[str] = None, json: bool = False) -> None:
if url == "-":
url_in = sys.stdin.read()
else:
url_in = url
bundle = decode_shinylive_url(str(url_in))

if json:
print_as_json(bundle)
return

if dir is not None:
write_files_from_shinylive_io(bundle, dir)
else:
print(create_shinylive_chunk_contents(bundle))


# #############################################################################
# ## Deprecated commands
# #############################################################################


def defunct_error_txt(cmd: str) -> str:
return f"Error: { defunct_help(cmd) }"

Expand Down
Loading