rsconnect-python 1.31.0__tar.gz → 1.31.1__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.31.0 → rsconnect_python-1.31.1}/PKG-INFO +1 -1
- {rsconnect_python-1.31.0 → rsconnect_python-1.31.1}/pyproject.toml +1 -1
- {rsconnect_python-1.31.0 → rsconnect_python-1.31.1}/pyproject.toml.orig +1 -1
- {rsconnect_python-1.31.0 → rsconnect_python-1.31.1}/rsconnect/api.py +60 -20
- {rsconnect_python-1.31.0 → rsconnect_python-1.31.1}/rsconnect/connect_cloud.py +209 -0
- {rsconnect_python-1.31.0 → rsconnect_python-1.31.1}/LICENSE.md +0 -0
- {rsconnect_python-1.31.0 → rsconnect_python-1.31.1}/README.md +0 -0
- {rsconnect_python-1.31.0 → rsconnect_python-1.31.1}/rsconnect/__init__.py +0 -0
- {rsconnect_python-1.31.0 → rsconnect_python-1.31.1}/rsconnect/actions.py +0 -0
- {rsconnect_python-1.31.0 → rsconnect_python-1.31.1}/rsconnect/actions_content.py +0 -0
- {rsconnect_python-1.31.0 → rsconnect_python-1.31.1}/rsconnect/actions_environment.py +0 -0
- {rsconnect_python-1.31.0 → rsconnect_python-1.31.1}/rsconnect/actions_integration.py +0 -0
- {rsconnect_python-1.31.0 → rsconnect_python-1.31.1}/rsconnect/bundle.py +0 -0
- {rsconnect_python-1.31.0 → rsconnect_python-1.31.1}/rsconnect/certificates.py +0 -0
- {rsconnect_python-1.31.0 → rsconnect_python-1.31.1}/rsconnect/environment.py +0 -0
- {rsconnect_python-1.31.0 → rsconnect_python-1.31.1}/rsconnect/environment_node.py +0 -0
- {rsconnect_python-1.31.0 → rsconnect_python-1.31.1}/rsconnect/environment_r.py +0 -0
- {rsconnect_python-1.31.0 → rsconnect_python-1.31.1}/rsconnect/exception.py +0 -0
- {rsconnect_python-1.31.0 → rsconnect_python-1.31.1}/rsconnect/git_metadata.py +0 -0
- {rsconnect_python-1.31.0 → rsconnect_python-1.31.1}/rsconnect/http_support.py +0 -0
- {rsconnect_python-1.31.0 → rsconnect_python-1.31.1}/rsconnect/json_web_token.py +0 -0
- {rsconnect_python-1.31.0 → rsconnect_python-1.31.1}/rsconnect/log.py +0 -0
- {rsconnect_python-1.31.0 → rsconnect_python-1.31.1}/rsconnect/main.py +0 -0
- {rsconnect_python-1.31.0 → rsconnect_python-1.31.1}/rsconnect/metadata.py +0 -0
- {rsconnect_python-1.31.0 → rsconnect_python-1.31.1}/rsconnect/models.py +0 -0
- {rsconnect_python-1.31.0 → rsconnect_python-1.31.1}/rsconnect/oauth.py +0 -0
- {rsconnect_python-1.31.0 → rsconnect_python-1.31.1}/rsconnect/py.typed +0 -0
- {rsconnect_python-1.31.0 → rsconnect_python-1.31.1}/rsconnect/pyproject.py +0 -0
- {rsconnect_python-1.31.0 → rsconnect_python-1.31.1}/rsconnect/quickstart/__init__.py +0 -0
- {rsconnect_python-1.31.0 → rsconnect_python-1.31.1}/rsconnect/quickstart/quickstart.py +0 -0
- {rsconnect_python-1.31.0 → rsconnect_python-1.31.1}/rsconnect/quickstart/templates/__init__.py +0 -0
- {rsconnect_python-1.31.0 → rsconnect_python-1.31.1}/rsconnect/quickstart/templates/api/README.md.tmpl +0 -0
- {rsconnect_python-1.31.0 → rsconnect_python-1.31.1}/rsconnect/quickstart/templates/api/__connect__.py.tmpl +0 -0
- {rsconnect_python-1.31.0 → rsconnect_python-1.31.1}/rsconnect/quickstart/templates/api/__init__.py.tmpl +0 -0
- {rsconnect_python-1.31.0 → rsconnect_python-1.31.1}/rsconnect/quickstart/templates/api/__main__.py.tmpl +0 -0
- {rsconnect_python-1.31.0 → rsconnect_python-1.31.1}/rsconnect/quickstart/templates/api/app.py.tmpl +0 -0
- {rsconnect_python-1.31.0 → rsconnect_python-1.31.1}/rsconnect/quickstart/templates/api/pyproject.toml.tmpl +0 -0
- {rsconnect_python-1.31.0 → rsconnect_python-1.31.1}/rsconnect/quickstart/templates/fastapi/README.md.tmpl +0 -0
- {rsconnect_python-1.31.0 → rsconnect_python-1.31.1}/rsconnect/quickstart/templates/fastapi/__connect__.py.tmpl +0 -0
- {rsconnect_python-1.31.0 → rsconnect_python-1.31.1}/rsconnect/quickstart/templates/fastapi/__init__.py.tmpl +0 -0
- {rsconnect_python-1.31.0 → rsconnect_python-1.31.1}/rsconnect/quickstart/templates/fastapi/__main__.py.tmpl +0 -0
- {rsconnect_python-1.31.0 → rsconnect_python-1.31.1}/rsconnect/quickstart/templates/fastapi/app.py.tmpl +0 -0
- {rsconnect_python-1.31.0 → rsconnect_python-1.31.1}/rsconnect/quickstart/templates/fastapi/pyproject.toml.tmpl +0 -0
- {rsconnect_python-1.31.0 → rsconnect_python-1.31.1}/rsconnect/quickstart/templates/notebook/README.md.tmpl +0 -0
- {rsconnect_python-1.31.0 → rsconnect_python-1.31.1}/rsconnect/quickstart/templates/notebook/notebook.ipynb.tmpl +0 -0
- {rsconnect_python-1.31.0 → rsconnect_python-1.31.1}/rsconnect/quickstart/templates/notebook/pyproject.toml.tmpl +0 -0
- {rsconnect_python-1.31.0 → rsconnect_python-1.31.1}/rsconnect/quickstart/templates/quarto/README.md.tmpl +0 -0
- {rsconnect_python-1.31.0 → rsconnect_python-1.31.1}/rsconnect/quickstart/templates/quarto/pyproject.toml.tmpl +0 -0
- {rsconnect_python-1.31.0 → rsconnect_python-1.31.1}/rsconnect/quickstart/templates/quarto/report.qmd.tmpl +0 -0
- {rsconnect_python-1.31.0 → rsconnect_python-1.31.1}/rsconnect/quickstart/templates/shiny/README.md.tmpl +0 -0
- {rsconnect_python-1.31.0 → rsconnect_python-1.31.1}/rsconnect/quickstart/templates/shiny/app.py.tmpl +0 -0
- {rsconnect_python-1.31.0 → rsconnect_python-1.31.1}/rsconnect/quickstart/templates/shiny/pyproject.toml.tmpl +0 -0
- {rsconnect_python-1.31.0 → rsconnect_python-1.31.1}/rsconnect/quickstart/templates/streamlit/README.md.tmpl +0 -0
- {rsconnect_python-1.31.0 → rsconnect_python-1.31.1}/rsconnect/quickstart/templates/streamlit/app.py.tmpl +0 -0
- {rsconnect_python-1.31.0 → rsconnect_python-1.31.1}/rsconnect/quickstart/templates/streamlit/pyproject.toml.tmpl +0 -0
- {rsconnect_python-1.31.0 → rsconnect_python-1.31.1}/rsconnect/quickstart/templates/voila/README.md.tmpl +0 -0
- {rsconnect_python-1.31.0 → rsconnect_python-1.31.1}/rsconnect/quickstart/templates/voila/pyproject.toml.tmpl +0 -0
- {rsconnect_python-1.31.0 → rsconnect_python-1.31.1}/rsconnect/shiny_express.py +0 -0
- {rsconnect_python-1.31.0 → rsconnect_python-1.31.1}/rsconnect/snowflake.py +0 -0
- {rsconnect_python-1.31.0 → rsconnect_python-1.31.1}/rsconnect/subprocesses/__init__.py +0 -0
- {rsconnect_python-1.31.0 → rsconnect_python-1.31.1}/rsconnect/subprocesses/inspect_environment.py +0 -0
- {rsconnect_python-1.31.0 → rsconnect_python-1.31.1}/rsconnect/timeouts.py +0 -0
- {rsconnect_python-1.31.0 → rsconnect_python-1.31.1}/rsconnect/utils_package.py +0 -0
- {rsconnect_python-1.31.0 → rsconnect_python-1.31.1}/rsconnect/validation.py +0 -0
- {rsconnect_python-1.31.0 → rsconnect_python-1.31.1}/rsconnect/version_check.py +0 -0
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name = "rsconnect_python"
|
|
3
3
|
description = "The Posit Connect command-line interface."
|
|
4
4
|
requires-python = ">=3.8"
|
|
5
|
-
version = "1.31.
|
|
5
|
+
version = "1.31.1"
|
|
6
6
|
dependencies = [ "typing-extensions>=4.8.0", "pip>=10.0.0", "uv>=0.9.0", "semver>=2.0.0,<4.0.0", "pyjwt>=2.4.0", "click>=8.0.0", "packaging>=20.0", "toml>=0.10; python_version < '3.11'",]
|
|
7
7
|
[[project.authors]]
|
|
8
8
|
name = "Posit, PBC"
|
|
@@ -2165,16 +2165,15 @@ for shinyapps.io. See command help for further details."
|
|
|
2165
2165
|
)
|
|
2166
2166
|
upload_result = S3Server(upload_url).handle_bad_response(upload_result, is_httpresponse=True)
|
|
2167
2167
|
|
|
2168
|
-
def
|
|
2169
|
-
"""The
|
|
2168
|
+
def bundle_manifest(self) -> dict[str, Any]:
|
|
2169
|
+
"""The manifest of the built bundle.
|
|
2170
2170
|
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
avoids having to thread it separately through each command.
|
|
2171
|
+
Every deploy path writes what Connect Cloud needs into the bundle's
|
|
2172
|
+
manifest, so reading it back from there avoids having to thread the same
|
|
2173
|
+
values separately through each command.
|
|
2175
2174
|
"""
|
|
2176
2175
|
if self.bundle is None:
|
|
2177
|
-
raise RSConnectException("A bundle must be created before
|
|
2176
|
+
raise RSConnectException("A bundle must be created before reading its manifest.")
|
|
2178
2177
|
|
|
2179
2178
|
position = self.bundle.tell()
|
|
2180
2179
|
try:
|
|
@@ -2186,12 +2185,34 @@ for shinyapps.io. See command help for further details."
|
|
|
2186
2185
|
extracted = tar.extractfile(member) if member is not None else None
|
|
2187
2186
|
if extracted is None:
|
|
2188
2187
|
raise RSConnectException("The bundle does not contain a manifest.json.")
|
|
2189
|
-
|
|
2188
|
+
return cast("dict[str, Any]", json.loads(extracted.read().decode("utf-8")))
|
|
2190
2189
|
except (tarfile.TarError, KeyError, ValueError) as exc:
|
|
2191
2190
|
raise RSConnectException("Could not read the bundle manifest: %s" % exc) from exc
|
|
2192
2191
|
finally:
|
|
2193
2192
|
self.bundle.seek(position)
|
|
2194
2193
|
|
|
2194
|
+
def python_version_for_connect_cloud(self) -> Optional[str]:
|
|
2195
|
+
"""The MAJOR.MINOR Python version to ask Connect Cloud for, or None.
|
|
2196
|
+
|
|
2197
|
+
None means the request omits the field: a redeploy keeps the version already
|
|
2198
|
+
set on the content, and a first deploy takes Connect Cloud's default. Content
|
|
2199
|
+
with no Python at all lands here too.
|
|
2200
|
+
"""
|
|
2201
|
+
manifest = self.bundle_manifest()
|
|
2202
|
+
environment: dict[str, Any] = manifest.get("environment") or {}
|
|
2203
|
+
requirement: dict[str, Any] = environment.get("python") or {}
|
|
2204
|
+
interpreter: dict[str, Any] = manifest.get("python") or {}
|
|
2205
|
+
requires: Optional[str] = requirement.get("requires")
|
|
2206
|
+
local_version: Optional[str] = interpreter.get("version")
|
|
2207
|
+
return connect_cloud.resolve_python_version(requires, local_version)
|
|
2208
|
+
|
|
2209
|
+
def primary_file_for_connect_cloud(self) -> str:
|
|
2210
|
+
"""The entrypoint to report to Connect Cloud, read from the built bundle.
|
|
2211
|
+
|
|
2212
|
+
Connect Cloud needs a `primary_file` and uses it to decide, for example,
|
|
2213
|
+
whether Shiny content is R or Python.
|
|
2214
|
+
"""
|
|
2215
|
+
manifest = self.bundle_manifest()
|
|
2195
2216
|
metadata = manifest.get("metadata") or {}
|
|
2196
2217
|
primary_file = metadata.get("entrypoint") or metadata.get("primary_rmd") or metadata.get("primary_html")
|
|
2197
2218
|
files = manifest.get("files") or {}
|
|
@@ -2302,6 +2323,7 @@ for shinyapps.io. See command help for further details."
|
|
|
2302
2323
|
title=self.title,
|
|
2303
2324
|
app_mode=self.app_mode,
|
|
2304
2325
|
primary_file=self.primary_file_for_connect_cloud(),
|
|
2326
|
+
python_version=self.python_version_for_connect_cloud(),
|
|
2305
2327
|
env_vars=self.env_vars,
|
|
2306
2328
|
update_title=not self.title_is_default,
|
|
2307
2329
|
app_id_is_explicit=self.app_id_is_explicit,
|
|
@@ -3293,6 +3315,7 @@ class ConnectCloudAccountSearchResults(TypedDict):
|
|
|
3293
3315
|
class ConnectCloudRevision(TypedDict):
|
|
3294
3316
|
id: str
|
|
3295
3317
|
content_id: NotRequired[str]
|
|
3318
|
+
python_version: NotRequired[Optional[str]]
|
|
3296
3319
|
status: NotRequired[str]
|
|
3297
3320
|
source_bundle_upload_url: NotRequired[str]
|
|
3298
3321
|
publish_result: NotRequired[Optional[str]]
|
|
@@ -3648,21 +3671,29 @@ class ConnectCloudClient(BearerTokenHTTPServer):
|
|
|
3648
3671
|
primary_file: str,
|
|
3649
3672
|
secrets: Optional[list[dict[str, str]]] = None,
|
|
3650
3673
|
access: Optional[str] = None,
|
|
3674
|
+
python_version: Optional[str] = None,
|
|
3651
3675
|
) -> ConnectCloudContent:
|
|
3652
3676
|
"""Create content to upload a bundle into.
|
|
3653
3677
|
|
|
3654
3678
|
`access` is the content's visibility ("public" or "private"). Omitted from
|
|
3655
3679
|
the request when None so the server picks its own default.
|
|
3680
|
+
|
|
3681
|
+
`python_version` is MAJOR.MINOR. Omitted when None, which is how content
|
|
3682
|
+
with no Python is created; Connect Cloud then falls back to its own
|
|
3683
|
+
default rather than reading the version out of the bundle's manifest.
|
|
3656
3684
|
"""
|
|
3685
|
+
next_revision: dict[str, Any] = {
|
|
3686
|
+
"source_type": "bundle",
|
|
3687
|
+
"content_type": content_type,
|
|
3688
|
+
"app_mode": app_mode,
|
|
3689
|
+
"primary_file": primary_file,
|
|
3690
|
+
}
|
|
3691
|
+
if python_version is not None:
|
|
3692
|
+
next_revision["python_version"] = python_version
|
|
3657
3693
|
body: dict[str, Any] = {
|
|
3658
3694
|
"account_id": account_id,
|
|
3659
3695
|
"title": title,
|
|
3660
|
-
"next_revision":
|
|
3661
|
-
"source_type": "bundle",
|
|
3662
|
-
"content_type": content_type,
|
|
3663
|
-
"app_mode": app_mode,
|
|
3664
|
-
"primary_file": primary_file,
|
|
3665
|
-
},
|
|
3696
|
+
"next_revision": next_revision,
|
|
3666
3697
|
"secrets": secrets or [],
|
|
3667
3698
|
}
|
|
3668
3699
|
if access is not None:
|
|
@@ -3680,6 +3711,7 @@ class ConnectCloudClient(BearerTokenHTTPServer):
|
|
|
3680
3711
|
new_bundle: bool = True,
|
|
3681
3712
|
title: Optional[str] = None,
|
|
3682
3713
|
access: Optional[str] = None,
|
|
3714
|
+
python_version: Optional[str] = None,
|
|
3683
3715
|
) -> ConnectCloudContent:
|
|
3684
3716
|
"""Update content, optionally minting a fresh revision to upload into.
|
|
3685
3717
|
|
|
@@ -3693,14 +3725,19 @@ class ConnectCloudClient(BearerTokenHTTPServer):
|
|
|
3693
3725
|
deploy without -E/-t must leave the existing values alone. `access` (the
|
|
3694
3726
|
content's visibility) is omitted the same way, so a redeploy without
|
|
3695
3727
|
-V keeps whatever visibility the content already has.
|
|
3728
|
+
|
|
3729
|
+
`python_version` is omitted when None for the same reason: a redeploy of
|
|
3730
|
+
content whose bundle carries no Python version keeps the version already
|
|
3731
|
+
set on the content, including one chosen in the Connect Cloud UI.
|
|
3696
3732
|
"""
|
|
3697
|
-
|
|
3698
|
-
"
|
|
3699
|
-
|
|
3700
|
-
|
|
3701
|
-
"content_type": content_type,
|
|
3702
|
-
},
|
|
3733
|
+
revision_overrides: dict[str, Any] = {
|
|
3734
|
+
"primary_file": primary_file,
|
|
3735
|
+
"app_mode": app_mode,
|
|
3736
|
+
"content_type": content_type,
|
|
3703
3737
|
}
|
|
3738
|
+
if python_version is not None:
|
|
3739
|
+
revision_overrides["python_version"] = python_version
|
|
3740
|
+
body: dict[str, Any] = {"revision_overrides": revision_overrides}
|
|
3704
3741
|
if secrets is not None:
|
|
3705
3742
|
body["secrets"] = secrets
|
|
3706
3743
|
if title is not None:
|
|
@@ -3895,6 +3932,7 @@ class ConnectCloudService:
|
|
|
3895
3932
|
update_title: bool = False,
|
|
3896
3933
|
app_id_is_explicit: bool = False,
|
|
3897
3934
|
visibility: Optional[str] = None,
|
|
3935
|
+
python_version: Optional[str] = None,
|
|
3898
3936
|
) -> ConnectCloudDeployResult:
|
|
3899
3937
|
"""Create or fetch the content item and get a revision to upload into.
|
|
3900
3938
|
|
|
@@ -3961,6 +3999,7 @@ class ConnectCloudService:
|
|
|
3961
3999
|
content_type=content_type,
|
|
3962
4000
|
app_mode=app_mode.name(),
|
|
3963
4001
|
primary_file=primary_file,
|
|
4002
|
+
python_version=python_version,
|
|
3964
4003
|
secrets=secrets,
|
|
3965
4004
|
access=visibility,
|
|
3966
4005
|
)
|
|
@@ -3975,6 +4014,7 @@ class ConnectCloudService:
|
|
|
3975
4014
|
primary_file=primary_file,
|
|
3976
4015
|
app_mode=app_mode.name(),
|
|
3977
4016
|
content_type=content_type,
|
|
4017
|
+
python_version=python_version,
|
|
3978
4018
|
secrets=secrets,
|
|
3979
4019
|
new_bundle=upload,
|
|
3980
4020
|
title=title if update_title and title else None,
|
|
@@ -12,7 +12,11 @@ import os
|
|
|
12
12
|
from typing import Any, NamedTuple, Optional
|
|
13
13
|
from urllib.parse import urlparse
|
|
14
14
|
|
|
15
|
+
from packaging.specifiers import InvalidSpecifier, Specifier, SpecifierSet
|
|
16
|
+
from packaging.version import InvalidVersion, Version
|
|
17
|
+
|
|
15
18
|
from .exception import RSConnectException
|
|
19
|
+
from .log import logger
|
|
16
20
|
from .oauth import (
|
|
17
21
|
ACCESS_TOKEN_FIELD,
|
|
18
22
|
CLIENT_SECRET_FIELD,
|
|
@@ -313,3 +317,208 @@ def credentials_from_keyring(url: str, nickname: str) -> tuple[Optional[str], Op
|
|
|
313
317
|
def delete_credentials_from_keyring(url: str, nickname: str) -> None:
|
|
314
318
|
"""Delete a saved credential's secrets from the system keyring."""
|
|
315
319
|
keyring_delete_values(keyring_key(url, nickname), _CREDENTIAL_FIELDS)
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
# The lowest MAJOR.MINOR Connect Cloud will accept for a revision's python_version
|
|
323
|
+
# (the RequestPythonVersion enum in https://api.connect.posit.cloud/openapi.json).
|
|
324
|
+
MINIMUM_PYTHON_VERSION = Version("3.9")
|
|
325
|
+
|
|
326
|
+
# Tail of both warnings below. resolve_python_version runs before the deploy knows
|
|
327
|
+
# whether the content already exists, so it has to name both outcomes.
|
|
328
|
+
_VERSION_CHOSEN_BY_CONNECT_CLOUD = (
|
|
329
|
+
"The content will run on the Python version already set on it in Posit Connect "
|
|
330
|
+
"Cloud, or Connect Cloud's default if this is a first deployment."
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
# Operators that put a floor under the versions a clause admits. "<" and "<=" bound
|
|
334
|
+
# only the top, and "!=" excludes a point, so neither says where the range starts.
|
|
335
|
+
_LOWER_BOUND_OPERATORS = (">=", ">", "==", "===", "~=")
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def _clause_version(clause: Specifier) -> Optional[Version]:
|
|
339
|
+
"""The version a single specifier clause names, or None if it cannot be parsed.
|
|
340
|
+
|
|
341
|
+
Strips the wildcard from "==3.11.*", which is not a version on its own.
|
|
342
|
+
"""
|
|
343
|
+
try:
|
|
344
|
+
return Version(clause.version.rstrip("*").rstrip("."))
|
|
345
|
+
except InvalidVersion:
|
|
346
|
+
return None
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def _effective_floor(specifier: SpecifierSet) -> Optional[Version]:
|
|
350
|
+
"""The lowest version the constraint admits, or None if it has no lower bound.
|
|
351
|
+
|
|
352
|
+
Clauses are ANDed, so the highest of their floors is the one that binds:
|
|
353
|
+
">=3.9,==3.12.*" starts at 3.12, not 3.9.
|
|
354
|
+
"""
|
|
355
|
+
floor = None
|
|
356
|
+
for clause in specifier:
|
|
357
|
+
if clause.operator not in _LOWER_BOUND_OPERATORS:
|
|
358
|
+
continue
|
|
359
|
+
version = _clause_version(clause)
|
|
360
|
+
if version is not None and (floor is None or version > floor):
|
|
361
|
+
floor = version
|
|
362
|
+
return floor
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
# Connect Cloud accepts a MAJOR.MINOR line, such as "3.11", and chooses the patch
|
|
366
|
+
# release. The helpers below determine whether a constraint permits none, some,
|
|
367
|
+
# or all patch releases in that line.
|
|
368
|
+
#
|
|
369
|
+
# SpecifierSet can test only concrete versions, so we sample patch 0 and, for each
|
|
370
|
+
# clause referring to the line, its named patch and the following patch. Between
|
|
371
|
+
# these boundaries the result cannot change, making those samples sufficient.
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def _line_boundary_patches(minor: str, specifier: SpecifierSet) -> list[int]:
|
|
375
|
+
"""The sample patches for a line. For "3.11" and ">3.11.5", [0, 5, 6]."""
|
|
376
|
+
line = Version(minor).release[:2]
|
|
377
|
+
patches = {0}
|
|
378
|
+
for clause in specifier:
|
|
379
|
+
version = _clause_version(clause)
|
|
380
|
+
if version is None:
|
|
381
|
+
continue
|
|
382
|
+
release = version.release
|
|
383
|
+
# Padded so a clause naming only a major, as ">4" does, is read against that
|
|
384
|
+
# major's first line rather than matching nothing.
|
|
385
|
+
clause_line = (release + (0, 0))[:2]
|
|
386
|
+
if clause_line != line:
|
|
387
|
+
continue
|
|
388
|
+
patch = release[2] if len(release) > 2 else 0
|
|
389
|
+
patches.update((patch, patch + 1))
|
|
390
|
+
return sorted(patches)
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def _line_permits_some_patch(minor: str, specifier: SpecifierSet) -> bool:
|
|
394
|
+
"""Return whether `specifier` permits at least one patch release in `minor`.
|
|
395
|
+
|
|
396
|
+
`minor` is a MAJOR.MINOR line such as "3.11". For example, ">3.11.5,<3.12"
|
|
397
|
+
permits the 3.11 line, while "!=3.11.*" does not.
|
|
398
|
+
"""
|
|
399
|
+
return any(Version("%s.%d" % (minor, patch)) in specifier for patch in _line_boundary_patches(minor, specifier))
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
# How many minor lines above the constraint's floor to consider when the floor's own
|
|
403
|
+
# line is excluded. Generous for any real requirement, and only there to stop a
|
|
404
|
+
# pathological constraint scanning forever. This is a bound on our own search, not on
|
|
405
|
+
# the versions Connect Cloud offers: it never rejects a version, it only gives up
|
|
406
|
+
# looking for one.
|
|
407
|
+
_LINE_SCAN_LIMIT = 20
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
def _lowest_admitted_line(floor: Version, specifier: SpecifierSet) -> Optional[str]:
|
|
411
|
+
"""The lowest MAJOR.MINOR at or above `floor` that the constraint admits.
|
|
412
|
+
|
|
413
|
+
Usually the floor's own line. It is not when an exclusion empties that line, as
|
|
414
|
+
">=3.9,!=3.9.*" does, which means 3.10 and is the reason this scans rather than
|
|
415
|
+
testing the floor alone.
|
|
416
|
+
"""
|
|
417
|
+
release = floor.release
|
|
418
|
+
# A floor naming only a major version, as ">=4" does, starts at that major's
|
|
419
|
+
# first line.
|
|
420
|
+
major, minor = release[0], release[1] if len(release) > 1 else 0
|
|
421
|
+
for offset in range(_LINE_SCAN_LIMIT + 1):
|
|
422
|
+
candidate = "%d.%d" % (major, minor + offset)
|
|
423
|
+
if _line_permits_some_patch(candidate, specifier):
|
|
424
|
+
return candidate
|
|
425
|
+
return None
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def _minor_version(version: Version) -> Optional[str]:
|
|
429
|
+
"""A version as the MAJOR.MINOR string Connect Cloud names its interpreters by.
|
|
430
|
+
|
|
431
|
+
None when the version names no minor, as "==3.*" does: there is no single line
|
|
432
|
+
to ask for.
|
|
433
|
+
"""
|
|
434
|
+
if len(version.release) < 2:
|
|
435
|
+
return None
|
|
436
|
+
return "%d.%d" % (version.release[0], version.release[1])
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def _parsed(version: str) -> Optional[Version]:
|
|
440
|
+
try:
|
|
441
|
+
return Version(version)
|
|
442
|
+
except InvalidVersion:
|
|
443
|
+
return None
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
def _log_requested(minor: str) -> str:
|
|
447
|
+
"""Log the version being asked for and return it.
|
|
448
|
+
|
|
449
|
+
Said on every deploy, and names the patch as Connect Cloud's choice. A patch the
|
|
450
|
+
user asked for is already gone by the time it reaches here -- adapt_python_requires
|
|
451
|
+
turns a .python-version of "3.11.5" into "~=3.11.0" -- so this line is the only
|
|
452
|
+
place they see that it was not honored.
|
|
453
|
+
"""
|
|
454
|
+
logger.info("Requesting Python %s from Posit Connect Cloud, which chooses the patch release." % minor)
|
|
455
|
+
return minor
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
def resolve_python_version(requires: Optional[str], local_version: Optional[str]) -> Optional[str]:
|
|
459
|
+
"""Pick the Connect Cloud Python version for a deploy.
|
|
460
|
+
|
|
461
|
+
`requires` is the PEP 440 constraint from the manifest's
|
|
462
|
+
``environment.python.requires``; `local_version` is the interpreter the
|
|
463
|
+
bundle was built against, from ``python.version``. Returns None when there is
|
|
464
|
+
nothing usable to send, which leaves the field off the request: a redeploy then
|
|
465
|
+
keeps the version already set on the content, and a first deploy takes Connect
|
|
466
|
+
Cloud's default.
|
|
467
|
+
|
|
468
|
+
The interpreter the content was built against wins when the constraint allows
|
|
469
|
+
it. Otherwise the lowest version the constraint admits is used.
|
|
470
|
+
|
|
471
|
+
Availability is not checked here: an unrecognized version is sent and Connect
|
|
472
|
+
Cloud rules on it. A version below the lowest Connect Cloud offers is the
|
|
473
|
+
exception, and is not sent at all.
|
|
474
|
+
|
|
475
|
+
Connect Cloud picks the patch itself, so a constraint that names one ("==3.11.14")
|
|
476
|
+
can only be honored as far as its minor line.
|
|
477
|
+
"""
|
|
478
|
+
specifier = None
|
|
479
|
+
if requires:
|
|
480
|
+
try:
|
|
481
|
+
specifier = SpecifierSet(requires)
|
|
482
|
+
except InvalidSpecifier:
|
|
483
|
+
logger.warning(
|
|
484
|
+
"Ignoring the manifest's Python version requirement, which is not a valid "
|
|
485
|
+
"PEP 440 constraint: %s" % requires
|
|
486
|
+
)
|
|
487
|
+
|
|
488
|
+
local = _parsed(local_version) if local_version else None
|
|
489
|
+
if local is not None and (specifier is None or local in specifier):
|
|
490
|
+
minor = _minor_version(local)
|
|
491
|
+
if minor is not None and Version(minor) >= MINIMUM_PYTHON_VERSION:
|
|
492
|
+
return _log_requested(minor)
|
|
493
|
+
# rsconnect runs on Pythons older than Connect Cloud offers. Without a
|
|
494
|
+
# requirement there is nothing else to go on, so let the platform choose;
|
|
495
|
+
# with one, the search below may still find a version it does offer.
|
|
496
|
+
if specifier is None:
|
|
497
|
+
if minor is None:
|
|
498
|
+
logger.warning(
|
|
499
|
+
'The manifest\'s Python version "%s" does not name a minor version to ask Posit '
|
|
500
|
+
"Connect Cloud for. %s" % (local_version, _VERSION_CHOSEN_BY_CONNECT_CLOUD)
|
|
501
|
+
)
|
|
502
|
+
else:
|
|
503
|
+
logger.warning(
|
|
504
|
+
"Posit Connect Cloud does not offer Python %s. %s" % (minor, _VERSION_CHOSEN_BY_CONNECT_CLOUD)
|
|
505
|
+
)
|
|
506
|
+
return None
|
|
507
|
+
|
|
508
|
+
if specifier is None:
|
|
509
|
+
return None
|
|
510
|
+
|
|
511
|
+
# Search from the requirement's lowest version or Connect Cloud's, whichever is
|
|
512
|
+
# higher, so the result is one Connect Cloud can actually run: ">=3.8" is met by
|
|
513
|
+
# every version it offers and resolves to 3.9. A requirement with no lower bound,
|
|
514
|
+
# or one naming only a major version, starts at Connect Cloud's lowest.
|
|
515
|
+
floor = _effective_floor(specifier)
|
|
516
|
+
start = max(floor, MINIMUM_PYTHON_VERSION) if floor is not None else MINIMUM_PYTHON_VERSION
|
|
517
|
+
minor = _lowest_admitted_line(start, specifier)
|
|
518
|
+
if minor is None:
|
|
519
|
+
logger.warning(
|
|
520
|
+
"No Python version Posit Connect Cloud offers satisfies the requirement %s, so it "
|
|
521
|
+
"will not be met. %s" % (requires, _VERSION_CHOSEN_BY_CONNECT_CLOUD)
|
|
522
|
+
)
|
|
523
|
+
return None
|
|
524
|
+
return _log_requested(minor)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{rsconnect_python-1.31.0 → rsconnect_python-1.31.1}/rsconnect/quickstart/templates/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{rsconnect_python-1.31.0 → rsconnect_python-1.31.1}/rsconnect/quickstart/templates/api/app.py.tmpl
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{rsconnect_python-1.31.0 → rsconnect_python-1.31.1}/rsconnect/quickstart/templates/shiny/app.py.tmpl
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{rsconnect_python-1.31.0 → rsconnect_python-1.31.1}/rsconnect/subprocesses/inspect_environment.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|