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/pyproject.py
ADDED
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Support for detecting various information from python projects metadata.
|
|
3
|
+
|
|
4
|
+
Metadata can only be loaded from static files (e.g. pyproject.toml, setup.cfg, etc.)
|
|
5
|
+
but not from setup.py due to its dynamic nature.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import configparser
|
|
9
|
+
import dataclasses
|
|
10
|
+
import pathlib
|
|
11
|
+
import re
|
|
12
|
+
import typing
|
|
13
|
+
from collections.abc import Mapping
|
|
14
|
+
|
|
15
|
+
from .log import logger
|
|
16
|
+
from .models import AppMode, AppModes
|
|
17
|
+
|
|
18
|
+
TOMLDecodeError: typing.Type[Exception]
|
|
19
|
+
try:
|
|
20
|
+
import tomllib
|
|
21
|
+
|
|
22
|
+
TOMLDecodeError = tomllib.TOMLDecodeError
|
|
23
|
+
except ImportError:
|
|
24
|
+
# Python 3.11+ has tomllib in the standard library
|
|
25
|
+
import toml as tomllib # type: ignore[no-redef]
|
|
26
|
+
|
|
27
|
+
TOMLDecodeError = tomllib.TomlDecodeError
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
PEP440_OPERATORS_REGEX = r"(===|==|!=|<=|>=|<|>|~=)"
|
|
31
|
+
VALID_VERSION_REQ_REGEX = rf"^({PEP440_OPERATORS_REGEX}?\d+(\.[\d\*]+)*)+$"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def detect_python_version_requirement(directory: typing.Union[str, pathlib.Path]) -> typing.Optional[str]:
|
|
35
|
+
"""Detect the python version requirement for a project.
|
|
36
|
+
|
|
37
|
+
The directory should contain a metadata file such as pyproject.toml,
|
|
38
|
+
setup.cfg, or .python-version.
|
|
39
|
+
|
|
40
|
+
Returns the python version requirement as a string or None if not found.
|
|
41
|
+
"""
|
|
42
|
+
for _, metadata_file in lookup_metadata_file(directory):
|
|
43
|
+
parser = get_python_version_requirement_parser(metadata_file)
|
|
44
|
+
try:
|
|
45
|
+
version_constraint = parser(metadata_file)
|
|
46
|
+
except InvalidVersionConstraintError as err:
|
|
47
|
+
logger.error(f"Invalid python version constraint in {metadata_file}, ignoring it: {err}")
|
|
48
|
+
continue
|
|
49
|
+
|
|
50
|
+
if version_constraint:
|
|
51
|
+
return version_constraint
|
|
52
|
+
|
|
53
|
+
return None
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def lookup_metadata_file(directory: typing.Union[str, pathlib.Path]) -> typing.List[typing.Tuple[str, pathlib.Path]]:
|
|
57
|
+
"""Given the directory of a project return the path of a usable metadata file.
|
|
58
|
+
|
|
59
|
+
The returned value is either a list of tuples [(filename, path)] or
|
|
60
|
+
an empty list [] if no metadata file was found.
|
|
61
|
+
|
|
62
|
+
The metadata files are returned in the priority they should be processed
|
|
63
|
+
to determine the python version requirements.
|
|
64
|
+
"""
|
|
65
|
+
directory = pathlib.Path(directory)
|
|
66
|
+
|
|
67
|
+
def _generate():
|
|
68
|
+
for filename in (".python-version", "pyproject.toml", "setup.cfg"):
|
|
69
|
+
path = directory / filename
|
|
70
|
+
if path.is_file():
|
|
71
|
+
yield (filename, path)
|
|
72
|
+
|
|
73
|
+
return list(_generate())
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def get_python_version_requirement_parser(
|
|
77
|
+
metadata_file: pathlib.Path,
|
|
78
|
+
) -> typing.Callable[[pathlib.Path], typing.Optional[str]]:
|
|
79
|
+
"""Given the metadata file, return the appropriate parser function.
|
|
80
|
+
|
|
81
|
+
The returned function takes a pathlib.Path and returns the parsed value.
|
|
82
|
+
"""
|
|
83
|
+
if metadata_file.name == "pyproject.toml":
|
|
84
|
+
return parse_pyproject_python_requires
|
|
85
|
+
elif metadata_file.name == "setup.cfg":
|
|
86
|
+
return parse_setupcfg_python_requires
|
|
87
|
+
elif metadata_file.name == ".python-version":
|
|
88
|
+
return parse_pyversion_python_requires
|
|
89
|
+
else:
|
|
90
|
+
raise NotImplementedError(f"Unknown metadata file type: {metadata_file.name}")
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def parse_pyproject_python_requires(pyproject_file: pathlib.Path) -> typing.Optional[str]:
|
|
94
|
+
"""Parse the project.requires-python field from a pyproject.toml file.
|
|
95
|
+
|
|
96
|
+
Assumes that the pyproject.toml file exists, is accessible and well formatted.
|
|
97
|
+
|
|
98
|
+
Returns None if the field is not found.
|
|
99
|
+
"""
|
|
100
|
+
content = pyproject_file.read_text()
|
|
101
|
+
pyproject = tomllib.loads(content)
|
|
102
|
+
|
|
103
|
+
return pyproject.get("project", {}).get("requires-python", None)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def parse_setupcfg_python_requires(setupcfg_file: pathlib.Path) -> typing.Optional[str]:
|
|
107
|
+
"""Parse the options.python_requires field from a setup.cfg file.
|
|
108
|
+
|
|
109
|
+
Assumes that the setup.cfg file exists, is accessible and well formatted.
|
|
110
|
+
|
|
111
|
+
Returns None if the field is not found.
|
|
112
|
+
"""
|
|
113
|
+
config = configparser.ConfigParser()
|
|
114
|
+
config.read(setupcfg_file)
|
|
115
|
+
|
|
116
|
+
return config.get("options", "python_requires", fallback=None)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def parse_pyversion_python_requires(pyversion_file: pathlib.Path) -> typing.Optional[str]:
|
|
120
|
+
"""Parse the python version from a .python-version file.
|
|
121
|
+
|
|
122
|
+
Assumes that the .python-version file exists, is accessible and well formatted.
|
|
123
|
+
|
|
124
|
+
Returns None if the field is not found.
|
|
125
|
+
"""
|
|
126
|
+
return adapt_python_requires(pyversion_file.read_text().strip())
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def adapt_python_requires(
|
|
130
|
+
python_requires: str,
|
|
131
|
+
) -> str:
|
|
132
|
+
"""Convert a literal python version to a PEP440 constraint.
|
|
133
|
+
|
|
134
|
+
Connect expects a PEP440 format, but the .python-version file can contain
|
|
135
|
+
plain version numbers and other formats.
|
|
136
|
+
|
|
137
|
+
We should convert them to the constraints that connect expects.
|
|
138
|
+
"""
|
|
139
|
+
current_contraints = python_requires.split(",")
|
|
140
|
+
|
|
141
|
+
def _adapt_contraint(constraints: typing.List[str]) -> typing.Generator[str, None, None]:
|
|
142
|
+
for constraint in constraints:
|
|
143
|
+
constraint = constraint.strip()
|
|
144
|
+
if "@" in constraint or "-" in constraint or "/" in constraint:
|
|
145
|
+
raise InvalidVersionConstraintError(f"python specific implementations are not supported: {constraint}")
|
|
146
|
+
|
|
147
|
+
if "b" in constraint or "rc" in constraint or "a" in constraint:
|
|
148
|
+
raise InvalidVersionConstraintError(f"pre-release versions are not supported: {constraint}")
|
|
149
|
+
|
|
150
|
+
if re.match(VALID_VERSION_REQ_REGEX, constraint) is None:
|
|
151
|
+
raise InvalidVersionConstraintError(f"Invalid python version: {constraint}")
|
|
152
|
+
|
|
153
|
+
if re.search(PEP440_OPERATORS_REGEX, constraint):
|
|
154
|
+
yield constraint
|
|
155
|
+
else:
|
|
156
|
+
# Convert to PEP440 format
|
|
157
|
+
if "*" in constraint:
|
|
158
|
+
yield f"=={constraint}"
|
|
159
|
+
else:
|
|
160
|
+
# only major specified “3” → ~=3.0 → >=3.0,<4.0
|
|
161
|
+
# major and minor specified “3.8” or “3.8.11” → ~=3.8.0 → >=3.8.0,<3.9.0
|
|
162
|
+
constraint = ".".join(constraint.split(".")[:2] + ["0"])
|
|
163
|
+
yield f"~={constraint}"
|
|
164
|
+
|
|
165
|
+
return ",".join(_adapt_contraint(current_contraints))
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
class InvalidVersionConstraintError(ValueError):
|
|
169
|
+
pass
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class InvalidPyprojectConfigError(ValueError):
|
|
173
|
+
"""Raised when ``[tool.rsconnect]`` is missing or incomplete."""
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
class UnsupportedAppModeError(ValueError):
|
|
177
|
+
"""Raised when ``[tool.rsconnect].app_mode`` names an app mode rsconnect does not know.
|
|
178
|
+
|
|
179
|
+
Kept distinct from :class:`InvalidPyprojectConfigError` because the CLI does
|
|
180
|
+
not append the quickstart hint for this failure.
|
|
181
|
+
"""
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
_MINIMUM_VALID_TOOL_RSCONNECT_SNIPPET = """[tool.rsconnect]
|
|
185
|
+
# e.g. python-streamlit, python-shiny, python-fastapi, jupyter-static, quarto-shiny
|
|
186
|
+
app_mode = "<app_mode>"
|
|
187
|
+
entrypoint = "<entrypoint>" # e.g. app.py"""
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def read_tool_rsconnect(pyproject_file: pathlib.Path) -> typing.Mapping[str, typing.Any]:
|
|
191
|
+
"""Read the ``[tool.rsconnect]`` deployment config from pyproject.toml.
|
|
192
|
+
|
|
193
|
+
Returns the section mapping unchanged so forward-compatible fields pass
|
|
194
|
+
through. Raises ``InvalidPyprojectConfigError`` when the section is
|
|
195
|
+
missing or when required ``app_mode`` / ``entrypoint`` fields are absent or
|
|
196
|
+
not non-empty strings.
|
|
197
|
+
"""
|
|
198
|
+
content = pyproject_file.read_text()
|
|
199
|
+
pyproject = tomllib.loads(content)
|
|
200
|
+
|
|
201
|
+
tool = pyproject.get("tool")
|
|
202
|
+
if tool is None:
|
|
203
|
+
raise InvalidPyprojectConfigError(
|
|
204
|
+
f"The [tool.rsconnect] section is missing. Add at least:\n\n{_MINIMUM_VALID_TOOL_RSCONNECT_SNIPPET}"
|
|
205
|
+
)
|
|
206
|
+
if not isinstance(tool, Mapping):
|
|
207
|
+
raise InvalidPyprojectConfigError(
|
|
208
|
+
f"[tool.rsconnect] is not a TOML table. Add at least:\n\n{_MINIMUM_VALID_TOOL_RSCONNECT_SNIPPET}"
|
|
209
|
+
)
|
|
210
|
+
tool = typing.cast(typing.Mapping[str, typing.Any], tool)
|
|
211
|
+
|
|
212
|
+
tool_rsconnect = tool.get("rsconnect")
|
|
213
|
+
if tool_rsconnect is None:
|
|
214
|
+
raise InvalidPyprojectConfigError(
|
|
215
|
+
f"The [tool.rsconnect] section is missing. Add at least:\n\n{_MINIMUM_VALID_TOOL_RSCONNECT_SNIPPET}"
|
|
216
|
+
)
|
|
217
|
+
if not isinstance(tool_rsconnect, Mapping):
|
|
218
|
+
raise InvalidPyprojectConfigError(
|
|
219
|
+
f"[tool.rsconnect] is not a TOML table. Add at least:\n\n{_MINIMUM_VALID_TOOL_RSCONNECT_SNIPPET}"
|
|
220
|
+
)
|
|
221
|
+
tool_rsconnect = typing.cast(typing.Mapping[str, typing.Any], tool_rsconnect)
|
|
222
|
+
|
|
223
|
+
for field in ("app_mode", "entrypoint"):
|
|
224
|
+
value = tool_rsconnect.get(field)
|
|
225
|
+
if not isinstance(value, str) or not value:
|
|
226
|
+
raise InvalidPyprojectConfigError(
|
|
227
|
+
f"The [tool.rsconnect] field {field} must be a non-empty string. Add at least:\n\n"
|
|
228
|
+
f"{_MINIMUM_VALID_TOOL_RSCONNECT_SNIPPET}"
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
return tool_rsconnect
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
@dataclasses.dataclass(frozen=True)
|
|
235
|
+
class PyprojectDeployTarget:
|
|
236
|
+
"""Deployment configuration resolved from ``[tool.rsconnect]`` in pyproject.toml."""
|
|
237
|
+
|
|
238
|
+
app_mode: AppMode
|
|
239
|
+
# The app_mode string as written in pyproject.toml; may be an alias of
|
|
240
|
+
# app_mode.name() and is what error messages should quote back to the user.
|
|
241
|
+
configured_app_mode: str
|
|
242
|
+
entrypoint: str
|
|
243
|
+
requirements_file: str
|
|
244
|
+
title: typing.Optional[str]
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def resolve_pyproject_deploy_target(
|
|
248
|
+
pyproject_file: pathlib.Path,
|
|
249
|
+
requirements_file: typing.Optional[str] = None,
|
|
250
|
+
title_override: typing.Optional[str] = None,
|
|
251
|
+
) -> PyprojectDeployTarget:
|
|
252
|
+
"""Resolve the deployment target described by ``[tool.rsconnect]`` in pyproject.toml.
|
|
253
|
+
|
|
254
|
+
Raises ``InvalidPyprojectConfigError`` when the config is missing or
|
|
255
|
+
incomplete, and ``UnsupportedAppModeError`` when ``app_mode`` does not name
|
|
256
|
+
a known app mode.
|
|
257
|
+
|
|
258
|
+
:param pathlib.Path pyproject_file: path to the project's pyproject.toml.
|
|
259
|
+
:param typing.Optional[str] requirements_file: caller override for the
|
|
260
|
+
requirements source; wins over ``[tool.rsconnect].requirements_file``.
|
|
261
|
+
:param typing.Optional[str] title_override: fallback title used when the
|
|
262
|
+
config declares none.
|
|
263
|
+
"""
|
|
264
|
+
config = read_tool_rsconnect(pyproject_file)
|
|
265
|
+
|
|
266
|
+
configured_app_mode = typing.cast(str, config["app_mode"])
|
|
267
|
+
app_mode = AppModes.get_by_name(configured_app_mode, return_unknown=True)
|
|
268
|
+
if app_mode == AppModes.UNKNOWN:
|
|
269
|
+
raise UnsupportedAppModeError(f"Unsupported app_mode '{configured_app_mode}' in [tool.rsconnect]")
|
|
270
|
+
|
|
271
|
+
# Requirements source precedence: caller override (the ``-r`` flag) >
|
|
272
|
+
# ``[tool.rsconnect].requirements_file`` > built-in default ``pyproject.toml``
|
|
273
|
+
# (top-level deps; Connect resolves transitive). An explicit default keeps the
|
|
274
|
+
# inspector from falling back to a ``pip freeze`` of the caller's interpreter.
|
|
275
|
+
# Malformed TOML values (wrong type, missing file) are surfaced by the
|
|
276
|
+
# inspector / file existence check.
|
|
277
|
+
return PyprojectDeployTarget(
|
|
278
|
+
app_mode=app_mode,
|
|
279
|
+
configured_app_mode=configured_app_mode,
|
|
280
|
+
entrypoint=typing.cast(str, config["entrypoint"]),
|
|
281
|
+
requirements_file=typing.cast(str, requirements_file or config.get("requirements_file") or "pyproject.toml"),
|
|
282
|
+
title=typing.cast(typing.Optional[str], config.get("title")) or title_override,
|
|
283
|
+
)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""``rsconnect quickstart`` package.
|
|
2
|
+
|
|
3
|
+
Public API:
|
|
4
|
+
|
|
5
|
+
- :func:`run_quickstart` — scaffold a new project.
|
|
6
|
+
|
|
7
|
+
Internal pieces (``TemplateSpec``, ``_REGISTRY``, etc.) live in
|
|
8
|
+
:mod:`rsconnect.quickstart.quickstart` and are not re-exported here.
|
|
9
|
+
Tests that need them (registry-extensibility) import the inner module
|
|
10
|
+
directly. The CLI alias vocabulary lives on :class:`rsconnect.models.AppModes`
|
|
11
|
+
(see :meth:`rsconnect.models.AppModes.cli_aliases`).
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from .quickstart import run_quickstart
|
|
15
|
+
|
|
16
|
+
__all__ = ["run_quickstart"]
|