diff --git a/cterasdk/asynchronous/core/files/browser.py b/cterasdk/asynchronous/core/files/browser.py index 637f103e..ae128ba1 100644 --- a/cterasdk/asynchronous/core/files/browser.py +++ b/cterasdk/asynchronous/core/files/browser.py @@ -70,6 +70,14 @@ async def listdir(self, path, depth=None, include_deleted=False): """ return await io.listdir(self._core, self.normalize(path), depth=depth, include_deleted=include_deleted) + async def exists(self, path): + """ + Check if item exists + + :param str path: Path + """ + return await io.exists(self._core, self.normalize(path)) + async def versions(self, path): """ List snapshots of a file or directory diff --git a/cterasdk/asynchronous/core/files/io.py b/cterasdk/asynchronous/core/files/io.py index 358605e5..5330344f 100644 --- a/cterasdk/asynchronous/core/files/io.py +++ b/cterasdk/asynchronous/core/files/io.py @@ -16,10 +16,18 @@ async def listdir(core, path, depth=None, include_deleted=False, search_criteria return await core.v1.api.execute('', 'fetchResources', param) -async def root(core, path): +async def exists(core, path): + try: + await metadata(core, path) + return True + except exceptions.ResourceNotFoundError: + return False + + +async def metadata(core, path): response = await listdir(core, path, 0) if response.root is None: - raise exceptions.RemoteStorageException(path.absolute) + raise exceptions.ResourceNotFoundError(path.absolute) return response.root @@ -81,7 +89,7 @@ async def move(core, *paths, destination=None): async def retrieve_remote_dir(core, directory): - resource = await root(core, directory) + resource = await metadata(core, directory) if not resource.isFolder: raise exceptions.RemoteStorageException('The destination path is not a directory', None, path=directory.absolute) return str(resource.cloudFolderInfo.uid) diff --git a/cterasdk/asynchronous/core/notifications.py b/cterasdk/asynchronous/core/notifications.py index 70fcdecd..d3928cc0 100644 --- a/cterasdk/asynchronous/core/notifications.py +++ b/cterasdk/asynchronous/core/notifications.py @@ -6,7 +6,7 @@ from .base_command import BaseCommand from ...common import Object from ...lib import CursorResponse -from ...exceptions import ClientResponseException, NotificationsError +from ...exceptions import HTTPError, NotificationsError class Notifications(BaseCommand): @@ -81,7 +81,7 @@ async def ancestors(self, descendant): logging.getLogger('cterasdk.metadata.connector').debug('Getting ancestors. %s', {'guid': param.guid, 'folder_id': param.folder_id}) try: return await self._core.v2.api.post('/metadata/ancestors', param) - except ClientResponseException: + except HTTPError: logging.getLogger('cterasdk.metadata.connector').error('Could not retrieve ancestors. %s', {'folder_id': param.folder_id, 'guid': param.guid}) raise diff --git a/cterasdk/asynchronous/edge/files/browser.py b/cterasdk/asynchronous/edge/files/browser.py index f4953081..1e2e3035 100644 --- a/cterasdk/asynchronous/edge/files/browser.py +++ b/cterasdk/asynchronous/edge/files/browser.py @@ -13,7 +13,23 @@ async def listdir(self, path): :param str path: Path """ - return await io.listdir(self._edge, path) + return await io.listdir(self._edge, self.normalize(path)) + + async def walk(self, path): + """ + Walk Directory Contents + + :param str path: Path to walk + """ + return io.walk(self._edge, path) + + async def exists(self, path): + """ + Check if item exists + + :param str path: Path + """ + return await io.exists(self._edge, self.normalize(path)) async def handle(self, path): """ diff --git a/cterasdk/asynchronous/edge/files/io.py b/cterasdk/asynchronous/edge/files/io.py index 92518f46..aaefad43 100644 --- a/cterasdk/asynchronous/edge/files/io.py +++ b/cterasdk/asynchronous/edge/files/io.py @@ -2,14 +2,33 @@ from ....cio.common import encode_request_parameter from ....cio import edge as fs from ....cio import exceptions +from ....exceptions import HTTPError logger = logging.getLogger('cterasdk.edge') async def listdir(edge, path): - with fs.listdir(path) as param: - return await edge.api.execute('/status/fileManager', 'listPhysicalFolders', param) + return fs.format_listdir_response(path.reference.as_posix(), await edge.io.propfind(path.absolute, 1)) + + +async def walk(edge, path): + paths = [fs.EdgePath.instance('/', path)] + while len(paths) > 0: + path = paths.pop(0) + entries = await listdir(edge, path) + for e in entries: + if e.is_dir: + paths.append(fs.EdgePath.instance('/', e)) + yield e + + +async def exists(edge, path): + try: + await edge.io.propfind(path.absolute, 0) + return True + except HTTPError: + return False async def mkdir(edge, path): diff --git a/cterasdk/cio/edge.py b/cterasdk/cio/edge.py index 63f46898..a3b6f5e2 100644 --- a/cterasdk/cio/edge.py +++ b/cterasdk/cio/edge.py @@ -1,6 +1,9 @@ import logging +from datetime import datetime from contextlib import contextmanager +from pathlib import Path from ..common import Object +from ..objects.uri import unquote from . import common, exceptions @@ -10,17 +13,49 @@ class EdgePath(common.BasePath): """Path for CTERA Edge Filer""" + def __init__(self, scope, reference): + """ + Initialize a CTERA Edge Filer Path. + + :param str scope: Scope. + :param str reference: Reference. + """ + if isinstance(reference, Object): + super().__init__(scope, reference.path) + elif isinstance(reference, str): + super().__init__(scope, reference) + else: + message = 'Path validation failed: ensure the path exists and is correctly formatted.' + logger.error(message) + raise ValueError(message) + @staticmethod def instance(scope, reference): return EdgePath(scope, reference) -@contextmanager -def listdir(path): - param = Object() - param.path = path - logger.info('Listing directory: %s', path) - yield param +def fetch_reference(href): + namespace = 'localFiles/' + return unquote(href[href.index(namespace)+len(namespace):]) + + +def format_listdir_response(parent, response): + entries = [] + for e in response: + path = fetch_reference(e.href) + if parent != path: + is_dir = e.getcontenttype == 'httpd/unix-directory' + param = Object( + path=path, + name=Path(path).name, + is_dir=is_dir, + is_file=not is_dir, + created_at=e.creationdate, + last_modified=datetime.strptime(e.getlastmodified, "%a, %d %b %Y %H:%M:%S GMT").isoformat(), + size=e.getcontentlength + ) + entries.append(param) + return entries @contextmanager diff --git a/cterasdk/clients/base.py b/cterasdk/clients/base.py index 1d2ae989..95352d0c 100644 --- a/cterasdk/clients/base.py +++ b/cterasdk/clients/base.py @@ -1,6 +1,6 @@ import logging import threading -from . import async_requests +from . import async_requests, errors from .settings import ClientSessionSettings, TraceSettings from ..common import utils @@ -103,11 +103,13 @@ def join_headers(self, request): def baseurl(self): return self._builder() - def request(self, request, *, on_response=None): - return self._request(request, on_response=on_response) + def request(self, request, *, on_response=None, on_error=None): + on_error = on_error if on_error else errors.DefaultHandler() + return self._request(request, on_response=on_response, on_error=on_error) - async def async_request(self, request, *, on_response=None): - return await self._request(request, on_response=on_response) + async def a_request(self, request, *, on_response=None, on_error=None): + on_error = on_error if on_error else errors.DefaultHandler() + return await self._request(request, on_response=on_response, on_error=on_error) async def close(self): await self._session.close() diff --git a/cterasdk/clients/clients.py b/cterasdk/clients/clients.py index 207b121f..0393d4e1 100644 --- a/cterasdk/clients/clients.py +++ b/cterasdk/clients/clients.py @@ -1,5 +1,5 @@ import asyncio -from . import errors +from .errors import XMLHandler, JSONHandler from .base import BaseClient, BaseResponse, run_threadsafe from .common import Serializers, Deserializers from . import async_requests, decorators @@ -10,35 +10,34 @@ class AsyncClient(BaseClient): """Asynchronous Client""" @decorators.authenticated - async def get(self, path, *, on_response=None, **kwargs): + async def get(self, path, *, on_response=None, on_error=None, **kwargs): request = async_requests.GetRequest(self._builder(path), **kwargs) - return await self.async_request(request, on_response=on_response) + return await self.a_request(request, on_response=on_response, on_error=on_error) @decorators.authenticated - async def put(self, path, data, *, data_serializer=None, on_response=None, **kwargs): + async def put(self, path, data, *, data_serializer=None, on_response=None, on_error=None, **kwargs): request = async_requests.PutRequest(self._builder(path), data=data_serializer(data), **kwargs) - return await self.async_request(request, on_response=on_response) + return await self.a_request(request, on_response=on_response, on_error=on_error) @decorators.authenticated - async def post(self, path, data, *, data_serializer=None, on_response=None, **kwargs): + async def post(self, path, data, *, data_serializer=None, on_response=None, on_error=None, **kwargs): request = async_requests.PostRequest(self._builder(path), data=data_serializer(data), **kwargs) - return await self.async_request(request, on_response=on_response) + return await self.a_request(request, on_response=on_response, on_error=on_error) @decorators.authenticated - async def form_data(self, path, data, *, on_response=None, **kwargs): + async def form_data(self, path, data, *, on_response=None, on_error=None, **kwargs): request = async_requests.PostRequest(self._builder(path), data=Serializers.FormData(data), **kwargs) - return await self.async_request(request, on_response=on_response) + return await self.a_request(request, on_response=on_response, on_error=on_error) @decorators.authenticated - async def delete(self, path, *, on_response=None, **kwargs): + async def delete(self, path, *, on_response=None, on_error=None, **kwargs): request = async_requests.DeleteRequest(self._builder(path), **kwargs) - return await self.async_request(request, on_response=on_response) + return await self.a_request(request, on_response=on_response, on_error=on_error) - async def _request(self, request, *, on_response=None): + async def _request(self, request, *, on_response=None, on_error=None): on_response = on_response if on_response else AsyncResponse.new() response = await self._session.await_promise(self.join_headers(request), on_response=on_response) - error_message = await response.text() if response.status > 399 else None - return errors.accept(response, error_message) + return response if response.ok else await on_error.a_accept(response) class AsyncFolders(AsyncClient): @@ -57,30 +56,34 @@ class AsyncWebDAV(AsyncClient): """WebDAV""" async def download(self, path, **kwargs): - return await super().get(path, **kwargs) + return await super().get(path, on_error=XMLHandler(), **kwargs) - async def propfind(self, path): - request = async_requests.PropfindRequest(self._builder(path)) - response = await self.async_request(request) + @decorators.authenticated + async def propfind(self, path, depth): + request = async_requests.PropfindRequest(self._builder(path), headers={'depth': str(depth)}) + response = await self.a_request(request, on_error=XMLHandler()) return await response.dav() + @decorators.authenticated async def mkcol(self, path): request = async_requests.MkcolRequest(self._builder(path)) - response = await self.async_request(request) + response = await self.a_request(request, on_error=XMLHandler()) return await response.text() + @decorators.authenticated async def copy(self, source, destination, *, overwrite=False): request = async_requests.CopyRequest(self._builder(source), headers=self._webdav_headers(destination, overwrite)) - response = await self.async_request(request) + response = await self.a_request(request, on_error=XMLHandler()) return await response.xml() + @decorators.authenticated async def move(self, source, destination, *, overwrite=False): request = async_requests.MoveRequest(self._builder(source), headers=self._webdav_headers(destination, overwrite)) - response = await self.async_request(request) + response = await self.a_request(request, on_error=XMLHandler()) return await response.xml() async def delete(self, path): # pylint: disable=arguments-differ - response = await super().delete(path) + response = await super().delete(path, on_error=XMLHandler()) return await response.text() def _webdav_headers(self, destination, overwrite): @@ -97,38 +100,38 @@ def __init__(self, builder=None, session=None, settings=None, authenticator=None self.headers.persist_headers({'Content-Type': 'application/json'}) async def get(self, path, **kwargs): - response = await super().get(path, **kwargs) + response = await super().get(path, on_error=JSONHandler(), **kwargs) return await response.json() async def put(self, path, data, **kwargs): - response = await super().put(path, data, data_serializer=Serializers.JSON, **kwargs) + response = await super().put(path, data, data_serializer=Serializers.JSON, on_error=JSONHandler(), **kwargs) return await response.json() async def post(self, path, data, **kwargs): - response = await super().post(path, data, data_serializer=Serializers.JSON, **kwargs) + response = await super().post(path, data, data_serializer=Serializers.JSON, on_error=JSONHandler(), **kwargs) return await response.json() async def delete(self, path, **kwargs): - response = await super().delete(path, **kwargs) + response = await super().delete(path, on_error=JSONHandler(), **kwargs) return await response.json() class AsyncXML(AsyncClient): async def get(self, path, **kwargs): - response = await super().get(path, **kwargs) + response = await super().get(path, on_error=XMLHandler(), **kwargs) return await response.xml() async def put(self, path, data, **kwargs): - response = await super().put(path, data, data_serializer=Serializers.XML, **kwargs) + response = await super().put(path, data, data_serializer=Serializers.XML, on_error=XMLHandler(), **kwargs) return await response.xml() async def post(self, path, data, **kwargs): - response = await super().post(path, data, data_serializer=Serializers.XML, **kwargs) + response = await super().post(path, data, data_serializer=Serializers.XML, on_error=XMLHandler(), **kwargs) return await response.xml() async def delete(self, path, **kwargs): - response = await super().delete(path, **kwargs) + response = await super().delete(path, on_error=XMLHandler(), **kwargs) return await response.xml() @@ -167,7 +170,7 @@ async def defaults(self, classname): class AsyncResponse(BaseResponse): """Asynchronous Response Object""" - async def async_iter_content(self, chunk_size=None): + async def a_iter_content(self, chunk_size=None): async for chunk in self._response.content.iter_chunked(chunk_size if chunk_size else 5120): yield chunk @@ -198,45 +201,44 @@ class Client(BaseClient): """Synchronous Client""" @decorators.authenticated - def handle(self, path, *, on_response=None, **kwargs): + def handle(self, path, *, on_response=None, on_error=None, **kwargs): request = async_requests.GetRequest(self._builder(path), **kwargs) - return self.request(request, on_response=on_response) + return self.request(request, on_response=on_response, on_error=on_error) @decorators.authenticated - def get(self, path, *, on_response=None, **kwargs): + def get(self, path, *, on_response=None, on_error=None, **kwargs): request = async_requests.GetRequest(self._builder(path), **kwargs) - return self.request(request, on_response=on_response) + return self.request(request, on_response=on_response, on_error=on_error) @decorators.authenticated - def put(self, path, data, *, data_serializer=None, on_response=None, **kwargs): + def put(self, path, data, *, data_serializer=None, on_response=None, on_error=None, **kwargs): request = async_requests.PutRequest(self._builder(path), data=data_serializer(data), **kwargs) - return self.request(request, on_response=on_response) + return self.request(request, on_response=on_response, on_error=on_error) @decorators.authenticated - def post(self, path, data, *, data_serializer=None, on_response=None, **kwargs): + def post(self, path, data, *, data_serializer=None, on_response=None, on_error=None, **kwargs): request = async_requests.PostRequest(self._builder(path), data=data_serializer(data), **kwargs) - return self.request(request, on_response=on_response) + return self.request(request, on_response=on_response, on_error=on_error) @decorators.authenticated - def form_data(self, path, data, *, on_response=None, **kwargs): + def form_data(self, path, data, *, on_response=None, on_error=None, **kwargs): request = async_requests.PostRequest(self._builder(path), data=Serializers.FormData(data), **kwargs) - return self.request(request, on_response=on_response) + return self.request(request, on_response=on_response, on_error=on_error) @decorators.authenticated - def multipart(self, path, form, *, on_response=None, **kwargs): + def multipart(self, path, form, *, on_response=None, on_error=None, **kwargs): request = async_requests.PostRequest(self._builder(path), data=form.data, **kwargs) - return self.request(request, on_response=on_response) + return self.request(request, on_response=on_response, on_error=on_error) @decorators.authenticated - def delete(self, path, *, on_response=None, **kwargs): + def delete(self, path, *, on_response=None, on_error=None, **kwargs): request = async_requests.DeleteRequest(self._builder(path), **kwargs) - return self.request(request, on_response=on_response) + return self.request(request, on_response=on_response, on_error=on_error) - def _request(self, request, *, on_response=None): + def _request(self, request, *, on_response=None, on_error=None): on_response = on_response if on_response else SyncResponse.new() response = execute(self._session.await_promise, self.join_headers(request), on_response=on_response) - error_message = response.text() if response.status > 399 else None - return errors.accept(response, error_message) + return response if response.ok else on_error.accept(response) def close(self): # pylint: disable=invalid-overridden-method return execute(super().close) @@ -257,30 +259,34 @@ def upload(self, path, data, **kwargs): class WebDAV(Client): def download(self, path, **kwargs): - return super().handle(path, **kwargs) + return super().handle(path, on_error=XMLHandler(), **kwargs) - def propfind(self, path): - request = async_requests.PropfindRequest(self._builder(path)) - response = self.request(request) + @decorators.authenticated + def propfind(self, path, depth): + request = async_requests.PropfindRequest(self._builder(path), headers={'depth': str(depth)}) + response = self.request(request, on_error=XMLHandler()) return response.dav() + @decorators.authenticated def mkcol(self, path): request = async_requests.MkcolRequest(self._builder(path)) - response = self.request(request) + response = self.request(request, on_error=XMLHandler()) return response.text() + @decorators.authenticated def copy(self, source, destination, *, overwrite=False): request = async_requests.CopyRequest(self._builder(source), headers=self._webdav_headers(destination, overwrite)) - response = self.request(request) + response = self.request(request, on_error=XMLHandler()) return response.xml() + @decorators.authenticated def move(self, source, destination, *, overwrite=False): request = async_requests.MoveRequest(self._builder(source), headers=self._webdav_headers(destination, overwrite)) - response = self.request(request) + response = self.request(request, on_error=XMLHandler()) return response.xml() def delete(self, path): # pylint: disable=arguments-differ - response = super().delete(path) + response = super().delete(path, on_error=XMLHandler()) return response.text() def _webdav_headers(self, destination, overwrite): @@ -298,23 +304,23 @@ def __init__(self, builder=None, session=None, settings=None, authenticator=None self._type = {'Content-Type': 'text/plain'} def get(self, path, **kwargs): - response = super().get(path, **kwargs) + response = super().get(path, on_error=XMLHandler(), **kwargs) return response.xml() def put(self, path, data, **kwargs): - response = super().put(path, data, data_serializer=Serializers.XML, headers=self._type, **kwargs) + response = super().put(path, data, data_serializer=Serializers.XML, headers=self._type, on_error=XMLHandler(), **kwargs) return response.xml() def post(self, path, data, **kwargs): - response = super().post(path, data, data_serializer=Serializers.XML, headers=self._type, **kwargs) + response = super().post(path, data, data_serializer=Serializers.XML, headers=self._type, on_error=XMLHandler(), **kwargs) return response.xml() def form_data(self, path, data, **kwargs): - response = super().form_data(path, data, **kwargs) + response = super().form_data(path, data, on_error=XMLHandler(), **kwargs) return response.xml() def delete(self, path, **kwargs): - response = super().delete(path, **kwargs) + response = super().delete(path, on_error=XMLHandler(), **kwargs) return response.xml() @@ -322,19 +328,19 @@ class JSON(Client): """JSON Serializer and Deserializer""" def get(self, path, **kwargs): - response = super().get(path, **kwargs) + response = super().get(path, on_error=JSONHandler(), **kwargs) return response.json() def put(self, path, data, **kwargs): - response = super().put(path, data, data_serializer=Serializers.JSON, **kwargs) + response = super().put(path, data, data_serializer=Serializers.JSON, on_error=JSONHandler(), **kwargs) return response.json() def post(self, path, data, **kwargs): - response = super().post(path, data, data_serializer=Serializers.JSON, **kwargs) + response = super().post(path, data, data_serializer=Serializers.JSON, on_error=JSONHandler(), **kwargs) return response.json() def delete(self, path, **kwargs): - response = super().delete(path, **kwargs) + response = super().delete(path, on_error=JSONHandler(), **kwargs) return response.json() @@ -380,7 +386,7 @@ class Migrate(JSON): """CTERA Migrate Service""" def login(self): - response = Client.get(self, '/auth/user') + response = Client.get(self, '/auth/user', on_error=JSONHandler()) self.headers.persist_response_header(response, 'x-mt-x') return response.json() @@ -403,7 +409,7 @@ class SyncResponse(AsyncResponse): def iter_content(self, chunk_size=None): while True: try: - yield execute(super().async_iter_content(chunk_size).__anext__) + yield execute(super().a_iter_content(chunk_size).__anext__) except StopAsyncIteration: break diff --git a/cterasdk/clients/decorators.py b/cterasdk/clients/decorators.py index 1c93ce69..bdac38de 100644 --- a/cterasdk/clients/decorators.py +++ b/cterasdk/clients/decorators.py @@ -4,6 +4,9 @@ from ..exceptions import SessionExpired, NotLoggedIn +logger = logging.getLogger('cterasdk.common') + + def authenticated(execute_request): @functools.wraps(execute_request) def authenticate_then_execute(self, *args, **kwargs): @@ -11,8 +14,8 @@ def authenticate_then_execute(self, *args, **kwargs): try: return execute_request(self, *args, **kwargs) except SessionExpired: - logging.getLogger('cterasdk.common').error('Session expired.') + logger.error('Session expired.') self.cookies.clear() - logging.getLogger('cterasdk.common').error('Not logged in.') + logger.error('Not logged in.') raise NotLoggedIn() return authenticate_then_execute diff --git a/cterasdk/clients/errors.py b/cterasdk/clients/errors.py index d51496aa..25356aa3 100644 --- a/cterasdk/clients/errors.py +++ b/cterasdk/clients/errors.py @@ -1,26 +1,128 @@ -import aiohttp - +from abc import ABC, abstractmethod +from http import HTTPStatus +from ..exceptions import HTTPError from ..common import Object from ..convert import fromjsonstr, fromxmlstr -from ..exceptions import ClientResponseException -class ClientError(Object): +class Error(Object): + """ + Error object. + + :ivar cterasdk.common.object.Object request: Request + :ivar cterasdk.common.object.Object response: Response + """ + def __init__(self, response, error): + super().__init__( + request=Object( + method=response.method, + url=response.real_url.human_repr() + ), + response=Object( + status=response.status, + error=error + ) + ) + + +class ErrorHandler(ABC): + + async def a_accept(self, response): + error = self._accept(response, await response.text()) + raise_error(response.status, error) + + def accept(self, response): + error = self._accept(response, response.text()) + raise_error(response.status, error) + + @abstractmethod + def _accept(self, response, message): + raise NotImplementedError("Subclass must implement the '_accept' method") + + +class DefaultHandler(ErrorHandler): + + def _accept(self, response, message): + return Error(response, message) + + +class XMLHandler(ErrorHandler): + + def _accept(self, response, message): + return Error(response, fromxmlstr(message) or message) + + +class JSONHandler(ErrorHandler): + + def _accept(self, response, message): + return Error(response, fromjsonstr(message) or message) + + +class BadRequest(HTTPError): + + def __init__(self, error): + super().__init__(HTTPStatus.BAD_REQUEST, error) + + +class Unauthorized(HTTPError): + + def __init__(self, error): + super().__init__(HTTPStatus.UNAUTHORIZED, error) + + +class Forbidden(HTTPError): + + def __init__(self, error): + super().__init__(HTTPStatus.FORBIDDEN, error) + + +class NotFound(HTTPError): + + def __init__(self, error): + super().__init__(HTTPStatus.NOT_FOUND, error) + + +class Unprocessable(HTTPError): + + def __init__(self, error): + super().__init__(HTTPStatus.UNPROCESSABLE_ENTITY, error) + + +class InternalServerError(HTTPError): + + def __init__(self, error): + super().__init__(HTTPStatus.INTERNAL_SERVER_ERROR, error) + + +class BadGateway(HTTPError): + + def __init__(self, error): + super().__init__(HTTPStatus.BAD_GATEWAY, error) + + +class ServiceUnavailable(HTTPError): + + def __init__(self, error): + super().__init__(HTTPStatus.SERVICE_UNAVAILABLE, error) + + +class GatewayTimeout(HTTPError): - def __init__(self, exception, message): - super().__init__() - self.request = Object() - self.request.method = exception.request_info.method - self.request.url = str(exception.request_info.real_url) - self.response = Object() - self.response.status = exception.status - self.response.message = fromxmlstr(message) or fromjsonstr(message) or message + def __init__(self, error): + super().__init__(HTTPStatus.GATEWAY_TIMEOUT, error) -def accept(response, error_message): - try: - response.raise_for_status() - except aiohttp.ClientResponseError as exception: - error_object = ClientError(exception, error_message) - raise ClientResponseException(error_object) - return response +def raise_error(status, error): + exceptions = { + HTTPStatus.BAD_REQUEST: BadRequest, + HTTPStatus.UNAUTHORIZED: Unauthorized, + HTTPStatus.FORBIDDEN: Forbidden, + HTTPStatus.NOT_FOUND: NotFound, + HTTPStatus.UNPROCESSABLE_ENTITY: Unprocessable, + HTTPStatus.INTERNAL_SERVER_ERROR: InternalServerError, + HTTPStatus.BAD_GATEWAY: BadGateway, + HTTPStatus.SERVICE_UNAVAILABLE: ServiceUnavailable, + HTTPStatus.GATEWAY_TIMEOUT: GatewayTimeout + } + exception = exceptions.get(status, HTTPError) + raise exception(error) diff --git a/cterasdk/clients/tracers/postman.py b/cterasdk/clients/tracers/postman.py index 6d233d3c..66694ac8 100644 --- a/cterasdk/clients/tracers/postman.py +++ b/cterasdk/clients/tracers/postman.py @@ -29,7 +29,14 @@ async def on_request_headers_sent(session, ctx, params): :param aiohttp.ClientSession session: Session. :param cterasdk.transcriber.postman.Command ctx: Command. """ - ctx.request.request_headers({k: v for k, v in params.headers.items() if k in ['Cookie', 'Authorization', 'Content-Type']}) + ctx.request.request_headers({k: v for k, v in params.headers.items() if k in [ + 'Cookie', + 'Authorization', + 'Content-Type', + 'Depth', + 'Overwrite', + 'Destination' + ]}) async def on_request_chunk_sent(session, ctx, params): # pylint: disable=unused-argument diff --git a/cterasdk/convert/deserializers.py b/cterasdk/convert/deserializers.py index ac41b215..5fea4c43 100644 --- a/cterasdk/convert/deserializers.py +++ b/cterasdk/convert/deserializers.py @@ -1,12 +1,16 @@ +import re import json import queue import logging from xml.etree.ElementTree import fromstring, ParseError -from .types import XMLTypes +from .types import XMLTypes, DAVTypes from ..common import Item, Object, Device +logger = logging.getLogger('cterasdk.deserializers') + + def ParseValue(data): if data is None: @@ -50,7 +54,7 @@ def fromjsonstr(fromstr): try: root.node = json.loads(fromstr) except json.decoder.JSONDecodeError: - logging.getLogger('cterasdk.convert').debug('Parsing failed. Expected JSON format but received invalid input.') + logger.debug('Error: Unable to parse input as JSON.') return None root.parent = None root.value = None @@ -84,25 +88,87 @@ def fromjsonstr(fromstr): return root.value -def fromdavxmlstr(string): - return fromstring(string) +def loadxmlstr(string): + if not string: + logger.debug("Nothing to parse: Input payload is empty.") + try: + return True, fromstring(string) + except ParseError: + logger.debug('Error: Unable to parse input as XML.') + return False, None -def fromxmlstr(string): # pylint: disable=too-many-branches,too-many-statements - if not string: - logging.getLogger('cterasdk.convert').debug('Empty payload received.') +def without_namespace(t): + return re.sub(r'^{[^}]+}', '', t) + +def fromdavxmlstr(s): + """ + Convert an WebdAV XML String to a Python Object. + + :param str s: String + """ root = Item() + + success, root.node = loadxmlstr(s) + if not success: + return None + root.value = None root.parent = None - try: - root.node = fromstring(string) - except ParseError: - logging.getLogger('cterasdk.convert').debug('Parsing failed. Expected XML format but received invalid input.') + q = queue.Queue() + q.put(root) + while not q.empty(): + item = q.get() + tag = without_namespace(item.node.tag) + if tag == DAVTypes.MULTISTATUS: + item.value = [] + SetAppendValue(item, item.value) + for kidnode in item.node: + kid = Item() + kid.parent = item + kid.node = kidnode + q.put(kid) + elif tag == DAVTypes.RESPONSE: + item.value = Object() + SetAppendValue(item, item.value) + for kidnode in item.node: + kid = Item() + kid.id = without_namespace(kidnode.tag) + kid.parent = item + kid.node = kidnode + q.put(kid) + elif tag in [DAVTypes.PROP, DAVTypes.PROPSTAT]: + for kidnode in item.node: + kid = Item() + kid.id = without_namespace(kidnode.tag) + kid.parent = item.parent + kid.node = kidnode + q.put(kid) + elif tag in [DAVTypes.HREF, DAVTypes.CREATED_DATE, DAVTypes.LAST_MODIFIED, + DAVTypes.CONTENT_TYPE, DAVTypes.CONTENT_LENGTH]: + value = ParseValue(item.node.text) + SetAppendValue(item, value) + return root.value + + +def fromxmlstr(s): # pylint: disable=too-many-branches,too-many-statements + """ + Convert an XML String to a Python Object. + + :param str s: String + """ + root = Item() + + success, root.node = loadxmlstr(s) + if not success: return None + root.value = None + root.parent = None + q = queue.Queue() q.put(root) while not q.empty(): diff --git a/cterasdk/convert/types.py b/cterasdk/convert/types.py index a548bfb7..20fe73d7 100644 --- a/cterasdk/convert/types.py +++ b/cterasdk/convert/types.py @@ -11,3 +11,15 @@ class XMLTypes: FIRMWARE = 'firmware' NS = 'xmlns:xsi' LOCATION = 'xsi:noNamespaceSchemaLocation' + + +class DAVTypes: + HREF = 'href' + PROPSTAT = 'propstat' + PROP = 'prop' + RESPONSE = 'response' + MULTISTATUS = 'multistatus' + CREATED_DATE = 'creationdate' + LAST_MODIFIED = 'getlastmodified' + CONTENT_TYPE = 'getcontenttype' + CONTENT_LENGTH = 'getcontentlength' diff --git a/cterasdk/core/files/browser.py b/cterasdk/core/files/browser.py index 6ed9e461..7e23d88f 100644 --- a/cterasdk/core/files/browser.py +++ b/cterasdk/core/files/browser.py @@ -73,6 +73,14 @@ def listdir(self, path, depth=None, include_deleted=False): """ return io.listdir(self._core, self.normalize(path), depth=depth, include_deleted=include_deleted) + def exists(self, path): + """ + Check if item exists + + :param str path: Path + """ + return io.exists(self._core, self.normalize(path)) + def versions(self, path): """ List snapshots of a file or directory diff --git a/cterasdk/core/files/io.py b/cterasdk/core/files/io.py index 56ea1ce8..3f2f211d 100644 --- a/cterasdk/core/files/io.py +++ b/cterasdk/core/files/io.py @@ -17,10 +17,18 @@ def listdir(core, path, depth=None, include_deleted=False, search_criteria=None, return core.api.execute('', 'fetchResources', param) -def root(core, path): +def exists(core, path): + try: + metadata(core, path) + return True + except exceptions.ResourceNotFoundError: + return False + + +def metadata(core, path): response = listdir(core, path, 0) if response.root is None: - raise exceptions.RemoteStorageException(path.absolute) + raise exceptions.ResourceNotFoundError(path.absolute) return response.root @@ -82,7 +90,7 @@ def move(core, *paths, destination=None): def retrieve_remote_dir(core, directory): - resource = root(core, directory) + resource = metadata(core, directory) if not resource.isFolder: raise exceptions.RemoteStorageException('The destination path is not a directory', None, path=directory.absolute) return str(resource.cloudFolderInfo.uid) @@ -199,7 +207,7 @@ def add_share_recipients(core, path, recipients): def _obtain_valid_recipients(core, path, recipients): - resource_info = root(core, path) + resource_info = metadata(core, path) valid_recipients = [] for recipient in filter(fs.valid_recipient, recipients): if not recipient.type == CollaboratorType.EXT: @@ -213,7 +221,7 @@ def _obtain_valid_recipients(core, path, recipients): def unshare(core, path): - resource_info = root(core, path) + resource_info = metadata(core, path) with fs.unshare(resource_info, path) as param: return core.api.execute('', 'shareResource', param) diff --git a/cterasdk/core/startup.py b/cterasdk/core/startup.py index c8cde16f..af4f56a2 100644 --- a/cterasdk/core/startup.py +++ b/cterasdk/core/startup.py @@ -1,7 +1,7 @@ import logging import time -from ..exceptions import CTERAException, ClientResponseException +from ..exceptions import CTERAException, HTTPError from .base_command import BaseCommand @@ -36,7 +36,7 @@ def wait(self, retries=120, seconds=5): logging.getLogger('cterasdk.core').debug('Current server status. %s', {'status': current_status}) attempt = attempt + 1 time.sleep(seconds) - except (ConnectionError, TimeoutError, ClientResponseException) as e: + except (ConnectionError, TimeoutError, HTTPError) as e: logging.getLogger('cterasdk.core').debug('Exception. %s', e.__dict__) attempt = attempt + 1 time.sleep(seconds) diff --git a/cterasdk/direct/lib.py b/cterasdk/direct/lib.py index a7e71b05..803da1fa 100644 --- a/cterasdk/direct/lib.py +++ b/cterasdk/direct/lib.py @@ -9,7 +9,7 @@ DownloadConnectionError, DecryptKeyError, DecryptBlockError, NotFoundError, DecompressBlockError, BlockValidationException, \ BlockListConnectionError, DirectIOError -from ..exceptions import ClientResponseException +from ..exceptions import HTTPError logger = logging.getLogger('cterasdk.direct') @@ -70,9 +70,9 @@ async def get_object_coro(): except IOError as error: error_message = 'io' exception = DownloadError(error, file_id, chunk) - except ClientResponseException as error: + except HTTPError as error: error_message = 'unknown' - exception = DownloadError(error.response, file_id, chunk) + exception = DownloadError(error.error, file_id, chunk) error_messages = { "connection": "Connection error", @@ -243,12 +243,12 @@ async def get_chunks_coro(): logger.error('Could not find blocks for file ID: %s.', file_id) raise BlocksNotFoundError(file_id) return Metadata(file_id, response) - except ClientResponseException as error: - if error.response.status == 400: + except HTTPError as error: + if error.code == 400: raise NotFoundError(file_id) - if error.response.status == 401: + if error.code == 401: raise UnAuthorized(file_id) - if error.response.status == 422: + if error.code == 422: raise UnprocessableContent(file_id) raise error except ConnectionError: diff --git a/cterasdk/edge/files/browser.py b/cterasdk/edge/files/browser.py index d8265522..f25b8c87 100644 --- a/cterasdk/edge/files/browser.py +++ b/cterasdk/edge/files/browser.py @@ -13,7 +13,23 @@ def listdir(self, path): :param str path: Path """ - return io.listdir(self._edge, path) + return io.listdir(self._edge, self.normalize(path)) + + def walk(self, path): + """ + Walk Directory Contents + + :param str path: Path to walk + """ + return io.walk(self._edge, path) + + def exists(self, path): + """ + Check if item exists + + :param str path: Path + """ + return io.exists(self._edge, self.normalize(path)) def handle(self, path): """ diff --git a/cterasdk/edge/files/io.py b/cterasdk/edge/files/io.py index 2d4c2bb2..92b037ca 100644 --- a/cterasdk/edge/files/io.py +++ b/cterasdk/edge/files/io.py @@ -2,14 +2,33 @@ from ...cio.common import encode_request_parameter from ...cio import edge as fs from ...cio import exceptions +from ...exceptions import HTTPError logger = logging.getLogger('cterasdk.edge') def listdir(edge, path): - with fs.listdir(path) as param: - return edge.api.execute('/status/fileManager', 'listPhysicalFolders', param) + return fs.format_listdir_response(path.reference.as_posix(), edge.io.propfind(path.absolute, 1)) + + +def walk(edge, path): + paths = [fs.EdgePath.instance('/', path)] + while len(paths) > 0: + path = paths.pop(0) + entries = listdir(edge, path) + for e in entries: + if e.is_dir: + paths.append(fs.EdgePath.instance('/', e)) + yield e + + +def exists(edge, path): + try: + edge.io.propfind(path.absolute, 0) + return True + except HTTPError: + return False def mkdir(edge, path): diff --git a/cterasdk/edge/shares.py b/cterasdk/edge/shares.py index fe57361f..3833cd6c 100644 --- a/cterasdk/edge/shares.py +++ b/cterasdk/edge/shares.py @@ -421,7 +421,9 @@ def remove_screened_file_types(self, name, extensions): self._edge.api.put('/config/fileservices/share/' + share.name + '/screenedFileTypes', new_list) def _validate_root_directory(self, name): - response = self._edge.files.listdir('/') + param = Object() + param.path = '/' + response = self._edge.api.execute('/status/fileManager', 'listPhysicalFolders', param) for root in response: if root.fullpath == f'/{name}': logging.getLogger('cterasdk.edge').debug("Found root directory. %s", diff --git a/cterasdk/edge/volumes.py b/cterasdk/edge/volumes.py index ddb92c1e..f13d5001 100644 --- a/cterasdk/edge/volumes.py +++ b/cterasdk/edge/volumes.py @@ -108,7 +108,7 @@ def delete(self, name): return response except CTERAException as error: logging.getLogger('cterasdk.edge').error("Volume deletion failed. %s", {'name': name}) - raise CTERAException('Volume deletion falied', error) + raise CTERAException('Volume deletion failed', error) def delete_all(self): """ Delete all volumes """ diff --git a/cterasdk/exceptions.py b/cterasdk/exceptions.py index 72754738..87a53229 100644 --- a/cterasdk/exceptions.py +++ b/cterasdk/exceptions.py @@ -42,10 +42,19 @@ def __init__(self): super().__init__('Not logged in.') -class ClientResponseException(CTERAException): - - def __init__(self, error_object): - super().__init__('An error occurred while processing the HTTP request.', error_object) +class HTTPError(CTERAException): + """ + HTTP Error + + :ivar int code: Status code + :ivar str name: Reason + :ivar cterasdk.clients.errors.Error error: Error object + """ + def __init__(self, status, error): + super().__init__(error.request.url) + self.code = status.value + self.name = status.name + self.error = error class NotificationsError(CTERAException): diff --git a/cterasdk/lib/storage/asynfs.py b/cterasdk/lib/storage/asynfs.py index 4b004b65..02f361ca 100644 --- a/cterasdk/lib/storage/asynfs.py +++ b/cterasdk/lib/storage/asynfs.py @@ -33,6 +33,6 @@ async def overwrite(p, handle): if isinstance(fd, bytes): await fd.write(handle) else: - async for chunk in handle.async_iter_content(chunk_size=8192): + async for chunk in handle.a_iter_content(chunk_size=8192): await fd.write(chunk) return p.as_posix() diff --git a/cterasdk/objects/asynchronous/core.py b/cterasdk/objects/asynchronous/core.py index f11a4bdd..94a2f78c 100644 --- a/cterasdk/objects/asynchronous/core.py +++ b/cterasdk/objects/asynchronous/core.py @@ -39,10 +39,6 @@ def __init__(self, core): def upload(self): return self._upload.upload - @property - def propfind(self): - return self._webdav.propfind - @property def download(self): return self._webdav.get diff --git a/cterasdk/objects/synchronous/core.py b/cterasdk/objects/synchronous/core.py index 9dedb080..55dc53a3 100644 --- a/cterasdk/objects/synchronous/core.py +++ b/cterasdk/objects/synchronous/core.py @@ -32,10 +32,6 @@ def __init__(self, core): def upload(self): return self._upload.upload - @property - def propfind(self): - return self._webdav.propfind - @property def download(self): return self._webdav.download diff --git a/tests/ut/aio/direct/test_get_metadata.py b/tests/ut/aio/direct/test_get_metadata.py index 1f5f6b2a..1975e993 100644 --- a/tests/ut/aio/direct/test_get_metadata.py +++ b/tests/ut/aio/direct/test_get_metadata.py @@ -1,9 +1,10 @@ import asyncio import errno +from http import HTTPStatus from unittest import mock import munch from cterasdk import ctera_direct -from cterasdk import exceptions, Object +from cterasdk import exceptions from . import base @@ -35,8 +36,9 @@ async def test_get_file_metadata_not_found(self): self.assertEqual(error.exception.filename, self._file_id) async def test_get_file_metadata_error_400(self): - self._direct._client._api.get.side_effect = exceptions.ClientResponseException( # pylint: disable=protected-access - BaseDirectMetadata._create_error_object(400) + self._direct._client._api.get.side_effect = exceptions.HTTPError( # pylint: disable=protected-access + HTTPStatus.BAD_REQUEST, + BaseDirectMetadata._create_error_object() ) with mock.patch('asyncio.sleep'): with self.assertRaises(ctera_direct.exceptions.NotFoundError) as error: @@ -46,8 +48,9 @@ async def test_get_file_metadata_error_400(self): self.assertEqual(error.exception.filename, self._file_id) async def test_get_file_metadata_error_401(self): - self._direct._client._api.get.side_effect = exceptions.ClientResponseException( # pylint: disable=protected-access - BaseDirectMetadata._create_error_object(401) + self._direct._client._api.get.side_effect = exceptions.HTTPError( # pylint: disable=protected-access + HTTPStatus.UNAUTHORIZED, + BaseDirectMetadata._create_error_object() ) with mock.patch('asyncio.sleep'): with self.assertRaises(ctera_direct.exceptions.UnAuthorized) as error: @@ -57,8 +60,9 @@ async def test_get_file_metadata_error_401(self): self.assertEqual(error.exception.filename, self._file_id) async def test_get_file_metadata_error_422(self): - self._direct._client._api.get.side_effect = exceptions.ClientResponseException( # pylint: disable=protected-access - BaseDirectMetadata._create_error_object(422) + self._direct._client._api.get.side_effect = exceptions.HTTPError( # pylint: disable=protected-access + HTTPStatus.UNPROCESSABLE_ENTITY, + BaseDirectMetadata._create_error_object() ) with mock.patch('asyncio.sleep'): with self.assertRaises(ctera_direct.exceptions.UnprocessableContent) as error: @@ -68,13 +72,15 @@ async def test_get_file_metadata_error_422(self): self.assertEqual(error.exception.filename, self._file_id) async def test_get_file_metadata_unknown_error(self): - self._direct._client._api.get.side_effect = exceptions.ClientResponseException( # pylint: disable=protected-access - BaseDirectMetadata._create_error_object(500) + url = '/xyz' + self._direct._client._api.get.side_effect = exceptions.HTTPError( # pylint: disable=protected-access + HTTPStatus.INTERNAL_SERVER_ERROR, + BaseDirectMetadata._create_error_object() ) with mock.patch('asyncio.sleep'): - with self.assertRaises(exceptions.ClientResponseException) as error: + with self.assertRaises(exceptions.HTTPError) as error: await self._direct.metadata(self._file_id) - self.assertEqual(error.exception.message, 'An error occurred while processing the HTTP request.') + self.assertEqual(error.exception.error.request.url, url) async def test_get_file_metadata_connection_error(self): self._direct._client._api.get.side_effect = ConnectionError # pylint: disable=protected-access @@ -97,8 +103,9 @@ async def test_get_file_metadata_timeout(self): self.assertEqual(error.exception.filename, self._file_id) @staticmethod - def _create_error_object(status): - param = Object() - param.response = Object() - param.response.status = status - return param + def _create_error_object(): + return munch.Munch( + dict(request=munch.Munch( + dict(url='/xyz') + )) + ) diff --git a/tests/ut/aio/direct/test_get_object.py b/tests/ut/aio/direct/test_get_object.py index eedb817f..d3ccc073 100644 --- a/tests/ut/aio/direct/test_get_object.py +++ b/tests/ut/aio/direct/test_get_object.py @@ -1,9 +1,10 @@ import asyncio +from http import HTTPStatus import errno from unittest import mock import munch from cterasdk.direct.lib import get_object -from cterasdk import exceptions, ctera_direct, Object +from cterasdk import exceptions, ctera_direct from . import base @@ -54,23 +55,26 @@ async def stream_reader(): async def test_get_client_error(self): chunk = BaseDirectMetadata._create_chunk() - self._direct._client._client.get.side_effect = exceptions.ClientResponseException( # pylint: disable=protected-access - self._create_error_object(500) + self._direct._client._client.get.side_effect = exceptions.HTTPError( # pylint: disable=protected-access + HTTPStatus.INTERNAL_SERVER_ERROR, + BaseDirectMetadata._create_error_object(HTTPStatus.INTERNAL_SERVER_ERROR.value) ) with mock.patch('asyncio.sleep'): with self.assertRaises(ctera_direct.exceptions.DownloadError) as error: await get_object(self._direct._client._client, self._file_id, chunk) # pylint: disable=protected-access self.assertEqual(error.exception.errno, errno.EIO) - self.assertEqual(error.exception.strerror.status, 500) + self.assertEqual(error.exception.strerror.response.status, 500) self.assertEqual(error.exception.filename, self._file_id) self.assert_equal_objects(error.exception.block, BaseDirectMetadata._create_block_info(self._file_id, chunk)) @staticmethod def _create_error_object(status): - param = Object() - param.response = Object() - param.response.status = status - return param + return munch.Munch( + dict( + request=munch.Munch(dict(url='/xyz')), + response=munch.Munch(dict(status=status)) + ) + ) @staticmethod def _create_block_info(file_id, chunk): diff --git a/tests/ut/aio/test_login.py b/tests/ut/aio/test_login.py index 778d06fe..a29a0eaf 100644 --- a/tests/ut/aio/test_login.py +++ b/tests/ut/aio/test_login.py @@ -1,4 +1,6 @@ from unittest import mock +from http import HTTPStatus +import munch from cterasdk.common import Object from cterasdk import exceptions from tests.ut.aio import base_core @@ -26,7 +28,14 @@ async def test_login_success(self): async def test_login_failure(self): self._init_global_admin() - self._global_admin.v1.api.form_data = mock.AsyncMock(side_effect=exceptions.ClientResponseException(Object())) + self._global_admin.v1.api.form_data = mock.AsyncMock(side_effect=exceptions.HTTPError( + HTTPStatus.FORBIDDEN, + munch.Munch( + dict(request=munch.Munch( + dict(url='/xyz') + )) + ) + )) with self.assertRaises(exceptions.CTERAException): await self._global_admin.login(self._username, self._password) diff --git a/tests/ut/aio/test_notifications.py b/tests/ut/aio/test_notifications.py index 9df84ca9..5c44eb7d 100644 --- a/tests/ut/aio/test_notifications.py +++ b/tests/ut/aio/test_notifications.py @@ -1,4 +1,5 @@ from unittest import mock +from http import HTTPStatus import munch from cterasdk.asynchronous.core import notifications from cterasdk.core.types import CloudFSFolderFindingHelper, UserAccount @@ -95,11 +96,19 @@ async def test_ancestors_success(self): self.assertEqual(ret, post_response) async def test_ancestors_error(self): + url = '/xyz' self._init_global_admin() - self._global_admin.v2.api.post = mock.AsyncMock(side_effect=exceptions.ClientResponseException(munch.Munch({}))) - with self.assertRaises(exceptions.ClientResponseException) as error: + self._global_admin.v2.api.post = mock.AsyncMock(side_effect=exceptions.HTTPError( + HTTPStatus.FORBIDDEN, + munch.Munch( + dict(request=munch.Munch( + dict(url=url) + )) + ) + )) + with self.assertRaises(exceptions.HTTPError) as error: await notifications.Notifications(self._global_admin).ancestors(self._descendant) - self.assertEqual(error.exception.message, 'An error occurred while processing the HTTP request.') + self.assertEqual(error.exception.error.request.url, url) @staticmethod def _create_parameter(folder_ids=None, cursor=None, max_results=None, timeout=None): diff --git a/tests/ut/edge/base_edge.py b/tests/ut/edge/base_edge.py index ca51e43d..d5ac71ec 100644 --- a/tests/ut/edge/base_edge.py +++ b/tests/ut/edge/base_edge.py @@ -23,10 +23,12 @@ def _init_filer(self, get_response=None, put_response=None, post_response=None, self._filer.api.execute = mock.MagicMock(return_value=execute_response) self._filer.api.delete = mock.MagicMock(return_value=delete_response) - def _init_webdav(self, upload_response=None, mkcol_response=None, copy_response=None, move_response=None, delete_response=None): + def _init_webdav(self, upload_response=None, propfind_response=None, mkcol_response=None, + copy_response=None, move_response=None, delete_response=None): self._filer._ctera_clients = mock.PropertyMock() # pylint: disable=protected-access self._filer._ctera_clients.io = mock.PropertyMock() # pylint: disable=protected-access self._filer._ctera_clients.io.upload = mock.PropertyMock(return_value=upload_response) # pylint: disable=protected-access + self._filer._ctera_clients.io.propfind = mock.PropertyMock(return_value=propfind_response) # pylint: disable=protected-access self._filer._ctera_clients.io.mkdir = mock.PropertyMock(return_value=mkcol_response) # pylint: disable=protected-access self._filer._ctera_clients.io.move = mock.PropertyMock(return_value=move_response) # pylint: disable=protected-access self._filer._ctera_clients.io.copy = mock.PropertyMock(return_value=copy_response) # pylint: disable=protected-access diff --git a/tests/ut/edge/test_volumes.py b/tests/ut/edge/test_volumes.py index a0085d19..93e84a98 100644 --- a/tests/ut/edge/test_volumes.py +++ b/tests/ut/edge/test_volumes.py @@ -182,7 +182,7 @@ def test_delete_volume_raise(self): volumes.Volumes(self._filer).delete(self._volume_1_name) self._filer.tasks.by_name.assert_called_once_with(' '.join(['Mounting', self._volume_1_name, 'file system'])) self._filer.api.delete.assert_called_once_with('/config/storage/volumes/' + self._volume_1_name) - self.assertEqual('Volume deletion falied', error.exception.message) + self.assertEqual('Volume deletion failed', error.exception.message) def test_delete_all_volume_success(self): delete_response = 'Success'