pythonanywhere-clis 1.0.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.
@@ -0,0 +1,151 @@
1
+ import time
2
+ import uuid
3
+
4
+ import typer
5
+
6
+ from pa_cli.api.consoles import ConsolesClient
7
+ from pa_cli.cli.utils import get_client
8
+ from pa_cli.config import Config
9
+ from pa_cli.exceptions import AuthError, NetworkError, NotFoundError
10
+
11
+ app = typer.Typer(help="Manage consoles on PythonAnywhere.")
12
+
13
+
14
+ @app.command("list")
15
+ def list_consoles():
16
+ """List all consoles."""
17
+ account, client = get_client(ConsolesClient)
18
+ consoles = client.list(account["username"])
19
+ if not consoles:
20
+ typer.echo("No consoles found.")
21
+ return
22
+ for console in consoles:
23
+ typer.echo(f"ID: {console['id']}, Name: {console['name']}")
24
+
25
+
26
+ @app.command()
27
+ def activate(
28
+ console_id: int = typer.Argument(..., help="Console ID"),
29
+ ):
30
+ """Activate a console via WebSocket (requires login)."""
31
+ try:
32
+ from pa_cli.crawler.console_crawler import ConsoleCrawler
33
+
34
+ account = Config.load(verbose=True)
35
+
36
+ if "password" not in account:
37
+ typer.echo("Password not found. Run 'pa account login' first.", err=True)
38
+ raise typer.Exit(code=1)
39
+
40
+ crawler = ConsoleCrawler(host=account.get("host", "www.pythonanywhere.com"))
41
+ crawler.login(account["username"], account["password"])
42
+ crawler.activate(account["username"], console_id)
43
+ typer.echo(f"Console {console_id} activated successfully.")
44
+ except AuthError as e:
45
+ typer.echo(f"Auth error: {e}", err=True)
46
+ raise typer.Exit(code=1)
47
+ except NetworkError as e:
48
+ typer.echo(f"Network error: {e}", err=True)
49
+ raise typer.Exit(code=1)
50
+
51
+
52
+ @app.command()
53
+ def create(
54
+ executable: str = typer.Option("bash", help="Console executable"),
55
+ ):
56
+ """Create a new console."""
57
+ account, client = get_client(ConsolesClient)
58
+ result = client.create(account["username"], executable)
59
+ typer.echo(f"Console created: id={result['id']}, executable={result['executable']}")
60
+
61
+
62
+ @app.command()
63
+ def send(
64
+ console_id: int = typer.Argument(..., help="Console ID"),
65
+ command: str = typer.Argument(..., help="Command to send"),
66
+ wait: bool = typer.Option(True, "--wait/--no-wait", "-w/-W", help="Wait for output"),
67
+ timeout: int = typer.Option(30, "--timeout", "-t", help="Max seconds to wait for output"),
68
+ ):
69
+ """Send input to a console and get output."""
70
+ account, client = get_client(ConsolesClient)
71
+
72
+ if not wait:
73
+ client.send_input(account["username"], console_id, command + "\n")
74
+ typer.echo(f"Sent to console {console_id}: {command}")
75
+ return
76
+
77
+ # Get baseline output before sending command
78
+ baseline = client.get_output(account["username"], console_id)
79
+ baseline_output = baseline.get("output", "")
80
+
81
+ # Generate unique completion marker (timestamp + random hex)
82
+ import time as _time
83
+ marker = f"__PA_CLI_DONE_{int(_time.time())}_{uuid.uuid4().hex}__"
84
+
85
+ # Send command + marker echo
86
+ client.send_input(
87
+ account["username"],
88
+ console_id,
89
+ f"{command}\necho {marker}\n",
90
+ )
91
+
92
+ # Poll for marker
93
+ elapsed = 0.0
94
+ poll_interval = 0.5
95
+ while elapsed < timeout:
96
+ time.sleep(poll_interval)
97
+ elapsed += poll_interval
98
+ result = client.get_output(account["username"], console_id)
99
+ output = result.get("output", "")
100
+
101
+ if marker in output and output != baseline_output:
102
+ # Extract new content (after baseline)
103
+ new_output = output[len(baseline_output):]
104
+ # Find and remove marker line
105
+ lines = new_output.split("\n")
106
+ user_lines = []
107
+ for line in lines:
108
+ if marker in line:
109
+ break
110
+ user_lines.append(line)
111
+ typer.echo("\n".join(user_lines).strip() or "(no output)")
112
+ return
113
+
114
+ typer.echo("Timeout waiting for command output", err=True)
115
+ raise typer.Exit(code=1)
116
+
117
+
118
+ @app.command("get-or-create")
119
+ def get_or_create(
120
+ executable: str = typer.Option("bash", "--executable", "-e", help="Console executable"),
121
+ ):
122
+ """Get an existing console or create a new one (auto-manage lifecycle)."""
123
+ try:
124
+ from pa_cli.crawler.console_crawler import ConsoleCrawler
125
+
126
+ account = Config.load(verbose=True)
127
+
128
+ if "password" not in account:
129
+ typer.echo("Password not found. Run 'pa account login' first.", err=True)
130
+ raise typer.Exit(code=1)
131
+
132
+ crawler = ConsoleCrawler(host=account.get("host", "www.pythonanywhere.com"))
133
+ crawler.login(account["username"], account["password"])
134
+ console_id = crawler.get_or_create(account["username"], executable=executable)
135
+ typer.echo(f"Console ready: {console_id}")
136
+ except AuthError as e:
137
+ typer.echo(f"Auth error: {e}", err=True)
138
+ raise typer.Exit(code=1)
139
+ except NetworkError as e:
140
+ typer.echo(f"Network error: {e}", err=True)
141
+ raise typer.Exit(code=1)
142
+
143
+
144
+ @app.command()
145
+ def kill(
146
+ console_id: int = typer.Argument(..., help="Console ID"),
147
+ ):
148
+ """Kill a console."""
149
+ account, client = get_client(ConsolesClient)
150
+ client.kill(account["username"], console_id)
151
+ typer.echo(f"Console {console_id} killed.")
@@ -0,0 +1,44 @@
1
+ import typer
2
+
3
+ from pa_cli.config import Config
4
+ from pa_cli.exceptions import PACliError, APIError, NetworkError
5
+ from pa_cli.workflows.deploy import deploy as deploy_workflow
6
+
7
+ app = typer.Typer(help="Deploy a local project to PythonAnywhere.")
8
+
9
+
10
+ @app.command()
11
+ def deploy(
12
+ local_dir: str = typer.Argument(..., help="Local project directory"),
13
+ domain: str = typer.Option(None, "--domain", "-d", help="Domain name (default: {username}.pythonanywhere.com)"),
14
+ python_version: str = typer.Option("python310", "--python", "-p", help="Python version"),
15
+ dry_run: bool = typer.Option(False, "--dry-run", "-n", help="Preview deploy without executing"),
16
+ ):
17
+ """Deploy a local project to PythonAnywhere."""
18
+ try:
19
+ account = Config.load(verbose=True)
20
+
21
+ if domain is None:
22
+ domain = f"{account['username']}.pythonanywhere.com"
23
+
24
+ url = deploy_workflow(
25
+ local_dir=local_dir,
26
+ username=account["username"],
27
+ token=account["token"],
28
+ host=account["host"],
29
+ domain=domain,
30
+ python_version=python_version,
31
+ dry_run=dry_run,
32
+ )
33
+
34
+ if not dry_run:
35
+ typer.echo(f"\nDeployed! Visit: {url}")
36
+ except NetworkError as e:
37
+ typer.echo(f"Network error: {e}", err=True)
38
+ raise typer.Exit(code=1)
39
+ except APIError as e:
40
+ typer.echo(f"API error: {e}", err=True)
41
+ raise typer.Exit(code=1)
42
+ except PACliError as e:
43
+ typer.echo(f"Deploy failed: {e}", err=True)
44
+ raise typer.Exit(code=1)
@@ -0,0 +1,285 @@
1
+ from pathlib import Path
2
+
3
+ import typer
4
+
5
+ from pa_cli.api.files import FilesClient
6
+ from pa_cli.cli.utils import get_client, fix_remote_path
7
+ from pa_cli.exceptions import APIError, NetworkError, NotFoundError
8
+
9
+ app = typer.Typer(help="Manage files on PythonAnywhere.")
10
+
11
+
12
+ @app.callback()
13
+ def main():
14
+ """Manage files on PythonAnywhere."""
15
+ pass
16
+
17
+
18
+ def _resolve_path(path: str | None, username: str) -> str:
19
+ """Resolve remote path. Relative paths are under /home/{username}/."""
20
+ if not path:
21
+ return f"/home/{username}/"
22
+ path = fix_remote_path(path)
23
+ if path.startswith("/"):
24
+ return path if path.endswith("/") else path + "/"
25
+ return f"/home/{username}/{path}".rstrip("/") + "/"
26
+
27
+
28
+ @app.command("ls")
29
+ def ls(
30
+ path: str = typer.Argument(None, help="Remote path to list (default: home directory)"),
31
+ ):
32
+ """List files and directories on PythonAnywhere."""
33
+ try:
34
+ account, client = get_client(FilesClient)
35
+
36
+ remote_path = _resolve_path(path, account["username"])
37
+ items = client.list(account["username"], remote_path)
38
+
39
+ if not items:
40
+ typer.echo("(empty directory)")
41
+ return
42
+
43
+ for name in sorted(items.keys()):
44
+ item_type = items[name].get("type", "file")
45
+ if item_type == "directory":
46
+ typer.echo(f" {name}/")
47
+ else:
48
+ typer.echo(f" {name}")
49
+ except NotFoundError as e:
50
+ typer.echo(f"Path not found: {e}", err=True)
51
+ raise typer.Exit(code=1)
52
+ except NetworkError as e:
53
+ typer.echo(f"Network error: {e}", err=True)
54
+ raise typer.Exit(code=1)
55
+ except APIError as e:
56
+ typer.echo(f"API error: {e}", err=True)
57
+ raise typer.Exit(code=1)
58
+
59
+
60
+ @app.command()
61
+ def download(
62
+ remote_path: str = typer.Argument(..., help="Remote path to download"),
63
+ local_path: str = typer.Argument(None, help="Local destination (default: current directory)"),
64
+ recursive: bool = typer.Option(False, "-r", "--recursive", help="Download directory recursively"),
65
+ ):
66
+ """Download a file or directory from PythonAnywhere."""
67
+ try:
68
+ account, client = get_client(FilesClient)
69
+
70
+ resolved = _resolve_path(remote_path, account["username"])
71
+
72
+ # Check if remote is a directory by listing its parent
73
+ parent = resolved.rsplit("/", 2)[0] + "/"
74
+ name = resolved.rstrip("/").rsplit("/", 1)[-1]
75
+ parent_items = client.list(account["username"], parent)
76
+
77
+ is_directory = False
78
+ if name in parent_items:
79
+ is_directory = parent_items[name].get("type") == "directory"
80
+
81
+ if is_directory and not recursive:
82
+ typer.echo("Error: Use -r/--recursive to download directories")
83
+ raise typer.Exit(code=1)
84
+
85
+ if is_directory:
86
+ target_dir = Path(local_path) if local_path else Path(name)
87
+ target_dir.mkdir(parents=True, exist_ok=True)
88
+ count = _download_recursive(client, account["username"], resolved, target_dir)
89
+ typer.echo(f"Downloaded {count} files to {target_dir}")
90
+ else:
91
+ file_path = resolved.rstrip("/")
92
+ content = client.download(account["username"], file_path)
93
+ target = Path(local_path) if local_path else Path(file_path.split("/")[-1])
94
+ target.parent.mkdir(parents=True, exist_ok=True)
95
+ target.write_bytes(content)
96
+ typer.echo(f"Downloaded {remote_path} -> {target}")
97
+ except NotFoundError as e:
98
+ typer.echo(f"File not found: {e}", err=True)
99
+ raise typer.Exit(code=1)
100
+ except NetworkError as e:
101
+ typer.echo(f"Network error: {e}", err=True)
102
+ raise typer.Exit(code=1)
103
+ except APIError as e:
104
+ typer.echo(f"API error: {e}", err=True)
105
+ raise typer.Exit(code=1)
106
+
107
+
108
+ def _download_recursive(client, username, remote_dir, local_dir):
109
+ """Recursively download a directory. Returns file count."""
110
+ items = client.list(username, remote_dir)
111
+ count = 0
112
+ for name, info in items.items():
113
+ remote = f"{remote_dir.rstrip('/')}/{name}"
114
+ local = local_dir / name
115
+ if info.get("type") == "directory":
116
+ local.mkdir(parents=True, exist_ok=True)
117
+ count += _download_recursive(client, username, remote + "/", local)
118
+ else:
119
+ content = client.download(username, remote)
120
+ local.parent.mkdir(parents=True, exist_ok=True)
121
+ local.write_bytes(content)
122
+ count += 1
123
+ return count
124
+
125
+
126
+ @app.command()
127
+ def upload(
128
+ local_path: str = typer.Argument(..., help="Local file or directory path"),
129
+ remote_path: str = typer.Argument(..., help="Remote path on PythonAnywhere"),
130
+ recursive: bool = typer.Option(False, "-r", "--recursive", help="Upload directory recursively"),
131
+ ):
132
+ """Upload a file or directory to PythonAnywhere."""
133
+ local = Path(local_path)
134
+
135
+ if not local.exists():
136
+ typer.echo(f"Error: {local_path} does not exist")
137
+ raise typer.Exit(code=1)
138
+
139
+ if local.is_dir() and not recursive:
140
+ typer.echo("Error: Use -r/--recursive to upload directories")
141
+ raise typer.Exit(code=1)
142
+
143
+ try:
144
+ account, client = get_client(FilesClient)
145
+
146
+ if local.is_file():
147
+ content = local.read_bytes()
148
+ status = client.upload(account["username"], remote_path, content)
149
+ typer.echo(f"Uploaded {local_path} -> {remote_path} (HTTP {status})")
150
+ else:
151
+ # Recursive directory upload
152
+ count = 0
153
+ for file in local.rglob("*"):
154
+ if file.is_file():
155
+ relative = file.relative_to(local)
156
+ remote = f"{remote_path.rstrip('/')}/{relative}".replace("\\", "/")
157
+ content = file.read_bytes()
158
+ client.upload(account["username"], remote, content)
159
+ count += 1
160
+ typer.echo(f"Uploaded {count} files to {remote_path}")
161
+ except NotFoundError as e:
162
+ typer.echo(f"Path not found: {e}", err=True)
163
+ raise typer.Exit(code=1)
164
+ except NetworkError as e:
165
+ typer.echo(f"Network error: {e}", err=True)
166
+ raise typer.Exit(code=1)
167
+ except APIError as e:
168
+ typer.echo(f"API error: {e}", err=True)
169
+ raise typer.Exit(code=1)
170
+
171
+
172
+ @app.command()
173
+ def rm(
174
+ path: str = typer.Argument(..., help="Remote path to delete"),
175
+ recursive: bool = typer.Option(False, "-r", "--recursive", help="Delete directory recursively"),
176
+ force: bool = typer.Option(False, "-f", "--force", help="Skip confirmation"),
177
+ ):
178
+ """Delete a file or directory on PythonAnywhere."""
179
+ try:
180
+ account, client = get_client(FilesClient)
181
+
182
+ resolved = _resolve_path(path, account["username"])
183
+
184
+ # Check if target is a directory
185
+ parent = resolved.rsplit("/", 2)[0] + "/"
186
+ name = resolved.rstrip("/").rsplit("/", 1)[-1]
187
+ parent_items = client.list(account["username"], parent)
188
+
189
+ is_directory = False
190
+ if name in parent_items:
191
+ is_directory = parent_items[name].get("type") == "directory"
192
+
193
+ if is_directory and not recursive:
194
+ typer.echo("Error: Use -r/--recursive to delete directories")
195
+ raise typer.Exit(code=1)
196
+
197
+ if not force:
198
+ if is_directory:
199
+ confirm = typer.confirm(f"Are you sure you want to delete '{path}' and all its contents?")
200
+ else:
201
+ confirm = typer.confirm(f"Are you sure you want to delete '{path}'?")
202
+ if not confirm:
203
+ typer.echo("Cancelled.")
204
+ raise typer.Exit()
205
+
206
+ file_path = resolved.rstrip("/")
207
+ client.delete(account["username"], file_path)
208
+
209
+ if is_directory:
210
+ typer.echo(f"Deleted {path} (recursive)")
211
+ else:
212
+ typer.echo(f"Deleted {path}")
213
+ except NotFoundError as e:
214
+ typer.echo(f"File not found: {e}", err=True)
215
+ raise typer.Exit(code=1)
216
+ except NetworkError as e:
217
+ typer.echo(f"Network error: {e}", err=True)
218
+ raise typer.Exit(code=1)
219
+ except APIError as e:
220
+ typer.echo(f"API error: {e}", err=True)
221
+ raise typer.Exit(code=1)
222
+
223
+
224
+ @app.command()
225
+ def share(
226
+ remote_path: str = typer.Argument(..., help="Remote path to share"),
227
+ ):
228
+ """Share a file and get a share link."""
229
+ try:
230
+ account, client = get_client(FilesClient)
231
+ resolved = _resolve_path(remote_path, account["username"])
232
+ share_url = client.share(account["username"], resolved)
233
+ full_url = f"https://www.pythonanywhere.com{share_url}"
234
+ typer.echo(f"Share link: {full_url}")
235
+ except NotFoundError as e:
236
+ typer.echo(f"File not found: {e}", err=True)
237
+ raise typer.Exit(code=1)
238
+ except NetworkError as e:
239
+ typer.echo(f"Network error: {e}", err=True)
240
+ raise typer.Exit(code=1)
241
+ except APIError as e:
242
+ typer.echo(f"API error: {e}", err=True)
243
+ raise typer.Exit(code=1)
244
+
245
+
246
+ @app.command()
247
+ def unshare(
248
+ remote_path: str = typer.Argument(..., help="Remote path to unshare"),
249
+ ):
250
+ """Stop sharing a file."""
251
+ try:
252
+ account, client = get_client(FilesClient)
253
+ resolved = _resolve_path(remote_path, account["username"])
254
+ client.unshare(account["username"], resolved)
255
+ typer.echo(f"Stopped sharing: {remote_path}")
256
+ except NotFoundError as e:
257
+ typer.echo(f"File not found或未分享: {e}", err=True)
258
+ raise typer.Exit(code=1)
259
+ except NetworkError as e:
260
+ typer.echo(f"Network error: {e}", err=True)
261
+ raise typer.Exit(code=1)
262
+ except APIError as e:
263
+ typer.echo(f"API error: {e}", err=True)
264
+ raise typer.Exit(code=1)
265
+
266
+
267
+ @app.command("share-status")
268
+ def share_status(
269
+ remote_path: str = typer.Argument(..., help="Remote path to check"),
270
+ ):
271
+ """Check if a file is shared."""
272
+ try:
273
+ account, client = get_client(FilesClient)
274
+ resolved = _resolve_path(remote_path, account["username"])
275
+ share_url = client.get_share_status(account["username"], resolved)
276
+ full_url = f"https://www.pythonanywhere.com{share_url}"
277
+ typer.echo(f"File is shared: {full_url}")
278
+ except NotFoundError:
279
+ typer.echo(f"File is not shared: {remote_path}")
280
+ except NetworkError as e:
281
+ typer.echo(f"Network error: {e}", err=True)
282
+ raise typer.Exit(code=1)
283
+ except APIError as e:
284
+ typer.echo(f"API error: {e}", err=True)
285
+ raise typer.Exit(code=1)
pa_cli/cli/init_cmd.py ADDED
@@ -0,0 +1,61 @@
1
+ import typer
2
+
3
+ from pa_cli.config import Config
4
+ from pa_cli.crawler.account_crawler import AccountCrawler
5
+ from pa_cli.exceptions import AuthError, NetworkError, NotFoundError
6
+
7
+ app = typer.Typer(help="Configure PythonAnywhere account.")
8
+
9
+
10
+ @app.callback(invoke_without_command=True)
11
+ def init_callback(
12
+ ctx: typer.Context,
13
+ username: str = typer.Option(None, "--username", "-u", help="PythonAnywhere username"),
14
+ password: str = typer.Option(None, "--password", "-p", help="Account password"),
15
+ host: str = typer.Option("www.pythonanywhere.com", "--host", "-h", help="PythonAnywhere host"),
16
+ ):
17
+ """Interactive setup for PythonAnywhere account.
18
+
19
+ Examples:
20
+ pa init # Interactive mode
21
+ pa init -u myuser -p mypass # Command-line mode
22
+ pa init -u myuser -p mypass -h eu.pythonanywhere.com
23
+ """
24
+ if ctx.invoked_subcommand is not None:
25
+ return
26
+
27
+ # Use command-line args if provided, otherwise prompt interactively
28
+ if username is None:
29
+ username = typer.prompt("PythonAnywhere username")
30
+ if password is None:
31
+ password = typer.prompt("Password", hide_input=True)
32
+
33
+ # Save credentials to config first so AccountCrawler can read them
34
+ Config.save(username=username, password=password, host=host)
35
+
36
+ # Auto-login and fetch API token
37
+ try:
38
+ crawler = AccountCrawler()
39
+ crawler.login()
40
+
41
+ # Try to get existing token, create one if it doesn't exist
42
+ try:
43
+ token = crawler.get_token()
44
+ Config.save(token=token)
45
+ typer.echo(f"Account '{username}' configured successfully.")
46
+ typer.echo(f"API token: {token}")
47
+ except NotFoundError:
48
+ token = crawler.create_token()
49
+ Config.save(token=token)
50
+ typer.echo(f"Account '{username}' configured successfully.")
51
+ typer.echo(f"Token created: {token}")
52
+ except AuthError as e:
53
+ typer.echo(f"Auth error: {e}", err=True)
54
+ typer.echo("Don't have an account? Register with: pa register", err=True)
55
+ raise typer.Exit(code=1)
56
+ except NetworkError as e:
57
+ typer.echo(f"Network error: {e}", err=True)
58
+ raise typer.Exit(code=1)
59
+ except NotFoundError as e:
60
+ typer.echo(f"Not found: {e}", err=True)
61
+ raise typer.Exit(code=1)
pa_cli/cli/main.py ADDED
@@ -0,0 +1,69 @@
1
+ import logging
2
+ import sys
3
+
4
+ import typer
5
+
6
+ from pa_cli.cli.init_cmd import app as init_app
7
+ from pa_cli.cli.files_cmd import app as files_app
8
+ from pa_cli.cli.consoles_cmd import app as consoles_app
9
+ from pa_cli.cli.webapps_cmd import app as webapps_app
10
+ from pa_cli.cli.deploy_cmd import app as deploy_app
11
+ from pa_cli.cli.account_cmd import app as account_app
12
+ from pa_cli.cli.register_cmd import app as register_app
13
+ from pa_cli.cli.status_cmd import app as status_app
14
+ from pa_cli.cli.tasks_cmd import app as tasks_app
15
+ from pa_cli.cli.always_on_cmd import app as always_on_app
16
+
17
+ try:
18
+ from importlib.metadata import version
19
+ __version__ = version("pa-cli")
20
+ except Exception:
21
+ __version__ = "0.0.0"
22
+
23
+
24
+ def version_callback(value: bool):
25
+ if value:
26
+ typer.echo(f"pa-cli {__version__}")
27
+ raise typer.Exit()
28
+
29
+
30
+ def verbose_callback(value: bool):
31
+ if value:
32
+ logging.basicConfig(
33
+ level=logging.DEBUG,
34
+ format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
35
+ stream=sys.stderr,
36
+ )
37
+ return value
38
+
39
+
40
+ app = typer.Typer(
41
+ help="CLI tool for automating PythonAnywhere deployments.",
42
+ no_args_is_help=True,
43
+ )
44
+
45
+
46
+ @app.callback()
47
+ def main(
48
+ version: bool = typer.Option(False, "--version", "-V", help="Show version and exit", callback=version_callback, is_eager=True),
49
+ verbose: bool = typer.Option(False, "--verbose", "-v", help="Enable verbose logging", callback=verbose_callback, is_eager=True),
50
+ ):
51
+ pass
52
+
53
+ # Direct commands (single command)
54
+ app.add_typer(init_app, name="init", help="Configure PythonAnywhere account")
55
+ app.add_typer(deploy_app, name="deploy", help="Deploy a local project to PythonAnywhere")
56
+ app.add_typer(register_app, name="register", help="Register a new PythonAnywhere account")
57
+
58
+ # Command groups (multiple subcommands)
59
+ app.add_typer(files_app, name="files", help="Manage files on PythonAnywhere")
60
+ app.add_typer(consoles_app, name="console", help="Manage consoles on PythonAnywhere")
61
+ app.add_typer(webapps_app, name="webapp", help="Manage web apps on PythonAnywhere")
62
+ app.add_typer(account_app, name="account", help="Account management")
63
+ app.add_typer(status_app, name="status", help="Query system status and resource usage")
64
+ app.add_typer(tasks_app, name="tasks", help="Manage scheduled tasks on PythonAnywhere")
65
+ app.add_typer(always_on_app, name="always-on", help="Manage always-on tasks on PythonAnywhere")
66
+
67
+
68
+ if __name__ == "__main__":
69
+ app()
@@ -0,0 +1,32 @@
1
+ import typer
2
+
3
+ from pa_cli.crawler.account_crawler import AccountCrawler
4
+ from pa_cli.exceptions import AuthError, NetworkError
5
+
6
+ app = typer.Typer(help="Register a new PythonAnywhere account.")
7
+
8
+
9
+ @app.command()
10
+ def register():
11
+ """Register a new PythonAnywhere account."""
12
+ username = typer.prompt("Username (letters and numbers only)")
13
+ email = typer.prompt("Email")
14
+ password = typer.prompt("Password", hide_input=True)
15
+ confirm_password = typer.prompt("Confirm password", hide_input=True)
16
+
17
+ if password != confirm_password:
18
+ typer.echo("Passwords do not match.", err=True)
19
+ raise typer.Exit(code=1)
20
+
21
+ try:
22
+ crawler = AccountCrawler()
23
+ crawler.register(username, email, password)
24
+ typer.echo(f"Account '{username}' registered successfully!")
25
+ typer.echo("Please check your email to verify your account.")
26
+ typer.echo("Then run: pa init")
27
+ except AuthError as e:
28
+ typer.echo(f"Registration failed: {e}", err=True)
29
+ raise typer.Exit(code=1)
30
+ except NetworkError as e:
31
+ typer.echo(f"Network error: {e}", err=True)
32
+ raise typer.Exit(code=1)