44import getpass
55from pathlib import Path
66from textwrap import dedent
7+ from typing import Any
78
89from dateutil .parser import parse
910from snakesay import snakesay
@@ -22,11 +23,15 @@ class Webapp:
2223 Methods:
2324 - :meth:`Webapp.create`: Create a new webapp.
2425 - :meth:`Webapp.create_static_file_mapping`: Create a static file mapping.
26+ - :meth:`Webapp.add_default_static_files_mappings`: Add default static files mappings.
2527 - :meth:`Webapp.reload`: Reload the webapp.
2628 - :meth:`Webapp.set_ssl`: Set the SSL certificate and private key.
2729 - :meth:`Webapp.get_ssl_info`: Retrieve SSL certificate information.
2830 - :meth:`Webapp.delete_log`: Delete a log file.
2931 - :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.
3035
3136 Class Methods:
3237 - :meth:`Webapp.list_webapps`: List all webapps for the current user.
@@ -43,7 +48,12 @@ def __eq__(self, other: Webapp) -> bool:
4348 return self .domain == other .domain
4449
4550 def sanity_checks (self , nuke : bool ) -> None :
46- """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+ """
4757 print (snakesay ("Running API sanity checks" ))
4858 token = os .environ .get ("API_TOKEN" )
4959 if not token :
@@ -98,14 +108,18 @@ def create_static_file_mapping(self, url_path: str, directory_path: Path) -> Non
98108
99109 :param url_path: URL path (e.g., '/static/')
100110 :param directory_path: Filesystem path to serve (as Path)
111+
112+ :raises PythonAnywhereApiException: if API call fails
101113 """
102114 url = f"{ self .domain_url } static_files/"
103115 call_api (url , "post" , json = dict (url = url_path , path = str (directory_path )))
104116
105117 def add_default_static_files_mappings (self , project_path : Path ) -> None :
106- """Add default static files mappings for /static/ and /media/
118+ """Add default static files mappings for /static/ and /media/.
107119
108120 :param project_path: path to the project
121+
122+ :raises PythonAnywhereApiException: if API call fails
109123 """
110124 self .create_static_file_mapping ("/static/" , Path (project_path ) / "static" )
111125 self .create_static_file_mapping ("/media/" , Path (project_path ) / "media" )
@@ -134,10 +148,12 @@ def reload(self) -> None:
134148 raise PythonAnywhereApiException (f"POST to reload webapp via API failed, got { response } :{ response .text } " )
135149
136150 def set_ssl (self , certificate : str , private_key : str ) -> None :
137- """Set SSL certificate and private key for webapp
151+ """Set SSL certificate and private key for webapp.
138152
139153 :param certificate: SSL certificate
140154 :param private_key: SSL private key
155+
156+ :raises PythonAnywhereApiException: if API call fails
141157 """
142158 print (snakesay (f"Setting up SSL for { self .domain } via API" ))
143159 url = f"{ self .domain_url } ssl/"
@@ -154,7 +170,12 @@ def set_ssl(self, certificate: str, private_key: str) -> None:
154170 )
155171
156172 def get_ssl_info (self ) -> dict [str , Any ]:
157- """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+ """
158179 url = f"{ self .domain_url } ssl/"
159180 response = call_api (url , "get" )
160181 if not response .ok :
@@ -191,10 +212,11 @@ def delete_log(self, log_type: str, index: int = 0) -> None:
191212 if not response .ok :
192213 raise PythonAnywhereApiException (f"DELETE log file via API failed, got { response } :{ response .text } " )
193214
194- def get_log_info (self ) -> dict :
195- """Get log files info
215+ def get_log_info (self ) -> dict [ str , list [ int ]] :
216+ """Get log files info.
196217
197- :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
198220
199221 :raises PythonAnywhereApiException: if API call fails"""
200222 url = f"{ self .files_url } tree/?path=/var/log/"
@@ -222,12 +244,63 @@ def get_log_info(self) -> dict:
222244 return logs
223245
224246 @classmethod
225- def list_webapps (cls ) -> list :
247+ def list_webapps (cls ) -> list [ dict [ str , Any ]] :
226248 """List all webapps for the current user.
227249
228250 :returns: list of webapps info as dictionaries
251+
252+ :raises PythonAnywhereApiException: if API call fails
229253 """
230254 response = call_api (cls .webapps_url , "get" )
231255 if not response .ok :
232- raise PythonAnywhereApiException (f"GET webapps via API failed, got { response } :{ response .text } " )
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+
233306 return response .json ()
0 commit comments