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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pythonanywhere-core
3
- Version: 0.1.7
3
+ Version: 0.1.9
4
4
  Summary: API wrapper for programmatic management of PythonAnywhere services.
5
5
  License: MIT
6
6
  Keywords: pythonanywhere,api,cloud,web hosting
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "pythonanywhere-core"
3
- version = "0.1.7"
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 = "^7.3.1"
30
- pytest-cov = "^4.0.0"
29
+ pytest = "^8.0.0"
30
+ pytest-cov = "^5.0.0"
31
31
  pytest-mock = "^3.10.0"
32
- responses = "^0.24.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 status code and sharing link on
120
- success, raises otherwise. Status code is 201 on success, 200
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
- return result.status_code, result.json()["url"]
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
- return result.json()["url"] if result.ok else ""
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'