ctxis/lather

View on GitHub
lather/managers.py

Summary

Maintainability
F
2 wks
Test Coverage
# -*- coding: utf-8 -*-
import logging

from .decorators import require_client
from .decorators import require_default
from .exceptions import ObjectDoesNotExist
from .exceptions import ObjectsDoNotExist
from .exceptions import MultipleObjectReturned

from suds import WebFault

log = logging.getLogger('lather_client')


class Instance(object):
    def __init__(self, company, id=None, client=None):
        self.company = company
        self.id = id
        self.client = client

    def __repr__(self):
        path = '%s.%s' % (self.__class__.__module__, self.__class__.__name__)
        id = getattr(self, 'id', None)
        company = getattr(self, 'company', None)
        if id is not None and company is not None:
            return '<%s: (%s, %s)>' % (path, company, id)
        return '<%s>' % path


class BaseQuerySet(object):
    def __init__(self, manager, model):
        self.manager = manager
        self.model = model
        self.queryset = None
        self.client = self._connect()

    def __len__(self):
        return self.count()

    def __iter__(self):
        if not self.queryset:
            raise Exception('No queryset found')

        return iter(self.queryset)

    def __getitem__(self, item):
        if not self.queryset:
            raise Exception('No queryset found')

        return self.queryset[item]

    def __repr__(self):
        path = '%s.%s' % (self.__class__.__module__, self.__class__.__name__)
        queryset = getattr(self, 'queryset', None)
        if queryset:
            if self.count() >= 10:
                return "[%s '...(remaining elements truncated)...']" \
                       % ', '.join([str(r) for r in queryset])
            else:
                return '[%s]' % ', '.join([str(r) for r in queryset])
        return '<%s>' % path

    def _connect(self, *args, **kwargs):
        """
        Central method which makes the connection to the api
        """
        page = self.model._meta.page
        return self.model.client.connect(page, *args, **kwargs)

    def _query(self, client, method, **kwargs):
        """
        Central method which executes the api calls
        """
        params = {}
        if not method:
            raise TypeError('Method must be a string not None.')

        if self.model._meta.journal:
            params.update({'CurrentJnlBatchName': self.model._meta.journal})
            kwargs.update(params)

        if kwargs.get('attrs', None):
            params.update({self.model.__name__: kwargs.get('attrs')})
            return getattr(client, method)(**params)

        return getattr(client, method)(**kwargs)

    def _check_kwargs(self, method, **kwargs):
        page = self.model._meta.page
        params = self.model.client.get_service_params(method, page)

        # Because elements are suds.sax.text.Text, convert them to str
        params = [str(x[0]) for x in params]
        if params:
            if len(kwargs.keys()) > len(params):
                raise AttributeError('You have specified too many arguments.')
            for k in params:
                if k not in kwargs.keys():
                    raise AttributeError('You have to specify the arg: %s' % k)

    def _create_inst(self, response, obj=None):
        if obj:
            obj.populate_attrs(response)
            obj.add_id(getattr(response, self.model._meta.default_id))
            return

        inst = self.model()
        inst.populate_attrs(response)
        inst.add_id(getattr(response, self.model._meta.default_id))
        return inst

    def _get_response_id(self, response):
        return getattr(response, self.model._meta.default_id)

    @require_client
    def create(self, **kwargs):
        attrs = dict(attrs=kwargs)
        return self._query(self.client, self.model._meta.create, **attrs)

    @require_client
    def update(self, **kwargs):
        attrs = dict(attrs=kwargs)
        return self._query(self.client, self.model._meta.update, **attrs)

    @require_client
    def get(self, **kwargs):
        # When the result is None the suds raise AttributeError, handle
        # this error to return ObjectNotFound
        try:
            response = self._query(self.client, self.model._meta.get, **kwargs)
        except AttributeError:
            raise ObjectDoesNotExist('Object not found')

        if isinstance(response, list):
            raise MultipleObjectReturned('This function can return only one '
                                         'object but the query returned '
                                         'multiple results. Use all instead')

        return response

    @require_client
    def delete(self, **kwargs):
        id = kwargs.get(self.model._meta.default_id, None)
        if not id:
            raise Exception('You have to provide a keyword: %s'
                            % self.model._meta.default_id)

        if not self._query(self.client, self.model._meta.delete, **kwargs):
            return False

        return True

    @require_client
    def all(self):
        return iter(self._query(self.client, self.model._meta.all))

    @require_client
    def filter(self, **kwargs):
        index = 0
        params = {}
        filters = []

        service_params = self.client.get_service_params(self.model._meta.filter)

        if self.model._meta.journal:
            params.update({'CurrentJnlBatchName': self.model._meta.journal})
            index = 1
        params.update({'filter': filters})

        for k in kwargs.keys():
            filter = self.client.factory(service_params[index][1].type[0])
            setattr(filter, 'Field', k)
            setattr(filter, 'Criteria', kwargs.get(k))
            filters.append(filter)

        # TODO: Try pipe filters
        try:
            response = iter(
                getattr(self.client, self.model._meta.filter)(**params)[0]
            )
        except IndexError:
            raise ObjectsDoNotExist('Objects not found')

        return response

    def count(self):
        """
        Return the number of the results if the queryset exists
        """
        if not self.queryset:
            raise Exception('No queryset found')

        return len(list(self.queryset))


class QuerySet(BaseQuerySet):

    @require_client
    def create(self, obj=None, **kwargs):
        response = super(QuerySet, self).create(**kwargs)

        if obj:
            self._create_inst(response, obj)
        else:
            return self._create_inst(response)

    @require_client
    def update(self, obj=None, **kwargs):
        if obj:
            if self.queryset:
                for inst in self.queryset:
                    kwargs.update({self.model._meta.default_id:
                                       getattr(inst, self.model._meta.default_id)})
                    attrs = dict(attrs=kwargs)
                    response = super(QuerySet, self).update(**attrs)
                    self._create_inst(response, obj)
            else:
                kwargs.update({self.model._meta.default_id:
                                   getattr(obj, self.model._meta.default_id)})
                attrs = dict(attrs=kwargs)
                response = super(QuerySet, self).update(**attrs)
                self._create_inst(response, obj)
        else:
            attrs = dict(attrs=kwargs)
            response = super(QuerySet, self).update(**attrs)
            return self._create_inst(response)

    @require_client
    def get(self, **kwargs):
        #TODO: The following line cause one request more
        #self._check_kwargs(self.model._meta.get, **kwargs)

        self.queryset = []

        response = super(QuerySet, self).get(**kwargs)

        inst = self._create_inst(response)
        self.queryset.append(inst)

        return inst

    @require_client
    def delete(self, obj=None, **kwargs):
        if obj:
            kwargs = { self.model._meta.default_id:
                             getattr(obj, self.model._meta.default_id) }

        return super(QuerySet, self).delete(**kwargs)

    @require_client
    def all(self):
        self.queryset = []
        response = super(QuerySet, self).all()

        for r in response:
            self.queryset.append(self._create_inst(r))

        return self

    @require_client
    @require_default
    def get_or_create(self, **kwargs):
        created = False
        defaults = kwargs.pop('defaults', None)
        # Sometimes, the returning object doesn't contain all the attributes
        # from the defaults so if you specify a new value to an attribute
        # contained in the defaults but not in the returned object, it will not
        # saved, so we are going to add this attributes to the declared_fields
        if not self.model._meta.declared_fields:
            self.model._meta.add_declared_fields_from_names(defaults.keys())

        try:
            inst = self.get(**kwargs)
            created = False
        except ObjectDoesNotExist:
            # Update the defaults with the kwargs which contains the query
            # fields
            duplicate_keywords = []
            non_declared_fields = []
            for keyword in defaults.keys():
                if keyword in kwargs.keys():
                    duplicate_keywords.append(keyword)
                if self.model._meta.declared_fields:
                    if keyword not in self.model._meta.get_declared_field_names():
                        non_declared_fields.append(keyword)

            if duplicate_keywords:
                raise AttributeError('Duplicate keywords at kwargs and defaults: '
                                     '%s' % ', '.join(duplicate_keywords))

            if non_declared_fields:
                raise AttributeError('Some of the keys are not specified '
                                     'at the fields: %s'
                                     % ', '.join(non_declared_fields))
            kwargs.update(defaults)
            inst = self.create(**kwargs)
            created = True
        return inst, created

    @require_client
    @require_default
    def update_or_create(self, companies=None, **kwargs):
        created = False
        defaults = kwargs.pop('defaults', None)
        # Same as above
        if not self.model._meta.declared_fields:
            self.model._meta.add_declared_fields_from_names(defaults.keys())

        try:
            inst = self.get(**kwargs)
            self.update(inst, **defaults)
            created = False
        except ObjectDoesNotExist:
            # Update the defaults with the kwargs which contains the query
            # fields
            duplicate_keywords = []
            non_declared_fields = []
            for keyword in defaults.keys():
                if keyword in kwargs.keys():
                    duplicate_keywords.append(keyword)
                if self.model._meta.declared_fields:
                    if keyword not in self.model._meta.get_declared_field_names():
                        non_declared_fields.append(keyword)

            if duplicate_keywords:
                raise AttributeError('Duplicate keywords at kwargs and defaults: '
                                     '%s' % ', '.join(duplicate_keywords))

            if non_declared_fields:
                raise AttributeError('Some of the keys are not specified '
                                     'at the fields: %s'
                                     % ', '.join(non_declared_fields))
            kwargs.update(defaults)
            inst = self.create(**kwargs)
            created = True
        return inst, created

    @require_client
    def filter(self, **kwargs):
        self.queryset = []
        response = super(QuerySet, self).filter(**kwargs)
        for result in response:
            inst = self._create_inst(response)
            self.queryset.append(inst)

        return self


class NavQuerySet(BaseQuerySet):
    def __init__(self, manager, model):
        self.manager = manager
        self.model = model
        self.queryset = None

    @require_client
    def create(self, obj=None, companies=None, **kwargs):
        if obj:
            # Send the create action to the appropriate companies
            for instance in obj.get_instances():
                # If the instance object contains the id attribute then came
                # from the update_or_create() -> update() -> create() because
                # the user added some new companies, so the object contains
                # some instance objects with id
                if instance.id:
                    continue
                self.client = instance.client
                if not self.client:
                    self.client = self._connect(instance.company)

                response = super(NavQuerySet, self).create(**kwargs)
                obj.populate_attrs(response)
                obj.add_id(instance.company, self.client,
                            self._get_response_id(response))
        else:
            inst = None
            if not companies:
                companies = self.model.client.companies
            for company in companies:
                self.client = self._connect(company)
                response = super(NavQuerySet, self).create(**kwargs)
                if not inst:
                    inst = self.model()
                    inst.populate_attrs(response)
                inst.add_id(company, self.client,
                             self._get_response_id(response))

            return inst

    @require_client
    def update(self, obj=None, companies=None, delete=False, **kwargs):
        if obj:
            # This if will be true when the previous function is the
            # update_or_create
            if self.queryset:
                existing_companies = []
                for inst in self.queryset:
                    # Finally we will have:
                    # add_companies: contains new companies and will create
                    # update_companies: contains existing companies and will update
                    # delete_companies: contains deleted companies and will delete
                    add_companies = []
                    existing_companies.extend(inst.get_companies())
                    delete_companies = []
                    if companies:
                        add_companies = companies[:]
                        update_companies = []
                        for company in existing_companies:
                            if company in add_companies:
                                update_companies.append(company)
                                add_companies.pop(add_companies.index(company))
                            else:
                                delete_companies.append(company)
                    else:
                        update_companies = existing_companies[:]

                    new_add_companies = add_companies[:]
                    for company in add_companies:
                        for inst2 in self.queryset:
                            if company in inst2.get_companies():
                                if company not in update_companies:
                                    update_companies.append(company)
                                new_add_companies.pop(new_add_companies.index(company))

                    add_companies = new_add_companies[:]


                    for instance in inst.get_instances():
                        if instance.company in update_companies:
                            kwargs.update(
                                { self.model._meta.default_id: instance.id })
                            #self.client = instance.client
                            #if not self.client:
                            self.client = self._connect(instance.company)

                            response = super(NavQuerySet, self).update(**kwargs)
                            inst.populate_attrs(response)
                            inst.add_id(instance.company, self.client,
                                         self._get_response_id(response))
                        elif instance.company in delete_companies:
                            if isinstance(delete, dict):
                                delete.update(
                                    { self.model._meta.default_id: instance.id })
                                #self.client = instance.client
                                #if not self.client:
                                self.client = self._connect(instance.company)

                                response = super(NavQuerySet, self).update(**delete)
                                inst.remove_id(instance)
                            if delete == True:
                                tmp_dict = {self.model._meta.default_id:
                                                str(instance.id)}
                                self.delete(**tmp_dict)
                                inst.remove_id(instance)
                    # Now create the new entries
                    if add_companies:
                        inst.add_companies(add_companies)
                        inst.save()
            else:
                for instance in obj.get_instances():
                    kwargs.update({self.model._meta.default_id: instance.id})
                    self.client = instance.client
                    if not self.client:
                        self.client = self._connect(instance.company)

                    response = super(NavQuerySet, self).update(**kwargs)
                    obj.populate_attrs(response)
                    obj.add_id(instance.company, self.client,
                                self._get_response_id(response))
        else:
            inst = None
            ids = kwargs.pop(self.model._meta.default_id, None)
            if not ids:
                raise TypeError("You have to provide a keyword: %s"
                                % self.model._meta.default_id)

            if isinstance(ids, list):
                for instance in ids:
                    if not isinstance(instance, Instance):
                        raise TypeError('The list must contain Key objects')

                    self.client = instance.client
                    if not self.client:
                        self.client = self._connect(instance.company)
                    kwargs.update({self.model._meta.default_id: instance.id})
                    response = super(NavQuerySet, self).update(**kwargs)
                    if not inst:
                        inst = self.model()
                        inst.populate_attrs(response)
                    inst.add_id(instance.company, self.client,
                                 self._get_response_id(response))
            else:
                # TODO: Does not work for some reason, maybe needs the company
                if not companies:
                    companies = self.model.client.companies
                skipped = 0
                for company in companies:
                    self.client = self._connect(company)
                    kwargs.update({self.model._meta.default_id: ids})
                    # because whenever tries to update something which
                    # doesn't exist raise this error
                    try:
                        response = super(NavQuerySet, self).update(**kwargs)
                    except WebFault:
                        skipped += 1
                        continue

                    if not inst:
                        inst = self.model()
                        inst.populate_attrs(response)
                    inst.add_id(company, self.client,
                                 self._get_response_id(response))

                if skipped == len(companies):
                    raise ObjectDoesNotExist('Object not found')

            return inst

    @require_client
    def get(self, **kwargs):
        #TODO: The following line cause one request more
        #self._check_kwargs(self.model._meta.get, **kwargs)

        self.queryset = []
        companies = self.model.client.companies
        for company in companies:
            self.client = self._connect(company)
            try:
                response = super(NavQuerySet, self).get(**kwargs)
            except ObjectDoesNotExist:
                continue

            inst = self.model()
            inst.populate_attrs(response)
            inst.add_id(company, self.client, self._get_response_id(response))

            if self.queryset:
                found = False
                for obj in self.queryset:
                    # TODO: Test keys (if are the same at all companies ) and
                    # TODO: give Warning about this
                    if inst == obj:
                        obj_keys = obj.get_instances()
                        inst_keys = inst.get_instances()
                        obj_keys.extend(inst_keys)
                        found = True

                if not found:
                    self.queryset.append(inst)

            else:
                self.queryset.append(inst)

        if len(self.queryset) > 1:
            return self
        elif len(self.queryset) == 1:
            return self.queryset[0]
        else:
            raise ObjectDoesNotExist('Object not found')

    @require_client
    def delete(self, obj=None, companies=None, **kwargs):
        success = True
        if obj:
            # Send the delete action to the appropriate companies
            for instance in obj.get_instances():
                self.client = instance.client
                if not self.client:
                    self.client = self._connect(instance.company)
                tmp_dict = {self.model._meta.default_id: instance.id}
                if not super(NavQuerySet, self).delete(**tmp_dict):
                    success = False
        else:
            # Send the delete action to all the companies
            ids = kwargs.get(self.model._meta.default_id, None)
            if self.queryset and not ids:
                tmp_list = self.queryset[:]
                for inst in self.queryset:
                    success = True
                    for instance in inst.get_instances():
                        if not isinstance(instance, Instance):
                            raise TypeError(
                                'The list must contain Key objects')
                        self.client = instance.client
                        if not self.client:
                            self.client = self._connect(instance.company)
                        tmp_dict = {self.model._meta.default_id: instance.id}
                        if not super(NavQuerySet, self).delete(**tmp_dict):
                            success = False
                    if success:
                        tmp_list.pop(tmp_list.index(inst))
                # Assign the undeleted objects
                self.queryset = tmp_list[:]
            else:
                if not ids:
                    raise Exception('You have to provide a keyword: %s'
                                    % self.model._meta.default_id)

                if isinstance(ids, list):
                    for instance in ids:
                        if not isinstance(instance, Instance):
                            raise TypeError(
                                'The list must contain Key objects')
                        self.client = instance.client
                        if not self.client:
                            self.client = self._connect(instance.company)
                        tmp_dict = {self.model._meta.default_id: instance.id}
                        if not super(NavQuerySet, self).delete(**tmp_dict):
                            success = False

                else:
                    if not companies:
                        companies = self.model.client.companies
                    skipped = 0
                    for company in companies:
                        self.client = self._connect(company)
                        # because whenever tries to delete something which
                        # doesn't exist raise this error
                        try:
                            if not super(NavQuerySet, self).delete(**kwargs):
                                success = False
                        except WebFault:
                            skipped += 1
                            continue

                    if skipped == len(companies):
                        success = False
        return success

    @require_client
    def all(self):
        raise NotImplemented("Nav doesn't support any method which returns "
                             "all objects")

    @require_client
    @require_default
    def get_or_create(self, companies=None, **kwargs):
        created = False
        defaults = kwargs.pop('defaults', None)
        # Sometimes, the returning object doesn't contain all the attributes
        # from the defaults so if you specify a new value to an attribute
        # contained in the defaults but not in the returned object, it will not
        # saved, so we are going to add this attributes to the declared_fields
        if not self.model._meta.declared_fields:
            self.model._meta.add_declared_fields_from_names(defaults.keys())

        try:
            inst = self.get(**kwargs)
            created = False
        except ObjectDoesNotExist:
            # Update the defaults with the kwargs which contains the query
            # fields
            duplicate_keywords = []
            non_declared_fields = []
            for keyword in defaults.keys():
                if keyword in kwargs.keys():
                    duplicate_keywords.append(keyword)
                if self.model._meta.declared_fields:
                    if keyword not in self.model._meta.get_declared_field_names():
                        non_declared_fields.append(keyword)

            if duplicate_keywords:
                raise AttributeError('Duplicate keywords at kwargs and defaults: '
                                     '%s' % ', '.join(duplicate_keywords))

            if non_declared_fields:
                raise AttributeError('Some of the keys are not specified '
                                     'at the fields: %s'
                                     % ', '.join(non_declared_fields))
            kwargs.update(defaults)
            inst = self.create(companies=companies, **kwargs)
            created = True
        return inst, created

    @require_client
    @require_default
    def update_or_create(self, companies=None, delete=False, **kwargs):
        created = False
        defaults = kwargs.pop('defaults', None)
        # Same as above
        if not self.model._meta.declared_fields:
            self.model._meta.add_declared_fields_from_names(defaults.keys())

        try:
            inst = self.get(**kwargs)
            self.update(inst, companies, delete, **defaults)
            created = False
        except ObjectDoesNotExist:
            # Update the defaults with the kwargs which contains the query
            # fields
            duplicate_keywords = []
            non_declared_fields = []
            for keyword in defaults.keys():
                if keyword in kwargs.keys():
                    duplicate_keywords.append(keyword)
                if self.model._meta.declared_fields:
                    if keyword not in self.model._meta.get_declared_field_names():
                        non_declared_fields.append(keyword)

            if duplicate_keywords:
                raise AttributeError('Duplicate keywords at kwargs and defaults: '
                                     '%s' % ', '.join(duplicate_keywords))

            if non_declared_fields:
                raise AttributeError('Some of the keys are not specified '
                                     'at the fields: %s'
                                     % ', '.join(non_declared_fields))
            kwargs.update(defaults)
            inst = self.create(companies=companies, **kwargs)
            created = True
        return inst, created

    @require_client
    def filter(self, **kwargs):
        self.queryset = []
        companies = self.model.client.companies
        for company in companies:
            self.client = self._connect(company)
            try:
                response = super(NavQuerySet, self).filter(**kwargs)
            except ObjectsDoNotExist:
                continue

            for result in response:
                log.debug('[%s] From the company %s we got the result: %s'
                          % (log.name.upper(), company, result))
                inst = self.model()
                inst.populate_attrs(result)
                inst.add_id(company, self.client,
                             self._get_response_id(result))

                if self.queryset:
                    found = False
                    for obj in self.queryset:
                        if inst == obj:
                            obj_keys = obj.get_instances()
                            inst_keys = inst.get_instances()
                            obj_keys.extend(inst_keys)
                            found = True

                    if not found:
                        self.queryset.append(inst)
                else:
                    self.queryset.append(inst)

        return self


class Manager(object):
    queryset_class = QuerySet

    def __init__(self, model):
        self.model = model

    def __getattr__(self, item):
        log.debug('[%s] Calling manager __getattr__: %s' % (log.name.upper(),
                                                            item))
        if hasattr(self.queryset_class, item):
            def wrapper(*args, **kwargs):
                log.debug('[%s] called with %r and %r' % (log.name.upper(),
                                                          args, kwargs))
                queryset = self.queryset_class(self, self.model)
                func = getattr(queryset, item)
                return func(*args, **kwargs)

            return wrapper
        raise AttributeError(item)


class NavManager(Manager):
    queryset_class = NavQuerySet