from xml.etree.ElementTree import Element, SubElement, tostring

from defusedxml.ElementTree import fromstring
from moneyed import Money
import requests
from structlog import get_logger

from .xml_helpers import money_to_amount_and_currency, parse_money
from ..config import datatrans_authorize_url, mpo_merchant_id, sign_mpo
from ..models import AliasRegistration, Payment

logger = get_logger()

def pay_with_alias(amount: Money, alias_registration_id: str, client_ref: str) -> Payment:
    """
    Charges money using datatrans, given a previously registered credit card alias.

    :param amount: The amount and currency we want to charge
    :param alias_registration_id: The alias registration to use
    :param client_ref: A unique reference for this charge
    :return: a Payment (either successful or not)
    """
    if amount.amount <= 0:
        raise ValueError('Pay with alias takes a strictly positive amount')

    alias_registration = AliasRegistration.objects.get(pk=alias_registration_id)

    logger.info('paying-with-alias', amount=amount, client_ref=client_ref,
                alias_registration=alias_registration)

    request_xml = build_pay_with_alias_request_xml(amount, client_ref, alias_registration)

    logger.info('sending-pay-with-alias-request', url=datatrans_authorize_url, data=request_xml)

    response = requests.post(
        url=datatrans_authorize_url,
        headers={'Content-Type': 'application/xml'},
        data=request_xml)

    logger.info('processing-pay-with-alias-response', response=response.content)

    charge_response = parse_pay_with_alias_response_xml(response.content)
    charge_response.save()
    charge_response.send_signal()

    return charge_response

def build_pay_with_alias_request_xml(amount: Money, client_ref: str, alias_registration: AliasRegistration) -> bytes:
    merchant_id = mpo_merchant_id
    client_ref = client_ref

    root = Element('authorizationService')
    root.set('version', '3')

    body = SubElement(root, 'body')
    body.set('merchantId', merchant_id)

    transaction = SubElement(body, 'transaction')
    transaction.set('refno', client_ref)

    request = SubElement(transaction, 'request')

    amount, currency = money_to_amount_and_currency(amount)
    SubElement(request, 'amount').text = str(amount)
    SubElement(request, 'currency').text = currency
    SubElement(request, 'aliasCC').text = alias_registration.card_alias
    SubElement(request, 'expm').text = str(alias_registration.expiry_month)
    SubElement(request, 'expy').text = str(alias_registration.expiry_year)
    SubElement(request, 'reqtype').text = 'CAA'
    SubElement(request, 'sign').text = sign_mpo(merchant_id, amount, currency, client_ref)

    return tostring(root, encoding='utf8')

def parse_pay_with_alias_response_xml(xml: bytes) -> Payment:
    body = fromstring(xml).find('body')
    status = body.get('status')
    transaction = body.find('transaction')
    trx_status = transaction.get('trxStatus')
    request = transaction.find('request')

    common_attributes = dict(
        merchant_id=body.get('merchantId'),
        card_alias=request.find('aliasCC').text,
        expiry_month=int(request.find('expm').text),
        expiry_year=int(request.find('expy').text),
        request_type=request.find('reqtype').text,
        client_ref=transaction.get('refno'),
        amount=parse_money(request)
    )

    if status == 'accepted' and trx_status == 'response':
        response = transaction.find('response')

        transaction_id = response.find('uppTransactionId').text
        masked_card_number = response.find('maskedCC').text
        return_customer_country = response.find('returnCustomerCountry').text

        response_code = response.find('responseCode').text
        response_message = response.find('responseMessage').text
        authorization_code = response.find('authorizationCode').text
        acquirer_authorization_code = response.find('acqAuthorizationCode').text

        d = dict(
            success=True,
            transaction_id=transaction_id,
            masked_card_number=masked_card_number,
            credit_card_country=return_customer_country,
            response_code=response_code,
            response_message=response_message,
            authorization_code=authorization_code,
            acquirer_authorization_code=acquirer_authorization_code,
        )
        d.update(common_attributes)
    else:
        error = transaction.find('error')

        d = dict(
            success=False,
            error_code=error.find('errorCode').text,
            error_message=error.find('errorMessage').text,
            error_detail=error.find('errorDetail').text,
            acquirer_error_code=text_or_else(error.find('acqErrorCode')),
            transaction_id=error.find('uppTransactionId').text,
            credit_card_country=text_or_else(error.find('returnCustomerCountry')),
        )
        d.update(**common_attributes)
    return Payment(**d)

def text_or_else(element, else_value='') -> str:
    if element is not None:
        return element.text
    else:
        return else_value


from defusedxml.ElementTree import fromstring
from structlog import get_logger
from typing import Union

from .xml_helpers import parse_money
from ..config import sign_web
from ..models import AliasRegistration, Payment

logger = get_logger()

def handle_notification(xml: str) -> None:
    notification = parse_notification_xml(xml)
    logger.debug('processing-notification', notification=notification)
    notification.save()
    notification.send_signal()

def parse_notification_xml(xml: str) -> Union[AliasRegistration, Payment]:
    """"
    Both alias registration and payments are received here.
    We can differentiate them by looking at the use-alias user-parameter (and verifying the amount is o).

    """
    body = fromstring(xml).find('body')
    transaction = body.find('transaction')
    _user_parameters = transaction.find('userParameters')

    def get_named_parameter(name):
        return _user_parameters.find("parameter[@name='" + name + "']")

    def success():
        return transaction.get('status') == 'success'

    def parse_success():
        # From the spec: sign2 is only returned in the success case
        computed_signature = sign_web(body.get('merchantId'), transaction.find('amount').text,
                                      transaction.find('currency').text,
                                      transaction.find('uppTransactionId').text)

        sign2 = get_named_parameter('sign2').text
        if computed_signature != sign2:
            raise ValueError('sign2 did not match computed signature')

        success = transaction.find('success')
        d = dict(
            response_code=success.find('responseCode').text,
            response_message=success.find('responseMessage').text,
            authorization_code=success.find('authorizationCode').text,
            acquirer_authorization_code=success.find('acqAuthorizationCode').text,
        )
        return {k: v for k, v in d.items() if v is not None}

    def parse_error():
        error = transaction.find('error')
        d = dict(
            error_code=error.find('errorCode').text,
            error_message=error.find('errorMessage').text,
            error_detail=error.find('errorDetail').text)

        acquirer_error_code = get_named_parameter('acqErrorCode')
        if acquirer_error_code is not None:
            d['acquirer_error_code'] = acquirer_error_code.text

        return {k: v for k, v in d.items() if v is not None}

    def parse_common_attributes():
        d = dict(
            transaction_id=transaction.find('uppTransactionId').text,
            merchant_id=body.get('merchantId'),
            client_ref=transaction.get('refno'),
            amount=parse_money(transaction))

        payment_method = transaction.find('pmethod')
        if payment_method is not None:
            d['payment_method'] = payment_method.text

        request_type = transaction.find('reqtype')
        if request_type is not None:
            d['request_type'] = request_type.text

        credit_card_country = get_named_parameter('returnCustomerCountry')
        if credit_card_country is not None:
            d['credit_card_country'] = credit_card_country.text

        expiry_month = get_named_parameter('expm')
        if expiry_month is not None:
            d['expiry_month'] = int(expiry_month.text)

        expiry_year = get_named_parameter('expy')
        if expiry_year is not None:
            d['expiry_year'] = int(expiry_year.text)

        return d

    # End of inner helper functions, we're back inside parse_notification_xml

    use_alias_parameter = get_named_parameter('useAlias')
    if use_alias_parameter is not None and use_alias_parameter.text == 'true':
        # It's an alias registration

        d = dict(parse_common_attributes())

        masked_card_number = get_named_parameter('maskedCC')
        if masked_card_number is not None:
            d['masked_card_number'] = masked_card_number.text

        card_alias = get_named_parameter('aliasCC')
        if card_alias is not None:
            d['card_alias'] = card_alias.text

        if success():
            d['success'] = True
            d.update(parse_success())
        else:
            d['success'] = False
            d.update(parse_error())

        return AliasRegistration(**d)
    else:
        # It's a payment or a charge
        if success():
            d = dict(success=True)
            cardno = get_named_parameter('cardno')
            if cardno is not None:
                d['masked_card_number'] = cardno.text
            d.update(parse_common_attributes())
            d.update(parse_success())
            return Payment(**d)
        else:
            d = dict(success=False)
            d.update(parse_common_attributes())
            d.update(parse_error())
            return Payment(**d)


# Copyright Collab 2015-2016
# See LICENSE for details.

"""
`django-admin-footer` application.
"""

from __future__ import unicode_literals

#: Application version.
__version__ = (1, 0, 1)

def short_version(version=None):
    """
    Return short application version. For example: `1.0.0`.
    """
    v = version or __version__
    return '.'.join([str(x) for x in v[:3]])

def get_version(version=None):
    """
    Return full version nr, inc. rc, beta etc tags.

    For example: `2.0.0a1`
    :rtype: str
    """
    v = version or __version__
    if len(v) == 4:
        return '{0}{1}'.format(short_version(v), v[3])

    return short_version(v)

#: Full version number.
version = get_version()


from xml.etree.ElementTree import Element, SubElement, tostring

import requests
from defusedxml.ElementTree import fromstring
from moneyed import Money
from structlog import get_logger

from .xml_helpers import money_to_amount_and_currency, parse_money
from ..config import datatrans_processor_url, sign_web
from ..models import Payment, Refund

logger = get_logger()

def refund(amount: Money, payment_id: str) -> Refund:
    """
    Refunds (partially or completely) a previously authorized and settled payment.
    :param amount: The amount and currency we want to refund. Must be positive, in the same currency
    as the original payment, and not exceed the amount of the original payment.
    :param payment_id: The id of the payment to refund.
    :return: a Refund (either successful or not).
    """
    if amount.amount <= 0:
        raise ValueError('Refund takes a strictly positive amount')
    payment = Payment.objects.get(pk=payment_id)
    if not payment.success:
        raise ValueError('Only successful payments can be refunded')
    if payment.amount.currency != amount.currency:
        raise ValueError('Refund currency must be identical to original payment currency')
    if amount.amount > payment.amount.amount:
        raise ValueError('Refund amount exceeds original payment amount')

    logger.info('refunding-payment', amount=str(amount),
                payment=dict(amount=str(payment.amount), transaction_id=payment.transaction_id,
                             masked_card_number=payment.masked_card_number))

    client_ref = '{}-r'.format(payment.client_ref)

    request_xml = build_refund_request_xml(amount=amount,
                                           original_transaction_id=payment.transaction_id,
                                           client_ref=client_ref,
                                           merchant_id=payment.merchant_id)

    logger.info('sending-refund-request', url=datatrans_processor_url, data=request_xml)

    response = requests.post(
        url=datatrans_processor_url,
        headers={'Content-Type': 'application/xml'},
        data=request_xml)

    logger.info('processing-refund-response', response=response.content)

    refund_response = parse_refund_response_xml(response.content)
    refund_response.save()
    refund_response.send_signal()

    return refund_response

def build_refund_request_xml(amount: Money, client_ref: str, original_transaction_id: str, merchant_id: str) -> bytes:
    client_ref = client_ref

    root = Element('paymentService')
    root.set('version', '1')

    body = SubElement(root, 'body')
    body.set('merchantId', merchant_id)

    transaction = SubElement(body, 'transaction')
    transaction.set('refno', client_ref)

    request = SubElement(transaction, 'request')

    amount, currency = money_to_amount_and_currency(amount)
    SubElement(request, 'amount').text = str(amount)
    SubElement(request, 'currency').text = currency
    SubElement(request, 'uppTransactionId').text = original_transaction_id
    SubElement(request, 'transtype').text = '06'
    SubElement(request, 'sign').text = sign_web(merchant_id, amount, currency, client_ref)

    return tostring(root, encoding='utf8')

def parse_refund_response_xml(xml: bytes) -> Refund:
    body = fromstring(xml).find('body')
    status = body.get('status')
    transaction = body.find('transaction')
    trx_status = transaction.get('trxStatus')
    request = transaction.find('request')

    common_attributes = dict(
        merchant_id=body.get('merchantId'),
        client_ref=transaction.get('refno'),
        amount=parse_money(request),
        payment_transaction_id=request.find('uppTransactionId').text,
    )

    request_type = request.find('reqtype')
    if request_type is not None:
        common_attributes['request_type'] = request_type.text

    if status == 'accepted' and trx_status == 'response':
        response = transaction.find('response')

        transaction_id = response.find('uppTransactionId').text
        response_code = response.find('responseCode').text
        response_message = response.find('responseMessage').text
        authorization_code = response.find('authorizationCode').text
        acquirer_authorization_code = response.find('acqAuthorizationCode').text

        d = dict(
            success=True,
            transaction_id=transaction_id,
            response_code=response_code,
            response_message=response_message,
            authorization_code=authorization_code,
            acquirer_authorization_code=acquirer_authorization_code,
        )
        d.update(common_attributes)
        return Refund(**d)
    else:
        error = transaction.find('error')

        acquirer_error_code_element = error.find('acqErrorCode')

        d = dict(
            success=False,
            error_code=error.find('errorCode').text,
            error_message=error.find('errorMessage').text,
            error_detail=error.find('errorDetail').text,
            acquirer_error_code=acquirer_error_code_element.text if acquirer_error_code_element is not None else '',
        )
        d.update(**common_attributes)
        return Refund(**d)


# :coding: utf-8
# :copyright: Copyright (c) 2014 Martin Pengelly-Phillips
# :license: See LICENSE.txt.

import os

from PySide import QtGui, QtCore

import riffle.resource
import riffle.model
import riffle.icon_factory

class FilesystemBrowser(QtGui.QDialog):
    '''FilesystemBrowser dialog.'''

    def __init__(self, root='', parent=None, iconFactory=None):
        '''Initialise browser with *root* path.

        Use an empty *root* path to specify the computer.

        *parent* is the optional owner of this UI element.

        *iconFactory* specifies the optional factory to pass to the model for
        customising icons.

        '''
        super(FilesystemBrowser, self).__init__(parent=parent)
        self._root = root
        self._iconFactory = iconFactory
        self._selected = []
        self._construct()
        self._postConstruction()

    def _construct(self):
        '''Construct widget.'''
        self.setLayout(QtGui.QVBoxLayout())

        self._headerLayout = QtGui.QHBoxLayout()

        self._locationWidget = QtGui.QComboBox()
        self._headerLayout.addWidget(self._locationWidget, stretch=1)

        self._upButton = QtGui.QToolButton()
        self._upButton.setIcon(QtGui.QIcon(':riffle/icon/up'))
        self._headerLayout.addWidget(self._upButton)

        self.layout().addLayout(self._headerLayout)

        self._contentSplitter = QtGui.QSplitter()

        self._bookmarksWidget = QtGui.QListView()
        self._contentSplitter.addWidget(self._bookmarksWidget)

        self._filesystemWidget = QtGui.QTableView()
        self._filesystemWidget.setSelectionBehavior(
            self._filesystemWidget.SelectRows
        )
        self._filesystemWidget.setSelectionMode(
            self._filesystemWidget.SingleSelection
        )
        self._filesystemWidget.verticalHeader().hide()

        self._contentSplitter.addWidget(self._filesystemWidget)

        proxy = riffle.model.FilesystemSortProxy(self)
        model = riffle.model.Filesystem(
            path=self._root, parent=self, iconFactory=self._iconFactory
        )
        proxy.setSourceModel(model)
        proxy.setDynamicSortFilter(True)

        self._filesystemWidget.setModel(proxy)
        self._filesystemWidget.setSortingEnabled(True)

        self._contentSplitter.setStretchFactor(1, 1)
        self.layout().addWidget(self._contentSplitter)

        self._footerLayout = QtGui.QHBoxLayout()
        self._footerLayout.addStretch(1)

        self._cancelButton = QtGui.QPushButton('Cancel')
        self._footerLayout.addWidget(self._cancelButton)

        self._acceptButton = QtGui.QPushButton('Choose')
        self._footerLayout.addWidget(self._acceptButton)

        self.layout().addLayout(self._footerLayout)

    def _postConstruction(self):
        '''Perform post-construction operations.'''
        self.setWindowTitle('Filesystem Browser')
        self._filesystemWidget.sortByColumn(0, QtCore.Qt.AscendingOrder)

        # TODO: Remove once bookmarks widget implemented.
        self._bookmarksWidget.hide()

        self._acceptButton.setDefault(True)
        self._acceptButton.setDisabled(True)

        self._acceptButton.clicked.connect(self.accept)
        self._cancelButton.clicked.connect(self.reject)

        self._configureShortcuts()

        self.setLocation(self._root)

        self._filesystemWidget.horizontalHeader().setResizeMode(
            QtGui.QHeaderView.ResizeToContents
        )
        self._filesystemWidget.horizontalHeader().setResizeMode(
            0, QtGui.QHeaderView.Stretch
        )

        self._upButton.clicked.connect(self._onNavigateUpButtonClicked)
        self._locationWidget.currentIndexChanged.connect(
            self._onNavigate
        )

        self._filesystemWidget.activated.connect(self._onActivateItem)
        selectionModel = self._filesystemWidget.selectionModel()
        selectionModel.currentRowChanged.connect(self._onSelectItem)

    def _configureShortcuts(self):
        '''Add keyboard shortcuts to navigate the filesystem.'''
        self._upShortcut = QtGui.QShortcut(
            QtGui.QKeySequence('Backspace'), self
        )
        self._upShortcut.setAutoRepeat(False)
        self._upShortcut.activated.connect(self._onNavigateUpButtonClicked)

    def _onActivateItem(self, index):
        '''Handle activation of item in listing.'''
        item = self._filesystemWidget.model().item(index)
        if not isinstance(item, riffle.model.File):
            self._acceptButton.setDisabled(True)
            self.setLocation(item.path, interactive=True)

    def _onSelectItem(self, selection, previousSelection):
        '''Handle selection of item in listing.'''
        self._acceptButton.setEnabled(True)
        del self._selected[:]
        item = self._filesystemWidget.model().item(selection)
        self._selected.append(item.path)

    def _onNavigate(self, index):
        '''Handle selection of path segment.'''
        if index > 0:
            self.setLocation(
                self._locationWidget.itemData(index), interactive=True
            )

    def _onNavigateUpButtonClicked(self):
        '''Navigate up a directory on button click.'''
        index = self._locationWidget.currentIndex()
        self._onNavigate(index + 1)

    def _segmentPath(self, path):
        '''Return list of valid *path* segments.'''
        parts = []
        model = self._filesystemWidget.model()

        # Separate root path from remainder.
        remainder = path

        while True:
            if remainder == model.root.path:
                break

            if remainder:
                parts.append(remainder)

            head, tail = os.path.split(remainder)
            if head == remainder:
                break

            remainder = head

        parts.append(model.root.path)
        return parts

    def setLocation(self, path, interactive=False):
        '''Set current location to *path*.

        *path* must be the same as root or under the root.

        .. note::

            Comparisons are case-sensitive. If you set the root as 'D:/' then
            location can be set as 'D:/folder' *not* 'd:/folder'.

        If *interactive* is True, catch any exception occurring and display an
        appropriate warning dialog to the user. Otherwise allow exceptions to
        bubble up as normal.

        '''
        try:
            self._setLocation(path)
        except Exception as error:
            if not interactive:
                raise
            else:
                warning_dialog = QtGui.QMessageBox(
                    QtGui.QMessageBox.Warning,
                    'Location is not available',
                    '{0} is not accessible.'.format(path),
                    QtGui.QMessageBox.Ok,
                    self
                )
                warning_dialog.setDetailedText(str(error))
                warning_dialog.exec_()

    def _setLocation(self, path):
        '''Set current location to *path*.

        *path* must be the same as root or under the root.

        .. note::

            Comparisons are case-sensitive. If you set the root as 'D:/' then
            location can be set as 'D:/folder' *not* 'd:/folder'.

        '''
        model = self._filesystemWidget.model()

        if not path.startswith(model.root.path):
            raise ValueError('Location must be root or under root.')

        # Ensure children for each segment in path are loaded.
        segments = self._segmentPath(path)
        for segment in reversed(segments):
            pathIndex = model.pathIndex(segment)
            model.fetchMore(pathIndex)

        self._filesystemWidget.setRootIndex(model.pathIndex(path))
        self._locationWidget.clear()

        # Add history entry for each segment.
        for segment in segments:
            index = model.pathIndex(segment)
            if not index.isValid():
                # Root item.
                icon = model.iconFactory.icon(
                    riffle.icon_factory.IconType.Computer
                )
                self._locationWidget.addItem(
                    icon, model.root.path or model.root.name, model.root.path
                )
            else:
                icon = model.icon(index)
                self._locationWidget.addItem(icon, segment, segment)

        if self._locationWidget.count() > 1:
            self._upButton.setEnabled(True)
            self._upShortcut.setEnabled(True)
        else:
            self._upButton.setEnabled(False)
            self._upShortcut.setEnabled(False)

    def selected(self):
        '''Return selected paths.'''
        return self._selected[:]


# :coding: utf-8
# :copyright: Copyright (c) 2014 Martin Pengelly-Phillips
# :license: See LICENSE.txt.

import sys
import os
import re
import subprocess

from setuptools import setup, find_packages, Command
from distutils.command.build import build as BuildCommand
from setuptools.command.install import install as InstallCommand
from distutils.command.clean import clean as CleanCommand
from setuptools.command.test import test as TestCommand
import distutils

ROOT_PATH = os.path.dirname(
    os.path.realpath(__file__)
)

SOURCE_PATH = os.path.join(
    ROOT_PATH, 'source'
)

RESOURCE_PATH = os.path.join(
    ROOT_PATH, 'resource'
)

RESOURCE_TARGET_PATH = os.path.join(
    SOURCE_PATH, 'riffle', 'resource.py'
)

README_PATH = os.path.join(ROOT_PATH, 'README.rst')

ON_READ_THE_DOCS = os.environ.get('READTHEDOCS', None) == 'True'

# Custom commands.
class BuildResources(Command):
    '''Build additional resources.'''

    user_options = []

    def initialize_options(self):
        '''Configure default options.'''

    def finalize_options(self):
        '''Finalize options to be used.'''
        self.resource_source_path = os.path.join(RESOURCE_PATH, 'resource.qrc')
        self.resource_target_path = RESOURCE_TARGET_PATH

    def run(self):
        '''Run build.'''
        if ON_READ_THE_DOCS:
            # PySide not available.
            return

        try:
            pyside_rcc_command = 'pyside-rcc'

            # On Windows, pyside-rcc is not automatically available on the
            # PATH so try to find it manually.
            if sys.platform == 'win32':
                import PySide
                pyside_rcc_command = os.path.join(
                    os.path.dirname(PySide.__file__),
                    'pyside-rcc.exe'
                )

            subprocess.check_call([
                pyside_rcc_command,
                '-o',
                self.resource_target_path,
                self.resource_source_path
            ])
        except (subprocess.CalledProcessError, OSError):
            print(
                'Error compiling resource.py using pyside-rcc. Possibly '
                'pyside-rcc could not be found. You might need to manually add '
                'it to your PATH.'
            )
            raise SystemExit()

class Build(BuildCommand):
    '''Custom build to pre-build resources.'''

    def run(self):
        '''Run build ensuring build_resources called first.'''
        self.run_command('build_resources')
        BuildCommand.run(self)

class Install(InstallCommand):
    '''Custom install to pre-build resources.'''

    def do_egg_install(self):
        '''Run install ensuring build_resources called first.

        .. note::

            `do_egg_install` used rather than `run` as sometimes `run` is not
            called at all by setuptools.

        '''
        self.run_command('build_resources')
        InstallCommand.do_egg_install(self)

class Clean(CleanCommand):
    '''Custom clean to remove built resources and distributions.'''

    def run(self):
        '''Run clean.'''
        relative_resource_path = os.path.relpath(
            RESOURCE_TARGET_PATH, ROOT_PATH
        )
        if os.path.exists(relative_resource_path):
            os.remove(relative_resource_path)
        else:
            distutils.log.warn(
                '\'{0}\' does not exist -- can\'t clean it'
                .format(relative_resource_path)
            )

        relative_compiled_resource_path = relative_resource_path + 'c'
        if os.path.exists(relative_compiled_resource_path):
            os.remove(relative_compiled_resource_path)
        else:
            distutils.log.warn(
                '\'{0}\' does not exist -- can\'t clean it'
                .format(relative_compiled_resource_path)
            )
        CleanCommand.run(self)

class PyTest(TestCommand):
    '''Pytest command.'''

    def finalize_options(self):
        TestCommand.finalize_options(self)
        self.test_args = []
        self.test_suite = True

    def run_tests(self):
        '''Import pytest and run.'''
        import pytest
        errno = pytest.main(self.test_args)
        raise SystemExit(errno)

with open(os.path.join(SOURCE_PATH, 'riffle', '_version.py')) as _version_file:
    VERSION = re.match(
        r'.*__version__ = \'(.*?)\'',
        _version_file.read(),
        re.DOTALL
    ).group(1)

# Compute dependencies.
SETUP_REQUIRES = [
    'PySide >= 1.2.2, < 2',
    'sphinx >= 1.2.2, < 2',
    'sphinx_rtd_theme >= 0.1.6, < 1',
    'lowdown >= 0.1.0, < 2',
    'mock >=2, < 3'
]
INSTALL_REQUIRES = [
    'PySide >= 1.2.2, < 2',
    'clique >= 1.2.0, < 2'
]
TEST_REQUIRES = [
    'pytest >= 2.3.5, < 3'
]

# Readthedocs requires Sphinx extensions to be specified as part of
# install_requires in order to build properly.
if ON_READ_THE_DOCS:
    INSTALL_REQUIRES.extend(SETUP_REQUIRES)

    # PySide not available.
    SETUP_REQUIRES = [
        requirement for requirement in SETUP_REQUIRES
        if not requirement.startswith("PySide ")
    ]

    INSTALL_REQUIRES = [
        requirement for requirement in INSTALL_REQUIRES
        if not requirement.startswith("PySide ")
    ]

setup(
    name='Riffle',
    version=VERSION,
    description='Filesystem browser for PySide.',
    long_description=open(README_PATH).read(),
    keywords='filesystem, browser, pyside, qt, pyqt',
    url='https://gitlab.com/4degrees/riffle',
    author='Martin Pengelly-Phillips',
    author_email='martin@4degrees.ltd.uk',
    license='Apache License (2.0)',
    packages=find_packages(SOURCE_PATH),
    package_dir={
        '': 'source'
    },
    setup_requires=SETUP_REQUIRES,
    install_requires=INSTALL_REQUIRES,
    tests_require=TEST_REQUIRES,
    extras_require={
        'setup': SETUP_REQUIRES,
        'tests': TEST_REQUIRES,
        'dev': SETUP_REQUIRES + TEST_REQUIRES
    },
    cmdclass={
        'build': Build,
        'build_resources': BuildResources,
        'install': Install,
        'clean': Clean,
        'test': PyTest
    }
)


# :coding: utf-8
# :copyright: Copyright (c) 2014 Martin Pengelly-Phillips
# :license: See LICENSE.txt.

import os
from datetime import datetime

from PySide.QtCore import Qt, QAbstractItemModel, QModelIndex, QDir
from PySide.QtGui import QSortFilterProxyModel
import clique

def ItemFactory(path):
    '''Return appropriate :py:class:`Item` instance for *path*.

    If *path* is null then return Computer root.

    '''
    if not path:
        return Computer()

    elif os.path.isfile(path):
        return File(path)

    elif os.path.ismount(path):
        return Mount(path)

    elif os.path.isdir(path):
        return Directory(path)

    else:
        raise ValueError('Could not determine correct type for path: {0}'
                         .format(path))

class Item(object):
    '''Represent filesystem item.'''

    def __init__(self, path):
        '''Initialise item with *path*.'''
        super(Item, self).__init__()
        self.path = path

        self.children = []
        self.parent = None
        self._fetched = False

    def __repr__(self):
        '''Return representation.'''
        return '<{0} {1}>'.format(self.__class__.__name__, self.path)

    @property
    def name(self):
        '''Return name of item.'''
        return os.path.basename(self.path) or self.path

    @property
    def size(self):
        '''Return size of item.'''
        return os.path.getsize(self.path)

    @property
    def type(self):
        '''Return type of item as string.'''
        return ''

    @property
    def modified(self):
        '''Return last modified date of item.'''
        return datetime.fromtimestamp(os.path.getmtime(self.path))

    @property
    def row(self):
        '''Return index of this item in its parent or 0 if no parent.'''
        if self.parent:
            return self.parent.children.index(self)

        return 0

    def addChild(self, item):
        '''Add *item* as child of this item.'''
        if item.parent and item.parent != self:
            item.parent.removeChild(item)

        self.children.append(item)
        item.parent = self

    def removeChild(self, item):
        '''Remove *item* from children.'''
        item.parent = None
        self.children.remove(item)

    def canFetchMore(self):
        '''Return whether more items can be fetched under this one.'''
        if not self._fetched:
            if self.mayHaveChildren():
                return True

        return False

    def mayHaveChildren(self):
        '''Return whether item may have children.'''
        return True

    def fetchChildren(self):
        '''Fetch and return new children.

        Will only fetch children whilst canFetchMore is True.

        .. note::

            It is the caller's responsibility to add each fetched child to this
            parent if desired using :py:meth:`Item.addChild`.

        '''
        if not self.canFetchMore():
            return []

        children = self._fetchChildren()
        self._fetched = True

        return children

    def _fetchChildren(self):
        '''Fetch and return new child items.

        Override in subclasses to fetch actual children and return list of
        *unparented* :py:class:`Item` instances.

        '''
        return []

    def refetch(self):
        '''Reload children.'''
        # Reset children
        for child in self.children[:]:
            self.removeChild(child)

        # Enable children fetching
        self._fetched = False

class Computer(Item):
    '''Represent root.'''

    def __init__(self):
        '''Initialise item.'''
        super(Computer, self).__init__('')

    @property
    def name(self):
        '''Return name of item.'''
        return 'Computer'

    @property
    def type(self):
        '''Return type of item as string.'''
        return 'Root'

    def _fetchChildren(self):
        '''Fetch and return new child items.'''
        children = []
        for entry in QDir.drives():
            path = os.path.normpath(entry.canonicalFilePath())
            children.append(Mount(path))

        return children

class File(Item):
    '''Represent file.'''

    @property
    def type(self):
        '''Return type of item as string.'''
        return 'File'

    def mayHaveChildren(self):
        '''Return whether item may have children.'''
        return False

class Directory(Item):
    '''Represent directory.'''

    @property
    def type(self):
        '''Return type of item as string.'''
        return 'Directory'

    def _fetchChildren(self):
        '''Fetch and return new child items.'''
        children = []

        # List paths under this directory.
        paths = []
        for name in os.listdir(self.path):
            paths.append(os.path.normpath(os.path.join(self.path, name)))

        # Handle collections.
        collections, remainder = clique.assemble(
            paths, [clique.PATTERNS['frames']]
        )

        for path in remainder:
            try:
                child = ItemFactory(path)
            except ValueError:
                pass
            else:
                children.append(child)

        for collection in collections:
            children.append(Collection(collection))

        return children

class Mount(Directory):
    '''Represent mount point.'''

    @property
    def type(self):
        '''Return type of item as string.'''
        return 'Mount'

    @property
    def size(self):
        '''Return size of item.'''
        return None

    @property
    def modified(self):
        '''Return last modified date of item.'''
        return None

class Collection(Item):
    '''Represent collection.'''

    def __init__(self, collection):
        '''Initialise item with *collection*.

        *collection* should be an instance of :py:class:`clique.Collection`.

        '''
        self._collection = collection
        super(Collection, self).__init__(self._collection.format())

    @property
    def type(self):
        '''Return type of item as string.'''
        return 'Collection'

    @property
    def size(self):
        '''Return size of item.'''
        return None

    @property
    def modified(self):
        '''Return last modified date of item.'''
        return None

    def _fetchChildren(self):
        '''Fetch and return new child items.'''
        children = []
        for path in self._collection:
            try:
                child = ItemFactory(path)
            except ValueError:
                pass
            else:
                children.append(child)

        return children

class Filesystem(QAbstractItemModel):
    '''Model representing filesystem.'''

    ITEM_ROLE = Qt.UserRole + 1

    def __init__(self, path='', parent=None, iconFactory=None):
        '''Initialise with root *path*.'''
        super(Filesystem, self).__init__(parent=parent)
        self.root = ItemFactory(path)
        self.columns = ['Name', 'Size', 'Type', 'Date Modified']

        if iconFactory is None:
            # Local import to circumvent circular dependency.
            import riffle.icon_factory
            iconFactory = riffle.icon_factory.IconFactory()

        self.iconFactory = iconFactory

    def rowCount(self, parent):
        '''Return number of children *parent* index has.'''
        if parent.column() > 0:
            return 0

        if parent.isValid():
            item = parent.internalPointer()
        else:
            item = self.root

        return len(item.children)

    def columnCount(self, parent):
        '''Return amount of data *parent* index has.'''
        return len(self.columns)

    def flags(self, index):
        '''Return flags for *index*.'''
        if not index.isValid():
            return Qt.NoItemFlags

        return Qt.ItemIsEnabled | Qt.ItemIsSelectable

    def index(self, row, column, parent):
        '''Return index for *row* and *column* under *parent*.'''
        if not self.hasIndex(row, column, parent):
            return QModelIndex()

        if not parent.isValid():
            item = self.root
        else:
            item = parent.internalPointer()

        try:
            child = item.children[row]
        except IndexError:
            return QModelIndex()
        else:
            return self.createIndex(row, column, child)

    def pathIndex(self, path):
        '''Return index of item with *path*.'''
        if path == self.root.path:
            return QModelIndex()

        if not path.startswith(self.root.path):
            return QModelIndex()

        parts = []
        while True:
            if path == self.root.path:
                break

            head, tail = os.path.split(path)
            if head == path:
                if path:
                    parts.append(path)
                break

            parts.append(tail)
            path = head

        parts.reverse()
        if parts:
            item = self.root
            count = 0

            for count, part in enumerate(parts):
                matched = False

                for child in item.children:
                    if child.name == part:
                        item = child
                        matched = True
                        break

                if not matched:
                    break

            if count + 1 == len(parts):
                return self.createIndex(item.row, 0, item)

        return QModelIndex()

    def parent(self, index):
        '''Return parent of *index*.'''
        if not index.isValid():
            return QModelIndex()

        item = index.internalPointer()
        if not item:
            return QModelIndex()

        parent = item.parent
        if not parent or parent == self.root:
            return QModelIndex()

        return self.createIndex(parent.row, 0, parent)

    def item(self, index):
        '''Return item at *index*.'''
        return self.data(index, role=self.ITEM_ROLE)

    def icon(self, index):
        '''Return icon for index.'''
        return self.data(index, role=Qt.DecorationRole)

    def data(self, index, role):
        '''Return data for *index* according to *role*.'''
        if not index.isValid():
            return None

        column = index.column()
        item = index.internalPointer()

        if role == self.ITEM_ROLE:
            return item

        elif role == Qt.DisplayRole:

            if column == 0:
                return item.name
            elif column == 1:
                if item.size:
                    return item.size
            elif column == 2:
                return item.type
            elif column == 3:
                if item.modified is not None:
                    return item.modified.strftime('%c')

        elif role == Qt.DecorationRole:
            if column == 0:
                return self.iconFactory.icon(item)

        elif role == Qt.TextAlignmentRole:
            if column == 1:
                return Qt.AlignRight
            else:
                return Qt.AlignLeft

        return None

    def headerData(self, section, orientation, role):
        '''Return label for *section* according to *orientation* and *role*.'''
        if orientation == Qt.Horizontal:
            if section < len(self.columns):
                column = self.columns[section]
                if role == Qt.DisplayRole:
                    return column

        return None

    def hasChildren(self, index):
        '''Return if *index* has children.

        Optimised to avoid loading children at this stage.

        '''
        if not index.isValid():
            item = self.root
        else:
            item = index.internalPointer()
            if not item:
                return False

        return item.mayHaveChildren()

    def canFetchMore(self, index):
        '''Return if more data available for *index*.'''
        if not index.isValid():
            item = self.root
        else:
            item = index.internalPointer()

        return item.canFetchMore()

    def fetchMore(self, index):
        '''Fetch additional data under *index*.'''
        if not index.isValid():
            item = self.root
        else:
            item = index.internalPointer()

        if item.canFetchMore():
            startIndex = len(item.children)
            additionalChildren = item.fetchChildren()
            endIndex = startIndex + len(additionalChildren) - 1
            if endIndex >= startIndex:
                self.beginInsertRows(index, startIndex, endIndex)
                for newChild in additionalChildren:
                    item.addChild(newChild)
                self.endInsertRows()

    def reset(self):
        '''Reset model'''
        self.beginResetModel()
        self.root.refetch()
        self.endResetModel()

class FilesystemSortProxy(QSortFilterProxyModel):
    '''Sort directories before files.'''

    def lessThan(self, left, right):
        '''Return ordering of *left* vs *right*.'''
        sourceModel = self.sourceModel()
        if sourceModel:
            leftItem = sourceModel.item(left)
            rightItem = sourceModel.item(right)

            if (isinstance(leftItem, Directory)
                and not isinstance(rightItem, Directory)):
                return self.sortOrder() == Qt.AscendingOrder

            elif (not isinstance(leftItem, Directory)
                and isinstance(rightItem, Directory)):
                return self.sortOrder() == Qt.DescendingOrder

        return super(FilesystemSortProxy, self).lessThan(left, right)

    @property
    def root(self):
        '''Return root of model.'''
        sourceModel = self.sourceModel()
        if not sourceModel:
            return None

        return sourceModel.root

    @property
    def iconFactory(self):
        '''Return iconFactory of model.'''
        sourceModel = self.sourceModel()
        if not sourceModel:
            return None

        return sourceModel.iconFactory

    def pathIndex(self, path):
        '''Return index of item with *path*.'''
        sourceModel = self.sourceModel()
        if not sourceModel:
            return QModelIndex()

        return self.mapFromSource(sourceModel.pathIndex(path))

    def item(self, index):
        '''Return item at *index*.'''
        sourceModel = self.sourceModel()

        if not sourceModel:
            return None

        return sourceModel.item(self.mapToSource(index))

    def icon(self, index):
        '''Return icon for index.'''
        sourceModel = self.sourceModel()
        if not sourceModel:
            return None

        return sourceModel.icon(self.mapToSource(index))

    def hasChildren(self, index):
        '''Return if *index* has children.'''
        sourceModel = self.sourceModel()

        if not sourceModel:
            return False

        return sourceModel.hasChildren(self.mapToSource(index))

    def canFetchMore(self, index):
        '''Return if more data available for *index*.'''
        sourceModel = self.sourceModel()

        if not sourceModel:
            return False

        return sourceModel.canFetchMore(self.mapToSource(index))

    def fetchMore(self, index):
        '''Fetch additional data under *index*.'''
        sourceModel = self.sourceModel()

        if not sourceModel:
            return False

        return sourceModel.fetchMore(self.mapToSource(index))


# :coding: utf-8
# :copyright: Copyright (c) 2014 Martin Pengelly-Phillips
# :license: See LICENSE.txt.

from PySide import QtGui

import riffle.model

class IconType(object):
    '''Icon types.'''

    Computer = 'Computer',
    File = 'File',
    Directory = 'Directory',
    Mount = 'Mount',
    Collection = 'Collection'
    Unknown = 'Unknown'

class IconFactory(object):
    '''Icon provider.'''

    def icon(self, specification):
        '''Return appropriate icon for *specification*.

        *specification* should be either:

            * An instance of :py:class:`riffle.model.Item`
            * One of the defined icon types (:py:class:`IconType`)

        '''
        if isinstance(specification, riffle.model.Item):
            specification = self.type(specification)

        icon = None

        if specification == IconType.Computer:
            icon = QtGui.QIcon(':riffle/icon/computer')

        elif specification == IconType.Mount:
            icon = QtGui.QIcon(':riffle/icon/drive')

        elif specification == IconType.Directory:
            icon = QtGui.QIcon(':riffle/icon/folder')

        elif specification == IconType.File:
            icon = QtGui.QIcon(':riffle/icon/file')

        elif specification == IconType.Collection:
            icon = QtGui.QIcon(':riffle/icon/collection')

        return icon

    def type(self, item):
        '''Return appropriate icon type for *item*.'''
        iconType = IconType.Unknown

        if isinstance(item, riffle.model.Computer):
            iconType = IconType.Computer

        elif isinstance(item, riffle.model.Mount):
            iconType = IconType.Mount

        elif isinstance(item, riffle.model.Directory):
            iconType = IconType.Directory

        elif isinstance(item, riffle.model.File):
            iconType = IconType.File

        elif isinstance(item, riffle.model.Collection):
            iconType = IconType.Collection

        return iconType


"""Fork and detach the current process."""
import errno
import os
import resource
import subprocess
import sys
import traceback
from multiprocessing import Value

maxfd = 2048

class Error(Exception):
    """Raised on error."""

class Detach(object):

    def __init__(self, stdout=None, stderr=None, stdin=None, close_fds=False, exclude_fds=None,
                 daemonize=False):
        """
        Fork and detach a process. The stdio streams of the child default to /dev/null but may be
        overridden with the `stdout`, `stderr`, and `stdin` parameters. If `close_fds` is True then
        all open file descriptors (except those passed as overrides for stdio) are closed by the
        child process. File descriptors in `exclude_fds` will not be closed. If `daemonize` is True
        then the parent process exits.
        """
        self.stdout = stdout
        self.stderr = stderr
        self.stdin = stdin
        self.close_fds = close_fds
        self.exclude_fds = set()
        self.daemonize = daemonize
        self.pid = None
        self.shared_pid = Value('i', 0)

        for item in list(exclude_fds or []) + [stdout, stderr, stdin]:
            if hasattr(item, 'fileno'):
                item = item.fileno()
            self.exclude_fds.add(item)

    def _get_max_fd(self):
        """Return the maximum file descriptor value."""
        limits = resource.getrlimit(resource.RLIMIT_NOFILE)
        result = limits[1]
        if result == resource.RLIM_INFINITY:
            result = maxfd
        return result

    def _close_fd(self, fd):
        """Close a file descriptor if it is open."""
        try:
            os.close(fd)
        except OSError, exc:
            if exc.errno != errno.EBADF:
                msg = "Failed to close file descriptor {}: {}".format(fd, exc)
                raise Error(msg)

    def _close_open_fds(self):
        """Close open file descriptors."""
        maxfd = self._get_max_fd()
        for fd in reversed(range(maxfd)):
            if fd not in self.exclude_fds:
                self._close_fd(fd)

    def _redirect(self, stream, target):
        """Redirect a system stream to the provided target."""
        if target is None:
            target_fd = os.open(os.devnull, os.O_RDWR)
        else:
            target_fd = target.fileno()
        os.dup2(target_fd, stream.fileno())

    def __enter__(self):
        """Fork and detach the process."""
        pid = os.fork()
        if pid > 0:
            # parent
            os.waitpid(pid, 0)
            self.pid = self.shared_pid.value
        else:
            # first child
            os.setsid()
            pid = os.fork()
            if pid > 0:
                # first child
                self.shared_pid.value = pid
                os._exit(0)
            else:
                # second child
                if self.close_fds:
                    self._close_open_fds()

                self._redirect(sys.stdout, self.stdout)
                self._redirect(sys.stderr, self.stderr)
                self._redirect(sys.stdin, self.stdin)
        return self

    def __exit__(self, exc_cls, exc_val, exc_tb):
        """Exit processes."""
        if self.daemonize or not self.pid:
            if exc_val:
                traceback.print_exception(exc_cls, exc_val, exc_tb)
            os._exit(0)

def call(args, stdout=None, stderr=None, stdin=None, daemonize=False,
         preexec_fn=None, shell=False, cwd=None, env=None):
    """
    Run an external command in a separate process and detach it from the current process. Excepting
    `stdout`, `stderr`, and `stdin` all file descriptors are closed after forking. If `daemonize`
    is True then the parent process exits. All stdio is redirected to `os.devnull` unless
    specified. The `preexec_fn`, `shell`, `cwd`, and `env` parameters are the same as their `Popen`
    counterparts. Return the PID of the child process if not daemonized.
    """
    stream = lambda s, m: s is None and os.open(os.devnull, m) or s
    stdout = stream(stdout, os.O_WRONLY)
    stderr = stream(stderr, os.O_WRONLY)
    stdin = stream(stdin, os.O_RDONLY)

    shared_pid = Value('i', 0)
    pid = os.fork()
    if pid > 0:
        os.waitpid(pid, 0)
        child_pid = shared_pid.value
        del shared_pid
        if daemonize:
            sys.exit(0)
        return child_pid
    else:
        os.setsid()
        proc = subprocess.Popen(args, stdout=stdout, stderr=stderr, stdin=stdin, close_fds=True,
                                preexec_fn=preexec_fn, shell=shell, cwd=cwd, env=env)
        shared_pid.value = proc.pid
        os._exit(0)


from os import environ
from functools import partial
from collections import OrderedDict

from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from django.utils.module_loading import module_has_submodule

try:
    from django.contrib.sites.shortcuts import get_current_site

except ImportError:  # Django < 1.7
    from django.contrib.sites.models import get_current_site

try:
    from django.apps import apps
    apps_get_model = apps.get_model

except ImportError:  # Django < 1.7
    from django.db.models import get_model
    apps_get_model = None

try:
    from django.utils.module_loading import import_module

except ImportError:
    # Django <=1.9.0
    from django.utils.importlib import import_module

from .utils import DomainGetter

def choices_list(*choices):
    """Helps to define choices for models, that could be addressed
    later as dictionaries.

    To be used in conjunction with `get_choices()`.

    Example:

        class MyModel(models.Model):

            TYPE_ONE = 1
            TYPE_TWO = 2

            TYPES = choices_list(
                (TYPE_ONE, 'Type one title'),
                (TYPE_TWO, 'Type two title'),
            )

            type = models.PositiveIntegerField('My type', choices=get_choices(TYPES), default=TYPE_TWO)

            def get_display_type(self):
                return self.TYPES[self.type]

    :param set|list|tuple choices:
    :rtype: OrderedDict
    :return: Choices ordered dictionary
    """
    return OrderedDict(choices)

def get_choices(choices_list):
    """Returns model field choices from a given choices list.
    Choices list is defined with `choices_list()`.

    :param OrderedDict choices_list:
    :rtype: tuple
    :return: Choices tuple
    """
    return tuple((key, val) for key, val in choices_list.items())

def set_form_widgets_attrs(form, attrs):
    """Applies a given HTML attributes to each field widget of a given form.

    Example:

        set_form_widgets_attrs(my_form, {'class': 'clickable'})

    """
    for _, field in form.fields.items():
        attrs_ = dict(attrs)
        for name, val in attrs.items():
            if hasattr(val, '__call__'):
                attrs_[name] = val(field)
        field.widget.attrs = field.widget.build_attrs(attrs_)

def get_model_class_from_string(model_path):
    """Returns a certain model as defined in a string formatted `<app_name>.<model_name>`.

    Example:

        model = get_model_class_from_string('myapp.MyModel')

    """
    try:
        app_name, model_name = model_path.split('.')
    except ValueError:
        raise ImproperlyConfigured('`%s` must have the following format: `app_name.model_name`.' % model_path)

    if apps_get_model is None:
        model = get_model(app_name, model_name)
    else:
        try:
            model = apps_get_model(app_name, model_name)
        except (LookupError, ValueError):
            model = None

    if model is None:
        raise ImproperlyConfigured('`%s` refers to a model `%s` that has not been installed.' % (model_path, model_name))

    return model

def get_model_class_from_settings(settings_module, settings_entry_name):
    """Returns a certain model as defined in a given settings module.

    Example:

        myapp/settings.py

            from django.conf import settings

            MY_MODEL = getattr(settings, 'MYAPP_MY_MODEL', 'myapp.MyModel')

        myapp/some.py

            from myapp import settings

            model = get_model_class_from_settings(settings, 'MY_MODEL')

    """
    return get_model_class_from_string(getattr(settings_module, settings_entry_name))

def get_site_url(request=None):
    """Tries to get a site URL from environment and settings
    in the following order:

    1. (SITE_PROTO / SITE_SCHEME) + SITE_DOMAIN
    2. SITE_URL
    3. Django Sites contrib
    4. Request object

    :param HttpRequest request: Request object to deduce URL from.
    :rtype: str

    """
    env = partial(environ.get)
    settings_ = partial(getattr, settings)

    domain = None
    scheme = None
    url = None

    for src in (env, settings_):
        if url is None:
            url = src('SITE_URL', None)

        if domain is None:
            domain = src('SITE_DOMAIN', None)

        if scheme is None:
            scheme = src('SITE_PROTO', src('SITE_SCHEME', None))

    if domain is None and url is not None:
        scheme, domain = url.split('://')[:2]

    if domain is None:
        site = get_current_site(request or DomainGetter(domain))
        domain = site.domain

    if scheme is None and request:
        scheme = request.scheme

    if domain is None:
        domain = 'undefined-domain.local'

    if scheme is None:
        scheme = 'http'

    domain = domain.rstrip('/')

    return '%s://%s' % (scheme, domain)

def import_app_module(app_name, module_name):
    """Returns a module from a given app by its name.

    :param str app_name:
    :param str module_name:
    :rtype: module or None

    """
    name_split = app_name.split('.')
    if name_split[-1][0].isupper():  # Seems that we have app config class path here.
        app_name = '.'.join(name_split[:-2])

    module = import_module(app_name)

    try:
        sub_module = import_module('%s.%s' % (app_name, module_name))
        return sub_module

    except:

        # The same bubbling strategy as in autodiscover_modules().
        if module_has_submodule(module, module_name):  # Module is in a package.
            raise

        return None

def import_project_modules(module_name):
    """Imports modules from registered apps using given module name
    and returns them as a list.

    :param str module_name:
    :rtype: list

    """
    from django.conf import settings

    submodules = []
    for app in settings.INSTALLED_APPS:
        module = import_app_module(app, module_name)
        if module is not None:
            submodules.append(module)

    return submodules


from copy import deepcopy
from functools import partial

from django import VERSION
from django import template
from django.template import TemplateDoesNotExist
from django.template.base import UNKNOWN_SOURCE, Lexer, Parser
from django.template.loader_tags import do_include, Node

try:
    from django.template.loader_tags import construct_relative_path

    # To prevent AttributeError: 'Parser' object has no attribute 'origin'
    def construct_relative_path_(parser, name):
        return construct_relative_path(parser.origin.template_name, name)

except ImportError:
    construct_relative_path_ = lambda parser, name: name  # Sorry sub here for now.

from ..toolbox import get_site_url

if VERSION >= (1, 9, 0):
    get_lexer = partial(Lexer)

else:
    get_lexer = partial(Lexer, origin=UNKNOWN_SOURCE)

register = template.Library()

@register.simple_tag(takes_context=True)
def site_url(context):
    """Tries to get a site URL from environment and settings.

    See toolbox.get_site_url() for description.

    Example:

        {% load etc_misc %}
        {% site_url %}

    """
    return get_site_url(request=context.get('request', None))

class DynamicIncludeNode(Node):

    context_key = '__include_context'

    def __init__(self, template, *args, **kwargs):
        self.fallback = kwargs.pop('fallback', None)
        self.template = template
        self.extra_context = kwargs.pop('extra_context', {})
        self.isolated_context = kwargs.pop('isolated_context', False)
        super(DynamicIncludeNode, self).__init__(*args, **kwargs)

    def render_(self, tpl_new, context):

        template = deepcopy(self.template)  # Do not mess with global template for threadsafety.
        template.var = tpl_new  # Cheat a little

        # Below is implementation from Django 2.1 generic IncludeNode.

        template = template.resolve(context)
        # Does this quack like a Template?
        if not callable(getattr(template, 'render', None)):
            # If not, try the cache and get_template().
            template_name = template
            cache = context.render_context.dicts[0].setdefault(self, {})
            template = cache.get(template_name)
            if template is None:
                template = context.template.engine.get_template(template_name)
                cache[template_name] = template
        # Use the base.Template of a backends.django.Template.
        elif hasattr(template, 'template'):
            template = template.template
        values = {
            name: var.resolve(context)
            for name, var in self.extra_context.items()
        }
        if self.isolated_context:
            return template.render(context.new(values))
        with context.push(**values):
            return template.render(context)

    def render(self, context):
        render_ = self.render_

        try:
            return render_(
                tpl_new=Parser(get_lexer(self.template.var).tokenize()).parse().render(context),
                context=context)

        except TemplateDoesNotExist:
            fallback = self.fallback

            if not fallback:  # pragma: nocover
                raise

            return render_(tpl_new=fallback.var, context=context)

@register.tag('include_')
def include_(parser, token):
    """Similar to built-in ``include`` template tag, but allowing
    template variables to be used in template name and a fallback template,
    thus making the tag more dynamic.

    .. warning:: Requires Django 1.8+

    Example:

        {% load etc_misc %}
        {% include_ "sub_{{ postfix_var }}.html" fallback "default.html" %}

    """
    bits = token.split_contents()

    dynamic = False

    # We fallback to built-in `include` if a template name contains no variables.
    if len(bits) >= 2:
        dynamic = '{{' in bits[1]

        if dynamic:
            fallback = None
            bits_new = []

            for bit in bits:

                if fallback is True:
                    # This bit is a `fallback` argument.
                    fallback = bit
                    continue

                if bit == 'fallback':
                    fallback = True

                else:
                    bits_new.append(bit)

            if fallback:
                fallback = parser.compile_filter(construct_relative_path_(parser, fallback))

            token.contents = ' '.join(bits_new)

    token.contents = token.contents.replace('include_', 'include')
    include_node = do_include(parser, token)

    if dynamic:
        # swap simple include with dynamic
        include_node = DynamicIncludeNode(
            include_node.template,
            extra_context=include_node.extra_context,
            isolated_context=include_node.isolated_context,
            fallback=fallback or None,
        )

    return include_node


from configparser import RawConfigParser
from pyum.httpclient import HTTPClient
from pyum.repometadata import RepoMetadata

import pathlib

__author__ = 'drews'

class RepoCollection(object):
    def __init__(self, path):
        self.path = pathlib.Path(path)
        self._repo_files = {}

    @property
    def repositories(self):
        """
        Return a list of all repository objects in the repofiles in the repo folder specified
        :return:
        """
        for repo_path in self.path.glob('*.repo'):
            for id, repository in self._get_repo_file(repo_path).repositories:
                yield id, repository

    def _get_repo_file(self, repo_path):
        """
        Lazy load RepoFile objects on demand.
        :param repo_path:
        :return:
        """
        if repo_path not in self._repo_files:
            self._repo_files[repo_path] = RepoFile(repo_path)
        return self._repo_files[repo_path]

class RepoFile(RawConfigParser, object):
    """
    Represents a file on disk, describing one or more repositories in /etc/yum.repods.d/*.repo
    """

    def __init__(self, path_to_config):
        """
        Path on disk to the ini file containing the repo description
        :param str path_to_config:
        :return:
        """
        super(RepoFile, self).__init__()
        self._path_to_config = str(path_to_config)
        self.read(self._path_to_config)
        self._yum_variables = {}

    def __getattr__(self, item):
        if item == 'keys':
            sections = self.sections()
            return sections
        else:
            return super(RepoFile, self).__getattribute__(item)

    def __getitem__(self, key):
        if type(key) is int:
            key = self.sections()[key]
            if not self.has_section(key):
                raise KeyError(key)
            return (key, Repo(
                    id=key,
                    basearch=self.yum_variables['basearch'],
                    releasever=self.yum_variables['releasever'],
                    **dict(self.items(key))
            ))
        else:
            if not self.has_section(key):
                raise KeyError(key)
            return Repo(
                    id=key,
                    basearch=self.yum_variables['basearch'],
                    releasever=self.yum_variables['releasever'],
                    **dict(self.items(key))
            )

    @property
    def repositories(self):
        for repository_id in self.sections():
            yield repository_id, Repo(repository_id, **dict(self.items(repository_id)))

    def set_yum_variables(self, releasever, basearch):
        """
        Variables which are usually injected from an OS level.
        :param str releasever: OS major version
        :param str basearch: One of either 'i386' or 'x86_64'.
        :return:
        """
        self.yum_variables = {
            'releasever': str(releasever),
            'basearch': basearch
        }

class Repo(HTTPClient):
    """
    Represents a single repository
    """

    def __init__(self, id, releasever=None, basearch=None, **kwargs):
        self.id = id
        self.repo_params = kwargs
        self._yum_variables = {
            'releasever': releasever,
            'basearch': basearch
        }
        enabled_state = self.repo_params.get('enabled', False)
        if enabled_state in ['0', '1']:
            enabled_state = (False if enabled_state == '0' else True)
        self.repo_params['enabled'] = enabled_state

    def __getattr__(self, key):
        if key not in self.repo_params:
            return super(Repo, self).__getattribute__(key)
        else:
            return self.render_string(self.repo_params[key])

    @property
    def enabled(self):
        return '1' if self.__getattr__('enabled') else '0'

    def render_string(self, string):
        if isinstance(string, str):
            for key, value in self._yum_variables.items():
                string = string.replace('$' + key, value)
        return string

    def set_yum_variables(self, releasever, basearch):
        """
        Variables which are usually injected from an OS level.
        :param int releasever: OS major version
        :param str basearch: One of either 'i386' or 'x86_64'.
        :return:
        """
        self._yum_variables = {
            'releasever': str(releasever),
            'basearch': basearch
        }

    def primary(self):
        return self._parse_repo_data().load().primary()

    def _parse_repo_data(self):
        if 'baseurl' in self.repo_params:
            return RepoMetadata(self.repo_params['baseurl'])
        else:
            mirrorlist = self._get_mirrorlist()
            mirror = None
            for mirror in mirrorlist:
                if not mirror.endswith('repodata/repomd.xml'):
                    mirror += 'repodata/repomd.xml'
                if self._url_is_reachable(mirror):
                    break
            return RepoMetadata(mirror)

    def _get_mirrorlist(self):
        if self._url_is_reachable(self.mirrorlist):
            mirrorlist = self.http_request(self.mirrorlist).decode('utf-8')
        else:
            raise HTTPClient.ConnectionError('Could not connect to \'{0}\''.format(self.mirrorlist))
        if mirrorlist.startswith("<?xml"):
            return self.parse_xml_mirrorlist(mirrorlist)
        else:
            return mirrorlist.split("\n")

    @staticmethod
    def parse_xml_mirrorlist(xml):
        mirrorlist = []
        doc = ElementTree.fromstring(xml)
        urls = doc.findall(
                ('.//{http://www.metalinker.org/}files/{http://www.metalinker.org/}file/{http://www.metalinker.org/}'
                 'resources/{http://www.metalinker.org/}url'))
        for url in urls:
            if ('type' in url.attrib) and (url.attrib['type'] in ['http', 'https']):
                mirrorlist.append(url.text)
        return mirrorlist


# import yum
import tempfile
from xml import etree

from pyrpm.rpm import RPM
from pyum.httpclient import HTTPClient

__author__ = 'drews'

class RpmParser(object):
    @staticmethod
    def load(fp):
        """

        :param fp:
        :return Rpm:
        """
        parser = etree.XMLParser(target=RpmParser())
        return etree.parse(fp.read(), parser)

    def __init__(self):
        self.rpm = Rpm()

class Rpm(object):
    """
    Represents the xml structure defining where to find a the specific version of an RPM.
    """
    xmlns = '{http://linux.duke.edu/metadata/common}'
    xmlns_rpm = '{http://linux.duke.edu/metadata/rpm}'

    @property
    def package(self):
        """
        Return the package from a URL
        :return:
        """
        return Package.from_url(self.uri())

    def uri(self):
        return '{0}/{1}'.format(self.repo_url, self.location)

    def parse_xml(self, xml):
        self.name = xml.find('.//{xmlns}name'.format(xmlns=self.xmlns)).text
        self.arch = xml.find('.//{xmlns}arch'.format(xmlns=self.xmlns)).text
        self.version = xml.find('.//{xmlns}version'.format(xmlns=self.xmlns)).attrib['ver']
        self.release = xml.find('.//{xmlns}version'.format(xmlns=self.xmlns)).attrib['rel']

        checksum_element = xml.find('.//{xmlns}checksum'.format(xmlns=self.xmlns))
        self.checksum_type = checksum_element.attrib['type']
        self.checksum = checksum_element.text

        self.summary = xml.find('.//{xmlns}summary'.format(xmlns=self.xmlns)).text
        self.description = xml.find('.//{xmlns}description'.format(xmlns=self.xmlns)).text
        self.packager = xml.find('.//{xmlns}packager'.format(xmlns=self.xmlns)).text
        self.url = xml.find('.//{xmlns}url'.format(xmlns=self.xmlns)).text

        self.build_time = xml.find('.//{xmlns}time'.format(xmlns=self.xmlns)).attrib['build']
        self.file_time = xml.find('.//{xmlns}time'.format(xmlns=self.xmlns)).attrib['file']

        size_element = xml.find('.//{xmlns}size'.format(xmlns=self.xmlns)).attrib
        self.installed_size = size_element['installed']
        self.package_size = size_element['package']
        self.archive_size = size_element['archive']

        self.location = xml.find('.//{xmlns}location'.format(xmlns=self.xmlns)).attrib['href']

        self._parse_xml_format(xml.find('.//{xmlns}format'.format(xmlns=self.xmlns)))

    def _parse_xml_format(self, xml):
        self.license = xml.find('.//{xmlns}license'.format(xmlns=self.xmlns_rpm)).text
        self.vendor = xml.find('.//{xmlns}vendor'.format(xmlns=self.xmlns_rpm)).text
        self.group = xml.find('.//{xmlns}group'.format(xmlns=self.xmlns_rpm)).text
        self.buildhost = xml.find('.//{xmlns}buildhost'.format(xmlns=self.xmlns_rpm)).text
        self.sourcerpm = xml.find('.//{xmlns}sourcerpm'.format(xmlns=self.xmlns_rpm)).text

        self.provides = []
        provides_entries = xml.findall('.//{xmlns}provides/{xmlns}entry'.format(xmlns=self.xmlns_rpm))
        for entry in provides_entries:
            if 'flags' not in entry.attrib:
                self.provides.append(entry.attrib['name'])

        self.requires = []
        requires_entries = xml.findall('.//{xmlns}requires/{xmlns}entry'.format(xmlns=self.xmlns_rpm))
        for entry in requires_entries:
            if 'flags' not in entry.attrib:
                self.requires.append(entry.attrib['name'])

class Format(object):
    pass

class Package(object):
    """
    Represents the XML description of an RPM in a repomd xml database.
    """

    @staticmethod
    def from_url(url):
        """
        Given a URL, return a package
        :param url:
        :return:
        """
        package_data = HTTPClient().http_request(url=url, decode=None)
        return Package(raw_data=package_data)

    MODE_RAW_DATA = 0
    MODE_FILE_PATH = 1

    def __init__(self, raw_data=None):
        self.mode = None
        if raw_data is not None:
            self.data = raw_data
            self.mode = self.MODE_RAW_DATA
            self.fp = tempfile.NamedTemporaryFile()
            self.fp.write(raw_data)
            self.fp.seek(0)
            self.rpm = RPM.load(self.fp)

    def __enter__(self):
        return self

    def dependencies(self):
        """
        Read the contents of the rpm itself
        :return:
        """
        cpio = self.rpm.gzip_file.read()
        content = cpio.read()
        return []

    # yb = yum.YumBase()

    def __exit__(self, *excinfo):
        """
        Make sure we close the file after we finish the 'with'
        :param excinfo:
        :return:
        """
        self.rpm.__exit__(*excinfo)
        if self.mode == self.MODE_RAW_DATA:
            self.fp.close()


import hashlib

try:
    from urllib.parse import urlencode
except ImportError:
    from urllib import urlencode

from django import template
from django.template.defaultfilters import safe
from django.contrib.auth import get_user_model

USER_MODEL = get_user_model()

register = template.Library()

def get_gravatar_url(obj, size=65, default='identicon'):
    """

    See http://ru.gravatar.com/site/implement/images/ for details on options.

    :param UserModel, str obj:
    :param int size:
    :param str default: 404, mm (mystery-man), identicon, monsterid, wavatar, retro, blank
    :return:
    """
    if isinstance(obj, USER_MODEL):
        email = obj.email or obj.username
    else:
        email = obj

    if email:
        return ('http://www.gravatar.com/avatar/%s/?%s' %
                (hashlib.md5(email.encode()).hexdigest(), urlencode({'size': size, 'd': default})))
    return ''

@register.simple_tag
def gravatar_get_url(obj, size=65, default='identicon'):
    """Returns Gravatar image URL for a given string or UserModel.

    Example:

        {% load gravatar %}
        {% gravatar_get_url user_model %}

    :param UserModel, str obj:
    :param int size:
    :param str default:
    :return:
    """
    return get_gravatar_url(obj, size=size, default=default)

@register.simple_tag
def gravatar_get_img(obj, size=65, default='identicon'):
    """Returns Gravatar image HTML tag for a given string or UserModel.

    Example:

        {% load gravatar %}
        {% gravatar_get_img user_model %}

    :param UserModel, str obj:
    :param int size:
    :param str default:
    :return:
    """
    url = get_gravatar_url(obj, size=size, default=default)
    if url:
        return safe('<img src="%s" class="gravatar">' % url)
    return ''


from pyum.httpclient import HTTPClient
from lxml import etree

__author__ = 'drews'

class DataParser(object):
    xmlns = '{http://linux.duke.edu/metadata/common}'
    xmlns_rpm = '{http://linux.duke.edu/metadata/rpm}'

class Data(HTTPClient):
    """
    Represents the common attributes of all database types within a yum repository
    """

    xml_parse = None

    @classmethod
    def parse(cls, xml_path):
        """
        Parses an xml_path with the inherited xml parser
        :param xml_path:
        :return:
        """
        parser = etree.XMLParser(target=cls.xml_parse())
        return etree.parse(xml_path, parser)

    def __init__(self, **kwargs):
        """
        Ingest all attributes
        :param kwargs:
        :return:
        """
        self._attributes = kwargs
        self.repo_url = None

    def __getitem__(self, item):
        """
        Forward attribute requests to self._attributes, or try to get them normally.
        :param item:
        :return:
        """
        if item not in self._attributes:
            raise KeyError("Could not find '{0}'".format(item))
        else:
            return self._attributes[item]

    def setRepoUrl(self, new_url):
        """
        Setter function for the repo URL
        :param new_url:
        :return:
        """
        self.repo_url = new_url

    def location(self):
        """
        Predefined format for the location. This should be an @property
        :return:
        """
        return "{0}/{1}".format(self.repo_url.replace('repodata/repomd.xml', ''), self._attributes['location'])

    def load(self):
        """
        Load the repo database from the remote source, and then parse it.
        :return:
        """
        data = self.http_request(self.location())
        self._parse(data)
        return self


# -*- coding: utf-8 -*-
#
#  __init__.py
#  proj
#

from __future__ import print_function

__author__ = 'Lars Yencken'
__email__ = 'lars@yencken.org'
__version__ = '0.1.0'

import glob
import os
import shutil
import sys
from functools import reduce

import arrow
import click

PROJ_ARCHIVE = os.environ.get('PROJ_ARCHIVE')

def bail(message):
    click.echo(message, err=True)
    sys.exit(1)

@click.group()
def main():
    """
    proj is a tool for managing many different projects, and archiving
    projects that you're no longer actively working on.

    It assumes you have a working folder containing active projects,
    and an archive folder with inactive projects. proj helps organise
    inactive projects by year and by quarter (e.g. 2013/q3/my-project).

    proj needs an archive directory specified by the PROJ_ARCHIVE
    environment variable.
    """
    if PROJ_ARCHIVE is None:
        bail('please set PROJ_ARCHIVE to your archive\'s location')

    if not os.path.isdir(PROJ_ARCHIVE):
        bail('archive directory does not exist: ' + PROJ_ARCHIVE)

@click.command()
@click.argument('folder', nargs=-1)
@click.option('-n', '--dry-run',
              is_flag=True,
              help="Don't make any changes")
def archive(folder, dry_run=False):
    "Move an active project to the archive."
    # error handling on archive_dir already done in main()

    for f in folder:
        if not os.path.exists(f):
            bail('folder does not exist: ' + f)

    _archive_safe(folder, PROJ_ARCHIVE, dry_run=dry_run)

def _last_modified(folder):
    try:
        return max(_time_modified(f) for f in _iter_files(folder))
    except ValueError:
        bail('no files in folder: ' + folder)

def _iter_files(folder):
    if os.path.isdir(folder):
        for dirname, subdirs, filenames in os.walk(folder):
            for basename in filenames:
                filename = os.path.join(dirname, basename)
                yield filename
    else:
        # it's actually just a file
        yield folder

def _time_modified(filename):
    return arrow.get(os.stat(filename).st_mtime)

def _to_quarter(t):
    return str(t.year), 'q' + str(1 + (t.month - 1) // 3)

def _archive_safe(folders, archive_dir, dry_run=False):
    for folder in folders:
        t = _last_modified(folder)
        year, quarter = _to_quarter(t)
        dest_dir = os.path.join(archive_dir, year, quarter,
                                os.path.basename(folder))

        print(folder, '-->', dest_dir)
        if not dry_run:
            parent_dir = os.path.dirname(dest_dir)
            _mkdir(parent_dir)
            shutil.move(folder, dest_dir)

def _mkdir(p):
    "The equivalent of 'mkdir -p' in shell."
    isdir = os.path.isdir

    stack = [os.path.abspath(p)]
    while not isdir(stack[-1]):
        parent_dir = os.path.dirname(stack[-1])
        stack.append(parent_dir)

    while stack:
        p = stack.pop()
        if not isdir(p):
            os.mkdir(p)

@click.command()
@click.argument('pattern', nargs=-1)
def list(pattern=()):
    "List the contents of the archive directory."
    # strategy: pick the intersection of all the patterns the user provides
    globs = ['*{0}*'.format(p) for p in pattern] + ['*']

    matches = []
    offset = len(PROJ_ARCHIVE) + 1
    for suffix in globs:
        glob_pattern = os.path.join(PROJ_ARCHIVE, '*', '*', suffix)
        matches.append(set(
            f[offset:] for f in glob.glob(glob_pattern)
        ))

    matches = reduce(lambda x, y: x.intersection(y),
                     matches)

    for m in sorted(matches):
        print(m)

@click.command()
@click.argument('folder')
def restore(folder):
    "Restore a project from the archive."
    if os.path.isdir(folder):
        bail('a folder of the same name already exists!')

    pattern = os.path.join(PROJ_ARCHIVE, '*', '*', folder)
    matches = glob.glob(pattern)
    if not matches:
        bail('no project matches: ' + folder)

    if len(matches) > 1:
        print('Warning: multiple matches, picking the most recent',
              file=sys.stderr)

    source = sorted(matches)[-1]
    print(source, '-->', folder)
    shutil.move(source, '.')

main.add_command(archive)
main.add_command(list)
main.add_command(restore)

if __name__ == '__main__':
    main()


'''High-level Client for interacting with the HBP Storage Service, providing
convenience functions for common operations'''

import logging
import os

from hbp_service_client.storage_service.api import ApiClient
from hbp_service_client.storage_service.exceptions import (
    StorageArgumentException, StorageNotFoundException)

L = logging.getLogger(__name__)

class Client(object):
    '''A client library for the Storage Service.

        Example:
            >>> #you'll have to have an access token ready
            >>> from hbp_service_client.storage_service.client import Client
            >>> storage_client = Client.new(my_access_token)
            >>> my_project_contents = storage_client.list('/my_project')
    '''

    __BROWSABLE_TYPES = ['project', 'folder']

    def __init__(self, client):
        '''
        Args:
           client: the low level api client
        '''
        self.api_client = client

    @classmethod
    def new(cls, access_token, environment='prod'):
        '''Create new storage service client.

            Arguments:
                environment(str): The service environment to be used for the client.
                    'prod' or 'dev'.
                access_token(str): The access token used to authenticate with the
                    service

            Returns:
                A storage_service.Client instance
        '''

        api_client = ApiClient.new(access_token, environment)
        return cls(api_client)

    def list(self, path):
        '''List the entities found directly under the given path.

        Args:
            path (str): The path of the entity to be listed. Must start with a '/'.

        Returns:
            The list of entity names directly under the given path:

                u'/12345/folder_1'

        Raises:
            StorageArgumentException: Invalid arguments
            StorageForbiddenException: Server response code 403
            StorageNotFoundException: Server response code 404
            StorageException: other 400-600 error codes
        '''

        self.__validate_storage_path(path)
        entity = self.api_client.get_entity_by_query(path=path)
        if entity['entity_type'] not in self.__BROWSABLE_TYPES:
            raise StorageArgumentException('The entity type "{0}" cannot be'
                                           'listed'.format(entity['entity_type']))
        entity_uuid = entity['uuid']
        file_names = []

        # get files
        more_pages = True
        page_number = 1
        while more_pages:
            response = self.api_client.list_folder_content(
                entity_uuid, page=page_number, ordering='name')
            more_pages = response['next'] is not None
            page_number += 1
            for child in response['results']:
                pattern = '/{name}' if child['entity_type'] == 'folder' else '{name}'
                file_names.append(pattern.format(name=child['name']))

        return file_names

    def download_file(self, path, target_path):
        '''Download a file from storage service to local disk.

        Existing files on the target path will be overwritten.
        The download is not recursive, as it only works on files.

        Args:
            path (str): The path of the entity to be downloaded. Must start with a '/'.

        Returns:
            None

        Raises:
            StorageArgumentException: Invalid arguments
            StorageForbiddenException: Server response code 403
            StorageNotFoundException: Server response code 404
            StorageException: other 400-600 error codes
        '''

        self.__validate_storage_path(path)
        entity = self.api_client.get_entity_by_query(path=path)
        if entity['entity_type'] != 'file':
            raise StorageArgumentException('Only file entities can be downloaded')

        signed_url = self.api_client.get_signed_url(entity['uuid'])
        response = self.api_client.download_signed_url(signed_url)

        with open(target_path, "wb") as output:
            for chunk in response.iter_content(chunk_size=1024):
                output.write(chunk)

    def exists(self, path):
        '''Check if a certain path exists in the storage service.

        Args:
            path (str): The path to be checked

        Returns:
            True if the path exists, False otherwise

        Raises:
            StorageArgumentException: Invalid arguments
            StorageForbiddenException: Server response code 403
            StorageNotFoundException: Server response code 404
            StorageException: other 400-600 error codes
        '''

        self.__validate_storage_path(path)
        try:
            metadata = self.api_client.get_entity_by_query(path=path)
        except StorageNotFoundException:
            return False

        return metadata and 'uuid' in metadata

    def get_parent(self, path):
        '''Get the parent entity of the entity pointed by the given path.

        Args:
            path (str): The path of the entity whose parent is needed

        Returns:
            A JSON object of the parent entity if found.

        Raises:
            StorageArgumentException: Invalid arguments
            StorageForbiddenException: Server response code 403
            StorageNotFoundException: Server response code 404
            StorageException: other 400-600 error codes
        '''

        self.__validate_storage_path(path, projects_allowed=False)
        path_steps = [step for step in path.split('/') if step]
        del path_steps[-1]
        parent_path = '/{0}'.format('/'.join(path_steps))
        return self.api_client.get_entity_by_query(path=parent_path)

    def mkdir(self, path):
        '''Create a folder in the storage service pointed by the given path.

        Args:
            path (str): The path of the folder to be created

        Returns:
            None

        Raises:
            StorageArgumentException: Invalid arguments
            StorageForbiddenException: Server response code 403
            StorageNotFoundException: Server response code 404
            StorageException: other 400-600 error codes
        '''

        self.__validate_storage_path(path, projects_allowed=False)
        parent_metadata = self.get_parent(path)
        self.api_client.create_folder(path.split('/')[-1], parent_metadata['uuid'])
        # no return necessary, function succeeds or we would have thrown an exception
        # before this point.

    def upload_file(self, local_file, dest_path, mimetype):
        '''Upload local file content to a storage service destination folder.

            Args:
                local_file(str)
                dest_path(str):
                    absolute Storage service path '/project' prefix is essential
                    suffix should be the name the file will have on in the destination folder
                    i.e.: /project/folder/.../file_name
                mimetype(str): set the contentType attribute

            Returns:
                The uuid of created file entity as string

            Raises:
                StorageArgumentException: Invalid arguments
                StorageForbiddenException: Server response code 403
                StorageNotFoundException: Server response code 404
                StorageException: other 400-600 error codes
        '''

        self.__validate_storage_path(dest_path)
        # get the paths of the target dir and the target file name
        if dest_path.endswith('/'):
            raise StorageArgumentException('Must specify target file name in dest_path argument')
        if local_file.endswith(os.path.sep):
            raise StorageArgumentException('Must specify source file name in local_file'
                                           ' argument, directory upload not supported')

        # create the file container
        new_file = self.api_client.create_file(
            name=dest_path.split('/').pop(),
            content_type=mimetype,
            parent=self.get_parent(dest_path)['uuid']
        )

        etag = self.api_client.upload_file_content(new_file['uuid'], source=local_file)
        new_file['etag'] = etag

        return new_file

    def delete(self, path):
        ''' Delete an entity from the storage service using its path.

            Args:
                path(str): The path of the entity to be delete

            Returns:
                The uuid of created file entity as string

            Raises:
                StorageArgumentException: Invalid arguments
                StorageForbiddenException: Server response code 403
                StorageNotFoundException: Server response code 404
                StorageException: other 400-600 error codes
        '''

        self.__validate_storage_path(path, projects_allowed=False)

        entity = self.api_client.get_entity_by_query(path=path)

        if entity['entity_type'] in self.__BROWSABLE_TYPES:
            # At this point it can only be a folder
            contents = self.api_client.list_folder_content(entity['uuid'])
            if contents['count'] > 0:
                raise StorageArgumentException(
                    'This method cannot delete non-empty folder. Please empty the folder first.')

            self.api_client.delete_folder(entity['uuid'])
        elif entity['entity_type'] == 'file':
            self.api_client.delete_file(entity['uuid'])

    @classmethod
    def __validate_storage_path(cls, path, projects_allowed=True):
        '''Validate a string as a valid storage path'''

        if not path or not isinstance(path, str) or path[0] != '/' or path == '/':
            raise StorageArgumentException(
                'The path must be a string, start with a slash (/), and be longer'
                ' than 1 character.')
        if not projects_allowed and len([elem for elem in path.split('/') if elem]) == 1:
            raise StorageArgumentException(
                'This method does not accept projects in the path.')