pythonanywhere-core 0.2.8__tar.gz → 0.3.0__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.2.8 → pythonanywhere_core-0.3.0}/PKG-INFO +7 -5
- {pythonanywhere_core-0.2.8 → pythonanywhere_core-0.3.0}/README.rst +6 -3
- {pythonanywhere_core-0.2.8 → pythonanywhere_core-0.3.0}/pyproject.toml +3 -3
- pythonanywhere_core-0.3.0/pythonanywhere_core/__init__.py +1 -0
- {pythonanywhere_core-0.2.8 → pythonanywhere_core-0.3.0}/pythonanywhere_core/base.py +32 -2
- pythonanywhere_core-0.3.0/pythonanywhere_core/exceptions.py +28 -0
- {pythonanywhere_core-0.2.8 → pythonanywhere_core-0.3.0}/pythonanywhere_core/files.py +23 -3
- {pythonanywhere_core-0.2.8 → pythonanywhere_core-0.3.0}/pythonanywhere_core/resources.py +2 -4
- {pythonanywhere_core-0.2.8 → pythonanywhere_core-0.3.0}/pythonanywhere_core/schedule.py +2 -3
- {pythonanywhere_core-0.2.8 → pythonanywhere_core-0.3.0}/pythonanywhere_core/students.py +2 -3
- {pythonanywhere_core-0.2.8 → pythonanywhere_core-0.3.0}/pythonanywhere_core/webapp.py +7 -28
- {pythonanywhere_core-0.2.8 → pythonanywhere_core-0.3.0}/pythonanywhere_core/website.py +3 -6
- pythonanywhere_core-0.2.8/pythonanywhere_core/__init__.py +0 -1
- pythonanywhere_core-0.2.8/pythonanywhere_core/exceptions.py +0 -18
- {pythonanywhere_core-0.2.8 → pythonanywhere_core-0.3.0}/LICENSE +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pythonanywhere-core
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: API wrapper for programmatic management of PythonAnywhere services.
|
|
5
5
|
License: MIT
|
|
6
6
|
License-File: LICENSE
|
|
@@ -20,16 +20,19 @@ Classifier: Programming Language :: Python :: 3.14
|
|
|
20
20
|
Classifier: Topic :: Software Development :: Libraries
|
|
21
21
|
Requires-Dist: python-dateutil (>=2.8.2,<3.0.0)
|
|
22
22
|
Requires-Dist: requests (>=2.30.0,<3.0.0)
|
|
23
|
-
Requires-Dist: snakesay (>=0.10.3,<0.11.0)
|
|
24
23
|
Requires-Dist: typing_extensions (>=4.5.0,<5.0.0)
|
|
25
24
|
Project-URL: Documentation, https://core.pythonanywhere.com/
|
|
26
25
|
Project-URL: Homepage, https://core.pythonanywhere.com/
|
|
27
26
|
Project-URL: Repository, https://github.com/pythonanywhere/pythonanywhere-core
|
|
28
27
|
Description-Content-Type: text/x-rst
|
|
29
28
|
|
|
30
|
-
|
|
29
|
+
pythonanywhere-core
|
|
30
|
+
===================
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
Python SDK for PythonAnywhere API - programmatic management of PythonAnywhere
|
|
33
|
+
services including webapps, files, scheduled tasks, students, and websites.
|
|
34
|
+
|
|
35
|
+
Core library behind the `PythonAnywhere cli tool`_.
|
|
33
36
|
|
|
34
37
|
.. _PythonAnywhere cli tool: https://pypi.org/project/pythonanywhere/
|
|
35
38
|
|
|
@@ -38,7 +41,6 @@ Documentation
|
|
|
38
41
|
|
|
39
42
|
Full documentation is available at https://core.pythonanywhere.com/
|
|
40
43
|
|
|
41
|
-
|
|
42
44
|
Development
|
|
43
45
|
===========
|
|
44
46
|
|
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
|
|
1
|
+
pythonanywhere-core
|
|
2
|
+
===================
|
|
2
3
|
|
|
3
|
-
|
|
4
|
+
Python SDK for PythonAnywhere API - programmatic management of PythonAnywhere
|
|
5
|
+
services including webapps, files, scheduled tasks, students, and websites.
|
|
6
|
+
|
|
7
|
+
Core library behind the `PythonAnywhere cli tool`_.
|
|
4
8
|
|
|
5
9
|
.. _PythonAnywhere cli tool: https://pypi.org/project/pythonanywhere/
|
|
6
10
|
|
|
@@ -9,7 +13,6 @@ Documentation
|
|
|
9
13
|
|
|
10
14
|
Full documentation is available at https://core.pythonanywhere.com/
|
|
11
15
|
|
|
12
|
-
|
|
13
16
|
Development
|
|
14
17
|
===========
|
|
15
18
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "pythonanywhere-core"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.3.0"
|
|
4
4
|
description = "API wrapper for programmatic management of PythonAnywhere services."
|
|
5
5
|
authors = ["PythonAnywhere <developers@pythonanywhere.com>"]
|
|
6
6
|
license = "MIT"
|
|
@@ -13,6 +13,7 @@ classifiers = [
|
|
|
13
13
|
"Intended Audience :: Developers",
|
|
14
14
|
"Topic :: Software Development :: Libraries",
|
|
15
15
|
"License :: OSI Approved :: MIT License",
|
|
16
|
+
"Programming Language :: Python :: 3.14",
|
|
16
17
|
"Programming Language :: Python :: 3.13",
|
|
17
18
|
"Programming Language :: Python :: 3.12",
|
|
18
19
|
"Programming Language :: Python :: 3.11",
|
|
@@ -24,11 +25,10 @@ keywords = ["pythonanywhere", "api", "cloud", "web hosting"]
|
|
|
24
25
|
python = "^3.10"
|
|
25
26
|
python-dateutil = "^2.8.2"
|
|
26
27
|
requests = "^2.30.0"
|
|
27
|
-
snakesay = "^0.10.3"
|
|
28
28
|
typing_extensions = "^4.5.0"
|
|
29
29
|
|
|
30
30
|
[tool.poetry.group.dev.dependencies]
|
|
31
|
-
pytest = "^
|
|
31
|
+
pytest = "^9.0.0"
|
|
32
32
|
pytest-cov = "^7.0.0"
|
|
33
33
|
pytest-mock = "^3.10.0"
|
|
34
34
|
responses = "^0.25.0"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.3.0"
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
+
import getpass
|
|
1
2
|
import os
|
|
3
|
+
import platform
|
|
2
4
|
from typing import Dict
|
|
3
5
|
|
|
4
6
|
import requests
|
|
5
7
|
|
|
8
|
+
from pythonanywhere_core import __version__
|
|
6
9
|
from pythonanywhere_core.exceptions import AuthenticationError, NoTokenError
|
|
7
10
|
|
|
8
11
|
PYTHON_VERSIONS: Dict[str, str] = {
|
|
@@ -18,6 +21,13 @@ PYTHON_VERSIONS: Dict[str, str] = {
|
|
|
18
21
|
}
|
|
19
22
|
|
|
20
23
|
|
|
24
|
+
def get_username() -> str:
|
|
25
|
+
"""Returns PythonAnywhere username from ``PYTHONANYWHERE_USERNAME``
|
|
26
|
+
environment variable, falling back to :func:`getpass.getuser`."""
|
|
27
|
+
|
|
28
|
+
return os.environ.get("PYTHONANYWHERE_USERNAME", getpass.getuser())
|
|
29
|
+
|
|
30
|
+
|
|
21
31
|
def get_api_endpoint(username: str, flavor: str) -> str:
|
|
22
32
|
hostname = os.environ.get(
|
|
23
33
|
"PYTHONANYWHERE_SITE",
|
|
@@ -54,15 +64,35 @@ def call_api(url: str, method: str, **kwargs) -> requests.Response:
|
|
|
54
64
|
:returns: requests.Response object
|
|
55
65
|
|
|
56
66
|
:raises AuthenticationError: if API returns 401
|
|
57
|
-
:raises NoTokenError: if API_TOKEN environment variable is not set
|
|
67
|
+
:raises NoTokenError: if API_TOKEN environment variable is not set
|
|
68
|
+
|
|
69
|
+
Client identification can be provided via PYTHONANYWHERE_CLIENT environment
|
|
70
|
+
variable (e.g., "pa/1.0.0" or "mcp-server/0.5.0") to help with usage analytics.
|
|
71
|
+
"""
|
|
58
72
|
|
|
59
73
|
token = os.environ.get("API_TOKEN")
|
|
60
74
|
if token is None:
|
|
61
75
|
raise NoTokenError(helpful_token_error_message())
|
|
76
|
+
|
|
77
|
+
base_user_agent = f"pythonanywhere-core/{__version__}"
|
|
78
|
+
client_info = os.environ.get("PYTHONANYWHERE_CLIENT")
|
|
79
|
+
|
|
80
|
+
if client_info:
|
|
81
|
+
user_agent = f"{base_user_agent} ({client_info}; Python/{platform.python_version()})"
|
|
82
|
+
else:
|
|
83
|
+
user_agent = f"{base_user_agent} (Python/{platform.python_version()})"
|
|
84
|
+
|
|
85
|
+
headers = {
|
|
86
|
+
"Authorization": f"Token {token}",
|
|
87
|
+
"User-Agent": user_agent
|
|
88
|
+
}
|
|
89
|
+
if "headers" in kwargs:
|
|
90
|
+
headers.update(kwargs.pop("headers"))
|
|
91
|
+
|
|
62
92
|
response = requests.request(
|
|
63
93
|
method=method,
|
|
64
94
|
url=url,
|
|
65
|
-
headers=
|
|
95
|
+
headers=headers,
|
|
66
96
|
**kwargs,
|
|
67
97
|
)
|
|
68
98
|
if response.status_code == 401:
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
class AuthenticationError(Exception):
|
|
2
|
+
pass
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class SanityException(Exception):
|
|
6
|
+
pass
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class PythonAnywhereApiException(Exception):
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class NoTokenError(PythonAnywhereApiException):
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class DomainAlreadyExistsException(PythonAnywhereApiException):
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class MissingCNAMEException(PythonAnywhereApiException):
|
|
22
|
+
def __init__(self):
|
|
23
|
+
super().__init__(
|
|
24
|
+
"Could not find a CNAME for your website. If you're using an A record, "
|
|
25
|
+
"CloudFlare, or some other way of pointing your domain at PythonAnywhere "
|
|
26
|
+
"then that should not be a problem. If you're not, you should double-check "
|
|
27
|
+
"your DNS setup."
|
|
28
|
+
)
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import
|
|
1
|
+
from pathlib import Path
|
|
2
2
|
from typing import Tuple, Union
|
|
3
3
|
from urllib.parse import urljoin
|
|
4
4
|
|
|
5
5
|
from requests.models import Response
|
|
6
6
|
|
|
7
|
-
from pythonanywhere_core.base import call_api, get_api_endpoint
|
|
7
|
+
from pythonanywhere_core.base import call_api, get_api_endpoint, get_username
|
|
8
8
|
from pythonanywhere_core.exceptions import PythonAnywhereApiException
|
|
9
9
|
|
|
10
10
|
|
|
@@ -38,7 +38,7 @@ class Files:
|
|
|
38
38
|
"""
|
|
39
39
|
|
|
40
40
|
|
|
41
|
-
base_url = get_api_endpoint(username=
|
|
41
|
+
base_url = get_api_endpoint(username=get_username(), flavor="files")
|
|
42
42
|
path_endpoint = urljoin(base_url, "path")
|
|
43
43
|
sharing_endpoint = urljoin(base_url, "sharing/")
|
|
44
44
|
tree_endpoint = urljoin(base_url, "tree/")
|
|
@@ -174,3 +174,23 @@ class Files:
|
|
|
174
174
|
return result.json()
|
|
175
175
|
|
|
176
176
|
raise PythonAnywhereApiException(f"GET to {url} failed, got {result}{self._error_msg(result)}")
|
|
177
|
+
|
|
178
|
+
def tree_post(self, local_dir_path: str, remote_dir_path: str) -> None:
|
|
179
|
+
"""Uploads contents of a local directory to remote path on
|
|
180
|
+
PythonAnywhere. Walks `local_dir_path` recursively and uploads
|
|
181
|
+
each file using :meth:`path_post`, preserving directory structure.
|
|
182
|
+
|
|
183
|
+
Raises :exc:`PythonAnywhereApiException` on first upload failure."""
|
|
184
|
+
|
|
185
|
+
local_dir = Path(local_dir_path)
|
|
186
|
+
if not local_dir.is_dir():
|
|
187
|
+
raise ValueError(f"{local_dir_path} is not a directory")
|
|
188
|
+
for path in sorted(local_dir.rglob("*")):
|
|
189
|
+
if path.is_file():
|
|
190
|
+
relative = path.relative_to(local_dir)
|
|
191
|
+
remote_path = f"{remote_dir_path}/{relative}"
|
|
192
|
+
self.path_post(remote_path, path.read_bytes())
|
|
193
|
+
elif path.is_dir() and not any(path.iterdir()):
|
|
194
|
+
placeholder = f"{remote_dir_path}/{path.relative_to(local_dir)}/.empty"
|
|
195
|
+
self.path_post(placeholder, b"")
|
|
196
|
+
self.path_delete(placeholder)
|
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
from pythonanywhere_core.base import call_api, get_api_endpoint
|
|
1
|
+
from pythonanywhere_core.base import call_api, get_api_endpoint, get_username
|
|
4
2
|
from pythonanywhere_core.exceptions import PythonAnywhereApiException
|
|
5
3
|
|
|
6
4
|
|
|
@@ -17,7 +15,7 @@ class CPU:
|
|
|
17
15
|
"""
|
|
18
16
|
|
|
19
17
|
def __init__(self):
|
|
20
|
-
self.base_url = get_api_endpoint(username=
|
|
18
|
+
self.base_url = get_api_endpoint(username=get_username(), flavor="cpu")
|
|
21
19
|
|
|
22
20
|
def get_cpu_usage(self):
|
|
23
21
|
"""Get current CPU usage information.
|
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import getpass
|
|
2
1
|
from typing import List, Optional
|
|
3
2
|
|
|
4
3
|
from typing_extensions import Literal
|
|
5
4
|
|
|
6
|
-
from pythonanywhere_core.base import call_api, get_api_endpoint
|
|
5
|
+
from pythonanywhere_core.base import call_api, get_api_endpoint, get_username
|
|
7
6
|
from pythonanywhere_core.exceptions import PythonAnywhereApiException
|
|
8
7
|
|
|
9
8
|
|
|
@@ -28,7 +27,7 @@ class Schedule:
|
|
|
28
27
|
- :meth:`Schedule.update`: Update an existing task.
|
|
29
28
|
"""
|
|
30
29
|
|
|
31
|
-
base_url: str = get_api_endpoint(username=
|
|
30
|
+
base_url: str = get_api_endpoint(username=get_username(), flavor="schedule")
|
|
32
31
|
|
|
33
32
|
def create(self, params: dict) -> Optional[dict]:
|
|
34
33
|
"""Creates new scheduled task using `params`.
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import getpass
|
|
2
1
|
from typing import Optional
|
|
3
2
|
|
|
4
|
-
from pythonanywhere_core.base import call_api, get_api_endpoint
|
|
3
|
+
from pythonanywhere_core.base import call_api, get_api_endpoint, get_username
|
|
5
4
|
|
|
6
5
|
|
|
7
6
|
class StudentsAPI:
|
|
@@ -22,7 +21,7 @@ class StudentsAPI:
|
|
|
22
21
|
- :meth:`StudentsAPI.delete`: Remove a student.
|
|
23
22
|
"""
|
|
24
23
|
|
|
25
|
-
base_url: str = get_api_endpoint(username=
|
|
24
|
+
base_url: str = get_api_endpoint(username=get_username(), flavor="students")
|
|
26
25
|
|
|
27
26
|
def get(self) -> Optional[dict]:
|
|
28
27
|
"""Returns list of PythonAnywhere students related with user's account.
|
|
@@ -1,16 +1,14 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import os
|
|
4
|
-
import getpass
|
|
5
4
|
from pathlib import Path
|
|
6
5
|
from textwrap import dedent
|
|
7
6
|
from typing import Any
|
|
8
7
|
|
|
9
8
|
from dateutil.parser import parse
|
|
10
|
-
from snakesay import snakesay
|
|
11
9
|
|
|
12
|
-
from pythonanywhere_core.base import call_api, get_api_endpoint, PYTHON_VERSIONS
|
|
13
|
-
from pythonanywhere_core.exceptions import SanityException, PythonAnywhereApiException
|
|
10
|
+
from pythonanywhere_core.base import call_api, get_api_endpoint, get_username, PYTHON_VERSIONS
|
|
11
|
+
from pythonanywhere_core.exceptions import SanityException, PythonAnywhereApiException, MissingCNAMEException
|
|
14
12
|
|
|
15
13
|
|
|
16
14
|
class Webapp:
|
|
@@ -36,7 +34,7 @@ class Webapp:
|
|
|
36
34
|
Class Methods:
|
|
37
35
|
- :meth:`Webapp.list_webapps`: List all webapps for the current user.
|
|
38
36
|
"""
|
|
39
|
-
username =
|
|
37
|
+
username = get_username()
|
|
40
38
|
files_url = get_api_endpoint(username=username, flavor="files")
|
|
41
39
|
webapps_url = get_api_endpoint(username=username, flavor="webapps")
|
|
42
40
|
|
|
@@ -54,7 +52,6 @@ class Webapp:
|
|
|
54
52
|
|
|
55
53
|
:raises SanityException: if API token is missing or webapp already exists
|
|
56
54
|
"""
|
|
57
|
-
print(snakesay("Running API sanity checks"))
|
|
58
55
|
token = os.environ.get("API_TOKEN")
|
|
59
56
|
if not token:
|
|
60
57
|
raise SanityException(
|
|
@@ -76,11 +73,11 @@ class Webapp:
|
|
|
76
73
|
f"You already have a webapp for {self.domain}.\n\nUse the --nuke option if you want to replace it."
|
|
77
74
|
)
|
|
78
75
|
|
|
79
|
-
def create(self, python_version: str, virtualenv_path: Path, project_path: Path, nuke: bool) -> None:
|
|
76
|
+
def create(self, python_version: str, virtualenv_path: Path | None, project_path: Path, nuke: bool) -> None:
|
|
80
77
|
"""Create a webapp for the given domain, using the given python version and virtualenv path
|
|
81
78
|
|
|
82
79
|
:param python_version: python version to use
|
|
83
|
-
:param virtualenv_path: path to the virtualenv
|
|
80
|
+
:param virtualenv_path: path to the virtualenv, or None to skip setting it
|
|
84
81
|
:param project_path: path to the project
|
|
85
82
|
:param nuke: if True, delete any existing webapp for this domain
|
|
86
83
|
|
|
@@ -127,24 +124,13 @@ class Webapp:
|
|
|
127
124
|
def reload(self) -> None:
|
|
128
125
|
"""Reload webapp
|
|
129
126
|
|
|
127
|
+
:raises MissingCNAMEException: if CNAME not found (reload succeeded)
|
|
130
128
|
:raises PythonAnywhereApiException: if API call fails"""
|
|
131
129
|
url = f"{self.domain_url}reload/"
|
|
132
130
|
response = call_api(url, "post")
|
|
133
131
|
if not response.ok:
|
|
134
132
|
if response.status_code == 409 and response.json()["error"] == "cname_error":
|
|
135
|
-
|
|
136
|
-
snakesay(
|
|
137
|
-
dedent(
|
|
138
|
-
"""
|
|
139
|
-
Could not find a CNAME for your website. If you're using an A record,
|
|
140
|
-
CloudFlare, or some other way of pointing your domain at PythonAnywhere
|
|
141
|
-
then that should not be a problem. If you're not, you should double-check
|
|
142
|
-
your DNS setup.
|
|
143
|
-
"""
|
|
144
|
-
)
|
|
145
|
-
)
|
|
146
|
-
)
|
|
147
|
-
return
|
|
133
|
+
raise MissingCNAMEException()
|
|
148
134
|
raise PythonAnywhereApiException(f"POST to reload webapp via API failed, got {response}:{response.text}")
|
|
149
135
|
|
|
150
136
|
def set_ssl(self, certificate: str, private_key: str) -> None:
|
|
@@ -155,7 +141,6 @@ class Webapp:
|
|
|
155
141
|
|
|
156
142
|
:raises PythonAnywhereApiException: if API call fails
|
|
157
143
|
"""
|
|
158
|
-
print(snakesay(f"Setting up SSL for {self.domain} via API"))
|
|
159
144
|
url = f"{self.domain_url}ssl/"
|
|
160
145
|
response = call_api(url, "post", json={"cert": certificate, "private_key": private_key})
|
|
161
146
|
if not response.ok:
|
|
@@ -193,12 +178,6 @@ class Webapp:
|
|
|
193
178
|
|
|
194
179
|
:raises PythonAnywhereApiException: if API call fails
|
|
195
180
|
"""
|
|
196
|
-
if index:
|
|
197
|
-
message = f"Deleting old (archive number {index}) {log_type} log file for {self.domain} via API"
|
|
198
|
-
else:
|
|
199
|
-
message = f"Deleting current {log_type} log file for {self.domain} via API"
|
|
200
|
-
print(snakesay(message))
|
|
201
|
-
|
|
202
181
|
if index == 1:
|
|
203
182
|
suffix = ".1"
|
|
204
183
|
elif index > 1:
|
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
from pythonanywhere_core.base import call_api, get_api_endpoint
|
|
1
|
+
from pythonanywhere_core.base import call_api, get_api_endpoint, get_username
|
|
5
2
|
from pythonanywhere_core.exceptions import DomainAlreadyExistsException, PythonAnywhereApiException
|
|
6
3
|
|
|
7
4
|
|
|
@@ -24,8 +21,8 @@ class Website:
|
|
|
24
21
|
"""
|
|
25
22
|
|
|
26
23
|
def __init__(self) -> None:
|
|
27
|
-
self.websites_base_url = get_api_endpoint(username=
|
|
28
|
-
self.domains_base_url = get_api_endpoint(username=
|
|
24
|
+
self.websites_base_url = get_api_endpoint(username=get_username(), flavor="websites")
|
|
25
|
+
self.domains_base_url = get_api_endpoint(username=get_username(), flavor="domains")
|
|
29
26
|
|
|
30
27
|
|
|
31
28
|
def create(self, domain_name: str, command: str) -> dict:
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.2.8"
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
class AuthenticationError(Exception):
|
|
2
|
-
pass
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
class SanityException(Exception):
|
|
6
|
-
pass
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
class PythonAnywhereApiException(Exception):
|
|
10
|
-
pass
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
class NoTokenError(PythonAnywhereApiException):
|
|
14
|
-
pass
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
class DomainAlreadyExistsException(PythonAnywhereApiException):
|
|
18
|
-
pass
|
|
File without changes
|