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,59 @@
1
+ import typer
2
+
3
+ from pa_cli.api.system import SystemClient
4
+ from pa_cli.cli.utils import get_client
5
+ from pa_cli.config import Config
6
+ from pa_cli.crawler.account_crawler import AccountCrawler
7
+ from pa_cli.exceptions import AuthError, NetworkError, NotFoundError
8
+
9
+ app = typer.Typer(help="Query system status and resource usage.")
10
+
11
+
12
+ @app.command()
13
+ def cpu():
14
+ """Show CPU usage."""
15
+ try:
16
+ account, client = get_client(SystemClient)
17
+ data = client.get_cpu_usage(account["username"])
18
+ used = data.get("daily_cpu_total_usage_seconds", 0)
19
+ limit = data.get("daily_cpu_limit_seconds", 0)
20
+ reset = data.get("next_reset_time", "N/A")
21
+ typer.echo(f"CPU Usage:")
22
+ typer.echo(f" Used: {used} seconds")
23
+ typer.echo(f" Limit: {limit} seconds")
24
+ typer.echo(f" Reset: {reset}")
25
+ except AuthError as e:
26
+ typer.echo(f"Auth error: {e}", err=True)
27
+ raise typer.Exit(code=1)
28
+ except NetworkError as e:
29
+ typer.echo(f"Network error: {e}", err=True)
30
+ raise typer.Exit(code=1)
31
+ except NotFoundError as e:
32
+ typer.echo(f"Not found: {e}", err=True)
33
+ raise typer.Exit(code=1)
34
+
35
+
36
+ @app.command()
37
+ def disk():
38
+ """Show disk usage."""
39
+ try:
40
+ account = Config.load(verbose=True)
41
+ crawler = AccountCrawler()
42
+ crawler.login()
43
+ data = crawler.get_disk_usage(account["username"])
44
+ used = data.get("used", "N/A")
45
+ quota = data.get("quota", "N/A")
46
+ percent = data.get("percent", "N/A")
47
+ typer.echo(f"Disk Usage:")
48
+ typer.echo(f" Used: {used}")
49
+ typer.echo(f" Total: {quota}")
50
+ typer.echo(f" Usage: {percent}")
51
+ except AuthError as e:
52
+ typer.echo(f"Auth error: {e}", err=True)
53
+ raise typer.Exit(code=1)
54
+ except NetworkError as e:
55
+ typer.echo(f"Network error: {e}", err=True)
56
+ raise typer.Exit(code=1)
57
+ except NotFoundError as e:
58
+ typer.echo(f"Not found: {e}", err=True)
59
+ raise typer.Exit(code=1)
@@ -0,0 +1,137 @@
1
+ import typer
2
+
3
+ from pa_cli.api.tasks import TasksClient
4
+ from pa_cli.cli.utils import get_client
5
+ from pa_cli.exceptions import AuthError, NetworkError, NotFoundError, APIError
6
+
7
+ app = typer.Typer(help="Manage scheduled tasks on PythonAnywhere.")
8
+
9
+
10
+ @app.command("list")
11
+ def list_tasks():
12
+ """List all scheduled tasks."""
13
+ try:
14
+ account, client = get_client(TasksClient)
15
+ tasks = client.list(account["username"])
16
+ if not tasks:
17
+ typer.echo("No scheduled tasks found.")
18
+ return
19
+ for task in tasks:
20
+ status = "enabled" if task.get("enabled") else "disabled"
21
+ typer.echo(f"ID: {task['id']}, Command: {task['command']}, "
22
+ f"Interval: {task['interval']}, Status: {status}")
23
+ except AuthError as e:
24
+ typer.echo(f"Auth error: {e}", err=True)
25
+ raise typer.Exit(code=1)
26
+ except NetworkError as e:
27
+ typer.echo(f"Network error: {e}", err=True)
28
+ raise typer.Exit(code=1)
29
+ except APIError as e:
30
+ typer.echo(f"API error: {e}", err=True)
31
+ raise typer.Exit(code=1)
32
+
33
+
34
+ @app.command()
35
+ def create(
36
+ command: str = typer.Argument(..., help="Command to execute"),
37
+ interval: str = typer.Option("daily", "--interval", "-i", help="Interval: hourly, daily, weekly, monthly"),
38
+ hour: int = typer.Option(0, "--hour", "-H", help="Hour to run (0-23)"),
39
+ minute: int = typer.Option(0, "--minute", "-M", help="Minute to run (0-59)"),
40
+ description: str = typer.Option("", "--description", "-d", help="Task description"),
41
+ ):
42
+ """Create a new scheduled task."""
43
+ try:
44
+ account, client = get_client(TasksClient)
45
+ task = client.create(
46
+ account["username"],
47
+ command=command,
48
+ interval=interval,
49
+ hour=hour,
50
+ minute=minute,
51
+ description=description,
52
+ )
53
+ typer.echo(f"Task created: ID={task['id']}, Command={task['command']}")
54
+ except AuthError as e:
55
+ typer.echo(f"Auth error: {e}", err=True)
56
+ raise typer.Exit(code=1)
57
+ except NetworkError as e:
58
+ typer.echo(f"Network error: {e}", err=True)
59
+ raise typer.Exit(code=1)
60
+ except APIError as e:
61
+ typer.echo(f"API error: {e}", err=True)
62
+ raise typer.Exit(code=1)
63
+
64
+
65
+ @app.command()
66
+ def delete(
67
+ task_id: int = typer.Argument(..., help="Task ID to delete"),
68
+ force: bool = typer.Option(False, "-f", "--force", help="Skip confirmation"),
69
+ ):
70
+ """Delete a scheduled task."""
71
+ try:
72
+ account, client = get_client(TasksClient)
73
+ if not force:
74
+ confirm = typer.confirm(f"Are you sure you want to delete task {task_id}?")
75
+ if not confirm:
76
+ typer.echo("Cancelled.")
77
+ raise typer.Exit()
78
+ client.delete(account["username"], task_id)
79
+ typer.echo(f"Task {task_id} deleted.")
80
+ except AuthError as e:
81
+ typer.echo(f"Auth error: {e}", err=True)
82
+ raise typer.Exit(code=1)
83
+ except NetworkError as e:
84
+ typer.echo(f"Network error: {e}", err=True)
85
+ raise typer.Exit(code=1)
86
+ except NotFoundError as e:
87
+ typer.echo(f"Task not found: {e}", err=True)
88
+ raise typer.Exit(code=1)
89
+ except APIError as e:
90
+ typer.echo(f"API error: {e}", err=True)
91
+ raise typer.Exit(code=1)
92
+
93
+
94
+ @app.command()
95
+ def enable(
96
+ task_id: int = typer.Argument(..., help="Task ID to enable"),
97
+ ):
98
+ """Enable a scheduled task."""
99
+ try:
100
+ account, client = get_client(TasksClient)
101
+ client.update(account["username"], task_id, enabled=True)
102
+ typer.echo(f"Task {task_id} enabled.")
103
+ except AuthError as e:
104
+ typer.echo(f"Auth error: {e}", err=True)
105
+ raise typer.Exit(code=1)
106
+ except NetworkError as e:
107
+ typer.echo(f"Network error: {e}", err=True)
108
+ raise typer.Exit(code=1)
109
+ except NotFoundError as e:
110
+ typer.echo(f"Task not found: {e}", err=True)
111
+ raise typer.Exit(code=1)
112
+ except APIError as e:
113
+ typer.echo(f"API error: {e}", err=True)
114
+ raise typer.Exit(code=1)
115
+
116
+
117
+ @app.command()
118
+ def disable(
119
+ task_id: int = typer.Argument(..., help="Task ID to disable"),
120
+ ):
121
+ """Disable a scheduled task."""
122
+ try:
123
+ account, client = get_client(TasksClient)
124
+ client.update(account["username"], task_id, enabled=False)
125
+ typer.echo(f"Task {task_id} disabled.")
126
+ except AuthError as e:
127
+ typer.echo(f"Auth error: {e}", err=True)
128
+ raise typer.Exit(code=1)
129
+ except NetworkError as e:
130
+ typer.echo(f"Network error: {e}", err=True)
131
+ raise typer.Exit(code=1)
132
+ except NotFoundError as e:
133
+ typer.echo(f"Task not found: {e}", err=True)
134
+ raise typer.Exit(code=1)
135
+ except APIError as e:
136
+ typer.echo(f"API error: {e}", err=True)
137
+ raise typer.Exit(code=1)
pa_cli/cli/utils.py ADDED
@@ -0,0 +1,27 @@
1
+ import re
2
+ from typing import Type, TypeVar, Tuple
3
+
4
+ from pa_cli.config import Config
5
+ from pa_cli.api.client import BaseClient
6
+
7
+ T = TypeVar("T", bound=BaseClient)
8
+
9
+
10
+ def get_client(client_class: Type[T]) -> Tuple[dict, T]:
11
+ """Load config and create API client."""
12
+ account = Config.load(verbose=True)
13
+ client = client_class(token=account["token"], host=account["host"])
14
+ return account, client
15
+
16
+
17
+ def fix_remote_path(path: str) -> str:
18
+ """Fix paths mangled by Git Bash (MSYS2) path conversion.
19
+
20
+ Git Bash converts /home/user/dir to D:/Git/home/user/dir.
21
+ This function detects and reverses the conversion.
22
+ """
23
+ if re.match(r"^[A-Za-z]:/", path):
24
+ match = re.search(r"(/home/\S+)", path)
25
+ if match:
26
+ return match.group(1)
27
+ return path
@@ -0,0 +1,262 @@
1
+ import typer
2
+
3
+ from pa_cli.api.files import FilesClient
4
+ from pa_cli.api.webapps import WebappsClient
5
+ from pa_cli.cli.utils import get_client, fix_remote_path
6
+ from pa_cli.config import Config
7
+ from pa_cli.crawler.account_crawler import AccountCrawler
8
+ from pa_cli.exceptions import AuthError, NetworkError, NotFoundError, APIError
9
+
10
+ app = typer.Typer(help="Manage web apps on PythonAnywhere.")
11
+
12
+
13
+ @app.command()
14
+ def create(
15
+ domain_name: str = typer.Argument(..., help="Domain name"),
16
+ python_version: str = typer.Option("python310", "--python", "-p", help="Python version"),
17
+ ):
18
+ """Create a new web app."""
19
+ account, client = get_client(WebappsClient)
20
+ client.create(account["username"], domain_name, python_version)
21
+ typer.echo(f"Webapp {domain_name} created with {python_version}")
22
+
23
+
24
+ @app.command()
25
+ def config(
26
+ domain_name: str = typer.Argument(None, help="Domain name (default: {username}.pythonanywhere.com)"),
27
+ source_dir: str = typer.Option(None, "--source-dir", "-s", help="Source directory path"),
28
+ virtualenv: str = typer.Option(None, "--virtualenv", "-v", help="Virtualenv path"),
29
+ python_version: str = typer.Option(None, "--python-version", "-p", help="Python version (e.g. 3.10, 3.11)"),
30
+ working_dir: str = typer.Option(None, "--working-dir", "-w", help="Working directory path"),
31
+ ):
32
+ """Configure a web app."""
33
+ account, client = get_client(WebappsClient)
34
+ if domain_name is None:
35
+ domain_name = f"{account['username']}.pythonanywhere.com"
36
+ kwargs = {}
37
+ if source_dir:
38
+ kwargs["source_directory"] = fix_remote_path(source_dir)
39
+ if virtualenv:
40
+ kwargs["virtualenv_path"] = fix_remote_path(virtualenv)
41
+ if python_version:
42
+ kwargs["python_version"] = python_version
43
+ if working_dir:
44
+ kwargs["working_directory"] = fix_remote_path(working_dir)
45
+ if not kwargs:
46
+ typer.echo("Error: No configuration specified. Use --source-dir, --virtualenv, --python-version, or --working-dir.", err=True)
47
+ raise typer.Exit(code=1)
48
+ client.update(account["username"], domain_name, **kwargs)
49
+ typer.echo(f"Webapp {domain_name} configured.")
50
+
51
+
52
+ @app.command()
53
+ def static(
54
+ domain_name: str = typer.Argument(..., help="Domain name"),
55
+ url: str = typer.Option(..., "--url", help="URL prefix"),
56
+ path: str = typer.Option(..., "--path", help="Directory path"),
57
+ ):
58
+ """Add a static file mapping."""
59
+ account, client = get_client(WebappsClient)
60
+ fixed_path = fix_remote_path(path)
61
+ client.add_static_file(account["username"], domain_name, url=url, path=fixed_path)
62
+ typer.echo(f"Static mapping added: {url} -> {fixed_path}")
63
+
64
+
65
+ @app.command()
66
+ def reload(
67
+ domain_name: str = typer.Argument(..., help="Domain name"),
68
+ ):
69
+ """Reload a web app."""
70
+ account, client = get_client(WebappsClient)
71
+ client.reload(account["username"], domain_name)
72
+ typer.echo(f"Webapp {domain_name} reloaded.")
73
+
74
+
75
+ @app.command("hits")
76
+ def hits(
77
+ domain_name: str = typer.Argument(None, help="Domain name (default: {username}.pythonanywhere.com)"),
78
+ ):
79
+ """Get web app hit statistics via crawler."""
80
+ try:
81
+ account = Config.load(verbose=True)
82
+ if domain_name is None:
83
+ domain_name = f"{account['username']}.pythonanywhere.com"
84
+ crawler = AccountCrawler()
85
+ crawler.login()
86
+ data = crawler.get_hits(domain_name)
87
+ typer.echo(f"Hit statistics for {domain_name}:")
88
+ for key, value in data.items():
89
+ typer.echo(f" {key}: {value}")
90
+ except AuthError as e:
91
+ typer.echo(f"Auth error: {e}", err=True)
92
+ raise typer.Exit(code=1)
93
+ except NetworkError as e:
94
+ typer.echo(f"Network error: {e}", err=True)
95
+ raise typer.Exit(code=1)
96
+ except NotFoundError as e:
97
+ typer.echo(f"Not found: {e}", err=True)
98
+ raise typer.Exit(code=1)
99
+
100
+
101
+ @app.command("reload-crawler")
102
+ def reload_crawler(
103
+ domain_name: str = typer.Argument(None, help="Domain name (default: {username}.pythonanywhere.com)"),
104
+ ):
105
+ """Reload a web app via crawler (alternative to API reload)."""
106
+ try:
107
+ account = Config.load(verbose=True)
108
+ if domain_name is None:
109
+ domain_name = f"{account['username']}.pythonanywhere.com"
110
+ crawler = AccountCrawler()
111
+ crawler.login()
112
+ if crawler.reload_webapp(domain_name):
113
+ typer.echo(f"Webapp {domain_name} reloaded successfully.")
114
+ else:
115
+ typer.echo(f"Failed to reload webapp {domain_name}.", err=True)
116
+ raise typer.Exit(code=1)
117
+ except AuthError as e:
118
+ typer.echo(f"Auth error: {e}", err=True)
119
+ raise typer.Exit(code=1)
120
+ except NetworkError as e:
121
+ typer.echo(f"Network error: {e}", err=True)
122
+ raise typer.Exit(code=1)
123
+ except NotFoundError as e:
124
+ typer.echo(f"Not found: {e}", err=True)
125
+ raise typer.Exit(code=1)
126
+
127
+
128
+ @app.command()
129
+ def delete(
130
+ domain_name: str = typer.Argument(..., help="Domain name"),
131
+ force: bool = typer.Option(False, "-f", "--force", help="Skip confirmation"),
132
+ ):
133
+ """Delete a web app."""
134
+ try:
135
+ account, client = get_client(WebappsClient)
136
+ if not force:
137
+ confirm = typer.confirm(f"Are you sure you want to delete {domain_name}?")
138
+ if not confirm:
139
+ typer.echo("Cancelled.")
140
+ raise typer.Exit()
141
+ client.delete(account["username"], domain_name)
142
+ typer.echo(f"Webapp {domain_name} deleted.")
143
+ except AuthError as e:
144
+ typer.echo(f"Auth error: {e}", err=True)
145
+ raise typer.Exit(code=1)
146
+ except NetworkError as e:
147
+ typer.echo(f"Network error: {e}", err=True)
148
+ raise typer.Exit(code=1)
149
+ except NotFoundError as e:
150
+ typer.echo(f"Not found: {e}", err=True)
151
+ raise typer.Exit(code=1)
152
+
153
+
154
+ @app.command()
155
+ def enable(
156
+ domain_name: str = typer.Argument(..., help="Domain name"),
157
+ ):
158
+ """Enable a web app."""
159
+ try:
160
+ account = Config.load(verbose=True)
161
+ crawler = AccountCrawler()
162
+ crawler.login()
163
+ if crawler.enable_webapp(domain_name):
164
+ typer.echo(f"Webapp {domain_name} enabled.")
165
+ else:
166
+ typer.echo(f"Failed to enable webapp {domain_name}.", err=True)
167
+ raise typer.Exit(code=1)
168
+ except AuthError as e:
169
+ typer.echo(f"Auth error: {e}", err=True)
170
+ raise typer.Exit(code=1)
171
+ except NetworkError as e:
172
+ typer.echo(f"Network error: {e}", err=True)
173
+ raise typer.Exit(code=1)
174
+ except NotFoundError as e:
175
+ typer.echo(f"Not found: {e}", err=True)
176
+ raise typer.Exit(code=1)
177
+
178
+
179
+ @app.command()
180
+ def disable(
181
+ domain_name: str = typer.Argument(..., help="Domain name"),
182
+ ):
183
+ """Disable a web app."""
184
+ try:
185
+ account = Config.load(verbose=True)
186
+ crawler = AccountCrawler()
187
+ crawler.login()
188
+ if crawler.disable_webapp(domain_name):
189
+ typer.echo(f"Webapp {domain_name} disabled.")
190
+ else:
191
+ typer.echo(f"Failed to disable webapp {domain_name}.", err=True)
192
+ raise typer.Exit(code=1)
193
+ except AuthError as e:
194
+ typer.echo(f"Auth error: {e}", err=True)
195
+ raise typer.Exit(code=1)
196
+ except NetworkError as e:
197
+ typer.echo(f"Network error: {e}", err=True)
198
+ raise typer.Exit(code=1)
199
+ except NotFoundError as e:
200
+ typer.echo(f"Not found: {e}", err=True)
201
+ raise typer.Exit(code=1)
202
+
203
+
204
+ @app.command()
205
+ def logs(
206
+ domain_name: str = typer.Argument(None, help="Domain name (default: {username}.pythonanywhere.com)"),
207
+ log_type: str = typer.Option("error", "--type", "-t", help="Log type: access, error, or server"),
208
+ lines: int = typer.Option(50, "--lines", "-n", help="Number of lines to show"),
209
+ ):
210
+ """Show web app logs."""
211
+ try:
212
+ account, client = get_client(FilesClient)
213
+ if domain_name is None:
214
+ domain_name = f"{account['username']}.pythonanywhere.com"
215
+
216
+ # Extract subdomain from domain_name (lowercase)
217
+ subdomain = domain_name.split(".")[0].lower()
218
+ log_file = f"/var/log/{subdomain}.pythonanywhere.com.{log_type}.log"
219
+
220
+ try:
221
+ content = client.download(account["username"], log_file)
222
+ text = content.decode("utf-8", errors="ignore")
223
+ # Show last N lines
224
+ all_lines = text.strip().split("\n")
225
+ for line in all_lines[-lines:]:
226
+ typer.echo(line)
227
+ except NotFoundError:
228
+ typer.echo(f"Log file not found: {log_file}", err=True)
229
+ raise typer.Exit(code=1)
230
+ except AuthError as e:
231
+ typer.echo(f"Auth error: {e}", err=True)
232
+ raise typer.Exit(code=1)
233
+ except NetworkError as e:
234
+ typer.echo(f"Network error: {e}", err=True)
235
+ raise typer.Exit(code=1)
236
+ except NotFoundError as e:
237
+ typer.echo(f"Not found: {e}", err=True)
238
+ raise typer.Exit(code=1)
239
+
240
+
241
+ @app.command()
242
+ def ssl(
243
+ domain_name: str = typer.Argument(None, help="Domain name (default: {username}.pythonanywhere.com)"),
244
+ ):
245
+ """Show SSL certificate information."""
246
+ try:
247
+ account, client = get_client(WebappsClient)
248
+ if domain_name is None:
249
+ domain_name = f"{account['username']}.pythonanywhere.com"
250
+ data = client.get_ssl_info(account["username"], domain_name)
251
+ cert_type = data.get("cert_type", "N/A")
252
+ typer.echo(f"SSL Certificate Info for {domain_name}:")
253
+ typer.echo(f" Type: {cert_type}")
254
+ except AuthError as e:
255
+ typer.echo(f"Auth error: {e}", err=True)
256
+ raise typer.Exit(code=1)
257
+ except NetworkError as e:
258
+ typer.echo(f"Network error: {e}", err=True)
259
+ raise typer.Exit(code=1)
260
+ except NotFoundError as e:
261
+ typer.echo(f"Not found: {e}", err=True)
262
+ raise typer.Exit(code=1)