plotly-cloud 0.1.0rc1__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/cli.py ADDED
@@ -0,0 +1,286 @@
1
+ """Main CLI entry point for Plotly Cloud."""
2
+
3
+ import asyncio
4
+ import sys
5
+ import textwrap
6
+ from collections.abc import Sequence
7
+ from typing import Union
8
+
9
+ import nest_asyncio
10
+ from rich.console import Console
11
+ from rich.panel import Panel
12
+
13
+ from plotly_cloud._commands import BaseCommand, CommandRegistry
14
+ from plotly_cloud._definitions import CommandArgument, HelpPanelStyle
15
+ from plotly_cloud._parser import parse_args, parse_group_and_command
16
+ from plotly_cloud.exceptions import PlotlyCloudError
17
+
18
+ console = Console()
19
+
20
+
21
+ def create_help_panel(arguments: Sequence[Union[CommandArgument, dict]], title: str, style: HelpPanelStyle) -> None:
22
+ """Create a Rich panel for command arguments with proper styling and layout."""
23
+ formatted_lines = []
24
+
25
+ for arg in arguments:
26
+ # Format the argument name with padding and color
27
+ arg_name = arg["name"].ljust(style["argument_width"])
28
+ formatted_arg = f"[{style['argument_color']}]{arg_name}[/{style['argument_color']}]"
29
+
30
+ # Format description with color and wrap to terminal width
31
+ max_width = console.width - style["argument_width"] - 4 # Account for padding and spacing
32
+ wrapper = textwrap.TextWrapper(width=max_width)
33
+ wrapped_desc = wrapper.fill(arg["help"])
34
+ description = f"[{style['description_color']}]{wrapped_desc}[/{style['description_color']}]"
35
+
36
+ # Combine argument and description on same line
37
+ arg_line = f"{formatted_arg} {description}"
38
+ formatted_lines.append(arg_line)
39
+
40
+ arg_default = arg.get("default")
41
+
42
+ # Add default value on separate line if present
43
+ if arg_default is not None:
44
+ default_text = (
45
+ f"[{style['description_color']}]{''.ljust(style['argument_width'])} "
46
+ f"(default: {arg_default})[/{style['description_color']}]"
47
+ )
48
+ formatted_lines.append(default_text)
49
+
50
+ content = "\n".join(formatted_lines)
51
+ panel = Panel(
52
+ content,
53
+ title=title,
54
+ title_align="left",
55
+ border_style=style["border_style"],
56
+ padding=(0, 1),
57
+ )
58
+ console.print(panel)
59
+
60
+
61
+ def create_error_panel(content, title="✗ Error"):
62
+ console.print()
63
+ panel = Panel(
64
+ content,
65
+ title=title,
66
+ title_align="left",
67
+ border_style="red",
68
+ padding=(0, 1),
69
+ )
70
+ console.print(panel)
71
+
72
+
73
+ def print_main_help(show_banner=True) -> None:
74
+ """Print main help with ASCII art and command groups."""
75
+ # Add ASCII art for main command
76
+ ascii_art = """[blue]
77
+ ██████╗ ██╗ ██████╗ ████████╗██╗ ██╗ ██╗
78
+ ██╔══██╗██║ ██╔═══██╗╚══██╔══╝██║ ╚██╗ ██╔╝
79
+ ██████╔╝██║ ██║ ██║ ██║ ██║ ╚████╔╝
80
+ ██╔═══╝ ██║ ██║ ██║ ██║ ██║ ╚██╔╝
81
+ ██║ ███████╗╚██████╔╝ ██║ ███████╗██║
82
+ ╚═╝ ╚══════╝ ╚═════╝ ╚═╝ ╚══════╝╚═╝[/blue]
83
+ [cyan] 📊 Interactive Data Visualization Platform 📈[/cyan]
84
+
85
+ [dim]Publish and manage your Dash applications with ease[/dim]
86
+
87
+ """
88
+ if show_banner:
89
+ console.print(ascii_art)
90
+
91
+ # Show command groups
92
+ for group_name, group in CommandRegistry.commands.items():
93
+ commands = []
94
+ for cmd_name, cmd_class in group["commands"].items():
95
+ commands.append({"name": f"{group_name} {cmd_name}", "help": cmd_class.short_description})
96
+
97
+ commands_style: HelpPanelStyle = {
98
+ "border_style": "dim blue",
99
+ "argument_color": "yellow",
100
+ "description_color": "dim",
101
+ "argument_width": 32,
102
+ }
103
+
104
+ panel_title = f"{group_name.upper()} COMMANDS"
105
+ create_help_panel(commands, panel_title, commands_style)
106
+
107
+ # Show global options
108
+ global_options = [
109
+ {
110
+ "name": "--verbose, -v",
111
+ "help": "Enable verbose output with detailed error information",
112
+ },
113
+ {"name": "--help, -h", "help": "Show this help message and exit"},
114
+ ]
115
+
116
+ options_style: HelpPanelStyle = {
117
+ "border_style": "dim white",
118
+ "argument_color": "yellow",
119
+ "description_color": "dim",
120
+ "argument_width": 32,
121
+ }
122
+
123
+ create_help_panel(global_options, "GLOBAL OPTIONS", options_style)
124
+
125
+
126
+ def print_group_help(group_name: str) -> None:
127
+ """Print help for a specific group."""
128
+ group = CommandRegistry.commands.get(group_name)
129
+ if not group:
130
+ create_error_panel(
131
+ f"Unknown command group: [yellow]'{group_name}[/yellow]'\n\n"
132
+ f"Available groups: [cyan]{', '.join(CommandRegistry.commands.keys())}[/cyan]"
133
+ )
134
+ sys.exit(1)
135
+
136
+ # Type guard to ensure group is not None
137
+ assert group is not None
138
+
139
+ # Create commands list for the panel
140
+ commands = []
141
+ for cmd_name, cmd_class in group["commands"].items():
142
+ commands.append({"name": f"{group_name} {cmd_name}", "help": cmd_class.short_description})
143
+
144
+ # Define style for group help
145
+ group_style: HelpPanelStyle = {
146
+ "border_style": "dim blue",
147
+ "argument_color": "yellow",
148
+ "description_color": "dim",
149
+ "argument_width": 32,
150
+ }
151
+
152
+ panel_title = f"{group_name.upper()} COMMANDS"
153
+ create_help_panel(commands, panel_title, group_style)
154
+
155
+ console.print(f"\nUse 'plotly {group_name} <command> --help' for more information about a command.")
156
+
157
+
158
+ def print_command_help(command_class: BaseCommand, group: str, command: str) -> None:
159
+ """Print help for a specific command."""
160
+ console.print(f"\n[bold blue]plotly {group} {command}[/bold blue]\n")
161
+
162
+ if command_class.description:
163
+ console.print(f"{command_class.description}\n")
164
+
165
+ # Use the PlotlyHelpFormatter logic for command arguments
166
+ if command_class.arguments:
167
+ # Define styles
168
+ args_style: HelpPanelStyle = {
169
+ "border_style": "dim blue",
170
+ "argument_color": "yellow",
171
+ "description_color": "dim",
172
+ "argument_width": 32,
173
+ }
174
+
175
+ options_style: HelpPanelStyle = {
176
+ "border_style": "dim white",
177
+ "argument_color": "yellow",
178
+ "description_color": "dim",
179
+ "argument_width": 32,
180
+ }
181
+
182
+ # Separate positional and optional arguments
183
+ positional_args = [arg for arg in command_class.arguments if not arg["name"].startswith("-")]
184
+ optional_args = [arg for arg in command_class.arguments if arg["name"].startswith("-")]
185
+
186
+ # Create panels for arguments
187
+ if positional_args:
188
+ create_help_panel(positional_args, "ARGUMENTS", args_style)
189
+
190
+ if optional_args:
191
+ create_help_panel(optional_args, "OPTIONS", options_style)
192
+
193
+ # Show global options
194
+ global_options = [
195
+ {
196
+ "name": "--verbose, -v",
197
+ "help": "Enable verbose output with detailed error information",
198
+ },
199
+ {"name": "--help, -h", "help": "Show this help message and exit"},
200
+ ]
201
+
202
+ options_style: HelpPanelStyle = {
203
+ "border_style": "dim white",
204
+ "argument_color": "yellow",
205
+ "description_color": "dim",
206
+ "argument_width": 32,
207
+ }
208
+
209
+ create_help_panel(global_options, "GLOBAL OPTIONS", options_style)
210
+
211
+
212
+ def main() -> None:
213
+ """Main CLI entry point."""
214
+ # Allow to run multiple calls to asyncio run.
215
+ nest_asyncio.apply()
216
+
217
+ # Parse group and command
218
+ group, command, args_index = parse_group_and_command()
219
+
220
+ # Handle help command
221
+ if group == "help" or command == "help":
222
+ if group == "help" and command == "help":
223
+ # Show main help
224
+ print_main_help()
225
+ sys.exit(0)
226
+ elif command == "help":
227
+ # Show group help
228
+ print_group_help(group)
229
+ sys.exit(0)
230
+
231
+ try:
232
+ # Find the command group
233
+ group_data = CommandRegistry.commands.get(group)
234
+
235
+ # Type guard to ensure group_data is not None
236
+ assert group_data is not None
237
+
238
+ # Find the command in the group
239
+ command_class = group_data["commands"].get(command)
240
+ if not command_class:
241
+ create_error_panel(
242
+ f"Unknown command: {group} {command}\n\n"
243
+ f"Available commands in [yellow]'{group}'[/yellow]: "
244
+ f"[cyan]{', '.join(group_data['commands'].keys())}[/cyan]"
245
+ )
246
+ sys.exit(1)
247
+
248
+ # Type guard to ensure command_class is not None
249
+ assert command_class is not None
250
+
251
+ # Parse arguments specific to this command
252
+ command_args = parse_args(command_class.arguments)
253
+
254
+ # Handle command help
255
+ if command_args.help:
256
+ print_command_help(command_class, group, command)
257
+ sys.exit(0)
258
+
259
+ # Execute the command
260
+ asyncio.run(command_class.execute(command_args))
261
+
262
+ except KeyboardInterrupt:
263
+ sys.exit(1)
264
+ except PlotlyCloudError as e:
265
+ # Display custom exceptions in a red panel with class name as title
266
+ args = parse_args([], args_index)
267
+ if args.verbose:
268
+ console.print_exception()
269
+ else:
270
+ content = str(e)
271
+ if e.details:
272
+ content += f"\n\n{e.details}"
273
+ create_error_panel(content, f"✗ {e.__class__.__name__}")
274
+ sys.exit(1)
275
+ except Exception as e:
276
+ # Fallback for unexpected exceptions
277
+ args = parse_args([], args_index)
278
+ if args.verbose:
279
+ console.print_exception()
280
+ else:
281
+ create_error_panel(str(e), "✗ Unexpected Error")
282
+ sys.exit(1)
283
+
284
+
285
+ if __name__ == "__main__":
286
+ main()
@@ -0,0 +1,6 @@
1
+ # Plotly Cloud CLI Configuration
2
+ # This file contains the configuration for Plotly Cloud.
3
+ # Edit this file to configure your OAuth client ID and API endpoint.
4
+
5
+ api_base_url = "cloud.plotly.com"
6
+ oauth_client_id = "client_01JBHED1ZCBXX7811MDKFJ74SG"
@@ -0,0 +1,198 @@
1
+ """Custom exceptions for Plotly Cloud CLI."""
2
+
3
+ from typing import Optional
4
+
5
+
6
+ class PlotlyCloudError(Exception):
7
+ """Base exception for all Plotly Cloud CLI errors."""
8
+
9
+ def __init__(self, message: str, details: Optional[str] = None):
10
+ """Initialize exception with message and optional details."""
11
+ super().__init__(message)
12
+ self.message = message
13
+ self.details = details
14
+
15
+ def __str__(self) -> str:
16
+ """Return formatted error message."""
17
+ if self.details:
18
+ return f"{self.message}: {self.details}"
19
+ return self.message
20
+
21
+
22
+ class AuthenticationError(PlotlyCloudError):
23
+ """Raised when authentication fails."""
24
+
25
+ pass
26
+
27
+
28
+ class ConfigurationError(PlotlyCloudError):
29
+ """Raised when configuration is invalid or missing."""
30
+
31
+ pass
32
+
33
+
34
+ class DeploymentError(PlotlyCloudError):
35
+ """Raised when deployment operations fail."""
36
+
37
+ pass
38
+
39
+
40
+ class ApplicationError(PlotlyCloudError):
41
+ """Raised when application operations fail."""
42
+
43
+ pass
44
+
45
+
46
+ # Authentication-specific exceptions
47
+ class OAuthClientError(AuthenticationError):
48
+ """Raised when OAuth client operations fail."""
49
+
50
+ pass
51
+
52
+
53
+ class TokenError(AuthenticationError):
54
+ """Raised when token operations fail."""
55
+
56
+ pass
57
+
58
+
59
+ class CredentialError(AuthenticationError):
60
+ """Raised when credential operations fail."""
61
+
62
+ pass
63
+
64
+
65
+ class ForbiddenError(AuthenticationError):
66
+ """Raised when access is forbidden (HTTP 403)."""
67
+
68
+ pass
69
+
70
+
71
+ # Configuration-specific exceptions
72
+ class CloudConfigError(ConfigurationError):
73
+ """Raised when cloud configuration operations fail."""
74
+
75
+ pass
76
+
77
+
78
+ class EnvironmentError(ConfigurationError):
79
+ """Raised when environment configuration is invalid."""
80
+
81
+ pass
82
+
83
+
84
+ # Deployment-specific exceptions
85
+ class DependencyValidationError(DeploymentError):
86
+ """Raised when dependency validation fails."""
87
+
88
+ pass
89
+
90
+
91
+ class PackagingError(DeploymentError):
92
+ """Raised when packaging operations fail."""
93
+
94
+ pass
95
+
96
+
97
+ class UploadError(DeploymentError):
98
+ """Raised when upload operations fail."""
99
+
100
+ pass
101
+
102
+
103
+ class AppCreationError(DeploymentError):
104
+ """Raised when app creation fails."""
105
+
106
+ pass
107
+
108
+
109
+ class AppPublishError(DeploymentError):
110
+ """Raised when app publishing fails."""
111
+
112
+ pass
113
+
114
+
115
+ class DeploymentClientError(DeploymentError):
116
+ """Raised when deployment client operations fail."""
117
+
118
+ pass
119
+
120
+
121
+ # Application-specific exceptions
122
+ class ModuleImportError(ApplicationError):
123
+ """Raised when module import fails."""
124
+
125
+ pass
126
+
127
+
128
+ class DashAppError(ApplicationError):
129
+ """Raised when Dash app operations fail."""
130
+
131
+ pass
132
+
133
+
134
+ class ServerError(ApplicationError):
135
+ """Raised when server operations fail."""
136
+
137
+ pass
138
+
139
+
140
+ # Network-specific exceptions
141
+ class APIError(PlotlyCloudError):
142
+ """Raised when API operations fail."""
143
+
144
+ def __init__(
145
+ self,
146
+ message: str,
147
+ status_code: Optional[int] = None,
148
+ details: Optional[str] = None,
149
+ ):
150
+ """Initialize API error with optional status code."""
151
+ super().__init__(message, details)
152
+ self.status_code = status_code
153
+
154
+ def __str__(self) -> str:
155
+ """Return formatted error message with status code."""
156
+ msg = self.message
157
+ if self.status_code:
158
+ msg = f"{msg} (HTTP {self.status_code})"
159
+ if self.details:
160
+ msg = f"{msg}: {self.details}"
161
+ return msg
162
+
163
+
164
+ class NetworkError(PlotlyCloudError):
165
+ """Raised when network operations fail."""
166
+
167
+ pass
168
+
169
+
170
+ class TimeoutError(PlotlyCloudError):
171
+ """Raised when operations timeout."""
172
+
173
+ pass
174
+
175
+
176
+ # File system exceptions
177
+ class FileSystemError(PlotlyCloudError):
178
+ """Raised when file system operations fail."""
179
+
180
+ pass
181
+
182
+
183
+ class FileNotFoundError(FileSystemError):
184
+ """Raised when required files are not found."""
185
+
186
+ pass
187
+
188
+
189
+ class FilePermissionError(FileSystemError):
190
+ """Raised when file permission operations fail."""
191
+
192
+ pass
193
+
194
+
195
+ class FileSizeError(FileSystemError):
196
+ """Raised when file size limits are exceeded."""
197
+
198
+ pass