plotly-cloud 0.2.1__tar.gz → 0.3.0__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.
- {plotly_cloud-0.2.1 → plotly_cloud-0.3.0}/PKG-INFO +1 -1
- {plotly_cloud-0.2.1 → plotly_cloud-0.3.0}/plotly_cloud/__init__.py +1 -1
- {plotly_cloud-0.2.1 → plotly_cloud-0.3.0}/plotly_cloud/_changes.py +1 -2
- {plotly_cloud-0.2.1 → plotly_cloud-0.3.0}/plotly_cloud/_commands.py +16 -16
- {plotly_cloud-0.2.1 → plotly_cloud-0.3.0}/plotly_cloud/_deploy.py +22 -5
- {plotly_cloud-0.2.1 → plotly_cloud-0.3.0}/plotly_cloud/_devtool_hooks.py +1 -1
- {plotly_cloud-0.2.1 → plotly_cloud-0.3.0}/plotly_cloud/_devtool_publish_rpc.py +1 -1
- {plotly_cloud-0.2.1 → plotly_cloud-0.3.0}/plotly_cloud/_oauth.py +8 -1
- {plotly_cloud-0.2.1 → plotly_cloud-0.3.0}/plotly_cloud/_parser.py +11 -1
- {plotly_cloud-0.2.1 → plotly_cloud-0.3.0}/plotly_cloud/cli.py +8 -0
- {plotly_cloud-0.2.1 → plotly_cloud-0.3.0}/pyproject.toml +1 -1
- {plotly_cloud-0.2.1 → plotly_cloud-0.3.0}/.gitignore +0 -0
- {plotly_cloud-0.2.1 → plotly_cloud-0.3.0}/LICENSE +0 -0
- {plotly_cloud-0.2.1 → plotly_cloud-0.3.0}/README.md +0 -0
- {plotly_cloud-0.2.1 → plotly_cloud-0.3.0}/plotly_cloud/_api_types.py +0 -0
- {plotly_cloud-0.2.1 → plotly_cloud-0.3.0}/plotly_cloud/_cloud_env.py +0 -0
- {plotly_cloud-0.2.1 → plotly_cloud-0.3.0}/plotly_cloud/_definitions.py +0 -0
- {plotly_cloud-0.2.1 → plotly_cloud-0.3.0}/plotly_cloud/_run_sync.py +0 -0
- {plotly_cloud-0.2.1 → plotly_cloud-0.3.0}/plotly_cloud/cloud-env.toml +0 -0
- {plotly_cloud-0.2.1 → plotly_cloud-0.3.0}/plotly_cloud/cloud_devtools.css +0 -0
- {plotly_cloud-0.2.1 → plotly_cloud-0.3.0}/plotly_cloud/cloud_devtools.js +0 -0
- {plotly_cloud-0.2.1 → plotly_cloud-0.3.0}/plotly_cloud/exceptions.py +0 -0
|
@@ -7,8 +7,7 @@ from concurrent.futures import ThreadPoolExecutor
|
|
|
7
7
|
_builtins_modules = list(sys.builtin_module_names) + ["frozen", "builtin"]
|
|
8
8
|
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
_builtins_modules += list(sys.stdlib_module_names) # type: ignore
|
|
10
|
+
_builtins_modules += list(getattr(sys, "stdlib_module_names", []))
|
|
12
11
|
|
|
13
12
|
|
|
14
13
|
def collect_module_files(*extras):
|
|
@@ -461,7 +461,7 @@ class PublishCommand(BaseCommand):
|
|
|
461
461
|
},
|
|
462
462
|
{
|
|
463
463
|
"name": "--poll-status",
|
|
464
|
-
"type": lambda x: x.lower() in ("true", "1", "yes", "on"),
|
|
464
|
+
"type": lambda x: x.lower() in ("true", "1", "yes", "on"),
|
|
465
465
|
"default": True,
|
|
466
466
|
"help": "Poll publishing status until completion (default: True)",
|
|
467
467
|
},
|
|
@@ -477,6 +477,11 @@ class PublishCommand(BaseCommand):
|
|
|
477
477
|
"default": 180,
|
|
478
478
|
"help": "Polling timeout in seconds",
|
|
479
479
|
},
|
|
480
|
+
{
|
|
481
|
+
"name": "--skip-size-check",
|
|
482
|
+
"action": "store_true",
|
|
483
|
+
"help": "Skip the standard 200MB project size validation (note: a hard 700MB maximum still applies)",
|
|
484
|
+
},
|
|
480
485
|
]
|
|
481
486
|
|
|
482
487
|
@classmethod
|
|
@@ -584,7 +589,7 @@ class PublishCommand(BaseCommand):
|
|
|
584
589
|
auth_task = progress.add_task("🔐 Checking authentication...", total=None)
|
|
585
590
|
|
|
586
591
|
client_id = cloud_config.get_oauth_client_id()
|
|
587
|
-
oauth_client = OAuthClient(client_id)
|
|
592
|
+
oauth_client = OAuthClient(client_id, token=getattr(args, "api_key", None))
|
|
588
593
|
|
|
589
594
|
# Check if authenticated, if not, perform login
|
|
590
595
|
if not await oauth_client.is_authenticated():
|
|
@@ -666,7 +671,8 @@ class PublishCommand(BaseCommand):
|
|
|
666
671
|
try:
|
|
667
672
|
# Create deployment zip
|
|
668
673
|
package_task = progress.add_task("Creating deployment package...", total=None)
|
|
669
|
-
|
|
674
|
+
skip_size_check = getattr(args, "skip_size_check", False)
|
|
675
|
+
zip_size = await create_deployment_zip(project_path, zip_path, skip_size_check)
|
|
670
676
|
progress.update(
|
|
671
677
|
package_task, description=f"✓ Created deployment package: {zip_size / (1024 * 1024):.1f}MB"
|
|
672
678
|
)
|
|
@@ -816,15 +822,12 @@ class StatusCommand(BaseCommand):
|
|
|
816
822
|
if not app_id:
|
|
817
823
|
raise ApplicationError("No app_id found in configuration. Publish your app first using 'plotly publish'.")
|
|
818
824
|
|
|
819
|
-
# Get OAuth client for authentication
|
|
820
825
|
client_id = cloud_config.get_oauth_client_id()
|
|
821
|
-
oauth_client = OAuthClient(client_id)
|
|
826
|
+
oauth_client = OAuthClient(client_id, token=getattr(args, "api_key", None))
|
|
822
827
|
|
|
823
|
-
# Check if authenticated
|
|
824
828
|
if not await oauth_client.is_authenticated():
|
|
825
829
|
raise ApplicationError("Not authenticated. Please run 'plotly login' first.")
|
|
826
830
|
|
|
827
|
-
# Get access token
|
|
828
831
|
auth_token = await oauth_client.get_access_token()
|
|
829
832
|
if not auth_token:
|
|
830
833
|
raise ApplicationError("Unable to retrieve access token. Please try logging in again.")
|
|
@@ -885,15 +888,12 @@ class TeamsCommand(BaseCommand):
|
|
|
885
888
|
@classmethod
|
|
886
889
|
async def execute(cls, args: ParsedArguments) -> None:
|
|
887
890
|
"""Execute teams command."""
|
|
888
|
-
# Get OAuth client for authentication
|
|
889
891
|
client_id = cloud_config.get_oauth_client_id()
|
|
890
|
-
oauth_client = OAuthClient(client_id)
|
|
892
|
+
oauth_client = OAuthClient(client_id, token=getattr(args, "api_key", None))
|
|
891
893
|
|
|
892
|
-
# Check if authenticated
|
|
893
894
|
if not await oauth_client.is_authenticated():
|
|
894
895
|
raise ApplicationError("Not authenticated. Please run 'plotly login' first.")
|
|
895
896
|
|
|
896
|
-
# Get access token
|
|
897
897
|
auth_token = await oauth_client.get_access_token()
|
|
898
898
|
if not auth_token:
|
|
899
899
|
raise ApplicationError("Unable to retrieve access token. Please try logging in again.")
|
|
@@ -944,25 +944,26 @@ class WhoamiCommand(BaseCommand):
|
|
|
944
944
|
@classmethod
|
|
945
945
|
async def execute(cls, args: ParsedArguments) -> None:
|
|
946
946
|
"""Execute the whoami command."""
|
|
947
|
+
token = getattr(args, "api_key", None)
|
|
948
|
+
if token:
|
|
949
|
+
console.print("✓ Authenticated via bearer token")
|
|
950
|
+
return
|
|
951
|
+
|
|
947
952
|
client_id = cloud_config.get_oauth_client_id()
|
|
948
953
|
oauth_client = OAuthClient(client_id)
|
|
949
954
|
|
|
950
|
-
# Check if authenticated
|
|
951
955
|
if not await oauth_client.is_authenticated():
|
|
952
956
|
console.print("✗ Not logged in")
|
|
953
957
|
return
|
|
954
958
|
|
|
955
|
-
# Load credentials to get user info
|
|
956
959
|
credentials = await oauth_client.load_credentials()
|
|
957
960
|
if not credentials:
|
|
958
961
|
console.print("✗ No credentials found")
|
|
959
962
|
return
|
|
960
963
|
|
|
961
|
-
# Try to refresh token to validate it
|
|
962
964
|
try:
|
|
963
965
|
await oauth_client.refresh_access_token()
|
|
964
966
|
|
|
965
|
-
# Extract user information from credentials
|
|
966
967
|
user_info = credentials.get("user", {})
|
|
967
968
|
email = user_info.get("email") or credentials.get("email")
|
|
968
969
|
|
|
@@ -972,6 +973,5 @@ class WhoamiCommand(BaseCommand):
|
|
|
972
973
|
console.print("✓ Logged in (no email information available)")
|
|
973
974
|
|
|
974
975
|
except (TokenError, CredentialError):
|
|
975
|
-
# Token is invalid and cannot be refreshed, clear credentials
|
|
976
976
|
await oauth_client.logout()
|
|
977
977
|
console.print("✗ Invalid token - credentials cleared")
|
|
@@ -30,8 +30,9 @@ from .exceptions import (
|
|
|
30
30
|
PlotlyCloudError,
|
|
31
31
|
)
|
|
32
32
|
|
|
33
|
-
# Maximum allowed zip file size (200MB)
|
|
33
|
+
# Maximum allowed zip file size (200MB default, 700MB absolute max)
|
|
34
34
|
MAX_ZIP_SIZE = 200 * 1024 * 1024
|
|
35
|
+
ABSOLUTE_MAX_ZIP_SIZE = 700 * 1024 * 1024
|
|
35
36
|
|
|
36
37
|
|
|
37
38
|
def parse_gitignore(project_path: str) -> set[str]:
|
|
@@ -111,19 +112,23 @@ def should_exclude_path(path: str, exclude_patterns: set[str]) -> bool:
|
|
|
111
112
|
return False
|
|
112
113
|
|
|
113
114
|
|
|
114
|
-
async def create_deployment_zip(
|
|
115
|
+
async def create_deployment_zip(
|
|
116
|
+
project_path: str, output_path: str, skip_size_check: bool = False
|
|
117
|
+
) -> int:
|
|
115
118
|
"""
|
|
116
119
|
Create a zip file for deployment, excluding files based on .gitignore.
|
|
117
120
|
|
|
118
121
|
Args:
|
|
119
122
|
project_path: Path to the project directory
|
|
120
123
|
output_path: Path where the zip file should be created
|
|
124
|
+
skip_size_check: If True, skip the maximum size validation
|
|
121
125
|
|
|
122
126
|
Returns:
|
|
123
127
|
Size of the created zip file in bytes
|
|
124
128
|
|
|
125
129
|
Raises:
|
|
126
|
-
|
|
130
|
+
FileSizeError: If zip file exceeds size limit and skip_size_check is False
|
|
131
|
+
FileSystemError: If the .gitignore file cannot be read
|
|
127
132
|
"""
|
|
128
133
|
exclude_patterns = parse_gitignore(project_path)
|
|
129
134
|
total_uncompressed_size = 0
|
|
@@ -165,7 +170,18 @@ async def create_deployment_zip(project_path: str, output_path: str) -> int:
|
|
|
165
170
|
# Check uncompressed size only
|
|
166
171
|
zip_size = os.path.getsize(output_path)
|
|
167
172
|
|
|
168
|
-
|
|
173
|
+
# Always enforce absolute maximum (700MB)
|
|
174
|
+
if total_uncompressed_size > ABSOLUTE_MAX_ZIP_SIZE:
|
|
175
|
+
os.remove(output_path) # Clean up the oversized zip
|
|
176
|
+
raise FileSizeError(
|
|
177
|
+
f"This directory exceeds {ABSOLUTE_MAX_ZIP_SIZE / (1024 * 1024):.0f}MB and couldn't be published",
|
|
178
|
+
f"Total size: {total_uncompressed_size / (1024 * 1024):.1f}MB. "
|
|
179
|
+
f"Maximum allowed: {ABSOLUTE_MAX_ZIP_SIZE / (1024 * 1024):.0f}MB. "
|
|
180
|
+
"Consider excluding large files in your .gitignore.",
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
# Check default limit (200MB) unless skipped
|
|
184
|
+
if not skip_size_check and total_uncompressed_size > MAX_ZIP_SIZE:
|
|
169
185
|
os.remove(output_path) # Clean up the oversized zip
|
|
170
186
|
raise FileSizeError(
|
|
171
187
|
f"This directory exceeds {MAX_ZIP_SIZE / (1024 * 1024):.0f}MB and couldn't be published",
|
|
@@ -247,7 +263,8 @@ def _handle_error_response(response: Optional[httpx.Response], operation: str) -
|
|
|
247
263
|
|
|
248
264
|
# Handle 403 Forbidden specifically
|
|
249
265
|
if response.status_code == 403:
|
|
250
|
-
|
|
266
|
+
details = response.text.strip()[:200] if response.text else ""
|
|
267
|
+
return ForbiddenError(f"Failed to {operation}: Access forbidden", details=details)
|
|
251
268
|
|
|
252
269
|
# Parse error based on content type
|
|
253
270
|
content_type = response.headers.get("content-type", "").lower()
|
|
@@ -226,7 +226,7 @@ class PlotlyCloudPublishRPC:
|
|
|
226
226
|
device_code = data.get("device_code")
|
|
227
227
|
status_code, response = await self.oauth_client.check_authentication_status(device_code)
|
|
228
228
|
if status_code == 200:
|
|
229
|
-
await self.oauth_client._save_credentials(
|
|
229
|
+
await self.oauth_client._save_credentials({**response})
|
|
230
230
|
return {"result": {"success": True}}
|
|
231
231
|
else:
|
|
232
232
|
error = response.get("error", "unknown_error")
|
|
@@ -59,8 +59,9 @@ AuthResponse = Union[AuthTokenResponse, AuthErrorResponse]
|
|
|
59
59
|
class OAuthClient:
|
|
60
60
|
"""OAuth client for WorkOS CLI Auth using device authorization flow."""
|
|
61
61
|
|
|
62
|
-
def __init__(self, client_id: str):
|
|
62
|
+
def __init__(self, client_id: str, token: Optional[str] = None):
|
|
63
63
|
self.client_id = client_id
|
|
64
|
+
self.token = token
|
|
64
65
|
self.credentials_path = self._get_credentials_path()
|
|
65
66
|
|
|
66
67
|
def _get_credentials_path(self) -> Path:
|
|
@@ -278,11 +279,15 @@ class OAuthClient:
|
|
|
278
279
|
|
|
279
280
|
async def is_authenticated(self) -> bool:
|
|
280
281
|
"""Check if user is authenticated."""
|
|
282
|
+
if self.token:
|
|
283
|
+
return True
|
|
281
284
|
credentials = await self.load_credentials()
|
|
282
285
|
return credentials is not None and "access_token" in credentials
|
|
283
286
|
|
|
284
287
|
async def get_access_token(self) -> Optional[str]:
|
|
285
288
|
"""Get current access token."""
|
|
289
|
+
if self.token:
|
|
290
|
+
return self.token
|
|
286
291
|
credentials = await self.load_credentials()
|
|
287
292
|
if credentials:
|
|
288
293
|
return credentials.get("access_token")
|
|
@@ -294,6 +299,8 @@ class OAuthClient:
|
|
|
294
299
|
Raises:
|
|
295
300
|
TokenError: If no refresh token available or refresh fails
|
|
296
301
|
"""
|
|
302
|
+
if self.token:
|
|
303
|
+
raise TokenError("Cannot refresh a static API key")
|
|
297
304
|
credentials = await self.load_credentials()
|
|
298
305
|
if not credentials or "refresh_token" not in credentials:
|
|
299
306
|
raise TokenError("No refresh token available")
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""Simple argument parser to replace argparse."""
|
|
2
2
|
|
|
3
|
+
import os
|
|
3
4
|
import sys
|
|
4
5
|
from typing import Any, List
|
|
5
6
|
|
|
@@ -78,9 +79,10 @@ def parse_args(command_arguments: List[CommandArgument], args_index=3) -> Parsed
|
|
|
78
79
|
|
|
79
80
|
result[key] = arg_spec.get("default")
|
|
80
81
|
|
|
81
|
-
# Add global
|
|
82
|
+
# Add global flags
|
|
82
83
|
result["verbose"] = False
|
|
83
84
|
result["help"] = False
|
|
85
|
+
result["api_key"] = os.getenv("PLOTLY_API_KEY", None)
|
|
84
86
|
|
|
85
87
|
# Check if the first argument is "help" and handle it specially
|
|
86
88
|
if len(args) > 0 and args[0] == "help":
|
|
@@ -103,6 +105,14 @@ def parse_args(command_arguments: List[CommandArgument], args_index=3) -> Parsed
|
|
|
103
105
|
i += 1
|
|
104
106
|
continue
|
|
105
107
|
|
|
108
|
+
if arg == "--api-key":
|
|
109
|
+
if i + 1 < len(args):
|
|
110
|
+
result["api_key"] = args[i + 1]
|
|
111
|
+
i += 2
|
|
112
|
+
else:
|
|
113
|
+
i += 1
|
|
114
|
+
continue
|
|
115
|
+
|
|
106
116
|
# Check if this is an optional argument
|
|
107
117
|
if arg.startswith("-"):
|
|
108
118
|
# Find matching optional argument specification
|
|
@@ -110,6 +110,10 @@ def print_main_help(show_banner=True) -> None:
|
|
|
110
110
|
|
|
111
111
|
# Show global options
|
|
112
112
|
global_options = [
|
|
113
|
+
{
|
|
114
|
+
"name": "--api-key",
|
|
115
|
+
"help": "API key for authentication (overrides OAuth, or set PLOTLY_API_KEY env var)",
|
|
116
|
+
},
|
|
113
117
|
{
|
|
114
118
|
"name": "--verbose, -v",
|
|
115
119
|
"help": "Enable verbose output with detailed error information",
|
|
@@ -196,6 +200,10 @@ def print_command_help(command_class: BaseCommand, group: str, command: str) ->
|
|
|
196
200
|
|
|
197
201
|
# Show global options
|
|
198
202
|
global_options = [
|
|
203
|
+
{
|
|
204
|
+
"name": "--api-key",
|
|
205
|
+
"help": "API key for authentication (overrides OAuth, or set PLOTLY_API_KEY env var)",
|
|
206
|
+
},
|
|
199
207
|
{
|
|
200
208
|
"name": "--verbose, -v",
|
|
201
209
|
"help": "Enable verbose output with detailed error information",
|
|
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
|