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/models.py
ADDED
|
@@ -0,0 +1,835 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Data models
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import fnmatch
|
|
8
|
+
import pathlib
|
|
9
|
+
import re
|
|
10
|
+
import sys
|
|
11
|
+
from typing import Callable, Literal, Optional, cast
|
|
12
|
+
|
|
13
|
+
import click
|
|
14
|
+
import semver
|
|
15
|
+
from click import ParamType
|
|
16
|
+
from click.types import StringParamType
|
|
17
|
+
|
|
18
|
+
# Even though TypedDict is available in Python 3.8, because it's used with NotRequired,
|
|
19
|
+
# they should both come from the same typing module.
|
|
20
|
+
# https://peps.python.org/pep-0655/#usage-in-python-3-11
|
|
21
|
+
if sys.version_info >= (3, 11):
|
|
22
|
+
from typing import TypedDict
|
|
23
|
+
else:
|
|
24
|
+
from typing_extensions import TypedDict
|
|
25
|
+
|
|
26
|
+
_version_search_pattern = r"(^[=><]{0,2})(.*)"
|
|
27
|
+
_content_guid_pattern = r"([^,]*),?(.*)"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class BuildStatus:
|
|
31
|
+
NEEDS_BUILD = "NEEDS_BUILD" # marked for build
|
|
32
|
+
RUNNING = "RUNNING" # running now
|
|
33
|
+
ABORTED = "ABORTED" # cancelled while running
|
|
34
|
+
COMPLETE = "COMPLETE" # completed successfully
|
|
35
|
+
ERROR = "ERROR" # completed with an error
|
|
36
|
+
|
|
37
|
+
_all = [NEEDS_BUILD, RUNNING, ABORTED, COMPLETE, ERROR]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class AppMode:
|
|
41
|
+
"""
|
|
42
|
+
Data class defining an "app mode" as understood by Posit
|
|
43
|
+
Connect
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
def __init__(
|
|
47
|
+
self,
|
|
48
|
+
ordinal: int,
|
|
49
|
+
name: AppModes.Modes,
|
|
50
|
+
text: str,
|
|
51
|
+
ext: Optional[str] = None,
|
|
52
|
+
):
|
|
53
|
+
self._ordinal = ordinal
|
|
54
|
+
self._name: AppModes.Modes = name
|
|
55
|
+
self._text = text
|
|
56
|
+
self._ext = ext
|
|
57
|
+
|
|
58
|
+
def ordinal(self):
|
|
59
|
+
return self._ordinal
|
|
60
|
+
|
|
61
|
+
def name(self) -> AppModes.Modes:
|
|
62
|
+
return self._name
|
|
63
|
+
|
|
64
|
+
def desc(self):
|
|
65
|
+
return self._text
|
|
66
|
+
|
|
67
|
+
def extension(self):
|
|
68
|
+
return self._ext
|
|
69
|
+
|
|
70
|
+
def cli_alias(self) -> Optional[str]:
|
|
71
|
+
"""Return the primary CLI alias for this mode, or ``None`` if absent.
|
|
72
|
+
|
|
73
|
+
"Primary" is the first key declared in :data:`AppModes._cli_aliases`
|
|
74
|
+
that maps to this mode; secondary aliases (e.g. ``flask`` for
|
|
75
|
+
``PYTHON_API``) are still resolvable via
|
|
76
|
+
:meth:`AppModes.get_by_cli_alias` but are not returned here.
|
|
77
|
+
"""
|
|
78
|
+
for alias, mode in AppModes._cli_aliases.items():
|
|
79
|
+
if mode is self:
|
|
80
|
+
return alias
|
|
81
|
+
return None
|
|
82
|
+
|
|
83
|
+
def __str__(self):
|
|
84
|
+
return self.name()
|
|
85
|
+
|
|
86
|
+
def __repr__(self):
|
|
87
|
+
return self.desc()
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class AppModes:
|
|
91
|
+
"""
|
|
92
|
+
Enumeration-like collection of known `AppMode`s with lookup
|
|
93
|
+
functions
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
UNKNOWN = AppMode(0, "unknown", "<unknown>")
|
|
97
|
+
SHINY = AppMode(1, "shiny", "Shiny App", ".R")
|
|
98
|
+
RMD = AppMode(3, "rmd-static", "R Markdown", ".Rmd")
|
|
99
|
+
SHINY_RMD = AppMode(2, "rmd-shiny", "Shiny App (Rmd)")
|
|
100
|
+
STATIC = AppMode(4, "static", "Static HTML", ".html")
|
|
101
|
+
PLUMBER = AppMode(5, "api", "API")
|
|
102
|
+
TENSORFLOW = AppMode(6, "tensorflow-saved-model", "TensorFlow Model")
|
|
103
|
+
JUPYTER_NOTEBOOK = AppMode(7, "jupyter-static", "Jupyter Notebook", ".ipynb")
|
|
104
|
+
PYTHON_API = AppMode(8, "python-api", "Python API")
|
|
105
|
+
DASH_APP = AppMode(9, "python-dash", "Dash Application")
|
|
106
|
+
STREAMLIT_APP = AppMode(10, "python-streamlit", "Streamlit Application")
|
|
107
|
+
BOKEH_APP = AppMode(11, "python-bokeh", "Bokeh Application")
|
|
108
|
+
PYTHON_FASTAPI = AppMode(12, "python-fastapi", "Python FastAPI")
|
|
109
|
+
SHINY_QUARTO = AppMode(13, "quarto-shiny", "Shiny Quarto Document")
|
|
110
|
+
STATIC_QUARTO = AppMode(14, "quarto-static", "Quarto Document", ".qmd")
|
|
111
|
+
PYTHON_SHINY = AppMode(15, "python-shiny", "Python Shiny Application")
|
|
112
|
+
JUPYTER_VOILA = AppMode(16, "jupyter-voila", "Jupyter Voila Application")
|
|
113
|
+
PYTHON_GRADIO = AppMode(17, "python-gradio", "Gradio Application")
|
|
114
|
+
PYTHON_PANEL = AppMode(18, "python-panel", "Panel Application")
|
|
115
|
+
NODE_JS = AppMode(20, "nodejs", "Node.js application")
|
|
116
|
+
|
|
117
|
+
_modes = [
|
|
118
|
+
UNKNOWN,
|
|
119
|
+
SHINY,
|
|
120
|
+
RMD,
|
|
121
|
+
SHINY_RMD,
|
|
122
|
+
STATIC,
|
|
123
|
+
PLUMBER,
|
|
124
|
+
TENSORFLOW,
|
|
125
|
+
JUPYTER_NOTEBOOK,
|
|
126
|
+
PYTHON_API,
|
|
127
|
+
DASH_APP,
|
|
128
|
+
STREAMLIT_APP,
|
|
129
|
+
BOKEH_APP,
|
|
130
|
+
PYTHON_FASTAPI,
|
|
131
|
+
SHINY_QUARTO,
|
|
132
|
+
STATIC_QUARTO,
|
|
133
|
+
PYTHON_SHINY,
|
|
134
|
+
JUPYTER_VOILA,
|
|
135
|
+
PYTHON_GRADIO,
|
|
136
|
+
PYTHON_PANEL,
|
|
137
|
+
NODE_JS,
|
|
138
|
+
]
|
|
139
|
+
|
|
140
|
+
Modes = Literal[
|
|
141
|
+
"unknown",
|
|
142
|
+
"shiny",
|
|
143
|
+
"rmd-static",
|
|
144
|
+
"rmd-shiny",
|
|
145
|
+
"static",
|
|
146
|
+
"api",
|
|
147
|
+
"tensorflow-saved-model",
|
|
148
|
+
"jupyter-static",
|
|
149
|
+
"python-api",
|
|
150
|
+
"python-dash",
|
|
151
|
+
"python-streamlit",
|
|
152
|
+
"python-bokeh",
|
|
153
|
+
"python-fastapi",
|
|
154
|
+
"quarto-shiny",
|
|
155
|
+
"quarto-static",
|
|
156
|
+
"python-shiny",
|
|
157
|
+
"jupyter-voila",
|
|
158
|
+
"python-gradio",
|
|
159
|
+
"python-panel",
|
|
160
|
+
"nodejs",
|
|
161
|
+
]
|
|
162
|
+
|
|
163
|
+
_cloud_to_connect_modes = {
|
|
164
|
+
"shiny": SHINY,
|
|
165
|
+
"rmarkdown_static": RMD,
|
|
166
|
+
"rmarkdown": SHINY_RMD,
|
|
167
|
+
"plumber": PLUMBER,
|
|
168
|
+
"flask": PYTHON_API,
|
|
169
|
+
"dash": DASH_APP,
|
|
170
|
+
"streamlit": STREAMLIT_APP,
|
|
171
|
+
"fastapi": PYTHON_FASTAPI,
|
|
172
|
+
"bokeh": BOKEH_APP,
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
# CLI alias vocabulary used by ``rsconnect deploy <alias>`` and
|
|
176
|
+
# ``rsconnect quickstart <alias>``. Many-to-one is allowed: ``api`` and
|
|
177
|
+
# ``flask`` both resolve to ``PYTHON_API``. NB: ``shiny`` here means
|
|
178
|
+
# ``PYTHON_SHINY``, which differs from cloud-name ``shiny`` (R Shiny in
|
|
179
|
+
# ``_cloud_to_connect_modes``); the two namespaces are independent.
|
|
180
|
+
_cli_aliases: dict[str, AppMode] = {
|
|
181
|
+
"api": PYTHON_API,
|
|
182
|
+
"flask": PYTHON_API,
|
|
183
|
+
"fastapi": PYTHON_FASTAPI,
|
|
184
|
+
"dash": DASH_APP,
|
|
185
|
+
"streamlit": STREAMLIT_APP,
|
|
186
|
+
"bokeh": BOKEH_APP,
|
|
187
|
+
"shiny": PYTHON_SHINY,
|
|
188
|
+
"gradio": PYTHON_GRADIO,
|
|
189
|
+
"panel": PYTHON_PANEL,
|
|
190
|
+
"notebook": JUPYTER_NOTEBOOK,
|
|
191
|
+
"voila": JUPYTER_VOILA,
|
|
192
|
+
"quarto": STATIC_QUARTO,
|
|
193
|
+
"quarto-shiny": SHINY_QUARTO,
|
|
194
|
+
"tensorflow": TENSORFLOW,
|
|
195
|
+
"html": STATIC,
|
|
196
|
+
"nodejs": NODE_JS,
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
@classmethod
|
|
200
|
+
def get_by_ordinal(cls, ordinal: int, return_unknown: bool = False) -> AppMode:
|
|
201
|
+
"""Get an AppMode by its associated ordinal (integer)"""
|
|
202
|
+
return cls._find_by(
|
|
203
|
+
lambda mode: mode.ordinal() == ordinal,
|
|
204
|
+
"with ordinal %s" % ordinal,
|
|
205
|
+
return_unknown,
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
# Aliases for app mode names that have been renamed in Connect. Local
|
|
209
|
+
# AppStore metadata may still contain the old name from a prior deploy;
|
|
210
|
+
# this lets resolve() succeed instead of raising on re-deploys.
|
|
211
|
+
_name_aliases = {
|
|
212
|
+
"nodejs-api": "nodejs",
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
@classmethod
|
|
216
|
+
def get_by_name(cls, name: str, return_unknown: bool = False) -> AppMode:
|
|
217
|
+
"""Get an AppMode by name"""
|
|
218
|
+
name = cls._name_aliases.get(name, name)
|
|
219
|
+
return cls._find_by(lambda mode: mode.name() == name, "named %s" % name, return_unknown)
|
|
220
|
+
|
|
221
|
+
@classmethod
|
|
222
|
+
def get_by_extension(cls, extension: Optional[str], return_unknown: bool = False) -> AppMode:
|
|
223
|
+
"""Get an app mode by its associated extension"""
|
|
224
|
+
# We can't allow a lookup by None since some modes have that for an extension.
|
|
225
|
+
if extension is None:
|
|
226
|
+
if return_unknown:
|
|
227
|
+
return cls.UNKNOWN
|
|
228
|
+
raise ValueError("No app mode with extension %s" % extension)
|
|
229
|
+
|
|
230
|
+
return cls._find_by(
|
|
231
|
+
lambda mode: mode.extension() == extension,
|
|
232
|
+
"with extension: %s" % extension,
|
|
233
|
+
return_unknown,
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
@classmethod
|
|
237
|
+
def get_by_cloud_name(cls, name: str) -> AppMode:
|
|
238
|
+
return cls._cloud_to_connect_modes.get(name, cls.UNKNOWN)
|
|
239
|
+
|
|
240
|
+
@classmethod
|
|
241
|
+
def get_by_cli_alias(cls, alias: str) -> AppMode:
|
|
242
|
+
"""Resolve a CLI alias to its canonical :class:`AppMode`.
|
|
243
|
+
|
|
244
|
+
Returns :attr:`UNKNOWN` for aliases not in :data:`_cli_aliases`.
|
|
245
|
+
Subcommands that accept only a subset of modes (e.g. ``rsconnect
|
|
246
|
+
quickstart``) check membership in their own registry after resolving
|
|
247
|
+
the alias here.
|
|
248
|
+
"""
|
|
249
|
+
return cls._cli_aliases.get(alias, cls.UNKNOWN)
|
|
250
|
+
|
|
251
|
+
@classmethod
|
|
252
|
+
def cli_aliases(cls) -> tuple[str, ...]:
|
|
253
|
+
"""All CLI aliases declared in :data:`_cli_aliases`, in declaration order."""
|
|
254
|
+
return tuple(cls._cli_aliases.keys())
|
|
255
|
+
|
|
256
|
+
@classmethod
|
|
257
|
+
def _find_by(cls, predicate: Callable[[AppMode], bool], message: str, return_unknown: bool) -> AppMode:
|
|
258
|
+
for mode in cls._modes:
|
|
259
|
+
if predicate(mode):
|
|
260
|
+
return mode
|
|
261
|
+
if return_unknown:
|
|
262
|
+
return cls.UNKNOWN
|
|
263
|
+
raise ValueError("No app mode %s" % message)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
class GlobMatcher(object):
|
|
267
|
+
"""
|
|
268
|
+
A simplified means of matching a path against a glob pattern. The key
|
|
269
|
+
limitation is that we support at most one occurrence of the `**` pattern.
|
|
270
|
+
"""
|
|
271
|
+
|
|
272
|
+
def __init__(self, pattern: str):
|
|
273
|
+
pattern = pathlib.PurePath(pattern).as_posix()
|
|
274
|
+
if pattern.endswith("/**/*"):
|
|
275
|
+
# Note: the index used here makes sure the pattern has a trailing
|
|
276
|
+
# slash. We want that.
|
|
277
|
+
self._pattern = pattern[:-4]
|
|
278
|
+
self.matches = self._match_with_starts_with
|
|
279
|
+
else:
|
|
280
|
+
self._pattern_parts: list[str | re.Pattern[str]]
|
|
281
|
+
self._wildcard_index: int | None
|
|
282
|
+
self._pattern_parts, self._wildcard_index = self._to_parts_list(pattern)
|
|
283
|
+
self.matches = self._match_with_list_parts
|
|
284
|
+
|
|
285
|
+
@staticmethod
|
|
286
|
+
def _to_parts_list(pattern: str) -> tuple[list[str | re.Pattern[str]], int | None]:
|
|
287
|
+
"""
|
|
288
|
+
Converts a glob expression into a list, with an entry for each directory
|
|
289
|
+
level. Each entry will be either a string, in which case an equality
|
|
290
|
+
check for that directory entry, or a regular expression, in which case
|
|
291
|
+
matching will be used. The string, '**', is special but we don't alter
|
|
292
|
+
it here. We do return its index.
|
|
293
|
+
|
|
294
|
+
:param pattern: the glob pattern to pull apart.
|
|
295
|
+
:return: a list of pattern pieces and the index of the special '**' pattern.
|
|
296
|
+
The index will be None if `**` is never found.
|
|
297
|
+
"""
|
|
298
|
+
# Incoming pattern is ALWAYS a Posix-style path.
|
|
299
|
+
parts_start = pattern.split("/")
|
|
300
|
+
parts_result: list[str | re.Pattern[str]] = []
|
|
301
|
+
depth_wildcard_index = None
|
|
302
|
+
for index, name in enumerate(parts_start):
|
|
303
|
+
value = name
|
|
304
|
+
if name == "**":
|
|
305
|
+
if depth_wildcard_index is not None:
|
|
306
|
+
raise ValueError('Only one occurrence of the "**" pattern is allowed.')
|
|
307
|
+
depth_wildcard_index = index
|
|
308
|
+
elif any(ch in name for ch in "*?["):
|
|
309
|
+
value = re.compile(r"\A" + fnmatch.translate(name))
|
|
310
|
+
parts_result.append(value)
|
|
311
|
+
|
|
312
|
+
return parts_result, depth_wildcard_index
|
|
313
|
+
|
|
314
|
+
def _match_with_starts_with(self, path: str | pathlib.PurePath):
|
|
315
|
+
path = pathlib.PurePath(path).as_posix()
|
|
316
|
+
return path.startswith(self._pattern)
|
|
317
|
+
|
|
318
|
+
def _match_with_list_parts(self, path: str | pathlib.PurePath):
|
|
319
|
+
path = pathlib.PurePath(path).as_posix()
|
|
320
|
+
parts = path.split("/")
|
|
321
|
+
|
|
322
|
+
def items_match(i1: int, i2: int):
|
|
323
|
+
if i2 >= len(parts):
|
|
324
|
+
return False
|
|
325
|
+
part1 = self._pattern_parts[i1]
|
|
326
|
+
if isinstance(part1, str):
|
|
327
|
+
return self._pattern_parts[i1] == parts[i2]
|
|
328
|
+
return part1.match(parts[i2]) is not None
|
|
329
|
+
|
|
330
|
+
wildcard_index = len(self._pattern_parts) if self._wildcard_index is None else self._wildcard_index
|
|
331
|
+
|
|
332
|
+
# Top-down...
|
|
333
|
+
for index in range(wildcard_index):
|
|
334
|
+
if not items_match(index, index):
|
|
335
|
+
return False
|
|
336
|
+
|
|
337
|
+
if self._wildcard_index is None:
|
|
338
|
+
return len(self._pattern_parts) == len(parts)
|
|
339
|
+
|
|
340
|
+
# Now, bottom-up...
|
|
341
|
+
pattern_index = len(self._pattern_parts) - 1
|
|
342
|
+
part_index = len(parts) - 1
|
|
343
|
+
|
|
344
|
+
while pattern_index > wildcard_index and part_index >= 0:
|
|
345
|
+
if not items_match(pattern_index, part_index):
|
|
346
|
+
return False
|
|
347
|
+
pattern_index = pattern_index - 1
|
|
348
|
+
part_index = part_index - 1
|
|
349
|
+
|
|
350
|
+
return pattern_index == wildcard_index
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
class GlobSet(object):
|
|
354
|
+
"""
|
|
355
|
+
Matches against a set of `GlobMatcher` patterns
|
|
356
|
+
"""
|
|
357
|
+
|
|
358
|
+
def __init__(self, patterns: list[str]):
|
|
359
|
+
self._matchers = [GlobMatcher(pattern) for pattern in patterns]
|
|
360
|
+
|
|
361
|
+
def matches(self, path: str):
|
|
362
|
+
"""
|
|
363
|
+
Determines whether the given path is matched by any of our glob
|
|
364
|
+
expressions.
|
|
365
|
+
|
|
366
|
+
:param path: the path to test.
|
|
367
|
+
:return: True, if the given path matches any of our glob patterns.
|
|
368
|
+
"""
|
|
369
|
+
return any(matcher.matches(path) for matcher in self._matchers)
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
# Strip quotes from string arguments that might be passed in by jq
|
|
373
|
+
# without the -r flag
|
|
374
|
+
class StrippedStringParamType(StringParamType):
|
|
375
|
+
name = "StrippedString"
|
|
376
|
+
|
|
377
|
+
def convert(self, value: str, param: Optional[click.Parameter], ctx: Optional[click.Context]) -> str:
|
|
378
|
+
value = super(StrippedStringParamType, self).convert(value, param, ctx)
|
|
379
|
+
return value.strip("\"'")
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
class ContentGuidWithBundle(object):
|
|
383
|
+
def __init__(self, guid: str, bundle_id: Optional[str] = None):
|
|
384
|
+
self.guid = guid
|
|
385
|
+
self.bundle_id = bundle_id
|
|
386
|
+
|
|
387
|
+
def __repr__(self):
|
|
388
|
+
if self.bundle_id:
|
|
389
|
+
return "%s,%s" % (self.guid, self.bundle_id)
|
|
390
|
+
return self.guid
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
class ContentGuidWithBundleParamType(StrippedStringParamType):
|
|
394
|
+
name = "ContentGuidWithBundle"
|
|
395
|
+
|
|
396
|
+
def convert( # pyright: ignore[reportIncompatibleMethodOverride]
|
|
397
|
+
self,
|
|
398
|
+
value: str | ContentGuidWithBundle,
|
|
399
|
+
param: Optional[click.Parameter],
|
|
400
|
+
ctx: Optional[click.Context],
|
|
401
|
+
):
|
|
402
|
+
if isinstance(value, ContentGuidWithBundle):
|
|
403
|
+
return value
|
|
404
|
+
if isinstance(value, str):
|
|
405
|
+
value = super(ContentGuidWithBundleParamType, self).convert(value, param, ctx)
|
|
406
|
+
m = re.match(_content_guid_pattern, value)
|
|
407
|
+
if m is not None:
|
|
408
|
+
guid_with_bundle = ContentGuidWithBundle(m.group(1))
|
|
409
|
+
if len(m.groups()) == 2 and len(m.group(2)) > 0:
|
|
410
|
+
try:
|
|
411
|
+
int(m.group(2))
|
|
412
|
+
except ValueError:
|
|
413
|
+
self.fail("Failed to parse bundle_id. Expected Int, but found: %s" % m.group(2))
|
|
414
|
+
guid_with_bundle.bundle_id = m.group(2)
|
|
415
|
+
return guid_with_bundle
|
|
416
|
+
self.fail("Failed to parse content guid arg %s" % value)
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
AppRole = Literal["owner", "editor", "viewer", "none"]
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
# Also known as AppRecord in Connect.
|
|
423
|
+
class ContentItemV0(TypedDict):
|
|
424
|
+
id: int
|
|
425
|
+
guid: str
|
|
426
|
+
access_type: Literal["all", "logged_in", "acl"]
|
|
427
|
+
connection_timeout: int | None
|
|
428
|
+
read_timeout: int | None
|
|
429
|
+
init_timeout: int | None
|
|
430
|
+
idle_timeout: int | None
|
|
431
|
+
max_processes: int | None
|
|
432
|
+
min_processes: int | None
|
|
433
|
+
max_conns_per_process: int | None
|
|
434
|
+
load_factor: float | None
|
|
435
|
+
memory_request: float | None
|
|
436
|
+
memory_limit: int | None
|
|
437
|
+
cpu_request: float | None
|
|
438
|
+
cpu_limit: int | None
|
|
439
|
+
amd_gpu_limit: int | None
|
|
440
|
+
nvidia_gpu_limit: int | None
|
|
441
|
+
url: str
|
|
442
|
+
vanity_url: bool
|
|
443
|
+
name: str
|
|
444
|
+
title: str | None
|
|
445
|
+
bundle_id: int | None
|
|
446
|
+
app_mode: int
|
|
447
|
+
content_category: str
|
|
448
|
+
has_parameters: bool
|
|
449
|
+
created_time: str
|
|
450
|
+
last_deployed_time: str
|
|
451
|
+
build_status: int
|
|
452
|
+
cluster_name: str | None
|
|
453
|
+
image_name: str | None
|
|
454
|
+
default_image_name: str | None
|
|
455
|
+
service_account_name: str | None
|
|
456
|
+
r_version: str | None
|
|
457
|
+
py_version: str | None
|
|
458
|
+
quarto_version: str | None
|
|
459
|
+
r_environment_management: bool | None
|
|
460
|
+
default_r_environment_management: bool | None
|
|
461
|
+
py_environment_management: bool | None
|
|
462
|
+
default_py_environment_management: bool | None
|
|
463
|
+
run_as: str | None
|
|
464
|
+
run_as_current_user: bool
|
|
465
|
+
description: str
|
|
466
|
+
EnvironmentJson: str | None
|
|
467
|
+
app_role: AppRole
|
|
468
|
+
owner_first_name: str
|
|
469
|
+
owner_last_name: str
|
|
470
|
+
owner_username: str
|
|
471
|
+
owner_guid: str
|
|
472
|
+
owner_email: str
|
|
473
|
+
owner_locked: bool
|
|
474
|
+
is_scheduled: bool
|
|
475
|
+
# Not sure how the following 4 fields are structured, so just use object for now.
|
|
476
|
+
git: object | None
|
|
477
|
+
users: object | None
|
|
478
|
+
groups: object | None
|
|
479
|
+
vanities: object | None
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
# Also known as V1 ContentOutputDTO in Connect (note: this is not V1 experimental).
|
|
483
|
+
class ContentItemV1(TypedDict):
|
|
484
|
+
guid: str
|
|
485
|
+
name: str
|
|
486
|
+
title: str | None
|
|
487
|
+
description: str
|
|
488
|
+
access_type: Literal["all", "logged_in", "acl"]
|
|
489
|
+
connection_timeout: int | None
|
|
490
|
+
read_timeout: int | None
|
|
491
|
+
init_timeout: int | None
|
|
492
|
+
idle_timeout: int | None
|
|
493
|
+
max_processes: int | None
|
|
494
|
+
min_processes: int | None
|
|
495
|
+
max_conns_per_process: int | None
|
|
496
|
+
load_factor: float | None
|
|
497
|
+
memory_request: float | None
|
|
498
|
+
memory_limit: int | None
|
|
499
|
+
cpu_request: float | None
|
|
500
|
+
cpu_limit: float | None
|
|
501
|
+
amd_gpu_limit: int | None
|
|
502
|
+
nvidia_gpu_limit: int | None
|
|
503
|
+
service_account_name: str | None
|
|
504
|
+
default_image_name: str | None
|
|
505
|
+
created_time: str
|
|
506
|
+
last_deployed_time: str
|
|
507
|
+
bundle_id: str | None
|
|
508
|
+
app_mode: AppModes.Modes
|
|
509
|
+
content_category: str
|
|
510
|
+
parameterized: bool
|
|
511
|
+
cluster_name: str | None
|
|
512
|
+
image_name: str | None
|
|
513
|
+
r_version: str | None
|
|
514
|
+
py_version: str | None
|
|
515
|
+
quarto_version: str | None
|
|
516
|
+
r_environment_management: bool | None
|
|
517
|
+
default_r_environment_management: bool | None
|
|
518
|
+
py_environment_management: bool | None
|
|
519
|
+
default_py_environment_management: bool | None
|
|
520
|
+
run_as: str | None
|
|
521
|
+
run_as_current_user: bool
|
|
522
|
+
owner_guid: str
|
|
523
|
+
content_url: str
|
|
524
|
+
dashboard_url: str
|
|
525
|
+
app_role: AppRole
|
|
526
|
+
id: str
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
VersionProgramName = Literal["r_version", "py_version", "quarto_version"]
|
|
530
|
+
ComparisonOperator = Literal[">", "<", ">=", "<=", "=", "=="]
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
class VersionSearchFilter(object):
|
|
534
|
+
def __init__(
|
|
535
|
+
self,
|
|
536
|
+
name: VersionProgramName,
|
|
537
|
+
comp: ComparisonOperator,
|
|
538
|
+
vers: str,
|
|
539
|
+
):
|
|
540
|
+
self.name = name
|
|
541
|
+
self.comp = comp
|
|
542
|
+
self.vers = vers
|
|
543
|
+
|
|
544
|
+
def __repr__(self):
|
|
545
|
+
return "%s %s %s" % (self.name, self.comp, self.vers)
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
class VersionSearchFilterParamType(ParamType):
|
|
549
|
+
name = "VersionSearchFilter"
|
|
550
|
+
|
|
551
|
+
def __init__(self, key: VersionProgramName):
|
|
552
|
+
"""
|
|
553
|
+
:param key: key refers to the left side of the version comparison.
|
|
554
|
+
In this case any interpreter in a content result, one of [py_version, r_version, quarto_version]
|
|
555
|
+
"""
|
|
556
|
+
self.key: VersionProgramName = key
|
|
557
|
+
|
|
558
|
+
def convert(
|
|
559
|
+
self,
|
|
560
|
+
value: str | VersionSearchFilter,
|
|
561
|
+
param: Optional[click.Parameter],
|
|
562
|
+
ctx: Optional[click.Context],
|
|
563
|
+
):
|
|
564
|
+
if isinstance(value, VersionSearchFilter):
|
|
565
|
+
return value
|
|
566
|
+
|
|
567
|
+
if isinstance(value, str):
|
|
568
|
+
m = re.match(_version_search_pattern, value)
|
|
569
|
+
if m is not None and len(m.groups()) == 2:
|
|
570
|
+
version_search = VersionSearchFilter(
|
|
571
|
+
name=self.key,
|
|
572
|
+
comp=cast(ComparisonOperator, m.group(1)),
|
|
573
|
+
vers=m.group(2),
|
|
574
|
+
)
|
|
575
|
+
|
|
576
|
+
# default to == if no comparator was provided
|
|
577
|
+
if not version_search.comp:
|
|
578
|
+
version_search.comp = "=="
|
|
579
|
+
|
|
580
|
+
if version_search.comp not in [">", "<", ">=", "<=", "=", "=="]:
|
|
581
|
+
self.fail("Failed to parse verison filter: %s is not a valid comparitor" % version_search.comp)
|
|
582
|
+
|
|
583
|
+
try:
|
|
584
|
+
semver.parse(version_search.vers) # pyright: ignore[reportUnknownMemberType]
|
|
585
|
+
except ValueError:
|
|
586
|
+
self.fail("Failed to parse version info: %s" % version_search.vers)
|
|
587
|
+
return version_search
|
|
588
|
+
|
|
589
|
+
self.fail("Failed to parse version filter %s" % value)
|
|
590
|
+
|
|
591
|
+
|
|
592
|
+
class TaskStatusResult(TypedDict):
|
|
593
|
+
type: str
|
|
594
|
+
data: object # Don't know the structure of this type yet
|
|
595
|
+
|
|
596
|
+
|
|
597
|
+
# https://docs.posit.co/connect/api/#get-/v1/tasks/-id-
|
|
598
|
+
class TaskStatusV1(TypedDict):
|
|
599
|
+
id: str
|
|
600
|
+
output: list[str]
|
|
601
|
+
finished: bool
|
|
602
|
+
code: int
|
|
603
|
+
error: str
|
|
604
|
+
last: int
|
|
605
|
+
result: TaskStatusResult | None
|
|
606
|
+
|
|
607
|
+
# redundant fields for compatibility with rsconnect-python.
|
|
608
|
+
last_status: int
|
|
609
|
+
status: list[str]
|
|
610
|
+
|
|
611
|
+
|
|
612
|
+
class BootstrapOutputDTO(TypedDict):
|
|
613
|
+
api_key: str
|
|
614
|
+
|
|
615
|
+
|
|
616
|
+
# This not the complete specification of the server settings data structure, but it is
|
|
617
|
+
# sufficient for the purposes of this package.
|
|
618
|
+
class ServerSettings(TypedDict):
|
|
619
|
+
hostname: str
|
|
620
|
+
version: str
|
|
621
|
+
|
|
622
|
+
|
|
623
|
+
class PyInfo(TypedDict):
|
|
624
|
+
installations: list[PyInstallation]
|
|
625
|
+
api_enabled: bool
|
|
626
|
+
|
|
627
|
+
|
|
628
|
+
class PyInstallation(TypedDict):
|
|
629
|
+
version: str
|
|
630
|
+
cluster_name: str
|
|
631
|
+
image_name: str
|
|
632
|
+
|
|
633
|
+
|
|
634
|
+
class BuildOutputDTO(TypedDict):
|
|
635
|
+
task_id: str
|
|
636
|
+
|
|
637
|
+
|
|
638
|
+
class BundleMetadata(TypedDict):
|
|
639
|
+
id: str
|
|
640
|
+
|
|
641
|
+
|
|
642
|
+
class ListEntryOutputDTO(TypedDict):
|
|
643
|
+
language: str
|
|
644
|
+
version: str
|
|
645
|
+
image_name: str
|
|
646
|
+
|
|
647
|
+
|
|
648
|
+
class DeleteInputDTO(TypedDict):
|
|
649
|
+
language: str
|
|
650
|
+
version: str
|
|
651
|
+
image_name: str
|
|
652
|
+
dry_run: bool
|
|
653
|
+
|
|
654
|
+
|
|
655
|
+
class DeleteOutputDTO(TypedDict):
|
|
656
|
+
language: str
|
|
657
|
+
version: str
|
|
658
|
+
iamge_name: str
|
|
659
|
+
task_id: str | None
|
|
660
|
+
|
|
661
|
+
|
|
662
|
+
class UserRecord(TypedDict):
|
|
663
|
+
email: str
|
|
664
|
+
username: str
|
|
665
|
+
first_name: str
|
|
666
|
+
last_name: str
|
|
667
|
+
password: str
|
|
668
|
+
user_role: str
|
|
669
|
+
created_time: str
|
|
670
|
+
updated_time: str
|
|
671
|
+
active_time: str | None
|
|
672
|
+
confirmed: bool
|
|
673
|
+
locked: bool
|
|
674
|
+
guid: str
|
|
675
|
+
preferences: dict[str, object]
|
|
676
|
+
privileges: list[str]
|
|
677
|
+
|
|
678
|
+
|
|
679
|
+
class EnvironmentInstallation(TypedDict):
|
|
680
|
+
version: str
|
|
681
|
+
path: str
|
|
682
|
+
|
|
683
|
+
|
|
684
|
+
class EnvironmentInstallations(TypedDict):
|
|
685
|
+
installations: list[EnvironmentInstallation]
|
|
686
|
+
|
|
687
|
+
|
|
688
|
+
class EnvironmentVolumeSource(TypedDict, total=False):
|
|
689
|
+
volume_type: str
|
|
690
|
+
nfs_host: str | None
|
|
691
|
+
nfs_export_path: str | None
|
|
692
|
+
pvc_name: str | None
|
|
693
|
+
|
|
694
|
+
|
|
695
|
+
class EnvironmentVolumeTarget(TypedDict):
|
|
696
|
+
path: str
|
|
697
|
+
read_only: bool | None
|
|
698
|
+
|
|
699
|
+
|
|
700
|
+
class EnvironmentVolumeMount(TypedDict):
|
|
701
|
+
source: EnvironmentVolumeSource
|
|
702
|
+
target: EnvironmentVolumeTarget
|
|
703
|
+
|
|
704
|
+
|
|
705
|
+
class EnvironmentV1(TypedDict):
|
|
706
|
+
id: str
|
|
707
|
+
guid: str
|
|
708
|
+
created_time: str
|
|
709
|
+
updated_time: str
|
|
710
|
+
title: str | None
|
|
711
|
+
description: str | None
|
|
712
|
+
cluster_name: str
|
|
713
|
+
name: str
|
|
714
|
+
environment_type: str
|
|
715
|
+
matching: str
|
|
716
|
+
supervisor: str | None
|
|
717
|
+
managed_by: str | None
|
|
718
|
+
python: EnvironmentInstallations
|
|
719
|
+
quarto: EnvironmentInstallations
|
|
720
|
+
r: EnvironmentInstallations
|
|
721
|
+
tensorflow: EnvironmentInstallations
|
|
722
|
+
volume_mounts: list[EnvironmentVolumeMount]
|
|
723
|
+
|
|
724
|
+
|
|
725
|
+
class EnvironmentCreateInput(TypedDict, total=False):
|
|
726
|
+
title: str | None
|
|
727
|
+
description: str | None
|
|
728
|
+
cluster_name: str
|
|
729
|
+
name: str
|
|
730
|
+
matching: str | None
|
|
731
|
+
supervisor: str | None
|
|
732
|
+
python: EnvironmentInstallations
|
|
733
|
+
quarto: EnvironmentInstallations
|
|
734
|
+
r: EnvironmentInstallations
|
|
735
|
+
tensorflow: EnvironmentInstallations
|
|
736
|
+
volume_mounts: list[EnvironmentVolumeMount]
|
|
737
|
+
|
|
738
|
+
|
|
739
|
+
class EnvironmentUpdateInput(TypedDict, total=False):
|
|
740
|
+
title: str | None
|
|
741
|
+
description: str | None
|
|
742
|
+
matching: str | None
|
|
743
|
+
supervisor: str | None
|
|
744
|
+
python: EnvironmentInstallations
|
|
745
|
+
quarto: EnvironmentInstallations
|
|
746
|
+
r: EnvironmentInstallations
|
|
747
|
+
tensorflow: EnvironmentInstallations
|
|
748
|
+
volume_mounts: list[EnvironmentVolumeMount]
|
|
749
|
+
|
|
750
|
+
|
|
751
|
+
class EnvironmentPermissionV1(TypedDict):
|
|
752
|
+
id: str
|
|
753
|
+
guid: str
|
|
754
|
+
environment_guid: str
|
|
755
|
+
user_guid: str | None
|
|
756
|
+
group_guid: str | None
|
|
757
|
+
|
|
758
|
+
|
|
759
|
+
class EnvironmentPermissionInput(TypedDict, total=False):
|
|
760
|
+
user_guid: str | None
|
|
761
|
+
group_guid: str | None
|
|
762
|
+
|
|
763
|
+
|
|
764
|
+
class OAuthIntegrationPermission(TypedDict):
|
|
765
|
+
user_guid: str | None
|
|
766
|
+
group_guid: str | None
|
|
767
|
+
|
|
768
|
+
|
|
769
|
+
class OAuthIntegration(TypedDict):
|
|
770
|
+
guid: str
|
|
771
|
+
name: str | None
|
|
772
|
+
description: str | None
|
|
773
|
+
template: str | None
|
|
774
|
+
auth_type: str | None
|
|
775
|
+
config: dict[str, object]
|
|
776
|
+
permissions: list[OAuthIntegrationPermission]
|
|
777
|
+
environment_variables: list[str]
|
|
778
|
+
created_time: str
|
|
779
|
+
updated_time: str
|
|
780
|
+
|
|
781
|
+
|
|
782
|
+
class OAuthIntegrationInput(TypedDict, total=False):
|
|
783
|
+
name: str | None
|
|
784
|
+
description: str | None
|
|
785
|
+
template: str
|
|
786
|
+
config: dict[str, object]
|
|
787
|
+
permissions: list[OAuthIntegrationPermission] | None
|
|
788
|
+
|
|
789
|
+
|
|
790
|
+
class OAuthIntegrationUpdate(TypedDict, total=False):
|
|
791
|
+
name: str | None
|
|
792
|
+
description: str | None
|
|
793
|
+
config: dict[str, object]
|
|
794
|
+
permissions: list[OAuthIntegrationPermission] | None
|
|
795
|
+
|
|
796
|
+
|
|
797
|
+
class OAuthTemplate(TypedDict):
|
|
798
|
+
id: str
|
|
799
|
+
name: str
|
|
800
|
+
description: str
|
|
801
|
+
fields: list[object]
|
|
802
|
+
options: list[object]
|
|
803
|
+
|
|
804
|
+
|
|
805
|
+
class KeyValueParamType(ParamType):
|
|
806
|
+
name = "key=value"
|
|
807
|
+
|
|
808
|
+
def convert(
|
|
809
|
+
self,
|
|
810
|
+
value: str | tuple[str, str],
|
|
811
|
+
param: Optional[click.Parameter],
|
|
812
|
+
ctx: Optional[click.Context],
|
|
813
|
+
) -> tuple[str, str]:
|
|
814
|
+
if isinstance(value, tuple):
|
|
815
|
+
return value
|
|
816
|
+
try:
|
|
817
|
+
k, v = value.split("=", 1)
|
|
818
|
+
return (k.strip(), v.strip())
|
|
819
|
+
except ValueError:
|
|
820
|
+
self.fail(f"'{value}' is not in 'key=value' format", param, ctx)
|
|
821
|
+
|
|
822
|
+
|
|
823
|
+
class RepositoryInfo(TypedDict):
|
|
824
|
+
repository: str
|
|
825
|
+
branch: str
|
|
826
|
+
directory: str
|
|
827
|
+
polling: bool
|
|
828
|
+
last_error: str
|
|
829
|
+
last_known_commit: str
|
|
830
|
+
|
|
831
|
+
|
|
832
|
+
class RepositoryBundleOutput(TypedDict):
|
|
833
|
+
bundle_id: str
|
|
834
|
+
task_id: str
|
|
835
|
+
location: dict[str, str]
|