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.
- pa_cli/__init__.py +1 -0
- pa_cli/api/__init__.py +0 -0
- pa_cli/api/always_on.py +22 -0
- pa_cli/api/client.py +45 -0
- pa_cli/api/consoles.py +46 -0
- pa_cli/api/files.py +95 -0
- pa_cli/api/system.py +8 -0
- pa_cli/api/tasks.py +47 -0
- pa_cli/api/webapps.py +71 -0
- pa_cli/cli/__init__.py +0 -0
- pa_cli/cli/account_cmd.py +131 -0
- pa_cli/cli/always_on_cmd.py +82 -0
- pa_cli/cli/consoles_cmd.py +151 -0
- pa_cli/cli/deploy_cmd.py +44 -0
- pa_cli/cli/files_cmd.py +285 -0
- pa_cli/cli/init_cmd.py +61 -0
- pa_cli/cli/main.py +69 -0
- pa_cli/cli/register_cmd.py +32 -0
- pa_cli/cli/status_cmd.py +59 -0
- pa_cli/cli/tasks_cmd.py +137 -0
- pa_cli/cli/utils.py +27 -0
- pa_cli/cli/webapps_cmd.py +262 -0
- pa_cli/config.py +254 -0
- pa_cli/crawler/__init__.py +0 -0
- pa_cli/crawler/account_crawler.py +370 -0
- pa_cli/crawler/console_crawler.py +141 -0
- pa_cli/exceptions.py +18 -0
- pa_cli/workflows/__init__.py +0 -0
- pa_cli/workflows/deploy.py +215 -0
- pythonanywhere_clis-1.0.0.dist-info/METADATA +14 -0
- pythonanywhere_clis-1.0.0.dist-info/RECORD +35 -0
- pythonanywhere_clis-1.0.0.dist-info/WHEEL +5 -0
- pythonanywhere_clis-1.0.0.dist-info/entry_points.txt +2 -0
- pythonanywhere_clis-1.0.0.dist-info/licenses/LICENSE +21 -0
- pythonanywhere_clis-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import re
|
|
2
|
+
|
|
3
|
+
import requests
|
|
4
|
+
import websocket
|
|
5
|
+
from bs4 import BeautifulSoup
|
|
6
|
+
|
|
7
|
+
from pa_cli.exceptions import APIError, AuthError, NetworkError
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ConsoleCrawler:
|
|
11
|
+
def __init__(self, host: str = "www.pythonanywhere.com"):
|
|
12
|
+
self.base_url = f"https://{host}"
|
|
13
|
+
self.session = requests.Session()
|
|
14
|
+
self.session.headers.update({
|
|
15
|
+
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
def login(self, username: str, password: str) -> bool:
|
|
19
|
+
login_url = f"{self.base_url}/login/"
|
|
20
|
+
|
|
21
|
+
try:
|
|
22
|
+
login_page_resp = self.session.get(login_url)
|
|
23
|
+
login_page_resp.raise_for_status()
|
|
24
|
+
except requests.RequestException as e:
|
|
25
|
+
raise NetworkError(f"Failed to fetch login page: {e}") from e
|
|
26
|
+
|
|
27
|
+
soup = BeautifulSoup(login_page_resp.text, "html.parser")
|
|
28
|
+
csrf_input = soup.find("input", {"name": "csrfmiddlewaretoken"})
|
|
29
|
+
if csrf_input is None:
|
|
30
|
+
raise APIError("CSRF token not found on login page")
|
|
31
|
+
|
|
32
|
+
data = {
|
|
33
|
+
"csrfmiddlewaretoken": csrf_input["value"],
|
|
34
|
+
"auth-username": username,
|
|
35
|
+
"auth-password": password,
|
|
36
|
+
"login_view-current_step": "auth",
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
headers = {
|
|
40
|
+
"Referer": login_url,
|
|
41
|
+
"Origin": self.base_url,
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
try:
|
|
45
|
+
login_resp = self.session.post(login_url, data=data, headers=headers)
|
|
46
|
+
login_resp.raise_for_status()
|
|
47
|
+
except requests.RequestException as e:
|
|
48
|
+
raise NetworkError(f"Login request failed: {e}") from e
|
|
49
|
+
|
|
50
|
+
if "/login/" in login_resp.url:
|
|
51
|
+
raise AuthError("Login failed. Check your username and password.")
|
|
52
|
+
|
|
53
|
+
return True
|
|
54
|
+
|
|
55
|
+
def list(self, username: str) -> list:
|
|
56
|
+
url = f"{self.base_url}/api/v0/user/{username}/consoles/"
|
|
57
|
+
|
|
58
|
+
try:
|
|
59
|
+
resp = self.session.get(url)
|
|
60
|
+
resp.raise_for_status()
|
|
61
|
+
except requests.RequestException as e:
|
|
62
|
+
raise NetworkError(f"Failed to list consoles: {e}") from e
|
|
63
|
+
|
|
64
|
+
return resp.json()
|
|
65
|
+
|
|
66
|
+
def create(self, username: str, executable: str = "bash") -> dict:
|
|
67
|
+
csrftoken = self.session.cookies.get("csrftoken")
|
|
68
|
+
if not csrftoken:
|
|
69
|
+
raise APIError("CSRF token not found in session cookies")
|
|
70
|
+
|
|
71
|
+
url = f"{self.base_url}/api/v0/user/{username}/consoles/"
|
|
72
|
+
headers = {
|
|
73
|
+
"Referer": f"{self.base_url}/user/{username}/consoles/",
|
|
74
|
+
"X-CSRFToken": csrftoken,
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
try:
|
|
78
|
+
resp = self.session.post(url, json={"executable": executable}, headers=headers)
|
|
79
|
+
resp.raise_for_status()
|
|
80
|
+
except requests.RequestException as e:
|
|
81
|
+
raise NetworkError(f"Failed to create console: {e}") from e
|
|
82
|
+
|
|
83
|
+
return resp.json()
|
|
84
|
+
|
|
85
|
+
def delete(self, username: str, console_id: int) -> None:
|
|
86
|
+
csrftoken = self.session.cookies.get("csrftoken")
|
|
87
|
+
if not csrftoken:
|
|
88
|
+
raise APIError("CSRF token not found in session cookies")
|
|
89
|
+
|
|
90
|
+
url = f"{self.base_url}/api/v0/user/{username}/consoles/{console_id}/"
|
|
91
|
+
headers = {
|
|
92
|
+
"Referer": f"{self.base_url}/user/{username}/consoles/",
|
|
93
|
+
"X-CSRFToken": csrftoken,
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
try:
|
|
97
|
+
resp = self.session.delete(url, headers=headers)
|
|
98
|
+
resp.raise_for_status()
|
|
99
|
+
except requests.RequestException as e:
|
|
100
|
+
raise NetworkError(f"Failed to delete console: {e}") from e
|
|
101
|
+
|
|
102
|
+
def get_or_create(self, username: str, executable: str = "bash") -> int:
|
|
103
|
+
consoles = self.list(username)
|
|
104
|
+
|
|
105
|
+
if len(consoles) >= 2:
|
|
106
|
+
self.delete(username, consoles[0]["id"])
|
|
107
|
+
consoles = []
|
|
108
|
+
|
|
109
|
+
if consoles:
|
|
110
|
+
return consoles[0]["id"]
|
|
111
|
+
|
|
112
|
+
new_console = self.create(username, executable=executable)
|
|
113
|
+
return new_console["id"]
|
|
114
|
+
|
|
115
|
+
def activate(self, username: str, console_id: int) -> None:
|
|
116
|
+
frame_url = f"{self.base_url}/user/{username}/consoles/{console_id}/frame/"
|
|
117
|
+
|
|
118
|
+
try:
|
|
119
|
+
resp = self.session.get(frame_url)
|
|
120
|
+
resp.raise_for_status()
|
|
121
|
+
except requests.RequestException as e:
|
|
122
|
+
raise NetworkError(f"Failed to fetch console frame page: {e}") from e
|
|
123
|
+
|
|
124
|
+
match = re.search(r'LoadConsole\("([^"]+)",\s*"([^"]+)",\s*"([^"]+)"', resp.text)
|
|
125
|
+
if not match:
|
|
126
|
+
raise APIError("Could not parse WebSocket info from frame page")
|
|
127
|
+
|
|
128
|
+
console_server = match.group(1)
|
|
129
|
+
session_key = match.group(2)
|
|
130
|
+
parsed_console_id = match.group(3)
|
|
131
|
+
|
|
132
|
+
ws = None
|
|
133
|
+
try:
|
|
134
|
+
ws = websocket.create_connection(f"wss://{console_server}/sj/websocket")
|
|
135
|
+
ws.send(f"\x1b[{session_key};{parsed_console_id};;a")
|
|
136
|
+
ws.send("\x1b[8;24;80t")
|
|
137
|
+
except Exception as e:
|
|
138
|
+
raise NetworkError(f"WebSocket connection failed: {e}") from e
|
|
139
|
+
finally:
|
|
140
|
+
if ws:
|
|
141
|
+
ws.close()
|
pa_cli/exceptions.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
class PACliError(Exception):
|
|
2
|
+
"""Base exception for all pa-cli errors."""
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class AuthError(PACliError):
|
|
6
|
+
"""Authentication failed (login, token, password)."""
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class APIError(PACliError):
|
|
10
|
+
"""PythonAnywhere API returned an error."""
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class NotFoundError(APIError):
|
|
14
|
+
"""Requested resource does not exist (404)."""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class NetworkError(PACliError):
|
|
18
|
+
"""Network connection failed."""
|
|
File without changes
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import time
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
import typer
|
|
5
|
+
from rich.progress import Progress, SpinnerColumn, BarColumn, TextColumn, TimeRemainingColumn
|
|
6
|
+
|
|
7
|
+
from pa_cli.api.files import FilesClient
|
|
8
|
+
from pa_cli.api.consoles import ConsolesClient
|
|
9
|
+
from pa_cli.api.webapps import WebappsClient
|
|
10
|
+
from pa_cli.exceptions import PACliError
|
|
11
|
+
|
|
12
|
+
POLL_INTERVAL = 2 # seconds between output checks
|
|
13
|
+
MAX_WAIT = 300 # max seconds to wait for a command
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _wait_for_console(client: ConsolesClient, username: str, console_id: int) -> str:
|
|
17
|
+
"""Poll console output until the prompt returns or timeout."""
|
|
18
|
+
elapsed = 0
|
|
19
|
+
last_output = ""
|
|
20
|
+
while elapsed < MAX_WAIT:
|
|
21
|
+
time.sleep(POLL_INTERVAL)
|
|
22
|
+
elapsed += POLL_INTERVAL
|
|
23
|
+
result = client.get_output(username, console_id)
|
|
24
|
+
output = result.get("output", "")
|
|
25
|
+
if output != last_output:
|
|
26
|
+
last_output = output
|
|
27
|
+
# PythonAnywhere bash prompt patterns:
|
|
28
|
+
# - "username@host:~$ " (standard)
|
|
29
|
+
# - "$ " (simple)
|
|
30
|
+
# - ">>> " (Python REPL)
|
|
31
|
+
stripped = output.rstrip()
|
|
32
|
+
if (stripped.endswith("$") or
|
|
33
|
+
stripped.endswith(">>>") or
|
|
34
|
+
stripped.endswith("]$") or
|
|
35
|
+
stripped.endswith("#")):
|
|
36
|
+
break
|
|
37
|
+
return last_output
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def collect_files(local_dir: str) -> list:
|
|
41
|
+
"""Collect all files in directory recursively."""
|
|
42
|
+
local_path = Path(local_dir)
|
|
43
|
+
return [f for f in local_path.rglob("*") if f.is_file()]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def deploy_preview(account: dict, local_dir: str, domain: str, python_version: str) -> None:
|
|
47
|
+
"""Print deploy preview without executing any operations."""
|
|
48
|
+
local_path = Path(local_dir)
|
|
49
|
+
username = account["username"]
|
|
50
|
+
remote_base = f"/home/{username}/{local_path.name}"
|
|
51
|
+
|
|
52
|
+
typer.echo("=== 部署预览 (dry-run) ===\n")
|
|
53
|
+
|
|
54
|
+
# Step 1: File list
|
|
55
|
+
typer.echo("📁 Step 1: 将要上传的文件")
|
|
56
|
+
files = collect_files(local_dir)
|
|
57
|
+
max_display = 20
|
|
58
|
+
for f in files[:max_display]:
|
|
59
|
+
size = f.stat().st_size
|
|
60
|
+
typer.echo(f" - {f.relative_to(local_path)} ({size} bytes)")
|
|
61
|
+
if len(files) > max_display:
|
|
62
|
+
typer.echo(f" ... 还有 {len(files) - max_display} 个文件")
|
|
63
|
+
typer.echo(f" 共 {len(files)} 个文件\n")
|
|
64
|
+
|
|
65
|
+
# Step 2: Environment setup
|
|
66
|
+
typer.echo("📦 Step 2: 环境配置")
|
|
67
|
+
typer.echo(" - 创建 bash console")
|
|
68
|
+
typer.echo(f" - cd {remote_base}")
|
|
69
|
+
if (local_path / "requirements.txt").exists():
|
|
70
|
+
typer.echo(f" - mkvirtualenv {local_path.name} --python=/usr/bin/{python_version}")
|
|
71
|
+
typer.echo(f" - workon {local_path.name} && pip install -r requirements.txt")
|
|
72
|
+
typer.echo()
|
|
73
|
+
|
|
74
|
+
# Step 3: Webapp configuration
|
|
75
|
+
typer.echo("⚙️ Step 3: Webapp 配置")
|
|
76
|
+
typer.echo(f" - 创建/更新 webapp: {domain}")
|
|
77
|
+
typer.echo(f" - 源码目录: {remote_base}")
|
|
78
|
+
typer.echo()
|
|
79
|
+
|
|
80
|
+
# Step 4: Static file mapping
|
|
81
|
+
if (local_path / "static").exists():
|
|
82
|
+
typer.echo("📂 Step 4: 静态文件映射")
|
|
83
|
+
typer.echo(f" - /static/ -> {remote_base}/static")
|
|
84
|
+
typer.echo()
|
|
85
|
+
else:
|
|
86
|
+
typer.echo("📂 Step 4: 无 static 目录,跳过\n")
|
|
87
|
+
|
|
88
|
+
# Step 5: Reload
|
|
89
|
+
typer.echo("🔄 Step 5: Reload webapp\n")
|
|
90
|
+
|
|
91
|
+
typer.echo("以上为预览,使用不带 --dry-run 的命令执行:")
|
|
92
|
+
typer.echo(f" pa deploy {local_dir}")
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def deploy(
|
|
96
|
+
local_dir: str,
|
|
97
|
+
username: str,
|
|
98
|
+
token: str,
|
|
99
|
+
host: str,
|
|
100
|
+
domain: str,
|
|
101
|
+
python_version: str = "python310",
|
|
102
|
+
dry_run: bool = False,
|
|
103
|
+
) -> str:
|
|
104
|
+
local_path = Path(local_dir)
|
|
105
|
+
if not local_path.is_dir():
|
|
106
|
+
raise PACliError(f"{local_dir} is not a directory")
|
|
107
|
+
|
|
108
|
+
if dry_run:
|
|
109
|
+
account = {"username": username}
|
|
110
|
+
deploy_preview(account, local_dir, domain, python_version)
|
|
111
|
+
return ""
|
|
112
|
+
|
|
113
|
+
remote_base = f"/home/{username}/{local_path.name}"
|
|
114
|
+
files_client = FilesClient(token=token, host=host)
|
|
115
|
+
consoles_client = ConsolesClient(token=token, host=host)
|
|
116
|
+
webapps_client = WebappsClient(token=token, host=host)
|
|
117
|
+
|
|
118
|
+
# Step 1: Upload files with progress bar
|
|
119
|
+
print(f"[1/5] Uploading {local_dir} to {remote_base}...")
|
|
120
|
+
try:
|
|
121
|
+
files = collect_files(local_dir)
|
|
122
|
+
with Progress(
|
|
123
|
+
SpinnerColumn(),
|
|
124
|
+
TextColumn("[progress.description]{task.description}"),
|
|
125
|
+
BarColumn(),
|
|
126
|
+
TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
|
|
127
|
+
TextColumn("({task.completed}/{task.total})"),
|
|
128
|
+
TimeRemainingColumn(),
|
|
129
|
+
) as progress:
|
|
130
|
+
task = progress.add_task("Uploading files...", total=len(files))
|
|
131
|
+
for file in files:
|
|
132
|
+
relative = file.relative_to(local_path)
|
|
133
|
+
remote = f"{remote_base}/{relative}".replace("\\", "/")
|
|
134
|
+
content = file.read_bytes()
|
|
135
|
+
files_client.upload(username, remote, content)
|
|
136
|
+
progress.advance(task)
|
|
137
|
+
print(f" ✓ Uploaded {len(files)} files.")
|
|
138
|
+
except Exception as e:
|
|
139
|
+
print(f" ✗ Upload failed: {e}")
|
|
140
|
+
print(f" Hint: Retry with `pa deploy {local_dir} {domain}`")
|
|
141
|
+
raise
|
|
142
|
+
|
|
143
|
+
# Step 2: Setup environment via console
|
|
144
|
+
print(f"[2/5] Setting up environment...")
|
|
145
|
+
try:
|
|
146
|
+
console = consoles_client.create(username)
|
|
147
|
+
console_id = console["id"]
|
|
148
|
+
|
|
149
|
+
commands = [f"cd {remote_base}"]
|
|
150
|
+
if (local_path / "requirements.txt").exists():
|
|
151
|
+
commands.extend([
|
|
152
|
+
f"mkvirtualenv {local_path.name} --python=/usr/bin/{python_version}",
|
|
153
|
+
f"workon {local_path.name} && pip install -r requirements.txt",
|
|
154
|
+
])
|
|
155
|
+
|
|
156
|
+
for cmd in commands:
|
|
157
|
+
consoles_client.send_input(username, console_id, cmd + "\n")
|
|
158
|
+
_wait_for_console(consoles_client, username, console_id)
|
|
159
|
+
print(f" ✓ Environment configured.")
|
|
160
|
+
except Exception as e:
|
|
161
|
+
print(f" ✗ Environment setup failed: {e}")
|
|
162
|
+
print(f" Hint: Check requirements.txt and retry.")
|
|
163
|
+
raise
|
|
164
|
+
|
|
165
|
+
# Step 3: Create and configure webapp
|
|
166
|
+
print(f"[3/5] Configuring webapp {domain}...")
|
|
167
|
+
try:
|
|
168
|
+
webapps_client.create(username, domain, python_version)
|
|
169
|
+
except Exception as e:
|
|
170
|
+
if "already exists" not in str(e).lower():
|
|
171
|
+
print(f" ✗ Webapp creation failed: {e}")
|
|
172
|
+
raise
|
|
173
|
+
|
|
174
|
+
try:
|
|
175
|
+
webapps_client.update(
|
|
176
|
+
username,
|
|
177
|
+
domain,
|
|
178
|
+
source_directory=remote_base,
|
|
179
|
+
)
|
|
180
|
+
print(f" ✓ Webapp configured.")
|
|
181
|
+
except Exception as e:
|
|
182
|
+
print(f" ✗ Webapp configuration failed: {e}")
|
|
183
|
+
raise
|
|
184
|
+
|
|
185
|
+
# Step 4: Add static file mapping if static dir exists
|
|
186
|
+
static_local = local_path / "static"
|
|
187
|
+
if static_local.exists():
|
|
188
|
+
print(f"[4/5] Adding static file mapping...")
|
|
189
|
+
try:
|
|
190
|
+
webapps_client.add_static_file(
|
|
191
|
+
username,
|
|
192
|
+
domain,
|
|
193
|
+
url="/static/",
|
|
194
|
+
path=f"{remote_base}/static",
|
|
195
|
+
)
|
|
196
|
+
print(f" ✓ Static files mapped.")
|
|
197
|
+
except Exception as e:
|
|
198
|
+
print(f" ✗ Static file mapping failed: {e}")
|
|
199
|
+
raise
|
|
200
|
+
else:
|
|
201
|
+
print(f"[4/5] No static directory, skipping.")
|
|
202
|
+
|
|
203
|
+
# Step 5: Reload
|
|
204
|
+
print(f"[5/5] Reloading webapp...")
|
|
205
|
+
try:
|
|
206
|
+
webapps_client.reload(username, domain)
|
|
207
|
+
print(f" ✓ Webapp reloaded.")
|
|
208
|
+
except Exception as e:
|
|
209
|
+
print(f" ✗ Reload failed: {e}")
|
|
210
|
+
print(f" Hint: Run `pa webapp reload {domain}` manually.")
|
|
211
|
+
raise
|
|
212
|
+
|
|
213
|
+
url = f"https://{domain}"
|
|
214
|
+
print(f"\nDeploy complete: {url}")
|
|
215
|
+
return url
|
|
@@ -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,35 @@
|
|
|
1
|
+
pa_cli/__init__.py,sha256=kUR5RAFc7HCeiqdlX36dZOHkUI5wI6V_43RpEcD8b-0,22
|
|
2
|
+
pa_cli/config.py,sha256=lspnQqOiy2IQvt3TbluBAFO1TMd_Pd-eVQp5yv_QPKo,9101
|
|
3
|
+
pa_cli/exceptions.py,sha256=8iCO-E0CmMFLPoc8DD42Q_pBwydMr9UTvdZu0v31atI,399
|
|
4
|
+
pa_cli/api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
+
pa_cli/api/always_on.py,sha256=VdZH2fL6xoPm0oW9ZZhPqQ58WEALQz7s4GnMxg9jN54,854
|
|
6
|
+
pa_cli/api/client.py,sha256=2a9HhIEGo2km18Z1ZJ7e8cI8jOOIUrG2cLwUNmt3eGA,1693
|
|
7
|
+
pa_cli/api/consoles.py,sha256=Jlc7ESH-323Jc6U8AzQrtZCzkawxL69YX65nbPyB-Kc,1405
|
|
8
|
+
pa_cli/api/files.py,sha256=QgROylqN0JCMDmms9Vo9skPQ8tH13VepnEvSOvQbRPI,3731
|
|
9
|
+
pa_cli/api/system.py,sha256=jPtVim9yI9HDkW-p8Mp5iw5nvjERWHNv9dxbSk6XZ54,284
|
|
10
|
+
pa_cli/api/tasks.py,sha256=xqbD7pAIJZIoChvFJ5cu3D7FhV60mtNib1pnAo_PdwI,1735
|
|
11
|
+
pa_cli/api/webapps.py,sha256=BqTvWEd4vqGa-nJcLEqOOlspsq6a_mO52eOgn8jqH84,2347
|
|
12
|
+
pa_cli/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
13
|
+
pa_cli/cli/account_cmd.py,sha256=sQ6tDOQQdEYFVv3X96XMLSI8IGgo1g5VV3HRdONr4Bg,4399
|
|
14
|
+
pa_cli/cli/always_on_cmd.py,sha256=vXVIUYIU1-GDGZGaIfNxFaxqnsZnXEftgnfwA4Z87ls,3034
|
|
15
|
+
pa_cli/cli/consoles_cmd.py,sha256=iVM8A3eWA3TB5CqAoz2CxDTiNMndFfLCnORC0gy3IAs,5295
|
|
16
|
+
pa_cli/cli/deploy_cmd.py,sha256=HmEW9puQYyo0qQLycaSiCpY7EOvKh2ZaJrrSXRsY9lY,1601
|
|
17
|
+
pa_cli/cli/files_cmd.py,sha256=Pw69uT1p308AbMPtkh74TVAWh92TV6XsqAm3Th3pK5I,10762
|
|
18
|
+
pa_cli/cli/init_cmd.py,sha256=dF10pgI-JY0iViZMQPix9iBtWYZbn1Et_-UEsj-huj4,2415
|
|
19
|
+
pa_cli/cli/main.py,sha256=pZI3z17gy4jDnjXDn0AdIe1vFgnCXQKorWZcdvaYD1E,2451
|
|
20
|
+
pa_cli/cli/register_cmd.py,sha256=NgUptosvelNLpip74zBqsbBw1ZNmoXV8qWiY47n0dvU,1173
|
|
21
|
+
pa_cli/cli/status_cmd.py,sha256=Aq4B_GGcHCbLLkZpNp-1FjbHOWmnLYLNyvJY7kXBQLk,2067
|
|
22
|
+
pa_cli/cli/tasks_cmd.py,sha256=xhnHokyFQExYuhSAy78nMm0qioAQsvfLpPeGUdf27W4,4966
|
|
23
|
+
pa_cli/cli/utils.py,sha256=2YhPeBLG-I7YAjfS-GObw8_Ya5q5DrXxCFIgDMTZUm4,788
|
|
24
|
+
pa_cli/cli/webapps_cmd.py,sha256=s4UZd9Nvot1We3q4pn-oHKcc2bXU2kWWFCFlwBY-has,10044
|
|
25
|
+
pa_cli/crawler/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
26
|
+
pa_cli/crawler/account_crawler.py,sha256=dckmsaDahu_Qku3tMS-1kRai07P4enD2P9YJT8nnjzU,14612
|
|
27
|
+
pa_cli/crawler/console_crawler.py,sha256=Kkf8zwjPADkhOAgP41ZzqD0ynqCYB2yyhWi-75DABVg,4928
|
|
28
|
+
pa_cli/workflows/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
29
|
+
pa_cli/workflows/deploy.py,sha256=eyL-kj9Vu850_zYu9jKd-V7o5uh-Q2NVGoeolHQe8wY,7570
|
|
30
|
+
pythonanywhere_clis-1.0.0.dist-info/licenses/LICENSE,sha256=SmW5HVgeBBg_e-WKSP-Fr2H1-Cz-K0uLb99Hl_rOSKE,1088
|
|
31
|
+
pythonanywhere_clis-1.0.0.dist-info/METADATA,sha256=sGmWcU3lsTC2p7X35AxEsw-LzPvY0Yrp5n57xX6G4to,454
|
|
32
|
+
pythonanywhere_clis-1.0.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
33
|
+
pythonanywhere_clis-1.0.0.dist-info/entry_points.txt,sha256=AfIEhKkIidqwUmJGZXyv6cRk8rajD39WhgpmdxBkp5E,43
|
|
34
|
+
pythonanywhere_clis-1.0.0.dist-info/top_level.txt,sha256=xYc8GaLj6sfmBa_9zcWs60tYHX-xQihKcB91Te74q2c,7
|
|
35
|
+
pythonanywhere_clis-1.0.0.dist-info/RECORD,,
|
|
@@ -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 @@
|
|
|
1
|
+
pa_cli
|