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
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"cells": [
|
|
3
|
+
{
|
|
4
|
+
"cell_type": "markdown",
|
|
5
|
+
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
|
|
6
|
+
"metadata": {},
|
|
7
|
+
"source": [
|
|
8
|
+
"# Hello from $name"
|
|
9
|
+
]
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"cell_type": "code",
|
|
13
|
+
"id": "b2c3d4e5-f6a7-8901-bcde-f23456789012",
|
|
14
|
+
"execution_count": null,
|
|
15
|
+
"metadata": {},
|
|
16
|
+
"outputs": [],
|
|
17
|
+
"source": [
|
|
18
|
+
"print('Hello from $name!')"
|
|
19
|
+
]
|
|
20
|
+
}
|
|
21
|
+
],
|
|
22
|
+
"metadata": {
|
|
23
|
+
"kernelspec": {
|
|
24
|
+
"display_name": "Python 3",
|
|
25
|
+
"language": "python",
|
|
26
|
+
"name": "python3"
|
|
27
|
+
},
|
|
28
|
+
"language_info": {
|
|
29
|
+
"name": "python"
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"nbformat": 4,
|
|
33
|
+
"nbformat_minor": 5
|
|
34
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "$name"
|
|
3
|
+
version = "0.0.1"
|
|
4
|
+
requires-python = "$requires_python"
|
|
5
|
+
dependencies = [
|
|
6
|
+
"jupyter",
|
|
7
|
+
]
|
|
8
|
+
|
|
9
|
+
[tool.rsconnect]
|
|
10
|
+
app_mode = "jupyter-static"
|
|
11
|
+
entrypoint = "notebook.ipynb"
|
|
12
|
+
title = "$name"
|
|
13
|
+
requirements_file = "uv.lock"
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# $name
|
|
2
|
+
|
|
3
|
+
A Posit Connect project scaffolded by `rsconnect quickstart`.
|
|
4
|
+
|
|
5
|
+
## Run locally
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
uv run quarto preview report.qmd
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Deploy to Posit Connect
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
rsconnect deploy pyproject .
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Notes
|
|
18
|
+
|
|
19
|
+
- Quarto must be installed separately: https://quarto.org
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "$name"
|
|
3
|
+
version = "0.0.1"
|
|
4
|
+
requires-python = "$requires_python"
|
|
5
|
+
dependencies = [
|
|
6
|
+
"voila",
|
|
7
|
+
"jupyter",
|
|
8
|
+
]
|
|
9
|
+
|
|
10
|
+
[tool.rsconnect]
|
|
11
|
+
app_mode = "jupyter-voila"
|
|
12
|
+
entrypoint = "notebook.ipynb"
|
|
13
|
+
title = "$name"
|
|
14
|
+
requirements_file = "uv.lock"
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# The contents of this file are copied from:
|
|
2
|
+
# https://github.com/posit-dev/py-shiny/blob/feb4cb7f872922717c39753514ae2d7fa32f10a1/shiny/express/_is_express.py
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import ast
|
|
7
|
+
import re
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Literal, cast
|
|
11
|
+
|
|
12
|
+
__all__ = ("is_express_app",)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def is_express_app(app: str, app_dir: str | None) -> bool:
|
|
16
|
+
"""Detect whether an app file is a Shiny express app
|
|
17
|
+
|
|
18
|
+
Parameters
|
|
19
|
+
----------
|
|
20
|
+
app
|
|
21
|
+
App filename, like "app.py". It may be a relative path or absolute path.
|
|
22
|
+
app_dir
|
|
23
|
+
Directory containing the app file. If this is `None`, then `app` must be an
|
|
24
|
+
absolute path.
|
|
25
|
+
|
|
26
|
+
Returns
|
|
27
|
+
-------
|
|
28
|
+
:
|
|
29
|
+
`True` if it is a Shiny express app, `False` otherwise.
|
|
30
|
+
"""
|
|
31
|
+
if not app.lower().endswith(".py"):
|
|
32
|
+
return False
|
|
33
|
+
|
|
34
|
+
if app_dir is not None:
|
|
35
|
+
app_path = Path(app_dir) / app
|
|
36
|
+
else:
|
|
37
|
+
app_path = Path(app)
|
|
38
|
+
|
|
39
|
+
if not app_path.exists():
|
|
40
|
+
return False
|
|
41
|
+
|
|
42
|
+
try:
|
|
43
|
+
# Read the file, parse it, and look for any imports of shiny.express.
|
|
44
|
+
with open(app_path, encoding="utf-8") as f:
|
|
45
|
+
content = f.read()
|
|
46
|
+
|
|
47
|
+
# Check for magic comment in the first 1000 characters
|
|
48
|
+
forced_mode = find_magic_comment_mode(content[:1000])
|
|
49
|
+
if forced_mode == "express":
|
|
50
|
+
return True
|
|
51
|
+
elif forced_mode == "core":
|
|
52
|
+
return False
|
|
53
|
+
|
|
54
|
+
tree = ast.parse(content, app_path)
|
|
55
|
+
detector = DetectShinyExpressVisitor()
|
|
56
|
+
detector.visit(tree)
|
|
57
|
+
|
|
58
|
+
except Exception:
|
|
59
|
+
return False
|
|
60
|
+
|
|
61
|
+
return detector.found_shiny_express_import
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class DetectShinyExpressVisitor(ast.NodeVisitor):
|
|
65
|
+
def __init__(self):
|
|
66
|
+
super().__init__()
|
|
67
|
+
self.found_shiny_express_import = False
|
|
68
|
+
|
|
69
|
+
def visit_Import(self, node: ast.Import) -> None:
|
|
70
|
+
if any(alias.name == "shiny.express" for alias in node.names):
|
|
71
|
+
self.found_shiny_express_import = True
|
|
72
|
+
|
|
73
|
+
def visit_ImportFrom(self, node: ast.ImportFrom) -> None:
|
|
74
|
+
if node.module == "shiny.express":
|
|
75
|
+
self.found_shiny_express_import = True
|
|
76
|
+
elif node.module == "shiny" and any(alias.name == "express" for alias in node.names):
|
|
77
|
+
self.found_shiny_express_import = True
|
|
78
|
+
|
|
79
|
+
# Visit top-level nodes.
|
|
80
|
+
def visit_Module(self, node: ast.Module) -> None:
|
|
81
|
+
super().generic_visit(node)
|
|
82
|
+
|
|
83
|
+
# Don't recurse into any nodes, so the we'll only ever look at top-level nodes.
|
|
84
|
+
def generic_visit(self, node: ast.AST) -> None:
|
|
85
|
+
pass
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def find_magic_comment_mode(content: str) -> Literal["core", "express"] | None:
|
|
89
|
+
"""
|
|
90
|
+
Look for a magic comment of the form "# shiny_mode: express" or "# shiny_mode:
|
|
91
|
+
core".
|
|
92
|
+
|
|
93
|
+
If a line of the form "# shiny_mode: x" is found, where "x" is not "express" or
|
|
94
|
+
"core", then a message will be printed to stderr.
|
|
95
|
+
|
|
96
|
+
Returns
|
|
97
|
+
-------
|
|
98
|
+
:
|
|
99
|
+
`"express"` if Shiny Express comment is found, `"core"` if Shiny Core comment is
|
|
100
|
+
found, and `None` if no magic comment is found.
|
|
101
|
+
"""
|
|
102
|
+
m = re.search(r"^#[ \t]*shiny_mode:[ \t]*(\S*)[ \t]*$", content, re.MULTILINE)
|
|
103
|
+
if m is not None:
|
|
104
|
+
shiny_mode = cast(str, m.group(1))
|
|
105
|
+
if shiny_mode in ("express", "core"):
|
|
106
|
+
# The "type: ignore" is needed for mypy, which is used on some projects that
|
|
107
|
+
# use duplicates of this code.
|
|
108
|
+
return shiny_mode # type: ignore
|
|
109
|
+
else:
|
|
110
|
+
print(f'Invalid shiny_mode: "{shiny_mode}"', file=sys.stderr)
|
|
111
|
+
|
|
112
|
+
return None
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def escape_to_var_name(x: str) -> str:
|
|
116
|
+
"""
|
|
117
|
+
Given a string, escape it to a valid Python variable name which contains
|
|
118
|
+
[a-zA-Z0-9_]. All other characters will be escaped to _<hex>_. Also, if the first
|
|
119
|
+
character is a digit, it will be escaped to _<hex>_, because Python variable names
|
|
120
|
+
can't begin with a digit.
|
|
121
|
+
"""
|
|
122
|
+
encoded = ""
|
|
123
|
+
is_first = True
|
|
124
|
+
|
|
125
|
+
for char in x:
|
|
126
|
+
if is_first and re.match("[0-9]", char):
|
|
127
|
+
encoded += f"_{ord(char):x}_"
|
|
128
|
+
elif re.match("[a-zA-Z0-9]", char):
|
|
129
|
+
encoded += char
|
|
130
|
+
else:
|
|
131
|
+
encoded += f"_{ord(char):x}_"
|
|
132
|
+
|
|
133
|
+
if is_first:
|
|
134
|
+
is_first = False
|
|
135
|
+
|
|
136
|
+
return encoded
|
rsconnect/snowflake.py
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# pyright: reportMissingTypeStubs=false, reportUnusedImport=false
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
from subprocess import CalledProcessError, CompletedProcess, run
|
|
6
|
+
from typing import Any, Dict, List, Optional
|
|
7
|
+
|
|
8
|
+
from .exception import RSConnectException
|
|
9
|
+
from .log import logger
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def snow(*args: str) -> CompletedProcess[str]:
|
|
13
|
+
ensure_snow_installed()
|
|
14
|
+
return run(["snow"] + list(args), capture_output=True, text=True, check=True)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def ensure_snow_installed() -> None:
|
|
18
|
+
try:
|
|
19
|
+
import snowflake.cli # noqa: F401
|
|
20
|
+
|
|
21
|
+
logger.debug("snowflake-cli is installed.")
|
|
22
|
+
|
|
23
|
+
except ImportError:
|
|
24
|
+
logger.warning("snowflake-cli is not installed.")
|
|
25
|
+
try:
|
|
26
|
+
run(["snow", "--version"], capture_output=True, check=True)
|
|
27
|
+
except CalledProcessError:
|
|
28
|
+
raise RSConnectException("snow is installed but could not be run.")
|
|
29
|
+
except FileNotFoundError:
|
|
30
|
+
raise RSConnectException("snow cannot be found.")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def list_connections() -> List[Dict[str, Any]]:
|
|
34
|
+
|
|
35
|
+
try:
|
|
36
|
+
res = snow("connection", "list", "--format", "json")
|
|
37
|
+
connection_list = json.loads(res.stdout)
|
|
38
|
+
return connection_list
|
|
39
|
+
except CalledProcessError:
|
|
40
|
+
raise RSConnectException("Could not list snowflake connections.")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def get_parameters(name: Optional[str] = None) -> Dict[str, Any]:
|
|
44
|
+
"""Get Snowflake connection parameters.
|
|
45
|
+
Args:
|
|
46
|
+
name: The name of the connection to retrieve. If None, returns the default connection.
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
A dictionary of connection parameters.
|
|
50
|
+
"""
|
|
51
|
+
try:
|
|
52
|
+
from snowflake.connector.config_manager import CONFIG_MANAGER
|
|
53
|
+
except ImportError:
|
|
54
|
+
raise RSConnectException("snowflake-cli is not installed.")
|
|
55
|
+
try:
|
|
56
|
+
connections = CONFIG_MANAGER["connections"]
|
|
57
|
+
if not isinstance(connections, dict):
|
|
58
|
+
raise TypeError("connections is not a dictionary")
|
|
59
|
+
|
|
60
|
+
if name is None:
|
|
61
|
+
def_connection_name = CONFIG_MANAGER["default_connection_name"]
|
|
62
|
+
if not isinstance(def_connection_name, str):
|
|
63
|
+
raise TypeError("default_connection_name is not a string")
|
|
64
|
+
params = connections[def_connection_name]
|
|
65
|
+
else:
|
|
66
|
+
params = connections[name]
|
|
67
|
+
|
|
68
|
+
if not isinstance(params, dict):
|
|
69
|
+
raise TypeError("connection parameters is not a dictionary")
|
|
70
|
+
|
|
71
|
+
return {str(k): v for k, v in params.items()}
|
|
72
|
+
|
|
73
|
+
except (KeyError, AttributeError) as e:
|
|
74
|
+
raise RSConnectException(f"Could not get Snowflake connection: {e}")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def generate_jwt(name: Optional[str] = None) -> str:
|
|
78
|
+
|
|
79
|
+
_ = get_parameters(name)
|
|
80
|
+
connection_name = "" if name is None else name
|
|
81
|
+
|
|
82
|
+
try:
|
|
83
|
+
res = snow("connection", "generate-jwt", "--connection", connection_name, "--format", "json")
|
|
84
|
+
try:
|
|
85
|
+
output = json.loads(res.stdout)
|
|
86
|
+
except json.JSONDecodeError:
|
|
87
|
+
raise RSConnectException(f"Failed to parse JSON from snow-cli: {res.stdout}")
|
|
88
|
+
jwt = output.get("message")
|
|
89
|
+
if jwt is None:
|
|
90
|
+
raise RSConnectException(f"Failed to generate JWT: Missing 'message' field in response: {output}")
|
|
91
|
+
return jwt
|
|
92
|
+
except CalledProcessError as e:
|
|
93
|
+
raise RSConnectException(f"Failed to generate JWT for connection '{name}': {e.stderr}")
|
|
File without changes
|