Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
404d2f2
Probe driven development plan
amol- May 12, 2026
65c39e1
rsconnect quickstart stub handler
amol- May 12, 2026
788b8de
expose deploy pyproject command
amol- May 12, 2026
0463a65
Implement deploy pyproject
amol- May 12, 2026
781fd7f
setup the quickstart command infrastructure
amol- May 13, 2026
4665a85
basic templating infrastructure
amol- May 14, 2026
763fa55
encapsulate templates
amol- May 14, 2026
488b274
Quickstart command in place and working
amol- May 18, 2026
a5ddf6b
quickstart templates for quarto, fastapi, jupyter, shiny
amol- May 18, 2026
d3f76aa
documentation of quickstart command
amol- May 19, 2026
2add7e7
quickstart command docs with testing infrastructure
amol- May 19, 2026
dfe5a0a
note about the known risk in tests, it's acceptable
amol- May 19, 2026
1016e7c
Preparing for graduation
amol- May 19, 2026
08ca559
use older click version
amol- May 20, 2026
e80b254
backward compatibility older python
amol- May 20, 2026
0442ef4
Isolate from host global conf
amol- May 20, 2026
9bd9fb7
fixes for windows
amol- May 20, 2026
b71b6a9
Simplify python version logic
amol- May 20, 2026
6220dfd
Make pyproject a template file
amol- May 20, 2026
361ea84
Move READMEs to their own template files
amol- May 20, 2026
06d2194
Migrate from quarto --shiny to quarto-shiny
amol- May 20, 2026
7235a0b
Remove unecessay README
amol- May 20, 2026
050926a
Uniform toward AppModes
amol- May 20, 2026
73ca08a
uniform toward AppModes all commands
amol- May 20, 2026
7317576
handle text decoding
amol- May 20, 2026
c78da7f
Add --requirements-file option and requirements_file pyproject.toml o…
amol- May 21, 2026
2960bd1
temporarily remove quarto-shiny skeleton, not yet supported
amol- Jun 3, 2026
4f2bf3e
quality of life tweaks and --python option
amol- Jun 4, 2026
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
20 changes: 20 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
`--no-set-default` is passed. `CONNECT_SERVER` still takes precedence.
- New `environment` subcommand for managing execution environments on Connect.

### Added

- `rsconnect quickstart` command for scaffolding a new Connect-ready project.
Supported types: `streamlit`, `shiny`, `fastapi`, `api`, `flask`, `notebook`,
`voila`, `quarto`. Creates a uv-managed virtualenv and prints
the local-run and deploy commands.
- `rsconnect deploy pyproject` command for deploying a project described by
`pyproject.toml` with a `[tool.rsconnect]` table containing `app_mode` and

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wish this were tool.connect rather than tool.rsconnect but I imagine this was done since the CLI is still rsconnect (and we aren't migrating away from that yet), yeah?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, the best practice is to name the tool option exactly like the name of the tool that reads it.
So as far as it's named rsconnect the option would be tool.rsconnect.

But we can definitely alias it in the future without too much effort, so that we support both historical and new name if the tool gets renamed.

`entrypoint`. Designed as the deploy partner for projects scaffolded by
`rsconnect quickstart` but works with any conforming `pyproject.toml`.

### Changed

- Bumped the `click` dependency floor to `>=8.2.0`. The newer `click.Choice`
generic support is used by `rsconnect quickstart` to validate the project
type argument.
- Added `uv>=0.9.0` as a runtime dependency. `rsconnect quickstart` invokes
`uv venv` and `uv sync` to populate the scaffolded project's virtualenv.
`uv` installs as a self-contained wheel from PyPI alongside `rsconnect`.

## [1.29.0] - 2026-04-29

- Added `rsconnect deploy nodejs` command for deploying Node.js applications
Expand Down
3 changes: 3 additions & 0 deletions docs/commands/quickstart.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
::: mkdocs-click
:module: rsconnect.main
:command: quickstart
46 changes: 46 additions & 0 deletions docs/deploying.md
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,52 @@ library(rsconnect)
?rsconnect::writeManifest
```

### Deploying from a pyproject.toml

`rsconnect deploy pyproject` reads a `[tool.rsconnect]` table from a project's
`pyproject.toml` instead of taking the app mode and entrypoint as CLI arguments.
It is designed as the deploy partner for projects scaffolded by
`rsconnect quickstart`, but works with any project whose `pyproject.toml`
contains the required keys.

The `[tool.rsconnect]` table has two required keys:

- `app_mode` — the Connect app mode the deployment uses. Supported values
are `python-streamlit`, `python-shiny`, `python-fastapi`, `python-api`,
`jupyter-voila`, `jupyter-static`, `quarto-static`, and `quarto-shiny`.
- `entrypoint` — the file or importable path Connect runs. The expected form
depends on `app_mode`: a script filename such as `app.py` for Streamlit and
Shiny, a `module:object` reference such as `my_app.__connect__:app` for
FastAPI or WSGI APIs, a `report.qmd` for Quarto, and a `notebook.ipynb` for
Voila or static Jupyter content.

A minimal Streamlit project looks like this:

```toml
[project]
name = "my_app"
version = "0.1.0"
requires-python = ">=3.9"
dependencies = ["streamlit"]

[tool.rsconnect]
app_mode = "python-streamlit"
entrypoint = "app.py"
```

Projects scaffolded by `rsconnect quickstart` already contain this table.
From the parent directory of the project, deploy it with:

```bash
rsconnect deploy pyproject my_app/
```

The directory passed to `deploy pyproject` must contain `pyproject.toml`.
Dependencies follow the same resolution rules as the other deploy commands:
`[project.dependencies]` from `pyproject.toml` provides the dependency
snapshot, and `uv.lock` or `requirements.txt` may be supplied via
`--requirements-file` when a pinned environment is needed.

### Options for All Types of Deployments

These options apply to any type of content deployment.
Expand Down
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ nav:
- list: commands/list.md
- login: commands/login.md
- logout: commands/logout.md
- quickstart: commands/quickstart.md
- remove: commands/remove.md
- system: commands/system.md
- version: commands/version.md
Expand Down
7 changes: 6 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -84,13 +84,18 @@ extend_ignore = ["E203", "E231", "E302"]
per-file-ignores = ["tests/test_metadata.py: E501"]

[tool.setuptools]
packages = ["rsconnect"]
packages = ["rsconnect", "rsconnect.quickstart", "rsconnect.quickstart.templates"]

[tool.setuptools_scm]
write_to = "rsconnect/version.py"

[tool.setuptools.package-data]
rsconnect = ["py.typed"]
# Per-mode quickstart templates are shipped as package data so
# pkgutil.get_data() can read them from a wheel install. The glob covers
# every subdirectory (streamlit/, shiny/, fastapi/, api/, notebook/, quarto/)
# and every extension (.py, .ipynb, .qmd) without enumerating each one.
"rsconnect.quickstart.templates" = ["**/*"]

[tool.pytest.ini_options]
markers = ["vetiver: tests for vetiver"]
Expand Down
2 changes: 1 addition & 1 deletion rsconnect/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ def handle_bad_response(self, response: HTTPResponse | T, is_httpresponse: bool
if isinstance(response, HTTPResponse):
if response.exception:
raise RSConnectException(
"Exception trying to connect to %s - %s" % (self.url, response.exception), cause=response.exception
"Could not connect to %s - %s" % (self.url, response.exception), cause=response.exception

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Love this cleanup

)
# Sometimes an ISP will respond to an unknown server name by returning a friendly
# search page so trap that since we know we're expecting JSON from Connect. This
Expand Down
27 changes: 26 additions & 1 deletion rsconnect/bundle.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
from .exception import RSConnectException
from .log import VERBOSE, logger
from .models import AppMode, AppModes, GlobSet
from .shiny_express import escape_to_var_name, is_express_app

if TYPE_CHECKING:
from .actions import QuartoInspectResult
Expand Down Expand Up @@ -1182,8 +1183,12 @@ def infer_entrypoint_candidates(path: str, mimetype: str) -> list[str]:
def guess_deploy_dir(path: str | Path, entrypoint: Optional[str]) -> str:
if path and not exists(path):
raise RSConnectException(f"Path {path} does not exist.")
# The entrypoint is a bare basename meant to be resolved relative to ``path``
# (the later logic does ``join(abs_path, basename(entrypoint))``). Accept it
# when it exists relative to the CWD or inside ``path``; only reject when neither.
if entrypoint and not exists(entrypoint):
raise RSConnectException(f"Entrypoint {entrypoint} does not exist.")
if not (path and isfile(os.path.join(abspath(path), basename(entrypoint)))):
raise RSConnectException(f"Entrypoint {entrypoint} does not exist.")
abs_path = abspath(path)
abs_entrypoint = abspath(entrypoint) if entrypoint else None
if not path and not entrypoint:
Expand Down Expand Up @@ -1224,6 +1229,26 @@ def guess_deploy_dir(path: str | Path, entrypoint: Optional[str]) -> str:
return deploy_dir


def resolve_shiny_express_entrypoint(entrypoint: str, directory: str) -> str:
"""Rewrite a Shiny entrypoint to its Shiny Express module form when needed.

Connect runs Shiny Express apps through the ``shiny.express.app:<var>``
module rather than a plain file, so both ``deploy shiny`` and
``deploy pyproject`` must apply this rewrite to deploy a working app.

Accepts a bare module name (``"app"``) or a filename (``"app.py"``).
Returns the ``shiny.express.app:<escaped>`` form when the app file is a
Shiny Express app, otherwise returns ``entrypoint`` unchanged.

:param str entrypoint: the configured entrypoint, with or without ``.py``.
:param str directory: directory containing the app file.
"""
app_file = entrypoint if entrypoint.lower().endswith(".py") else entrypoint + ".py"
if is_express_app(app_file, directory):
return "shiny.express.app:" + escape_to_var_name(app_file)
return entrypoint


def abs_entrypoint(path: str | Path, entrypoint: str) -> str | None:
if isfile(entrypoint):
return abspath(entrypoint)
Expand Down
Loading
Loading