pythonanywhere-core 0.1.6__tar.gz → 0.1.8__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.6 → pythonanywhere_core-0.1.8}/PKG-INFO +3 -3
- {pythonanywhere_core-0.1.6 → pythonanywhere_core-0.1.8}/pyproject.toml +8 -6
- pythonanywhere_core-0.1.8/pythonanywhere_core/__init__.py +1 -0
- {pythonanywhere_core-0.1.6 → pythonanywhere_core-0.1.8}/pythonanywhere_core/base.py +10 -0
- pythonanywhere_core-0.1.8/pythonanywhere_core/files.py +171 -0
- pythonanywhere_core-0.1.8/pythonanywhere_core/students.py +49 -0
- {pythonanywhere_core-0.1.6 → pythonanywhere_core-0.1.8}/pythonanywhere_core/webapp.py +50 -1
- pythonanywhere_core-0.1.6/pythonanywhere_core/__init__.py +0 -1
- {pythonanywhere_core-0.1.6 → pythonanywhere_core-0.1.8}/LICENSE +0 -0
- {pythonanywhere_core-0.1.6 → pythonanywhere_core-0.1.8}/README.rst +0 -0
- {pythonanywhere_core-0.1.6 → pythonanywhere_core-0.1.8}/pythonanywhere_core/exceptions.py +0 -0
- {pythonanywhere_core-0.1.6 → pythonanywhere_core-0.1.8}/pythonanywhere_core/schedule.py +0 -0
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: pythonanywhere-core
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.8
|
|
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.
|
|
9
|
+
Requires-Python: >=3.8,<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.7
|
|
15
14
|
Classifier: Programming Language :: Python :: 3.8
|
|
16
15
|
Classifier: Programming Language :: Python :: 3.9
|
|
17
16
|
Classifier: Programming Language :: Python :: 3.10
|
|
18
17
|
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
19
|
Classifier: Topic :: Software Development :: Libraries
|
|
20
20
|
Requires-Dist: python-dateutil (>=2.8.2,<3.0.0)
|
|
21
21
|
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.1.
|
|
3
|
+
version = "0.1.8"
|
|
4
4
|
description = "API wrapper for programmatic management of PythonAnywhere services."
|
|
5
5
|
authors = ["PythonAnywhere <developers@pythonanywhere.com>"]
|
|
6
6
|
license = "MIT"
|
|
@@ -10,26 +10,28 @@ classifiers = [
|
|
|
10
10
|
"Intended Audience :: Developers",
|
|
11
11
|
"Topic :: Software Development :: Libraries",
|
|
12
12
|
"License :: OSI Approved :: MIT License",
|
|
13
|
+
"Programming Language :: Python :: 3.12",
|
|
13
14
|
"Programming Language :: Python :: 3.11",
|
|
14
15
|
"Programming Language :: Python :: 3.10",
|
|
15
16
|
"Programming Language :: Python :: 3.9",
|
|
16
17
|
"Programming Language :: Python :: 3.8",
|
|
17
|
-
"Programming Language :: Python :: 3.7",
|
|
18
18
|
]
|
|
19
19
|
keywords = ["pythonanywhere", "api", "cloud", "web hosting"]
|
|
20
20
|
|
|
21
21
|
[tool.poetry.dependencies]
|
|
22
|
-
python = "^3.
|
|
22
|
+
python = "^3.8"
|
|
23
23
|
python-dateutil = "^2.8.2"
|
|
24
24
|
requests = "^2.30.0"
|
|
25
25
|
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.8'
|
|
@@ -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())
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import getpass
|
|
2
|
+
from typing import Tuple, Union
|
|
3
|
+
from urllib.parse import urljoin
|
|
4
|
+
|
|
5
|
+
from requests.models import Response
|
|
6
|
+
|
|
7
|
+
from pythonanywhere_core.base import call_api, get_api_endpoint
|
|
8
|
+
from pythonanywhere_core.exceptions import PythonAnywhereApiException
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Files:
|
|
12
|
+
""" Interface for PythonAnywhere files API.
|
|
13
|
+
|
|
14
|
+
Uses `pythonanywhere_core.base` :method: `get_api_endpoint` to
|
|
15
|
+
create url, which is stored in a class variable `Files.base_url`,
|
|
16
|
+
then calls `call_api` with appropriate arguments to execute files
|
|
17
|
+
action.
|
|
18
|
+
|
|
19
|
+
Covers:
|
|
20
|
+
- GET, POST and DELETE for files path endpoint
|
|
21
|
+
- POST, GET and DELETE for files sharing endpoint
|
|
22
|
+
- GET for tree endpoint
|
|
23
|
+
|
|
24
|
+
"path" methods:
|
|
25
|
+
- use :method: `Files.path_get` to get contents of file or
|
|
26
|
+
directory from `path`
|
|
27
|
+
- use :method: `Files.path_post` to upload or update file at given
|
|
28
|
+
`dest_path` using contents from `source`
|
|
29
|
+
- use :method: `Files.path_delete` to delete file/directory on on
|
|
30
|
+
given `path`
|
|
31
|
+
|
|
32
|
+
"sharing" methods:
|
|
33
|
+
- use :method: `Files.sharing_post` to enable sharing a file from
|
|
34
|
+
`path` (if not shared before) and get a link to it
|
|
35
|
+
- use :method: `Files.sharing_get` to get sharing url for `path`
|
|
36
|
+
- use :method: `Files.sharing_delete` to disable sharing for
|
|
37
|
+
`path`
|
|
38
|
+
|
|
39
|
+
"tree" method:
|
|
40
|
+
- use :method: `Files.tree_get` to get list of regular files and
|
|
41
|
+
subdirectories of a directory at `path` (limited to 1000 results)
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
base_url = get_api_endpoint().format(username=getpass.getuser(), flavor="files")
|
|
45
|
+
path_endpoint = urljoin(base_url, "path")
|
|
46
|
+
sharing_endpoint = urljoin(base_url, "sharing/")
|
|
47
|
+
tree_endpoint = urljoin(base_url, "tree/")
|
|
48
|
+
|
|
49
|
+
def _error_msg(self, result: Response) -> str:
|
|
50
|
+
"""TODO: error responses should be unified at the API side """
|
|
51
|
+
|
|
52
|
+
if "application/json" in result.headers.get("content-type", ""):
|
|
53
|
+
jsn = result.json()
|
|
54
|
+
msg = jsn.get("detail") or jsn.get("message") or jsn.get("error", "")
|
|
55
|
+
return f": {msg}"
|
|
56
|
+
return ""
|
|
57
|
+
|
|
58
|
+
def path_get(self, path: str) -> Union[dict, bytes]:
|
|
59
|
+
"""Returns dictionary of directory contents when `path` is an
|
|
60
|
+
absolute path to of an existing directory or file contents if
|
|
61
|
+
`path` is an absolute path to an existing file -- both
|
|
62
|
+
available to the PythonAnywhere user. Raises when `path` is
|
|
63
|
+
invalid or unavailable."""
|
|
64
|
+
|
|
65
|
+
url = f"{self.path_endpoint}{path}"
|
|
66
|
+
|
|
67
|
+
result = call_api(url, "GET")
|
|
68
|
+
|
|
69
|
+
if result.status_code == 200:
|
|
70
|
+
if "application/json" in result.headers.get("content-type", ""):
|
|
71
|
+
return result.json()
|
|
72
|
+
return result.content
|
|
73
|
+
|
|
74
|
+
raise PythonAnywhereApiException(
|
|
75
|
+
f"GET to fetch contents of {url} failed, got {result}{self._error_msg(result)}"
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
def path_post(self, dest_path: str, content: bytes) -> int:
|
|
79
|
+
"""Uploads contents of `content` to `dest_path` which should be
|
|
80
|
+
a valid absolute path of a file available to a PythonAnywhere
|
|
81
|
+
user. If `dest_path` contains directories which don't exist
|
|
82
|
+
yet, they will be created.
|
|
83
|
+
|
|
84
|
+
Returns 200 if existing file on PythonAnywhere has been
|
|
85
|
+
updated with `source` contents, or 201 if file from
|
|
86
|
+
`dest_path` has been created with those contents."""
|
|
87
|
+
|
|
88
|
+
url = f"{self.path_endpoint}{dest_path}"
|
|
89
|
+
|
|
90
|
+
result = call_api(url, "POST", files={"content": content})
|
|
91
|
+
|
|
92
|
+
if result.ok:
|
|
93
|
+
return result.status_code
|
|
94
|
+
|
|
95
|
+
raise PythonAnywhereApiException(
|
|
96
|
+
f"POST to upload contents to {url} failed, got {result}{self._error_msg(result)}"
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
def path_delete(self, path: str) -> int:
|
|
100
|
+
"""Deletes the file at specified `path` (if file is a
|
|
101
|
+
directory it will be deleted as well).
|
|
102
|
+
|
|
103
|
+
Returns 204 on sucess, raises otherwise."""
|
|
104
|
+
|
|
105
|
+
url = f"{self.path_endpoint}{path}"
|
|
106
|
+
|
|
107
|
+
result = call_api(url, "DELETE")
|
|
108
|
+
|
|
109
|
+
if result.status_code == 204:
|
|
110
|
+
return result.status_code
|
|
111
|
+
|
|
112
|
+
raise PythonAnywhereApiException(
|
|
113
|
+
f"DELETE on {url} failed, got {result}{self._error_msg(result)}"
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
def sharing_post(self, path: str) -> Tuple[int, str]:
|
|
117
|
+
"""Starts sharing a file at `path`.
|
|
118
|
+
|
|
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
|
+
|
|
123
|
+
url = self.sharing_endpoint
|
|
124
|
+
|
|
125
|
+
result = call_api(url, "POST", json={"path": path})
|
|
126
|
+
|
|
127
|
+
if result.ok:
|
|
128
|
+
return result.status_code, result.json()["url"]
|
|
129
|
+
|
|
130
|
+
raise PythonAnywhereApiException(
|
|
131
|
+
f"POST to {url} to share '{path}' failed, got {result}{self._error_msg(result)}"
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
def sharing_get(self, path: str) -> str:
|
|
135
|
+
"""Checks sharing status for a `path`.
|
|
136
|
+
|
|
137
|
+
Returns url with sharing link if file is shared or an empty
|
|
138
|
+
string otherwise."""
|
|
139
|
+
|
|
140
|
+
url = f"{self.sharing_endpoint}?path={path}"
|
|
141
|
+
|
|
142
|
+
result = call_api(url, "GET")
|
|
143
|
+
|
|
144
|
+
return result.json()["url"] if result.ok else ""
|
|
145
|
+
|
|
146
|
+
def sharing_delete(self, path: str) -> int:
|
|
147
|
+
"""Stops sharing file at `path`.
|
|
148
|
+
|
|
149
|
+
Returns 204 on successful unshare."""
|
|
150
|
+
|
|
151
|
+
url = f"{self.sharing_endpoint}?path={path}"
|
|
152
|
+
|
|
153
|
+
result = call_api(url, "DELETE")
|
|
154
|
+
|
|
155
|
+
return result.status_code
|
|
156
|
+
|
|
157
|
+
def tree_get(self, path: str) -> dict:
|
|
158
|
+
"""Returns list of absolute paths of regular files and
|
|
159
|
+
subdirectories of a directory at `path`. Result is limited to
|
|
160
|
+
1000 items.
|
|
161
|
+
|
|
162
|
+
Raises if `path` does not point to an existing directory."""
|
|
163
|
+
|
|
164
|
+
url = f"{self.tree_endpoint}?path={path}"
|
|
165
|
+
|
|
166
|
+
result = call_api(url, "GET")
|
|
167
|
+
|
|
168
|
+
if result.ok:
|
|
169
|
+
return result.json()
|
|
170
|
+
|
|
171
|
+
raise PythonAnywhereApiException(f"GET to {url} failed, got {result}{self._error_msg(result)}")
|
|
@@ -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
|
+
)
|
|
@@ -13,6 +13,20 @@ from pythonanywhere_core.exceptions import SanityException, PythonAnywhereApiExc
|
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
class Webapp:
|
|
16
|
+
""" Interface for PythonAnywhere webapps API.
|
|
17
|
+
Uses `pythonanywhere_core.base` :method: `get_api_endpoint` to
|
|
18
|
+
create url, which is stored in a class variable `Webapp.base_url`,
|
|
19
|
+
then calls `call_api` with appropriate arguments to execute webapps
|
|
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
|
+
|
|
29
|
+
"""
|
|
16
30
|
def __init__(self, domain: str) -> None:
|
|
17
31
|
self.endpoint = get_api_endpoint()
|
|
18
32
|
self.username = getpass.getuser()
|
|
@@ -25,6 +39,7 @@ class Webapp:
|
|
|
25
39
|
return self.domain == other.domain
|
|
26
40
|
|
|
27
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"""
|
|
28
43
|
print(snakesay("Running API sanity checks"))
|
|
29
44
|
token = os.environ.get("API_TOKEN")
|
|
30
45
|
if not token:
|
|
@@ -48,6 +63,15 @@ class Webapp:
|
|
|
48
63
|
)
|
|
49
64
|
|
|
50
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
|
+
"""
|
|
51
75
|
print(snakesay("Creating web app via API"))
|
|
52
76
|
if nuke:
|
|
53
77
|
call_api(self.domain_url, "delete")
|
|
@@ -67,12 +91,19 @@ class Webapp:
|
|
|
67
91
|
)
|
|
68
92
|
|
|
69
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
|
+
"""
|
|
70
98
|
print(snakesay("Adding static files mappings for /static/ and /media/"))
|
|
71
99
|
url = f"{self.domain_url}static_files/"
|
|
72
100
|
call_api(url, "post", json=dict(url="/static/", path=str(Path(project_path) / "static")))
|
|
73
101
|
call_api(url, "post", json=dict(url="/media/", path=str(Path(project_path) / "media")))
|
|
74
102
|
|
|
75
103
|
def reload(self) -> None:
|
|
104
|
+
"""Reload webapp
|
|
105
|
+
|
|
106
|
+
:raises PythonAnywhereApiException: if API call fails"""
|
|
76
107
|
print(snakesay(f"Reloading {self.domain} via API"))
|
|
77
108
|
url = f"{self.domain_url}reload/"
|
|
78
109
|
response = call_api(url, "post")
|
|
@@ -94,6 +125,11 @@ class Webapp:
|
|
|
94
125
|
raise PythonAnywhereApiException(f"POST to reload webapp via API failed, got {response}:{response.text}")
|
|
95
126
|
|
|
96
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
|
+
"""
|
|
97
133
|
print(snakesay(f"Setting up SSL for {self.domain} via API"))
|
|
98
134
|
url = f"{self.domain_url}ssl/"
|
|
99
135
|
response = call_api(url, "post", json={"cert": certificate, "private_key": private_key})
|
|
@@ -109,6 +145,7 @@ class Webapp:
|
|
|
109
145
|
)
|
|
110
146
|
|
|
111
147
|
def get_ssl_info(self) -> dict[str, Any]:
|
|
148
|
+
"""Get SSL certificate info"""
|
|
112
149
|
url = f"{self.domain_url}ssl/"
|
|
113
150
|
response = call_api(url, "get")
|
|
114
151
|
if not response.ok:
|
|
@@ -119,6 +156,13 @@ class Webapp:
|
|
|
119
156
|
return result
|
|
120
157
|
|
|
121
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
|
+
"""
|
|
122
166
|
if index:
|
|
123
167
|
message = f"Deleting old (archive number {index}) {log_type} log file for {self.domain} via API"
|
|
124
168
|
else:
|
|
@@ -138,7 +182,12 @@ class Webapp:
|
|
|
138
182
|
if not response.ok:
|
|
139
183
|
raise PythonAnywhereApiException(f"DELETE log file via API failed, got {response}:{response.text}")
|
|
140
184
|
|
|
141
|
-
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"""
|
|
142
191
|
url = f"{self.files_url}tree/?path=/var/log/"
|
|
143
192
|
response = call_api(url, "get")
|
|
144
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
|