pythonanywhere-core 0.1.7__tar.gz → 0.1.8__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_core-0.1.7 → pythonanywhere_core-0.1.8}/PKG-INFO +1 -1
- {pythonanywhere_core-0.1.7 → pythonanywhere_core-0.1.8}/pyproject.toml +6 -4
- pythonanywhere_core-0.1.8/pythonanywhere_core/__init__.py +1 -0
- {pythonanywhere_core-0.1.7 → pythonanywhere_core-0.1.8}/pythonanywhere_core/base.py +10 -0
- pythonanywhere_core-0.1.8/pythonanywhere_core/students.py +49 -0
- {pythonanywhere_core-0.1.7 → pythonanywhere_core-0.1.8}/pythonanywhere_core/webapp.py +44 -1
- pythonanywhere_core-0.1.7/pythonanywhere_core/__init__.py +0 -1
- {pythonanywhere_core-0.1.7 → pythonanywhere_core-0.1.8}/LICENSE +0 -0
- {pythonanywhere_core-0.1.7 → pythonanywhere_core-0.1.8}/README.rst +0 -0
- {pythonanywhere_core-0.1.7 → pythonanywhere_core-0.1.8}/pythonanywhere_core/exceptions.py +0 -0
- {pythonanywhere_core-0.1.7 → pythonanywhere_core-0.1.8}/pythonanywhere_core/files.py +0 -0
- {pythonanywhere_core-0.1.7 → pythonanywhere_core-0.1.8}/pythonanywhere_core/schedule.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "pythonanywhere-core"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.8"
|
|
4
4
|
description = "API wrapper for programmatic management of PythonAnywhere services."
|
|
5
5
|
authors = ["PythonAnywhere <developers@pythonanywhere.com>"]
|
|
6
6
|
license = "MIT"
|
|
@@ -26,10 +26,12 @@ snakesay = "^0.10.2"
|
|
|
26
26
|
typing_extensions = "^4.5.0"
|
|
27
27
|
|
|
28
28
|
[tool.poetry.dev-dependencies]
|
|
29
|
-
pytest = "^
|
|
30
|
-
pytest-cov = "^
|
|
29
|
+
pytest = "^8.0.0"
|
|
30
|
+
pytest-cov = "^5.0.0"
|
|
31
31
|
pytest-mock = "^3.10.0"
|
|
32
|
-
responses = "^0.
|
|
32
|
+
responses = "^0.25.0"
|
|
33
|
+
sphinx = "7.1.2"
|
|
34
|
+
sphinx-rtd-theme = "^2.0.0"
|
|
33
35
|
|
|
34
36
|
[tool.black]
|
|
35
37
|
line-length = 120
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = '0.1.8'
|
|
@@ -41,6 +41,16 @@ def helpful_token_error_message() -> str:
|
|
|
41
41
|
|
|
42
42
|
|
|
43
43
|
def call_api(url: str, method: str, **kwargs) -> requests.Response:
|
|
44
|
+
"""Calls PythonAnywhere API with given url and method.
|
|
45
|
+
|
|
46
|
+
:param url: url to call
|
|
47
|
+
:param method: HTTP method to use
|
|
48
|
+
:param kwargs: additional keyword arguments to pass to requests.request
|
|
49
|
+
:returns: requests.Response object
|
|
50
|
+
|
|
51
|
+
:raises AuthenticationError: if API returns 401
|
|
52
|
+
:raises NoTokenError: if API_TOKEN environment variable is not set"""
|
|
53
|
+
|
|
44
54
|
token = os.environ.get("API_TOKEN")
|
|
45
55
|
if token is None:
|
|
46
56
|
raise NoTokenError(helpful_token_error_message())
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import getpass
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
from pythonanywhere_core.base import call_api, get_api_endpoint
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class StudentsAPI:
|
|
8
|
+
"""Interface for PythonAnywhere students API.
|
|
9
|
+
|
|
10
|
+
Uses `pythonanywhere.api.base` :method: `get_api_endpoint` to
|
|
11
|
+
create url, which is stored in a class variable `StudentsAPI.base_url`,
|
|
12
|
+
then calls `call_api` with appropriate arguments to execute student
|
|
13
|
+
action.
|
|
14
|
+
|
|
15
|
+
Covers:
|
|
16
|
+
- GET
|
|
17
|
+
- DELETE
|
|
18
|
+
|
|
19
|
+
Methods:
|
|
20
|
+
- use :method: `StudentsAPI.get` to get list of students
|
|
21
|
+
- use :method: `StudentsAPI.delete` to remove a student
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
base_url: str = get_api_endpoint().format(username=getpass.getuser(), flavor="students")
|
|
25
|
+
|
|
26
|
+
def get(self) -> Optional[dict]:
|
|
27
|
+
"""Returns list of PythonAnywhere students related with user's account."""
|
|
28
|
+
|
|
29
|
+
result = call_api(self.base_url, "GET")
|
|
30
|
+
|
|
31
|
+
if result.status_code == 200:
|
|
32
|
+
return result.json()
|
|
33
|
+
|
|
34
|
+
raise Exception(f"GET to list students failed, got {result.text}")
|
|
35
|
+
|
|
36
|
+
def delete(self, student_username: str) -> Optional[int]:
|
|
37
|
+
"""Returns 204 if student has been successfully removed, raises otherwise."""
|
|
38
|
+
|
|
39
|
+
url = f"{self.base_url}{student_username}"
|
|
40
|
+
|
|
41
|
+
result = call_api(url, "DELETE")
|
|
42
|
+
|
|
43
|
+
if result.status_code == 204:
|
|
44
|
+
return result.status_code
|
|
45
|
+
|
|
46
|
+
detail = f": {result.text}" if result.text else ""
|
|
47
|
+
raise Exception(
|
|
48
|
+
f"DELETE to remove student {student_username!r} failed, got {result}{detail}"
|
|
49
|
+
)
|
|
@@ -18,6 +18,14 @@ class Webapp:
|
|
|
18
18
|
create url, which is stored in a class variable `Webapp.base_url`,
|
|
19
19
|
then calls `call_api` with appropriate arguments to execute webapps
|
|
20
20
|
action.
|
|
21
|
+
|
|
22
|
+
Use :method: `Webapp.create` to create new webapp.
|
|
23
|
+
Use :method: `Webapp.reload` to reload webapp.
|
|
24
|
+
Use :method: `Webapp.set_ssl` to set SSL certificate and private key.
|
|
25
|
+
Use :method: `Webapp.get_ssl_info` to get SSL certificate info.
|
|
26
|
+
Use :method: `Webapp.delete_log` to delete log file.
|
|
27
|
+
Use :method: `Webapp.get_log_info` to get log files info.
|
|
28
|
+
|
|
21
29
|
"""
|
|
22
30
|
def __init__(self, domain: str) -> None:
|
|
23
31
|
self.endpoint = get_api_endpoint()
|
|
@@ -31,6 +39,7 @@ class Webapp:
|
|
|
31
39
|
return self.domain == other.domain
|
|
32
40
|
|
|
33
41
|
def sanity_checks(self, nuke: bool) -> None:
|
|
42
|
+
"""Check that we have a token, and that we don't already have a webapp for this domain"""
|
|
34
43
|
print(snakesay("Running API sanity checks"))
|
|
35
44
|
token = os.environ.get("API_TOKEN")
|
|
36
45
|
if not token:
|
|
@@ -54,6 +63,15 @@ class Webapp:
|
|
|
54
63
|
)
|
|
55
64
|
|
|
56
65
|
def create(self, python_version: str, virtualenv_path: Path, project_path: Path, nuke: bool) -> None:
|
|
66
|
+
"""Create a webapp for the given domain, using the given python version and virtualenv path
|
|
67
|
+
|
|
68
|
+
:param python_version: python version to use
|
|
69
|
+
:param virtualenv_path: path to the virtualenv
|
|
70
|
+
:param project_path: path to the project
|
|
71
|
+
:param nuke: if True, delete any existing webapp for this domain
|
|
72
|
+
|
|
73
|
+
:raises PythonAnywhereApiException: if API call fails
|
|
74
|
+
"""
|
|
57
75
|
print(snakesay("Creating web app via API"))
|
|
58
76
|
if nuke:
|
|
59
77
|
call_api(self.domain_url, "delete")
|
|
@@ -73,12 +91,19 @@ class Webapp:
|
|
|
73
91
|
)
|
|
74
92
|
|
|
75
93
|
def add_default_static_files_mappings(self, project_path: Path) -> None:
|
|
94
|
+
"""Add default static files mappings for /static/ and /media/
|
|
95
|
+
|
|
96
|
+
:param project_path: path to the project
|
|
97
|
+
"""
|
|
76
98
|
print(snakesay("Adding static files mappings for /static/ and /media/"))
|
|
77
99
|
url = f"{self.domain_url}static_files/"
|
|
78
100
|
call_api(url, "post", json=dict(url="/static/", path=str(Path(project_path) / "static")))
|
|
79
101
|
call_api(url, "post", json=dict(url="/media/", path=str(Path(project_path) / "media")))
|
|
80
102
|
|
|
81
103
|
def reload(self) -> None:
|
|
104
|
+
"""Reload webapp
|
|
105
|
+
|
|
106
|
+
:raises PythonAnywhereApiException: if API call fails"""
|
|
82
107
|
print(snakesay(f"Reloading {self.domain} via API"))
|
|
83
108
|
url = f"{self.domain_url}reload/"
|
|
84
109
|
response = call_api(url, "post")
|
|
@@ -100,6 +125,11 @@ class Webapp:
|
|
|
100
125
|
raise PythonAnywhereApiException(f"POST to reload webapp via API failed, got {response}:{response.text}")
|
|
101
126
|
|
|
102
127
|
def set_ssl(self, certificate: str, private_key: str) -> None:
|
|
128
|
+
"""Set SSL certificate and private key for webapp
|
|
129
|
+
|
|
130
|
+
:param certificate: SSL certificate
|
|
131
|
+
:param private_key: SSL private key
|
|
132
|
+
"""
|
|
103
133
|
print(snakesay(f"Setting up SSL for {self.domain} via API"))
|
|
104
134
|
url = f"{self.domain_url}ssl/"
|
|
105
135
|
response = call_api(url, "post", json={"cert": certificate, "private_key": private_key})
|
|
@@ -115,6 +145,7 @@ class Webapp:
|
|
|
115
145
|
)
|
|
116
146
|
|
|
117
147
|
def get_ssl_info(self) -> dict[str, Any]:
|
|
148
|
+
"""Get SSL certificate info"""
|
|
118
149
|
url = f"{self.domain_url}ssl/"
|
|
119
150
|
response = call_api(url, "get")
|
|
120
151
|
if not response.ok:
|
|
@@ -125,6 +156,13 @@ class Webapp:
|
|
|
125
156
|
return result
|
|
126
157
|
|
|
127
158
|
def delete_log(self, log_type: str, index: int = 0) -> None:
|
|
159
|
+
"""Delete log file
|
|
160
|
+
|
|
161
|
+
:param log_type: log type (access, error, server)
|
|
162
|
+
:param index: log file index (0 for current, 1 for previous, etc.)
|
|
163
|
+
|
|
164
|
+
:raises PythonAnywhereApiException: if API call fails
|
|
165
|
+
"""
|
|
128
166
|
if index:
|
|
129
167
|
message = f"Deleting old (archive number {index}) {log_type} log file for {self.domain} via API"
|
|
130
168
|
else:
|
|
@@ -144,7 +182,12 @@ class Webapp:
|
|
|
144
182
|
if not response.ok:
|
|
145
183
|
raise PythonAnywhereApiException(f"DELETE log file via API failed, got {response}:{response.text}")
|
|
146
184
|
|
|
147
|
-
def get_log_info(self):
|
|
185
|
+
def get_log_info(self) -> dict:
|
|
186
|
+
"""Get log files info
|
|
187
|
+
|
|
188
|
+
:returns: dictionary with log files info
|
|
189
|
+
|
|
190
|
+
:raises PythonAnywhereApiException: if API call fails"""
|
|
148
191
|
url = f"{self.files_url}tree/?path=/var/log/"
|
|
149
192
|
response = call_api(url, "get")
|
|
150
193
|
if not response.ok:
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = '0.1.0'
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|