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 HttpClient:
    """
    Implementation of all low-level API stuff, creating and sending requests,
    returning raw responses, authorization, etc.
    """

    def __init__(
            self, url=None, token=None, oauth_token=None, config=None,
            timeout=None, proxies=None, error_handlers=None,
            advance_access=False, pool_connections=None,
            pool_maxsize=None, pool_block=True, max_parallel_requests=None,
            retry_count=None, backoff_factor=None
    ):

        if (url, token, config) == (None, None, None):
            url, token, proxies, advance_access = config_vars(
                [None, 'default'], advance_access
            )

        elif config is not None:
            url = config.api_endpoint
            token = config.auth_token
            proxies = config.proxies
            advance_access = advance_access or config.advance_access

        else:
            proxies = format_proxies(proxies)

        if not url:
            raise SbgError(
                'URL is missing! Configuration may contain errors, '
                'or the url parameter is missing.'
            )

        self.url = url.rstrip('/')
        self.pool_connections = pool_connections
        self.pool_maxsize = pool_maxsize
        self.pool_block = pool_block
        self._session = generate_session(
            pool_connections=pool_connections,
            pool_maxsize=pool_maxsize,
            pool_block=pool_block,
            proxies=proxies,
            retry_count=retry_count,
            backoff_factor=backoff_factor,
        )
        self.timeout = timeout
        self._throttle_limit = (
            threading.Semaphore(max_parallel_requests)
            if max_parallel_requests
            else None
        )
        self._limit = None
        self._remaining = None
        self._reset = None
        self._request_id = None
        self.headers = {
            'Content-Type': 'application/json',
            'User-Agent': (
                'sevenbridges-python/{version} ({os}, Python/{python}; '
                'requests/{requests}; urllib3/{urllib3})'.format(**client_info)
            )
        }
        self.timeout = timeout
        self.token = token
        self.oauth_token = oauth_token
        if self.token:
            self.headers['X-SBG-Auth-Token'] = token
        elif self.oauth_token:
            self.headers['Authorization'] = f'Bearer {oauth_token}'
        else:
            raise SbgError(
                'Required authorization model not selected!. '
                'Please provide at least one token value.'
            )

        self.aa = advance_access
        if self.aa:
            logger.warning(
                'Advance access features enabled. '
                'AA API calls can be subject to changes.'
            )
        self.error_handlers = [maintenance_sleeper]
        if error_handlers and isinstance(error_handlers, list):
            for handler in error_handlers:
                if handler not in self.error_handlers:
                    self.error_handlers.append(handler)

    @property
    def session(self):
        return self._session

    @property
    def limit(self):
        self._rate_limit()
        return int(self._limit) if self._limit else self._limit

    @property
    def remaining(self):
        self._rate_limit()
        return int(self._remaining) if self._remaining else self._remaining

    @property
    def reset_time(self):
        self._rate_limit()
        return datetime.fromtimestamp(
            float(self._reset)
        ) if self._reset else self._reset

    @property
    def request_id(self):
        return self._request_id

    def add_error_handler(self, handler):
        if callable(handler) and handler not in self.error_handlers:
            self.error_handlers.append(handler)

    def remove_error_handler(self, handler):
        if callable(handler) and handler in self.error_handlers:
            self.error_handlers.remove(handler)

    def _rate_limit(self):
        self._request('GET', url='/rate_limit', append_base=True)

    @throttle
    @check_for_error
    def _request(self, verb, url, headers=None, params=None, data=None,
                 append_base=False, stream=False):
        if not url:
            raise SbgError(message='Request url must be provided')
        if append_base:
            url = self.url + url
        if not headers:
            headers = self.headers
        else:
            headers.update(self.headers)
        if not (self.token or self.oauth_token):
            raise SbgError(message='Api instance must be authenticated.')

        if hasattr(self, '_session_id'):
            if 'X-SBG-Auth-Token' in self.headers:
                del self.headers['X-SBG-Auth-Token']
            elif 'Authorization' in self.headers:
                del self.headers['Authorization']
            self.headers['X-SBG-Session-Id'] = getattr(self, '_session_id')

        # If advance access is enabled
        if self.aa:
            self.headers[AAHeader.key] = AAHeader.value

        request_data = {
            'verb': verb,
            'url': url,
            'headers': headers,
            'params': params
        }
        masked_request_data = mask_secrets(request_data)
        if not stream:
            masked_request_data.update({'data': data})
            logger.debug(
                "Request %s", masked_request_data,
                extra=masked_request_data
            )
            response = self._session.request(
                verb, url, params=params, data=json.dumps(data),
                headers=headers, timeout=self.timeout, stream=stream
            )
        else:
            logger.debug(
                'Stream Request %s', masked_request_data,
                extra=masked_request_data
            )
            response = self._session.request(
                verb, url, params=params, stream=stream, allow_redirects=True,
            )
        if self.error_handlers:
            while True:
                for error_handler in self.error_handlers:
                    handled_response = error_handler(self, response)
                    # if error handler 'is_repeatable', and error handling
                    # occurred, iterate again
                    if hasattr(error_handler, 'is_repeatable'):
                        if response != handled_response:
                            response = handled_response
                            break
                    else:
                        response = handled_response
                else:
                    break

        headers = response.headers
        self._limit = headers.get('X-RateLimit-Limit', self._limit)
        self._remaining = headers.get('X-RateLimit-Remaining', self._remaining)
        self._reset = headers.get('X-RateLimit-Reset', self._reset)
        self._last_response_time = response.elapsed.total_seconds()

        self._request_id = headers.get('X-Request-Id', self._request_id)
        return response

    def get(self, url, headers=None, params=None, data=None, append_base=True,
            stream=False):

        return self._request(
            'GET', url=url, headers=headers, params=params, data=data,
            append_base=append_base, stream=stream
        )

    def post(self, url, headers=None, params=None, data=None,
             append_base=True):
        return self._request('POST', url=url, headers=headers, params=params,
                             data=data, append_base=append_base)

    def put(self, url, headers=None, params=None, data=None, append_base=True):
        return self._request('PUT', url=url, headers=headers, params=params,
                             data=data, append_base=append_base)

    def patch(self, url, headers=None, params=None, data=None,
              append_base=True):
        return self._request('PATCH', url=url, headers=headers, params=params,
                             data=data, append_base=append_base)

    def delete(self, url, headers=None, params=None, append_base=True):
        return self._request('DELETE', url=url, headers=headers, params=params,
                             data={}, append_base=append_base)

    def __repr__(self):
        return '<API(%s) - "%s">' % (self.url, self.token)

    __str__ = __repr__ class App(Resource):
    """
    Central resource for managing apps.
    """
    _URL = {
        'query': '/apps',
        'get': '/apps/{id}',
        'get_revision': '/apps/{id}/{revision}',
        'create_revision': '/apps/{id}/{revision}/raw',
        'copy': '/apps/{id}/actions/copy',
        'sync': '/apps/{id}/actions/sync',
        'raw': '/apps/{id}/raw'
    }

    _CONTENT_TYPE = {
        AppRawFormat.JSON: 'application/json',
        AppRawFormat.YAML: 'application/yaml'
    }

    href = HrefField(read_only=True)
    _id = StringField(read_only=True, name='id')
    project = StringField(read_only=True)
    name = StringField(read_only=True)
    revision = IntegerField(read_only=True)
    raw = DictField(read_only=False)

    @property
    def id(self):
        _id, _rev = self._id.rsplit('/', 1)
        if re.match(r'^\d*$', _rev):
            return _id
        else:
            return 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 __str__(self):
        revision = self.field('revision')
        if revision:
            return f'<App: id={self.id} rev={self.revision}>'
        return f'<App: id={self.id}'

    @classmethod
    def query(cls, project=None, visibility=None, q=None, id=None, offset=None,
              limit=None, api=None):
        """
        Query (List) apps.
        :param project: Source project.
        :param visibility: private|public for private or public apps.
        :param q: List containing search terms.
        :param id: List contains app ids. Fetch apps with specific ids.
        :param offset: Pagination offset.
        :param limit: Pagination limit.
        :param api: Api instance.
        :return: collection object
        """
        if project:
            project = Transform.to_project(project)
        api = api or cls._API
        return super()._query(url=cls._URL['query'], project=project,
                              visibility=visibility, q=q, id=id,
                              offset=offset, limit=limit, api=api)

    @classmethod
    def get_revision(cls, id, revision, api=None):
        """
        Get app revision.
        :param id: App identifier.
        :param revision: App revision
        :param api: Api instance.
        :return: App object.
        """
        api = api if api else cls._API
        extra = {'resource': cls.__name__, 'query': {
            'id': id,
            'revision': revision
        }}
        logger.info('Get revision', extra=extra)
        app = api.get(url=cls._URL['get_revision'].format(
            id=id, revision=revision)).json()
        return App(api=api, **app)

    @classmethod
    def install_app(cls, id, raw, api=None, raw_format=None):
        """
        Installs and app.
        :param id:  App identifier.
        :param raw: Raw cwl data.
        :param api: Api instance.
        :param raw_format: Format of raw app data being sent, json by default
        :return: App object.
        """
        api = api if api else cls._API
        raw_format = raw_format.lower() if raw_format else AppRawFormat.JSON
        extra = {
            'resource': cls.__name__,
            'query': {
                'id': id,
                'data': raw
            }
        }
        logger.info('Installing app', extra=extra)

        # Set content type for raw app data
        if raw_format not in cls._CONTENT_TYPE.keys():
            raise SbgError(f'Unsupported raw data format: "{raw_format}".')
        headers = {'Content-Type': cls._CONTENT_TYPE[raw_format]}

        app = api.post(
            url=cls._URL['raw'].format(id=id),
            data=raw,
            headers=headers,
        ).json()
        app_wrapper = api.get(
            url=cls._URL['get'].format(id=app['sbg:id'])).json()
        return App(api=api, **app_wrapper)

    @classmethod
    def create_revision(cls, id, revision, raw, api=None):
        """
        Create a new app revision.
        :param id:  App identifier.
        :param revision: App revision.
        :param raw: Raw cwl object.
        :param api: Api instance.
        :return: App object.
        """

        api = api if api else cls._API
        extra = {'resource': cls.__name__, 'query': {
            'id': id,
            'data': raw
        }}
        logger.info('Creating app revision', extra=extra)
        app = api.post(url=cls._URL['create_revision'].format(
            id=id, revision=revision), data=raw).json()
        app_wrapper = api.get(
            url=cls._URL['get'].format(id=app['sbg:id'])).json()
        return App(api=api, **app_wrapper)

    def copy(self, project, name=None, strategy=None, use_revision=False,
             api=None):
        """
        Copies the current app.
        :param project: Destination project.
        :param name: Destination app name.
        :param strategy: App copy strategy.
        :param use_revision: Copy from set app revision.
        :param api: Api instance.
        :return: Copied App object.

        :Copy strategies:
        clone         copy all revisions and continue getting updates form the
                      original app (default method when the key is omitted)

        direct        copy only the latest revision and get the updates from
                      this point on

        clone_direct  copy the app like the direct strategy, but keep all
                      revisions

        transient     copy only the latest revision and continue getting
                      updates from the original app
        """
        api = api or self._API
        app_id = self._id if use_revision else self.id
        strategy = strategy or AppCopyStrategy.CLONE

        project = Transform.to_project(project)
        data = {
            'project': project,
            'strategy': strategy
        }
        if name:
            data['name'] = name
        extra = {
            'resource': type(self).__name__,
            'query': {
                'id': app_id,
                'data': data
            }
        }
        logger.info('Copying app', extra=extra)
        app = api.post(
            url=self._URL['copy'].format(id=app_id), data=data
        ).json()
        return App(api=api, **app)

    def sync(self):
        """
        Syncs the parent app changes with the current app instance.
        :return: Synced App object.
        """
        app = self._api.post(url=self._URL['sync'].format(id=self.id)).json()
        return App(api=self._api, **app) 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) class Task(Resource):
    """
    Central resource for managing tasks.
    """
    _URL = {
        'query': '/tasks',
        'get': '/tasks/{id}',
        'delete': '/tasks/{id}',
        'run': '/tasks/{id}/actions/run',
        'clone': '/tasks/{id}/actions/clone',
        'abort': '/tasks/{id}/actions/abort',
        'execution_details': "/tasks/{id}/execution_details",
        'bulk_get': '/bulk/tasks/get',
    }

    href = HrefField(read_only=True)
    id = UuidField(read_only=True)
    name = StringField(read_only=False)
    status = StringField(read_only=True)
    description = StringField(read_only=False)
    project = StringField(read_only=False)
    app = StringField(read_only=False)
    type = StringField(read_only=True)
    created_by = StringField(read_only=True)
    executed_by = StringField(read_only=True)
    start_time = DateTimeField(read_only=True)
    created_time = DateTimeField(read_only=True)
    end_time = DateTimeField(read_only=True)
    batch = BooleanField(read_only=False)
    batch_by = CompoundField(BatchBy, read_only=False)
    batch_group = CompoundField(BatchGroup, read_only=True)
    batch_input = StringField(read_only=False)
    parent = StringField(read_only=True)
    execution_status = CompoundField(ExecutionStatus, read_only=True)
    errors = DictField(read_only=True)
    warnings = DictField(read_only=True)
    price = CompoundField(Price, read_only=True)
    inputs = CompoundField(Input, read_only=False)
    outputs = CompoundField(Output, read_only=True)
    execution_settings = DictField(read_only=True)
    use_interruptible_instances = BooleanField(read_only=False)
    origin = StringField(read_only=True, name='origin_id')

    def __str__(self):
        return f'<Task: 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

    @classmethod
    def query(cls, project=None, status=None, batch=None,
              parent=None, created_from=None, created_to=None,
              started_from=None, started_to=None, ended_from=None,
              ended_to=None, offset=None, limit=None, order_by=None,
              order=None, origin=None, api=None):
        """
        Query (List) tasks. Date parameters may be both strings and python date
        objects.
        :param project: Target project. optional.
        :param status: Task status.
        :param batch: Only batch tasks.
        :param parent: Parent batch task identifier.
        :param ended_to: All tasks that ended until this date.
        :param ended_from: All tasks that ended from this date.
        :param started_to: All tasks that were started until this date.
        :param started_from: All tasks that were started from this date.
        :param created_to: All tasks that were created until this date.
        :param created_from: All tasks that were created from this date.
        :param offset: Pagination offset.
        :param limit: Pagination limit.
        :param order_by: Property to order by.
        :param order: Ascending or descending ordering.
        :param origin: Entity that created the task, e.g. automation run,
        if task was created by an automation run.
        :param api: Api instance.
        :return: Collection object.
        """
        api = api or cls._API
        if parent:
            parent = Transform.to_task(parent)
        if project:
            project = Transform.to_project(project)
        if created_from:
            created_from = Transform.to_datestring(created_from)
        if created_to:
            created_to = Transform.to_datestring(created_to)
        if started_from:
            started_from = Transform.to_datestring(started_from)
        if started_to:
            started_to = Transform.to_datestring(started_to)
        if ended_from:
            ended_from = Transform.to_datestring(ended_from)
        if ended_to:
            ended_to = Transform.to_datestring(ended_to)
        if origin:
            origin = Transform.to_automation_run(origin)

        return super()._query(
            url=cls._URL['query'], project=project, status=status, batch=batch,
            parent=parent, created_from=created_from, created_to=created_to,
            started_from=started_from, started_to=started_to,
            ended_from=ended_from, ended_to=ended_to, offset=offset,
            limit=limit, order_by=order_by, order=order, fields='_all',
            origin_id=origin, api=api
        )

    @classmethod
    def create(cls, name, project, app, revision=None, batch_input=None,
               batch_by=None, inputs=None, description=None, run=False,
               disable_batch=False, interruptible=None,
               execution_settings=None, api=None):

        """
        Creates a task on server.
        :param name: Task name.
        :param project: Project identifier.
        :param app: CWL app identifier.
        :param revision: CWL app revision.
        :param batch_input: Batch input.
        :param batch_by: Batch criteria.
        :param inputs: Input map.
        :param description: Task description.
        :param run: True if you want to run a task upon creation.
        :param disable_batch: If True disables batching of a batch task.
        :param interruptible: If True interruptible instance will be used.
        :param execution_settings: Execution settings for the task.
        :param api: Api instance.
        :return: Task object.
        :raises: TaskValidationError if validation Fails.
        :raises: SbgError if any exception occurs during request.
        """
        task_data = {}
        params = {}
        project = Transform.to_project(project)

        app_id = Transform.to_app(app)

        if revision:
            app_id = f'{app_id}/{revision}'
        else:
            if isinstance(app, App):
                app_id = f'{app_id}/{app.revision}'

        task_inputs = {
            'inputs': Task._serialize_inputs(inputs) if inputs else {}
        }

        if batch_input and batch_by:
            task_data['batch_input'] = batch_input
            task_data['batch_by'] = batch_by
            if disable_batch:
                params.update({'batch': False})

        task_meta = {
            'name': name,
            'project': project,
            'app': app_id,
            'description': description,
        }
        task_data.update(task_meta)
        task_data.update(task_inputs)

        if interruptible is not None:
            task_data['use_interruptible_instances'] = interruptible

        if execution_settings:
            task_data.update({'execution_settings': execution_settings})

        if run:
            params.update({'action': 'run'})

        api = api if api else cls._API
        created_task = api.post(cls._URL['query'], data=task_data,
                                params=params).json()
        if run and 'errors' in created_task and created_task['errors']:
            raise TaskValidationError(
                'Unable to run task! Task contains errors.',
                task=Task(api=api, **created_task)
            )

        return Task(api=api, **created_task)

    @inplace_reload
    def abort(self, inplace=True):
        """
        Abort task
        :param inplace Apply action on the current object or return a new one.
        :return: Task object.
        """
        extra = {
            'resource': type(self).__name__,
            'query': {'id': self.id}
        }
        logger.info('Aborting task', extra=extra)
        task_data = self._api.post(
            url=self._URL['abort'].format(id=self.id)).json()
        return Task(api=self._api, **task_data)

    @inplace_reload
    def run(self, batch=True, interruptible=None, inplace=True):
        """
        Run task
        :param batch if False batching will be disabled.
        :param interruptible: If true interruptible instance
        will be used.
        :param inplace Apply action on the current object or return a new one.
        :return: Task object.
        """
        params = {}
        if not batch:
            params['batch'] = False
        if interruptible is not None:
            params['use_interruptible_instances'] = interruptible
        extra = {
            'resource': type(self).__name__,
            'query': {'id': self.id, 'batch': batch}
        }
        logger.info('Running task', extra=extra)
        task_data = self._api.post(
            url=self._URL['run'].format(id=self.id), params=params).json()
        return Task(api=self._api, **task_data)

    def clone(self, run=True):
        """
        Clone task
        :param run: run task after cloning
        :return: Task object.
        """
        params = {}
        if run:
            params.update({'action': 'run'})

        extra = {
            'resource': type(self).__name__,
            'query': {'id': self.id, 'run': run}
        }
        logger.info('Cloning task', extra=extra)
        task_data = self._api.post(
            url=self._URL['clone'].format(id=self.id), params=params).json()

        return Task(api=self._api, **task_data)

    @inplace_reload
    def save(self, inplace=True):
        """
        Saves all modification to the task on the server.
        :param inplace Apply edits on the current instance or get a new one.
        :return: Task instance.
        """
        modified_data = self._modified_data()
        if modified_data:
            task_request_data = {}
            inputs = modified_data.pop('inputs', None)
            execution_settings = modified_data.pop('execution_settings', None)
            task_request_data.update(modified_data)

            if inputs:
                task_request_data['inputs'] = self._serialize_inputs(inputs)

            if execution_settings:
                task_request_data['execution_settings'] = (
                    self._serialize_execution_settings(execution_settings)
                )

            extra = {
                'resource': type(self).__name__,
                'query': {'id': self.id, 'data': task_request_data}
            }
            logger.info('Saving task', extra=extra)
            data = self._api.patch(url=self._URL['get'].format(id=self.id),
                                   data=task_request_data).json()
            task = Task(api=self._api, **data)
            return task

    def _serialize_execution_settings(self, execution_settings):
        instance_type = execution_settings.get(
            'instance_type',
            self.execution_settings.get('instance_type', None)
        )
        max_parallel_instances = execution_settings.get(
            'max_parallel_instances',
            self.execution_settings.get('max_parallel_instances', None)
        )
        use_memoization = execution_settings.get(
            'use_memoization',
            self.execution_settings.get('use_memoization', None)
        )
        use_elastic_disc = execution_settings.get(
            'use_elastic_disc',
            self.execution_settings.get('use_elastic_disc', None)
        )
        serialized_es_mapping = {
            'instance_type': instance_type,
            'max_parallel_instances': max_parallel_instances,
            'use_memoization': use_memoization,
            'use_elastic_disc': use_elastic_disc
        }
        serialized_es = dict()
        for key, value in serialized_es_mapping.items():
            if value is not None:
                serialized_es[key] = value

        return serialized_es

    @staticmethod
    def _serialize_inputs(input_value):
        """
        Recursively serialises input dictionary.
        :param input_value: input dictionary to serialize
        :return: serialized input dictionary
        """
        if isinstance(input_value, list):
            return_value = []
            for elem in input_value:
                return_value.append(Task._serialize_inputs(elem))
        elif isinstance(input_value, dict):
            return_value = {}
            for key in input_value:
                return_value[key] = Task._serialize_inputs(
                    input_value[key]
                )
        elif isinstance(input_value, File):
            return_value = Task._to_api_file_format(input_value)
        else:
            return_value = input_value
        return return_value

    @staticmethod
    def _to_api_file_format(_file):
        return {
            'class': (
                FileApiFormats.FOLDER if _file.is_folder()
                else FileApiFormats.FILE
            ),
            'path': _file.id
        }

    def get_execution_details(self):
        """
        Retrieves execution details for a task.
        :return: Execution details instance.
        """
        extra = {
            'resource': type(self).__name__,
            'query': {'id': self.id}
        }
        logger.info('Get execution details', extra=extra)
        data = self._api.get(
            self._URL['execution_details'].format(id=self.id)).json()
        return ExecutionDetails(api=self._api, **data)

    def get_batch_children(self, status=None, created_from=None,
                           created_to=None, started_from=None, started_to=None,
                           ended_from=None, ended_to=None, order_by=None,
                           order=None, offset=None, limit=None, api=None):
        """
        Retrieves batch child tasks for this task if its a batch task.
        :return: Collection instance.
        :raises SbError if task is not a batch task.
        """
        api = api or self._api
        if not self.batch:
            raise SbgError("This task is not a batch task.")
        return self.query(
            parent=self.id, status=status, created_from=created_from,
            created_to=created_to, started_from=started_from,
            started_to=started_to, ended_from=ended_from, ended_to=ended_to,
            order_by=order_by, order=order, offset=offset, limit=limit,
            api=api,
        )

    @classmethod
    def bulk_get(cls, tasks, api=None):
        """
        Retrieve tasks with specified ids in bulk
        :param tasks: Tasks to be retrieved.
        :param api: Api instance.
        :return: List of TaskBulkRecord objects.
        """
        api = api or cls._API
        task_ids = [Transform.to_task(task) for task in tasks]
        data = {'task_ids': task_ids}

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

    def wait(self=None, period=10, callback=None, *args, **kwargs):
        """Wait until task is complete
        :param period: Time in seconds between reloads
        :param callback: Function to call after the task has finished,
            arguments and keyword arguments can be provided for it
        :return: Return value of provided callback function or None if a
            callback function was not provided
        """
        while self.status not in TaskStatus.terminal_states:
            self.reload()
            time.sleep(period)

        if callback:
            return callback(*args, **kwargs) class Team(Resource):
    """
    Central resource for managing teams.
    """
    _URL = {
        'query': '/teams',
        'get': '/teams/{id}',
        'members_query': '/teams/{id}/members',
        'members_get': '/teams/{id}/members/{member}',
    }

    href = HrefField(read_only=True)
    id = StringField(read_only=True)
    name = StringField(read_only=False)

    def __str__(self):
        return f'<Team: 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

    @classmethod
    def query(cls, division, list_all=False, offset=None, limit=None,
              api=None):
        """
        :param division: Division slug.
        :param list_all: List all teams in division.
        :param offset: Pagination offset.
        :param limit: Pagination limit.
        :param api: Api instance.
        :return: Collection object.
        """
        division = Transform.to_division(division)
        api = api if api else cls._API
        return super()._query(
            url=cls._URL['query'], division=division, _all=list_all,
            offset=offset, limit=limit, fields='_all', api=api
        )

    @classmethod
    def create(cls, name, division, api=None):
        """
        Create team within a division
        :param name:  Team name.
        :param division: Parent division.
        :param api: Api instance.
        :return: Team object.
        """

        division = Transform.to_division(division)
        api = api if api else cls._API
        data = {
            'name': name,
            'division': division
        }

        extra = {
            'resource': cls.__name__,
            'query': data
        }
        logger.info('Creating team', extra=extra)
        created_team = api.post(cls._URL['query'], data=data).json()
        return Team(api=api, **created_team)

    @inplace_reload
    def save(self, inplace=True):
        """
        Saves all modification to the team on the server.
        :param inplace Apply edits on the current instance or get a new one.
        :return: Team instance.
        """
        modified_data = self._modified_data()
        if modified_data:
            extra = {
                'resource': type(self).__name__,
                'query': {
                    'id': self.id,
                    'modified_data': modified_data
                }
            }
            logger.info('Saving team', extra=extra)
            data = self._api.patch(url=self._URL['get'].format(id=self.id),
                                   data=modified_data).json()
            team = Team(api=self._api, **data)
            return team
        else:
            raise ResourceNotModified()

    def get_members(self, offset=None, limit=None):
        """
        Fetch team members for current team.
        :param offset: Pagination offset.
        :param limit: Pagination limit.
        :return: Collection object.
        """
        extra = {
            'resource': type(self).__name__,
            'query': {'id': self.id}
        }
        logger.info('Get team members', extra=extra)
        response = self._api.get(
            url=self._URL['members_query'].format(id=self.id),
            params={'offset': offset, 'limit': limit}
        )
        data = response.json()
        total = response.headers['x-total-matching-query']
        members = [TeamMember(api=self._api, **member) for member in
                   data['items']]
        links = [Link(**link) for link in data['links']]
        href = data['href']
        return Collection(resource=TeamMember, href=href, total=total,
                          items=members, links=links, api=self._api)

    def add_member(self, user):
        """
        Add member to team
        :param user: User object or user's username
        :return: Added user.
        """
        user = Transform.to_user(user)
        data = {
            'id': user
        }
        extra = {
            'resource': type(self).__name__,
            'query': {
                'id': self.id,
                'data': data,
            }
        }
        logger.info('Adding team member using id', extra=extra)
        response = self._api.post(
            url=self._URL['members_query'].format(id=self.id), data=data)
        member_data = response.json()
        return TeamMember(api=self._api, **member_data)

    def remove_member(self, user):
        """
        Remove member from the team.
        :param user: User to be removed.
        """
        member = Transform.to_user(user)
        extra = {
            'resource': type(self).__name__,
            'query': {
                'id': self.id,
                'user': user,
            }
        }
        logger.info('Removing team member', extra=extra)
        self._api.delete(
            url=self._URL['members_get'].format(id=self.id, member=member)
        ) class User(Resource):
    """
    Central resource for managing users.
    """
    _URL = {
        'me': '/user',
        'get': '/users/{id}',
        'query': '/users',
        'delete': '/users/{username}'
    }

    href = HrefField(read_only=True)
    username = StringField(read_only=True)
    email = StringField(read_only=True)
    first_name = StringField(read_only=True)
    last_name = StringField(read_only=True)
    affiliation = StringField(read_only=True)
    phone = StringField(read_only=True)
    address = StringField(read_only=True)
    state = StringField(read_only=True)
    country = StringField(read_only=True)
    zip_code = StringField(read_only=True)
    city = StringField(read_only=True)
    role = StringField(read_only=True)

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

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

    @classmethod
    def me(cls, api=None):
        """
        Retrieves current user information.
        :param api: Api instance.
        :return: User object.
        """
        api = api if api else cls._API
        extra = {
            'resource': cls.__name__,
            'query': {}
        }
        logger.info('Fetching user information', extra=extra)
        user_data = api.get(cls._URL['me']).json()
        return User(api=api, **user_data)

    @classmethod
    def get(cls, user, api=None):
        api = api if api else cls._API
        user = Transform.to_user(user)
        return super().get(id=user, api=api)

    @classmethod
    def query(cls, division, role=None, offset=None, limit=None, api=None):
        """Query division users
        :param division: Division slug.
        :param role: User role in division.
        :param offset: Pagination offset.
        :param limit: Pagination limit.
        :param api: Api instance.
        :return: Collection object.
        """
        api = api or cls._API
        params = {
            'division': Transform.to_division(division),
        }
        if role:
            params['role'] = role

        return super()._query(
            url=cls._URL['query'],
            api=api,
            offset=offset,
            limit=limit,
            **params
        )

    def disable(self, api=None):
        """
        Disable user
        :param api: Api instance.
        :return:
        """
        api = api or self._API
        api.delete(
            url=self._URL['delete'].format(username=self.username)
        ) class Marker(Resource):
    _URL = {
        'query': '/genome/markers',
        'get': '/genome/markers/{id}',
        'delete': '/genome/markers/{id}'
    }

    href = HrefField(read_only=True)
    id = StringField(read_only=True)
    file = StringField(read_only=True)
    name = StringField(read_only=False)
    chromosome = StringField(read_only=False)
    position = CompoundField(MarkerPosition, read_only=False)
    created_time = DateTimeField(read_only=True)
    created_by = StringField(read_only=True)

    def __str__(self):
        return f'<Marker: 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

    @classmethod
    def query(cls, file, offset=None, limit=None, api=None):
        """
        Queries genome markers on a file.
        :param file: Genome file - Usually bam file.
        :param offset: Pagination offset.
        :param limit: Pagination limit.
        :param api: Api instance.
        :return: Collection object.
        """
        api = api if api else cls._API

        file = Transform.to_file(file)
        return super()._query(
            url=cls._URL['query'], offset=offset, limit=limit,
            file=file, fields='_all', api=api
        )

    @classmethod
    def create(cls, file, name, position, chromosome, private=True, api=None):
        """
        Create a marker on a file.
        :param file: File object or identifier.
        :param name: Marker name.
        :param position: Marker position object.
        :param chromosome: Chromosome number.
        :param private: Whether the marker is private or public.
        :param api: Api instance.
        :return: Marker object.
        """
        api = api if api else cls._API

        file = Transform.to_file(file)
        data = {
            'file': file,
            'name': name,
            'position': position,
            'chromosome': chromosome,
            'private': private
        }

        extra = {
            'resource': cls.__name__,
            'query': data
        }
        logger.info('Creating marker', extra=extra)
        marker_data = api.post(url=cls._URL['query'], data=data).json()
        return Marker(api=api, **marker_data)

    @inplace_reload
    def save(self, inplace=True):
        """
        Saves all modification to the marker on the server.
        :param inplace Apply edits on the current instance or get a new one.
        :return: Marker instance.
        """
        modified_data = self._modified_data()
        if modified_data:
            extra = {
                'resource': type(self).__name__,
                'query': {
                    'id': self.id,
                    'modified_data': modified_data
                }
            }
            logger.info('Saving marker', extra=extra)
            data = self._api.patch(url=self._URL['get'].format(id=self.id),
                                   data=modified_data).json()
            marker = Marker(api=self._api, **data)
            return marker
        else:
            raise ResourceNotModified() 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,
                   'bucket': bucket,
                   'credentials': {'role_arn': role_arn,
                                   'external_id': external_id
                                   }
                   }
        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 using role auth', extra=extra)
        response = api.post(url=cls._URL['query'], data=data).json()
        return Volume(api=api, **response)

    @classmethod
    def create_google_volume(cls, name, bucket, client_email, private_key,
                             access_mode, description=None, prefix=None,
                             properties=None, api=None):

        """
        Create google volume.
        :param name: Volume name.
        :param bucket: Referenced bucket.
        :param client_email: Google client email.
        :param private_key: Google client private 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.GOOGLE,
                   'bucket': bucket,
                   'credentials': {'client_email': client_email,
                                   'private_key': private_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 google volume', extra=extra)
        response = api.post(url=cls._URL['query'], data=data).json()
        return Volume(api=api, **response)

    @classmethod
    def create_google_iam_volume(cls, name, bucket, configuration, access_mode,
                                 description=None, prefix=None,
                                 properties=None, api=None):

        """
        Create google volume.
        :param name: Volume name.
        :param bucket: Referenced bucket.
        :param configuration: Google configuration.
        :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.GOOGLE,
                   'bucket': bucket,
                   'credentials': {'configuration': configuration}
                   }
        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 google volume', extra=extra)
        response = api.post(url=cls._URL['query'], data=data).json()
        return Volume(api=api, **response)

    @classmethod
    def create_oss_volume(cls, name, bucket, endpoint, access_key_id,
                          secret_access_key, access_mode, description=None,
                          prefix=None, properties=None, api=None):
        """
        Create oss volume.
        :param name: Volume name.
        :param bucket: Referenced bucket.
        :param access_key_id: Access key identifier.
        :param secret_access_key: Secret access key.
        :param access_mode: Access Mode.
        :param endpoint: Volume Endpoint.
        :param description: Volume description.
        :param prefix: Volume prefix.
        :param properties: Volume properties.
        :param api: Api instance.
        :return: Volume object.
        """
        service = {
            'type': VolumeType.OSS,
            'bucket': bucket,
            'endpoint': endpoint,
            '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 oss volume', extra=extra)
        response = api.post(url=cls._URL['query'], data=data).json()
        return Volume(api=api, **response)

    @inplace_reload
    def save(self, inplace=True):
        """
        Saves all modification to the volume on the server.
        """
        modified_data = self._modified_data()
        if modified_data:
            extra = {
                'resource': type(self).__name__,
                'query': {
                    'id': self.id,
                    'modified_data': modified_data
                }
            }
            logger.info('Saving volume', extra=extra)
            data = self._api.patch(url=self._URL['get'].format(id=self.id),
                                   data=modified_data).json()
            volume = Volume(api=self._api, **data)
            return volume
        else:
            raise ResourceNotModified()

    def list(self, prefix=None, limit=None):
        params = {}
        if prefix:
            params['prefix'] = prefix
        if limit:
            params['limit'] = limit

        data = self._api.get(
            url=self._URL['list'].format(id=self.id), params=params).json()

        href = data['href']
        links = [VolumeLink(**link) for link in data['links']]

        objects = [
            VolumeObject(api=self._api, **item) for item in data['items']
        ]
        prefixes = [
            VolumePrefix(api=self._api, **prefix) for prefix in  # noqa: F812
            data['prefixes']
        ]
        return VolumeCollection(
            href=href, items=objects, links=links,
            prefixes=prefixes, api=self._api
        )

    def get_volume_object_info(self, location):
        """
        Fetches information about single volume object - usually file
        :param location: object location
        :return:
        """
        param = {'location': location}
        data = self._api.get(url=self._URL['object'].format(
            id=self.id), params=param).json()
        return VolumeObject(api=self._api, **data)

    def get_imports(self, project=None, state=None, offset=None, limit=None):
        """
        Fetches imports for this volume.
        :param project: Optional project identifier.
        :param state: Optional state.
        :param offset: Pagination offset.
        :param limit: Pagination limit.
        :return: Collection object.
        """
        return self._api.imports.query(volume=self, project=project,
                                       state=state, offset=offset, limit=limit)

    def get_exports(self, state=None, offset=None, limit=None):
        """
        Fetches exports for this volume.
        :param state: Optional state.
        :param offset: Pagination offset.
        :param limit: Pagination limit.
        :return: Collection object.
        """
        return self._api.exports.query(volume=self, state=state, offset=offset,
                                       limit=limit
                                       )

    def get_members(self, offset=None, limit=None):
        """
        Retrieves volume members.
        :param offset: Pagination offset.
        :param limit: Pagination limit.
        :return: Collection object.
        """
        extra = {
            'resource': type(self).__name__,
            'query': {'id': self.id}
        }
        logger.info('Get volume members', extra=extra)
        response = self._api.get(
            url=self._URL['members_query'].format(id=self.id),
            params={'offset': offset, 'limit': limit})
        data = response.json()
        total = response.headers['x-total-matching-query']
        members = [Member(api=self._api, **member) for member in data['items']]
        links = [Link(**link) for link in data['links']]
        href = data['href']
        return Collection(resource=Member, href=href, total=total,
                          items=members, links=links, api=self._api)

    def add_member(self, user, permissions):
        """
        Add a member to the volume.
        :param user:  Member username
        :param permissions: Permissions dictionary.
        :return: Member object.
        """
        user = Transform.to_user(user)
        data = {'username': user, 'type': 'USER'}

        if 'execute' in permissions:
            permissions.pop('execute')

        if isinstance(permissions, dict):
            data.update({
                'permissions': permissions
            })

        extra = {
            'resource': type(self).__name__,
            'query': {
                'id': self.id,
                'data': data,
            }
        }
        logger.info('Adding volume member', extra=extra)
        response = self._api.post(
            url=self._URL['members_query'].format(id=self.id), data=data)
        member_data = response.json()
        return Member(api=self._api, **member_data)

    def add_member_team(self, team, permissions):
        """
        Add a member (team) to a volume.
        :param team: Team object or team identifier.
        :param permissions: Permissions dictionary.
        :return: Member object.
        """
        team = Transform.to_team(team)
        data = {'username': team, 'type': 'TEAM'}

        if 'execute' in permissions:
            permissions.pop('execute')

        if isinstance(permissions, dict):
            data.update({
                'permissions': permissions
            })

        extra = {
            'resource': type(self).__name__,
            'query': {
                'id': self.id,
                'data': data,
            }
        }
        logger.info('Adding volume team member', extra=extra)
        response = self._api.post(
            url=self._URL['members_query'].format(id=self.id), data=data)
        member_data = response.json()
        return Member(api=self._api, **member_data)

    def add_member_division(self, division, permissions):
        """
        Add a member (team) to a volume.
        :param division: Division object or division identifier.
        :param permissions: Permissions dictionary.
        :return: Member object.
        """
        division = Transform.to_division(division)

        if 'execute' in permissions:
            permissions.pop('execute')

        data = {'username': division, 'type': 'DIVISION'}
        if isinstance(permissions, dict):
            data.update({
                'permissions': permissions
            })

        extra = {
            'resource': type(self).__name__,
            'query': {
                'id': self.id,
                'data': data,
            }
        }
        logger.info('Adding volume division member', extra=extra)
        response = self._api.post(
            url=self._URL['members_query'].format(id=self.id), data=data)
        member_data = response.json()
        return Member(api=self._api, **member_data)

    def get_member(self, username, api=None):
        """
        Fetches information about a single volume member
        :param username: Member name
        :param api: Api instance
        :return: Member object
        """
        api = api if api else self._API

        response = api.get(
            url=self._URL['member'].format(id=self.id, username=username),
        )
        data = response.json()
        return Member(api=api, **data)

    def remove_member(self, user):
        """
        Remove member from the volume.
        :param user: User to be removed.
        """
        username = Transform.to_user(user)
        extra = {
            'resource': type(self).__name__,
            'query': {
                'id': self.id,
                'user': user,
            }
        }
        logger.info('Removing volume member', extra=extra)
        self._api.delete(
            url=self._URL['member'].format(id=self.id, username=username)
        ) class Actions(Resource):
    _URL = {
        'send_feedback': '/action/notifications/feedback',
        'bulk_copy': '/action/files/copy'
    }

    def __str__(self):
        return '<Actions>'

    # noinspection PyShadowingBuiltins
    @classmethod
    def send_feedback(cls, type=FeedbackType.IDEA, referrer=None, text=None,
                      api=None):
        """
        Sends feedback to sevenbridges.
        :param type: FeedbackType wither IDEA, PROBLEM or THOUGHT.
        :param text: Feedback text.
        :param referrer: Feedback referrer.
        :param api: Api instance.
        """
        api = api if api else cls._API
        data = {
            'type': type,
            'text': text,
            'referrer': referrer if referrer else str(client_info)
        }

        extra = {
            'resource': cls.__name__,
            'query': data
        }
        logger.info('Sending feedback', extra=extra)
        api.post(url=cls._URL['send_feedback'], data=data)

    @classmethod
    def bulk_copy_files(cls, files, destination_project, api=None):
        """
        Bulk copy of files.
        :param files: List containing files to be copied.
        :param destination_project: Destination project.
        :param api: Api instance.
        :return: MultiStatus copy result.
        """
        api = api if api else cls._API
        files = [Transform.to_file(file) for file in files]
        data = {
            'project': destination_project,
            'file_ids': files
        }
        extra = {
            'resource': cls.__name__,
            'query': data
        }
        logger.info('Performing bulk copy', extra=extra)
        return api.post(url=cls._URL['bulk_copy'], data=data).json() class Dataset(Resource):
    """Central resource for managing datasets."""

    _URL = {
        'query': '/datasets',
        'owned_by': '/datasets/{username}',
        'get': '/datasets/{id}',
        'delete': '/datasets/{id}',
        'members': '/datasets/{id}/members',
        'member': '/datasets/{id}/members/{username}',
        'permissions': '/datasets/{id}/members/{username}/permissions',
    }

    href = HrefField(read_only=True)
    id = StringField(read_only=True)
    name = StringField(read_only=False)
    description = StringField(read_only=False)

    def __str__(self):
        return f'<Dataset: 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

    @classmethod
    def query(cls, visibility=None, api=None):
        """Query ( List ) datasets
        :param visibility: If provided as 'public', retrieves public datasets
        :param api: Api instance
        :return: Collection object
        """
        api = api if api else cls._API

        return super()._query(
            url=cls._URL['query'],
            visibility=visibility,
            fields='_all', api=api
        )

    @classmethod
    def get_owned_by(cls, username, api=None):
        """Query ( List ) datasets by owner
        :param api: Api instance
        :param username: Owner username
        :return: Collection object
        """
        api = api if api else cls._API

        return super()._query(
            url=cls._URL['owned_by'].format(username=username),
            fields='_all',
            api=api
        )

    @inplace_reload
    def save(self, inplace=True):
        """Save all modification to the dataset on the server.
        :param inplace: Apply edits on the current instance or get a new one.
        :return: Dataset instance.
        """
        modified_data = self._modified_data()
        if modified_data:
            dataset_request_data = {}

            name = modified_data.pop('name', None)
            description = modified_data.pop('description', None)
            dataset_request_data.update(modified_data)

            if name:
                dataset_request_data['name'] = name

            if description:
                dataset_request_data['description'] = description

            response = self._api.patch(
                url=self._URL['get'].format(id=self.id),
                data=dataset_request_data
            )
            data = response.json()

            dataset = Dataset(api=self._api, **data)
            return dataset

    def get_members(self, api=None):
        """Retrieve dataset members
        :param api: Api instance
        :return: Collection object
        """
        api = api or self._API

        response = api.get(url=self._URL['members'].format(id=self.id))

        data = response.json()
        total = response.headers['x-total-matching-query']
        members = [Member(api=api, **member) for member in data['items']]
        links = [Link(**link) for link in data['links']]
        href = data['href']

        return Collection(
            resource=Member,
            href=href,
            total=total,
            items=members,
            links=links,
            api=api
        )

    def get_member(self, username, api=None):
        """Retrieve dataset member
        :param username: Member name
        :param api: Api instance
        :return: Member object
        """
        api = api if api else self._API

        response = api.get(
            url=self._URL['member'].format(id=self.id, username=username),
        )
        data = response.json()
        return Member(api=api, **data)

    def add_member(self, username, permissions, api=None):
        """Add member to a dataset
        :param username: Member username
        :param permissions: Permissions dict
        :param api: Api instance
        :return: New member instance
        """
        api = api or self._API
        data = {
            'username': username,
            'permissions': permissions
        }

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

    def remove_member(self, member, api=None):
        """Remove member from a dataset
        :param member: Member username
        :param api: Api instance
        :return: None
        """
        api = api or self._API
        username = Transform.to_member(member)

        api.delete(
            url=self._URL['member'].format(
                id=self.id,
                username=username
            )
        ) class Invoice(Resource):
    """
     Central resource for managing invoices.
    """
    _URL = {
        'query': '/billing/invoices',
        'get': '/billing/invoices/{id}'
    }
    href = HrefField(read_only=True)
    id = StringField(read_only=True)
    pending = BooleanField(read_only=True)
    analysis_costs = CompoundField(Price, read_only=True)
    storage_costs = CompoundField(Price, read_only=True)
    total = CompoundField(Price, read_only=True)
    invoice_period = CompoundField(InvoicePeriod, read_only=True)

    def __str__(self):
        return f'<Invoice: 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

    @classmethod
    def query(cls, offset=None, limit=None, api=None):
        """
        Query (List) invoices.
        :param offset: Pagination offset.
        :param limit: Pagination limit.
        :param api: Api instance.
        :return: Collection object.
        """
        api = api if api else cls._API
        return super()._query(
            url=cls._URL['query'], offset=offset, limit=limit, fields='_all',
            api=api
        ) class Project(Resource):
    """
    Central resource for managing projects.
    """
    _URL = {
        'query': '/projects/{owner}',
        'create': '/projects',
        'get': '/projects/{id}',
        'delete': '/projects/{id}',
        'member': '/projects/{id}/members/{username}',
        'members_query': '/projects/{id}/members',
        'apps': '/apps',
        'files': '/files',
        'tasks': '/tasks'
    }
    href = HrefField(read_only=True)
    id = StringField(read_only=True)
    name = StringField(read_only=False)
    billing_group = UuidField(read_only=False)
    description = StringField(read_only=False)
    type = StringField(read_only=False, max_length=2)
    tags = BasicListField(read_only=False)
    settings = CompoundField(Settings, read_only=False)
    root_folder = StringField(read_only=True)
    created_by = StringField(read_only=True)
    created_on = DateTimeField(read_only=True)
    modified_on = DateTimeField(read_only=True)
    category = StringField(read_only=False)

    def __str__(self):
        return f'<Project: 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

    @classmethod
    def query(cls, owner=None, name=None, offset=None, limit=None, api=None,
              category=None, tags=None):
        """
        Query (List) projects
        :param owner: Owner username.
        :param name: Project name
        :param offset: Pagination offset.
        :param limit: Pagination limit.
        :param api: Api instance.
        :param category: Project category.
        :param tags: Project tags.
        :return: Collection object.
        """
        api = api if api else cls._API
        query_params = {}
        if owner:
            url = cls._URL['query'].format(owner=owner)
        else:
            url = cls._URL['query'].format(owner='')
        if name:
            query_params['name'] = name
        if category:
            query_params['category'] = category
        if tags:
            query_params['tags'] = Transform.to_tags(tags)
        return super()._query(
            url=url, offset=offset, limit=limit, fields='_all',
            api=api, **query_params
        )

    @classmethod
    def create(cls, name, billing_group=None, description=None, tags=None,
               settings=None, api=None):
        """
        Create a project.
        :param name:  Project name.
        :param billing_group: Project billing group.
        :param description:  Project description.
        :param tags: Project tags.
        :param settings: Project settings.
        :param api: Api instance.
        :return:
        """
        api = api if api else cls._API

        if name is None:
            raise SbgError('Project name is required!')

        data = {
            'name': name,
        }

        if billing_group:
            data['billing_group'] = Transform.to_billing_group(billing_group)

        if description:
            data['description'] = description
        if tags:
            data['tags'] = tags

        if settings:
            data['settings'] = settings

        extra = {
            'resource': cls.__name__,
            'query': data
        }
        logger.info('Creating project', extra=extra)
        project_data = api.post(url=cls._URL['create'], data=data).json()
        return Project(api=api, **project_data)

    @inplace_reload
    def save(self, inplace=True):
        """
        Saves all modification to the project on the server.
        :param inplace Apply edits on the current instance or get a new one.
        :return: Project instance.
        """
        modified_data = self._modified_data()
        if modified_data:
            extra = {
                'resource': type(self).__name__,
                'query': {
                    'id': self.id,
                    'modified_data': modified_data
                }
            }
            logger.info('Saving project', extra=extra)
            data = self._api.patch(url=self._URL['get'].format(id=self.id),
                                   data=modified_data).json()
            project = Project(api=self._api, **data)
            return project
        else:
            raise ResourceNotModified()

    def get_members(self, offset=None, limit=None):
        """
        Retrieves project members.
        :param offset: Pagination offset.
        :param limit: Pagination limit.
        :return: Collection object.
        """
        extra = {
            'resource': type(self).__name__,
            'query': {'id': self.id}
        }
        logger.info('Get members', extra=extra)
        response = self._api.get(
            url=self._URL['members_query'].format(id=self.id),
            params={'offset': offset, 'limit': limit})
        data = response.json()
        total = response.headers['x-total-matching-query']
        members = [Member(api=self._api, **member) for member in data['items']]
        links = [Link(**link) for link in data['links']]
        href = data['href']
        return Collection(resource=Member, href=href, total=total,
                          items=members, links=links, api=self._api)

    def add_member(self, user, permissions):
        """
        Add a member to the project.
        :param user:  Member username
        :param permissions: Permissions dictionary.
        :return: Member object.
        """
        user = Transform.to_user(user)
        data = {'username': user, 'type': 'USER'}
        if isinstance(permissions, dict):
            data.update({
                'permissions': permissions
            })

        extra = {
            'resource': type(self).__name__,
            'query': {
                'id': self.id,
                'data': data,
            }
        }
        logger.info('Adding member', extra=extra)
        response = self._api.post(
            url=self._URL['members_query'].format(id=self.id), data=data)
        member_data = response.json()
        return Member(api=self._api, **member_data)

    def add_member_team(self, team, permissions):
        """
        Add a member (team) to a project.
        :param team: Team object or team identifier.
        :param permissions: Permissions dictionary.
        :return: Member object.
        """
        team = Transform.to_team(team)
        data = {'username': team, 'type': 'TEAM'}
        if isinstance(permissions, dict):
            data.update({
                'permissions': permissions
            })

        extra = {
            'resource': type(self).__name__,
            'query': {
                'id': self.id,
                'data': data,
            }
        }
        logger.info('Adding team member', extra=extra)
        response = self._api.post(
            url=self._URL['members_query'].format(id=self.id), data=data)
        member_data = response.json()
        return Member(api=self._api, **member_data)

    def add_member_division(self, division, permissions):
        """
        Add a member (team) to a project.
        :param division: Division object or division identifier.
        :param permissions: Permissions dictionary.
        :return: Member object.
        """
        division = Transform.to_division(division)
        data = {'username': division, 'type': 'DIVISION'}
        if isinstance(permissions, dict):
            data.update({
                'permissions': permissions
            })

        extra = {
            'resource': type(self).__name__,
            'query': {
                'id': self.id,
                'data': data,
            }
        }
        logger.info('Adding division member', extra=extra)
        response = self._api.post(
            url=self._URL['members_query'].format(id=self.id), data=data)
        member_data = response.json()
        return Member(api=self._api, **member_data)

    def add_member_email(self, email, permissions=None):
        """
        Add a member to the project using member email.
        :param email: Member email.
        :param permissions: Permissions dictionary.
        :return: Member object.
        """
        data = {'email': email}

        if isinstance(permissions, dict):
            data.update({
                'permissions': permissions
            })

        extra = {
            'resource': type(self).__name__,
            'query': {
                'id': self.id,
                'data': data,
            }
        }
        logger.info('Adding member using email', extra=extra)
        response = self._api.post(
            url=self._URL['members_query'].format(id=self.id), data=data)
        member_data = response.json()
        return Member(api=self._api, **member_data)

    def get_member(self, username, api=None):
        """
        Fetches information about a single project member
        :param username: Member name
        :param api: Api instance
        :return: Member object
        """
        api = api if api else self._API

        response = api.get(
            url=self._URL['member'].format(id=self.id, username=username),
        )
        data = response.json()
        return Member(api=api, **data)

    def remove_member(self, user):
        """
        Remove member from the project.
        :param user: User to be removed.
        """
        username = Transform.to_user(user)
        extra = {
            'resource': type(self).__name__,
            'query': {
                'id': self.id,
                'user': user,
            }
        }
        logger.info('Removing member', extra=extra)
        self._api.delete(
            url=self._URL['member'].format(id=self.id, username=username)
        )

    def get_files(self, offset=None, limit=None):
        """
        Retrieves files in this project.
        :param offset: Pagination offset.
        :param limit: Pagination limit.
        :return: Collection object.
        """
        params = {'project': self.id, 'offset': offset, 'limit': limit}
        return self._api.files.query(api=self._api, **params)

    def add_files(self, files):
        """
        Adds files to this project.
        :param files: List of files or a Collection object.
        """
        for file in files:
            file.copy(project=self.id)

    def get_apps(self, offset=None, limit=None):
        """
        Retrieves apps in this project.
        :param offset:  Pagination offset.
        :param limit: Pagination limit.
        :return: Collection object.
        """
        params = {'project': self.id, 'offset': offset,
                  'limit': limit}
        return self._api.apps.query(api=self._api, **params)

    def get_tasks(self, status=None, offset=None, limit=None):
        """
        Retrieves tasks in this project.
        :param status: Optional task status.
        :param offset:  Pagination offset.
        :param limit: Pagination limit.
        :return: Collection object.
        """
        params = {'project': self.id, 'offset': offset, 'limit': limit}
        if status:
            params['status'] = status
        return self._api.tasks.query(api=self._api, **params)

    def get_imports(self, volume=None, state=None, offset=None, limit=None):
        """
        Fetches imports for this project.
        :param volume: Optional volume identifier.
        :param state: Optional state.
        :param offset: Pagination offset.
        :param limit: Pagination limit.
        :return: Collection object.
        """
        return self._api.imports.query(project=self.id, volume=volume,
                                       state=state, offset=offset, limit=limit)

    def get_exports(self, volume=None, state=None, offset=None, limit=None):
        """
        Fetches exports for this volume.
        :param volume: Optional volume identifier.
        :param state: Optional state.
        :param offset: Pagination offset.
        :param limit: Pagination limit.
        :return: Collection object.
        """
        return self._api.exports.query(project=self.id, volume=volume,
                                       state=state, offset=offset, limit=limit)

    def create_task(self, name, app, revision=None, batch_input=None,
                    batch_by=None, inputs=None, description=None, run=False,
                    disable_batch=False, interruptible=True,
                    execution_settings=None):
        """
        Creates a task for this project.

        :param name: Task name.
        :param app: CWL app identifier.
        :param revision: CWL app revision.
        :param batch_input: Batch input.
        :param batch_by: Batch criteria.
        :param inputs: Input map.
        :param description: Task description.
        :param run: True if you want to run a task upon creation.
        :param disable_batch: True if you want to disable batching.
        :param interruptible: True if you want to use interruptible instances.
        :param execution_settings: Execution settings for the task.
        :return: Task object.
        """
        return self._api.tasks.create(
            name=name, project=self, app=app, revision=revision,
            batch_input=batch_input, batch_by=batch_by, inputs=inputs,
            description=description, run=run, disable_batch=disable_batch,
            interruptible=interruptible, execution_settings=execution_settings
        ) class Division(Resource):
    """
    Central resource for managing divisions.
    """
    _URL = {
        'query': '/divisions',
        'get': '/divisions/{id}',
    }

    href = HrefField(read_only=True)
    id = StringField(read_only=True)
    name = StringField(read_only=True)

    def __str__(self):
        return f'<Division: 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

    @classmethod
    def query(cls, offset=None, limit=None, api=None):
        """
        Query (List) divisions.

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

    def get_teams(self, offset=None, limit=None):
        return self._api.teams.query(
            division=self.id, offset=offset, limit=limit
        )

    def get_members(self, role=None, offset=None, limit=None):
        return self._api.users.query(self, role=role, offset=offset,
                                     limit=limit) class AsyncJob(Resource):
    """
    Central resource for managing async jobs
    """

    _URL = {
        'list_file_jobs': '/async/files',
        'get_file_copy_job': '/async/files/copy/{id}',
        'get_file_delete_job': '/async/files/delete/{id}',
        'bulk_copy_files': '/async/files/copy',
        'bulk_delete_files': '/async/files/delete',
        'get_file_move_job': '/async/files/move/{id}',
        'bulk_move_files': '/async/files/move',
    }

    id = StringField(read_only=True)
    type = StringField(read_only=True)
    state = StringField(read_only=True)
    result = BasicListField(read_only=True)
    total_files = IntegerField(read_only=True)
    failed_files = IntegerField(read_only=True)
    completed_files = IntegerField(read_only=True)
    started_on = DateTimeField(read_only=True)
    finished_on = DateTimeField(read_only=True)

    def __str__(self):
        return f'<AsyncJob: type={self.type} 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

    @classmethod
    def get_file_copy_job(cls, id, api=None):
        """
        Retrieve file copy async job
        :param id: Async job identifier
        :param api: Api instance
        :return:
        """
        id = Transform.to_async_job(id)

        api = api if api else cls._API
        async_job = api.get(
            url=cls._URL['get_file_copy_job'].format(id=id)
        ).json()
        return AsyncJob(api=api, **async_job)

    @classmethod
    def get_file_move_job(cls, id, api=None):
        """
        Retrieve file move async job
        :param id: Async job identifier
        :param api: Api instance
        :return:
        """
        id = Transform.to_async_job(id)

        api = api if api else cls._API
        async_job = api.get(
            url=cls._URL['get_file_move_job'].format(id=id)
        ).json()
        return AsyncJob(api=api, **async_job)

    @classmethod
    def get_file_delete_job(cls, id, api=None):
        """
        :param id: Async job identifier
        :param api: Api instance
        :return:
        """
        id = Transform.to_async_job(id)

        api = api if api else cls._API
        async_job = api.get(
            url=cls._URL['get_file_delete_job'].format(id=id)
        ).json()
        return AsyncJob(api=api, **async_job)

    def get_result(self, api=None):
        """
        Get async job result in bulk format
        :return: List of AsyncFileBulkRecord objects
        """
        api = api or self._API
        if not self.result:
            return []
        return AsyncFileBulkRecord.parse_records(
            result=self.result,
            api=api
        )

    @classmethod
    def list_file_jobs(cls, offset=None, limit=None, api=None):
        """Query ( List ) async jobs
        :param offset: Pagination offset
        :param limit: Pagination limit
        :param api: Api instance
        :return: Collection object
        """
        api = api or cls._API
        return super()._query(
            api=api,
            url=cls._URL['list_file_jobs'],
            offset=offset,
            limit=limit,
        )

    @classmethod
    def file_bulk_copy(cls, files, api=None):
        api = api or cls._API
        data = {'items': files}
        logger.info('Submitting async job for copying files in bulk')
        response = api.post(
            url=cls._URL['bulk_copy_files'], data=data
        ).json()
        return AsyncJob(api=api, **response)

    @classmethod
    def file_bulk_move(cls, files, api=None):
        api = api or cls._API
        data = {'items': files}
        logger.info('Submitting async job for moving files in bulk')
        response = api.post(
            url=cls._URL['bulk_move_files'], data=data
        ).json()
        return AsyncJob(api=api, **response)

    @classmethod
    def file_bulk_delete(cls, files, api=None):
        api = api or cls._API
        data = {'items': files}
        logger.info('Submitting async job for deleting files in bulk')
        response = api.post(
            url=cls._URL['bulk_delete_files'], data=data
        ).json()
        return AsyncJob(api=api, **response) class Endpoints(Resource):
    """
    Central resource for managing Endpoints.
    """
    _URL = {
        'get': '/'
    }

    rate_limit_url = HrefField(read_only=True)
    user_url = HrefField(read_only=True)
    users_url = HrefField(read_only=True)
    billing_url = HrefField(read_only=True)
    projects_url = HrefField(read_only=True)
    files_url = HrefField(read_only=True)
    tasks_url = HrefField(read_only=True)
    apps_url = HrefField(read_only=True)
    action_url = HrefField(read_only=True)
    upload_url = HrefField(read_only=True)

    @classmethod
    def get(cls, api=None, **kwargs):
        """
        Get api links.
        :param api: Api instance.
        :return: Endpoints object.
        """
        api = api if api else cls._API
        extra = {
            'resource': cls.__name__,
            'query': {}
        }
        logger.info('Getting resources', extra=extra)
        endpoints = api.get(url=cls._URL['get']).json()
        return Endpoints(api=api, **endpoints)

    def __str__(self):
        return '<Endpoints>' class RateLimit(Resource):
    """
    Rate limit resource contains info regarding request and computation
    rate limits.
    """

    _URL = {
        'get': '/rate_limit'
    }
    rate = CompoundField(Rate, read_only=True)
    instance_limit = CompoundField(Rate, read_only=True)

    @classmethod
    def get(cls, id=None, api=None):
        api = api if api else cls._API
        resource = api.get(url=cls._URL['get']).json()
        return cls(api=api, **resource) class Export(Resource):
    """
    Central resource for managing exports.
    """
    _URL = {
        'query': '/storage/exports',
        'get': '/storage/exports/{id}',

        'bulk_get': '/bulk/storage/exports/get',
        'bulk_create': '/bulk/storage/exports/create',
    }

    href = HrefField(read_only=True)
    id = StringField(read_only=True)
    state = StringField(read_only=True)
    _source = DictField(name='source', read_only=True)
    destination = CompoundField(VolumeFile, read_only=True)
    started_on = DateTimeField(read_only=True)
    finished_on = DateTimeField(read_only=True)
    overwrite = BooleanField(read_only=True)
    error = CompoundField(Error, read_only=True)
    _result = DictField(name='result', read_only=True)
    properties = CompoundField(VolumeProperties, read_only=True)

    def __str__(self):
        return f'<Export: 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

    @property
    def source(self):
        try:
            return File(id=self._source['file'], api=self._api)
        except TypeError:
            return None

    @property
    def result(self):
        try:
            return File(api=self._api, **self._result)
        except TypeError:
            return None

    @classmethod
    def submit_export(cls, file, volume, location, properties=None,
                      overwrite=False, copy_only=False, api=None):

        """
        Submit new export job.
        :param file: File to be exported.
        :param volume: Volume identifier.
        :param location: Volume location.
        :param properties: Properties dictionary.
        :param overwrite: If true it will overwrite file if exists
        :param copy_only: If true files are kept on SevenBridges bucket.
        :param api: Api Instance.
        :return: Export object.
        """
        data = {}
        params = {}

        volume = Transform.to_volume(volume)
        file = Transform.to_file(file)
        destination = {
            'volume': volume,
            'location': location
        }
        source = {
            'file': file
        }
        if properties:
            data['properties'] = properties

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

        extra = {
            'resource': cls.__name__,
            'query': data
        }
        logger.info('Submitting export', extra=extra)

        api = api if api else cls._API
        if copy_only:
            params['copy_only'] = True
            _export = api.post(
                cls._URL['query'], data=data, params=params).json()
        else:
            _export = api.post(
                cls._URL['query'], data=data).json()

        return Export(api=api, **_export)

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

        """
        Query (List) exports.
        :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 volume:
            volume = Transform.to_volume(volume)

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

    @classmethod
    def bulk_get(cls, exports, api=None):
        """
        Retrieve exports in bulk.
        :param exports: Exports to be retrieved.
        :param api: Api instance.
        :return: list of ExportBulkRecord objects.
        """
        api = api or cls._API
        export_ids = [Transform.to_export(export) for export in exports]
        data = {'export_ids': export_ids}

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

    @classmethod
    def bulk_submit(cls, exports, copy_only=False, api=None):
        """
        Create exports in bulk.
        :param exports: List of dicts describing a wanted export.
        :param copy_only: If true files are kept on SevenBridges bucket.
        :param api: Api instance.
        :return: list of ExportBulkRecord objects.
        """
        if not exports:
            raise SbgError('Exports are required')

        api = api or cls._API

        items = []
        for export in exports:
            file_ = Transform.to_file(export.get('file'))
            volume = Transform.to_volume(export.get('volume'))
            location = Transform.to_location(export.get('location'))
            properties = export.get('properties', {})
            overwrite = export.get('overwrite', False)

            item = {
                'source': {
                    'file': file_
                },
                'destination': {
                    'volume': volume,
                    'location': location
                },
                'properties': properties,
                'overwrite': overwrite
            }

            items.append(item)

        data = {'items': items}
        params = {'copy_only': copy_only}

        response = api.post(
            url=cls._URL['bulk_create'], params=params, data=data
        )
        return ExportBulkRecord.parse_records(response=response, api=api) class Import(Resource):
    """
    Central resource for managing imports.
    """
    _URL = {
        'query': '/storage/imports',
        'get': '/storage/imports/{id}',

        'bulk_get': '/bulk/storage/imports/get',
        'bulk_create': '/bulk/storage/imports/create',
    }

    href = HrefField(read_only=True)
    id = StringField(read_only=True)
    state = StringField(read_only=True)
    preserve_folder_structure = BooleanField(read_only=True)
    autorename = BooleanField(read_only=True)
    source = CompoundField(VolumeFile, read_only=True)
    destination = CompoundField(ImportDestination, read_only=True)
    started_on = DateTimeField(read_only=True)
    finished_on = DateTimeField(read_only=True)
    overwrite = BooleanField(read_only=True)
    error = CompoundField(Error, read_only=True)
    _result = DictField(name='result', read_only=True)

    def __str__(self):
        return f'<Import: 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

    @property
    def result(self):
        try:
            return File(api=self._api, **self._result)
        except TypeError:
            return None

    @classmethod
    def submit_import(cls, volume, location, project=None, name=None,
                      overwrite=False, properties=None, parent=None,
                      preserve_folder_structure=True, autorename=False,
                      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')

        api = api or cls._API

        items = []
        for import_ in imports:
            project = import_.get('project')
            parent = import_.get('parent')

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

            volume = Transform.to_volume(import_.get('volume'))
            location = Transform.to_location(import_.get('location'))
            name = import_.get('name', None)
            overwrite = import_.get('overwrite', False)
            autorename = import_.get('autorename', None)
            preserve_folder_structure = import_.get(
                'preserve_folder_structure', None
            )

            if name:
                destination['name'] = name

            import_config = {
                'source': {
                    'volume': volume,
                    'location': location
                },
                'destination': destination,
                'overwrite': overwrite,
            }
            if autorename is not None:
                import_config['autorename'] = autorename
            if preserve_folder_structure is not None:
                import_config['preserve_folder_structure'] = (
                    preserve_folder_structure
                )
            items.append(import_config)

        data = {'items': items}
        response = api.post(url=cls._URL['bulk_create'], data=data)
        return ImportBulkRecord.parse_records(response=response, api=api) class DRSImportBulk(Resource):
    """
    Central resource for managing DRS imports.
    """
    _URL = {
        'get': '/bulk/drs/imports/{id}',
        'create': '/bulk/drs/imports/create',
    }

    id = StringField(read_only=True)
    href = HrefField(read_only=True)
    result = CompoundListField(FileImportResult, read_only=True)
    _result_files = []  # cache for result_files property
    state = StringField(read_only=True)
    started_on = DateTimeField(read_only=True)
    finished_on = DateTimeField(read_only=True)

    def __str__(self):
        return f'<DRSBulkImport: 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

    @property
    def result_files(self):
        """
        Retrieve files that were successfully imported.
        :return: List of File objects
        """
        try:
            cached_file_ids = set([
                file.resource.id for file in self._result_files
            ])

            imported_file_ids = set([
                file.resource.id
                for file in self.result if file.resource
            ])
            file_ids_to_retrieve = imported_file_ids - cached_file_ids
            if file_ids_to_retrieve:
                files = File.bulk_get(
                    files=file_ids_to_retrieve, api=self._api
                )
                self._result_files.extend(files)
            return self._result_files if self._result_files else None
        except TypeError:
            return None

    @classmethod
    def bulk_get(cls, import_job_id, api=None):
        """
        Retrieve DRS bulk import details
        :param import_job_id: Import id to be retrieved.
        :param api: Api instance.
        :return: DRSImportBulk object.
        """
        api = api or cls._API

        if not import_job_id:
            raise SbgError('DRS import is required!')
        elif not isinstance(import_job_id, str):
            raise SbgError('Invalid DRS import parameter!')

        response = api.get(
            url=cls._URL['get'].format(id=import_job_id)
        ).json()
        return DRSImportBulk(api=api, **response)

    @classmethod
    def bulk_submit(
            cls, imports, tags=None, conflict_resolution='SKIP', api=None
    ):
        """
        Submit DRS bulk import
        :param imports:  List of dicts describing a wanted import.
        :param tags: list of tags to be applied.
        :param conflict_resolution: Type of file naming conflict resolution.
        :param api: Api instance.
        :return: DRSImportBulk object.
        """
        if not imports:
            raise SbgError('Imports are required')

        api = api or cls._API

        items = []
        for import_ in imports:
            project = import_.get('project')
            parent = import_.get('parent')

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

            items.append(import_)

        data = {
            'conflict_resolution': conflict_resolution,
            'tags': tags,
            'items': items
        }
        response = api.post(url=cls._URL['create'], data=data).json()
        return DRSImportBulk(api=api, **response) class RequestParameters:
    MAX_URL_LENGTH = 6000
    DEFAULT_TIMEOUT = 300
    DEFAULT_RETRY_COUNT = 6
    DEFAULT_BACKOFF_FACTOR = 1
    DEFAULT_BULK_LIMIT = 100 class BillingGroup(Resource):
    """
    Central resource for managing billing groups.
    """
    _URL = {
        'query': '/billing/groups',
        'get': '/billing/groups/{id}'
    }
    href = HrefField(read_only=True)
    id = UuidField(read_only=True)
    owner = StringField(read_only=True)
    name = StringField(read_only=True)
    type = StringField(read_only=True)
    pending = BooleanField(read_only=True)
    disabled = BooleanField(read_only=False)
    balance = CompoundField(Price, read_only=True)

    def __str__(self):
        return f'<BillingGroup: 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

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

    def analysis_breakdown(self, date_from=None, date_to=None, invoice_id=None,
                           fields=None, offset=0, limit=50):
        """
        Get Billing group analysis breakdown for the current billing group.
        """
        return BillingGroupAnalysisBreakdown.query(
            self.id, self._api, date_from, date_to, invoice_id, fields,
            offset, limit
        )

    def storage_breakdown(self, date_from=None, date_to=None, invoice_id=None,
                          fields=None, offset=0, limit=50):
        """
        Get Billing group storage breakdown for the current billing group.
        """
        return BillingGroupStorageBreakdown.query(
            self.id, self._api, date_from, date_to, invoice_id, fields,
            offset, limit
        )

    def egress_breakdown(self, date_from=None, date_to=None, invoice_id=None,
                         fields=None, offset=0, limit=50):
        """
        Get Billing group egress breakdown for the current billing group.
        """
        return BillingGroupEgressBreakdown.query(
            self.id, self._api, date_from, date_to, invoice_id, fields,
            offset, limit
        ) class Automation(Resource):
    """
    Central resource for managing automations.
    """
    # noinspection PyProtectedMember
    _URL = {
        'query': '/automation/automations',
        'get': '/automation/automations/{id}',
        'member': AutomationMember._URL['get'],
        'members': AutomationMember._URL['query'],
        'packages': AutomationPackage._URL['query'],
        'archive': '/automation/automations/{automation_id}/actions/archive',
        'restore': '/automation/automations/{automation_id}/actions/restore'
    }

    href = HrefField(read_only=True)
    id = UuidField(read_only=True)
    name = StringField(read_only=False)
    description = StringField(read_only=False)
    billing_group = UuidField(read_only=False)
    owner = StringField(read_only=True)
    created_by = StringField(read_only=True)
    created_on = DateTimeField(read_only=True)
    modified_by = StringField(read_only=True)
    modified_on = DateTimeField(read_only=False)
    archived = BooleanField(read_only=True)
    secret_settings = DictField(read_only=False)
    memory_limit = IntegerField(read_only=False)
    project_based = 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'<Automation: id={self.id} name={self.name}>'

    @classmethod
    def query(
            cls, name=None, include_archived=False, project_based=None,
            offset=None, limit=None, api=None
    ):
        """
        Query (List) automations.
        :param name: Automation name.
        :param include_archived: Include archived automations also
        :param project_based: Search project based automations
        :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'],
            name=name,
            include_archived=include_archived,
            project_based=project_based,
            offset=offset,
            limit=limit,
            api=api,
        )

    @classmethod
    def create(cls, name, description=None, billing_group=None,
               secret_settings=None, project_based=None, memory_limit=None,
               api=None):
        """
        Create a automation template.
        :param name:  Automation name.
        :param billing_group: Automation billing group.
        :param description:  Automation description.
        :param secret_settings: Automation settings.
        :param project_based: Create project based automation template.
        :param memory_limit: Memory limit in MB.
        :param api: Api instance.
        :return:
        """
        api = api if api else cls._API

        if name is None:
            raise SbgError('Automation name is required!')

        data = {
            'name': name,
        }

        if billing_group:
            data['billing_group'] = Transform.to_billing_group(billing_group)

        if description:
            data['description'] = description
        if secret_settings:
            data['secret_settings'] = secret_settings
        if project_based:
            data['project_based'] = project_based
        if memory_limit:
            data['memory_limit'] = memory_limit

        extra = {
            'resource': cls.__name__,
            'query': data
        }
        logger.info('Creating automation template', extra=extra)
        automation_data = api.post(url=cls._URL['query'], data=data).json()
        return Automation(api=api, **automation_data)

    @inplace_reload
    def save(self, inplace=True):
        """
        Saves all modification to the automation template on the server.
        :param inplace Apply edits on the current instance or get a new one.
        :return: Automation instance.
        """
        modified_data = self._modified_data()
        if modified_data:
            extra = {
                'resource': type(self).__name__,
                'query': {
                    'id': self.id,
                    'modified_data': modified_data
                }
            }
            logger.info('Saving automation template', extra=extra)
            data = self._api.patch(url=self._URL['get'].format(id=self.id),
                                   data=modified_data).json()
            return Automation(api=self._api, **data)

        else:
            raise ResourceNotModified()

    @inplace_reload
    def archive(self):
        """
        Archive automation
        :return: Automation instance.
        """
        extra = {
            'resource': type(self).__name__,
            'query': {
                'id': self.id,
            }
        }
        logger.info('Archive automation', extra=extra)

        automation_data = self._api.post(
            url=self._URL['archive'].format(automation_id=self.id)
        ).json()
        return Automation(api=self._api, **automation_data)

    @inplace_reload
    def restore(self):
        """
        Restore archived automation
        :return: Automation instance.
        """
        extra = {
            'resource': type(self).__name__,
            'query': {
                'id': self.id,
            }
        }
        logger.info('Restore archived automation', extra=extra)

        automation_data = self._api.post(
            url=self._URL['restore'].format(automation_id=self.id)
        ).json()
        return Automation(api=self._api, **automation_data)

    def get_packages(self, offset=None, limit=None, api=None):
        """
        Return list of packages that belong to this automation
        :param offset: Pagination offset.
        :param limit: Pagination limit.
        :param api: sevenbridges Api instance.
        :return: AutomationPackage collection
        """
        api = api or self._API
        return AutomationPackage.query(
            automation=self.id, offset=offset, limit=limit, api=api
        )

    @classmethod
    def get_package(cls, package, api=None):
        """
        Return specified automation member
        :param package: Automation Package Id
        :param api: sevenbridges Api instance.
        :return: AutomationMember object
        """
        package_id = Transform.to_automation_package(package)
        api = api or cls._API
        return AutomationPackage.get(
            id=package_id, api=api
        )

    def add_package(
            self, version, file_path, schema, file_name=None,
            retry_count=RequestParameters.DEFAULT_RETRY_COUNT,
            timeout=RequestParameters.DEFAULT_TIMEOUT, part_size=None,
            api=None
    ):
        """
        Add a code package to automation template.
        :param version: The code package version.
        :param file_path: Path to the code package file to be uploaded.
        :param schema: IO schema for main step of execution.
        :param part_size: Size of upload part in bytes.
        :param file_name: Optional file name.
        :param retry_count: Upload retry count.
        :param timeout: Timeout for s3/google session.
        :param api: sevenbridges Api instance.
        :return: AutomationPackage
        """
        api = api or self._API
        if version is None:
            raise SbgError('Code package version is required!')

        if file_path is None:
            raise SbgError('Code package file path is required!')

        # Multipart upload the code package:
        upload = CodePackageUpload(
            file_path,
            self.id,
            api=api,
            part_size=part_size,
            file_name=file_name,
            retry_count=retry_count,
            timeout=timeout
        )
        upload.start()
        upload.wait()
        package_file = upload.result()

        # Create the automation package:
        return AutomationPackage.create(
            self.id,
            version=version,
            location=package_file.id,
            schema=schema,
            api=api
        )

    def get_member(self, username, api=None):
        """
        Return specified automation member
        :param username: Member username
        :param api: sevenbridges Api instance.
        :return: AutomationMember object
        """
        member = Transform.to_automation_member(username)
        api = api or self._API
        return AutomationMember.get(
            id=member, automation=self.id, api=api
        )

    def get_members(self, offset=None, limit=None, api=None):
        """
        Return list of automation members
        :param offset: Pagination offset.
        :param limit: Pagination limit.
        :param api: sevenbridges Api instance.
        :return: AutomationMember collection
        """
        api = api or self._API
        return AutomationMember.query(
            automation=self.id, offset=offset, limit=limit, api=api
        )

    def add_member(self, user, permissions, api=None):
        """
        Add member to the automation
        :param user: Member username
        :param permissions: Member permissions
        :param api: sevenbridges Api instance
        :return: AutomationMember object
        """
        api = api or self._API
        return AutomationMember.add(
            automation=self.id, user=user, permissions=permissions, api=api
        )

    def remove_member(self, user, api=None):
        """
        Remove a member from the automation
        :param user: Member username
        :param api: sevenbridges Api instance
        :return: None
        """
        api = api or self._API
        AutomationMember.remove(automation=self.id, user=user, api=api)

    def get_runs(self, package=None, status=None, name=None,
                 created_by=None, created_from=None, created_to=None,
                 project_id=None, order_by=None, order=None, offset=None,
                 limit=None, api=None):
        """
        Query automation runs that belong to this automation
        :param package: Package id
        :param status: Run status
        :param name: Automation run name
        :param created_by: Username of member that created the run
        :param created_from: Date the run was created after
        :param created_to: Date the run was created before
        :param project_id: Search runs by project id, if run is project based
        :param order_by: Property by which to order results
        :param order: Ascending or Descending ("asc" or "desc")
        :param offset: Pagination offset.
        :param limit: Pagination limit.
        :param api: sevenbridges Api instance
        :return: AutomationRun collection
        """
        api = api or self._API
        return AutomationRun.query(
            automation=self.id, package=package, status=status, name=name,
            created_by=created_by, created_from=created_from,
            created_to=created_to, project_id=project_id, order_by=order_by,
            order=order, offset=offset, limit=limit, api=api
        ) class AutomationRun(Resource):
    """
    Central resource for managing automation runs.
    """

    _URL = {
        'query': '/automation/runs',
        'get': '/automation/runs/{id}',
        'actions': '/automation/runs/{id}/actions/{action}',
        'state': '/automation/runs/{id}/state',
    }

    href = HrefField(read_only=True)
    id = StringField(read_only=True)
    name = StringField(read_only=False)
    automation = CompoundField(Automation, read_only=True)
    package = CompoundField(AutomationPackage, read_only=True)
    inputs = DictField(read_only=False)
    outputs = DictField(read_only=True)
    settings = DictField(read_only=False)
    created_on = DateTimeField(read_only=True)
    start_time = DateTimeField(read_only=True)
    end_time = DateTimeField(read_only=True)
    resumed_from = StringField(read_only=True)
    created_by = StringField(read_only=True)
    status = StringField(read_only=True)
    message = StringField(read_only=True)
    execution_details = DictField(read_only=True)
    memory_limit = IntegerField(read_only=False)
    project_id = StringField(read_only=True)

    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'<AutomationRun: id={self.id}>'

    @classmethod
    def query(cls, automation=None, package=None, status=None, name=None,
              created_by=None, created_from=None, created_to=None,
              project_id=None, order_by=None, order=None, offset=None,
              limit=None, api=None):
        """
        Query (List) automation runs.
        :param name: Automation run name
        :param automation: Automation template
        :param package: Package
        :param status: Run status
        :param created_by: Username of user that created the run
        :param order_by: Property by which to order results
        :param order: Ascending or descending ("asc" or "desc")
        :param created_from: Date the run is created after
        :param created_to: Date the run is created before
        :param project_id: Id of project if Automation run is project based
        :param offset: Pagination offset.
        :param limit: Pagination limit.
        :param api: Api instance.
        :return: collection object
        """
        if automation:
            automation = Transform.to_automation(automation)

        if package:
            package = Transform.to_automation_package(package)

        api = api or cls._API
        return super()._query(
            url=cls._URL['query'],
            name=name,
            automation_id=automation,
            package_id=package,
            status=status,
            created_by=created_by,
            created_from=created_from,
            created_to=created_to,
            project_id=project_id,
            order_by=order_by,
            order=order,
            offset=offset,
            limit=limit,
            api=api,
        )

    @classmethod
    def create(cls, package, inputs=None, settings=None, resume_from=None,
               name=None, secret_settings=None, memory_limit=None, api=None):
        """
        Create and start a new run.
        :param package: Automation package id
        :param inputs: Input dictionary
        :param settings: Settings override dictionary
        :param resume_from: Run to resume from
        :param name: Automation run name
        :param secret_settings: dict to override secret_settings from
        automation template
        :param memory_limit: Memory limit in MB.
        :param api: sevenbridges Api instance
        :return: AutomationRun object
        """
        package = Transform.to_automation_package(package)

        data = {'package': package}
        if inputs:
            data['inputs'] = inputs
        else:
            data['inputs'] = dict()
        if settings:
            data['settings'] = settings
        if resume_from:
            data['resume_from'] = resume_from
        if name:
            data['name'] = name
        if secret_settings:
            data['secret_settings'] = secret_settings
        if memory_limit:
            data['memory_limit'] = memory_limit

        api = api or cls._API
        automation_run = api.post(
            url=cls._URL['query'],
            data=data,
        ).json()
        return AutomationRun(api=api, **automation_run)

    @inplace_reload
    def save(self, inplace=True):
        """
        Saves all modification to the automation run on the server.
        :param inplace Apply edits on the current instance or get a new one.
        :return: Automation run instance.
        """
        modified_data = self._modified_data()
        if modified_data:
            extra = {
                'resource': type(self).__name__,
                'query': {
                    'id': self.id,
                    'modified_data': modified_data
                }
            }
            logger.info('Saving automation run', extra=extra)
            data = self._api.patch(url=self._URL['get'].format(id=self.id),
                                   data=modified_data).json()
            return AutomationRun(api=self._api, **data)
        else:
            raise ResourceNotModified()

    @classmethod
    def rerun(
        cls, id, package=None, inputs=None, settings=None, resume_from=None,
        name=None, secret_settings=None, merge=True, api=None
    ):
        """
        Create and start rerun of existing automation.
        :param id: Automation id to rerun
        :param package: Automation package id
        :param inputs: Input dictionary
        :param settings: Settings override dictionary
        :param resume_from: Run to resume from
        :param name: Automation run name
        :param secret_settings: dict to override secret_settings from
        automation template
        :param merge: merge settings and inputs of run
        :param api: sevenbridges Api instance
        :return: AutomationRun object
        """
        data = {'merge': merge}
        if package:
            data['package'] = package
        if inputs:
            data['inputs'] = inputs
        if settings:
            data['settings'] = settings
        if resume_from:
            data['resume_from'] = resume_from
        if name:
            data['name'] = name
        if secret_settings:
            data['secret_settings'] = secret_settings

        api = api or cls._API
        automation_run = api.post(
            url=cls._URL['actions'].format(
                id=id, action=AutomationRunActions.RERUN
            )
        ).json()
        return AutomationRun(api=api, **automation_run)

    def stop(self, api=None):
        """
        Stop automation run.
        :param api: sevenbridges Api instance.
        :return: AutomationRun object
        """
        api = api or self._API

        return api.post(
            url=self._URL['actions'].format(
                id=self.id, action=AutomationRunActions.STOP
            )
        ).content

    def get_log_file(self, api=None):
        """
        Retrieve automation run log.
        :param api: sevenbridges Api instance
        :return: Log string
        """
        api = api or self._API
        log_file_data = self.execution_details.get('log_file')
        return File(api=api, **log_file_data) if log_file_data else None

    def get_state(self, api=None):
        """
        Retrieve automation run state.
        :param api: sevenbridges Api instance
        :return: State file json contents as string
        """
        api = api or self._API
        return api.get(self._URL['state'].format(id=self.id)).json() class AutomationPackage(Resource):
    """
    Central resource for managing automation packages.
    """
    _URL = {
        'query': '/automation/automations/{automation_id}/packages',
        'get': '/automation/packages/{id}',
        'archive': "/automation/automations/{automation_id}"
                   "/packages/{id}/actions/archive",
        'restore': "/automation/automations/{automation_id}"
                   "/packages/{id}/actions/restore",
    }

    id = StringField(read_only=True)
    automation = UuidField(read_only=True)
    version = StringField(read_only=True)
    location = StringField(read_only=True)
    schema = DictField(read_only=True)
    created_by = StringField(read_only=True)
    created_on = DateTimeField(read_only=True)
    archived = BooleanField(read_only=True)
    custom_url = StringField(read_only=False)
    memory_limit = IntegerField(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'<AutomationPackage: id={self.id}>'

    @classmethod
    def query(cls, automation, offset=None, limit=None, api=None):
        """
        Query (List) automation packages.
        :param automation: Automation id.
        :param offset: Pagination offset.
        :param limit: Pagination limit.
        :param api: Api instance.
        :return: collection object
        """
        automation_id = Transform.to_automation(automation)

        api = api or cls._API
        return super()._query(
            url=cls._URL['query'].format(automation_id=automation_id),
            offset=offset,
            limit=limit,
            api=api,
        )

    @classmethod
    def create(cls, automation, version, location, schema, memory_limit=None,
               api=None):
        """
        Create a code package.
        :param automation: Automation id.
        :param version: File ID of the uploaded code package.
        :param location: The code package version.
        :param schema: IO schema for main step of execution.
        :param memory_limit: Memory limit in MB.
        :param api: Api instance.
        :return:
        """
        automation_id = Transform.to_automation(automation)

        api = api if api else cls._API

        if version is None:
            raise SbgError('Code package version is required!')

        if location is None:
            raise SbgError('Code package location is required!')

        if schema is None:
            raise SbgError('Schema is required!')

        data = {
            'version': version,
            'location': location,
            'schema': schema,
            'memory_limit': memory_limit,
        }

        extra = {
            'resource': cls.__name__,
            'query': data
        }

        package_data = api.post(
            cls._URL['query'].format(automation_id=automation_id), data=data
        ).json()
        logger.info(
            'Add code package to automation with id %s',
            automation_id, extra=extra
        )
        return AutomationPackage(api=api, **package_data)

    @inplace_reload
    def archive(self):
        """
        Archive package
        :return: AutomationPackage object.
        """
        automation_id = Transform.to_automation(self.automation)

        extra = {
            'resource': type(self).__name__,
            'query': {
                'id': self.id,
            }
        }
        logger.info('Archive automation package', extra=extra)

        package_data = self._api.post(
            url=self._URL['archive'].format(
                automation_id=automation_id, id=self.id
            )
        ).json()
        return AutomationPackage(api=self._api, **package_data)

    @inplace_reload
    def restore(self):
        """
        Restore archived package
        :return: AutomationPackage object.
        """
        automation_id = Transform.to_automation(self.automation)
        extra = {
            'resource': type(self).__name__,
            'query': {
                'id': self.id,
            }
        }
        logger.info('Restore archived automation package', extra=extra)

        package_data = self._api.post(
            url=self._URL['restore'].format(
                automation_id=automation_id, id=self.id
            )
        ).json()
        return AutomationPackage(api=self._api, **package_data)

    @inplace_reload
    def save(self, inplace=True):
        """
        Saves all modification to the automation package on the server.
        :param inplace Apply edits on the current instance or get a new one.
        :return: AutomationPackage instance.
        """
        modified_data = self._modified_data()
        if modified_data:
            extra = {
                'resource': type(self).__name__,
                'query': {
                    'id': self.id,
                    'modified_data': modified_data
                }
            }
            logger.info('Saving automation package', extra=extra)
            data = self._api.patch(url=self._URL['get'].format(id=self.id),
                                   data=modified_data).json()
            return AutomationPackage(api=self._api, **data)

        else:
            raise ResourceNotModified()
from concurrent.futures import ThreadPoolExecutor
from requests.adapters import DEFAULT_POOLSIZE
from sevenbridges.errors import SbgError
from sevenbridges.http.client import HttpClient
from sevenbridges.models.app import App
from sevenbridges.models.file import File
from sevenbridges.models.task import Task
from sevenbridges.models.team import Team
from sevenbridges.models.user import User
from sevenbridges.models.marker import Marker
from sevenbridges.models.volume import Volume
from sevenbridges.models.actions import Actions
from sevenbridges.models.dataset import Dataset
from sevenbridges.models.invoice import Invoice
from sevenbridges.models.project import Project
from sevenbridges.models.division import Division
from sevenbridges.models.async_jobs import AsyncJob
from sevenbridges.models.endpoints import Endpoints
from sevenbridges.models.rate_limit import RateLimit
from sevenbridges.models.storage_export import Export
from sevenbridges.models.storage_import import Import
from sevenbridges.models.drs_import import DRSImportBulk
from sevenbridges.models.enums import RequestParameters
from sevenbridges.models.billing_group import BillingGroup
from sevenbridges.models.automation import (
    Automation, AutomationRun, AutomationPackage
)





class Api(HttpClient):
    """Api aggregates all resource classes into single place"""

    actions = Actions
    apps = App
    async_jobs = AsyncJob
    automations = Automation
    automation_runs = AutomationRun
    automation_packages = AutomationPackage
    billing_groups = BillingGroup
    datasets = Dataset
    divisions = Division
    drs_imports = DRSImportBulk
    endpoints = Endpoints
    exports = Export
    files = File
    imports = Import
    invoices = Invoice
    markers = Marker
    projects = Project
    rate_limit = RateLimit
    tasks = Task
    teams = Team
    users = User

    volumes = Volume