Skip to content

Commit 65ac5de

Browse files
Adds list and get subcommands for webapps.
1 parent 70c3507 commit 65ac5de

File tree

2 files changed

+203
-98
lines changed

2 files changed

+203
-98
lines changed

cli/webapp.py

Lines changed: 108 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,51 @@
66
import typer
77
from pythonanywhere_core.webapp import Webapp
88
from snakesay import snakesay
9+
from tabulate import tabulate
910

1011
from pythonanywhere.project import Project
1112
from pythonanywhere.utils import ensure_domain
1213

1314
app = typer.Typer(no_args_is_help=True)
1415

1516

17+
@app.command(name="list")
18+
def list_():
19+
"""List all your webapps"""
20+
webapps = Webapp.list_webapps()
21+
if not webapps:
22+
typer.echo(snakesay("No webapps found."))
23+
return
24+
25+
for webapp in webapps:
26+
typer.echo(webapp['domain_name'])
27+
28+
29+
@app.command()
30+
def get(
31+
domain_name: str = typer.Option(
32+
"your-username.pythonanywhere.com",
33+
"-d",
34+
"--domain",
35+
help="Domain name, eg www.mydomain.com",
36+
)
37+
):
38+
"""Get details for a specific webapp"""
39+
domain_name = ensure_domain(domain_name)
40+
webapp = Webapp(domain_name)
41+
webapp_info = webapp.get()
42+
43+
table = [
44+
["Domain", webapp_info['domain_name']],
45+
["Python version", webapp_info.get('python_version', 'unknown')],
46+
["Source directory", webapp_info.get('source_directory', 'not set')],
47+
["Virtualenv path", webapp_info.get('virtualenv_path', 'not set')],
48+
["Enabled", webapp_info.get('enabled', 'unknown')]
49+
]
50+
51+
typer.echo(tabulate(table, tablefmt="simple"))
52+
53+
1654
@app.command()
1755
def create(
1856
domain_name: str = typer.Option(
@@ -32,6 +70,7 @@ def create(
3270
help="*Irrevocably* delete any existing web app config on this domain. Irrevocably.",
3371
),
3472
):
73+
"""Create a new webapp with virtualenv and project setup"""
3574
domain = ensure_domain(domain_name)
3675
project = Project(domain, python_version)
3776
project.sanity_checks(nuke=nuke)
@@ -50,58 +89,21 @@ def create(
5089
)
5190

5291

53-
class LogType(str, Enum):
54-
access = "access"
55-
error = "error"
56-
server = "server"
57-
all = "all"
58-
59-
60-
def index_callback(value: str):
61-
if value == "all" or (value.isnumeric() and int(value) in range(10)):
62-
return value
63-
raise typer.BadParameter(
64-
"log_index has to be 0 for current log, 1-9 for one of archive logs or all for all of them"
65-
)
66-
67-
6892
@app.command()
69-
def delete_logs(
93+
def reload(
7094
domain_name: str = typer.Option(
7195
"your-username.pythonanywhere.com",
7296
"-d",
7397
"--domain",
7498
help="Domain name, eg www.mydomain.com",
75-
),
76-
log_type: LogType = typer.Option(
77-
LogType.all,
78-
"-t",
79-
"--log-type",
80-
),
81-
log_index: str = typer.Option(
82-
"all",
83-
"-i",
84-
"--log-index",
85-
callback=index_callback,
86-
help="0 for current log, 1-9 for one of archive logs or all for all of them",
87-
),
99+
)
88100
):
89-
webapp = Webapp(ensure_domain(domain_name))
90-
log_types = ["access", "error", "server"]
91-
logs = webapp.get_log_info()
92-
if log_type == "all" and log_index == "all":
93-
for key in log_types:
94-
for log in logs[key]:
95-
webapp.delete_log(key, log)
96-
elif log_type == "all":
97-
for key in log_types:
98-
webapp.delete_log(key, int(log_index))
99-
elif log_index == "all":
100-
for i in logs[log_type]:
101-
webapp.delete_log(log_type, int(i))
102-
else:
103-
webapp.delete_log(log_type, int(log_index))
104-
typer.echo(snakesay("All done!"))
101+
"""Reload a webapp to apply code or configuration changes"""
102+
domain_name = ensure_domain(domain_name)
103+
webapp = Webapp(domain_name)
104+
typer.echo(snakesay(f"Reloading {domain_name} via API"))
105+
webapp.reload()
106+
typer.echo(snakesay(f"{domain_name} has been reloaded"))
105107

106108

107109
@app.command()
@@ -134,6 +136,7 @@ def install_ssl(
134136
"-- this happens by default, use this option to suppress it.",
135137
),
136138
):
139+
"""Install SSL certificate and private key for a webapp"""
137140
with open(certificate_file, "r") as f:
138141
certificate = f.read()
139142

@@ -157,17 +160,73 @@ def install_ssl(
157160
)
158161

159162

163+
class LogType(str, Enum):
164+
access = "access"
165+
error = "error"
166+
server = "server"
167+
all = "all"
168+
169+
170+
def index_callback(value: str):
171+
if value == "all" or (value.isnumeric() and int(value) in range(10)):
172+
return value
173+
raise typer.BadParameter(
174+
"log_index has to be 0 for current log, 1-9 for one of archive logs or all for all of them"
175+
)
176+
177+
160178
@app.command()
161-
def reload(
179+
def delete_logs(
180+
domain_name: str = typer.Option(
181+
"your-username.pythonanywhere.com",
182+
"-d",
183+
"--domain",
184+
help="Domain name, eg www.mydomain.com",
185+
),
186+
log_type: LogType = typer.Option(
187+
LogType.all,
188+
"-t",
189+
"--log-type",
190+
),
191+
log_index: str = typer.Option(
192+
"all",
193+
"-i",
194+
"--log-index",
195+
callback=index_callback,
196+
help="0 for current log, 1-9 for one of archive logs or all for all of them",
197+
),
198+
):
199+
"""Delete webapp log files (access, error, server logs)"""
200+
webapp = Webapp(ensure_domain(domain_name))
201+
log_types = ["access", "error", "server"]
202+
logs = webapp.get_log_info()
203+
if log_type == "all" and log_index == "all":
204+
for key in log_types:
205+
for log in logs[key]:
206+
webapp.delete_log(key, log)
207+
elif log_type == "all":
208+
for key in log_types:
209+
webapp.delete_log(key, int(log_index))
210+
elif log_index == "all":
211+
for i in logs[log_type]:
212+
webapp.delete_log(log_type, int(i))
213+
else:
214+
webapp.delete_log(log_type, int(log_index))
215+
typer.echo(snakesay("All done!"))
216+
217+
218+
@app.command()
219+
def delete(
162220
domain_name: str = typer.Option(
163221
"your-username.pythonanywhere.com",
164222
"-d",
165223
"--domain",
166224
help="Domain name, eg www.mydomain.com",
167225
)
168226
):
227+
"""Delete a webapp"""
169228
domain_name = ensure_domain(domain_name)
170229
webapp = Webapp(domain_name)
171-
typer.echo(snakesay(f"Reloading {domain_name} via API"))
172-
webapp.reload()
173-
typer.echo(snakesay(f"{domain_name} has been reloaded"))
230+
typer.echo(snakesay(f"Deleting {domain_name} via API"))
231+
webapp.delete()
232+
typer.echo(snakesay(f"{domain_name} has been deleted"))

tests/test_cli_webapp.py

Lines changed: 95 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,43 @@ def test_main_subcommand_without_args_prints_help():
4848
assert "Show this message and exit." in result.stdout
4949

5050

51+
def test_list_webapps(mocker):
52+
mock_webapp_class = mocker.patch("cli.webapp.Webapp")
53+
mock_webapp_class.list_webapps.return_value = [
54+
{"domain_name": "example1.com", "python_version": "python38"},
55+
{"domain_name": "example2.pythonanywhere.com", "python_version": "python311"},
56+
]
57+
58+
result = runner.invoke(app, ["list"])
59+
60+
assert result.exit_code == 0
61+
mock_webapp_class.list_webapps.assert_called_once()
62+
assert "example1.com" in result.stdout
63+
assert "example2.pythonanywhere.com" in result.stdout
64+
assert "python38" not in result.stdout
65+
66+
67+
def test_get_webapp(mock_webapp, domain_name):
68+
mock_webapp.return_value.get.return_value = {
69+
"domain_name": domain_name,
70+
"python_version": "python38",
71+
"source_directory": "/home/user/mysite/",
72+
"virtualenv_path": "/home/user/.virtualenvs/mysite/",
73+
"enabled": True,
74+
}
75+
76+
result = runner.invoke(app, ["get", "-d", domain_name])
77+
78+
assert result.exit_code == 0
79+
mock_webapp.assert_called_once_with(domain_name)
80+
mock_webapp.return_value.get.assert_called_once()
81+
assert domain_name in result.stdout
82+
assert "python38" in result.stdout
83+
assert "/home/user/mysite/" in result.stdout
84+
assert "Domain" in result.stdout
85+
assert "Python version" in result.stdout
86+
87+
5188
def test_create_calls_all_stuff_in_right_order(mocker):
5289
mock_project = mocker.patch("cli.webapp.Project")
5390

@@ -78,6 +115,59 @@ def test_create_calls_all_stuff_in_right_order(mocker):
78115
)
79116

80117

118+
def test_reload(mock_webapp, domain_name):
119+
result = runner.invoke(app, ["reload", "-d", domain_name])
120+
121+
assert f"{domain_name} has been reloaded" in result.stdout
122+
mock_webapp.assert_called_once_with(domain_name)
123+
assert mock_webapp.return_value.method_calls == [call.reload()]
124+
125+
126+
def test_install_ssl_with_default_reload(mock_webapp, domain_name, file_with_content):
127+
mock_webapp.return_value.get_ssl_info.return_value = {
128+
"not_after": datetime(2018, 8, 24, 17, 16, 23, tzinfo=tzutc())
129+
}
130+
certificate = "certificate"
131+
certificate_file = file_with_content(certificate)
132+
private_key = "private_key"
133+
private_key_file = file_with_content(private_key)
134+
135+
result = runner.invoke(
136+
app,
137+
["install-ssl", domain_name, certificate_file, private_key_file],
138+
)
139+
140+
mock_webapp.assert_called_once_with(domain_name)
141+
mock_webapp.return_value.set_ssl.assert_called_once_with(certificate, private_key)
142+
mock_webapp.return_value.reload.assert_called_once()
143+
assert f"for {domain_name}" in result.stdout
144+
assert "2018-08-24," in result.stdout
145+
146+
147+
def test_install_ssl_with_reload_suppressed(
148+
mock_webapp, domain_name, file_with_content
149+
):
150+
certificate = "certificate"
151+
certificate_file = file_with_content(certificate)
152+
private_key = "private_key"
153+
private_key_file = file_with_content(private_key)
154+
155+
runner.invoke(
156+
app,
157+
[
158+
"install-ssl",
159+
domain_name,
160+
certificate_file,
161+
private_key_file,
162+
"--suppress-reload",
163+
],
164+
)
165+
166+
mock_webapp.assert_called_once_with(domain_name)
167+
mock_webapp.return_value.set_ssl.assert_called_once_with(certificate, private_key)
168+
mock_webapp.return_value.reload.assert_not_called()
169+
170+
81171
def test_delete_all_logs(mock_webapp, domain_name):
82172
result = runner.invoke(
83173
app,
@@ -154,54 +244,10 @@ def test_validates_log_number(mock_webapp):
154244
assert "log_index has to be 0 for current" in result.stdout
155245

156246

157-
def test_install_ssl_with_default_reload(mock_webapp, domain_name, file_with_content):
158-
mock_webapp.return_value.get_ssl_info.return_value = {
159-
"not_after": datetime(2018, 8, 24, 17, 16, 23, tzinfo=tzutc())
160-
}
161-
certificate = "certificate"
162-
certificate_file = file_with_content(certificate)
163-
private_key = "private_key"
164-
private_key_file = file_with_content(private_key)
247+
def test_delete_webapp(mock_webapp, domain_name):
248+
result = runner.invoke(app, ["delete", "-d", domain_name])
165249

166-
result = runner.invoke(
167-
app,
168-
["install-ssl", domain_name, certificate_file, private_key_file],
169-
)
170-
171-
mock_webapp.assert_called_once_with(domain_name)
172-
mock_webapp.return_value.set_ssl.assert_called_once_with(certificate, private_key)
173-
mock_webapp.return_value.reload.assert_called_once()
174-
assert f"for {domain_name}" in result.stdout
175-
assert "2018-08-24," in result.stdout
176-
177-
178-
def test_install_ssl_with_reload_suppressed(
179-
mock_webapp, domain_name, file_with_content
180-
):
181-
certificate = "certificate"
182-
certificate_file = file_with_content(certificate)
183-
private_key = "private_key"
184-
private_key_file = file_with_content(private_key)
185-
186-
runner.invoke(
187-
app,
188-
[
189-
"install-ssl",
190-
domain_name,
191-
certificate_file,
192-
private_key_file,
193-
"--suppress-reload",
194-
],
195-
)
196-
197-
mock_webapp.assert_called_once_with(domain_name)
198-
mock_webapp.return_value.set_ssl.assert_called_once_with(certificate, private_key)
199-
mock_webapp.return_value.reload.assert_not_called()
200-
201-
202-
def test_reload(mock_webapp, domain_name):
203-
result = runner.invoke(app, ["reload", "-d", domain_name])
204-
205-
assert f"{domain_name} has been reloaded" in result.stdout
250+
assert result.exit_code == 0
206251
mock_webapp.assert_called_once_with(domain_name)
207-
assert mock_webapp.return_value.method_calls == [call.reload()]
252+
mock_webapp.return_value.delete.assert_called_once()
253+
assert f"{domain_name} has been deleted" in result.stdout

0 commit comments

Comments
 (0)