rsconnect-python 1.32.0__tar.gz → 1.33.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {rsconnect_python-1.32.0 → rsconnect_python-1.33.0}/PKG-INFO +17 -8
- {rsconnect_python-1.32.0 → rsconnect_python-1.33.0}/README.md +13 -5
- {rsconnect_python-1.32.0 → rsconnect_python-1.33.0}/pyproject.toml +6 -3
- {rsconnect_python-1.32.0 → rsconnect_python-1.33.0}/pyproject.toml.orig +3 -3
- {rsconnect_python-1.32.0 → rsconnect_python-1.33.0}/rsconnect/api.py +129 -101
- {rsconnect_python-1.32.0 → rsconnect_python-1.33.0}/rsconnect/bundle.py +44 -6
- {rsconnect_python-1.32.0 → rsconnect_python-1.33.0}/rsconnect/connect_cloud.py +2 -2
- {rsconnect_python-1.32.0 → rsconnect_python-1.33.0}/rsconnect/environment.py +125 -53
- {rsconnect_python-1.32.0 → rsconnect_python-1.33.0}/rsconnect/main.py +267 -120
- {rsconnect_python-1.32.0 → rsconnect_python-1.33.0}/rsconnect/metadata.py +3 -3
- {rsconnect_python-1.32.0 → rsconnect_python-1.33.0}/rsconnect/models.py +16 -60
- {rsconnect_python-1.32.0 → rsconnect_python-1.33.0}/rsconnect/oauth.py +182 -1
- {rsconnect_python-1.32.0 → rsconnect_python-1.33.0}/rsconnect/preflight.py +31 -21
- {rsconnect_python-1.32.0 → rsconnect_python-1.33.0}/rsconnect/preflight_node.py +2 -2
- {rsconnect_python-1.32.0 → rsconnect_python-1.33.0}/rsconnect/pyproject.py +19 -2
- {rsconnect_python-1.32.0 → rsconnect_python-1.33.0}/rsconnect/subprocesses/inspect_environment.py +205 -37
- {rsconnect_python-1.32.0 → rsconnect_python-1.33.0}/rsconnect/validation.py +6 -4
- {rsconnect_python-1.32.0 → rsconnect_python-1.33.0}/LICENSE.md +0 -0
- {rsconnect_python-1.32.0 → rsconnect_python-1.33.0}/rsconnect/__init__.py +0 -0
- {rsconnect_python-1.32.0 → rsconnect_python-1.33.0}/rsconnect/actions.py +0 -0
- {rsconnect_python-1.32.0 → rsconnect_python-1.33.0}/rsconnect/actions_content.py +0 -0
- {rsconnect_python-1.32.0 → rsconnect_python-1.33.0}/rsconnect/actions_environment.py +0 -0
- {rsconnect_python-1.32.0 → rsconnect_python-1.33.0}/rsconnect/actions_integration.py +0 -0
- {rsconnect_python-1.32.0 → rsconnect_python-1.33.0}/rsconnect/certificates.py +0 -0
- {rsconnect_python-1.32.0 → rsconnect_python-1.33.0}/rsconnect/device_login.py +0 -0
- {rsconnect_python-1.32.0 → rsconnect_python-1.33.0}/rsconnect/environment_node.py +0 -0
- {rsconnect_python-1.32.0 → rsconnect_python-1.33.0}/rsconnect/environment_r.py +0 -0
- {rsconnect_python-1.32.0 → rsconnect_python-1.33.0}/rsconnect/exception.py +0 -0
- {rsconnect_python-1.32.0 → rsconnect_python-1.33.0}/rsconnect/git_metadata.py +0 -0
- {rsconnect_python-1.32.0 → rsconnect_python-1.33.0}/rsconnect/http_support.py +0 -0
- {rsconnect_python-1.32.0 → rsconnect_python-1.33.0}/rsconnect/json_web_token.py +0 -0
- {rsconnect_python-1.32.0 → rsconnect_python-1.33.0}/rsconnect/log.py +0 -0
- {rsconnect_python-1.32.0 → rsconnect_python-1.33.0}/rsconnect/py.typed +0 -0
- {rsconnect_python-1.32.0 → rsconnect_python-1.33.0}/rsconnect/quickstart/__init__.py +0 -0
- {rsconnect_python-1.32.0 → rsconnect_python-1.33.0}/rsconnect/quickstart/quickstart.py +0 -0
- {rsconnect_python-1.32.0 → rsconnect_python-1.33.0}/rsconnect/quickstart/templates/__init__.py +0 -0
- {rsconnect_python-1.32.0 → rsconnect_python-1.33.0}/rsconnect/quickstart/templates/api/README.md.tmpl +0 -0
- {rsconnect_python-1.32.0 → rsconnect_python-1.33.0}/rsconnect/quickstart/templates/api/__connect__.py.tmpl +0 -0
- {rsconnect_python-1.32.0 → rsconnect_python-1.33.0}/rsconnect/quickstart/templates/api/__init__.py.tmpl +0 -0
- {rsconnect_python-1.32.0 → rsconnect_python-1.33.0}/rsconnect/quickstart/templates/api/__main__.py.tmpl +0 -0
- {rsconnect_python-1.32.0 → rsconnect_python-1.33.0}/rsconnect/quickstart/templates/api/app.py.tmpl +0 -0
- {rsconnect_python-1.32.0 → rsconnect_python-1.33.0}/rsconnect/quickstart/templates/api/pyproject.toml.tmpl +0 -0
- {rsconnect_python-1.32.0 → rsconnect_python-1.33.0}/rsconnect/quickstart/templates/fastapi/README.md.tmpl +0 -0
- {rsconnect_python-1.32.0 → rsconnect_python-1.33.0}/rsconnect/quickstart/templates/fastapi/__connect__.py.tmpl +0 -0
- {rsconnect_python-1.32.0 → rsconnect_python-1.33.0}/rsconnect/quickstart/templates/fastapi/__init__.py.tmpl +0 -0
- {rsconnect_python-1.32.0 → rsconnect_python-1.33.0}/rsconnect/quickstart/templates/fastapi/__main__.py.tmpl +0 -0
- {rsconnect_python-1.32.0 → rsconnect_python-1.33.0}/rsconnect/quickstart/templates/fastapi/app.py.tmpl +0 -0
- {rsconnect_python-1.32.0 → rsconnect_python-1.33.0}/rsconnect/quickstart/templates/fastapi/pyproject.toml.tmpl +0 -0
- {rsconnect_python-1.32.0 → rsconnect_python-1.33.0}/rsconnect/quickstart/templates/notebook/README.md.tmpl +0 -0
- {rsconnect_python-1.32.0 → rsconnect_python-1.33.0}/rsconnect/quickstart/templates/notebook/notebook.ipynb.tmpl +0 -0
- {rsconnect_python-1.32.0 → rsconnect_python-1.33.0}/rsconnect/quickstart/templates/notebook/pyproject.toml.tmpl +0 -0
- {rsconnect_python-1.32.0 → rsconnect_python-1.33.0}/rsconnect/quickstart/templates/quarto/README.md.tmpl +0 -0
- {rsconnect_python-1.32.0 → rsconnect_python-1.33.0}/rsconnect/quickstart/templates/quarto/pyproject.toml.tmpl +0 -0
- {rsconnect_python-1.32.0 → rsconnect_python-1.33.0}/rsconnect/quickstart/templates/quarto/report.qmd.tmpl +0 -0
- {rsconnect_python-1.32.0 → rsconnect_python-1.33.0}/rsconnect/quickstart/templates/shiny/README.md.tmpl +0 -0
- {rsconnect_python-1.32.0 → rsconnect_python-1.33.0}/rsconnect/quickstart/templates/shiny/app.py.tmpl +0 -0
- {rsconnect_python-1.32.0 → rsconnect_python-1.33.0}/rsconnect/quickstart/templates/shiny/pyproject.toml.tmpl +0 -0
- {rsconnect_python-1.32.0 → rsconnect_python-1.33.0}/rsconnect/quickstart/templates/streamlit/README.md.tmpl +0 -0
- {rsconnect_python-1.32.0 → rsconnect_python-1.33.0}/rsconnect/quickstart/templates/streamlit/app.py.tmpl +0 -0
- {rsconnect_python-1.32.0 → rsconnect_python-1.33.0}/rsconnect/quickstart/templates/streamlit/pyproject.toml.tmpl +0 -0
- {rsconnect_python-1.32.0 → rsconnect_python-1.33.0}/rsconnect/quickstart/templates/voila/README.md.tmpl +0 -0
- {rsconnect_python-1.32.0 → rsconnect_python-1.33.0}/rsconnect/quickstart/templates/voila/pyproject.toml.tmpl +0 -0
- {rsconnect_python-1.32.0 → rsconnect_python-1.33.0}/rsconnect/shiny_express.py +0 -0
- {rsconnect_python-1.32.0 → rsconnect_python-1.33.0}/rsconnect/snowflake.py +0 -0
- {rsconnect_python-1.32.0 → rsconnect_python-1.33.0}/rsconnect/subprocesses/__init__.py +0 -0
- {rsconnect_python-1.32.0 → rsconnect_python-1.33.0}/rsconnect/timeouts.py +0 -0
- {rsconnect_python-1.32.0 → rsconnect_python-1.33.0}/rsconnect/utils_package.py +0 -0
- {rsconnect_python-1.32.0 → rsconnect_python-1.33.0}/rsconnect/version_check.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: rsconnect_python
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.33.0
|
|
4
4
|
Summary: The Posit Connect command-line interface.
|
|
5
5
|
Author: Posit, PBC
|
|
6
6
|
Author-email: Posit, PBC <rsconnect@posit.co>
|
|
@@ -26,7 +26,7 @@ License: # MIT License
|
|
|
26
26
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
27
27
|
SOFTWARE.
|
|
28
28
|
Requires-Dist: typing-extensions>=4.8.0
|
|
29
|
-
Requires-Dist: pip>=
|
|
29
|
+
Requires-Dist: pip>=20.3
|
|
30
30
|
Requires-Dist: uv>=0.9.0
|
|
31
31
|
Requires-Dist: semver>=2.0.0,<4.0.0
|
|
32
32
|
Requires-Dist: pyjwt>=2.4.0 ; python_full_version < '3.9'
|
|
@@ -35,7 +35,8 @@ Requires-Dist: click>=8.0.0
|
|
|
35
35
|
Requires-Dist: packaging>=20.0
|
|
36
36
|
Requires-Dist: toml>=0.10 ; python_full_version < '3.11'
|
|
37
37
|
Requires-Dist: keyring>=23.0.0 ; extra == 'keyring'
|
|
38
|
-
Requires-Dist: snowflake-cli ; extra == 'snowflake'
|
|
38
|
+
Requires-Dist: snowflake-cli>=2.8.2 ; python_full_version < '3.10' and extra == 'snowflake'
|
|
39
|
+
Requires-Dist: snowflake-cli>=3.23.0 ; python_full_version >= '3.10' and extra == 'snowflake'
|
|
39
40
|
Requires-Python: >=3.8
|
|
40
41
|
Project-URL: Repository, http://github.com/posit-dev/rsconnect-python
|
|
41
42
|
Project-URL: Documentation, https://docs.posit.co/rsconnect-python
|
|
@@ -69,12 +70,10 @@ python -m pip install rsconnect-python
|
|
|
69
70
|
|
|
70
71
|
## Usage with Posit Connect
|
|
71
72
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
Store your credentials:
|
|
73
|
+
Log in to your Posit Connect server:
|
|
75
74
|
|
|
76
75
|
```bash
|
|
77
|
-
rsconnect
|
|
76
|
+
rsconnect login https://connect.example.com
|
|
78
77
|
```
|
|
79
78
|
|
|
80
79
|
Deploy your application:
|
|
@@ -83,6 +82,16 @@ Deploy your application:
|
|
|
83
82
|
rsconnect deploy shiny app.py --title "my shiny app"
|
|
84
83
|
```
|
|
85
84
|
|
|
85
|
+
Browser login requires Posit Connect 2026.02.0 or later. In remote containers,
|
|
86
|
+
or other sessions where the browser cannot reach the CLI's local callback, use
|
|
87
|
+
`--use-device-code` with Connect 2026.06.0 or later.
|
|
88
|
+
|
|
89
|
+
For older servers, or automation that cannot use identity tokens, [get an API
|
|
90
|
+
key](https://docs.posit.co/connect/user/api-keys/) and store it with
|
|
91
|
+
`rsconnect add`.
|
|
92
|
+
See the [authentication guide](https://docs.posit.co/rsconnect-python/#managing-server-information)
|
|
93
|
+
for identity token requirements and other login options.
|
|
94
|
+
|
|
86
95
|
[Read more about publisher and admin capabilities on the docs site.](https://docs.posit.co/rsconnect-python)
|
|
87
96
|
|
|
88
97
|
## Usage with Posit Connect Cloud
|
|
@@ -99,7 +108,7 @@ Deploy your application:
|
|
|
99
108
|
rsconnect deploy shiny app.py --name cloud --title "my shiny app"
|
|
100
109
|
```
|
|
101
110
|
|
|
102
|
-
For non-interactive use such as CI, pass
|
|
111
|
+
For non-interactive use such as CI, pass client credentials with
|
|
103
112
|
`--client-id` and `--client-secret`, or the `CONNECT_CLOUD_CLIENT_ID` and
|
|
104
113
|
`CONNECT_CLOUD_CLIENT_SECRET` environment variables.
|
|
105
114
|
|
|
@@ -24,12 +24,10 @@ python -m pip install rsconnect-python
|
|
|
24
24
|
|
|
25
25
|
## Usage with Posit Connect
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
Store your credentials:
|
|
27
|
+
Log in to your Posit Connect server:
|
|
30
28
|
|
|
31
29
|
```bash
|
|
32
|
-
rsconnect
|
|
30
|
+
rsconnect login https://connect.example.com
|
|
33
31
|
```
|
|
34
32
|
|
|
35
33
|
Deploy your application:
|
|
@@ -38,6 +36,16 @@ Deploy your application:
|
|
|
38
36
|
rsconnect deploy shiny app.py --title "my shiny app"
|
|
39
37
|
```
|
|
40
38
|
|
|
39
|
+
Browser login requires Posit Connect 2026.02.0 or later. In remote containers,
|
|
40
|
+
or other sessions where the browser cannot reach the CLI's local callback, use
|
|
41
|
+
`--use-device-code` with Connect 2026.06.0 or later.
|
|
42
|
+
|
|
43
|
+
For older servers, or automation that cannot use identity tokens, [get an API
|
|
44
|
+
key](https://docs.posit.co/connect/user/api-keys/) and store it with
|
|
45
|
+
`rsconnect add`.
|
|
46
|
+
See the [authentication guide](https://docs.posit.co/rsconnect-python/#managing-server-information)
|
|
47
|
+
for identity token requirements and other login options.
|
|
48
|
+
|
|
41
49
|
[Read more about publisher and admin capabilities on the docs site.](https://docs.posit.co/rsconnect-python)
|
|
42
50
|
|
|
43
51
|
## Usage with Posit Connect Cloud
|
|
@@ -54,7 +62,7 @@ Deploy your application:
|
|
|
54
62
|
rsconnect deploy shiny app.py --name cloud --title "my shiny app"
|
|
55
63
|
```
|
|
56
64
|
|
|
57
|
-
For non-interactive use such as CI, pass
|
|
65
|
+
For non-interactive use such as CI, pass client credentials with
|
|
58
66
|
`--client-id` and `--client-secret`, or the `CONNECT_CLOUD_CLIENT_ID` and
|
|
59
67
|
`CONNECT_CLOUD_CLIENT_SECRET` environment variables.
|
|
60
68
|
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
name = "rsconnect_python"
|
|
3
3
|
description = "The Posit Connect command-line interface."
|
|
4
4
|
requires-python = ">=3.8"
|
|
5
|
-
version = "1.
|
|
5
|
+
version = "1.33.0"
|
|
6
6
|
dependencies = [
|
|
7
7
|
"typing-extensions>=4.8.0",
|
|
8
|
-
"pip>=
|
|
8
|
+
"pip>=20.3",
|
|
9
9
|
"uv>=0.9.0",
|
|
10
10
|
"semver>=2.0.0,<4.0.0",
|
|
11
11
|
"pyjwt>=2.4.0; python_version < '3.9'",
|
|
@@ -31,7 +31,10 @@ rsconnect = "rsconnect.main:cli"
|
|
|
31
31
|
|
|
32
32
|
[project.optional-dependencies]
|
|
33
33
|
keyring = ["keyring>=23.0.0"]
|
|
34
|
-
snowflake = [
|
|
34
|
+
snowflake = [
|
|
35
|
+
"snowflake-cli>=2.8.2; python_version < '3.10'",
|
|
36
|
+
"snowflake-cli>=3.23.0; python_version >= '3.10'",
|
|
37
|
+
]
|
|
35
38
|
|
|
36
39
|
[project.urls]
|
|
37
40
|
Repository = "http://github.com/posit-dev/rsconnect-python"
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
name = "rsconnect_python"
|
|
3
3
|
description = "The Posit Connect command-line interface."
|
|
4
4
|
requires-python = ">=3.8"
|
|
5
|
-
version = "1.
|
|
6
|
-
dependencies = [ "typing-extensions>=4.8.0", "pip>=
|
|
5
|
+
version = "1.33.0"
|
|
6
|
+
dependencies = [ "typing-extensions>=4.8.0", "pip>=20.3", "uv>=0.9.0", "semver>=2.0.0,<4.0.0", "pyjwt>=2.4.0; python_version < '3.9'", "pyjwt>=2.15.0; python_version >= '3.9'", "click>=8.0.0", "packaging>=20.0", "toml>=0.10; python_version < '3.11'",]
|
|
7
7
|
[[project.authors]]
|
|
8
8
|
name = "Posit, PBC"
|
|
9
9
|
email = "rsconnect@posit.co"
|
|
@@ -28,7 +28,7 @@ rsconnect = "rsconnect.main:cli"
|
|
|
28
28
|
|
|
29
29
|
[project.optional-dependencies]
|
|
30
30
|
keyring = [ "keyring>=23.0.0",]
|
|
31
|
-
snowflake = [ "snowflake-cli",]
|
|
31
|
+
snowflake = [ "snowflake-cli>=2.8.2; python_version < '3.10'", "snowflake-cli>=3.23.0; python_version >= '3.10'",]
|
|
32
32
|
|
|
33
33
|
[project.urls]
|
|
34
34
|
Repository = "http://github.com/posit-dev/rsconnect-python"
|
|
@@ -86,7 +86,6 @@ from .models import (
|
|
|
86
86
|
BootstrapOutputDTO,
|
|
87
87
|
BuildOutputDTO,
|
|
88
88
|
BundleMetadata,
|
|
89
|
-
ContentItemV0,
|
|
90
89
|
ContentItemV1,
|
|
91
90
|
DeleteInputDTO,
|
|
92
91
|
DeleteOutputDTO,
|
|
@@ -106,6 +105,7 @@ from .models import (
|
|
|
106
105
|
ServerSettings,
|
|
107
106
|
TaskStatusV1,
|
|
108
107
|
UserRecord,
|
|
108
|
+
is_content_guid,
|
|
109
109
|
)
|
|
110
110
|
from .snowflake import generate_jwt, get_parameters
|
|
111
111
|
from .timeouts import get_task_timeout, get_task_timeout_help_message
|
|
@@ -492,6 +492,16 @@ def server_supports_git_metadata(server_version: Optional[str]) -> bool:
|
|
|
492
492
|
return False
|
|
493
493
|
|
|
494
494
|
|
|
495
|
+
# App modes that Connect renders (or serves as-is) at deploy time rather than running
|
|
496
|
+
# as a process. See RSConnectExecutor.is_rendered_content.
|
|
497
|
+
_RENDERED_APP_MODES = {
|
|
498
|
+
AppModes.RMD.name(),
|
|
499
|
+
AppModes.STATIC.name(),
|
|
500
|
+
AppModes.JUPYTER_NOTEBOOK.name(),
|
|
501
|
+
AppModes.STATIC_QUARTO.name(),
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
|
|
495
505
|
def server_supports_draft_deploy(server_version: Optional[str]) -> bool:
|
|
496
506
|
"""
|
|
497
507
|
Check if the server supports deploying a bundle as a draft and activating it
|
|
@@ -553,77 +563,29 @@ class RSConnectClient(BearerTokenHTTPServer):
|
|
|
553
563
|
return isinstance(self._server, RSConnectServer) and bool(self._server.oauth_client_id)
|
|
554
564
|
|
|
555
565
|
def _attempt_token_refresh(self) -> bool:
|
|
556
|
-
from .oauth import
|
|
557
|
-
InvalidClientError,
|
|
558
|
-
discover_oauth_metadata,
|
|
559
|
-
keyring_delete_tokens,
|
|
560
|
-
keyring_get_tokens,
|
|
561
|
-
keyring_store_token,
|
|
562
|
-
refresh_access_token,
|
|
563
|
-
register_client,
|
|
564
|
-
)
|
|
565
|
-
from .metadata import ServerStore
|
|
566
|
+
from .oauth import InvalidClientError, NotLoggedInError, refresh_stored_tokens
|
|
566
567
|
|
|
567
568
|
server = cast(RSConnectServer, self._server)
|
|
568
|
-
|
|
569
|
-
# The keyring is where a login stores its tokens; the entry's own fields are
|
|
570
|
-
# the fallback for a machine without one.
|
|
571
|
-
_, refresh_token = keyring_get_tokens(server.url)
|
|
572
|
-
if not refresh_token:
|
|
573
|
-
entry = ServerStore().saved_entry(server.server_name, server.url)
|
|
574
|
-
if entry:
|
|
575
|
-
refresh_token = entry.get("oauth_refresh_token") # type: ignore[assignment]
|
|
576
|
-
if not refresh_token:
|
|
577
|
-
return False
|
|
578
|
-
|
|
579
569
|
try:
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
570
|
+
new_access = refresh_stored_tokens(
|
|
571
|
+
server.url,
|
|
572
|
+
server.oauth_client_id or "",
|
|
573
|
+
server.server_name,
|
|
574
|
+
server.insecure,
|
|
575
|
+
server.ca_data,
|
|
576
|
+
rejected_access_token=server.oauth_access_token,
|
|
583
577
|
)
|
|
578
|
+
except NotLoggedInError:
|
|
579
|
+
return False
|
|
584
580
|
except InvalidClientError:
|
|
585
|
-
|
|
586
|
-
keyring_delete_tokens(server.url)
|
|
587
|
-
store = ServerStore()
|
|
588
|
-
entry = store.saved_entry(server.server_name, server.url)
|
|
589
|
-
if entry:
|
|
590
|
-
entry_name = str(entry.get("name", server.server_name or server.url))
|
|
591
|
-
store.update_oauth_tokens(entry_name, None, None, None)
|
|
592
|
-
try:
|
|
593
|
-
metadata = discover_oauth_metadata(server.url, server.insecure, server.ca_data)
|
|
594
|
-
new_client_id = register_client(metadata, server.url, server.insecure, server.ca_data)
|
|
595
|
-
server.oauth_client_id = new_client_id
|
|
596
|
-
if entry:
|
|
597
|
-
entry["oauth_client_id"] = new_client_id # type: ignore[typeddict-unknown-key]
|
|
598
|
-
store._set(entry_name, entry) # type: ignore[possibly-undefined]
|
|
599
|
-
logger.warning("OAuth client was re-registered; please run `rsconnect login` again.")
|
|
600
|
-
except Exception as exc:
|
|
601
|
-
logger.warning(
|
|
602
|
-
"OAuth client re-registration failed (%s). Please run `rsconnect login` again." % type(exc).__name__
|
|
603
|
-
)
|
|
581
|
+
logger.warning("The OAuth client was deleted on the server; please run `rsconnect login` again.")
|
|
604
582
|
return False
|
|
605
583
|
except Exception as exc:
|
|
606
584
|
logger.warning("OAuth token refresh failed (%s)." % type(exc).__name__)
|
|
607
585
|
return False
|
|
608
586
|
|
|
609
|
-
new_access = token_response["access_token"]
|
|
610
|
-
new_refresh = token_response.get("refresh_token", refresh_token)
|
|
611
|
-
expires_in = token_response.get("expires_in")
|
|
612
|
-
import time
|
|
613
|
-
|
|
614
|
-
new_expiry = time.time() + expires_in if expires_in else None
|
|
615
|
-
|
|
616
587
|
self.authorization(f"Bearer {new_access}")
|
|
617
588
|
server.oauth_access_token = new_access
|
|
618
|
-
|
|
619
|
-
stored = keyring_store_token(server.url, new_access, new_refresh)
|
|
620
|
-
if not stored:
|
|
621
|
-
store = ServerStore()
|
|
622
|
-
entry = store.saved_entry(server.server_name, server.url)
|
|
623
|
-
if entry:
|
|
624
|
-
entry_name = str(entry.get("name", server.server_name or server.url))
|
|
625
|
-
store.update_oauth_tokens(entry_name, new_access, new_refresh, new_expiry)
|
|
626
|
-
|
|
627
589
|
return True
|
|
628
590
|
|
|
629
591
|
def _tweak_response(self, response: HTTPResponse) -> JsonData | HTTPResponse:
|
|
@@ -639,7 +601,11 @@ class RSConnectClient(BearerTokenHTTPServer):
|
|
|
639
601
|
return response
|
|
640
602
|
|
|
641
603
|
def bootstrap(self) -> BootstrapOutputDTO | HTTPResponse:
|
|
642
|
-
response = cast(Union[BootstrapOutputDTO, HTTPResponse], self.post("v1/
|
|
604
|
+
response = cast(Union[BootstrapOutputDTO, HTTPResponse], self.post("v1/bootstrap"))
|
|
605
|
+
# Connect versions before 2024.05.0 only provide the experimental endpoint. The server version
|
|
606
|
+
# is not exposed to unauthenticated requests, so fall back based on the 404 instead.
|
|
607
|
+
if isinstance(response, HTTPResponse) and response.status == 404:
|
|
608
|
+
response = cast(Union[BootstrapOutputDTO, HTTPResponse], self.post("v1/experimental/bootstrap"))
|
|
643
609
|
# TODO: The place where bootstrap() is called expects a JSON object if the response is successfule, and a
|
|
644
610
|
# HTTPResponse if it is not; then it handles the error. This is different from the other methods, and probably
|
|
645
611
|
# should be changed in the future. For this to work, we will _not_ call .handle_bad_response() here at present.
|
|
@@ -679,11 +645,6 @@ class RSConnectClient(BearerTokenHTTPServer):
|
|
|
679
645
|
response = self.get("v1/server_settings/nodejs")
|
|
680
646
|
return cast(typing.Dict[str, Any], self._server.handle_bad_response(response))
|
|
681
647
|
|
|
682
|
-
def app_get(self, app_id: str) -> ContentItemV0:
|
|
683
|
-
response = cast(Union[ContentItemV0, HTTPResponse], self.get(f"applications/{app_id}"))
|
|
684
|
-
response = self._server.handle_bad_response(response)
|
|
685
|
-
return response
|
|
686
|
-
|
|
687
648
|
def add_environment_vars(self, content_guid: str, env_vars: list[tuple[str, str]]):
|
|
688
649
|
env_body = [dict(name=kv[0], value=kv[1]) for kv in env_vars]
|
|
689
650
|
return self.patch(f"v1/content/{content_guid}/environment", body=env_body)
|
|
@@ -738,22 +699,6 @@ class RSConnectClient(BearerTokenHTTPServer):
|
|
|
738
699
|
response = self._server.handle_bad_response(response)
|
|
739
700
|
return response
|
|
740
701
|
|
|
741
|
-
def get_content_by_id(self, id: str) -> ContentItemV1:
|
|
742
|
-
"""
|
|
743
|
-
Get content by ID, which can be either a numeric ID (legacy) or GUID.
|
|
744
|
-
|
|
745
|
-
:param app_id: Either a numeric ID (e.g., "1234") or GUID (e.g., "abc-def-123")
|
|
746
|
-
:return: ContentItemV1 data
|
|
747
|
-
"""
|
|
748
|
-
# Check if it looks like a GUID (contains hyphens)
|
|
749
|
-
if "-" in str(id):
|
|
750
|
-
return self.content_get(id)
|
|
751
|
-
else:
|
|
752
|
-
# Legacy numeric ID - get v0 content first to get GUID
|
|
753
|
-
app_v0 = self.app_get(id)
|
|
754
|
-
# TODO: deprecation warning here
|
|
755
|
-
return self.content_get(app_v0["guid"])
|
|
756
|
-
|
|
757
702
|
def content_create(self, name: str) -> ContentItemV1:
|
|
758
703
|
response = cast(Union[ContentItemV1, HTTPResponse], self.post("v1/content", body={"name": name}))
|
|
759
704
|
response = self._server.handle_bad_response(response)
|
|
@@ -765,10 +710,10 @@ class RSConnectClient(BearerTokenHTTPServer):
|
|
|
765
710
|
"""
|
|
766
711
|
Upload a bundle to the server.
|
|
767
712
|
|
|
768
|
-
:param
|
|
713
|
+
:param content_guid: Content GUID
|
|
769
714
|
:param tarball: Bundle tarball file object
|
|
770
715
|
:param metadata: Optional metadata dictionary (e.g., git metadata)
|
|
771
|
-
:return:
|
|
716
|
+
:return: BundleMetadata for the uploaded bundle
|
|
772
717
|
"""
|
|
773
718
|
if metadata:
|
|
774
719
|
# Use multipart form upload when metadata is provided
|
|
@@ -966,7 +911,7 @@ class RSConnectClient(BearerTokenHTTPServer):
|
|
|
966
911
|
Creates or updates a git-backed content item in Posit Connect. Connect will clone
|
|
967
912
|
the repository and regularly poll it for updates.
|
|
968
913
|
|
|
969
|
-
:param app_id: Existing content
|
|
914
|
+
:param app_id: Existing content GUID to update, or None to create new content
|
|
970
915
|
:param name: Name for the content item (used if creating new)
|
|
971
916
|
:param repository: URL of the git repository (https:// only)
|
|
972
917
|
:param branch: Branch to deploy from
|
|
@@ -982,11 +927,10 @@ class RSConnectClient(BearerTokenHTTPServer):
|
|
|
982
927
|
app = self.content_create(name)
|
|
983
928
|
else:
|
|
984
929
|
try:
|
|
985
|
-
app = self.
|
|
930
|
+
app = self.content_get(app_id)
|
|
986
931
|
except RSConnectException as e:
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
) from e
|
|
932
|
+
# app_id may have come from either --guid or --app-id (publish_args folds them together).
|
|
933
|
+
raise RSConnectException(f"{e} Omit --guid/--app-id to create new content instead.") from e
|
|
990
934
|
|
|
991
935
|
app_guid = app["guid"]
|
|
992
936
|
|
|
@@ -1182,8 +1126,7 @@ class RSConnectClient(BearerTokenHTTPServer):
|
|
|
1182
1126
|
else:
|
|
1183
1127
|
# assume content exists. if it was deleted then Connect will raise an error
|
|
1184
1128
|
try:
|
|
1185
|
-
|
|
1186
|
-
app = self.get_content_by_id(app_id)
|
|
1129
|
+
app = self.content_get(app_id)
|
|
1187
1130
|
except RSConnectException as e:
|
|
1188
1131
|
raise RSConnectException(f"{e} Try setting the --new flag to overwrite the previous deployment.") from e
|
|
1189
1132
|
|
|
@@ -1459,6 +1402,7 @@ class RSConnectExecutor:
|
|
|
1459
1402
|
infer_target=(path is not None) if infer_target is None else infer_target,
|
|
1460
1403
|
)
|
|
1461
1404
|
self.setup_client(cookies)
|
|
1405
|
+
self.validate_explicit_app_id()
|
|
1462
1406
|
|
|
1463
1407
|
@classmethod
|
|
1464
1408
|
def fromConnectServer(
|
|
@@ -1515,6 +1459,22 @@ class RSConnectExecutor:
|
|
|
1515
1459
|
polling=polling,
|
|
1516
1460
|
)
|
|
1517
1461
|
|
|
1462
|
+
def validate_explicit_app_id(self) -> None:
|
|
1463
|
+
"""Check an explicit --guid or --app-id against the kind of server it targets."""
|
|
1464
|
+
if self.app_id is None:
|
|
1465
|
+
return
|
|
1466
|
+
if isinstance(self.remote_server, (RSConnectServer, SPCSConnectServer)):
|
|
1467
|
+
if not is_content_guid(str(self.app_id)):
|
|
1468
|
+
raise RSConnectException(
|
|
1469
|
+
"--app-id must be a content GUID; numeric content IDs are no longer supported. "
|
|
1470
|
+
"Find the GUID in the content's Info panel in Connect or with `rsconnect content search`."
|
|
1471
|
+
)
|
|
1472
|
+
elif self.ctx is not None and self.ctx.params.get("guid"):
|
|
1473
|
+
raise RSConnectException(
|
|
1474
|
+
"--guid applies only to Posit Connect. "
|
|
1475
|
+
"Use --app-id to target existing Connect Cloud or shinyapps.io content."
|
|
1476
|
+
)
|
|
1477
|
+
|
|
1518
1478
|
def output_overlap_header(self, previous: bool) -> bool:
|
|
1519
1479
|
if self.logger and not previous:
|
|
1520
1480
|
self.logger.warning(
|
|
@@ -2678,6 +2638,9 @@ for shinyapps.io. See command help for further details."
|
|
|
2678
2638
|
# validate_connect_cloud_server, so without this check a viewer role is
|
|
2679
2639
|
# caught only by the deploy -- after the source record has been removed.
|
|
2680
2640
|
if not service.can_publish_to(account):
|
|
2641
|
+
sso_error = service.sso_blocked_error(account)
|
|
2642
|
+
if sso_error:
|
|
2643
|
+
raise sso_error
|
|
2681
2644
|
raise RSConnectException(
|
|
2682
2645
|
'You have access to the Posit Connect Cloud account "%s" but do not have '
|
|
2683
2646
|
"permission to publish to it, so the next deploy from this directory would "
|
|
@@ -2739,12 +2702,33 @@ for shinyapps.io. See command help for further details."
|
|
|
2739
2702
|
self._draft_deploy_supported = server_supports_draft_deploy(server_version)
|
|
2740
2703
|
return self._draft_deploy_supported
|
|
2741
2704
|
|
|
2705
|
+
@property
|
|
2706
|
+
def is_rendered_content(self) -> bool:
|
|
2707
|
+
"""Whether the bundle is rendered or static content rather than a running app.
|
|
2708
|
+
|
|
2709
|
+
Connect renders a document when its bundle is deployed and renders it again
|
|
2710
|
+
when a draft is activated, so verifying a draft first would render it twice.
|
|
2711
|
+
Requesting the rendered output also can't catch anything the deploy task's
|
|
2712
|
+
render didn't already, and Connect reverts to the previous bundle if an
|
|
2713
|
+
activating render fails.
|
|
2714
|
+
"""
|
|
2715
|
+
app_mode = self.bundle_manifest().get("metadata", {}).get("appmode")
|
|
2716
|
+
return app_mode in _RENDERED_APP_MODES
|
|
2717
|
+
|
|
2718
|
+
def should_verify(self, no_verify: bool) -> bool:
|
|
2719
|
+
"""Whether to verify the deployed content.
|
|
2720
|
+
|
|
2721
|
+
Rendered content is never verified, even as a draft: requesting a draft's
|
|
2722
|
+
rendered output only serves what the deploy task already rendered.
|
|
2723
|
+
"""
|
|
2724
|
+
return not no_verify and not self.is_rendered_content
|
|
2725
|
+
|
|
2742
2726
|
def should_deploy_as_draft(self, draft: bool, no_verify: bool) -> bool:
|
|
2743
2727
|
"""Whether the bundle should be deployed without activating it.
|
|
2744
2728
|
|
|
2745
2729
|
An explicit ``--draft`` always deploys a draft. Otherwise we deploy a draft only
|
|
2746
2730
|
when we are going to verify it before activating, which requires server support.
|
|
2747
|
-
With ``--no-verify
|
|
2731
|
+
With ``--no-verify``, or for rendered content, we activate immediately.
|
|
2748
2732
|
"""
|
|
2749
2733
|
if draft:
|
|
2750
2734
|
if not isinstance(self.client, RSConnectClient):
|
|
@@ -2756,10 +2740,21 @@ for shinyapps.io. See command help for further details."
|
|
|
2756
2740
|
# would be the opposite of what the user asked for, so fail loudly.
|
|
2757
2741
|
raise RSConnectException("Deploying as a draft requires Posit Connect 2025.06.0 or later.")
|
|
2758
2742
|
return True
|
|
2759
|
-
if no_verify:
|
|
2743
|
+
if not self.should_verify(no_verify):
|
|
2760
2744
|
return False
|
|
2761
2745
|
return self.supports_verify_before_activate
|
|
2762
2746
|
|
|
2747
|
+
def verify_and_activate(self, draft: bool, no_verify: bool):
|
|
2748
|
+
"""Verify the deployed content, then activate it if it was deployed as a
|
|
2749
|
+
draft only so that it could be verified first."""
|
|
2750
|
+
if not self.should_verify(no_verify):
|
|
2751
|
+
return self
|
|
2752
|
+
self.verify_deployment()
|
|
2753
|
+
if not draft and self.deployed_info and self.deployed_info.get("draft_url"):
|
|
2754
|
+
# The draft bundle verified successfully, so activate it.
|
|
2755
|
+
self.activate_deployment().emit_task_log()
|
|
2756
|
+
return self
|
|
2757
|
+
|
|
2763
2758
|
@cls_logged("Verifying deployed content...")
|
|
2764
2759
|
def verify_deployment(self):
|
|
2765
2760
|
if isinstance(self.remote_server, (RSConnectServer, SPCSConnectServer)):
|
|
@@ -2847,10 +2842,10 @@ for shinyapps.io. See command help for further details."
|
|
|
2847
2842
|
elif app_id is not None:
|
|
2848
2843
|
# Don't read app metadata if app-id is specified. Instead, we need
|
|
2849
2844
|
# to get this from the remote.
|
|
2850
|
-
if isinstance(self.remote_server, RSConnectServer):
|
|
2845
|
+
if isinstance(self.remote_server, (RSConnectServer, SPCSConnectServer)):
|
|
2851
2846
|
try:
|
|
2852
2847
|
with RSConnectClient(self.remote_server) as client:
|
|
2853
|
-
content = client.
|
|
2848
|
+
content = client.content_get(app_id)
|
|
2854
2849
|
existing_app_mode = AppModes.get_by_ordinal(content["app_mode"], True)
|
|
2855
2850
|
except RSConnectException as e:
|
|
2856
2851
|
raise RSConnectException(
|
|
@@ -3319,6 +3314,8 @@ class ConnectCloudAccount(TypedDict):
|
|
|
3319
3314
|
id: str
|
|
3320
3315
|
name: str
|
|
3321
3316
|
permissions: NotRequired[list[str]]
|
|
3317
|
+
role: NotRequired[Optional[str]]
|
|
3318
|
+
sso_enabled: NotRequired[bool]
|
|
3322
3319
|
|
|
3323
3320
|
|
|
3324
3321
|
class ConnectCloudAccountSearchResults(TypedDict):
|
|
@@ -3420,7 +3417,7 @@ class ConnectCloudClient(BearerTokenHTTPServer):
|
|
|
3420
3417
|
def _attempt_token_refresh(self) -> bool:
|
|
3421
3418
|
"""Mint a new access token and apply it to this client.
|
|
3422
3419
|
|
|
3423
|
-
Uses the client credentials grant when
|
|
3420
|
+
Uses the client credentials grant when client credentials are
|
|
3424
3421
|
stored, and the refresh token otherwise. Returns whether a new token was
|
|
3425
3422
|
obtained.
|
|
3426
3423
|
|
|
@@ -3447,8 +3444,8 @@ class ConnectCloudClient(BearerTokenHTTPServer):
|
|
|
3447
3444
|
logger.warning("Posit Connect Cloud token refresh failed (%s)." % type(exc).__name__)
|
|
3448
3445
|
return False
|
|
3449
3446
|
raise RSConnectException(
|
|
3450
|
-
"The Posit Connect Cloud
|
|
3451
|
-
"rotated. Create
|
|
3447
|
+
"The Posit Connect Cloud client credentials were rejected — they have been revoked or "
|
|
3448
|
+
"rotated. Create new ones at %s/identity/credentials, then save them with `%s`."
|
|
3452
3449
|
% (server.urls().auth, self._add_command(service_account=True))
|
|
3453
3450
|
) from exc
|
|
3454
3451
|
except InvalidGrantError as exc:
|
|
@@ -3545,7 +3542,7 @@ class ConnectCloudClient(BearerTokenHTTPServer):
|
|
|
3545
3542
|
# A refresh persists the new tokens and nothing else. Every other field
|
|
3546
3543
|
# is taken from the saved entry alone — never from this run, which may
|
|
3547
3544
|
# be publishing to a different account on the same login, or carrying
|
|
3548
|
-
#
|
|
3545
|
+
# client credentials from the environment that must not be
|
|
3549
3546
|
# grafted onto an interactively created entry. `rsconnect add` is what
|
|
3550
3547
|
# changes those. This is how the R client's withTokenRefreshRetry()
|
|
3551
3548
|
# writes back too. ServerStore.set writes the file itself, and omits
|
|
@@ -3650,6 +3647,9 @@ class ConnectCloudClient(BearerTokenHTTPServer):
|
|
|
3650
3647
|
"skipping the publish permission check." % name
|
|
3651
3648
|
)
|
|
3652
3649
|
if not self._can_publish(account):
|
|
3650
|
+
sso_error = self._sso_blocked_error(self._server, account)
|
|
3651
|
+
if sso_error:
|
|
3652
|
+
raise sso_error
|
|
3653
3653
|
raise RSConnectException(
|
|
3654
3654
|
'You have access to the Posit Connect Cloud account "%s" but do not have '
|
|
3655
3655
|
"permission to publish to it. Ask an account administrator for the publisher role." % name
|
|
@@ -3678,6 +3678,29 @@ class ConnectCloudClient(BearerTokenHTTPServer):
|
|
|
3678
3678
|
return True
|
|
3679
3679
|
return _CONNECT_CLOUD_PUBLISH_PERMISSION in permissions
|
|
3680
3680
|
|
|
3681
|
+
@staticmethod
|
|
3682
|
+
def _sso_blocked_error(server: ConnectCloudServer, account: ConnectCloudAccount) -> Optional[RSConnectException]:
|
|
3683
|
+
"""The error for client credentials that SSO keeps from publishing to this account, if that is
|
|
3684
|
+
why the account cannot be published to.
|
|
3685
|
+
|
|
3686
|
+
The server does not grant content:create on SSO accounts to tokens without
|
|
3687
|
+
a recent SSO login, which client credentials never have.
|
|
3688
|
+
"""
|
|
3689
|
+
if not (
|
|
3690
|
+
server.client_id
|
|
3691
|
+
and server.client_secret
|
|
3692
|
+
and account.get("sso_enabled")
|
|
3693
|
+
and account.get("role") in ("owner", "admin", "publisher")
|
|
3694
|
+
):
|
|
3695
|
+
return None
|
|
3696
|
+
name = account.get("name")
|
|
3697
|
+
return RSConnectException(
|
|
3698
|
+
'The Posit Connect Cloud account "%s" requires single sign-on (SSO). '
|
|
3699
|
+
"Client credentials cannot publish to accounts that require SSO. Log in interactively "
|
|
3700
|
+
"with SSO instead: run `rsconnect add --connect-cloud -n <nickname> -A %s` without "
|
|
3701
|
+
"client credentials." % (name, name)
|
|
3702
|
+
)
|
|
3703
|
+
|
|
3681
3704
|
def get_content(self, content_id: str) -> ConnectCloudContent:
|
|
3682
3705
|
response = cast(Union[ConnectCloudContent, HTTPResponse], self.get(f"/contents/{content_id}"))
|
|
3683
3706
|
content = self._server.handle_bad_response(response)
|
|
@@ -4100,6 +4123,12 @@ class ConnectCloudService:
|
|
|
4100
4123
|
"""
|
|
4101
4124
|
return ConnectCloudClient._can_publish(account)
|
|
4102
4125
|
|
|
4126
|
+
def sso_blocked_error(self, account: ConnectCloudAccount) -> Optional[RSConnectException]:
|
|
4127
|
+
"""The error get_account_by_name raises when SSO keeps client credentials from
|
|
4128
|
+
publishing to this account, or None when that is not why publishing is refused.
|
|
4129
|
+
"""
|
|
4130
|
+
return ConnectCloudClient._sso_blocked_error(self._server, account)
|
|
4131
|
+
|
|
4103
4132
|
def content_url(self, content_id: str, account_id: Optional[str]) -> str:
|
|
4104
4133
|
"""Build the browsable URL for a content item.
|
|
4105
4134
|
|
|
@@ -4306,8 +4335,7 @@ def emit_task_log(
|
|
|
4306
4335
|
with RSConnectClient(connect_server) as client:
|
|
4307
4336
|
result = client.wait_for_task(task_id, log_callback, abort_func, timeout, poll_wait, raise_on_error)
|
|
4308
4337
|
result = connect_server.handle_bad_response(result)
|
|
4309
|
-
|
|
4310
|
-
content = client.get_content_by_id(app_id)
|
|
4338
|
+
content = client.content_get(app_id)
|
|
4311
4339
|
app_url = content["dashboard_url"]
|
|
4312
4340
|
return (app_url, *result)
|
|
4313
4341
|
|