Skip to content

Commit 0419933

Browse files
authored
Saimon/add support for sdk commands (#345)
1 parent d160512 commit 0419933

48 files changed

Lines changed: 848 additions & 100 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.pylintrc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ persistent=yes
3838

3939
# When enabled, pylint would attempt to guess common misconfiguration and emit
4040
# user-friendly hints instead of false-positive error messages.
41-
suggestion-mode=yes
41+
# suggestion-mode=yes
4242

4343
# Allow loading of arbitrary C extensions. Extensions are imported into the
4444
# active Python interpreter and may run arbitrary code.
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
from .browser import CloudDrive # noqa: E402, F401
1+
from .browser import CloudDrive, InvitationBrowser # noqa: E402, F401

cterasdk/asynchronous/core/files/browser.py

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
from .. import query
22
from ....cio.core.commands import Open, OpenMany, Upload, Download, EnsureDirectory, \
33
DownloadMany, UnShare, CreateDirectory, GetMetadata, GetProperties, ListVersions, RecursiveIterator, \
4-
Delete, Recover, Rename, GetShareMetadata, Link, Copy, Move, ResourceIterator, GetPermalink
5-
from ..base_command import BaseCommand
4+
Delete, Recover, Rename, GetShareMetadata, Link, Copy, Move, ResourceIterator, GetPermalink, GetExternalShareInfo
5+
from ....cio.core.types import InvitationPath
66
from ....lib.storage import commonfs
7+
from ..base_command import BaseCommand
78
from . import io
89

910

@@ -355,3 +356,44 @@ async def unshare(self, path):
355356
:param str path: Path of file/folder.
356357
"""
357358
return await UnShare(io.update_share, self._core, path).a_execute()
359+
360+
361+
class InvitationBrowser:
362+
363+
def __init__(self, core):
364+
self._invitation = InvitationPath.from_context(core.invite)
365+
self._core = core
366+
self._file_browser = CloudDrive(core)
367+
368+
def listdir(self, path=None):
369+
return self._file_browser.listdir(self._invitation.join(path))
370+
371+
def walk(self, path=None):
372+
return self._file_browser.walk(self._invitation.join(path))
373+
374+
async def properties(self, path):
375+
return await self._file_browser.properties(self._invitation.join(path))
376+
377+
async def exists(self, path):
378+
return await self._file_browser.exists(self._invitation.join(path))
379+
380+
async def mkdir(self, path):
381+
return await self._file_browser.mkdir(self._invitation.join(path))
382+
383+
async def makedirs(self, path):
384+
return await self._file_browser.makedirs(self._invitation.join(path))
385+
386+
async def download(self, path, destination=None):
387+
return await self._file_browser.download(self._invitation.join(path), destination)
388+
389+
async def download_many(self, directory, objects, destination=None):
390+
return await self._file_browser.download_many(self._invitation.join(directory), objects, destination)
391+
392+
async def upload(self, destination, handle, name=None, size=None):
393+
return await self._file_browser.upload(self._invitation.join(destination), handle, name, size)
394+
395+
async def upload_file(self, path, destination):
396+
return await self._file_browser.upload_file(path, self._invitation.join(destination))
397+
398+
async def details(self):
399+
return await GetExternalShareInfo(io.get_share_details, self._core, self._core.invite).a_execute()

cterasdk/asynchronous/core/files/io.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,3 +73,7 @@ async def add_share_recipients(core, path, members):
7373

7474
async def public_link(core, param):
7575
return await core.v1.api.execute('', 'createShare', param)
76+
77+
78+
async def get_share_details(core, param):
79+
return await core.v1.api.execute('', 'getShareDetails', param)

cterasdk/asynchronous/core/invitation/__init__.py

Whitespace-only changes.
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import logging
2+
from ..base_command import BaseCommand
3+
4+
5+
logger = logging.getLogger('cterasdk.core')
6+
7+
8+
class Login(BaseCommand):
9+
"""
10+
Portal Login APIs
11+
"""
12+
13+
async def login(self, key, value):
14+
logger.info('Creating external session. %s', {'invite': value})
15+
await self._core.clients.v1.ctera.get('', params={key: value})
16+
17+
async def logout(self):
18+
"""No logout for external users"""

cterasdk/cio/common.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ def name(self):
6060

6161
@property
6262
def parent(self):
63-
return self.__class__(self._reference.parent.as_posix()) # pylint: disable=no-value-for-parameter
63+
return self.__class__(self._scope, self._reference.parent.as_posix()) # pylint: disable=no-value-for-parameter
6464

6565
@property
6666
def absolute(self):
@@ -80,7 +80,7 @@ def is_relative_to(self, p):
8080

8181
@resolver
8282
def relative_to(self, p):
83-
return self.__class__(self.reference.relative_to(p).as_posix()) # pylint: disable=no-value-for-parameter
83+
return self.__class__(self._scope, self.reference.relative_to(p).as_posix()) # pylint: disable=no-value-for-parameter
8484

8585
@property
8686
def extension(self):
@@ -92,15 +92,17 @@ def join(self, p):
9292
9393
:param str p: Path.
9494
"""
95-
return self.__class__(self.reference.joinpath(p).as_posix()) # pylint: disable=no-value-for-parameter
95+
if p is not None:
96+
return self.__class__(self._scope, self.reference.joinpath(p).as_posix()) # pylint: disable=no-value-for-parameter
97+
return self
9698

9799
@property
98100
def parts(self):
99101
return self.reference.parts
100102

101103
def __getitem__(self, key):
102104
if isinstance(key, slice):
103-
return self.__class__(self.parts[key]) # pylint: disable=no-value-for-parameter
105+
return self.__class__(self._scope, self.parts[key]) # pylint: disable=no-value-for-parameter
104106
if isinstance(key, int):
105107
return self.parts[key]
106108
raise TypeError("Invalid argument type")

cterasdk/cio/core/commands.py

Lines changed: 45 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from ...common import Object, DateTimeUtils
99
from ...core.enum import ProtectionLevel, CollaboratorType, SearchType, PortalAccountType, FileAccessMode, \
1010
UploadError, ResourceScope, ResourceError, Context, Administrators
11-
from ...core.types import PortalAccount, UserAccount, GroupAccount, Collaborator
11+
from ...core.types import PortalAccount, UserAccount, GroupAccount, Collaborator, PortalInvitation
1212
from ... import exceptions
1313
from ...lib.storage import synfs, asynfs, commonfs
1414
from .types import SrcDstParam, CreateShareParam, ActionResourcesParam, FetchResourcesError, \
@@ -47,15 +47,16 @@ def ensure_user_access(user_session, path):
4747
The target path to validate.
4848
"""
4949
relative = path.relative
50-
if user_session.account.role.name in Administrators:
51-
if user_session.context == Context.admin:
52-
if not administrator_namespace(path):
53-
return False, exceptions.io.core.ContextError(relative)
54-
if not user_session.account.role.access_end_user_folders and is_password_protected(path):
55-
return False, exceptions.io.core.PrivilegeError(relative)
56-
elif user_session.context == Context.ServicesPortal:
57-
if not user_session.account.role.access_end_user_folders and is_password_protected(path):
58-
return False, exceptions.io.core.PrivilegeError(relative)
50+
if not user_session.context == Context.Invitations:
51+
if user_session.account.role.name in Administrators:
52+
if user_session.context == Context.admin:
53+
if not administrator_namespace(path):
54+
return False, exceptions.io.core.ContextError(relative)
55+
if not user_session.account.role.access_end_user_folders and is_password_protected(path):
56+
return False, exceptions.io.core.PrivilegeError(relative)
57+
elif user_session.context == Context.ServicesPortal:
58+
if not user_session.account.role.access_end_user_folders and is_password_protected(path):
59+
return False, exceptions.io.core.PrivilegeError(relative)
5960
return True, None
6061

6162

@@ -412,7 +413,7 @@ class OpenMany(PortalCommand):
412413

413414
def __init__(self, function, receiver, resource, directory, *objects):
414415
super().__init__(function, receiver)
415-
self.uid = str(resource.cloudFolderInfo.uid)
416+
self.uid = str(resource.cloudFolderInfo.uid) if receiver.context != Context.Invitations else f'share/{receiver.invite}'
416417
self.directory = automatic_resolution(directory, receiver.context)
417418
self.objects = objects
418419

@@ -589,7 +590,6 @@ def _generator(self):
589590
def generate(self):
590591
for path in self._generator():
591592
try:
592-
print('Enumerating: ', path or '.')
593593
for o in ResourceIterator(self._function, self._receiver, path, None, self.include_deleted, None, None).execute():
594594
yield self._process_object(o)
595595
except (exceptions.io.core.ListDirectoryError, exceptions.io.core.PrivilegeError) as e:
@@ -605,7 +605,7 @@ async def a_generate(self):
605605

606606
def _process_object(self, o):
607607
if o.is_dir:
608-
self.tree.append(o.path.relative)
608+
self.tree.append(o.path)
609609
return o
610610

611611
@staticmethod
@@ -668,7 +668,7 @@ def _parents_generator(self):
668668
if self.parents:
669669
parts = self.path.parts
670670
for i in range(1, len(parts)):
671-
yield automatic_resolution('/'.join(parts[:i]), self._receiver.context)
671+
yield self.path[:i]
672672
else:
673673
yield self.path
674674

@@ -712,7 +712,7 @@ def _handle_response(self, r):
712712
if r == ResourceError.InvalidName:
713713
cause = exceptions.io.core.FilenameError(path)
714714
if r == ResourceError.PermissionDenied:
715-
cause = exceptions.io.core.ReservedNameError(path)
715+
cause = exceptions.io.core.PrivilegeError(path)
716716

717717
raise error from cause
718718

@@ -745,6 +745,36 @@ def _handle_exception(self, e):
745745
raise exceptions.io.core.GetShareMetadataError(path) from e
746746

747747

748+
class GetExternalShareInfo(PortalCommand):
749+
750+
def __init__(self, function, receiver, invite):
751+
super().__init__(function, receiver)
752+
self.invite = invite
753+
754+
def _before_command(self):
755+
logger.info('Querying external share details: %s', self.invite)
756+
757+
def get_parameter(self):
758+
return self.invite
759+
760+
def _execute(self):
761+
with self.trace_execution():
762+
return self._function(self._receiver, self.get_parameter())
763+
764+
async def _a_execute(self):
765+
with self.trace_execution():
766+
return await self._function(self._receiver, self.get_parameter())
767+
768+
def _handle_response(self, r):
769+
return PortalInvitation.from_server_object(r)
770+
771+
def _handle_exception(self, e):
772+
error = exceptions.io.core.ExternalShareError(self._receiver.uri)
773+
if 'no longer valid' in e.error.response.error.msg:
774+
raise error from exceptions.io.core.InvalidShareError(self.invite)
775+
raise error
776+
777+
748778
class Link(PortalCommand):
749779

750780
def __init__(self, function, receiver, path, access, expire_in):

cterasdk/cio/core/types.py

Lines changed: 47 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from datetime import datetime
44
from ...common import Object
55
from ..common import BasePath, BaseResource
6+
from ...core.enum import Context
67
from ...lib.iterator import DefaultResponse
78

89

@@ -71,11 +72,12 @@ def _parse_from_str(path):
7172
7273
:param str path: Path
7374
"""
74-
groups = [f'(?P<{o.__name__}>{namespace})' for namespace, o in Namespaces.items()]
75+
groups = [f'(?P<{c.__name__}>{c.expr})' for c in [ServicesPortalPath, GlobalAdminPath, InvitationPath]]
7576
regex = re.compile(f"^{'|'.join(groups)}")
7677
match = re.match(regex, path)
7778
if match:
78-
return Namespaces[match.group()](path[match.end():])
79+
scope, reference = path[:match.end()], path[match.end():]
80+
return resolve_namespace_from_href(scope)(scope, reference)
7981
raise ValueError(f'Could not determine object path: {path}')
8082

8183

@@ -84,25 +86,55 @@ class ServicesPortalPath(PortalPath):
8486
ServicesPortal Path Object
8587
"""
8688
Namespace = '/ServicesPortal/webdav'
89+
expr = Namespace
8790

88-
def __init__(self, reference):
89-
super().__init__(ServicesPortalPath.Namespace, reference)
91+
@staticmethod
92+
def from_context(reference):
93+
return ServicesPortalPath(ServicesPortalPath.Namespace, reference)
9094

9195

9296
class GlobalAdminPath(PortalPath):
9397
"""
9498
Global Admin Path Object
9599
"""
96100
Namespace = '/admin/webdav'
101+
expr = Namespace
102+
103+
@staticmethod
104+
def from_context(reference):
105+
return ServicesPortalPath(GlobalAdminPath.Namespace, reference)
106+
107+
108+
class InvitationPath(PortalPath):
109+
"""
110+
Invitation Path Object
111+
"""
112+
Namespace = '/invitations/webdav'
113+
expr = f'{Namespace}/share/([a-zA-Z0-9]+)'
114+
115+
@staticmethod
116+
def from_context(reference):
117+
return PortalPath.from_str(f'{InvitationPath.Namespace}/share/{reference}')
118+
97119

98-
def __init__(self, reference):
99-
super().__init__(GlobalAdminPath.Namespace, reference)
120+
def resolve_namespace_from_context(ctx):
121+
if ctx == Context.admin:
122+
return GlobalAdminPath
123+
if ctx == Context.ServicesPortal:
124+
return ServicesPortalPath
125+
if ctx == Context.Invitations:
126+
return InvitationPath
127+
return None
100128

101129

102-
Namespaces = {
103-
ServicesPortalPath.Namespace: ServicesPortalPath,
104-
GlobalAdminPath.Namespace: GlobalAdminPath
105-
}
130+
def resolve_namespace_from_href(href):
131+
if href.startswith(GlobalAdminPath.Namespace):
132+
return GlobalAdminPath
133+
if href.startswith(ServicesPortalPath.Namespace):
134+
return ServicesPortalPath
135+
if href.startswith(InvitationPath.Namespace):
136+
return InvitationPath
137+
raise ValueError(f'Could not find namespace associated with href: {href}')
106138

107139

108140
def resolve(path, namespace=None):
@@ -111,6 +143,7 @@ def resolve(path, namespace=None):
111143
112144
:param object path: Path
113145
:param namespace: :class:`cterasdk.cio.core.types.ServicesPortalPath` or :class:`cterasdk.cio.core.types.GlobalAdminPath` (optional)
146+
or :class:`cterasdk.cio.core.types.InvitationPath` (optional)
114147
"""
115148
if isinstance(path, PortalPath):
116149
return path
@@ -123,7 +156,7 @@ def resolve(path, namespace=None):
123156

124157
if namespace:
125158
if path is None or isinstance(path, str):
126-
return namespace(path or '')
159+
return namespace.from_context(path or '')
127160

128161
raise ValueError(f'Error: Could not resolve path: {path}. Type: {type(path)}')
129162

@@ -144,14 +177,14 @@ def wrapper():
144177
return wrapper()
145178

146179

147-
def automatic_resolution(p, context=None):
180+
def automatic_resolution(p, ctx=None):
148181
"""
149182
Automatic Resolution of Path Object
150183
151184
:param object p: Path
152-
:param str,optional context: Context (e.g. 'ServicesPortal' or 'admin')
185+
:param str,optional ctx: Context (e.g. 'ServicesPortal', 'admin', or 'invitations')
153186
"""
154-
namespace = Namespaces.get(f'/{context}/webdav', None)
187+
namespace = resolve_namespace_from_context(ctx)
155188

156189
if isinstance(p, (list, tuple)):
157190
return create_generator(p, namespace)

0 commit comments

Comments
 (0)