Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,15 @@ All notable changes to this project will be documented in this file.
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

- You can now deploy Quarto documents in addition to Quarto projects. This
requires RStudio Connect release 2021.08.0 or later. Use `rsconnect deploy
quarto` to deploy, or `rsconnect write-manifest quarto` to create a manifest
file.

## [1.8.1] - 2022-05-31

### Changed
Expand Down
44 changes: 27 additions & 17 deletions rsconnect/actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -505,7 +505,11 @@ def quarto_inspect(
"""
Runs 'quarto inspect' against the target and returns its output as a
parsed JSON object.

The JSON result has different structure depending on whether or not the
target is a directory or a file.
"""

args = [quarto, "inspect", target]
try:
inspect_json = check_output(args, universal_newlines=True, stderr=subprocess.STDOUT)
Expand All @@ -527,7 +531,7 @@ def validate_quarto_engines(inspect):


def write_quarto_manifest_json(
directory: str,
file_or_directory: str,
inspect: typing.Any,
app_mode: AppMode,
environment: Environment,
Expand All @@ -538,7 +542,7 @@ def write_quarto_manifest_json(
"""
Creates and writes a manifest.json file for the given Quarto project.

:param directory: The directory containing the Quarto project.
:param file_or_directory: The Quarto document or the directory containing the Quarto project.
:param inspect: The parsed JSON from a 'quarto inspect' against the project.
:param app_mode: The application mode to assume (such as AppModes.STATIC_QUARTO)
:param environment: The (optional) Python environment to use.
Expand All @@ -547,10 +551,20 @@ def write_quarto_manifest_json(
:param image: the optional docker image to be specified for off-host execution. Default = None.
"""

extra_files = validate_extra_files(directory, extra_files)
manifest, _ = make_quarto_manifest(directory, inspect, app_mode, environment, extra_files, excludes, image)
manifest_path = join(directory, "manifest.json")
manifest, _ = make_quarto_manifest(
file_or_directory,
inspect,
app_mode,
environment,
extra_files,
excludes,
image,
)

base_dir = file_or_directory
if not isdir(file_or_directory):
base_dir = dirname(file_or_directory)
manifest_path = join(base_dir, "manifest.json")
write_manifest_json(manifest_path, manifest)


Expand Down Expand Up @@ -1307,7 +1321,7 @@ def gather_basic_deployment_info_from_manifest(
def gather_basic_deployment_info_for_quarto(
connect_server: api.RSConnectServer,
app_store: AppStore,
directory: str,
file_or_directory: str,
new: bool,
app_id: int,
title: str,
Expand All @@ -1317,7 +1331,7 @@ def gather_basic_deployment_info_for_quarto(

:param connect_server: The Connect server information.
:param app_store: The store for the specified Quarto project directory.
:param directory: The target Quarto project directory.
:param file_or_directory: The Quarto document or directory containing the Quarto project.
:param new: A flag to force a new deployment.
:param app_id: The identifier of the content to redeploy.
:param title: The content title (optional). A default title is generated when one is not provided.
Expand Down Expand Up @@ -1349,11 +1363,11 @@ def gather_basic_deployment_info_for_quarto(
) % (app_mode.desc(), existing_app_mode.desc())
raise api.RSConnectException(msg)

if directory[-1] == "/":
directory = directory[:-1]
if file_or_directory[-1] == "/":
file_or_directory = file_or_directory[:-1]

default_title = not bool(title)
title = title or _default_title(directory)
title = title or _default_title(file_or_directory)

return (
app_id,
Expand Down Expand Up @@ -1589,19 +1603,18 @@ def create_api_deployment_bundle(


def create_quarto_deployment_bundle(
directory: str,
file_or_directory: str,
extra_files: typing.List[str],
excludes: typing.List[str],
app_mode: AppMode,
inspect: typing.Dict[str, typing.Any],
environment: Environment,
extra_files_need_validating: bool,
image: str = None,
) -> typing.IO[bytes]:
"""
Create an in-memory bundle, ready to deploy.

:param directory: the directory that contains the code being deployed.
:param file_or_directory: The Quarto document or the directory containing the Quarto project.
:param extra_files: a sequence of any extra files to include in the bundle.
:param excludes: a sequence of glob patterns that will exclude matched files.
:param entry_point: the module/executable object for the WSGi framework.
Expand All @@ -1614,13 +1627,10 @@ def create_quarto_deployment_bundle(
:param image: the optional docker image to be specified for off-host execution. Default = None.
:return: the bundle.
"""
if extra_files_need_validating:
extra_files = validate_extra_files(directory, extra_files)

if app_mode is None:
app_mode = AppModes.STATIC_QUARTO

return make_quarto_source_bundle(directory, inspect, app_mode, environment, extra_files, excludes, image)
return make_quarto_source_bundle(file_or_directory, inspect, app_mode, environment, extra_files, excludes, image)


def deploy_bundle(
Expand Down
49 changes: 30 additions & 19 deletions rsconnect/bundle.py
Original file line number Diff line number Diff line change
Expand Up @@ -309,9 +309,8 @@ def make_notebook_source_bundle(
return bundle_file


#
def make_quarto_source_bundle(
directory: str,
file_or_directory: str,
inspect: typing.Dict[str, typing.Any],
app_mode: AppMode,
environment: Environment,
Expand All @@ -326,17 +325,21 @@ def make_quarto_source_bundle(
Returns a file-like object containing the bundle tarball.
"""
manifest, relevant_files = make_quarto_manifest(
directory, inspect, app_mode, environment, extra_files, excludes, image
file_or_directory, inspect, app_mode, environment, extra_files, excludes, image
)
bundle_file = tempfile.TemporaryFile(prefix="rsc_bundle")

base_dir = file_or_directory
if not isdir(file_or_directory):
base_dir = basename(file_or_directory)

with tarfile.open(mode="w:gz", fileobj=bundle_file) as bundle:
bundle_add_buffer(bundle, "manifest.json", json.dumps(manifest, indent=2))
if environment:
bundle_add_buffer(bundle, environment.filename, environment.contents)

for rel_path in relevant_files:
bundle_add_file(bundle, rel_path, directory)
bundle_add_file(bundle, rel_path, base_dir)

# rewind file pointer
bundle_file.seek(0)
Expand Down Expand Up @@ -804,7 +807,7 @@ def _create_quarto_file_list(


def make_quarto_manifest(
directory: str,
file_or_directory: str,
quarto_inspection: typing.Dict[str, typing.Any],
app_mode: AppMode,
environment: Environment,
Expand All @@ -815,7 +818,7 @@ def make_quarto_manifest(
"""
Makes a manifest for a Quarto project.

:param directory: The directory containing the Quarto project.
:param file_or_directory: The Quarto document or the directory containing the Quarto project.
:param quarto_inspection: The parsed JSON from a 'quarto inspect' against the project.
:param app_mode: The application mode to assume.
:param environment: The (optional) Python environment to use.
Expand All @@ -827,21 +830,29 @@ def make_quarto_manifest(
if environment:
extra_files = list(extra_files or []) + [environment.filename]

excludes = list(excludes or []) + [".quarto"]
base_dir = file_or_directory
if isdir(file_or_directory):
# Directory as a Quarto project.
excludes = list(excludes or []) + [".quarto"]

project_config = quarto_inspection.get("config", {}).get("project", {})
output_dir = project_config.get("output-dir", None)
if output_dir:
excludes = excludes + [output_dir]
project_config = quarto_inspection.get("config", {}).get("project", {})
output_dir = project_config.get("output-dir", None)
if output_dir:
excludes = excludes + [output_dir]
else:
render_targets = project_config.get("render", [])
for target in render_targets:
t, _ = splitext(target)
# TODO: Single-file inspect would give inspect.formats.html.pandoc.output-file
# For foo.qmd, we would get an output-file=foo.html, but foo_files is not available.
excludes = excludes + [t + ".html", t + "_files"]

relevant_files = _create_quarto_file_list(base_dir, extra_files, excludes)
else:
render_targets = project_config.get("render", [])
for target in render_targets:
t, _ = splitext(target)
# TODO: Single-file inspect would give inspect.formats.html.pandoc.output-file
# For foo.qmd, we would get an output-file=foo.html, but foo_files is not available.
excludes = excludes + [t + ".html", t + "_files"]
# Standalone Quarto document
base_dir = dirname(file_or_directory)
relevant_files = [file_or_directory] + extra_files

relevant_files = _create_quarto_file_list(directory, extra_files, excludes)
manifest = make_source_manifest(
app_mode,
environment,
Expand All @@ -851,6 +862,6 @@ def make_quarto_manifest(
)

for rel_path in relevant_files:
manifest_add_file(manifest, rel_path, directory)
manifest_add_file(manifest, rel_path, base_dir)

return manifest, relevant_files
Loading