pythonanywhere-core 0.1.7__tar.gz → 0.1.9__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.9}/PKG-INFO +1 -1
- {pythonanywhere_core-0.1.7 → pythonanywhere_core-0.1.9}/pyproject.toml +6 -4
- pythonanywhere_core-0.1.9/pythonanywhere_core/__init__.py +1 -0
- {pythonanywhere_core-0.1.7 → pythonanywhere_core-0.1.9}/pythonanywhere_core/base.py +10 -0
- {pythonanywhere_core-0.1.7 → pythonanywhere_core-0.1.9}/pythonanywhere_core/files.py +14 -6
- pythonanywhere_core-0.1.9/pythonanywhere_core/students.py +49 -0
- {pythonanywhere_core-0.1.7 → pythonanywhere_core-0.1.9}/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.9}/LICENSE +0 -0
- {pythonanywhere_core-0.1.7 → pythonanywhere_core-0.1.9}/README.rst +0 -0
- {pythonanywhere_core-0.1.7 → pythonanywhere_core-0.1.9}/pythonanywhere_core/exceptions.py +0 -0
- {pythonanywhere_core-0.1.7 → pythonanywhere_core-0.1.9}/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.9"
|
|
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.9'
|
|
@@ -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())
|
|
@@ -55,6 +55,9 @@ class Files:
|
|
|
55
55
|
return f": {msg}"
|
|
56
56
|
return ""
|
|
57
57
|
|
|
58
|
+
def _make_sharing_url(self, sharing_url_suffix):
|
|
59
|
+
return urljoin(self.base_url.split("api")[0], sharing_url_suffix)
|
|
60
|
+
|
|
58
61
|
def path_get(self, path: str) -> Union[dict, bytes]:
|
|
59
62
|
"""Returns dictionary of directory contents when `path` is an
|
|
60
63
|
absolute path to of an existing directory or file contents if
|
|
@@ -116,16 +119,18 @@ class Files:
|
|
|
116
119
|
def sharing_post(self, path: str) -> Tuple[int, str]:
|
|
117
120
|
"""Starts sharing a file at `path`.
|
|
118
121
|
|
|
119
|
-
Returns a tuple with a
|
|
120
|
-
success, raises otherwise.
|
|
121
|
-
if file has been already shared."""
|
|
122
|
+
Returns a tuple with a message and sharing link on
|
|
123
|
+
success, raises otherwise. Message is "successfully shared" on success,
|
|
124
|
+
"was already shared" if file has been already shared."""
|
|
122
125
|
|
|
123
126
|
url = self.sharing_endpoint
|
|
124
127
|
|
|
125
128
|
result = call_api(url, "POST", json={"path": path})
|
|
126
129
|
|
|
127
130
|
if result.ok:
|
|
128
|
-
|
|
131
|
+
msg = {200: "was already shared", 201: "successfully shared"}[result.status_code]
|
|
132
|
+
sharing_url_suffix = result.json()["url"]
|
|
133
|
+
return msg, self._make_sharing_url(sharing_url_suffix)
|
|
129
134
|
|
|
130
135
|
raise PythonAnywhereApiException(
|
|
131
136
|
f"POST to {url} to share '{path}' failed, got {result}{self._error_msg(result)}"
|
|
@@ -140,8 +145,11 @@ class Files:
|
|
|
140
145
|
url = f"{self.sharing_endpoint}?path={path}"
|
|
141
146
|
|
|
142
147
|
result = call_api(url, "GET")
|
|
143
|
-
|
|
144
|
-
|
|
148
|
+
if result.ok:
|
|
149
|
+
sharing_url_suffix = result.json()["url"]
|
|
150
|
+
return self._make_sharing_url(sharing_url_suffix)
|
|
151
|
+
else:
|
|
152
|
+
return ""
|
|
145
153
|
|
|
146
154
|
def sharing_delete(self, path: str) -> int:
|
|
147
155
|
"""Stops sharing file at `path`.
|
|
@@ -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
|