Skip to content

Commit 870b5b6

Browse files
authored
Merge pull request #209 from MerginMaps/improve-client-error
Improve ClientError handling
2 parents 45d4454 + dd16754 commit 870b5b6

File tree

5 files changed

+104
-25
lines changed

5 files changed

+104
-25
lines changed

.github/workflows/autotests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name: Auto Tests
22
on: [push]
33
env:
4-
TEST_MERGIN_URL: https://test.dev.merginmaps.com/
4+
TEST_MERGIN_URL: https://app.dev.merginmaps.com/
55
TEST_API_USERNAME: test_plugin
66
TEST_API_PASSWORD: ${{ secrets.MERGINTEST_API_PASSWORD }}
77
TEST_API_USERNAME2: test_plugin2

mergin/client.py

Lines changed: 22 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
import typing
1818
import warnings
1919

20-
from .common import ClientError, LoginError, InvalidProject
20+
from .common import ClientError, LoginError, InvalidProject, ErrorCode
2121
from .merginproject import MerginProject
2222
from .client_pull import (
2323
download_file_finalize,
@@ -205,19 +205,20 @@ def _do_request(self, request):
205205
try:
206206
return self.opener.open(request)
207207
except urllib.error.HTTPError as e:
208-
if e.headers.get("Content-Type", "") == "application/problem+json":
209-
info = json.load(e)
210-
err_detail = info.get("detail")
211-
else:
212-
err_detail = e.read().decode("utf-8")
208+
server_response = json.load(e)
209+
210+
# We first to try to get the value from the response otherwise we set a default value
211+
err_detail = server_response.get("detail", e.read().decode("utf-8"))
212+
server_code = server_response.get("code", None)
213213

214-
error_msg = (
215-
f"HTTP Error: {e.code} {e.reason}\n"
216-
f"URL: {request.get_full_url()}\n"
217-
f"Method: {request.get_method()}\n"
218-
f"Detail: {err_detail}"
214+
raise ClientError(
215+
detail=err_detail,
216+
url=request.get_full_url(),
217+
server_code=server_code,
218+
server_response=server_response,
219+
http_error=e.code,
220+
http_method=request.get_method(),
219221
)
220-
raise ClientError(error_msg)
221222
except urllib.error.URLError as e:
222223
# e.g. when DNS resolution fails (no internet connection?)
223224
raise ClientError("Error requesting " + request.full_url + ": " + str(e))
@@ -429,9 +430,9 @@ def create_workspace(self, workspace_name):
429430

430431
try:
431432
self.post("/v1/workspace", params, {"Content-Type": "application/json"})
432-
except Exception as e:
433-
detail = f"Workspace name: {workspace_name}"
434-
raise ClientError(str(e), detail)
433+
except ClientError as e:
434+
e.extra = f"Workspace name: {workspace_name}"
435+
raise e
435436

436437
def create_project(self, project_name, is_public=False, namespace=None):
437438
"""
@@ -478,9 +479,9 @@ def create_project(self, project_name, is_public=False, namespace=None):
478479
namespace = self.username()
479480
try:
480481
self.post(f"/v1/project/{namespace}", params, {"Content-Type": "application/json"})
481-
except Exception as e:
482-
detail = f"Namespace: {namespace}, project name: {project_name}"
483-
raise ClientError(str(e), detail)
482+
except ClientError as e:
483+
e.extra = f"Namespace: {namespace}, project name: {project_name}"
484+
raise e
484485

485486
def create_project_and_push(self, project_name, directory, is_public=False, namespace=None):
486487
"""
@@ -776,9 +777,9 @@ def set_project_access(self, project_path, access):
776777
try:
777778
request = urllib.request.Request(url, data=json.dumps(params).encode(), headers=json_headers, method="PUT")
778779
self._do_request(request)
779-
except Exception as e:
780-
detail = f"Project path: {project_path}"
781-
raise ClientError(str(e), detail)
780+
except ClientError as e:
781+
e.extra = f"Project path: {project_path}"
782+
raise e
782783

783784
def add_user_permissions_to_project(self, project_path, usernames, permission_level):
784785
"""

mergin/common.py

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import os
2-
2+
from enum import Enum
33

44
CHUNK_SIZE = 100 * 1024 * 1024
55

@@ -10,8 +10,35 @@
1010
this_dir = os.path.dirname(os.path.realpath(__file__))
1111

1212

13+
# Error code from the public API, add to the end of enum as we handle more eror
14+
class ErrorCode(Enum):
15+
ProjectsLimitHit = "ProjectsLimitHit"
16+
StorageLimitHit = "StorageLimitHit"
17+
18+
1319
class ClientError(Exception):
14-
pass
20+
def __init__(self, detail, url=None, server_code=None, server_response=None, http_error=None, http_method=None):
21+
self.detail = detail
22+
self.url = url
23+
self.http_error = http_error
24+
self.http_method = http_method
25+
26+
self.server_code = server_code
27+
self.server_response = server_response
28+
29+
self.extra = None
30+
31+
def __str__(self):
32+
string_res = f"Detail: {self.detail}\n"
33+
if self.http_error:
34+
string_res += f"HTTP Error: {self.http_error}\n"
35+
if self.url:
36+
string_res += f"URL: {self.url}\n"
37+
if self.http_method:
38+
string_res += f"Method: {self.http_method}\n"
39+
if self.extra:
40+
string_res += f"{self.extra}\n"
41+
return string_res
1542

1643

1744
class LoginError(Exception):

mergin/test/test_client.py

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
decode_token_data,
2020
TokenError,
2121
ServerType,
22+
ErrorCode,
2223
)
2324
from ..client_push import push_project_async, push_project_cancel
2425
from ..client_pull import (
@@ -796,7 +797,7 @@ def test_available_storage_validation(mcStorage):
796797
assert got_right_err
797798

798799
# Expecting empty project
799-
project_info = get_project_info(mcStorage, API_USER, test_project)
800+
project_info = get_project_info(mcStorage, STORAGE_WORKSPACE, test_project)
800801
assert project_info["version"] == "v0"
801802
assert project_info["disk_usage"] == 0
802803

@@ -2629,3 +2630,34 @@ def test_editor_push(mc: MerginClient, mc2: MerginClient):
26292630
conflicted_file = project_file
26302631
# There is no conflicted qgs file
26312632
assert conflicted_file is None
2633+
2634+
2635+
def test_error_push_already_named_project(mc: MerginClient):
2636+
test_project = "test_push_already_existing"
2637+
project = API_USER + "/" + test_project
2638+
project_dir = os.path.join(TMP_DIR, test_project)
2639+
2640+
with pytest.raises(ClientError) as e:
2641+
mc.create_project_and_push(test_project, project_dir)
2642+
assert e.value.detail == "Project with the same name already exists"
2643+
assert e.value.http_error == 409
2644+
assert e.value.http_method == "POST"
2645+
assert e.value.url == f"{mc.url}v1/project/test_plugin"
2646+
2647+
2648+
def test_error_projects_limit_hit(mcStorage: MerginClient):
2649+
test_project = "test_another_project_above_projects_limit"
2650+
test_project_fullname = STORAGE_WORKSPACE + "/" + test_project
2651+
2652+
project_dir = os.path.join(TMP_DIR, test_project, API_USER)
2653+
2654+
with pytest.raises(ClientError) as e:
2655+
mcStorage.create_project_and_push(test_project_fullname, project_dir)
2656+
assert e.value.server_code == ErrorCode.ProjectsLimitHit.value
2657+
assert (
2658+
e.value.detail
2659+
== "Maximum number of projects is reached. Please upgrade your subscription to create new projects (ProjectsLimitHit)"
2660+
)
2661+
assert e.value.http_error == 422
2662+
assert e.value.http_method == "POST"
2663+
assert e.value.url == f"{mcStorage.url}v1/project/testpluginstorage"

mergin/utils.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,3 +275,22 @@ def is_mergin_config(path: str) -> bool:
275275
"""Check if the given path is for file mergin-config.json"""
276276
filename = os.path.basename(path).lower()
277277
return filename == "mergin-config.json"
278+
279+
280+
def bytes_to_human_size(bytes: int):
281+
"""
282+
Convert bytes to human readable size
283+
example :
284+
bytes_to_human_size(5600000) -> "5.3 MB"
285+
"""
286+
precision = 1
287+
if bytes < 1e-5:
288+
return "0.0 MB"
289+
elif bytes < 1024.0 * 1024.0:
290+
return f"{round( bytes / 1024.0, precision )} KB"
291+
elif bytes < 1024.0 * 1024.0 * 1024.0:
292+
return f"{round( bytes / 1024.0 / 1024.0, precision)} MB"
293+
elif bytes < 1024.0 * 1024.0 * 1024.0 * 1024.0:
294+
return f"{round( bytes / 1024.0 / 1024.0 / 1024.0, precision )} GB"
295+
else:
296+
return f"{round( bytes / 1024.0 / 1024.0 / 1024.0 / 1024.0, precision )} TB"

0 commit comments

Comments
 (0)