Skip to content

Commit 3cc4cf9

Browse files
authored
Merge pull request #272 from rstudio/bcwu-pyshinyapps
pyshiny & shinyapps.io
2 parents 4444f01 + 873f8a8 commit 3cc4cf9

26 files changed

+1668
-326
lines changed

.github/workflows/main.yml

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,6 @@ on:
55
tags: ['*']
66
pull_request:
77
branches: [master]
8-
permissions:
9-
id-token: write
10-
contents: write
118
jobs:
129
test:
1310
strategy:
@@ -128,7 +125,8 @@ jobs:
128125
- if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
129126
uses: aws-actions/configure-aws-credentials@v1
130127
with:
131-
role-to-assume: ${{ secrets.DOCS_AWS_ROLE }}
128+
aws-access-key-id: ${{ secrets.DOCS_AWS_ID }}
129+
aws-secret-access-key: ${{ secrets.DOCS_AWS_SECRET }}
132130
aws-region: us-east-1
133131
- if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
134132
run: make promote-docs-in-s3

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
/rsconnect/version.py
1919
htmlcov
2020
/tests/testdata/**/rsconnect-python/
21+
test-home/
2122
/docs/docs/index.md
2223
/docs/docs/changelog.md
2324
/rsconnect-build

my-shiny-app/app.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
from shiny import *
2+
3+
app_ui = ui.page_fluid(
4+
ui.input_slider("n", "N", 0, 100, 20),
5+
ui.output_text_verbatim("txt", placeholder=True),
6+
)
7+
8+
def server(input, output, session):
9+
@output()
10+
@render_text()
11+
def txt():
12+
return f"n*2 is {input.n() * 2}"
13+
14+
app = App(app_ui, server)

my-shiny-app/manifest.json

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"version": 1,
3+
"metadata": {
4+
"appmode": "python-shiny",
5+
"entrypoint": "app"
6+
},
7+
"locale": "en_US.UTF-8",
8+
"python": {
9+
"version": "3.8.12",
10+
"package_manager": {
11+
"name": "pip",
12+
"version": "22.0.4",
13+
"package_file": "requirements.txt"
14+
}
15+
},
16+
"files": {
17+
"requirements.txt": {
18+
"checksum": "aa7771af430e482763c29ce773e399ae"
19+
},
20+
"app.py": {
21+
"checksum": "3aa0db2cc926c4e573783a56749cfb7c"
22+
}
23+
}
24+
}

my-shiny-app/requirements.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
--extra-index-url https://rstudio.github.io/pyshiny-site/pypi/
2+
shiny
3+
websockets

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ click>=7.0.0
33
coverage
44
flake8
55
funcsigs
6+
httpretty==1.1.4
67
importlib-metadata
78
ipykernel
89
ipython

rsconnect/actions.py

Lines changed: 96 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,15 @@ def test_server(connect_server):
218218
raise RSConnectException("\n".join(failures))
219219

220220

221+
def test_shinyapps_server(server: api.ShinyappsServer):
222+
with api.ShinyappsClient(server) as client:
223+
try:
224+
result = client.get_current_user()
225+
server.handle_bad_response(result)
226+
except RSConnectException as exc:
227+
raise RSConnectException("Failed to verify with shinyapps.io ({}).".format(exc))
228+
229+
221230
def test_api_key(connect_server):
222231
"""
223232
Test that an API Key may be used to authenticate with the given RStudio Connect server.
@@ -313,7 +322,7 @@ def check_server_capabilities(connect_server, capability_functions, details_sour
313322
raise RSConnectException(message)
314323

315324

316-
def _make_deployment_name(connect_server, title, force_unique) -> str:
325+
def _make_deployment_name(remote_server: api.TargetableServer, title: str, force_unique: bool) -> str:
317326
"""
318327
Produce a name for a deployment based on its title. It is assumed that the
319328
title is already defaulted and validated as appropriate (meaning the title
@@ -324,7 +333,7 @@ def _make_deployment_name(connect_server, title, force_unique) -> str:
324333
that we collapse repeating underscores and, if the name is too short, it is
325334
padded to the left with underscores.
326335
327-
:param connect_server: the information needed to interact with the Connect server.
336+
:param remote_server: the information needed to interact with the Connect server.
328337
:param title: the title to start with.
329338
:param force_unique: a flag noting whether the generated name must be forced to be
330339
unique.
@@ -338,7 +347,7 @@ def _make_deployment_name(connect_server, title, force_unique) -> str:
338347

339348
# Now, make sure it's unique, if needed.
340349
if force_unique:
341-
name = api.find_unique_name(connect_server, name)
350+
name = api.find_unique_name(remote_server, name)
342351

343352
return name
344353

@@ -903,6 +912,62 @@ def deploy_python_fastapi(
903912
)
904913

905914

915+
def deploy_python_shiny(
916+
connect_server,
917+
directory,
918+
extra_files,
919+
excludes,
920+
entry_point,
921+
new=False,
922+
app_id=None,
923+
title=None,
924+
python=None,
925+
conda_mode=False,
926+
force_generate=False,
927+
log_callback=None,
928+
):
929+
"""
930+
A function to deploy a Python Shiny module to RStudio Connect. Depending on the files involved
931+
and network latency, this may take a bit of time.
932+
933+
:param connect_server: the Connect server information.
934+
:param directory: the app directory to deploy.
935+
:param extra_files: any extra files that should be included in the deploy.
936+
:param excludes: a sequence of glob patterns that will exclude matched files.
937+
:param entry_point: the module/executable object for the WSGi framework.
938+
:param new: a flag to force this as a new deploy.
939+
:param app_id: the ID of an existing application to deploy new files for.
940+
:param title: an optional title for the deploy. If this is not provided, ne will
941+
be generated.
942+
:param python: the optional name of a Python executable.
943+
:param conda_mode: use conda to build an environment.yml
944+
instead of conda, when conda is not supported on RStudio Connect (version<=1.8.0).
945+
:param force_generate: force generating "requirements.txt" or "environment.yml",
946+
even if it already exists.
947+
:param log_callback: the callback to use to write the log to. If this is None
948+
(the default) the lines from the deployment log will be returned as a sequence.
949+
If a log callback is provided, then None will be returned for the log lines part
950+
of the return tuple.
951+
:return: the ultimate URL where the deployed app may be accessed and the sequence
952+
of log lines. The log lines value will be None if a log callback was provided.
953+
"""
954+
return _deploy_by_python_framework(
955+
connect_server,
956+
directory,
957+
extra_files,
958+
excludes,
959+
entry_point,
960+
gather_basic_deployment_info_for_shiny,
961+
new,
962+
app_id,
963+
title,
964+
python,
965+
conda_mode,
966+
force_generate,
967+
log_callback,
968+
)
969+
970+
906971
def deploy_dash_app(
907972
connect_server: api.RSConnectServer,
908973
directory: str,
@@ -1447,7 +1512,7 @@ def _generate_gather_basic_deployment_info_for_python(app_mode: AppMode) -> typi
14471512
"""
14481513

14491514
def gatherer(
1450-
connect_server: api.RSConnectServer,
1515+
remote_server: api.TargetableServer,
14511516
app_store: AppStore,
14521517
directory: str,
14531518
entry_point: str,
@@ -1456,7 +1521,7 @@ def gatherer(
14561521
title: str,
14571522
) -> typing.Tuple[str, int, str, str, bool, AppMode]:
14581523
return _gather_basic_deployment_info_for_framework(
1459-
connect_server,
1524+
remote_server,
14601525
app_store,
14611526
directory,
14621527
entry_point,
@@ -1474,10 +1539,11 @@ def gatherer(
14741539
gather_basic_deployment_info_for_dash = _generate_gather_basic_deployment_info_for_python(AppModes.DASH_APP)
14751540
gather_basic_deployment_info_for_streamlit = _generate_gather_basic_deployment_info_for_python(AppModes.STREAMLIT_APP)
14761541
gather_basic_deployment_info_for_bokeh = _generate_gather_basic_deployment_info_for_python(AppModes.BOKEH_APP)
1542+
gather_basic_deployment_info_for_shiny = _generate_gather_basic_deployment_info_for_python(AppModes.PYTHON_SHINY)
14771543

14781544

14791545
def _gather_basic_deployment_info_for_framework(
1480-
connect_server: api.RSConnectServer,
1546+
remote_server: api.TargetableServer,
14811547
app_store: AppStore,
14821548
directory: str,
14831549
entry_point: str,
@@ -1489,7 +1555,7 @@ def _gather_basic_deployment_info_for_framework(
14891555
"""
14901556
Helps to gather the necessary info for performing a deployment.
14911557
1492-
:param connect_server: the Connect server information.
1558+
:param remote_server: the server information.
14931559
:param app_store: the store for the specified directory.
14941560
:param directory: the primary file being deployed.
14951561
:param entry_point: the entry point for the API in '<module>:<object> format. if
@@ -1514,13 +1580,19 @@ def _gather_basic_deployment_info_for_framework(
15141580
if app_id is None:
15151581
# Possible redeployment - check for saved metadata.
15161582
# Use the saved app information unless overridden by the user.
1517-
app_id, existing_app_mode = app_store.resolve(connect_server.url, app_id, app_mode)
1583+
app_id, existing_app_mode = app_store.resolve(remote_server.url, app_id, app_mode)
15181584
logger.debug("Using app mode from app %s: %s" % (app_id, app_mode))
15191585
elif app_id is not None:
15201586
# Don't read app metadata if app-id is specified. Instead, we need
15211587
# to get this from Connect.
1522-
app = api.get_app_info(connect_server, app_id)
1523-
existing_app_mode = AppModes.get_by_ordinal(app.get("app_mode", 0), True)
1588+
if isinstance(remote_server, api.RSConnectServer):
1589+
app = api.get_app_info(remote_server, app_id)
1590+
existing_app_mode = AppModes.get_by_ordinal(app.get("app_mode", 0), True)
1591+
elif isinstance(remote_server, api.ShinyappsServer):
1592+
app = api.get_shinyapp_info(remote_server, app_id)
1593+
existing_app_mode = AppModes.get_by_cloud_name(app.json_data["mode"])
1594+
else:
1595+
raise RSConnectException("Unable to infer Connect client.")
15241596
if existing_app_mode and app_mode != existing_app_mode:
15251597
msg = (
15261598
"Deploying with mode '%s',\n"
@@ -1538,7 +1610,7 @@ def _gather_basic_deployment_info_for_framework(
15381610
return (
15391611
entry_point,
15401612
app_id,
1541-
_make_deployment_name(connect_server, title, app_id is None),
1613+
_make_deployment_name(remote_server, title, app_id is None),
15421614
title,
15431615
default_title,
15441616
app_mode,
@@ -1697,7 +1769,7 @@ def create_quarto_deployment_bundle(
16971769

16981770

16991771
def deploy_bundle(
1700-
connect_server: api.RSConnectServer,
1772+
remote_server: api.TargetableServer,
17011773
app_id: int,
17021774
name: str,
17031775
title: str,
@@ -1708,7 +1780,7 @@ def deploy_bundle(
17081780
"""
17091781
Deploys the specified bundle.
17101782
1711-
:param connect_server: the Connect server information.
1783+
:param remote_server: the server information.
17121784
:param app_id: the ID of the app to deploy, if this is a redeploy.
17131785
:param name: the name for the deploy.
17141786
:param title: the title for the deploy.
@@ -1718,7 +1790,17 @@ def deploy_bundle(
17181790
:return: application information about the deploy. This includes the ID of the
17191791
task that may be queried for deployment progress.
17201792
"""
1721-
return api.do_bundle_deploy(connect_server, app_id, name, title, title_is_default, bundle, env_vars)
1793+
ce = RSConnectExecutor(
1794+
server=remote_server,
1795+
app_id=app_id,
1796+
name=name,
1797+
title=title,
1798+
title_is_default=title_is_default,
1799+
bundle=bundle,
1800+
env_vars=env_vars,
1801+
)
1802+
ce.deploy_bundle()
1803+
return ce.state["deployed_info"]
17221804

17231805

17241806
def spool_deployment_log(connect_server, app, log_callback):

rsconnect/actions_content.py

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,11 @@
44
import json
55
import time
66
import traceback
7-
87
from concurrent.futures import ThreadPoolExecutor, as_completed
98
from datetime import datetime, timedelta
10-
119
import semver
1210

13-
from .api import RSConnect, emit_task_log
11+
from .api import RSConnectClient, emit_task_log
1412
from .log import logger
1513
from .models import BuildStatus, ContentGuidWithBundle
1614
from .metadata import ContentBuildStore
@@ -37,7 +35,7 @@ def build_add_content(connect_server, content_guids_with_bundle):
3735
+ "please wait for it to finish before adding new content."
3836
)
3937

40-
with RSConnect(connect_server, timeout=120) as client:
38+
with RSConnectClient(connect_server, timeout=120) as client:
4139
if len(content_guids_with_bundle) == 1:
4240
all_content = [client.content_get(content_guids_with_bundle[0].guid)]
4341
else:
@@ -228,7 +226,7 @@ def _monitor_build(connect_server, content_items):
228226

229227
def _build_content_item(connect_server, content, poll_wait):
230228
init_content_build_store(connect_server)
231-
with RSConnect(connect_server) as client:
229+
with RSConnectClient(connect_server) as client:
232230
# Pending futures will still try to execute when ThreadPoolExecutor.shutdown() is called
233231
# so just exit immediately if the current build has been aborted.
234232
# ThreadPoolExecutor.shutdown(cancel_futures=) isnt available until py3.9
@@ -292,7 +290,7 @@ def download_bundle(connect_server, guid_with_bundle):
292290
"""
293291
:param guid_with_bundle: models.ContentGuidWithBundle
294292
"""
295-
with RSConnect(connect_server, timeout=120) as client:
293+
with RSConnectClient(connect_server, timeout=120) as client:
296294
# bundle_id not provided so grab the latest
297295
if not guid_with_bundle.bundle_id:
298296
content = client.get_content(guid_with_bundle.guid)
@@ -311,7 +309,7 @@ def get_content(connect_server, guid):
311309
:param guid: a single guid as a string or list of guids.
312310
:return: a list of content items.
313311
"""
314-
with RSConnect(connect_server, timeout=120) as client:
312+
with RSConnectClient(connect_server, timeout=120) as client:
315313
if isinstance(guid, str):
316314
result = [client.get_content(guid)]
317315
else:
@@ -322,7 +320,7 @@ def get_content(connect_server, guid):
322320
def search_content(
323321
connect_server, published, unpublished, content_type, r_version, py_version, title_contains, order_by
324322
):
325-
with RSConnect(connect_server, timeout=120) as client:
323+
with RSConnectClient(connect_server, timeout=120) as client:
326324
result = client.search_content()
327325
result = _apply_content_filters(
328326
result, published, unpublished, content_type, r_version, py_version, title_contains

0 commit comments

Comments
 (0)