class SbgError(Exception):
    """Base class for SBG errors.

    Provides a base exception for all errors
    that are thrown by sevenbridges-python library.
    """

    def __init__(self, message=None, code=None, status=None, more_info=None):
        """
        :param code: Custom error code
        :param status: Http status code.
        :param message: Message describing the error.
        """
        self.code = code
        self.status = status
        self.message = message
        self.more_info = more_info

    def __str__(self):
        return str(self.message) 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 DictField(Field, dict):
    def __init__(self, read_only, name=None):
        super().__init__(name=name, read_only=read_only) 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 BulkRecord(Resource):
    error = CompoundField(cls=Error, read_only=False)
    resource = CompoundField(cls=Resource, read_only=False)

    def __str__(self):
        return f'<BulkRecord valid={self.valid}>'

    @property
    def valid(self):
        return self.error is None

    @classmethod
    def parse_records(cls, response, api=None):
        api = api or cls._API
        records = []
        data = response.json()
        for item in data.get('items', []):
            record = cls(api=api)
            record._set('error', item.get('error'))
            record._set('resource', item.get('resource'))
            records.append(record)
        return records class Error(Resource):
    """
    Error resource describes the error that happened and provides
    http status, custom codes and messages as well as the link to
    online resources.
    """
    status = IntegerField(read_only=True)
    code = IntegerField(read_only=True)
    message = StringField(read_only=True)
    more_info = StringField(read_only=True)

    def __str__(self):
        return f'<Error: status={self.status}, code={self.code}>' class ImportDestination(Resource):
    """
    ImportDestination resource describes the location of the file
    imported on to SevenBridges platform or related product.
    """
    project = StringField(read_only=True)
    parent = StringField(read_only=True)
    name = StringField(read_only=True)

    def __str__(self):
        return f'<ImportDestination: project={self.project}, name={self.name}>' class VolumeFile(Resource):
    """
    VolumeFile resource describes the location of the file
    on the external volume.
    """
    volume = StringField(read_only=True)
    location = StringField(read_only=True)

    def __str__(self):
        return f'<VolumeFile: volume={self.volume}, location={self.location}>'

    def __eq__(self, other):
        if type(other) is not type(self):
            return False
        return self is other or self.location == other.location class File(Resource):
    """
    Central resource for managing files.
    """
    FOLDER_TYPE = 'folder'

    _URL = {
        'query': '/files',
        'scroll': '/files/scroll',
        'get': '/files/{id}',
        'delete': '/files/{id}',
        'copy': '/files/{id}/actions/copy',
        'download_info': '/files/{id}/download_info',
        'metadata': '/files/{id}/metadata',
        'tags': '/files/{id}/tags',

        'bulk_get': '/bulk/files/get',
        'bulk_delete': '/bulk/files/delete',
        'bulk_update': '/bulk/files/update',
        'bulk_edit': '/bulk/files/edit',

        'create_folder': '/files',
        'list_folder': '/files/{id}/list',
        'scroll_folder': '/files/{id}/scroll',
        'copy_to_folder': '/files/{file_id}/actions/copy',
        'move_to_folder': '/files/{file_id}/actions/move',
    }

    href = HrefField(read_only=True)
    id = StringField(read_only=True)
    type = StringField(read_only=True)
    name = StringField(read_only=False)
    size = IntegerField(read_only=True)
    parent = StringField(read_only=True)
    project = StringField(read_only=True)
    created_on = DateTimeField(read_only=True)
    modified_on = DateTimeField(read_only=True)
    origin = CompoundField(FileOrigin, read_only=True)
    storage = CompoundField(FileStorage, read_only=True)
    metadata = CompoundField(Metadata, read_only=False)
    tags = BasicListField(read_only=False)
    _secondary_files = BasicListField(read_only=False, name='_secondary_files')

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

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

    def is_folder(self):
        return self.type.lower() == self.FOLDER_TYPE

    @property
    def secondary_files(self):
        if self.field('_secondary_files'):
            return [
                File(api=self._api, **data)
                for data in self._secondary_files
            ]

    @classmethod
    def query(cls, project=None, names=None, metadata=None, origin=None,
              tags=None, offset=None, limit=None, dataset=None,
              api=None, parent=None, cont_token=None):
        """
        Query ( List ) files, requires project or dataset
        :param project: Project id
        :param names: Name list
        :param metadata: Metadata query dict
        :param origin: Origin query dict
        :param tags: List of tags to filter on
        :param offset: Pagination offset
        :param limit: Pagination limit
        :param dataset: Dataset id
        :param api: Api instance.
        :param parent: Folder id or File object with type folder
        :param cont_token: Pagination continuation token
        :return: Collection object.
        """

        if cont_token and offset:
            raise SbgError(
                'Offset and continuation token parameters'
                'are mutually exclusive.'
            )

        if cont_token and metadata:
            raise SbgError(
                'Metadata filtering cannot be combined '
                'with continuation token pagination.'
            )

        api = api or cls._API

        query_params = {}

        if project:
            project = Transform.to_project(project)
            query_params['project'] = project

        if dataset:
            dataset = Transform.to_dataset(dataset)
            query_params['dataset'] = dataset

        if parent:
            query_params['parent'] = Transform.to_file(parent)

        if not (project or dataset or parent):
            raise SbgError('Project, dataset or parent must be provided.')

        if [project, parent, dataset].count(None) < 2:
            raise SbgError(
                'Only one out of project, parent or dataset must be provided.'
            )

        if names is not None and isinstance(names, list):
            if len(names) == 0:
                names.append("")
            query_params['name'] = names

        metadata_params = {}
        if metadata and isinstance(metadata, dict):
            for k, v in metadata.items():
                metadata_params[f'metadata.{k}'] = metadata[k]

        if tags:
            query_params['tag'] = tags

        query_params.update(metadata_params)

        origin_params = {}
        if origin and isinstance(origin, dict):
            for k, v in origin.items():
                origin_params[f'origin.{k}'] = origin[k]

        query_params.update(origin_params)

        return super()._query(
            api=api, url=cls._URL['scroll' if cont_token else 'query'],
            token=cont_token, offset=offset,
            limit=limit, fields='_all', **query_params
        )

    @classmethod
    def upload(cls, path, project=None, parent=None, file_name=None,
               overwrite=False, retry=RequestParameters.DEFAULT_RETRY_COUNT,
               timeout=RequestParameters.DEFAULT_TIMEOUT, part_size=None,
               wait=True, api=None):
        """
        Uploads a file using multipart upload and returns an upload handle
        if the wait parameter is set to False. If wait is set to True it
        will block until the upload is completed.

        :param path: File path on local disc.
        :param project: Project identifier
        :param parent: Parent folder identifier
        :param file_name: Optional file name.
        :param overwrite: If true will overwrite the file on the server.
        :param retry:  Number of retries if error occurs during upload.
        :param timeout:  Timeout for http requests.
        :param part_size:  Part size in bytes.
        :param wait:  If true will wait for upload to complete.
        :param api: Api instance.
        """

        api = api or cls._API
        extra = {'resource': cls.__name__, 'query': {
            'path': path,
            'project': project,
            'file_name': file_name,
            'overwrite': overwrite,
            'retry': retry,
            'timeout': timeout,
            'part_size': part_size,
            'wait': wait,
        }}
        logger.info('Uploading file', extra=extra)

        if not project and not parent:
            raise SbgError('A project or parent identifier is required.')

        if project and parent:
            raise SbgError(
                'Project and parent identifiers are mutually exclusive.'
            )

        if project:
            project = Transform.to_project(project)

        if parent:
            parent = Transform.to_file(parent)

        upload = Upload(
            file_path=path, project=project, parent=parent,
            file_name=file_name, overwrite=overwrite, retry_count=retry,
            timeout=timeout, part_size=part_size, api=api
        )
        if wait:
            upload.start()
            upload.wait()
            return upload
        else:
            return upload

    def copy(self, project, name=None):
        """
        Copies the current file.
        :param project: Destination project.
        :param name: Destination file name.
        :return: Copied File object.
        """
        project = Transform.to_project(project)
        data = {
            'project': project
        }
        if name:
            data['name'] = name
        extra = {'resource': type(self).__name__, 'query': {
            'id': self.id,
            'data': data
        }}
        logger.info('Copying file', extra=extra)
        new_file = self._api.post(url=self._URL['copy'].format(id=self.id),
                                  data=data).json()
        return File(api=self._api, **new_file)

    def download_info(self):
        """
        Fetches download information containing file url
        that can be used to download file.
        :return: Download info object.
        """
        info = self._api.get(url=self._URL['download_info'].format(id=self.id))
        return DownloadInfo(api=self._api, **info.json())

    def download(self, path, retry=RequestParameters.DEFAULT_RETRY_COUNT,
                 timeout=RequestParameters.DEFAULT_TIMEOUT, chunk_size=None,
                 wait=True, overwrite=False):
        """
        Downloads the file and returns a download handle.
        Download will not start until .start() method is invoked.
        :param path: Full path to the new file.
        :param retry:  Number of retries if error occurs during download.
        :param timeout:  Timeout for http requests.
        :param chunk_size:  Chunk size in bytes.
        :param wait: If true will wait for download to complete.
        :param overwrite: If True will silently overwrite existing file.
        :return: Download handle.
        """

        if not overwrite and os.path.exists(path):
            raise LocalFileAlreadyExists(message=path)

        extra = {'resource': type(self).__name__, 'query': {
            'id': self.id,
            'path': path,
            'overwrite': overwrite,
            'retry': retry,
            'timeout': timeout,
            'chunk_size': chunk_size,
            'wait': wait,
        }}
        logger.info('Downloading file', extra=extra)
        info = self.download_info()
        download = Download(
            url=info.url, file_path=path, retry_count=retry, timeout=timeout,
            part_size=chunk_size, api=self._api
        )
        if wait:
            download.start()
            download.wait()
        else:
            return download

    @inplace_reload
    def save(self, inplace=True, silent=False):
        """
        Saves all modification to the file on the server. By default this
        method raises an error if you are trying to save an instance that was
        not changed. Set check_if_modified param to False to disable
        this behaviour.
        :param inplace: Apply edits to the current instance or get a new one.
        :param silent: If Raises exception if file wasn't modified.
        :raise ResourceNotModified
        :return: File instance.
        """
        reload = False
        modified_data = self._modified_data()
        if not (silent or modified_data):
            raise ResourceNotModified()

        # Update metadata
        if 'metadata' in modified_data:
            if hasattr(self, '_overwrite_metadata'):
                self.metadata = self._api.put(
                    url=self._URL['metadata'].format(id=self.id),
                    data=modified_data['metadata']
                ).json()
                delattr(self, '_overwrite_metadata')
            else:
                self.metadata = self._api.patch(
                    url=self._URL['metadata'].format(id=self.id),
                    data=modified_data['metadata']
                ).json()
            reload = True
            modified_data.pop('metadata')

        # Update tags
        if 'tags' in modified_data:
            self.tags = self._api.put(
                url=self._URL['tags'].format(id=self.id),
                data=modified_data['tags']
            ).json()
            reload = True
            modified_data.pop('tags')

        # Update everything else
        if modified_data:
            data = self._api.patch(
                url=self._URL['get'].format(id=self.id), data=modified_data
            ).json()
            self._update_read_only(data)

        return self if not reload else self.reload()

    def stream(self, part_size=32 * PartSize.KB):
        """
        Creates an iterator which can be used to stream the file content.
        :param part_size: Size of the part in bytes. Default 32KB
        :return Iterator
        """
        download_info = self.download_info()
        response = self._api.get(
            url=download_info.url, stream=True, append_base=False
        )
        for part in response.iter_content(part_size):
            yield part

    def reload(self):
        """
        Refreshes the file with the data from the server.
        """
        # Use standard resource reload
        super().reload()

        # If file.metadata = value was executed
        # file object will have attribute _overwrite_metadata=True,
        # which tells us to force overwrite of metadata on the server.
        # This is metadata specific. Once we reload the resource we delete the
        # attribute _overwrite_metadata from the instance.
        if hasattr(self, '_overwrite_metadata'):
            delattr(self, '_overwrite_metadata')

        return self

    def content(self, path=None, overwrite=True, encoding='utf-8'):
        """
        Downloads file to the specified path or as temporary file
        and reads the file content in memory.
         Should not be used on very large files.

        :param path: Path for file download If omitted tmp file will be used.
        :param overwrite: Overwrite file if exists locally
        :param encoding: File encoding, by default it is UTF-8
        :return: File content.
        """
        if path:
            self.download(wait=True, path=path, overwrite=overwrite)
            with open(path, 'r', encoding=encoding) as fp:
                return fp.read()

        with tempfile.NamedTemporaryFile() as tmpfile:
            self.download(wait=True, path=tmpfile.name, overwrite=overwrite)
            with open(tmpfile.name, 'r', encoding=encoding) as fp:
                return fp.read()

    @classmethod
    def bulk_get(cls, files, api=None):
        """
        Retrieve files with specified ids in bulk
        :param files: Files to be retrieved.
        :param api: Api instance.
        :return: List of FileBulkRecord objects.
        """
        api = api or cls._API
        file_ids = [Transform.to_file(file_) for file_ in files]
        data = {'file_ids': file_ids}

        logger.debug('Getting files in bulk.')
        response = api.post(url=cls._URL['bulk_get'], data=data)
        return FileBulkRecord.parse_records(response=response, api=api)

    @classmethod
    def bulk_delete(cls, files, api=None):
        """
        Delete files with specified ids in bulk
        :param files: Files to be deleted.
        :param api: Api instance.
        :return: List of FileBulkRecord objects.
        """
        api = api or cls._API
        file_ids = [Transform.to_file(file_) for file_ in files]
        data = {'file_ids': file_ids}

        logger.debug('Deleting files in bulk.')
        response = api.post(url=cls._URL['bulk_delete'], data=data)
        return FileBulkRecord.parse_records(response=response, api=api)

    @classmethod
    def bulk_update(cls, files, api=None):
        """
        This call updates the details for multiple specified files.
        Use this call to set new information for the files, thus replacing
        all existing information and erasing omitted parameters. For each
        of the specified files, the call sets a new name, new tags and
        metadata.
        :param files: List of file instances.
        :param api: Api instance.
        :return: List of FileBulkRecord objects.
        """
        if not files:
            raise SbgError('Files are required.')

        api = api or cls._API
        data = {
            'items': [
                {
                    'id': file_.id,
                    'name': file_.name,
                    'tags': file_.tags,
                    'metadata': file_.metadata,
                }
                for file_ in files
            ]
        }

        logger.debug('Updating files in bulk.')
        response = api.post(url=cls._URL['bulk_update'], data=data)
        return FileBulkRecord.parse_records(response=response, api=api)

    @classmethod
    def bulk_edit(cls, files, api=None):
        """
        This call edits the details for multiple specified files.
        Use this call to modify the existing information for the files
        or add new information while preserving omitted parameters.
        For each of the specified files, the call edits its name, tags
        and metadata.
        :param files: List of file instances.
        :param api: Api instance.
        :return: List of FileBulkRecord objects.
        """
        if not files:
            raise SbgError('Files are required.')

        api = api or cls._API
        data = {
            'items': [
                {
                    'id': file_.id,
                    'name': file_.name,
                    'tags': file_.tags,
                    'metadata': file_.metadata,
                }
                for file_ in files
            ]
        }

        logger.debug('Editing files in bulk.')
        response = api.post(url=cls._URL['bulk_edit'], data=data)
        return FileBulkRecord.parse_records(response=response, api=api)

    def list_files(self, offset=None, limit=None, api=None, cont_token=None):
        """List files in a folder
        :param api: Api instance
        :param offset: Pagination offset
        :param limit: Pagination limit
        :param cont_token: Pagination continuation token
        :return: List of files
        """

        if cont_token and offset:
            raise SbgError(
                'Offset and continuation token parameters'
                'are mutually exclusive.'
            )

        api = api or self._API

        if not self.is_folder():
            raise SbgError(f'{self.name} is not a folder')

        url = self._URL[
            'scroll_folder' if cont_token else 'list_folder'
        ].format(id=self.id)

        return super(File, type(self))._query(
            api=api, url=url, token=cont_token, offset=offset,
            limit=limit, fields='_all'
        )

    @classmethod
    def create_folder(cls, name, parent=None, project=None,
                      api=None):
        """Create a new folder
        :param name: Folder name
        :param parent: Parent folder
        :param project: Project to create folder in
        :param api: Api instance
        :return: New folder
        """
        api = api or cls._API

        data = {
            'name': name,
            'type': cls.FOLDER_TYPE
        }

        if not parent and not project:
            raise SbgError('Parent or project must be provided')

        if parent and project:
            raise SbgError(
                'Providing both "parent" and "project" is not allowed'
            )

        if parent:
            data['parent'] = Transform.to_file(file_=parent)

        if project:
            data['project'] = Transform.to_project(project=project)

        response = api.post(url=cls._URL['create_folder'], data=data).json()
        return cls(api=api, **response)

    def copy_to_folder(self, parent, name=None, api=None):
        """Copy file to folder
        :param parent: Folder to copy file to
        :param name: New file name
        :param api: Api instance
        :return: New file instance
        """
        api = api or self._API

        if self.is_folder():
            raise SbgError('Copying folders is not supported')

        data = {
            'parent': Transform.to_file(parent)
        }

        if name:
            data['name'] = name

        response = api.post(
            url=self._URL['copy_to_folder'].format(file_id=self.id),
            data=data
        ).json()
        return File(api=api, **response)

    def move_to_folder(self, parent, name=None, api=None):
        """Move file to folder
        :param parent: Folder to move file to
        :param name: New file name
        :param api: Api instance
        :return: New file instance
        """
        api = api or self._API

        if self.is_folder():
            raise SbgError('Moving folders is not supported')

        data = {
            'parent': Transform.to_file(parent)
        }

        if name:
            data['name'] = name

        response = api.post(
            url=self._URL['move_to_folder'].format(file_id=self.id),
            data=data
        ).json()
        return File(api=api, **response)
import logging
from sevenbridges.errors import SbgError
from sevenbridges.meta.fields import (
    HrefField, StringField, CompoundField, DateTimeField, BooleanField,
    DictField
)
from sevenbridges.meta.resource import Resource
from sevenbridges.meta.transformer import Transform
from sevenbridges.models.bulk import BulkRecord
from sevenbridges.models.compound.error import Error
from sevenbridges.models.compound.volumes.import_destination import (
    ImportDestination
)
from sevenbridges.models.compound.volumes.volume_file import VolumeFile
from sevenbridges.models.file import File
                      api=None):
        """
        Submits new import job.
        :param volume: Volume identifier.
        :param location: Volume location.
        :param project: Project identifier.
        :param name: Optional file name.
        :param overwrite: If true it will overwrite file if exists.
        :param properties: Properties dictionary.
        :param parent: The ID of the target folder to which the item should be
            imported. Should not be used together with project.
        :param preserve_folder_structure: Whether to keep the exact source
            folder structure. The default value is true if the item being
            imported is a folder. Should not be used if you are importing
            a file.
        :param autorename: Whether to automatically rename the item
            (by prefixing its name with an underscore and number) if
            another one with the same name already exists at the destination.
        :param api: Api instance.
        :return: Import object.
        """
        data = {}
        volume = Transform.to_volume(volume)

        if project and parent:
            raise SbgError(
                'Project and parent identifiers are mutually exclusive'
            )
        elif project:
            project = Transform.to_project(project)
            destination = {
                'project': project
            }
        elif parent:
            parent = Transform.to_file(parent)
            destination = {
                'parent': parent
            }
        else:
            raise SbgError('Project or parent identifier is required.')

        source = {
            'volume': volume,
            'location': location
        }

        if name:
            destination['name'] = name

        data['source'] = source
        data['destination'] = destination
        data['overwrite'] = overwrite

        if not preserve_folder_structure:
            data['preserve_folder_structure'] = preserve_folder_structure

        if autorename:
            data['autorename'] = autorename

        if properties:
            data['properties'] = properties

        api = api if api else cls._API
        extra = {
            'resource': cls.__name__,
            'query': data
        }
        logger.info('Submitting import', extra=extra)
        _import = api.post(cls._URL['query'], data=data).json()
        return Import(api=api, **_import)

    @classmethod
    def query(cls, project=None, volume=None, state=None, offset=None,
              limit=None, api=None):
        """
        Query (List) imports.
        :param project: Optional project identifier.
        :param volume: Optional volume identifier.
        :param state: Optional import sate.
        :param offset: Pagination offset.
        :param limit: Pagination limit.
        :param api: Api instance.
        :return: Collection object.
        """
        api = api or cls._API

        if project:
            project = Transform.to_project(project)
        if volume:
            volume = Transform.to_volume(volume)

        return super()._query(
            url=cls._URL['query'], project=project, volume=volume, state=state,
            fields='_all', offset=offset, limit=limit, api=api
        )

    @classmethod
    def bulk_get(cls, imports, api=None):
        """
        Retrieve imports in bulk
        :param imports: Imports to be retrieved.
        :param api: Api instance.
        :return: List of ImportBulkRecord objects.
        """
        api = api or cls._API
        import_ids = [Transform.to_import(import_) for import_ in imports]
        data = {'import_ids': import_ids}

        response = api.post(url=cls._URL['bulk_get'], data=data)
        return ImportBulkRecord.parse_records(response=response, api=api)

    @classmethod
    def bulk_submit(cls, imports, api=None):
        """
        Submit imports in bulk
        :param imports: List of dicts describing a wanted import.
        :param api: Api instance.
        :return: List of ImportBulkRecord objects.
        """
        if not imports:

            raise SbgError('Imports are required')