pythonanywhere-clis 1.0.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.
Files changed (43) hide show
  1. pythonanywhere_clis-1.0.0/LICENSE +21 -0
  2. pythonanywhere_clis-1.0.0/PKG-INFO +14 -0
  3. pythonanywhere_clis-1.0.0/README.md +262 -0
  4. pythonanywhere_clis-1.0.0/pa_cli/__init__.py +1 -0
  5. pythonanywhere_clis-1.0.0/pa_cli/api/__init__.py +0 -0
  6. pythonanywhere_clis-1.0.0/pa_cli/api/always_on.py +22 -0
  7. pythonanywhere_clis-1.0.0/pa_cli/api/client.py +45 -0
  8. pythonanywhere_clis-1.0.0/pa_cli/api/consoles.py +46 -0
  9. pythonanywhere_clis-1.0.0/pa_cli/api/files.py +95 -0
  10. pythonanywhere_clis-1.0.0/pa_cli/api/system.py +8 -0
  11. pythonanywhere_clis-1.0.0/pa_cli/api/tasks.py +47 -0
  12. pythonanywhere_clis-1.0.0/pa_cli/api/webapps.py +71 -0
  13. pythonanywhere_clis-1.0.0/pa_cli/cli/__init__.py +0 -0
  14. pythonanywhere_clis-1.0.0/pa_cli/cli/account_cmd.py +131 -0
  15. pythonanywhere_clis-1.0.0/pa_cli/cli/always_on_cmd.py +82 -0
  16. pythonanywhere_clis-1.0.0/pa_cli/cli/consoles_cmd.py +151 -0
  17. pythonanywhere_clis-1.0.0/pa_cli/cli/deploy_cmd.py +44 -0
  18. pythonanywhere_clis-1.0.0/pa_cli/cli/files_cmd.py +285 -0
  19. pythonanywhere_clis-1.0.0/pa_cli/cli/init_cmd.py +61 -0
  20. pythonanywhere_clis-1.0.0/pa_cli/cli/main.py +69 -0
  21. pythonanywhere_clis-1.0.0/pa_cli/cli/register_cmd.py +32 -0
  22. pythonanywhere_clis-1.0.0/pa_cli/cli/status_cmd.py +59 -0
  23. pythonanywhere_clis-1.0.0/pa_cli/cli/tasks_cmd.py +137 -0
  24. pythonanywhere_clis-1.0.0/pa_cli/cli/utils.py +27 -0
  25. pythonanywhere_clis-1.0.0/pa_cli/cli/webapps_cmd.py +262 -0
  26. pythonanywhere_clis-1.0.0/pa_cli/config.py +254 -0
  27. pythonanywhere_clis-1.0.0/pa_cli/crawler/__init__.py +0 -0
  28. pythonanywhere_clis-1.0.0/pa_cli/crawler/account_crawler.py +370 -0
  29. pythonanywhere_clis-1.0.0/pa_cli/crawler/console_crawler.py +141 -0
  30. pythonanywhere_clis-1.0.0/pa_cli/exceptions.py +18 -0
  31. pythonanywhere_clis-1.0.0/pa_cli/workflows/__init__.py +0 -0
  32. pythonanywhere_clis-1.0.0/pa_cli/workflows/deploy.py +215 -0
  33. pythonanywhere_clis-1.0.0/pyproject.toml +27 -0
  34. pythonanywhere_clis-1.0.0/pythonanywhere_clis.egg-info/PKG-INFO +14 -0
  35. pythonanywhere_clis-1.0.0/pythonanywhere_clis.egg-info/SOURCES.txt +41 -0
  36. pythonanywhere_clis-1.0.0/pythonanywhere_clis.egg-info/dependency_links.txt +1 -0
  37. pythonanywhere_clis-1.0.0/pythonanywhere_clis.egg-info/entry_points.txt +2 -0
  38. pythonanywhere_clis-1.0.0/pythonanywhere_clis.egg-info/requires.txt +8 -0
  39. pythonanywhere_clis-1.0.0/pythonanywhere_clis.egg-info/top_level.txt +1 -0
  40. pythonanywhere_clis-1.0.0/setup.cfg +4 -0
  41. pythonanywhere_clis-1.0.0/tests/test_account_crawler.py +1196 -0
  42. pythonanywhere_clis-1.0.0/tests/test_config.py +662 -0
  43. pythonanywhere_clis-1.0.0/tests/test_console_crawler.py +516 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 pythonanywhere-cli contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,14 @@
1
+ Metadata-Version: 2.4
2
+ Name: pythonanywhere-clis
3
+ Version: 1.0.0
4
+ Summary: CLI tool for automating PythonAnywhere deployments
5
+ Requires-Python: >=3.10
6
+ License-File: LICENSE
7
+ Requires-Dist: typer>=0.9.0
8
+ Requires-Dist: requests>=2.28.0
9
+ Requires-Dist: beautifulsoup4>=4.12.0
10
+ Requires-Dist: websocket-client>=1.6.0
11
+ Provides-Extra: dev
12
+ Requires-Dist: pytest>=7.0; extra == "dev"
13
+ Requires-Dist: pytest-mock>=3.10; extra == "dev"
14
+ Dynamic: license-file
@@ -0,0 +1,262 @@
1
+ # pythonanywhere-cli
2
+
3
+ CLI tool for automating PythonAnywhere deployments. **Local project → Live website, one step.**
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install -e .
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```bash
14
+ # 1. Register a new account (if needed)
15
+ pa register
16
+
17
+ # 2. Configure your account (auto-fetches API token)
18
+ pa init
19
+
20
+ # 3. Deploy a project
21
+ pa deploy ./my-site
22
+ ```
23
+
24
+ ## Commands
25
+
26
+ ### Account Management
27
+
28
+ | Command | Description | Auth |
29
+ |---------|-------------|------|
30
+ | `pa init` | Configure account (auto-fetches token) | - |
31
+ | `pa register` | Register a new PythonAnywhere account | - |
32
+ | `pa account list` | List all configured accounts | - |
33
+ | `pa account switch <username>` | Switch default account | - |
34
+ | `pa account remove <username>` | Remove an account | - |
35
+ | `pa account login` | Store password for crawler operations | - |
36
+ | `pa account token` | Fetch API token from account page | Password |
37
+ | `pa account extend` | Extend free tier account expiry | Password |
38
+
39
+ ### File Management
40
+
41
+ | Command | Description | Auth |
42
+ |---------|-------------|------|
43
+ | `pa files ls [path]` | List remote directory contents | Token |
44
+ | `pa files download <remote> [local]` | Download a file | Token |
45
+ | `pa files download <remote> [local] -r` | Download directory recursively | Token |
46
+ | `pa files rm <path>` | Delete a remote file | Token |
47
+ | `pa files rm <path> -r` | Delete directory recursively | Token |
48
+ | `pa files upload <local> <remote>` | Upload a single file | Token |
49
+ | `pa files upload <local> <remote> -r` | Upload directory recursively | Token |
50
+ | `pa files share <path>` | Share a file and get link | Token |
51
+ | `pa files unshare <path>` | Stop sharing a file | Token |
52
+ | `pa files share-status <path>` | Check if a file is shared | Token |
53
+
54
+ ### Console Management
55
+
56
+ | Command | Description | Auth |
57
+ |---------|-------------|------|
58
+ | `pa console list` | List all consoles | Token |
59
+ | `pa console create` | Create a new console | Token |
60
+ | `pa console send <id> <cmd>` | Send command and get output | Token |
61
+ | `pa console kill <id>` | Kill a console | Token |
62
+ | `pa console activate <id>` | Activate console via WebSocket | Password |
63
+ | `pa console get-or-create` | Get existing or create new console | Password |
64
+
65
+ ### Web App Management
66
+
67
+ | Command | Description | Auth |
68
+ |---------|-------------|------|
69
+ | `pa webapp create <domain>` | Create a web app | Token |
70
+ | `pa webapp config <domain> --source-dir <path>` | Configure source directory | Token |
71
+ | `pa webapp config <domain> --virtualenv <path>` | Configure virtualenv path | Token |
72
+ | `pa webapp static <domain> --url <url> --path <path>` | Add static file mapping | Token |
73
+ | `pa webapp reload <domain>` | Reload web app (API) | Token |
74
+ | `pa webapp reload-crawler <domain>` | Reload web app (crawler) | Password |
75
+ | `pa webapp hits <domain>` | Get hit statistics | Password |
76
+ | `pa webapp delete <domain>` | Delete a web app | Token |
77
+ | `pa webapp enable <domain>` | Enable a web app | Token |
78
+ | `pa webapp disable <domain>` | Disable a web app | Token |
79
+ | `pa webapp logs <domain>` | Show web app logs | Token |
80
+ | `pa webapp ssl <domain>` | Show SSL certificate info | Token |
81
+
82
+ ### Deployment
83
+
84
+ | Command | Description | Auth |
85
+ |---------|-------------|------|
86
+ | `pa deploy <dir>` | One-click deploy to default domain | Token |
87
+ | `pa deploy <dir> --domain <domain>` | One-click deploy to custom domain | Token |
88
+
89
+ ### System Status
90
+
91
+ | Command | Description | Auth |
92
+ |---------|-------------|------|
93
+ | `pa status cpu` | Show CPU usage | Token |
94
+ | `pa status disk` | Show disk usage | Password |
95
+
96
+ ### Scheduled Tasks
97
+
98
+ | Command | Description | Auth |
99
+ |---------|-------------|------|
100
+ | `pa tasks list` | List all scheduled tasks | Token |
101
+ | `pa tasks create <command>` | Create a new scheduled task | Token |
102
+ | `pa tasks delete <id>` | Delete a scheduled task | Token |
103
+ | `pa tasks enable <id>` | Enable a scheduled task | Token |
104
+ | `pa tasks disable <id>` | Disable a scheduled task | Token |
105
+
106
+ ### Always-on Tasks
107
+
108
+ | Command | Description | Auth |
109
+ |---------|-------------|------|
110
+ | `pa always-on list` | List all always-on tasks | Token |
111
+ | `pa always-on create <command>` | Create a new always-on task | Token |
112
+ | `pa always-on delete <id>` | Delete an always-on task | Token |
113
+
114
+ ## Typical Workflows
115
+
116
+ ### Deploy a new project
117
+
118
+ ```bash
119
+ pa init # Configure account
120
+ pa deploy ./my-site # One-click deploy
121
+ ```
122
+
123
+ ### Manage existing web app
124
+
125
+ ```bash
126
+ pa webapp reload mysite.pythonanywhere.com # Reload
127
+ pa webapp hits mysite.pythonanywhere.com # Check traffic
128
+ pa account extend # Extend expiry
129
+ ```
130
+
131
+ ### Work with consoles
132
+
133
+ ```bash
134
+ pa console list # See available consoles
135
+ pa console get-or-create # Get or create a console
136
+ pa console activate 12345 # Activate it
137
+ pa console send 12345 "ls -la" # Run a command
138
+ pa console kill 12345 # Clean up
139
+ ```
140
+
141
+ ### Manage multiple accounts
142
+
143
+ ```bash
144
+ pa init # Add first account
145
+ pa init # Add second account (becomes default)
146
+ pa account list # See all accounts
147
+ pa account switch user1 # Switch back to first
148
+ pa deploy ./site # Deploys under user1
149
+ pa account remove user2 # Remove an account
150
+ ```
151
+
152
+ ## Configuration
153
+
154
+ Configuration is stored at `~/.pa-cli/config.json`:
155
+
156
+ ```json
157
+ {
158
+ "accounts": [
159
+ {
160
+ "username": "yourusername",
161
+ "token": "your-api-token",
162
+ "host": "www.pythonanywhere.com",
163
+ "password": "your-password"
164
+ }
165
+ ],
166
+ "default_account": "yourusername"
167
+ }
168
+ ```
169
+
170
+ ## Architecture
171
+
172
+ ```
173
+ pa_cli/
174
+ ├── api/ # REST API clients (Token auth)
175
+ │ ├── client.py # BaseClient with Token auth
176
+ │ ├── consoles.py
177
+ │ ├── files.py
178
+ │ ├── webapps.py
179
+ │ ├── system.py
180
+ │ ├── tasks.py
181
+ │ └── always_on.py
182
+ ├── cli/ # CLI commands (Typer)
183
+ │ ├── main.py
184
+ │ ├── utils.py # Shared utilities (get_client, _fix_remote_path)
185
+ │ ├── init_cmd.py
186
+ │ ├── register_cmd.py
187
+ │ ├── account_cmd.py
188
+ │ ├── files_cmd.py
189
+ │ ├── consoles_cmd.py
190
+ │ ├── webapps_cmd.py
191
+ │ ├── deploy_cmd.py
192
+ │ ├── status_cmd.py
193
+ │ ├── tasks_cmd.py
194
+ │ └── always_on_cmd.py
195
+ ├── crawler/ # Browser simulation (Session auth)
196
+ │ ├── account_crawler.py
197
+ │ └── console_crawler.py
198
+ ├── workflows/ # Deployment orchestration
199
+ │ └── deploy.py
200
+ ├── config.py # Configuration management
201
+ └── exceptions.py # Exception hierarchy
202
+ ```
203
+
204
+ ## Dependencies
205
+
206
+ - `typer` - CLI framework
207
+ - `requests` - HTTP client
208
+ - `beautifulsoup4` - HTML parsing
209
+ - `websocket-client` - WebSocket connections
210
+
211
+ ## Testing
212
+
213
+ ```bash
214
+ # Run all tests
215
+ pytest
216
+
217
+ # Run with verbose output
218
+ pytest -v
219
+
220
+ # Run specific test file
221
+ pytest tests/test_account_crawler.py
222
+ ```
223
+
224
+ **Test coverage:** 267+ tests passing
225
+
226
+ ## Roadmap
227
+
228
+ ### ✅ Completed (P0/P1)
229
+
230
+ - [x] Account configuration (`pa init`)
231
+ - [x] Account registration (`pa register`)
232
+ - [x] Auto-fetch API token (`pa account token`)
233
+ - [x] Auto-extend expiry (`pa account extend`)
234
+ - [x] File upload (`pa files upload`)
235
+ - [x] File browsing (`pa files ls`)
236
+ - [x] File download (`pa files download`)
237
+ - [x] File deletion (`pa files rm`)
238
+ - [x] Console management (`pa console *`)
239
+ - [x] Web app management (`pa webapp *`)
240
+ - [x] One-click deployment (`pa deploy`)
241
+ - [x] Hit statistics (`pa webapp hits`)
242
+ - [x] Multi-account management (`pa account switch`)
243
+ - [x] CPU usage query (`pa status cpu`)
244
+ - [x] Disk usage query (`pa status disk`)
245
+
246
+ ### ✅ Completed (P2)
247
+
248
+ - [x] Log management (`pa webapp logs`)
249
+ - [x] Webapp enable/disable (`pa webapp enable/disable`)
250
+ - [x] Delete webapp (`pa webapp delete`)
251
+ - [x] File sharing (`pa files share/unshare/share-status`)
252
+ - [x] SSL info (`pa webapp ssl`)
253
+ - [x] Scheduled tasks (`pa tasks`)
254
+ - [x] Always-on tasks (`pa always-on`)
255
+
256
+ ### 🔲 Planned (P3)
257
+
258
+ - [ ] Database info (`pa databases`) - API only supports listing, not create/delete
259
+
260
+ ## License
261
+
262
+ MIT
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
File without changes
@@ -0,0 +1,22 @@
1
+ from pa_cli.api.client import BaseClient
2
+
3
+
4
+ class AlwaysOnClient(BaseClient):
5
+ def list(self, username: str) -> list:
6
+ """List all always-on tasks."""
7
+ response = self._request("GET", "/api/v0/user/{username}/always_on/", username=username)
8
+ return response.json()
9
+
10
+ def create(self, username: str, command: str, enabled: bool = True) -> dict:
11
+ """Create a new always-on task."""
12
+ response = self._request(
13
+ "POST",
14
+ "/api/v0/user/{username}/always_on/",
15
+ username=username,
16
+ json={"command": command, "enabled": enabled},
17
+ )
18
+ return response.json()
19
+
20
+ def delete(self, username: str, task_id: int) -> None:
21
+ """Delete an always-on task."""
22
+ self._request("DELETE", "/api/v0/user/{username}/always_on/{id}/", username=username, id=task_id)
@@ -0,0 +1,45 @@
1
+ import requests
2
+
3
+ from pa_cli.exceptions import APIError, NetworkError, NotFoundError
4
+
5
+
6
+ class BaseClient:
7
+ def __init__(self, token: str, host: str = "www.pythonanywhere.com"):
8
+ self.host = host
9
+ self.base_url = f"https://{host}"
10
+ self.session = requests.Session()
11
+ self.session.headers.update({"Authorization": f"Token {token}"})
12
+
13
+ def _build_url(self, path: str, **kwargs) -> str:
14
+ return f"{self.base_url}{path.format(**kwargs)}"
15
+
16
+ def _request(self, method: str, path: str, **kwargs) -> requests.Response:
17
+ url = self._build_url(path, **kwargs)
18
+
19
+ # Extract path params from kwargs (used in URL formatting)
20
+ path_params = {k for k in kwargs if "{" + k + "}" in path}
21
+ request_kwargs = {k: v for k, v in kwargs.items() if k not in path_params}
22
+
23
+ try:
24
+ response = self.session.request(method, url, **request_kwargs)
25
+ except requests.ConnectionError as e:
26
+ raise NetworkError(f"Connection failed: {e}") from e
27
+ except requests.Timeout as e:
28
+ raise NetworkError(f"Request timed out: {e}") from e
29
+ except requests.RequestException as e:
30
+ raise NetworkError(f"Request failed: {e}") from e
31
+
32
+ if response.status_code == 404:
33
+ raise NotFoundError(f"Not found: {path}")
34
+
35
+ try:
36
+ response.raise_for_status()
37
+ except requests.HTTPError as e:
38
+ detail = ""
39
+ try:
40
+ detail = response.json().get("detail", "")
41
+ except Exception:
42
+ detail = response.text
43
+ raise APIError(f"API error {response.status_code}: {detail}") from e
44
+
45
+ return response
@@ -0,0 +1,46 @@
1
+ from pa_cli.api.client import BaseClient
2
+
3
+
4
+ class ConsolesClient(BaseClient):
5
+ def list(self, username: str) -> list:
6
+ response = self._request(
7
+ "GET",
8
+ "/api/v0/user/{username}/consoles/",
9
+ username=username,
10
+ )
11
+ return response.json()
12
+
13
+ def create(self, username: str, executable: str = "bash") -> dict:
14
+ response = self._request(
15
+ "POST",
16
+ "/api/v0/user/{username}/consoles/",
17
+ username=username,
18
+ json={"executable": executable},
19
+ )
20
+ return response.json()
21
+
22
+ def send_input(self, username: str, console_id: int, input_text: str) -> None:
23
+ self._request(
24
+ "POST",
25
+ "/api/v0/user/{username}/consoles/{id}/send_input/",
26
+ username=username,
27
+ id=console_id,
28
+ data={"input": input_text},
29
+ )
30
+
31
+ def get_output(self, username: str, console_id: int) -> dict:
32
+ response = self._request(
33
+ "GET",
34
+ "/api/v0/user/{username}/consoles/{id}/get_latest_output/",
35
+ username=username,
36
+ id=console_id,
37
+ )
38
+ return response.json()
39
+
40
+ def kill(self, username: str, console_id: int) -> None:
41
+ self._request(
42
+ "DELETE",
43
+ "/api/v0/user/{username}/consoles/{id}/",
44
+ username=username,
45
+ id=console_id,
46
+ )
@@ -0,0 +1,95 @@
1
+ from pa_cli.api.client import BaseClient
2
+ from pa_cli.exceptions import APIError, NotFoundError
3
+
4
+
5
+ class FilesClient(BaseClient):
6
+ def upload(self, username: str, remote_path: str, content: bytes) -> int:
7
+ url = self._build_url(
8
+ "/api/v0/user/{username}/files/path{remote_path}",
9
+ username=username,
10
+ remote_path=remote_path,
11
+ )
12
+ response = self.session.post(url, files={"content": content})
13
+ if response.status_code == 404:
14
+ raise NotFoundError(f"Not found: {remote_path}")
15
+ try:
16
+ response.raise_for_status()
17
+ except Exception as e:
18
+ raise APIError(f"Upload failed: {response.status_code} {response.text}") from e
19
+ return response.status_code
20
+
21
+ def list(self, username: str, remote_path: str) -> dict:
22
+ """List files and directories at remote path. Returns dict of {name: {type, url}}."""
23
+ url = self._build_url(
24
+ "/api/v0/user/{username}/files/path{remote_path}",
25
+ username=username,
26
+ remote_path=remote_path,
27
+ )
28
+ response = self.session.get(url)
29
+ if response.status_code == 404:
30
+ raise NotFoundError(f"Not found: {remote_path}")
31
+ try:
32
+ response.raise_for_status()
33
+ except Exception as e:
34
+ raise APIError(f"List failed: {response.status_code} {response.text}") from e
35
+ return response.json()
36
+
37
+ def download(self, username: str, remote_path: str) -> bytes:
38
+ """Download a file from remote path. Returns file content as bytes."""
39
+ url = self._build_url(
40
+ "/api/v0/user/{username}/files/path{remote_path}",
41
+ username=username,
42
+ remote_path=remote_path,
43
+ )
44
+ response = self.session.get(url)
45
+ if response.status_code == 404:
46
+ raise NotFoundError(f"Not found: {remote_path}")
47
+ try:
48
+ response.raise_for_status()
49
+ except Exception as e:
50
+ raise APIError(f"Download failed: {response.status_code} {response.text}") from e
51
+ return response.content
52
+
53
+ def delete(self, username: str, remote_path: str) -> None:
54
+ """Delete a file or directory at remote path."""
55
+ url = self._build_url(
56
+ "/api/v0/user/{username}/files/path{remote_path}",
57
+ username=username,
58
+ remote_path=remote_path,
59
+ )
60
+ response = self.session.delete(url)
61
+ if response.status_code == 404:
62
+ raise NotFoundError(f"Not found: {remote_path}")
63
+ try:
64
+ response.raise_for_status()
65
+ except Exception as e:
66
+ raise APIError(f"Delete failed: {response.status_code} {response.text}") from e
67
+
68
+ def share(self, username: str, remote_path: str) -> str:
69
+ """Share a file and return the share URL."""
70
+ response = self._request(
71
+ "POST",
72
+ "/api/v0/user/{username}/files/sharing/",
73
+ username=username,
74
+ json={"path": remote_path},
75
+ )
76
+ return response.json()["url"]
77
+
78
+ def unshare(self, username: str, remote_path: str) -> None:
79
+ """Stop sharing a file."""
80
+ self._request(
81
+ "DELETE",
82
+ "/api/v0/user/{username}/files/sharing/",
83
+ username=username,
84
+ params={"path": remote_path},
85
+ )
86
+
87
+ def get_share_status(self, username: str, remote_path: str) -> str:
88
+ """Get share status for a file. Returns share URL or raises NotFoundError."""
89
+ response = self._request(
90
+ "GET",
91
+ "/api/v0/user/{username}/files/sharing/",
92
+ username=username,
93
+ params={"path": remote_path},
94
+ )
95
+ return response.json()["url"]
@@ -0,0 +1,8 @@
1
+ from pa_cli.api.client import BaseClient
2
+
3
+
4
+ class SystemClient(BaseClient):
5
+ def get_cpu_usage(self, username: str) -> dict:
6
+ """Get CPU usage stats."""
7
+ response = self._request("GET", "/api/v0/user/{username}/cpu/", username=username)
8
+ return response.json()
@@ -0,0 +1,47 @@
1
+ from pa_cli.api.client import BaseClient
2
+
3
+
4
+ class TasksClient(BaseClient):
5
+ def list(self, username: str) -> list:
6
+ """List all scheduled tasks."""
7
+ response = self._request("GET", "/api/v0/user/{username}/schedule/", username=username)
8
+ return response.json()
9
+
10
+ def get(self, username: str, task_id: int) -> dict:
11
+ """Get a specific scheduled task."""
12
+ response = self._request("GET", "/api/v0/user/{username}/schedule/{id}/", username=username, id=task_id)
13
+ return response.json()
14
+
15
+ def create(self, username: str, command: str, interval: str = "daily",
16
+ hour: int = 0, minute: int = 0, enabled: bool = True,
17
+ description: str = "") -> dict:
18
+ """Create a new scheduled task."""
19
+ response = self._request(
20
+ "POST",
21
+ "/api/v0/user/{username}/schedule/",
22
+ username=username,
23
+ json={
24
+ "command": command,
25
+ "interval": interval,
26
+ "hour": hour,
27
+ "minute": minute,
28
+ "enabled": enabled,
29
+ "description": description,
30
+ },
31
+ )
32
+ return response.json()
33
+
34
+ def update(self, username: str, task_id: int, **kwargs) -> dict:
35
+ """Update a scheduled task."""
36
+ response = self._request(
37
+ "PATCH",
38
+ "/api/v0/user/{username}/schedule/{id}/",
39
+ username=username,
40
+ id=task_id,
41
+ json=kwargs,
42
+ )
43
+ return response.json()
44
+
45
+ def delete(self, username: str, task_id: int) -> None:
46
+ """Delete a scheduled task."""
47
+ self._request("DELETE", "/api/v0/user/{username}/schedule/{id}/", username=username, id=task_id)
@@ -0,0 +1,71 @@
1
+ from pa_cli.api.client import BaseClient
2
+
3
+
4
+ class WebappsClient(BaseClient):
5
+ def create(self, username: str, domain_name: str, python_version: str) -> None:
6
+ self._request(
7
+ "POST",
8
+ "/api/v0/user/{username}/webapps/",
9
+ username=username,
10
+ data={"domain_name": domain_name, "python_version": python_version},
11
+ )
12
+
13
+ def update(self, username: str, domain_name: str, **kwargs) -> None:
14
+ self._request(
15
+ "PUT",
16
+ "/api/v0/user/{username}/webapps/{domain_name}/",
17
+ username=username,
18
+ domain_name=domain_name,
19
+ json=kwargs,
20
+ )
21
+
22
+ def delete(self, username: str, domain_name: str) -> None:
23
+ self._request(
24
+ "DELETE",
25
+ "/api/v0/user/{username}/webapps/{domain_name}/",
26
+ username=username,
27
+ domain_name=domain_name,
28
+ )
29
+
30
+ def enable(self, username: str, domain_name: str) -> None:
31
+ self._request(
32
+ "POST",
33
+ "/api/v0/user/{username}/webapps/{domain_name}/enable/",
34
+ username=username,
35
+ domain_name=domain_name,
36
+ )
37
+
38
+ def disable(self, username: str, domain_name: str) -> None:
39
+ self._request(
40
+ "POST",
41
+ "/api/v0/user/{username}/webapps/{domain_name}/disable/",
42
+ username=username,
43
+ domain_name=domain_name,
44
+ )
45
+
46
+ def add_static_file(self, username: str, domain_name: str, url: str, path: str) -> None:
47
+ self._request(
48
+ "POST",
49
+ "/api/v0/user/{username}/webapps/{domain_name}/static_files/",
50
+ username=username,
51
+ domain_name=domain_name,
52
+ data={"url": url, "path": path},
53
+ )
54
+
55
+ def reload(self, username: str, domain_name: str) -> None:
56
+ self._request(
57
+ "POST",
58
+ "/api/v0/user/{username}/webapps/{domain_name}/reload/",
59
+ username=username,
60
+ domain_name=domain_name,
61
+ )
62
+
63
+ def get_ssl_info(self, username: str, domain_name: str) -> dict:
64
+ """Get SSL certificate information."""
65
+ response = self._request(
66
+ "GET",
67
+ "/api/v0/user/{username}/webapps/{domain_name}/ssl/",
68
+ username=username,
69
+ domain_name=domain_name,
70
+ )
71
+ return response.json()
File without changes