plotly-cloud 0.1.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.
- plotly_cloud/__init__.py +3 -0
- plotly_cloud/_api_types.py +37 -0
- plotly_cloud/_changes.py +77 -0
- plotly_cloud/_cloud_env.py +93 -0
- plotly_cloud/_commands.py +880 -0
- plotly_cloud/_definitions.py +109 -0
- plotly_cloud/_deploy.py +470 -0
- plotly_cloud/_devtool_hooks.py +61 -0
- plotly_cloud/_devtool_publish_rpc.py +294 -0
- plotly_cloud/_oauth.py +335 -0
- plotly_cloud/_parser.py +171 -0
- plotly_cloud/cli.py +300 -0
- plotly_cloud/cloud-env.toml +6 -0
- plotly_cloud/cloud_devtools.css +1 -0
- plotly_cloud/cloud_devtools.js +15 -0
- plotly_cloud/exceptions.py +198 -0
- plotly_cloud-0.1.0.dist-info/METADATA +294 -0
- plotly_cloud-0.1.0.dist-info/RECORD +21 -0
- plotly_cloud-0.1.0.dist-info/WHEEL +4 -0
- plotly_cloud-0.1.0.dist-info/entry_points.txt +5 -0
- plotly_cloud-0.1.0.dist-info/licenses/LICENSE +21 -0
plotly_cloud/__init__.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""API type definitions for Plotly Cloud."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, List
|
|
4
|
+
|
|
5
|
+
from typing_extensions import NotRequired, TypedDict
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class AppRequest(TypedDict):
|
|
9
|
+
pythonVersion: NotRequired[str]
|
|
10
|
+
entrypointModule: NotRequired[str]
|
|
11
|
+
name: NotRequired[str]
|
|
12
|
+
appUrl: NotRequired[str]
|
|
13
|
+
description: NotRequired[str]
|
|
14
|
+
isViewPrivate: NotRequired[str]
|
|
15
|
+
invitations: NotRequired[List[str]]
|
|
16
|
+
environmentVariables: NotRequired[List[Dict[str, Any]]]
|
|
17
|
+
|
|
18
|
+
EnvironmentVariables = Dict[str, Any]
|
|
19
|
+
|
|
20
|
+
class ErrorResponse(TypedDict):
|
|
21
|
+
error: NotRequired[str]
|
|
22
|
+
error_description: NotRequired[str]
|
|
23
|
+
|
|
24
|
+
class App(TypedDict):
|
|
25
|
+
id: NotRequired[str]
|
|
26
|
+
author_id: NotRequired[str]
|
|
27
|
+
name: NotRequired[str]
|
|
28
|
+
description: NotRequired[str]
|
|
29
|
+
app_url: NotRequired[str]
|
|
30
|
+
is_view_private: NotRequired[bool]
|
|
31
|
+
created_with_desktop: NotRequired[bool]
|
|
32
|
+
desktop_app_version: NotRequired[str]
|
|
33
|
+
environment_variables: NotRequired[EnvironmentVariables]
|
|
34
|
+
invitations: NotRequired[List[str]]
|
|
35
|
+
status: NotRequired[str]
|
|
36
|
+
created_at: NotRequired[str]
|
|
37
|
+
last_published: NotRequired[str]
|
plotly_cloud/_changes.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import hashlib
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
6
|
+
|
|
7
|
+
_builtins_modules = list(sys.builtin_module_names) + ["frozen", "builtin"]
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
if hasattr(sys, "stdlib_module_names"):
|
|
11
|
+
_builtins_modules += list(sys.stdlib_module_names) # type: ignore
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def collect_module_files(*extras):
|
|
15
|
+
collected = []
|
|
16
|
+
for mod in sys.modules.values():
|
|
17
|
+
parent = getattr(mod.__spec__, "parent", "")
|
|
18
|
+
if (
|
|
19
|
+
hasattr(mod, "__file__")
|
|
20
|
+
and mod.__file__ is not None
|
|
21
|
+
and mod.__file__.endswith(".py")
|
|
22
|
+
and parent not in _builtins_modules
|
|
23
|
+
and mod.__name__ not in _builtins_modules
|
|
24
|
+
):
|
|
25
|
+
collected.append(mod.__file__)
|
|
26
|
+
return collected + list(extras)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def hash_file_content(filename):
|
|
30
|
+
with open(filename, encoding="utf-8") as f:
|
|
31
|
+
content = f.read()
|
|
32
|
+
return hashlib.sha256(content.encode("utf-8"), usedforsecurity=False).hexdigest(), filename
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def stat_file_time(filename):
|
|
36
|
+
return os.stat(filename).st_mtime, filename
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
async def until_change(collector, *extras, initial_hashes=None):
|
|
40
|
+
# We hash ten files at a time.
|
|
41
|
+
pool = ThreadPoolExecutor(max_workers=10)
|
|
42
|
+
loop = asyncio.get_event_loop()
|
|
43
|
+
|
|
44
|
+
changed = False
|
|
45
|
+
hashed_files = {}
|
|
46
|
+
|
|
47
|
+
async def collect():
|
|
48
|
+
collected = []
|
|
49
|
+
files = collector(*extras)
|
|
50
|
+
for f in files:
|
|
51
|
+
collected.append(loop.run_in_executor(pool, stat_file_time, f))
|
|
52
|
+
|
|
53
|
+
content = await asyncio.gather(*collected)
|
|
54
|
+
result = {}
|
|
55
|
+
|
|
56
|
+
for hashed, filename in content:
|
|
57
|
+
result[filename] = hashed
|
|
58
|
+
|
|
59
|
+
return result
|
|
60
|
+
|
|
61
|
+
if initial_hashes is None or not hashed_files:
|
|
62
|
+
hashed_files = await collect()
|
|
63
|
+
|
|
64
|
+
while not changed:
|
|
65
|
+
collected = await collect()
|
|
66
|
+
|
|
67
|
+
if len(collected) != len(hashed_files):
|
|
68
|
+
return
|
|
69
|
+
|
|
70
|
+
for co_key, co_value in collected.items():
|
|
71
|
+
if co_key not in hashed_files:
|
|
72
|
+
return
|
|
73
|
+
|
|
74
|
+
if co_value != hashed_files[co_key]:
|
|
75
|
+
return
|
|
76
|
+
|
|
77
|
+
await asyncio.sleep(1.0)
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""Cloud environment configuration management for Plotly Cloud CLI."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import tomli
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
|
|
9
|
+
from .exceptions import CloudConfigError, EnvironmentError
|
|
10
|
+
|
|
11
|
+
console = Console()
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class CloudConfig:
|
|
15
|
+
"""Manages cloud configuration for Plotly Cloud CLI."""
|
|
16
|
+
|
|
17
|
+
def __init__(self):
|
|
18
|
+
self._config_cache = None
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
def config_path(self) -> Path:
|
|
22
|
+
"""Path to the cloud-env.toml configuration file."""
|
|
23
|
+
return Path(__file__).parent / "cloud-env.toml"
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def config(self) -> dict:
|
|
27
|
+
"""Load and cache cloud configuration."""
|
|
28
|
+
if self._config_cache is None:
|
|
29
|
+
if not self.config_path.exists():
|
|
30
|
+
raise CloudConfigError(
|
|
31
|
+
f"Cloud configuration not found at {self.config_path}",
|
|
32
|
+
"Configuration file is missing from the package.",
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
try:
|
|
36
|
+
with open(self.config_path, "rb") as f:
|
|
37
|
+
self._config_cache = tomli.load(f)
|
|
38
|
+
except Exception as e:
|
|
39
|
+
raise CloudConfigError("Failed to load cloud configuration", str(e)) from e
|
|
40
|
+
|
|
41
|
+
return self._config_cache
|
|
42
|
+
|
|
43
|
+
def get_oauth_client_id(self) -> str:
|
|
44
|
+
"""Get OAuth client ID from configuration."""
|
|
45
|
+
# Check environment variable first (overrides config file)
|
|
46
|
+
client_id = os.getenv("PLOTLY_OAUTH_CLIENT_ID", "")
|
|
47
|
+
if client_id:
|
|
48
|
+
return client_id
|
|
49
|
+
|
|
50
|
+
try:
|
|
51
|
+
client_id = self.config.get("oauth_client_id", "")
|
|
52
|
+
if client_id:
|
|
53
|
+
return client_id
|
|
54
|
+
except CloudConfigError:
|
|
55
|
+
pass
|
|
56
|
+
|
|
57
|
+
raise EnvironmentError("OAuth client ID not configured. Set 'oauth_client_id' in cloud-env.toml.")
|
|
58
|
+
|
|
59
|
+
def get_api_base_url(self) -> str:
|
|
60
|
+
"""Get API base URL from configuration."""
|
|
61
|
+
# Check environment variable first (overrides config file)
|
|
62
|
+
api_url = os.getenv("PLOTLY_API_BASE_URL", "")
|
|
63
|
+
if api_url:
|
|
64
|
+
return api_url
|
|
65
|
+
|
|
66
|
+
try:
|
|
67
|
+
api_url = self.config.get("api_base_url", "")
|
|
68
|
+
if api_url:
|
|
69
|
+
return api_url
|
|
70
|
+
except CloudConfigError:
|
|
71
|
+
pass
|
|
72
|
+
|
|
73
|
+
raise EnvironmentError("API base URL not configured. Set 'api_base_url' in cloud-env.toml.")
|
|
74
|
+
|
|
75
|
+
def validate(self) -> bool:
|
|
76
|
+
"""Validate cloud configuration setup."""
|
|
77
|
+
try:
|
|
78
|
+
client_id = self.get_oauth_client_id()
|
|
79
|
+
api_url = self.get_api_base_url()
|
|
80
|
+
|
|
81
|
+
if not all([client_id, api_url]):
|
|
82
|
+
console.print("✗ Incomplete cloud configuration")
|
|
83
|
+
return False
|
|
84
|
+
|
|
85
|
+
console.print("✓ Cloud configuration is valid")
|
|
86
|
+
return True
|
|
87
|
+
except (CloudConfigError, EnvironmentError) as e:
|
|
88
|
+
console.print(f"✗ Configuration error: {e}")
|
|
89
|
+
return False
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
# Global instance
|
|
93
|
+
cloud_config = CloudConfig()
|