Skip to content

Commit a8fed9d

Browse files
web app creation.
1 parent 576ac4e commit a8fed9d

File tree

3 files changed

+358
-1
lines changed

3 files changed

+358
-1
lines changed

pythonanywhere_core/webapp.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
from __future__ import annotations
2+
3+
import os
4+
import getpass
5+
from pathlib import Path
6+
from textwrap import dedent
7+
8+
from snakesay import snakesay
9+
10+
from pythonanywhere_core.base import call_api, get_api_endpoint, PYTHON_VERSIONS
11+
from pythonanywhere_core.exceptions import SanityException, PythonAnywhereApiException
12+
13+
14+
class Webapp:
15+
def __init__(self, domain: str) -> None:
16+
self.domain = domain
17+
18+
def __eq__(self, other: Webapp) -> bool:
19+
return self.domain == other.domain
20+
21+
def sanity_checks(self, nuke: bool) -> None:
22+
print(snakesay("Running API sanity checks"))
23+
token = os.environ.get("API_TOKEN")
24+
if not token:
25+
raise SanityException(
26+
dedent(
27+
"""
28+
Could not find your API token.
29+
You may need to create it on the Accounts page?
30+
You will also need to close this console and open a new one once you've done that.
31+
"""
32+
)
33+
)
34+
35+
if nuke:
36+
return
37+
38+
endpoint = get_api_endpoint().format(username=getpass.getuser(), flavor="webapps")
39+
url = f"{endpoint}{self.domain}/"
40+
response = call_api(url, "get")
41+
if response.status_code == 200:
42+
raise SanityException(
43+
f"You already have a webapp for {self.domain}.\n\nUse the --nuke option if you want to replace it."
44+
)
45+
46+
def create(self, python_version: str, virtualenv_path: Path, project_path: Path, nuke: bool) -> None:
47+
print(snakesay("Creating web app via API"))
48+
base_url = get_api_endpoint().format(username=getpass.getuser(), flavor="webapps")
49+
domain_url = f"{base_url}{self.domain}/"
50+
if nuke:
51+
call_api(domain_url, "delete")
52+
patch_url = domain_url
53+
response = call_api(
54+
base_url, "post", data={"domain_name": self.domain, "python_version": PYTHON_VERSIONS[python_version]}
55+
)
56+
if not response.ok or response.json().get("status") == "ERROR":
57+
raise PythonAnywhereApiException(
58+
f"POST to create webapp via API failed, got {response}:{response.text}"
59+
)
60+
response = call_api(
61+
patch_url, "patch", data={"virtualenv_path": virtualenv_path, "source_directory": project_path}
62+
)
63+
if not response.ok:
64+
raise PythonAnywhereApiException(
65+
"PATCH to set virtualenv path and source directory via API failed,"
66+
"got {response}:{response_text}".format(response=response, response_text=response.text)
67+
)

tests/conftest.py

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,15 @@
11
import os
2+
import shutil
3+
from getpass import getuser
4+
from pathlib import Path
25

36
import pytest
47
import responses
8+
import tempfile
9+
10+
11+
def _get_temp_dir():
12+
return Path(tempfile.mkdtemp())
513

614

715
@pytest.fixture
@@ -21,4 +29,71 @@ def api_token():
2129
if old_token is None:
2230
del os.environ["API_TOKEN"]
2331
else:
24-
os.environ["API_TOKEN"] = old_token
32+
os.environ["API_TOKEN"] = old_token
33+
34+
35+
@pytest.fixture(scope="session")
36+
def local_pip_cache(request):
37+
previous_cache = request.config.cache.get("pythonanywhere/pip-cache", None)
38+
if previous_cache:
39+
return Path(previous_cache)
40+
else:
41+
new_cache = _get_temp_dir()
42+
request.config.cache.set("pythonanywhere/pip-cache", str(new_cache))
43+
return new_cache
44+
45+
46+
@pytest.fixture
47+
def fake_home(local_pip_cache):
48+
tempdir = _get_temp_dir()
49+
cache_dir = tempdir / ".cache"
50+
cache_dir.mkdir()
51+
(cache_dir / "pip").symlink_to(local_pip_cache)
52+
53+
old_home = os.environ["HOME"]
54+
old_home_contents = set(Path(old_home).iterdir())
55+
56+
os.environ["HOME"] = str(tempdir)
57+
yield tempdir
58+
os.environ["HOME"] = old_home
59+
shutil.rmtree(str(tempdir), ignore_errors=True)
60+
61+
new_stuff = set(Path(old_home).iterdir()) - old_home_contents
62+
if new_stuff:
63+
raise Exception(f"home mocking failed somewehere: {new_stuff}, {tempdir}")
64+
65+
66+
@pytest.fixture
67+
def virtualenvs_folder():
68+
actual_virtualenvs = Path(f"/home/{getuser()}/.virtualenvs")
69+
if actual_virtualenvs.is_dir():
70+
old_virtualenvs = set(Path(actual_virtualenvs).iterdir())
71+
else:
72+
old_virtualenvs = {}
73+
74+
tempdir = _get_temp_dir()
75+
old_workon = os.environ.get("WORKON_HOME")
76+
os.environ["WORKON_HOME"] = str(tempdir)
77+
yield tempdir
78+
if old_workon:
79+
os.environ["WORKON_HOME"] = old_workon
80+
else:
81+
del os.environ["WORKON_HOME"]
82+
shutil.rmtree(str(tempdir), ignore_errors=True)
83+
84+
if actual_virtualenvs.is_dir():
85+
new_envs = set(actual_virtualenvs.iterdir()) - set(old_virtualenvs)
86+
if new_envs:
87+
raise Exception(f"virtualenvs path mocking failed somewehere: {new_envs}, {tempdir}")
88+
89+
90+
@pytest.fixture(scope="function")
91+
def no_api_token():
92+
if "API_TOKEN" not in os.environ:
93+
yield
94+
95+
else:
96+
old_token = os.environ["API_TOKEN"]
97+
del os.environ["API_TOKEN"]
98+
yield
99+
os.environ["API_TOKEN"] = old_token

tests/test_webapp.py

Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
import getpass
2+
import json
3+
4+
import pytest
5+
import responses
6+
from urllib.parse import urlencode
7+
8+
from pythonanywhere_core.base import get_api_endpoint, PYTHON_VERSIONS
9+
from pythonanywhere_core.exceptions import SanityException, PythonAnywhereApiException
10+
from pythonanywhere_core.webapp import Webapp
11+
12+
13+
def test_init():
14+
app = Webapp("www.my-domain.com")
15+
assert app.domain == "www.my-domain.com"
16+
17+
18+
def test_compare_equal():
19+
assert Webapp("www.my-domain.com") == Webapp("www.my-domain.com")
20+
21+
22+
def test_compare_not_equal():
23+
assert Webapp("www.my-domain.com") != Webapp("www.other-domain.com")
24+
25+
26+
@pytest.fixture
27+
def domain():
28+
return "www.domain.com"
29+
30+
31+
@pytest.fixture
32+
def base_url():
33+
return get_api_endpoint().format(username=getpass.getuser(), flavor='webapps')
34+
35+
36+
@pytest.fixture
37+
def domain_url(base_url, domain):
38+
return f"{base_url}{domain}/"
39+
40+
41+
@pytest.fixture
42+
def webapp(domain):
43+
return Webapp(domain)
44+
45+
46+
def test_does_not_complain_if_api_token_exists(api_token, api_responses, domain_url, webapp):
47+
api_responses.add(responses.GET, domain_url, status=404)
48+
webapp.sanity_checks(nuke=False) # should not raise
49+
50+
51+
def test_raises_if_no_api_token_exists(api_responses, no_api_token, webapp):
52+
with pytest.raises(SanityException) as e:
53+
webapp.sanity_checks(nuke=False)
54+
assert "Could not find your API token" in str(e.value)
55+
56+
57+
def test_raises_if_webapp_already_exists(api_token, api_responses, domain, domain_url, webapp):
58+
api_responses.add(
59+
responses.GET,
60+
domain_url,
61+
status=200,
62+
body=json.dumps({"id": 1, "domain_name": domain}),
63+
)
64+
65+
with pytest.raises(SanityException) as e:
66+
webapp.sanity_checks(nuke=False)
67+
68+
assert f"You already have a webapp for {domain}" in str(e.value)
69+
assert "nuke" in str(e.value)
70+
71+
72+
def test_does_not_raise_if_no_webapp(api_token, api_responses, domain_url, webapp):
73+
api_responses.add(responses.GET, domain_url, status=404)
74+
webapp.sanity_checks(nuke=False) # should not raise
75+
76+
77+
def test_nuke_option_overrides_all_but_token_check(
78+
api_token, api_responses, domain, fake_home, virtualenvs_folder, webapp
79+
):
80+
(fake_home / domain).mkdir()
81+
(virtualenvs_folder / domain).mkdir()
82+
83+
webapp.sanity_checks(nuke=True) # should not raise
84+
85+
86+
def test_does_post_to_create_webapp(api_responses, api_token, base_url, domain, domain_url, webapp):
87+
api_responses.add(
88+
responses.POST,
89+
base_url,
90+
status=201,
91+
body=json.dumps({"status": "OK"}),
92+
)
93+
api_responses.add(responses.PATCH, domain_url, status=200)
94+
95+
webapp.create(
96+
"3.10", "/virtualenv/path", "/project/path", nuke=False
97+
)
98+
99+
post = api_responses.calls[0]
100+
assert post.request.url == base_url
101+
assert post.request.body == urlencode(
102+
{"domain_name": domain, "python_version": PYTHON_VERSIONS["3.10"]}
103+
)
104+
assert post.request.headers["Authorization"] == f"Token {api_token}"
105+
106+
107+
def test_does_patch_to_update_virtualenv_path_and_source_directory(
108+
api_responses, api_token, base_url, domain_url, webapp
109+
):
110+
api_responses.add(
111+
responses.POST,
112+
base_url,
113+
status=201,
114+
body=json.dumps({"status": "OK"}),
115+
)
116+
api_responses.add(responses.PATCH, domain_url, status=200)
117+
118+
webapp.create(
119+
"3.10", "/virtualenv/path", "/project/path", nuke=False
120+
)
121+
122+
patch = api_responses.calls[1]
123+
assert patch.request.url == domain_url
124+
assert patch.request.body == urlencode(
125+
{"virtualenv_path": "/virtualenv/path", "source_directory": "/project/path"}
126+
)
127+
assert patch.request.headers["Authorization"] == f"Token {api_token}"
128+
129+
130+
def test_raises_if_post_does_not_20x(api_responses, api_token, base_url, webapp):
131+
api_responses.add(
132+
responses.POST, base_url, status=500, body="an error"
133+
)
134+
135+
with pytest.raises(PythonAnywhereApiException) as e:
136+
webapp.create(
137+
"3.10", "/virtualenv/path", "/project/path", nuke=False
138+
)
139+
140+
assert "POST to create webapp via API failed" in str(e.value)
141+
assert "an error" in str(e.value)
142+
143+
144+
def test_raises_if_post_returns_a_200_with_status_error(api_responses, api_token, base_url, webapp):
145+
api_responses.add(
146+
responses.POST,
147+
base_url,
148+
status=200,
149+
body=json.dumps(
150+
{
151+
"status": "ERROR",
152+
"error_type": "bad",
153+
"error_message": "bad things happened",
154+
}
155+
),
156+
)
157+
158+
with pytest.raises(PythonAnywhereApiException) as e:
159+
webapp.create(
160+
"3.10", "/virtualenv/path", "/project/path", nuke=False
161+
)
162+
163+
assert "POST to create webapp via API failed" in str(e.value)
164+
assert "bad things happened" in str(e.value)
165+
166+
167+
def test_raises_if_patch_does_not_20x(api_responses, api_token, base_url, domain_url, webapp):
168+
api_responses.add(
169+
responses.POST,
170+
base_url,
171+
status=201,
172+
body=json.dumps({"status": "OK"}),
173+
)
174+
api_responses.add(
175+
responses.PATCH,
176+
domain_url,
177+
status=400,
178+
json={"message": "an error"},
179+
)
180+
181+
with pytest.raises(PythonAnywhereApiException) as e:
182+
webapp.create(
183+
"3.7", "/virtualenv/path", "/project/path", nuke=False
184+
)
185+
186+
assert (
187+
"PATCH to set virtualenv path and source directory via API failed"
188+
in str(e.value)
189+
)
190+
assert "an error" in str(e.value)
191+
192+
193+
def test_does_delete_first_for_nuke_call(api_responses, api_token, base_url, domain_url, webapp):
194+
api_responses.add(responses.DELETE, domain_url, status=200)
195+
api_responses.add(
196+
responses.POST, base_url, status=201, body=json.dumps({"status": "OK"})
197+
)
198+
api_responses.add(responses.PATCH, domain_url, status=200)
199+
200+
webapp.create("3.10", "/virtualenv/path", "/project/path", nuke=True)
201+
202+
delete = api_responses.calls[0]
203+
assert delete.request.method == "DELETE"
204+
assert delete.request.url == domain_url
205+
assert delete.request.headers["Authorization"] == f"Token {api_token}"
206+
207+
208+
def test_ignores_404_from_delete_call_when_nuking(api_responses, api_token, base_url, domain_url, webapp):
209+
api_responses.add(responses.DELETE, domain_url, status=404)
210+
api_responses.add(
211+
responses.POST, base_url, status=201, body=json.dumps({"status": "OK"})
212+
)
213+
api_responses.add(responses.PATCH, domain_url, status=200)
214+
215+
webapp.create("3.10", "/virtualenv/path", "/project/path", nuke=True)

0 commit comments

Comments
 (0)