rsconnect-python 1.30.0__py3-none-any.whl
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/__init__.py +13 -0
- rsconnect/actions.py +565 -0
- rsconnect/actions_content.py +508 -0
- rsconnect/actions_environment.py +160 -0
- rsconnect/actions_integration.py +118 -0
- rsconnect/api.py +2582 -0
- rsconnect/bundle.py +2481 -0
- rsconnect/certificates.py +39 -0
- rsconnect/environment.py +390 -0
- rsconnect/environment_node.py +115 -0
- rsconnect/environment_r.py +300 -0
- rsconnect/exception.py +15 -0
- rsconnect/git_metadata.py +180 -0
- rsconnect/http_support.py +595 -0
- rsconnect/json_web_token.py +178 -0
- rsconnect/log.py +253 -0
- rsconnect/main.py +5889 -0
- rsconnect/metadata.py +879 -0
- rsconnect/models.py +835 -0
- rsconnect/oauth.py +623 -0
- rsconnect/py.typed +0 -0
- rsconnect/pyproject.py +283 -0
- rsconnect/quickstart/__init__.py +16 -0
- rsconnect/quickstart/quickstart.py +486 -0
- rsconnect/quickstart/templates/__init__.py +16 -0
- rsconnect/quickstart/templates/api/README.md.tmpl +15 -0
- rsconnect/quickstart/templates/api/__connect__.py.tmpl +3 -0
- rsconnect/quickstart/templates/api/__init__.py.tmpl +1 -0
- rsconnect/quickstart/templates/api/__main__.py.tmpl +14 -0
- rsconnect/quickstart/templates/api/app.py.tmpl +11 -0
- rsconnect/quickstart/templates/api/pyproject.toml.tmpl +13 -0
- rsconnect/quickstart/templates/fastapi/README.md.tmpl +15 -0
- rsconnect/quickstart/templates/fastapi/__connect__.py.tmpl +3 -0
- rsconnect/quickstart/templates/fastapi/__init__.py.tmpl +1 -0
- rsconnect/quickstart/templates/fastapi/__main__.py.tmpl +16 -0
- rsconnect/quickstart/templates/fastapi/app.py.tmpl +11 -0
- rsconnect/quickstart/templates/fastapi/pyproject.toml.tmpl +14 -0
- rsconnect/quickstart/templates/notebook/README.md.tmpl +15 -0
- rsconnect/quickstart/templates/notebook/notebook.ipynb.tmpl +34 -0
- rsconnect/quickstart/templates/notebook/pyproject.toml.tmpl +13 -0
- rsconnect/quickstart/templates/quarto/README.md.tmpl +19 -0
- rsconnect/quickstart/templates/quarto/pyproject.toml.tmpl +11 -0
- rsconnect/quickstart/templates/quarto/report.qmd.tmpl +8 -0
- rsconnect/quickstart/templates/shiny/README.md.tmpl +15 -0
- rsconnect/quickstart/templates/shiny/app.py.tmpl +3 -0
- rsconnect/quickstart/templates/shiny/pyproject.toml.tmpl +13 -0
- rsconnect/quickstart/templates/streamlit/README.md.tmpl +15 -0
- rsconnect/quickstart/templates/streamlit/app.py.tmpl +3 -0
- rsconnect/quickstart/templates/streamlit/pyproject.toml.tmpl +13 -0
- rsconnect/quickstart/templates/voila/README.md.tmpl +15 -0
- rsconnect/quickstart/templates/voila/pyproject.toml.tmpl +14 -0
- rsconnect/shiny_express.py +136 -0
- rsconnect/snowflake.py +93 -0
- rsconnect/subprocesses/__init__.py +0 -0
- rsconnect/subprocesses/inspect_environment.py +362 -0
- rsconnect/timeouts.py +89 -0
- rsconnect/utils_package.py +261 -0
- rsconnect/validation.py +156 -0
- rsconnect/version_check.py +154 -0
- rsconnect_python-1.30.0.dist-info/METADATA +89 -0
- rsconnect_python-1.30.0.dist-info/RECORD +63 -0
- rsconnect_python-1.30.0.dist-info/WHEEL +4 -0
- rsconnect_python-1.30.0.dist-info/entry_points.txt +3 -0
rsconnect/__init__.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def _resolve_version() -> str:
|
|
5
|
+
for distribution in ("rsconnect_python", "rsconnect"):
|
|
6
|
+
try:
|
|
7
|
+
return version(distribution)
|
|
8
|
+
except PackageNotFoundError:
|
|
9
|
+
continue
|
|
10
|
+
return "NOTSET"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
VERSION = _resolve_version()
|
rsconnect/actions.py
ADDED
|
@@ -0,0 +1,565 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Public API for managing settings and deploying content.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import contextlib
|
|
8
|
+
import json
|
|
9
|
+
import logging
|
|
10
|
+
import re
|
|
11
|
+
import shutil
|
|
12
|
+
import subprocess
|
|
13
|
+
import sys
|
|
14
|
+
import traceback
|
|
15
|
+
import typing
|
|
16
|
+
from os.path import basename, exists, join, relpath
|
|
17
|
+
from typing import Optional, Sequence, cast
|
|
18
|
+
from warnings import warn
|
|
19
|
+
|
|
20
|
+
# Even though TypedDict is available in Python 3.8, because it's used with NotRequired,
|
|
21
|
+
# they should both come from the same typing module.
|
|
22
|
+
# https://peps.python.org/pep-0655/#usage-in-python-3-11
|
|
23
|
+
if sys.version_info >= (3, 11):
|
|
24
|
+
from typing import NotRequired, TypedDict
|
|
25
|
+
else:
|
|
26
|
+
from typing_extensions import NotRequired, TypedDict
|
|
27
|
+
|
|
28
|
+
from urllib.parse import urlparse
|
|
29
|
+
|
|
30
|
+
import click
|
|
31
|
+
|
|
32
|
+
from . import api
|
|
33
|
+
from .bundle import (
|
|
34
|
+
get_default_entrypoint,
|
|
35
|
+
make_api_bundle,
|
|
36
|
+
make_quarto_source_bundle,
|
|
37
|
+
read_manifest_file,
|
|
38
|
+
)
|
|
39
|
+
from .environment import Environment
|
|
40
|
+
from .environment_r import REnvironment
|
|
41
|
+
from .exception import RSConnectException
|
|
42
|
+
from .log import VERBOSE, logger
|
|
43
|
+
from .models import AppMode, AppModes
|
|
44
|
+
|
|
45
|
+
line_width = 45
|
|
46
|
+
_module_pattern = re.compile(r"^[A-Za-z0-9_]+:[A-Za-z0-9_]+$")
|
|
47
|
+
_name_sub_pattern = re.compile(r"[^A-Za-z0-9_ -]+")
|
|
48
|
+
_repeating_sub_pattern = re.compile(r"_+")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@contextlib.contextmanager
|
|
52
|
+
def cli_feedback(label: str, stderr: bool = False):
|
|
53
|
+
"""Context manager for OK/ERROR feedback from the CLI.
|
|
54
|
+
|
|
55
|
+
If the enclosed block succeeds, OK will be emitted.
|
|
56
|
+
If it fails, ERROR will be emitted.
|
|
57
|
+
Errors will also be classified as operational errors (prefixed with 'Error')
|
|
58
|
+
vs. internal errors (prefixed with 'Internal Error'). In verbose mode,
|
|
59
|
+
tracebacks will be emitted for internal errors.
|
|
60
|
+
"""
|
|
61
|
+
if label:
|
|
62
|
+
pad = line_width - len(label)
|
|
63
|
+
click.secho(label + "... " + " " * pad, nl=False, err=stderr)
|
|
64
|
+
logger.set_in_feedback(True)
|
|
65
|
+
|
|
66
|
+
def passed():
|
|
67
|
+
if label:
|
|
68
|
+
click.secho("[OK]", fg="green", err=stderr)
|
|
69
|
+
|
|
70
|
+
def failed(err: str):
|
|
71
|
+
if label:
|
|
72
|
+
click.secho("[ERROR]", fg="red", err=stderr)
|
|
73
|
+
click.secho(str(err), fg="bright_red", err=stderr)
|
|
74
|
+
sys.exit(1)
|
|
75
|
+
|
|
76
|
+
try:
|
|
77
|
+
yield
|
|
78
|
+
passed()
|
|
79
|
+
except RSConnectException as exc:
|
|
80
|
+
failed("Error: " + exc.message)
|
|
81
|
+
except Exception as exc:
|
|
82
|
+
traceback.print_exc()
|
|
83
|
+
failed("Internal error: " + str(exc))
|
|
84
|
+
finally:
|
|
85
|
+
logger.set_in_feedback(False)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def set_verbosity(verbose: int):
|
|
89
|
+
"""Set the verbosity level based on a passed flag
|
|
90
|
+
|
|
91
|
+
:param verbose: boolean specifying verbose or not
|
|
92
|
+
"""
|
|
93
|
+
if verbose == 0:
|
|
94
|
+
logger.setLevel(logging.INFO)
|
|
95
|
+
elif verbose == 1:
|
|
96
|
+
logger.setLevel(VERBOSE)
|
|
97
|
+
else:
|
|
98
|
+
logger.setLevel(logging.DEBUG)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _verify_server(connect_server: api.RSConnectServer):
|
|
102
|
+
"""
|
|
103
|
+
Test whether the server identified by the given full URL can be reached and is
|
|
104
|
+
running Connect.
|
|
105
|
+
|
|
106
|
+
:param connect_server: the Connect server information.
|
|
107
|
+
:return: the server settings from the Connect server.
|
|
108
|
+
"""
|
|
109
|
+
uri = urlparse(connect_server.url)
|
|
110
|
+
if not uri.netloc:
|
|
111
|
+
raise RSConnectException('Invalid server URL: "%s"' % connect_server.url)
|
|
112
|
+
return api.verify_server(connect_server)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _to_server_check_list(url: str) -> list[str]:
|
|
116
|
+
"""
|
|
117
|
+
Build a list of servers to check from the given one. If the specified server
|
|
118
|
+
appears not to have a scheme, then we'll provide https and http variants to test.
|
|
119
|
+
|
|
120
|
+
:param url: the server URL text to start with.
|
|
121
|
+
:return: a list of server strings to test.
|
|
122
|
+
"""
|
|
123
|
+
warn("This method has been moved and will be deprecated.", DeprecationWarning, stacklevel=2)
|
|
124
|
+
# urlparse will end up with an empty netloc in this case.
|
|
125
|
+
if "//" not in url:
|
|
126
|
+
items = ["https://%s", "http://%s"]
|
|
127
|
+
# urlparse would parse this correctly and end up with an empty scheme.
|
|
128
|
+
elif url.startswith("//"):
|
|
129
|
+
items = ["https:%s", "http:%s"]
|
|
130
|
+
else:
|
|
131
|
+
items = ["%s"]
|
|
132
|
+
|
|
133
|
+
return [item % url for item in items]
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def test_server(connect_server: api.RSConnectServer) -> tuple[api.RSConnectServer, object]:
|
|
137
|
+
"""
|
|
138
|
+
Test whether the given server can be reached and is running Connect. The server
|
|
139
|
+
may be provided with or without a scheme. If a scheme is omitted, the server will
|
|
140
|
+
be tested with both `https` and `http` until one of them works.
|
|
141
|
+
|
|
142
|
+
:param connect_server: the Connect server information.
|
|
143
|
+
:return: a second server object with any scheme expansions applied and the server
|
|
144
|
+
settings from the server.
|
|
145
|
+
"""
|
|
146
|
+
warn("This method has been moved and will be deprecated.", DeprecationWarning, stacklevel=2)
|
|
147
|
+
url = connect_server.url
|
|
148
|
+
key = connect_server.api_key
|
|
149
|
+
insecure = connect_server.insecure
|
|
150
|
+
ca_data = connect_server.ca_data
|
|
151
|
+
failures: list[str] = []
|
|
152
|
+
for test in _to_server_check_list(url):
|
|
153
|
+
try:
|
|
154
|
+
connect_server = api.RSConnectServer(test, key, insecure, ca_data)
|
|
155
|
+
result = _verify_server(connect_server)
|
|
156
|
+
return connect_server, result
|
|
157
|
+
except RSConnectException as exc:
|
|
158
|
+
failures.append(" %s - failed to verify as Posit Connect (%s)." % (test, str(exc)))
|
|
159
|
+
|
|
160
|
+
# In case the user may need https instead of http...
|
|
161
|
+
if len(failures) == 1 and url.startswith("http://"):
|
|
162
|
+
failures.append(' Do you need to use "https://%s"?' % url[7:])
|
|
163
|
+
|
|
164
|
+
# If we're here, nothing worked.
|
|
165
|
+
raise RSConnectException("\n".join(failures))
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def test_rstudio_server(server: api.PositServer):
|
|
169
|
+
with api.PositClient(server) as client:
|
|
170
|
+
try:
|
|
171
|
+
client.get_current_user()
|
|
172
|
+
except RSConnectException as exc:
|
|
173
|
+
raise RSConnectException("Failed to verify with {} ({}).".format(server.remote_name, exc))
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def test_spcs_server(server: api.SPCSConnectServer):
|
|
177
|
+
with api.RSConnectClient(server) as client:
|
|
178
|
+
try:
|
|
179
|
+
client.me()
|
|
180
|
+
except RSConnectException as exc:
|
|
181
|
+
raise RSConnectException("Failed to verify with {} ({}).".format(server.remote_name, exc))
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def test_api_key(connect_server: api.RSConnectServer) -> str:
|
|
185
|
+
"""
|
|
186
|
+
Test that an API Key may be used to authenticate with the given Posit Connect server.
|
|
187
|
+
If the API key verifies, we return the username of the associated user.
|
|
188
|
+
|
|
189
|
+
:param connect_server: the Connect server information.
|
|
190
|
+
:return: the username of the user to whom the API key belongs.
|
|
191
|
+
"""
|
|
192
|
+
warn("This method has been moved and will be deprecated.", DeprecationWarning, stacklevel=2)
|
|
193
|
+
return api.verify_api_key(connect_server)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def which_quarto(quarto: Optional[str] = None) -> str:
|
|
197
|
+
"""
|
|
198
|
+
Identify a valid Quarto executable. When a Quarto location is not provided
|
|
199
|
+
as input, an attempt is made to discover Quarto from the PATH and other
|
|
200
|
+
well-known locations.
|
|
201
|
+
"""
|
|
202
|
+
if quarto:
|
|
203
|
+
found = shutil.which(quarto)
|
|
204
|
+
if not found:
|
|
205
|
+
raise RSConnectException('The Quarto installation, "%s", does not exist or is not executable.' % quarto)
|
|
206
|
+
return found
|
|
207
|
+
|
|
208
|
+
# Fallback -- try to find Quarto when one was not supplied.
|
|
209
|
+
locations = [
|
|
210
|
+
# Discover using $PATH
|
|
211
|
+
"quarto",
|
|
212
|
+
# Location used by some installers, and often-added symbolic link.
|
|
213
|
+
"/usr/local/bin/quarto",
|
|
214
|
+
# Location used by some installers.
|
|
215
|
+
"/opt/quarto/bin/quarto",
|
|
216
|
+
# macOS RStudio IDE embedded installation
|
|
217
|
+
"/Applications/RStudio.app/Contents/MacOS/quarto/bin/quarto",
|
|
218
|
+
# macOS RStudio IDE electron embedded installation; location not final.
|
|
219
|
+
# see: https://github.com/rstudio/rstudio/issues/10674
|
|
220
|
+
]
|
|
221
|
+
|
|
222
|
+
for each in locations:
|
|
223
|
+
found = shutil.which(each)
|
|
224
|
+
if found:
|
|
225
|
+
return found
|
|
226
|
+
raise RSConnectException("Unable to locate a Quarto installation.")
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
class QuartoInspectResultQuarto(TypedDict):
|
|
230
|
+
version: str
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
class QuartoInspectResultConfigProject(TypedDict):
|
|
234
|
+
render: list[str]
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
class QuartoInspectResultConfig(TypedDict):
|
|
238
|
+
project: QuartoInspectResultConfigProject
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
class QuartoInspectResult(TypedDict):
|
|
242
|
+
quarto: QuartoInspectResultQuarto
|
|
243
|
+
engines: list[str]
|
|
244
|
+
config: NotRequired[QuartoInspectResultConfig]
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def quarto_inspect(
|
|
248
|
+
quarto: str,
|
|
249
|
+
target: str,
|
|
250
|
+
check_output: typing.Callable[..., bytes] = subprocess.check_output,
|
|
251
|
+
) -> QuartoInspectResult:
|
|
252
|
+
"""
|
|
253
|
+
Runs 'quarto inspect' against the target and returns its output as a
|
|
254
|
+
parsed JSON object.
|
|
255
|
+
|
|
256
|
+
The JSON result has different structure depending on whether or not the
|
|
257
|
+
target is a directory or a file.
|
|
258
|
+
"""
|
|
259
|
+
|
|
260
|
+
args = [quarto, "inspect", target]
|
|
261
|
+
try:
|
|
262
|
+
inspect_json = check_output(args, universal_newlines=True, stderr=subprocess.STDOUT)
|
|
263
|
+
except subprocess.CalledProcessError as e:
|
|
264
|
+
raise RSConnectException("Error inspecting target: %s" % e.output)
|
|
265
|
+
return cast(QuartoInspectResult, json.loads(inspect_json))
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def validate_quarto_engines(inspect: QuartoInspectResult):
|
|
269
|
+
"""
|
|
270
|
+
The markdown and jupyter engines are supported. Not knitr.
|
|
271
|
+
"""
|
|
272
|
+
supported = ["markdown", "jupyter"]
|
|
273
|
+
engines = inspect.get("engines", [])
|
|
274
|
+
unsupported = [engine for engine in engines if engine not in supported]
|
|
275
|
+
if unsupported:
|
|
276
|
+
raise RSConnectException("The following Quarto engine(s) are not supported: %s" % ", ".join(unsupported))
|
|
277
|
+
return engines
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
# ===============================================================================
|
|
281
|
+
# START: Compatibility entry point used by the vetiver-python package.
|
|
282
|
+
# vetiver's `deploy_connect` calls `deploy_python_fastapi` (below), which routes
|
|
283
|
+
# through `deploy_app` and the local `validate_*` helpers. This is a supported
|
|
284
|
+
# shim; keep these signatures stable. The `pyright: ignore` comments remain
|
|
285
|
+
# because the kwargs-forwarding style predates strict typing.
|
|
286
|
+
# ===============================================================================
|
|
287
|
+
def validate_extra_files(directory: str, extra_files: Sequence[str]):
|
|
288
|
+
"""
|
|
289
|
+
If the user specified a list of extra files, validate that they all exist and are
|
|
290
|
+
beneath the given directory and, if so, return a list of them made relative to that
|
|
291
|
+
directory.
|
|
292
|
+
|
|
293
|
+
:param directory: the directory that the extra files must be relative to.
|
|
294
|
+
:param extra_files: the list of extra files to qualify and validate.
|
|
295
|
+
:return: the extra files qualified by the directory.
|
|
296
|
+
"""
|
|
297
|
+
warn("This method has been moved and will be deprecated.", DeprecationWarning, stacklevel=2)
|
|
298
|
+
result: list[str] = []
|
|
299
|
+
if extra_files:
|
|
300
|
+
for extra in extra_files:
|
|
301
|
+
extra_file = relpath(extra, directory)
|
|
302
|
+
# It's an error if we have to leave the given dir to get to the extra
|
|
303
|
+
# file.
|
|
304
|
+
if extra_file.startswith("../"):
|
|
305
|
+
raise RSConnectException("%s must be under %s." % (extra_file, directory))
|
|
306
|
+
if not exists(join(directory, extra_file)):
|
|
307
|
+
raise RSConnectException("Could not find file %s under %s" % (extra, directory))
|
|
308
|
+
result.append(extra_file)
|
|
309
|
+
return result
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def validate_entry_point(entry_point: Optional[str], directory: str):
|
|
313
|
+
"""
|
|
314
|
+
Validates the entry point specified by the user, expanding as necessary. If the
|
|
315
|
+
user specifies nothing, a module of "app" is assumed. If the user specifies a
|
|
316
|
+
module only, the object is assumed to be the same name as the module.
|
|
317
|
+
|
|
318
|
+
:param entry_point: the entry point as specified by the user.
|
|
319
|
+
:return: the fully expanded and validated entry point and the module file name..
|
|
320
|
+
"""
|
|
321
|
+
warn("This method has been moved and will be deprecated.", DeprecationWarning, stacklevel=2)
|
|
322
|
+
if not entry_point:
|
|
323
|
+
entry_point = get_default_entrypoint(directory)
|
|
324
|
+
|
|
325
|
+
parts = entry_point.split(":")
|
|
326
|
+
|
|
327
|
+
if len(parts) > 2:
|
|
328
|
+
raise RSConnectException('Entry point is not in "module:object" format.')
|
|
329
|
+
|
|
330
|
+
return entry_point
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def deploy_app(
|
|
334
|
+
name: Optional[str] = None,
|
|
335
|
+
server: Optional[str] = None,
|
|
336
|
+
api_key: Optional[str] = None,
|
|
337
|
+
insecure: Optional[bool] = None,
|
|
338
|
+
cacert: Optional[typing.IO[str]] = None,
|
|
339
|
+
ca_data: Optional[str] = None,
|
|
340
|
+
entry_point: Optional[str] = None,
|
|
341
|
+
excludes: Optional[list[str]] = None,
|
|
342
|
+
new: bool = False,
|
|
343
|
+
app_id: Optional[str] = None,
|
|
344
|
+
title: Optional[str] = None,
|
|
345
|
+
python: Optional[str] = None,
|
|
346
|
+
force_generate: bool = False,
|
|
347
|
+
verbose: Optional[bool] = None,
|
|
348
|
+
directory: Optional[str] = None,
|
|
349
|
+
extra_files: Optional[list[str]] = None,
|
|
350
|
+
env_vars: Optional[dict[str, str]] = None,
|
|
351
|
+
image: Optional[str] = None,
|
|
352
|
+
env_management_py: Optional[bool] = None,
|
|
353
|
+
env_management_r: Optional[bool] = None,
|
|
354
|
+
account: Optional[str] = None,
|
|
355
|
+
token: Optional[str] = None,
|
|
356
|
+
secret: Optional[str] = None,
|
|
357
|
+
app_mode: Optional[AppMode] = None,
|
|
358
|
+
connect_server: Optional[api.TargetableServer] = None,
|
|
359
|
+
**kws: object,
|
|
360
|
+
):
|
|
361
|
+
kwargs = locals()
|
|
362
|
+
kwargs["entry_point"] = entry_point = validate_entry_point(entry_point, directory) # pyright: ignore
|
|
363
|
+
kwargs["extra_files"] = extra_files = validate_extra_files(directory, extra_files) # pyright: ignore
|
|
364
|
+
|
|
365
|
+
if isinstance(connect_server, api.RSConnectServer):
|
|
366
|
+
kwargs.update(
|
|
367
|
+
dict(
|
|
368
|
+
url=connect_server.url,
|
|
369
|
+
api_key=connect_server.api_key,
|
|
370
|
+
insecure=connect_server.insecure,
|
|
371
|
+
ca_data=connect_server.ca_data,
|
|
372
|
+
cookies=connect_server.cookie_jar,
|
|
373
|
+
)
|
|
374
|
+
)
|
|
375
|
+
elif isinstance(connect_server, api.ShinyappsServer):
|
|
376
|
+
kwargs.update(
|
|
377
|
+
dict(
|
|
378
|
+
url=connect_server.url,
|
|
379
|
+
account=connect_server.account_name,
|
|
380
|
+
token=connect_server.token,
|
|
381
|
+
secret=connect_server.secret,
|
|
382
|
+
)
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
environment = Environment.create_python_environment(
|
|
386
|
+
directory, # pyright: ignore
|
|
387
|
+
requirements_file="requirements.txt" if not force_generate else None,
|
|
388
|
+
python=python,
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
# At this point, kwargs has a lot of things, but we can need to prune it down to just the things that
|
|
392
|
+
# the RSConnectExecutor constructor knows about.
|
|
393
|
+
executor_params = [
|
|
394
|
+
"ctx",
|
|
395
|
+
"name",
|
|
396
|
+
"url",
|
|
397
|
+
"api_key",
|
|
398
|
+
"insecure",
|
|
399
|
+
"cacert",
|
|
400
|
+
"ca_data",
|
|
401
|
+
"cookies",
|
|
402
|
+
"account",
|
|
403
|
+
"token",
|
|
404
|
+
"secret",
|
|
405
|
+
"timeout",
|
|
406
|
+
"logger",
|
|
407
|
+
"path",
|
|
408
|
+
"server",
|
|
409
|
+
"exclude",
|
|
410
|
+
"new",
|
|
411
|
+
"app_id",
|
|
412
|
+
"title",
|
|
413
|
+
"visibility",
|
|
414
|
+
"disable_env_management",
|
|
415
|
+
"env_vars",
|
|
416
|
+
]
|
|
417
|
+
shared_keys = set(executor_params).intersection(kwargs.keys())
|
|
418
|
+
kwargs = {key: kwargs[key] for key in shared_keys}
|
|
419
|
+
|
|
420
|
+
ce = api.RSConnectExecutor(**kwargs)
|
|
421
|
+
(
|
|
422
|
+
ce.validate_server()
|
|
423
|
+
.validate_app_mode(app_mode=app_mode) # pyright: ignore
|
|
424
|
+
.make_bundle(
|
|
425
|
+
make_api_bundle,
|
|
426
|
+
directory, # pyright: ignore
|
|
427
|
+
entry_point,
|
|
428
|
+
app_mode, # pyright: ignore
|
|
429
|
+
environment,
|
|
430
|
+
extra_files,
|
|
431
|
+
excludes, # pyright: ignore
|
|
432
|
+
image=image,
|
|
433
|
+
env_management_py=env_management_py,
|
|
434
|
+
env_management_r=env_management_r,
|
|
435
|
+
)
|
|
436
|
+
.deploy_bundle()
|
|
437
|
+
.save_deployed_info()
|
|
438
|
+
.emit_task_log()
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
# ===============================================================================
|
|
443
|
+
# END compatibility entry point for the vetiver-python package
|
|
444
|
+
# ===============================================================================
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
def deploy_python_fastapi(
|
|
448
|
+
connect_server: api.TargetableServer,
|
|
449
|
+
directory: str,
|
|
450
|
+
extra_files: typing.List[str],
|
|
451
|
+
excludes: typing.List[str],
|
|
452
|
+
entry_point: str,
|
|
453
|
+
new: bool,
|
|
454
|
+
app_id: int,
|
|
455
|
+
title: str,
|
|
456
|
+
python: str,
|
|
457
|
+
conda_mode: bool,
|
|
458
|
+
force_generate: bool,
|
|
459
|
+
log_callback: typing.Callable[..., None],
|
|
460
|
+
image: Optional[str] = None,
|
|
461
|
+
env_management_py: Optional[bool] = None,
|
|
462
|
+
env_management_r: Optional[bool] = None,
|
|
463
|
+
):
|
|
464
|
+
"""
|
|
465
|
+
A function to deploy a Python ASGI API module to Posit Connect. Depending on the files involved
|
|
466
|
+
and network latency, this may take a bit of time.
|
|
467
|
+
:param connect_server: the Connect server information.
|
|
468
|
+
:param directory: the app directory to deploy.
|
|
469
|
+
:param extra_files: any extra files that should be included in the deploy.
|
|
470
|
+
:param excludes: a sequence of glob patterns that will exclude matched files.
|
|
471
|
+
:param entry_point: the module/executable object for the WSGi framework.
|
|
472
|
+
:param new: a flag to force this as a new deploy. Previous default = False.
|
|
473
|
+
:param app_id: the ID of an existing application to deploy new files for. Previous default = None.
|
|
474
|
+
:param title: an optional title for the deploy. If this is not provided, one will
|
|
475
|
+
be generated. Previous default = None.
|
|
476
|
+
:param python: the optional name of a Python executable. Previous default = None.
|
|
477
|
+
:param conda_mode: depricated parameter, included for compatibility. Ignored.
|
|
478
|
+
:param force_generate: force generating "requirements.txt" or "environment.yml",
|
|
479
|
+
even if it already exists. Previous default = False.
|
|
480
|
+
:param log_callback: the callback to use to write the log to. If this is None
|
|
481
|
+
(the default) the lines from the deployment log will be returned as a sequence.
|
|
482
|
+
If a log callback is provided, then None will be returned for the log lines part
|
|
483
|
+
of the return tuple. Previous default = None.
|
|
484
|
+
:param image: the optional docker image to be specified for off-host execution. Default = None.
|
|
485
|
+
:param env_management_py: False prevents Connect from managing the Python environment for this bundle.
|
|
486
|
+
The server administrator is responsible for installing packages in the runtime environment. Default = None.
|
|
487
|
+
:param env_management_r: False prevents Connect from managing the R environment for this bundle.
|
|
488
|
+
The server administrator is responsible for installing packages in the runtime environment. Default = None.
|
|
489
|
+
:return: the ultimate URL where the deployed app may be accessed and the sequence
|
|
490
|
+
of log lines. The log lines value will be None if a log callback was provided.
|
|
491
|
+
"""
|
|
492
|
+
return deploy_app(app_mode=AppModes.PYTHON_FASTAPI, **locals())
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
def create_quarto_deployment_bundle(
|
|
496
|
+
file_or_directory: str,
|
|
497
|
+
extra_files: Sequence[str],
|
|
498
|
+
excludes: Sequence[str],
|
|
499
|
+
app_mode: AppMode,
|
|
500
|
+
inspect: QuartoInspectResult,
|
|
501
|
+
environment: Optional[Environment],
|
|
502
|
+
image: Optional[str] = None,
|
|
503
|
+
env_management_py: Optional[bool] = None,
|
|
504
|
+
env_management_r: Optional[bool] = None,
|
|
505
|
+
r_environment: Optional[REnvironment] = None,
|
|
506
|
+
) -> typing.IO[bytes]:
|
|
507
|
+
"""
|
|
508
|
+
Create an in-memory bundle, ready to deploy.
|
|
509
|
+
|
|
510
|
+
:param file_or_directory: The Quarto document or the directory containing the Quarto project.
|
|
511
|
+
:param extra_files: a sequence of any extra files to include in the bundle.
|
|
512
|
+
:param excludes: a sequence of glob patterns that will exclude matched files.
|
|
513
|
+
:param entry_point: the module/executable object for the WSGi framework.
|
|
514
|
+
:param app_mode: the mode of the app being deployed.
|
|
515
|
+
:param environment: environmental information.
|
|
516
|
+
:param extra_files_need_validating: a flag indicating whether the list of extra
|
|
517
|
+
files should be validated or not. Part of validating includes qualifying each
|
|
518
|
+
with the specified directory. If you provide False here, make sure the names
|
|
519
|
+
are properly qualified first. Previous default = True.
|
|
520
|
+
:param image: the optional docker image to be specified for off-host execution. Default = None.
|
|
521
|
+
:param env_management_py: False prevents Connect from managing the Python environment for this bundle.
|
|
522
|
+
The server administrator is responsible for installing packages in the runtime environment. Default = None.
|
|
523
|
+
:param env_management_r: False prevents Connect from managing the R environment for this bundle.
|
|
524
|
+
The server administrator is responsible for installing packages in the runtime environment. Default = None.
|
|
525
|
+
:param r_environment: optional R dependencies detected from renv.lock to add to the manifest.
|
|
526
|
+
:return: the bundle.
|
|
527
|
+
"""
|
|
528
|
+
if app_mode is None:
|
|
529
|
+
app_mode = AppModes.STATIC_QUARTO
|
|
530
|
+
|
|
531
|
+
return make_quarto_source_bundle(
|
|
532
|
+
file_or_directory,
|
|
533
|
+
inspect,
|
|
534
|
+
app_mode,
|
|
535
|
+
environment,
|
|
536
|
+
extra_files,
|
|
537
|
+
excludes,
|
|
538
|
+
image,
|
|
539
|
+
env_management_py,
|
|
540
|
+
env_management_r,
|
|
541
|
+
r_environment,
|
|
542
|
+
)
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
def describe_manifest(file_name: str) -> tuple[str | None, str | None]:
|
|
546
|
+
"""
|
|
547
|
+
Determine the entry point and/or primary file from the given manifest file.
|
|
548
|
+
If no entry point is recorded in the manifest, then None will be returned for
|
|
549
|
+
that. The same is true for the primary document. None will be returned for
|
|
550
|
+
both if the file doesn't exist or doesn't look like a manifest file.
|
|
551
|
+
|
|
552
|
+
:param file_name: the name of the manifest file to read.
|
|
553
|
+
:return: the entry point and primary document from the manifest.
|
|
554
|
+
"""
|
|
555
|
+
warn("This method has been moved and will be deprecated.", DeprecationWarning, stacklevel=2)
|
|
556
|
+
if basename(file_name) == "manifest.json" and exists(file_name):
|
|
557
|
+
manifest, _ = read_manifest_file(file_name)
|
|
558
|
+
metadata = manifest.get("metadata")
|
|
559
|
+
if metadata:
|
|
560
|
+
# noinspection SpellCheckingInspection
|
|
561
|
+
return (
|
|
562
|
+
metadata.get("entrypoint"),
|
|
563
|
+
metadata.get("primary_rmd") or metadata.get("primary_html"),
|
|
564
|
+
)
|
|
565
|
+
return None, None
|