def inplace_reload(method):
    """
    Executes the wrapped function and reloads the object
    with data returned from the server.
    """

    # noinspection PyProtectedMember
    def wrapped(obj, *args, **kwargs):
        in_place = True if kwargs.get('inplace') in (True, None) else False
        api_object = method(obj, *args, **kwargs)
        if in_place and api_object:
            obj._data = api_object._data
            obj._dirty = api_object._dirty
            obj._old = copy.deepcopy(api_object._data.data)
            obj._data.fetched = False
            return obj
        elif api_object:
            return api_object
        else:
            return obj

    return wrapped class ResourceNotModified(SbgError):
    def __init__(self):
        super().__init__(
            code=-1,
            status=-1,
            message=(
                'No relevant changes were detected in order to update the '
                'resource on the server.'
            )
        ) class Collection(list):
    """
    Wrapper for SevenBridges pageable resources.
    Among the actual collection items it contains information regarding
    the total number of entries available in on the server and resource href.
    """

    resource = None

    def __init__(self, resource, href, total, items, links, api):
        super().__init__(items)
        self.resource = resource
        self.href = href
        self.links = links
        self._items = items
        self._total = total
        self._api = api

    @property
    def total(self):
        return int(self._total)

    def all(self):
        """
        Fetches all available items.
        :return: Collection object.
        """
        page = self._load(self.href)
        while True:
            try:
                for item in page._items:
                    yield item
                page = page.next_page()
            except PaginationError:
                break

    def _load(self, url):
        if self.resource is None:
            raise SbgError('Undefined collection resource.')
        else:
            response = self._api.get(url, append_base=False)
            data = response.json()
            total = response.headers['x-total-matching-query']
            items = [
                self.resource(api=self._api, **group)
                for group in data['items']
            ]
            links = [Link(**link) for link in data['links']]
            href = data['href']
            return Collection(
                resource=self.resource, href=href, total=total,
                items=items, links=links, api=self._api
            )

    def next_page(self):
        """
        Fetches next result set.
        :return: Collection object.
        """
        for link in self.links:
            if link.rel.lower() == 'next':
                return self._load(link.href)
        raise PaginationError('No more entries.')

    def previous_page(self):
        """
        Fetches previous result set.
        :return: Collection object.
        """
        for link in self.links:
            if link.rel.lower() == 'prev':
                return self._load(link.href)
        raise PaginationError('No more entries.')

    def __repr__(self):
        return (
            f'<Collection: total={self.total}, available={len(self._items)}>'
        ) class VolumeCollection(Collection):
    def __init__(self, href, items, links, prefixes, api):
        super().__init__(
            VolumeObject, href, 0, items, links, api)
        self.prefixes = prefixes

    @property
    def total(self):
        return -1

    def next_page(self):
        """
        Fetches next result set.
        :return: VolumeCollection object.
        """
        for link in self.links:
            if link.next:
                return self._load(link.next)
        raise PaginationError('No more entries.')

    def previous_page(self):
        raise PaginationError('Cannot paginate backwards')

    def _load(self, url):
        if self.resource is None:
            raise SbgError('Undefined collection resource.')
        else:
            response = self._api.get(url, append_base=False)
            data = response.json()
            items = [
                self.resource(api=self._api, **group) for group in
                data['items']
            ]
            prefixes = [
                VolumePrefix(api=self._api, **prefix) for prefix in
                data['prefixes']
            ]
            links = [VolumeLink(**link) for link in data['links']]
            href = data['href']
            return VolumeCollection(
                href=href, items=items, links=links,
                prefixes=prefixes, api=self._api
            )

    def __repr__(self):
        return f'<VolumeCollection: items={len(self._items)}>' class HrefField(Field):
    def __init__(self, read_only, name=None):
        super().__init__(name=name, read_only=read_only) class StringField(Field):
    def __init__(self, read_only, name=None, max_length=None):
        super().__init__(name=name, read_only=read_only)
        self.max_length = max_length

    def validate(self, value):
        value = super().validate(value)
        if value and not isinstance(value, str):
            raise ValidationError(
                f'{value} is not a valid value for {type(self).__name__}'
            )
        if self.max_length is not None and len(value) > self.max_length:
            raise ValidationError(f'{self.name}: max length exceeded.')
        return value class CompoundField(Field):
    def __init__(self, cls, read_only, name=None, validator=None):
        super().__init__(
            name=name, read_only=read_only, validator=validator
        )
        self.cls = cls

    def __get__(self, instance, owner):
        data = instance._data[self.name]
        # empty is used for read only fields, None all for others
        if data is not Field.EMPTY and data is not None:
            return self.cls(api=instance._api, _parent=instance, **data)
        else:
            return None class DateTimeField(Field):
    def __init__(self, read_only, name=None):
        super().__init__(name=name, read_only=read_only)

    def __get__(self, instance, cls):
        data = super().__get__(instance, cls)
        if data:
            datetime_format = "%Y-%m-%dT%H:%M:%S"
            if '.' in data:
                datetime_format += ".%f"
            if 'Z' in data:
                datetime_format += "Z"
            return datetime.strptime(data, datetime_format) class BooleanField(Field):
    def __init__(self, read_only, name=None):
        super().__init__(name=name, read_only=read_only)

    def validate(self, value):
        if value and not isinstance(value, bool):
            raise ValidationError(
                '{value} is not a valid value for {type(self).__name__}'
            )
        return value class Resource(metaclass=ResourceMeta):
    """
    Resource is base class for all resources, hiding implementation details
    of magic of injecting instance of API and common operations (like generic
    query).
    """
    _API = None
    _URL = {}

    def __init__(self, api, *args, **kwargs):
        self.api = api

    @classmethod
    def _query(cls, **kwargs):
        """
        Generic query implementation that is used
        by the resources.
        """
        from sevenbridges.models.link import Link
        from sevenbridges.meta.collection import Collection

        api = kwargs.pop('api', cls._API)
        url = kwargs.pop('url')

        # Check for valid limit value
        if kwargs.get('limit') is not None and kwargs['limit'] <= 0:
            kwargs['limit'] = RequestParameters.DEFAULT_BULK_LIMIT

        extra = {'resource': cls.__name__, 'query': kwargs}
        logger.info('Querying %s resource', cls, extra=extra)
        response = api.get(url=url, params=kwargs)
        try:
            data = response.json()
        except JSONDecodeError:
            raise NonJSONResponseError(
                status=response.status_code,
                message=str(response.text)
            ) from None

        total = response.headers['x-total-matching-query']

        items = [cls(api=api, **item) for item in data['items']]
        links = [Link(**link) for link in data['links']]
        href = data['href']
        return Collection(
            resource=cls, href=href, total=total, items=items,
            links=links, api=api
        )

    @classmethod
    def get(cls, id, api=None):
        """
        Fetches the resource from the server.
        :param id: Resource identifier
        :param api: sevenbridges Api instance.
        :return: Resource object.
        """
        id = Transform.to_resource(id)
        api = api if api else cls._API
        if 'get' in cls._URL:
            extra = {'resource': cls.__name__, 'query': {'id': id}}
            logger.info('Fetching %s resource', cls, extra=extra)
            resource = api.get(url=cls._URL['get'].format(id=id)).json()
            return cls(api=api, **resource)
        else:
            raise SbgError('Unable to retrieve resource!')

    def delete(self):
        """
        Deletes the resource on the server.
        """
        if 'delete' in self._URL and hasattr(self, 'id'):
            extra = {'resource': type(self).__name__, 'query': {
                'id': self.id}}
            logger.info("Deleting %s resource.", self, extra=extra)
            self._api.delete(url=self._URL['delete'].format(id=self.id))
        else:
            raise SbgError('Resource can not be deleted!')

    def reload(self):
        """
        Refreshes the resource with the data from the server.
        """
        try:
            if hasattr(self, 'href'):
                data = self._api.get(self.href, append_base=False).json()
                resource = type(self)(api=self._api, **data)
            elif hasattr(self, 'id') and hasattr(self, '_URL') and \
                    'get' in self._URL:
                data = self._api.get(
                    self._URL['get'].format(id=self.id)).json()
                resource = type(self)(api=self._api, **data)
            else:
                raise SbgError(
                    'Resource can not be refreshed, "id" property not set or '
                    'retrieval for this resource is not available.'
                )
            query = {'id': self.id} if hasattr(self, 'id') else {}
            extra = {'resource': type(self).__name__, 'query': query}
            logger.info('Reloading %s resource.', self, extra=extra)
        except Exception as e:
            raise SbgError(
                f'Resource can not be refreshed due to an error: {e}'
            )

        self._data = resource._data
        self._dirty = resource._dirty
        self.update_old()
        return self

    def update_old(self):
        self._old = copy.deepcopy(self._data.data)

    def field(self, name):
        """
        Return field value if it's set
        :param name: Field name
        :return: Field value or None
        """
        return self._data.data.get(name, None)

    def _set(self, key, value):
        """
        Set property value in internal storage
        :param key: Property name
        :param value: Property value
        """
        self._data.data[key] = value class Transform:
    @staticmethod
    def to_resource(resource):
        from sevenbridges.meta.resource import Resource
        if not resource:
            raise SbgError('Invalid id value!')
        elif isinstance(resource, Resource) and hasattr(resource, 'id'):
            return resource.id
        elif isinstance(resource, str):
            return resource
        else:
            raise SbgError('Invalid id value!')

    @staticmethod
    def to_project(project):
        """Serializes project to id string
        :param project: object to serialize
        :return: string id
        """
        from sevenbridges.models.project import Project
        if not project:
            raise SbgError('Project is required!')
        elif isinstance(project, Project):
            return project.id
        elif isinstance(project, str):
            return project
        else:
            raise SbgError('Invalid project parameter!')

    @staticmethod
    def to_task(task):
        """Serializes task to id string
        :param task: object to serialize
        :return: string id
        """
        from sevenbridges.models.task import Task
        if not task:
            raise SbgError('Task is required!')
        elif isinstance(task, Task):
            return task.id
        elif isinstance(task, str):
            return task
        else:
            raise SbgError('Invalid task parameter!')

    @staticmethod
    def to_app(app):
        """Serializes app to id string
        :param app: object to serialize
        :return: string id
        """
        from sevenbridges.models.app import App
        if not app:
            raise SbgError('App is required!')
        elif isinstance(app, App):
            return app.id
        elif isinstance(app, str):
            return app
        else:
            raise SbgError('Invalid app parameter!')

    @staticmethod
    def to_file(file_):
        """Serializes file to id string
        :param file_: object to serialize
        :return: string id
        """
        from sevenbridges.models.file import File
        if not file_:
            raise SbgError('File is required!')
        elif isinstance(file_, File):
            return file_.id
        elif isinstance(file_, str):
            return file_
        else:
            raise SbgError('Invalid file parameter!')

    @staticmethod
    def to_user(user):
        """Serializes user to id string
        :param user: object to serialize
        :return: string id
        """
        from sevenbridges.models.user import User
        if not user:
            raise SbgError('User is required!')
        elif isinstance(user, User):
            return user.username
        elif isinstance(user, str):
            return user
        else:
            raise SbgError('Invalid user parameter!')

    @staticmethod
    def to_billing_group(billing_group):
        """Serializes billing_group to id string
        :param billing_group: object to serialize
        :return: string id
        """
        from sevenbridges.models.billing_group import BillingGroup
        if not billing_group:
            raise SbgError('Billing group is required!')
        elif isinstance(billing_group, BillingGroup):
            return billing_group.id
        elif isinstance(billing_group, str):
            return billing_group
        else:
            raise SbgError('Invalid billing group parameter!')

    @staticmethod
    def to_volume(volume):
        """Serializes volume to id string
        :param volume: object to serialize
        :return: string id
        """
        from sevenbridges.models.volume import Volume
        if not volume:
            raise SbgError('Volume is required!')
        elif isinstance(volume, Volume):
            return volume.id
        elif isinstance(volume, str):
            return volume
        else:
            raise SbgError('Invalid volume parameter!')

    @staticmethod
    def to_marker(marker):
        """Serializes marker to string
        :param marker: object to serialize
        :return: string id
        """
        from sevenbridges.models.marker import Marker
        if not marker:
            raise SbgError('Marker is required!')
        elif isinstance(marker, Marker):
            return marker.id
        elif isinstance(marker, str):
            return marker
        else:
            raise SbgError('Invalid marker parameter!')

    @staticmethod
    def to_datestring(d):
        """Serializes date to string
        :param d: object to serialize
        :return: string date
        """
        if not d:
            raise SbgError('Date is required!')
        elif isinstance(d, str):
            return d
        elif isinstance(d, datetime):
            return d.isoformat().split('.', 1)[0]

    @staticmethod
    def to_division(division):
        """Serializes division to id string
        :param division: object to serialize
        :return: string id
        """
        from sevenbridges.models.division import Division
        if not division:
            raise SbgError('Division is required!')
        elif isinstance(division, Division):
            return division.id
        elif isinstance(division, str):
            return division
        else:
            raise SbgError('Invalid division parameter!')

    @staticmethod
    def to_team(team):
        """Serializes team to id string
        :param team: object to serialize
        :return: string id
        """
        from sevenbridges.models.team import Team
        if not team:
            raise SbgError('Team is required!')
        elif isinstance(team, Team):
            return team.id
        elif isinstance(team, str):
            return team
        else:
            raise SbgError('Invalid team parameter!')

    @staticmethod
    def to_import(import_):
        """Serializes import to id string
        :param import_: object to serialize
        :return: string id
        """
        from sevenbridges.models.storage_import import Import
        if not import_:
            raise SbgError('Import is required!')
        elif isinstance(import_, Import):
            return import_.id
        elif isinstance(import_, str):
            return import_
        else:
            raise SbgError('Invalid import parameter!')

    @staticmethod
    def to_export(export):
        """Serializes export to id string
        :param export: object to serialize
        :return: string id
        """
        from sevenbridges.models.storage_export import Export
        if not export:
            raise SbgError('Export is required!')
        elif isinstance(export, Export):
            return export.id
        elif isinstance(export, str):
            return export
        else:
            raise SbgError('Invalid export parameter!')

    @staticmethod
    def to_location(location):
        """Serializes location to string
        :param location: object to serialize
        :return: string
        """
        if not location:
            raise SbgError('Location is required!')
        if isinstance(location, str):
            return location
        else:
            raise SbgError('Invalid location parameter!')

    @staticmethod
    def to_dataset(dataset):
        from sevenbridges.models.dataset import Dataset
        if not dataset:
            raise SbgError('Dataset is required!')
        if isinstance(dataset, Dataset):
            return dataset.id
        if isinstance(dataset, str):
            return dataset
        else:
            raise SbgError('Invalid dataset parameter!')

    @staticmethod
    def to_member(member):
        from sevenbridges.models.member import Member
        if not member:
            raise SbgError('Member is required!')
        if isinstance(member, Member):
            return member.username
        if isinstance(member, str):
            return member
        else:
            raise SbgError('Invalid member parameter!')

    @staticmethod
    def to_automation(automation):
        from sevenbridges.models.automation import Automation
        if not automation:
            raise SbgError('Automation is required!')
        if isinstance(automation, Automation):
            return automation.id
        if isinstance(automation, str):
            return automation
        else:
            raise SbgError('Invalid automation parameter!')

    @staticmethod
    def to_automation_member(member):
        from sevenbridges.models.automation import AutomationMember
        if not member:
            raise SbgError('Automation member is required!')
        if isinstance(member, AutomationMember):
            return member.username
        if isinstance(member, str):
            return member
        else:
            raise SbgError('Invalid automation member parameter!')

    @staticmethod
    def to_automation_package(package):
        from sevenbridges.models.automation import AutomationPackage
        if not package:
            raise SbgError('Automation package is required!')
        if isinstance(package, AutomationPackage):
            return package.id
        if isinstance(package, str):
            return package
        else:
            raise SbgError('Invalid automation package parameter!')

    @staticmethod
    def to_async_job(async_job):
        from sevenbridges.models.async_jobs import AsyncJob
        if not async_job:
            raise SbgError('Async job is required!')
        if isinstance(async_job, AsyncJob):
            return async_job.id
        if isinstance(async_job, str):
            return async_job
        else:
            raise SbgError('Invalid async job parameter!')

    @staticmethod
    def to_tags(tags):
        if not isinstance(tags, list):
            raise SbgError('Tags argument must be a list.')
        tag_list = []
        for tag in tags:
            if "," in tag:
                raise SbgError('Tags must not contain comma character.')
            tag_list.append(tag)
        return tag_list

    @staticmethod
    def to_automation_run(automation_run):
        from sevenbridges.models.automation import AutomationRun
        if not automation_run:
            raise SbgError('Automation run is required!')
        if isinstance(automation_run, AutomationRun):
            return automation_run.id
        if isinstance(automation_run, str):
            return automation_run
        else:
            raise SbgError('Invalid automation run parameter!') class VolumeService(CompoundMutableDict, Resource):
    """
    Volume Service resource. Contains information about external
    storage provider.
    """
    _name = 'service'

    def __init__(self, **kwargs):
        super().__init__(**kwargs)

    def __getitem__(self, item):
        try:
            # noinspection PyProtectedMember
            return self._parent._data[self._name][item]
        except KeyError:
            return None class VolumeObject(Resource):
    """
    Volume object resource contains information about single
    file (object) entry in a specific volume.
    """
    href = StringField(read_only=True)
    location = StringField(read_only=True)
    volume = StringField(read_only=True)
    type = StringField(read_only=True)
    metadata = DictField(read_only=True)

    def __str__(self):
        return f'<VolumeObject: location={self.location}>' class VolumePrefix(Resource):
    """
    Volume prefix resource contains information about volume prefixes
    """
    href = StringField(read_only=True)
    prefix = StringField(read_only=True)
    volume = StringField(read_only=True)

    def __str__(self):
        return f'<VolumePrefix: prefix={self.prefix}>' class VolumeType:
    S3 = 'S3'
    GOOGLE = 'GCS'
    OSS = 'OSS' class Link(Resource):
    """
    Pagination links.
    """
    href = HrefField(read_only=True)
    rel = StringField(read_only=True)
    method = StringField(read_only=True)

    def __str__(self):
        return (
            f'<Link: method={self.method}, rel={self.rel}, href={self.href}>'
        ) class VolumeLink(Resource):
    """
    Pagination links for volumes.
    """
    next = HrefField(read_only=True)

    def __str__(self):
        return f'<VolumeLink: next={self.next}>' class Member(Resource):
    """
    Central resource for managing members.
    This resource is reused on both projects and volumes.
    """
    _URL = {
        'permissions': '/permissions'
    }
    href = HrefField(read_only=True)
    id = StringField(read_only=True)
    username = StringField(read_only=False)
    email = StringField(read_only=False)
    type = StringField(read_only=False)
    permissions = CompoundField(Permissions, read_only=False)

    def __str__(self):
        return f'<Member: username={self.username}>'

    def __eq__(self, other):
        if type(other) is not type(self):
            return False
        return (
            self is other or
            self.id == other.id or
            self.username == other.username
        )

    @inplace_reload
    def save(self, inplace=True):
        """
        Saves modification to the api server.
        """
        data = self._modified_data()
        data = data['permissions']
        if data:
            url = self.href + self._URL['permissions']
            extra = {'resource': type(self).__name__, 'query': data}
            logger.info('Modifying permissions', extra=extra)
            self._api.patch(url=url, data=data, append_base=False)
        else:
            raise ResourceNotModified()
import logging
from sevenbridges.decorators import inplace_reload
from sevenbridges.errors import ResourceNotModified
from sevenbridges.meta.collection import Collection, VolumeCollection
from sevenbridges.meta.fields import (
    HrefField, StringField, CompoundField, DateTimeField, BooleanField
)
from sevenbridges.meta.resource import Resource
from sevenbridges.meta.transformer import Transform
from sevenbridges.models.compound.volumes.service import VolumeService
from sevenbridges.models.compound.volumes.volume_object import VolumeObject
from sevenbridges.models.compound.volumes.volume_prefix import VolumePrefix
from sevenbridges.models.enums import VolumeType
from sevenbridges.models.link import Link, VolumeLink
from sevenbridges.models.member import Member


logger = logging.getLogger(__name__)


# noinspection PyProtectedMember
class Volume(Resource):
    """
    Central resource for managing volumes.
    """
    _URL = {
        'query': '/storage/volumes',
        'get': '/storage/volumes/{id}',
        'delete': '/storage/volumes/{id}',
        'list': '/storage/volumes/{id}/list',
        'object': '/storage/volumes/{id}/object',
        'member': '/storage/volumes/{id}/members/{username}',
        'members_query': '/storage/volumes/{id}/members',
    }

    href = HrefField(read_only=True)
    id = StringField(read_only=True)
    name = StringField(read_only=False)
    description = StringField(read_only=False)
    access_mode = StringField(read_only=False)
    service = CompoundField(VolumeService, read_only=False)
    created_on = DateTimeField(read_only=True)
    modified_on = DateTimeField(read_only=True)
    active = BooleanField(read_only=False)

    def __eq__(self, other):
        if type(other) is not type(self):
            return False
        return self is other or self.id == other.id

    def __str__(self):
        return f'<Volume: id={self.id}>'

    @classmethod
    def query(cls, offset=None, limit=None, api=None):

        """
        Query (List) volumes.
        :param offset: Pagination offset.
        :param limit: Pagination limit.
        :param api: Api instance.
        :return: Collection object.
        """
        api = api or cls._API
        return super()._query(
            url=cls._URL['query'], offset=offset, limit=limit,
            fields='_all', api=api
        )

    @classmethod
    def create_s3_volume(cls, name, bucket, access_key_id, secret_access_key,
                         access_mode, description=None, prefix=None,
                         properties=None, api=None):

        """
        Create s3 volume.
        :param name: Volume name.
        :param bucket: Referenced bucket.
        :param access_key_id: Amazon access key identifier.
        :param secret_access_key: Amazon secret access key.
        :param access_mode: Access Mode.
        :param description: Volume description.
        :param prefix: Volume prefix.
        :param properties: Volume properties.
        :param api: Api instance.
        :return: Volume object.
        """
        service = {'type': VolumeType.S3,
                   'bucket': bucket,
                   'credentials': {'access_key_id': access_key_id,
                                   'secret_access_key': secret_access_key
                                   }
                   }
        if prefix:
            service['prefix'] = prefix
        if properties:
            service['properties'] = properties

        data = {'name': name,
                'service': service,
                'access_mode': access_mode
                }
        if description:
            data['description'] = description
        api = api or cls._API
        extra = {
            'resource': cls.__name__,
            'query': data
        }
        logger.info('Creating s3 volume', extra=extra)
        response = api.post(url=cls._URL['query'], data=data).json()
        return Volume(api=api, **response)

    @classmethod
    def create_s3_volume_role_auth(cls, name, bucket, role_arn, external_id,
                                   access_mode, description=None, prefix=None,
                                   properties=None, api=None):

        """
        Create s3 volume using IAM Role auth.
        :param name: Volume name.
        :param bucket: Referenced bucket.
        :param role_arn: Amazon role ARN.
        :param external_id: Amazon role external id.
        :param access_mode: Access Mode.
        :param description: Volume description.
        :param prefix: Volume prefix.
        :param properties: Volume properties.
        :param api: Api instance.
        :return: Volume object.
        """

        service = {'type': VolumeType.S3,