Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
55d6fef
show task result
mmarchetti Mar 15, 2022
23ae554
define pyshiny mode
mmarchetti Mar 17, 2022
11cd292
remove github actions
mmarchetti Apr 20, 2022
7cef656
add example PyShiny
bcwu Apr 21, 2022
1550cd3
add websockets to requirements.txt
bcwu Apr 25, 2022
a08e4a7
Merge remote-tracking branch 'public/master'
bcwu May 3, 2022
64572da
Merge remote-tracking branch 'public/master'
bcwu May 4, 2022
a12d2e2
initial deploy_shiny
bcwu May 4, 2022
ec7224f
Merge pull request #2 from rstudio/deploy_shiny
bcwu May 6, 2022
4922470
Revert "Merge pull request #265 from rstudio/revert-261-mslynch-shiny…
Jul 8, 2022
fb570c5
Merge remote-tracking branch 'public/master' into bcwu-merge-refactor
bcwu Jul 11, 2022
c117289
Merge remote-tracking branch 'public/mslynch-shinyapps-io-refactor' i…
bcwu Jul 11, 2022
905775a
add alias `RSConnect = RSConnectClient` for backwards compatibility w…
Jul 19, 2022
5d1680b
Merge branch 'master' into mslynch-shinyapps-io-refactor
Jul 19, 2022
dde98d0
Merge remote-tracking branch 'public/mslynch-shinyapps-io-refactor' i…
bcwu Jul 20, 2022
7dd0efa
support deploying shiny apps to shinyapps.io without adding a credent…
Jul 20, 2022
b2d3fb8
remove response body from error logging
Jul 20, 2022
6849ffc
use supported_by_shinyapps for deploy_shiny
Jul 20, 2022
0ae28ae
change account short flag to -A to avoid collision with app id
Jul 20, 2022
91a9e07
change account short flag to -A to avoid collision with app id
Jul 20, 2022
5a4ff5c
use decorators consistently to allow for error propagation
Jul 20, 2022
2b71a6c
Merge branch 'deploy-shiny-without-add' into shinyapps
bcwu Jul 20, 2022
2ea4452
make shinyapps.io auth optional when they are not used
bcwu Jul 21, 2022
53d4c97
Merge remote-tracking branch 'public/mslynch-shinyapps-io-refactor' i…
bcwu Jul 21, 2022
873f8a8
Revert "remove github actions"
bcwu Jul 27, 2022
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
6 changes: 2 additions & 4 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,6 @@ on:
tags: ['*']
pull_request:
branches: [master]
permissions:
id-token: write
contents: write
jobs:
test:
strategy:
Expand Down Expand Up @@ -128,7 +125,8 @@ jobs:
- if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
uses: aws-actions/configure-aws-credentials@v1
with:
role-to-assume: ${{ secrets.DOCS_AWS_ROLE }}
aws-access-key-id: ${{ secrets.DOCS_AWS_ID }}
aws-secret-access-key: ${{ secrets.DOCS_AWS_SECRET }}
aws-region: us-east-1
- if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
run: make promote-docs-in-s3
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
/rsconnect/version.py
htmlcov
/tests/testdata/**/rsconnect-python/
test-home/
/docs/docs/index.md
/docs/docs/changelog.md
/rsconnect-build
Expand Down
14 changes: 14 additions & 0 deletions my-shiny-app/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from shiny import *

app_ui = ui.page_fluid(
ui.input_slider("n", "N", 0, 100, 20),
ui.output_text_verbatim("txt", placeholder=True),
)

def server(input, output, session):
@output()
@render_text()
def txt():
return f"n*2 is {input.n() * 2}"

app = App(app_ui, server)
24 changes: 24 additions & 0 deletions my-shiny-app/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"version": 1,
"metadata": {
"appmode": "python-shiny",
"entrypoint": "app"
},
"locale": "en_US.UTF-8",
"python": {
"version": "3.8.12",
"package_manager": {
"name": "pip",
"version": "22.0.4",
"package_file": "requirements.txt"
}
},
"files": {
"requirements.txt": {
"checksum": "aa7771af430e482763c29ce773e399ae"
},
"app.py": {
"checksum": "3aa0db2cc926c4e573783a56749cfb7c"
}
}
}
3 changes: 3 additions & 0 deletions my-shiny-app/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
--extra-index-url https://rstudio.github.io/pyshiny-site/pypi/
shiny
websockets
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ click>=7.0.0
coverage
flake8
funcsigs
httpretty==1.1.4
importlib-metadata
ipykernel
ipython
Expand Down
110 changes: 96 additions & 14 deletions rsconnect/actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,15 @@ def test_server(connect_server):
raise RSConnectException("\n".join(failures))


def test_shinyapps_server(server: api.ShinyappsServer):
with api.ShinyappsClient(server) as client:
try:
result = client.get_current_user()
server.handle_bad_response(result)
except RSConnectException as exc:
raise RSConnectException("Failed to verify with shinyapps.io ({}).".format(exc))


def test_api_key(connect_server):
"""
Test that an API Key may be used to authenticate with the given RStudio Connect server.
Expand Down Expand Up @@ -313,7 +322,7 @@ def check_server_capabilities(connect_server, capability_functions, details_sour
raise RSConnectException(message)


def _make_deployment_name(connect_server, title, force_unique) -> str:
def _make_deployment_name(remote_server: api.TargetableServer, title: str, force_unique: bool) -> str:
"""
Produce a name for a deployment based on its title. It is assumed that the
title is already defaulted and validated as appropriate (meaning the title
Expand All @@ -324,7 +333,7 @@ def _make_deployment_name(connect_server, title, force_unique) -> str:
that we collapse repeating underscores and, if the name is too short, it is
padded to the left with underscores.

:param connect_server: the information needed to interact with the Connect server.
:param remote_server: the information needed to interact with the Connect server.
:param title: the title to start with.
:param force_unique: a flag noting whether the generated name must be forced to be
unique.
Expand All @@ -338,7 +347,7 @@ def _make_deployment_name(connect_server, title, force_unique) -> str:

# Now, make sure it's unique, if needed.
if force_unique:
name = api.find_unique_name(connect_server, name)
name = api.find_unique_name(remote_server, name)

return name

Expand Down Expand Up @@ -903,6 +912,62 @@ def deploy_python_fastapi(
)


def deploy_python_shiny(
connect_server,
directory,
extra_files,
excludes,
entry_point,
new=False,
app_id=None,
title=None,
python=None,
conda_mode=False,
force_generate=False,
log_callback=None,
):
"""
A function to deploy a Python Shiny module to RStudio Connect. Depending on the files involved
and network latency, this may take a bit of time.

:param connect_server: the Connect server information.
:param directory: the app directory to deploy.
:param extra_files: any extra files that should be included in the deploy.
:param excludes: a sequence of glob patterns that will exclude matched files.
:param entry_point: the module/executable object for the WSGi framework.
:param new: a flag to force this as a new deploy.
:param app_id: the ID of an existing application to deploy new files for.
:param title: an optional title for the deploy. If this is not provided, ne will
be generated.
:param python: the optional name of a Python executable.
:param conda_mode: use conda to build an environment.yml
instead of conda, when conda is not supported on RStudio Connect (version<=1.8.0).
:param force_generate: force generating "requirements.txt" or "environment.yml",
even if it already exists.
:param log_callback: the callback to use to write the log to. If this is None
(the default) the lines from the deployment log will be returned as a sequence.
If a log callback is provided, then None will be returned for the log lines part
of the return tuple.
:return: the ultimate URL where the deployed app may be accessed and the sequence
of log lines. The log lines value will be None if a log callback was provided.
"""
return _deploy_by_python_framework(
connect_server,
directory,
extra_files,
excludes,
entry_point,
gather_basic_deployment_info_for_shiny,
new,
app_id,
title,
python,
conda_mode,
force_generate,
log_callback,
)


def deploy_dash_app(
connect_server: api.RSConnectServer,
directory: str,
Expand Down Expand Up @@ -1447,7 +1512,7 @@ def _generate_gather_basic_deployment_info_for_python(app_mode: AppMode) -> typi
"""

def gatherer(
connect_server: api.RSConnectServer,
remote_server: api.TargetableServer,
app_store: AppStore,
directory: str,
entry_point: str,
Expand All @@ -1456,7 +1521,7 @@ def gatherer(
title: str,
) -> typing.Tuple[str, int, str, str, bool, AppMode]:
return _gather_basic_deployment_info_for_framework(
connect_server,
remote_server,
app_store,
directory,
entry_point,
Expand All @@ -1474,10 +1539,11 @@ def gatherer(
gather_basic_deployment_info_for_dash = _generate_gather_basic_deployment_info_for_python(AppModes.DASH_APP)
gather_basic_deployment_info_for_streamlit = _generate_gather_basic_deployment_info_for_python(AppModes.STREAMLIT_APP)
gather_basic_deployment_info_for_bokeh = _generate_gather_basic_deployment_info_for_python(AppModes.BOKEH_APP)
gather_basic_deployment_info_for_shiny = _generate_gather_basic_deployment_info_for_python(AppModes.PYTHON_SHINY)


def _gather_basic_deployment_info_for_framework(
connect_server: api.RSConnectServer,
remote_server: api.TargetableServer,
app_store: AppStore,
directory: str,
entry_point: str,
Expand All @@ -1489,7 +1555,7 @@ def _gather_basic_deployment_info_for_framework(
"""
Helps to gather the necessary info for performing a deployment.

:param connect_server: the Connect server information.
:param remote_server: the server information.
:param app_store: the store for the specified directory.
:param directory: the primary file being deployed.
:param entry_point: the entry point for the API in '<module>:<object> format. if
Expand All @@ -1514,13 +1580,19 @@ def _gather_basic_deployment_info_for_framework(
if app_id is None:
# Possible redeployment - check for saved metadata.
# Use the saved app information unless overridden by the user.
app_id, existing_app_mode = app_store.resolve(connect_server.url, app_id, app_mode)
app_id, existing_app_mode = app_store.resolve(remote_server.url, app_id, app_mode)
logger.debug("Using app mode from app %s: %s" % (app_id, app_mode))
elif app_id is not None:
# Don't read app metadata if app-id is specified. Instead, we need
# to get this from Connect.
app = api.get_app_info(connect_server, app_id)
existing_app_mode = AppModes.get_by_ordinal(app.get("app_mode", 0), True)
if isinstance(remote_server, api.RSConnectServer):
app = api.get_app_info(remote_server, app_id)
existing_app_mode = AppModes.get_by_ordinal(app.get("app_mode", 0), True)
elif isinstance(remote_server, api.ShinyappsServer):
app = api.get_shinyapp_info(remote_server, app_id)
existing_app_mode = AppModes.get_by_cloud_name(app.json_data["mode"])
else:
raise RSConnectException("Unable to infer Connect client.")
if existing_app_mode and app_mode != existing_app_mode:
msg = (
"Deploying with mode '%s',\n"
Expand All @@ -1538,7 +1610,7 @@ def _gather_basic_deployment_info_for_framework(
return (
entry_point,
app_id,
_make_deployment_name(connect_server, title, app_id is None),
_make_deployment_name(remote_server, title, app_id is None),
title,
default_title,
app_mode,
Expand Down Expand Up @@ -1697,7 +1769,7 @@ def create_quarto_deployment_bundle(


def deploy_bundle(
connect_server: api.RSConnectServer,
remote_server: api.TargetableServer,
app_id: int,
name: str,
title: str,
Expand All @@ -1708,7 +1780,7 @@ def deploy_bundle(
"""
Deploys the specified bundle.

:param connect_server: the Connect server information.
:param remote_server: the server information.
:param app_id: the ID of the app to deploy, if this is a redeploy.
:param name: the name for the deploy.
:param title: the title for the deploy.
Expand All @@ -1718,7 +1790,17 @@ def deploy_bundle(
:return: application information about the deploy. This includes the ID of the
task that may be queried for deployment progress.
"""
return api.do_bundle_deploy(connect_server, app_id, name, title, title_is_default, bundle, env_vars)
ce = RSConnectExecutor(
server=remote_server,
app_id=app_id,
name=name,
title=title,
title_is_default=title_is_default,
bundle=bundle,
env_vars=env_vars,
)
ce.deploy_bundle()
return ce.state["deployed_info"]


def spool_deployment_log(connect_server, app, log_callback):
Expand Down
14 changes: 6 additions & 8 deletions rsconnect/actions_content.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,11 @@
import json
import time
import traceback

from concurrent.futures import ThreadPoolExecutor, as_completed
from datetime import datetime, timedelta

import semver

from .api import RSConnect, emit_task_log
from .api import RSConnectClient, emit_task_log
from .log import logger
from .models import BuildStatus, ContentGuidWithBundle
from .metadata import ContentBuildStore
Expand All @@ -37,7 +35,7 @@ def build_add_content(connect_server, content_guids_with_bundle):
+ "please wait for it to finish before adding new content."
)

with RSConnect(connect_server, timeout=120) as client:
with RSConnectClient(connect_server, timeout=120) as client:
if len(content_guids_with_bundle) == 1:
all_content = [client.content_get(content_guids_with_bundle[0].guid)]
else:
Expand Down Expand Up @@ -228,7 +226,7 @@ def _monitor_build(connect_server, content_items):

def _build_content_item(connect_server, content, poll_wait):
init_content_build_store(connect_server)
with RSConnect(connect_server) as client:
with RSConnectClient(connect_server) as client:
# Pending futures will still try to execute when ThreadPoolExecutor.shutdown() is called
# so just exit immediately if the current build has been aborted.
# ThreadPoolExecutor.shutdown(cancel_futures=) isnt available until py3.9
Expand Down Expand Up @@ -292,7 +290,7 @@ def download_bundle(connect_server, guid_with_bundle):
"""
:param guid_with_bundle: models.ContentGuidWithBundle
"""
with RSConnect(connect_server, timeout=120) as client:
with RSConnectClient(connect_server, timeout=120) as client:
# bundle_id not provided so grab the latest
if not guid_with_bundle.bundle_id:
content = client.get_content(guid_with_bundle.guid)
Expand All @@ -311,7 +309,7 @@ def get_content(connect_server, guid):
:param guid: a single guid as a string or list of guids.
:return: a list of content items.
"""
with RSConnect(connect_server, timeout=120) as client:
with RSConnectClient(connect_server, timeout=120) as client:
if isinstance(guid, str):
result = [client.get_content(guid)]
else:
Expand All @@ -322,7 +320,7 @@ def get_content(connect_server, guid):
def search_content(
connect_server, published, unpublished, content_type, r_version, py_version, title_contains, order_by
):
with RSConnect(connect_server, timeout=120) as client:
with RSConnectClient(connect_server, timeout=120) as client:
result = client.search_content()
result = _apply_content_filters(
result, published, unpublished, content_type, r_version, py_version, title_contains
Expand Down
Loading