pythonanywhere-core 0.2.3__tar.gz → 0.2.5__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,21 +1,20 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.3
2
2
  Name: pythonanywhere-core
3
- Version: 0.2.3
3
+ Version: 0.2.5
4
4
  Summary: API wrapper for programmatic management of PythonAnywhere services.
5
5
  License: MIT
6
6
  Keywords: pythonanywhere,api,cloud,web hosting
7
7
  Author: PythonAnywhere
8
8
  Author-email: developers@pythonanywhere.com
9
- Requires-Python: >=3.8,<4.0
9
+ Requires-Python: >=3.10,<4.0
10
10
  Classifier: Development Status :: 3 - Alpha
11
11
  Classifier: Intended Audience :: Developers
12
12
  Classifier: License :: OSI Approved :: MIT License
13
13
  Classifier: Programming Language :: Python :: 3
14
- Classifier: Programming Language :: Python :: 3.8
15
- Classifier: Programming Language :: Python :: 3.9
16
14
  Classifier: Programming Language :: Python :: 3.10
17
15
  Classifier: Programming Language :: Python :: 3.11
18
16
  Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
19
18
  Classifier: Topic :: Software Development :: Libraries
20
19
  Requires-Dist: python-dateutil (>=2.8.2,<3.0.0)
21
20
  Requires-Dist: requests (>=2.30.0,<3.0.0)
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "pythonanywhere-core"
3
- version = "0.2.3"
3
+ version = "0.2.5"
4
4
  description = "API wrapper for programmatic management of PythonAnywhere services."
5
5
  authors = ["PythonAnywhere <developers@pythonanywhere.com>"]
6
6
  license = "MIT"
@@ -10,28 +10,27 @@ classifiers = [
10
10
  "Intended Audience :: Developers",
11
11
  "Topic :: Software Development :: Libraries",
12
12
  "License :: OSI Approved :: MIT License",
13
+ "Programming Language :: Python :: 3.13",
13
14
  "Programming Language :: Python :: 3.12",
14
15
  "Programming Language :: Python :: 3.11",
15
16
  "Programming Language :: Python :: 3.10",
16
- "Programming Language :: Python :: 3.9",
17
- "Programming Language :: Python :: 3.8",
18
17
  ]
19
18
  keywords = ["pythonanywhere", "api", "cloud", "web hosting"]
20
19
 
21
20
  [tool.poetry.dependencies]
22
- python = "^3.8"
21
+ python = "^3.10"
23
22
  python-dateutil = "^2.8.2"
24
23
  requests = "^2.30.0"
25
24
  snakesay = "^0.10.3"
26
25
  typing_extensions = "^4.5.0"
27
26
 
28
- [tool.poetry.dev-dependencies]
27
+ [tool.poetry.group.dev.dependencies]
29
28
  pytest = "^8.0.0"
30
29
  pytest-cov = "^5.0.0"
31
30
  pytest-mock = "^3.10.0"
32
31
  responses = "^0.25.0"
33
- sphinx = "7.1.2"
34
- sphinx-rtd-theme = "^2.0.0"
32
+ sphinx = "7.4.7"
33
+ sphinx-rtd-theme = "^3.0.0"
35
34
 
36
35
  [tool.black]
37
36
  line-length = 120
@@ -0,0 +1 @@
1
+ __version__ = "0.2.5"
@@ -12,6 +12,8 @@ PYTHON_VERSIONS: Dict[str, str] = {
12
12
  "3.9": "python39",
13
13
  "3.10": "python310",
14
14
  "3.11": "python311",
15
+ "3.12": "python312",
16
+ "3.13": "python313",
15
17
  }
16
18
 
17
19
 
@@ -4,6 +4,7 @@ import os
4
4
  import getpass
5
5
  from pathlib import Path
6
6
  from textwrap import dedent
7
+ from typing import Any
7
8
 
8
9
  from dateutil.parser import parse
9
10
  from snakesay import snakesay
@@ -21,16 +22,25 @@ class Webapp:
21
22
 
22
23
  Methods:
23
24
  - :meth:`Webapp.create`: Create a new webapp.
25
+ - :meth:`Webapp.create_static_file_mapping`: Create a static file mapping.
26
+ - :meth:`Webapp.add_default_static_files_mappings`: Add default static files mappings.
24
27
  - :meth:`Webapp.reload`: Reload the webapp.
25
28
  - :meth:`Webapp.set_ssl`: Set the SSL certificate and private key.
26
29
  - :meth:`Webapp.get_ssl_info`: Retrieve SSL certificate information.
27
30
  - :meth:`Webapp.delete_log`: Delete a log file.
28
31
  - :meth:`Webapp.get_log_info`: Retrieve log file information.
32
+ - :meth:`Webapp.get`: Retrieve webapp information.
33
+ - :meth:`Webapp.delete`: Delete webapp.
34
+ - :meth:`Webapp.patch`: Patch webapp.
35
+
36
+ Class Methods:
37
+ - :meth:`Webapp.list_webapps`: List all webapps for the current user.
29
38
  """
39
+ username = getpass.getuser()
40
+ files_url = get_api_endpoint(username=username, flavor="files")
41
+ webapps_url = get_api_endpoint(username=username, flavor="webapps")
42
+
30
43
  def __init__(self, domain: str) -> None:
31
- self.username = getpass.getuser()
32
- self.files_url = get_api_endpoint(username=self.username, flavor="files")
33
- self.webapps_url = get_api_endpoint(username=self.username, flavor="webapps")
34
44
  self.domain = domain
35
45
  self.domain_url = f"{self.webapps_url}{self.domain}/"
36
46
 
@@ -38,7 +48,12 @@ class Webapp:
38
48
  return self.domain == other.domain
39
49
 
40
50
  def sanity_checks(self, nuke: bool) -> None:
41
- """Check that we have a token, and that we don't already have a webapp for this domain"""
51
+ """Check that we have a token, and that we don't already have a webapp for this domain.
52
+
53
+ :param nuke: if True, skip the check for existing webapp
54
+
55
+ :raises SanityException: if API token is missing or webapp already exists
56
+ """
42
57
  print(snakesay("Running API sanity checks"))
43
58
  token = os.environ.get("API_TOKEN")
44
59
  if not token:
@@ -71,7 +86,6 @@ class Webapp:
71
86
 
72
87
  :raises PythonAnywhereApiException: if API call fails
73
88
  """
74
- print(snakesay("Creating web app via API"))
75
89
  if nuke:
76
90
  call_api(self.domain_url, "delete")
77
91
  response = call_api(
@@ -89,21 +103,31 @@ class Webapp:
89
103
  "PATCH to set virtualenv path and source directory via API failed," f"got {response}:{response.text}"
90
104
  )
91
105
 
106
+ def create_static_file_mapping(self, url_path: str, directory_path: Path) -> None:
107
+ """Create a static file mapping via the API.
108
+
109
+ :param url_path: URL path (e.g., '/static/')
110
+ :param directory_path: Filesystem path to serve (as Path)
111
+
112
+ :raises PythonAnywhereApiException: if API call fails
113
+ """
114
+ url = f"{self.domain_url}static_files/"
115
+ call_api(url, "post", json=dict(url=url_path, path=str(directory_path)))
116
+
92
117
  def add_default_static_files_mappings(self, project_path: Path) -> None:
93
- """Add default static files mappings for /static/ and /media/
118
+ """Add default static files mappings for /static/ and /media/.
94
119
 
95
120
  :param project_path: path to the project
121
+
122
+ :raises PythonAnywhereApiException: if API call fails
96
123
  """
97
- print(snakesay("Adding static files mappings for /static/ and /media/"))
98
- url = f"{self.domain_url}static_files/"
99
- call_api(url, "post", json=dict(url="/static/", path=str(Path(project_path) / "static")))
100
- call_api(url, "post", json=dict(url="/media/", path=str(Path(project_path) / "media")))
124
+ self.create_static_file_mapping("/static/", Path(project_path) / "static")
125
+ self.create_static_file_mapping("/media/", Path(project_path) / "media")
101
126
 
102
127
  def reload(self) -> None:
103
128
  """Reload webapp
104
129
 
105
130
  :raises PythonAnywhereApiException: if API call fails"""
106
- print(snakesay(f"Reloading {self.domain} via API"))
107
131
  url = f"{self.domain_url}reload/"
108
132
  response = call_api(url, "post")
109
133
  if not response.ok:
@@ -124,10 +148,12 @@ class Webapp:
124
148
  raise PythonAnywhereApiException(f"POST to reload webapp via API failed, got {response}:{response.text}")
125
149
 
126
150
  def set_ssl(self, certificate: str, private_key: str) -> None:
127
- """Set SSL certificate and private key for webapp
151
+ """Set SSL certificate and private key for webapp.
128
152
 
129
153
  :param certificate: SSL certificate
130
154
  :param private_key: SSL private key
155
+
156
+ :raises PythonAnywhereApiException: if API call fails
131
157
  """
132
158
  print(snakesay(f"Setting up SSL for {self.domain} via API"))
133
159
  url = f"{self.domain_url}ssl/"
@@ -144,7 +170,12 @@ class Webapp:
144
170
  )
145
171
 
146
172
  def get_ssl_info(self) -> dict[str, Any]:
147
- """Get SSL certificate info"""
173
+ """Get SSL certificate info.
174
+
175
+ :returns: dictionary with SSL certificate information including parsed expiration date
176
+
177
+ :raises PythonAnywhereApiException: if API call fails
178
+ """
148
179
  url = f"{self.domain_url}ssl/"
149
180
  response = call_api(url, "get")
150
181
  if not response.ok:
@@ -181,10 +212,11 @@ class Webapp:
181
212
  if not response.ok:
182
213
  raise PythonAnywhereApiException(f"DELETE log file via API failed, got {response}:{response.text}")
183
214
 
184
- def get_log_info(self) -> dict:
185
- """Get log files info
215
+ def get_log_info(self) -> dict[str, list[int]]:
216
+ """Get log files info.
186
217
 
187
- :returns: dictionary with log files info
218
+ :returns: dictionary with log files info, keys are log types ('access', 'error', 'server'),
219
+ values are lists of log file indices
188
220
 
189
221
  :raises PythonAnywhereApiException: if API call fails"""
190
222
  url = f"{self.files_url}tree/?path=/var/log/"
@@ -210,3 +242,65 @@ class Webapp:
210
242
  continue
211
243
  logs[log_type].append(log_index)
212
244
  return logs
245
+
246
+ @classmethod
247
+ def list_webapps(cls) -> list[dict[str, Any]]:
248
+ """List all webapps for the current user.
249
+
250
+ :returns: list of webapps info as dictionaries
251
+
252
+ :raises PythonAnywhereApiException: if API call fails
253
+ """
254
+ response = call_api(cls.webapps_url, "get")
255
+ if not response.ok:
256
+ raise PythonAnywhereApiException(
257
+ f"GET webapps via API failed, "
258
+ f"got {response}:{response.text}"
259
+ )
260
+ return response.json()
261
+
262
+ def get(self) -> dict[str, Any]:
263
+ """Retrieve webapp information.
264
+
265
+ :returns: dictionary with webapp information
266
+
267
+ :raises PythonAnywhereApiException: if API call fails
268
+ """
269
+ response = call_api(self.domain_url, "get")
270
+
271
+ if not response.ok:
272
+ raise PythonAnywhereApiException(
273
+ f"GET webapp for {self.domain} via API failed, got {response}:{response.text}"
274
+ )
275
+
276
+ return response.json()
277
+
278
+ def delete(self) -> None:
279
+ """Delete webapp.
280
+
281
+ :raises PythonAnywhereApiException: if API call fails
282
+ """
283
+ response = call_api(self.domain_url, "delete")
284
+
285
+ if response.status_code != 204:
286
+ raise PythonAnywhereApiException(
287
+ f"DELETE webapp for {self.domain} via API failed, got {response}:{response.text}"
288
+ )
289
+
290
+ def patch(self, data: dict) -> dict[str, Any]:
291
+ """Patch webapp with provided data.
292
+
293
+ :param data: dictionary with data to update
294
+ :returns: dictionary with updated webapp information
295
+
296
+ :raises PythonAnywhereApiException: if API call fails
297
+ """
298
+ response = call_api(self.domain_url, "patch", data=data)
299
+
300
+ if not response.ok:
301
+ raise PythonAnywhereApiException(
302
+ f"PATCH webapp for {self.domain} via API failed, "
303
+ f"got {response}:{response.text}"
304
+ )
305
+
306
+ return response.json()
@@ -1 +0,0 @@
1
- __version__ = '0.2.3'