Skip to content

Commit 13e158b

Browse files
frankenjoehagenw
andauthored
Raise generic BackendError (#87)
* add BackendError class * DOC: add BackendError * raise backend error * fix comment * clean up * DOC: add docstring example * TST: full code coverage * TST: set os dependent coverage configuration file * Update .github/workflows/test.yml Co-authored-by: Hagen Wierstorf <[email protected]> * TST: try runner.os * DOC: ls() remove FileNotFoundError * DOC: latest_version() update raises section * Update audbackend/core/backend.py Co-authored-by: Hagen Wierstorf <[email protected]> * DOC: put_archive update raises * DOC: put_archive() fix order * Artifactory._exists(): do not catch errors * TST: test BackendError with all functions --------- Co-authored-by: Hagen Wierstorf <[email protected]>
1 parent c07913f commit 13e158b

13 files changed

+183
-110
lines changed

.coveragerc.Linux

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
[report]
2+
exclude_lines =
3+
pragma: no cover
4+
pragma: no Linux cover

.coveragerc.Windows

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
[report]
2+
exclude_lines =
3+
pragma: no cover
4+
pragma: no Windows cover

.coveragerc.macOS

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
[report]
2+
exclude_lines =
3+
pragma: no cover
4+
pragma: no macOS cover

.github/workflows/test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ jobs:
4343
ARTIFACTORY_USERNAME: ${{ secrets.ARTIFACTORY_USERNAME }}
4444
ARTIFACTORY_API_KEY: ${{ secrets.ARTIFACTORY_API_KEY }}
4545
run: |
46-
python -m pytest
46+
python -m pytest --cov-config=.coveragerc.${{ runner.os }}
4747
4848
- name: Upload coverage to Codecov
4949
uses: codecov/codecov-action@v3

audbackend/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
)
66
from audbackend.core.artifactory import Artifactory
77
from audbackend.core.backend import Backend
8+
from audbackend.core.errors import BackendError
89
from audbackend.core.filesystem import FileSystem
910
from audbackend.core.repository import Repository
1011
from audbackend.core.utils import md5

audbackend/core/artifactory.py

Lines changed: 9 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
1-
import dohq_artifactory
21
import os
32

4-
import requests
53
import typing
64

75
import audfactory
86

7+
from audbackend.core import utils
98
from audbackend.core.backend import Backend
109

1110

@@ -43,10 +42,8 @@ def _exists(
4342
) -> bool:
4443
r"""Check if file exists on backend."""
4544
path = self._path(path, version)
46-
try:
47-
return audfactory.path(path).exists()
48-
except self._non_existing_path_error:
49-
return False
45+
path = audfactory.path(path)
46+
return path.exists()
5047

5148
def _folder(
5249
self,
@@ -82,18 +79,15 @@ def _ls(
8279
self,
8380
folder: str,
8481
):
85-
r"""List all files under folder.
86-
87-
Return an empty list if no files match or folder does not exist.
82+
r"""List all files under folder."""
8883

89-
"""
9084
folder = self._folder(folder)
91-
9285
folder = audfactory.path(folder)
93-
try:
94-
paths = [str(x) for x in folder.glob("**/*") if x.is_file()]
95-
except self._non_existing_path_error: # pragma: nocover
96-
paths = []
86+
87+
if not folder.exists():
88+
utils.raise_file_not_found_error(str(folder))
89+
90+
paths = [str(x) for x in folder.glob("**/*") if x.is_file()]
9791

9892
# <host>/<repository>/<folder>/<name>
9993
# ->
@@ -172,23 +166,3 @@ def _versions(
172166
vs = [v for v in vs if self._exists(path, v)]
173167

174168
return vs
175-
176-
_non_existing_path_error = (
177-
RuntimeError,
178-
requests.exceptions.HTTPError,
179-
dohq_artifactory.exception.ArtifactoryException,
180-
)
181-
r"""Error expected for non-existing paths.
182-
183-
If a user has no permission to a given path
184-
or the path does not exists
185-
:func:`audfactory.path` might return a
186-
``RuntimeError``,
187-
``requests.exceptions.HTTPError``
188-
for ``dohq_artifactory<0.8``
189-
or
190-
``artifactory.exception.ArtifactoryException``
191-
for ``dohq_artifactory>=0.8``.
192-
So we better catch all of them.
193-
194-
"""

audbackend/core/backend.py

Lines changed: 73 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ def checksum(
5555
MD5 checksum
5656
5757
Raises:
58-
FileNotFoundError: if file does not exist on backend
58+
BackendError: if an error is raised on the backend
5959
ValueError: if ``path`` contains invalid character
6060
6161
Examples:
@@ -64,10 +64,12 @@ def checksum(
6464
6565
"""
6666
utils.check_path_for_allowed_chars(path)
67-
if not self._exists(path, version):
68-
utils.raise_file_not_found_error(path, version=version)
6967

70-
return self._checksum(path, version)
68+
return utils.call_function_on_backend(
69+
self._checksum,
70+
path,
71+
version,
72+
)
7173

7274
def _exists(
7375
self,
@@ -92,6 +94,7 @@ def exists(
9294
``True`` if file exists
9395
9496
Raises:
97+
BackendError: if an error is raised on the backend
9598
ValueError: if ``path`` contains invalid character
9699
97100
Examples:
@@ -101,7 +104,11 @@ def exists(
101104
"""
102105
utils.check_path_for_allowed_chars(path)
103106

104-
return self._exists(path, version)
107+
return utils.call_function_on_backend(
108+
self._exists,
109+
path,
110+
version,
111+
)
105112

106113
def get_archive(
107114
self,
@@ -129,7 +136,7 @@ def get_archive(
129136
extracted files
130137
131138
Raises:
132-
FileNotFoundError: if archive does not exist on backend
139+
BackendError: if an error is raised on the backend
133140
FileNotFoundError: if ``tmp_root`` does not exist
134141
PermissionError: if the user lacks write permissions
135142
for ``dst_path``
@@ -195,7 +202,7 @@ def get_file(
195202
full path to local file
196203
197204
Raises:
198-
FileNotFoundError: if ``src_path`` does not exist on backend
205+
BackendError: if an error is raised on the backend
199206
PermissionError: if the user lacks write permissions
200207
for ``dst_path``
201208
ValueError: if ``src_path`` contains invalid character
@@ -210,13 +217,25 @@ def get_file(
210217
211218
"""
212219
utils.check_path_for_allowed_chars(src_path)
213-
if not self._exists(src_path, version):
214-
utils.raise_file_not_found_error(src_path, version=version)
215220

216-
dst_path = audeer.safe_path(dst_path)
217-
audeer.mkdir(os.path.dirname(dst_path))
218-
219-
self._get_file(src_path, dst_path, version, verbose)
221+
dst_path = audeer.path(dst_path)
222+
dst_root = os.path.dirname(dst_path)
223+
224+
audeer.mkdir(dst_root)
225+
if (
226+
not os.access(dst_root, os.W_OK) or
227+
(os.path.exists(dst_path) and not os.access(dst_root, os.W_OK))
228+
): # pragma: no Windows cover
229+
msg = f"Permission denied: '{dst_path}'"
230+
raise PermissionError(msg)
231+
232+
utils.call_function_on_backend(
233+
self._get_file,
234+
src_path,
235+
dst_path,
236+
version,
237+
verbose,
238+
)
220239

221240
return dst_path
222241

@@ -264,7 +283,8 @@ def latest_version(
264283
version string
265284
266285
Raises:
267-
RuntimeError: if ``path`` does not exist on backend
286+
BackendError: if an error is raised on the backend
287+
RuntimeError: if no version is found
268288
ValueError: if ``path`` contains invalid character
269289
270290
Examples:
@@ -288,11 +308,7 @@ def _ls(
288308
self,
289309
folder: str,
290310
) -> typing.List[typing.Tuple[str, str, str]]: # pragma: no cover
291-
r"""List all files under folder.
292-
293-
Return an empty list if no files match or folder does not exist.
294-
295-
"""
311+
r"""List all files under folder."""
296312
raise NotImplementedError()
297313

298314
def ls(
@@ -323,7 +339,7 @@ def ls(
323339
list of tuples (path, version)
324340
325341
Raises:
326-
FileNotFoundError: if ``folder`` does not exist
342+
BackendError: if an error is raised on the backend
327343
ValueError: if ``folder`` contains invalid character
328344
329345
Examples:
@@ -340,16 +356,9 @@ def ls(
340356
utils.check_path_for_allowed_chars(folder)
341357
if not folder.endswith('/'):
342358
folder += '/'
343-
paths = self._ls(folder)
359+
paths = utils.call_function_on_backend(self._ls, folder)
344360
paths = sorted(paths)
345361

346-
if len(paths) == 0:
347-
if folder == '/':
348-
# special case that there are no files on the backend
349-
return []
350-
else:
351-
utils.raise_file_not_found_error(folder)
352-
353362
if pattern:
354363
paths = [(p, v) for p, v in paths if fnmatch.fnmatch(p, pattern)]
355364

@@ -399,10 +408,12 @@ def put_archive(
399408
verbose: show debug messages
400409
401410
Raises:
402-
FileNotFoundError: if one or more files do not exist
403-
FileNotFoundError: if ``tmp_root`` does not exist
404-
ValueError: if ``dst_path`` contains invalid character
411+
BackendError: if an error is raised on the backend
412+
FileNotFoundError: if ``src_root``,
413+
``tmp_root``,
414+
or one or more ``files`` do not exist
405415
RuntimeError: if extension of ``dst_path`` is not supported
416+
ValueError: if ``dst_path`` contains invalid character
406417
407418
Examples:
408419
>>> backend.exists('a.tar.gz', '1.0.0')
@@ -414,16 +425,23 @@ def put_archive(
414425
415426
"""
416427
utils.check_path_for_allowed_chars(dst_path)
417-
src_root = audeer.safe_path(src_root)
428+
src_root = audeer.path(src_root)
429+
430+
if not os.path.exists(src_root):
431+
utils.raise_file_not_found_error(src_root)
418432

419-
if isinstance(files, str):
420-
files = [files]
433+
files = audeer.to_list(files)
421434

422435
for file in files:
423436
path = os.path.join(src_root, file)
424437
if not os.path.exists(path):
425438
utils.raise_file_not_found_error(path)
426439

440+
if tmp_root is not None:
441+
tmp_root = audeer.path(tmp_root)
442+
if not os.path.exists(tmp_root):
443+
utils.raise_file_not_found_error(tmp_root)
444+
427445
with tempfile.TemporaryDirectory(dir=tmp_root) as tmp:
428446

429447
archive = audeer.path(tmp, os.path.basename(dst_path))
@@ -475,6 +493,7 @@ def put_file(
475493
file path on backend
476494
477495
Raises:
496+
BackendError: if an error is raised on the backend
478497
FileNotFoundError: if ``src_path`` does not exist
479498
ValueError: if ``dst_path`` contains invalid character
480499
@@ -493,10 +512,16 @@ def put_file(
493512

494513
# skip if file with same checksum already exists
495514
if not (
496-
self._exists(dst_path, version)
497-
and self._checksum(dst_path, version) == utils.md5(src_path)
515+
self.exists(dst_path, version)
516+
and self.checksum(dst_path, version) == utils.md5(src_path)
498517
):
499-
self._put_file(src_path, dst_path, version, verbose)
518+
utils.call_function_on_backend(
519+
self._put_file,
520+
src_path,
521+
dst_path,
522+
version,
523+
verbose,
524+
)
500525

501526
def _remove_file(
502527
self,
@@ -518,7 +543,7 @@ def remove_file(
518543
version: version string
519544
520545
Raises:
521-
FileNotFoundError: if ``path`` does not exist on backend
546+
BackendError: if an error is raised on the backend
522547
ValueError: if ``path`` contains invalid character
523548
524549
Examples:
@@ -530,10 +555,12 @@ def remove_file(
530555
531556
"""
532557
utils.check_path_for_allowed_chars(path)
533-
if not self._exists(path, version):
534-
utils.raise_file_not_found_error(path, version=version)
535558

536-
path = self._remove_file(path, version)
559+
utils.call_function_on_backend(
560+
self._remove_file,
561+
path,
562+
version,
563+
)
537564

538565
@property
539566
def sep(self) -> str:
@@ -587,6 +614,7 @@ def versions(
587614
list of versions in ascending order
588615
589616
Raises:
617+
BackendError: if an error is raised on the backend
590618
ValueError: if ``path`` contains invalid character
591619
592620
Examples:
@@ -596,6 +624,9 @@ def versions(
596624
"""
597625
utils.check_path_for_allowed_chars(path)
598626

599-
vs = self._versions(path)
627+
vs = utils.call_function_on_backend(
628+
self._versions,
629+
path,
630+
)
600631

601632
return audeer.sort_versions(vs)

audbackend/core/errors.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
class BackendError(Exception):
2+
r"""Wrapper for any error raised on the backend.
3+
4+
Args:
5+
exception: exception raised by backend
6+
7+
Examples:
8+
>>> try:
9+
... backend.checksum('does-not-exist', '1.0.0')
10+
... except BackendError as ex:
11+
... ex.exception
12+
FileNotFoundError(2, 'No such file or directory')
13+
14+
"""
15+
def __init__(
16+
self,
17+
exception: Exception,
18+
):
19+
self.exception = exception
20+
r"""Exception raised by backend."""
21+
22+
super().__init__(
23+
'An exception was raised by the backend, '
24+
'please see stack trace for further information.'
25+
)

0 commit comments

Comments
 (0)