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.
- pythonanywhere_clis-1.0.0/LICENSE +21 -0
- pythonanywhere_clis-1.0.0/PKG-INFO +14 -0
- pythonanywhere_clis-1.0.0/README.md +262 -0
- pythonanywhere_clis-1.0.0/pa_cli/__init__.py +1 -0
- pythonanywhere_clis-1.0.0/pa_cli/api/__init__.py +0 -0
- pythonanywhere_clis-1.0.0/pa_cli/api/always_on.py +22 -0
- pythonanywhere_clis-1.0.0/pa_cli/api/client.py +45 -0
- pythonanywhere_clis-1.0.0/pa_cli/api/consoles.py +46 -0
- pythonanywhere_clis-1.0.0/pa_cli/api/files.py +95 -0
- pythonanywhere_clis-1.0.0/pa_cli/api/system.py +8 -0
- pythonanywhere_clis-1.0.0/pa_cli/api/tasks.py +47 -0
- pythonanywhere_clis-1.0.0/pa_cli/api/webapps.py +71 -0
- pythonanywhere_clis-1.0.0/pa_cli/cli/__init__.py +0 -0
- pythonanywhere_clis-1.0.0/pa_cli/cli/account_cmd.py +131 -0
- pythonanywhere_clis-1.0.0/pa_cli/cli/always_on_cmd.py +82 -0
- pythonanywhere_clis-1.0.0/pa_cli/cli/consoles_cmd.py +151 -0
- pythonanywhere_clis-1.0.0/pa_cli/cli/deploy_cmd.py +44 -0
- pythonanywhere_clis-1.0.0/pa_cli/cli/files_cmd.py +285 -0
- pythonanywhere_clis-1.0.0/pa_cli/cli/init_cmd.py +61 -0
- pythonanywhere_clis-1.0.0/pa_cli/cli/main.py +69 -0
- pythonanywhere_clis-1.0.0/pa_cli/cli/register_cmd.py +32 -0
- pythonanywhere_clis-1.0.0/pa_cli/cli/status_cmd.py +59 -0
- pythonanywhere_clis-1.0.0/pa_cli/cli/tasks_cmd.py +137 -0
- pythonanywhere_clis-1.0.0/pa_cli/cli/utils.py +27 -0
- pythonanywhere_clis-1.0.0/pa_cli/cli/webapps_cmd.py +262 -0
- pythonanywhere_clis-1.0.0/pa_cli/config.py +254 -0
- pythonanywhere_clis-1.0.0/pa_cli/crawler/__init__.py +0 -0
- pythonanywhere_clis-1.0.0/pa_cli/crawler/account_crawler.py +370 -0
- pythonanywhere_clis-1.0.0/pa_cli/crawler/console_crawler.py +141 -0
- pythonanywhere_clis-1.0.0/pa_cli/exceptions.py +18 -0
- pythonanywhere_clis-1.0.0/pa_cli/workflows/__init__.py +0 -0
- pythonanywhere_clis-1.0.0/pa_cli/workflows/deploy.py +215 -0
- pythonanywhere_clis-1.0.0/pyproject.toml +27 -0
- pythonanywhere_clis-1.0.0/pythonanywhere_clis.egg-info/PKG-INFO +14 -0
- pythonanywhere_clis-1.0.0/pythonanywhere_clis.egg-info/SOURCES.txt +41 -0
- pythonanywhere_clis-1.0.0/pythonanywhere_clis.egg-info/dependency_links.txt +1 -0
- pythonanywhere_clis-1.0.0/pythonanywhere_clis.egg-info/entry_points.txt +2 -0
- pythonanywhere_clis-1.0.0/pythonanywhere_clis.egg-info/requires.txt +8 -0
- pythonanywhere_clis-1.0.0/pythonanywhere_clis.egg-info/top_level.txt +1 -0
- pythonanywhere_clis-1.0.0/setup.cfg +4 -0
- pythonanywhere_clis-1.0.0/tests/test_account_crawler.py +1196 -0
- pythonanywhere_clis-1.0.0/tests/test_config.py +662 -0
- 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
|