syndicate/core/conf/bucket_view.py (97 lines of code) (raw):
import re
from abc import ABC, abstractmethod
from typing import Union
from collections.abc import Iterable
NAMED_S3_URI_PATTERN = r'^(?P<proto>s3:\/\/)?(?:(?P<name>[0-9a-z\-]+)' \
r'(?:\/)?)(?P<key>(?<=\/)(?:[0-9a-z\-_]+(?:\/)?)+)?$'
S3_PATTERN_GROUP_NAMES = ('proto', 'name', 'key')
class AbstractViewDigest(ABC):
"""
Abstract parsing view class.
"""
@abstractmethod
def parse(self, value: str):
if not isinstance(value, str):
raise TypeError('Value to be parsed must be of string type.')
class RegexViewDigest(AbstractViewDigest):
def __init__(self):
self._expression = None
self._groups: Iterable = tuple()
def parse(self, value: str) -> dict:
"""
Regex digestion of the incoming string value.
:return: dict | keys = self.groups
"""
super(self.__class__, self).parse(value)
if not self.expression:
raise RuntimeError('No expression has been assigned.')
match = self.expression.match(value)
return match.groupdict() if match else dict()
@property
def expression(self):
return self._expression
@expression.setter
def expression(self, pattern: str):
"""
Prepares a regex expression, based on the provided pattern.
The pattern is assigned, only if the expression contains
requires groups, if there are any.
"""
try:
pending = re.compile(pattern)
except re.error as exception:
raise exception
if any(each for each in self.groups if each not in pending.groupindex):
raise KeyError(f'Pattern {pattern}, must include'
f' named groups {", ".join(self.groups)}')
self._expression = pending
@property
def groups(self) -> Iterable:
return self._groups
@groups.setter
def groups(self, other: Iterable):
"""
Sets up required expression groups
as an iterable collection of strings.
"""
if not isinstance(other, Iterable) and any(
each for each in other if not isinstance(each,str)
):
raise TypeError('Required groups must be an iterable'
' and contain only string elements.')
self._groups = other
class AbstractBucketView(ABC):
"""
Abstract bucket view.
"""
class BucketViewRuntimeError(RuntimeError):
"""
The runtime parent bucket view error class.
"""
def __init__(self):
self._raw = None
self._digest = None
@property
@abstractmethod
def name(self):
raise NotImplementedError
@property
@abstractmethod
def raw(self):
return self._raw
@raw.setter
@abstractmethod
def raw(self, value: str) -> None:
"""
Abstract raw value setter, which provides
argument type verification.
:raises: TypeError
"""
if not self.digest:
raise self.BucketViewRuntimeError('View digest hasn\'t been set.')
if not isinstance(value, str):
raise TypeError('Raw value must be of string type.')
@property
def digest(self):
return self._digest
@digest.setter
def digest(self, other: AbstractViewDigest):
if isinstance(other, AbstractViewDigest):
self._digest = other
else:
raise TypeError('View digest-parser must '
'be of AbstractViewDigest type.')
class URIBucketView(AbstractBucketView):
class InvalidS3URIException(AbstractBucketView.BucketViewRuntimeError):
"""
An error class, meant to thrown once an invalid S3 URI is inputted.
"""
def __init__(self):
"""
Initializes a bucket view, with an empty parsed-value maintainer.
"""
super(self.__class__, self).__init__()
self._parsed = None
@property
def raw(self) -> Union[str, None]:
return self._raw
@raw.setter
def raw(self, value: str):
"""
Installs a raw url value, which alters the
parsed-value maintainer in runtime.
"""
super(self.__class__, self.__class__).raw.fset(self, value)
self._raw = value
self._parsed = self.digest.parse(value)
@property
def name(self) -> Union[str, None]:
"""
Returns the name of a bucket, retrieving netloc of the url.
"""
value = self._parsed.get('name', '') if self._parsed else None
return '' if value is None else value
@property
def key(self) -> str:
"""
Returns the key-object path compound.
"""
value = self._parsed.get('key', '') if self._parsed else None
return '' if value is None else value