Ejemplo n.º 1
0
 def test_enum_key_should_have_right_format(self):
     with assert_raises(ValueError):
         Enum(1, 2)
     with assert_raises(ValueError):
         Enum('1A', 'B')
     with assert_raises(ValueError):
         Enum('A-B', 'B')
     Enum('A_B_3', 'B')
Ejemplo n.º 2
0
 def test_enum_should_contain_only_defined_values(self):
     enum = Enum('A', 'B')
     assert_equal(enum.A, 'A')
     assert_equal(enum.B, 'B')
     assert_equal(list(enum), ['A', 'B'])
     assert_equal(enum.all, ('A', 'B'))
     assert_equal(enum.get_name('A'), 'A')
     with assert_raises(AttributeError):
         enum.C  # pylint: disable=W0104
     assert_is_none(enum.get_name('C'))
     assert_in('A', enum)
     assert_in(enum.A, enum)
     assert_not_in('C', enum)
Ejemplo n.º 3
0
 def test_enum_with_distinct_key_and_value_should_contain_only_defined_values(
         self):
     enum = Enum(('A', 'c'), ('B', 'd'))
     assert_equal(enum.A, 'c')
     assert_equal(enum.B, 'd')
     assert_equal(list(enum), ['c', 'd'])
     assert_equal(enum.all, ('c', 'd'))
     assert_equal(enum.get_name('c'), 'A')
     with assert_raises(AttributeError):
         enum.C  # pylint: disable=W0104
     assert_is_none(enum.get_name('f'))
     assert_in('c', enum)
     assert_in(enum.A, enum)
     assert_not_in('A', enum)
Ejemplo n.º 4
0
 def test_enum_should_contain_only_defined_values(self):
     enum = Enum(
         'A', 'B'
     )
     assert_equal(enum.A, 'A')
     assert_equal(enum.B, 'B')
     assert_equal(list(enum), ['A', 'B'])
     assert_equal(enum.all, ('A', 'B'))
     assert_equal(enum.get_name('A'), 'A')
     with assert_raises(AttributeError):
         enum.C  # pylint: disable=W0104
     assert_is_none(enum.get_name('C'))
     assert_in('A', enum)
     assert_in(enum.A, enum)
     assert_not_in('C', enum)
Ejemplo n.º 5
0
 def test_enum_with_distinct_key_and_value_should_contain_only_defined_values(self):
     enum = Enum(
         ('A', 'c'), ('B', 'd')
     )
     assert_equal(enum.A, 'c')
     assert_equal(enum.B, 'd')
     assert_equal(list(enum), ['c', 'd'])
     assert_equal(enum.all, ('c', 'd'))
     assert_equal(enum.get_name('c'), 'A')
     with assert_raises(AttributeError):
         enum.C  # pylint: disable=W0104
     assert_is_none(enum.get_name('f'))
     assert_in('c', enum)
     assert_in(enum.A, enum)
     assert_not_in('A', enum)
Ejemplo n.º 6
0
class Serializer:
    """
    REST serializer and deserializer, firstly is data serialized to standard python data types and after that is
    used convertor for final serialization
    """

    SERIALIZATION_TYPES = Enum('VERBOSE', 'RAW', 'BOTH')

    def __init__(self, request=None):
        self.request = request

    def _get_serializer(self, data):
        return get_serializer(data, request=self.request)

    def _data_to_python(self, data, serialization_format, lazy=False, **kwargs):
        return self._get_serializer(data).serialize(data, serialization_format, **kwargs)

    def _lazy_data_to_python(self, data, serialization_format, lazy=False, **kwargs):
        if lazy:
            return LazySerializedData(
                self._get_serializer(data), data, serialization_format, lazy=lazy, **kwargs
            )
        else:
            return self._data_to_python(data, serialization_format, lazy=lazy, **kwargs)

    def serialize(self, data, serialization_format, **kwargs):
        raise NotImplementedError

    def deserialize(self, data):
        raise NotImplementedError
Ejemplo n.º 7
0
class Serializer(object):
    """
    REST serializer and deserializer, firstly is data serialized to standard python data types and after that is
    used convertor for final serialization
    """

    SERIALIZATION_TYPES = Enum('VERBOSE', 'RAW', 'BOTH')

    def _get_resource(self, request, obj):
        from .resource import typemapper

        resource_class = typemapper.get(type(obj))
        if resource_class:
            return resource_class(request)

    def _to_python_via_resource(self, request, thing, serialization_format,
                                **kwargs):
        resource = self._get_resource(request, thing)
        if resource:
            thing._resource = resource
            return resource.serializer(resource)._to_python(
                request, thing, serialization_format, **kwargs)
        else:
            return None

    def _find_to_serializer(self, thing):
        for serializer in value_serializers:
            if serializer._can_transform_to_python(thing):
                return serializer

    def _to_python_chain(self, request, thing, serialization_format, **kwargs):
        if not hasattr(thing, '_resource'):
            result = self._to_python_via_resource(request, thing,
                                                  serialization_format,
                                                  **kwargs)
            if result:
                return result
        serializer = self._find_to_serializer(thing)
        if serializer:
            return serializer._to_python(request, thing, serialization_format,
                                         **kwargs)
        raise NotImplementedError('Serializer not found for %s' % thing)

    def _to_python(self, request, thing, serialization_format, **kwargs):
        return self._to_python_chain(request, thing, serialization_format,
                                     **kwargs)

    def _can_transform_to_python(self, thing):
        raise NotImplementedError
Ejemplo n.º 8
0
from chamber.utils.datastructures import Enum


DIRECTION = Enum(
    'ASC',
    'DESC',
)
Ejemplo n.º 9
0
from collections import OrderedDict
import re

from chamber.utils.datastructures import Enum
from django.apps import apps
from django.conf import settings as django_settings
from django.utils.module_loading import import_string

from attrdict import AttrDict

DEFAULT_SENDER_BACKEND_NAME = 'default'

CONTROLLER_TYPES = Enum(
    'SMS',
    'EMAIL',
    'DIALER',
    'PUSH_NOTIFICATION',
)

DEFAULTS = {
    # SMS configuration
    'SMS_BACKENDS': {
        DEFAULT_SENDER_BACKEND_NAME: {
            'backend': 'pymess.backend.sms.dummy.DummySMSBackend'
        }
    },
    'SMS_DEFAULT_SENDER_BACKEND_NAME': DEFAULT_SENDER_BACKEND_NAME,
    'SMS_BACKEND_ROUTER': 'pymess.backend.routers.DefaultBackendRouter',
    'SMS_TEMPLATE_MODEL': 'pymess.SMSTemplate',
    'SMS_USE_ACCENT': False,
    'SMS_DEFAULT_PHONE_CODE': None,
Ejemplo n.º 10
0
class ATSSMSBackend(SMSBackend):
    """
    SMS backend that implements ATS operator service https://www.atspraha.cz/
    Backend supports check SMS delivery
    """

    REQUEST_TYPES = Enum(
        'SMS',
        'DELIVERY_REQUEST',
    )

    TEMPLATES = {
        'base': 'pymess/sms/ats/base.xml',
        REQUEST_TYPES.SMS: 'pymess/sms/ats/sms.xml',
        REQUEST_TYPES.DELIVERY_REQUEST: 'pymess/sms/ats/delivery_request.xml',
    }

    class ATSSendingError(Exception):
        pass

    ATS_STATES = ChoicesNumEnum(
        # SMS delivery receipts
        ('NOT_FOUND', _('not found'), 20),
        ('NOT_SENT', _('not sent yet'), 21),
        ('SENT', _('sent'), 22),
        ('DELIVERED', _('delivered'), 23),
        ('NOT_DELIVERED', _('not delivered'), 24),
        ('UNKNOWN', _('not able to determine the state'), 25),
        # Authentication
        ('AUTHENTICATION_FAILED', _('authentication failed'), 100),
        # Internal errors
        ('DB_ERROR', _('DB error'), 200),
        # Request states
        ('OK', _('SMS is OK and ready to be sent'), 0),
        ('UNSPECIFIED_ERROR', _('unspecified error'), 1),
        ('BATCH_WITH_NOT_UNIQUE_UNIQ',
         _('one of the requests has not unique "uniq"'), 300),
        ('SMS_NOT_UNIQUE_UNIQ', _('SMS has not unique "uniq"'), 310),
        ('SMS_NO_KW', _('SMS lacks keyword'), 320),
        ('KW_INVALID', _('keyword not valid'), 321),
        ('NO_SENDER', _('no sender specified'), 330),
        ('SENDER_INVALID', _('sender not valid'), 331),
        ('MO_PR_NOT_ALLOWED', _('MO PR SMS not allowed'), 332),
        ('MT_PR_NOT_ALLOWED', _('MT PR SMS not allowed'), 333),
        ('MT_PR_DAILY_LIMIT', _('MT PR SMS daily limit exceeded'), 334),
        ('MT_PR_TOTAL_LIMIT', _('MT PR SMS total limit exceeded'), 335),
        ('GEOGRAPHIC_NOT_ALLOWED', _('geographic number is not allowed'), 336),
        ('MT_SK_NOT_ALLOWED', _('MT SMS to Slovakia not allowed'), 337),
        ('SHORTCODES_NOT_ALLOWED', _('shortcodes not allowed'), 338),
        ('UNKNOWN_SENDER', _('sender is unknown'), 339),
        ('UNSPECIFIED_SMS_TYPE', _('type of SMS not specified'), 340),
        ('TOO_LONG', _('SMS too long'), 341),
        ('TOO_MANY_PARTS', _('too many SMS parts (max. is 10)'), 342),
        ('WRONG_SENDER_OR_RECEIVER', _('wrong number of sender/receiver'),
         343),
        ('NO_RECIPIENT_OR_WRONG_FORMAT',
         _('recipient is missing or in wrong format'), 350),
        ('TEXTID_NOT_ALLOWED', _('using "textid" is not allowed'), 360),
        ('WRONG_TEXTID', _('"textid" is in wrong format'), 361),
        ('LONG_SMS_TEXTID_NOT_ALLOWED',
         _('long SMS with "textid" not allowed'), 362),
        # XML errors
        ('XML_MISSING', _('XML body missing'), 701),
        ('XML_UNREADABLE', _('XML is not readable'), 702),
        ('WRONG_HTTP_METHOD', _('unknown HTTP method or not HTTP POST'), 703),
        ('XML_INVALID', _('XML invalid'), 705),
    )

    ATS_STATES_MAPPING = {
        ATS_STATES.NOT_FOUND: OutputSMSMessage.STATE.ERROR,
        ATS_STATES.NOT_SENT: OutputSMSMessage.STATE.SENDING,
        ATS_STATES.SENT: OutputSMSMessage.STATE.SENT,
        ATS_STATES.DELIVERED: OutputSMSMessage.STATE.DELIVERED,
        ATS_STATES.NOT_DELIVERED: OutputSMSMessage.STATE.ERROR,
        ATS_STATES.OK: OutputSMSMessage.STATE.SENDING,
        ATS_STATES.UNSPECIFIED_ERROR: OutputSMSMessage.STATE.ERROR,
        ATS_STATES.BATCH_WITH_NOT_UNIQUE_UNIQ: OutputSMSMessage.STATE.ERROR,
        ATS_STATES.SMS_NOT_UNIQUE_UNIQ: OutputSMSMessage.STATE.ERROR,
        ATS_STATES.SMS_NO_KW: OutputSMSMessage.STATE.ERROR,
        ATS_STATES.KW_INVALID: OutputSMSMessage.STATE.ERROR,
        ATS_STATES.NO_SENDER: OutputSMSMessage.STATE.ERROR,
        ATS_STATES.SENDER_INVALID: OutputSMSMessage.STATE.ERROR,
        ATS_STATES.MO_PR_NOT_ALLOWED: OutputSMSMessage.STATE.ERROR,
        ATS_STATES.MT_SK_NOT_ALLOWED: OutputSMSMessage.STATE.ERROR,
        ATS_STATES.SHORTCODES_NOT_ALLOWED: OutputSMSMessage.STATE.ERROR,
        ATS_STATES.UNKNOWN_SENDER: OutputSMSMessage.STATE.ERROR,
        ATS_STATES.UNSPECIFIED_SMS_TYPE: OutputSMSMessage.STATE.ERROR,
        ATS_STATES.TOO_LONG: OutputSMSMessage.STATE.ERROR,
        ATS_STATES.TOO_MANY_PARTS: OutputSMSMessage.STATE.ERROR,
        ATS_STATES.WRONG_SENDER_OR_RECEIVER: OutputSMSMessage.STATE.ERROR,
        ATS_STATES.NO_RECIPIENT_OR_WRONG_FORMAT: OutputSMSMessage.STATE.ERROR,
        ATS_STATES.TEXTID_NOT_ALLOWED: OutputSMSMessage.STATE.ERROR,
        ATS_STATES.WRONG_TEXTID: OutputSMSMessage.STATE.ERROR,
        ATS_STATES.LONG_SMS_TEXTID_NOT_ALLOWED: OutputSMSMessage.STATE.ERROR,
    }

    config = AttrDict({
        'UNIQ_PREFIX': '',
        'VALIDITY': 60,
        'TEXTID': None,
        'URL': 'http://fik.atspraha.cz/gwfcgi/XMLServerWrapper.fcgi',
        'OPTID': '',
        'TIMEOUT': 5,  # 5s
    })

    def _get_extra_sender_data(self):
        return {
            'prefix': self.config.UNIQ_PREFIX,
            'validity': self.config.VALIDITY,
            'kw': self.config.PROJECT_KEYWORD,
            'textid': self.config.TEXTID,
        }

    def get_extra_message_kwargs(self):
        return {
            'sender': self.config.OUTPUT_SENDER_NUMBER,
        }

    def _serialize_messages(self, messages, request_type):
        """
        Serialize SMS messages to the XML
        :param messages: list of SMS messages
        :param request_type: type of the request to the ATS operator
        :return: serialized XML message that will be sent to the ATS service
        """
        return render_to_string(
            self.TEMPLATES['base'], {
                'username':
                self.config.USERNAME,
                'password':
                self.config.PASSWORD,
                'template_type':
                self.TEMPLATES[request_type],
                'messages':
                messages,
                'prefix':
                str(self.config.UNIQ_PREFIX) + '-',
                'sender':
                self.config.OUTPUT_SENDER_NUMBER,
                'dlr':
                1,
                'validity':
                self.config.VALIDITY,
                'kw':
                self.config.PROJECT_KEYWORD,
                'billing':
                0,
                'extra':
                mark_safe(' textid="{textid}"'.format(
                    textid=self.config.TEXTID)) if self.config.TEXTID else '',
            })

    def _send_requests(self,
                       messages,
                       request_type,
                       is_sending=False,
                       **change_sms_kwargs):
        """
        Performs the actual POST request for input messages and request type.
        :param messages: list of SMS messages
        :param request_type: type of the request
        :param is_sending: True if method is called after sending message
        :param change_sms_kwargs: extra kwargs that will be stored to the message object
        """
        requests_xml = self._serialize_messages(messages, request_type)
        try:
            resp = generate_session(slug='pymess - ATS SMS',
                                    related_objects=list(messages)).post(
                                        self.config.URL,
                                        data=requests_xml,
                                        headers={'Content-Type': 'text/xml'},
                                        timeout=self.config.TIMEOUT)
            if resp.status_code != 200:
                raise self.ATSSendingError(
                    'ATS operator returned invalid response status code: {}'.
                    format(resp.status_code))
            self._update_sms_states_from_response(
                messages, self._parse_response_codes(resp.text), is_sending,
                **change_sms_kwargs)
        except requests.exceptions.RequestException as ex:
            raise self.ATSSendingError(
                'ATS operator returned returned exception: {}'.format(str(ex)))

    def _update_sms_states_from_response(self,
                                         messages,
                                         parsed_response,
                                         is_sending=False,
                                         **change_sms_kwargs):
        """
        Higher-level function performing serialization of ATS requests, parsing ATS server response and updating
        SMS messages state according the received response.
        :param messages: list of SMS messages
        :param parsed_response: parsed HTTP response from the ATS service
        :param is_sending: True if update is called after sending message
        :param change_sms_kwargs: extra kwargs that will be stored to the message object
        """

        messages_dict = {message.pk: message for message in messages}

        missing_uniq = set(messages_dict.keys()) - set(parsed_response.keys())
        if missing_uniq:
            raise self.ATSSendingError(
                'ATS operator not returned SMS info with uniq: {}'.format(
                    ', '.join(map(str, missing_uniq))))

        extra_uniq = set(parsed_response.keys()) - set(messages_dict.keys())
        if extra_uniq:
            raise self.ATSSendingError(
                'ATS operator returned SMS info about unknown uniq: {}'.format(
                    ', '.join(map(str, extra_uniq))))

        for uniq, ats_state in parsed_response.items():
            sms = messages_dict[uniq]
            state = self.ATS_STATES_MAPPING.get(ats_state)
            error = self.ATS_STATES.get_label(
                ats_state) if state == OutputSMSMessage.STATE.ERROR else None
            if is_sending:
                if error:
                    self._update_message_after_sending_error(
                        sms,
                        state=state,
                        error=error,
                        extra_sender_data={'sender_state': ats_state},
                        **change_sms_kwargs)
                else:
                    self._update_message_after_sending(
                        sms,
                        state=state,
                        extra_sender_data={'sender_state': ats_state},
                        **change_sms_kwargs)
            else:
                self._update_message(
                    sms,
                    state=state,
                    error=error,
                    extra_sender_data={'sender_state': ats_state},
                    **change_sms_kwargs)

    def publish_messages(self, messages):
        self._send_requests(messages,
                            request_type=self.REQUEST_TYPES.SMS,
                            is_sending=True,
                            sent_at=timezone.now())

    def publish_message(self, message):
        try:
            self._send_requests([message],
                                request_type=self.REQUEST_TYPES.SMS,
                                is_sending=True,
                                sent_at=timezone.now())
        except self.ATSSendingError as ex:
            self._update_message_after_sending_error(
                message,
                state=OutputSMSMessage.STATE.ERROR,
                error=str(ex),
            )
        except requests.exceptions.RequestException as ex:
            # Service is probably unavailable sending will be retried
            self._update_message_after_sending_error(message, error=str(ex))
            # Do not re-raise caught exception. Re-raise exception causes transaction rollback (lost of information
            # about exception).

    def _parse_response_codes(self, xml):
        """
        Finds all <code> tags in the given XML and returns a mapping "uniq" -> "response code" for all SMS.
        In case of an error, the error is logged.
        :param xml: XML from the ATL response
        :return: dictionary with pair {SMS uniq: response status code}
        """

        soup = BeautifulSoup(xml, 'html.parser')
        code_tags = soup.find_all('code')

        error_message = ', '.join([
            (str(self.ATS_STATES.get_label(c)) if c in self.ATS_STATES.all else
             'ATS returned an unknown state {}.'.format(c))
            for c in [
                int(error_code.string)
                for error_code in code_tags if not error_code.attrs.get('uniq')
            ]
        ], )

        if error_message:
            raise self.ATSSendingError(
                'Error returned from ATS operator: {}'.format(error_message))

        return {
            int(code.attrs['uniq'].lstrip(str(self.config.UNIQ_PREFIX) + '-')):
            int(code.string)
            for code in code_tags if code.attrs.get('uniq')
        }

    def update_sms_states(self, messages):
        self._send_requests(messages,
                            request_type=self.REQUEST_TYPES.DELIVERY_REQUEST)
Ejemplo n.º 11
0
from pyston.utils import LOOKUP_SEP

from .exceptions import FilterValueError, OperatorFilterError

OPERATORS = Enum(
    ('GT', 'gt'),
    ('LT', 'lt'),
    ('EQ', 'eq'),
    ('NEQ', 'neq'),
    ('LTE', 'lte'),
    ('GTE', 'gte'),
    ('CONTAINS', 'contains'),
    ('ICONTAINS', 'icontains'),
    ('RANGE', 'range'),
    ('EXACT', 'exact'),
    ('IEXACT', 'iexact'),
    ('STARTSWITH', 'startswith'),
    ('ISTARTSWITH', 'istartswith'),
    ('ENDSWITH', 'endswith'),
    ('IENDSWITH', 'iendswith'),
    ('IN', 'in'),
    ('RANGE', 'range'),
    ('ALL', 'all'),
    ('ISNULL', 'isnull'),
)

NONE_LABEL = _('(None)')


class Operator:
Ejemplo n.º 12
0
class SMSOperatorBackend(SMSBackend):
    """
    SMS backend that implements ATS operator service https://www.sms-operator.cz/
    Backend supports check SMS delivery
    """
    class SMSOperatorSendingError(Exception):
        pass

    REQUEST_TYPES = Enum(
        'SMS',
        'DELIVERY_REQUEST',
    )

    TEMPLATES = {
        'base':
        'pymess/sms/sms_operator/base.xml',
        REQUEST_TYPES.SMS:
        'pymess/sms/sms_operator/sms.xml',
        REQUEST_TYPES.DELIVERY_REQUEST:
        'pymess/sms/sms_operator/delivery_request.xml',
    }

    SMS_OPERATOR_STATES = ChoicesNumEnum(
        # SMS states
        ('DELIVERED', _('delivered'), 0),
        ('NOT_DELIVERED', _('not delivered'), 1),
        ('PHONE_NUMBER_NOT_EXISTS', _('number not exists'), 2),

        # SMS not moved to GSM operator
        ('TIMEOUTED', _('timeouted'), 3),
        ('INVALID_PHONE_NUMBER', _('wrong number format'), 4),
        ('ANOTHER_ERROR', _('another error'), 5),
        ('EVENT_ERROR', _('event error'), 6),
        ('SMS_TEXT_TOO_LONG', _('SMS text too long'), 7),

        # SMS with more parts
        ('PARTLY_DELIVERED', _('partly delivered'), 10),
        ('UNKNOWN', _('unknown'), 11),
        ('PARLY_DELIVERED_PARTLY_UNKNOWN',
         _('partly delivered, partly unknown'), 12),
        ('PARTLY_NOT_DELIVERED_PARTLY_UNKNOWN',
         _('partly not delivered, partly unknown'), 13),
        ('PARTLY_DELIVERED_PARTLY_NOT_DELIVERED_PARTLY_UNKNOWN',
         _('partly delivered, partly not delivered, partly unknown'), 14),
        ('NOT_FOUND', _('not found'), 15),
    )

    SMS_OPERATOR_STATES_MAPPING = {
        SMS_OPERATOR_STATES.DELIVERED:
        OutputSMSMessage.STATE.DELIVERED,
        SMS_OPERATOR_STATES.NOT_DELIVERED:
        OutputSMSMessage.STATE.ERROR_UPDATE,
        SMS_OPERATOR_STATES.PHONE_NUMBER_NOT_EXISTS:
        OutputSMSMessage.STATE.ERROR_UPDATE,
        SMS_OPERATOR_STATES.TIMEOUTED:
        OutputSMSMessage.STATE.ERROR_UPDATE,
        SMS_OPERATOR_STATES.INVALID_PHONE_NUMBER:
        OutputSMSMessage.STATE.ERROR_UPDATE,
        SMS_OPERATOR_STATES.ANOTHER_ERROR:
        OutputSMSMessage.STATE.ERROR_UPDATE,
        SMS_OPERATOR_STATES.EVENT_ERROR:
        OutputSMSMessage.STATE.ERROR_UPDATE,
        SMS_OPERATOR_STATES.SMS_TEXT_TOO_LONG:
        OutputSMSMessage.STATE.ERROR_UPDATE,
        SMS_OPERATOR_STATES.PARTLY_DELIVERED:
        OutputSMSMessage.STATE.ERROR_UPDATE,
        SMS_OPERATOR_STATES.UNKNOWN:
        OutputSMSMessage.STATE.SENDING,
        SMS_OPERATOR_STATES.PARLY_DELIVERED_PARTLY_UNKNOWN:
        OutputSMSMessage.STATE.SENDING,
        SMS_OPERATOR_STATES.PARTLY_NOT_DELIVERED_PARTLY_UNKNOWN:
        OutputSMSMessage.STATE.SENDING,
        SMS_OPERATOR_STATES.PARTLY_DELIVERED_PARTLY_NOT_DELIVERED_PARTLY_UNKNOWN:
        OutputSMSMessage.STATE.SENDING,
        SMS_OPERATOR_STATES.NOT_FOUND:
        OutputSMSMessage.STATE.ERROR_UPDATE,
    }

    config = AttrDict({
        'URL': 'https://www.sms-operator.cz/webservices/webservice.aspx',
        'UNIQ_PREFIX': '',
        'TIMEOUT': 5,  # 5s
    })

    def _get_extra_sender_data(self):
        return {
            'prefix': self.config.UNIQ_PREFIX,
        }

    def _serialize_messages(self, messages, request_type):
        """
        Serialize SMS messages to the XML
        :param messages: list of SMS messages
        :param request_type: type of the request to the SMS operator
        :return: serialized XML message that will be sent to the SMS operator service
        """
        return render_to_string(
            self.TEMPLATES['base'], {
                'username':
                self.config.USERNAME,
                'password':
                self.config.PASSWORD,
                'prefix':
                str(self.config.UNIQ_PREFIX) + '-',
                'template_type':
                self.TEMPLATES[request_type],
                'messages':
                messages,
                'type':
                'SMS'
                if request_type == self.REQUEST_TYPES.SMS else 'SMS-Status',
            })

    def _send_requests(self,
                       messages,
                       request_type,
                       is_sending=False,
                       **change_sms_kwargs):
        """
        Performs the actual POST request for input messages and request type.
        :param messages: list of SMS messages
        :param request_type: type of the request
        :param is_sending: True if method is called after sending message
        :param change_sms_kwargs: extra kwargs that will be stored to the message object
        """
        requests_xml = self._serialize_messages(messages, request_type)
        try:
            resp = generate_session(slug='pymess - SMS operator',
                                    related_objects=list(messages)).post(
                                        self.config.URL,
                                        data=requests_xml,
                                        headers={'Content-Type': 'text/xml'},
                                        timeout=self.config.TIMEOUT)
            if resp.status_code != 200:
                raise self.SMSOperatorSendingError(
                    'SMS operator returned invalid response status code: {}'.
                    format(resp.status_code))
            self._update_sms_states_from_response(
                messages, self._parse_response_codes(resp.text), is_sending,
                **change_sms_kwargs)
        except requests.exceptions.RequestException as ex:
            raise self.SMSOperatorSendingError(
                'SMS operator returned returned exception: {}'.format(str(ex)))

    def _update_sms_states_from_response(self,
                                         messages,
                                         parsed_response,
                                         is_sending=False,
                                         **change_sms_kwargs):
        """
        Higher-level function performing serialization of SMS operator requests, parsing ATS server response and
        updating  SMS messages state according the received response.
        :param messages: list of SMS messages
        :param parsed_response: parsed HTTP response from the SMS operator service
        :param is_sending: True if update is called after sending message
        :param change_sms_kwargs: extra kwargs that will be stored to the message object
        """

        messages_dict = {message.pk: message for message in messages}

        missing_uniq = set(messages_dict.keys()) - set(parsed_response.keys())
        if missing_uniq:
            raise self.SMSOperatorSendingError(
                'SMS operator not returned SMS info with uniq: {}'.format(
                    ', '.join(map(str, missing_uniq))))

        extra_uniq = set(parsed_response.keys()) - set(messages_dict.keys())
        if extra_uniq:
            raise self.SMSOperatorSendingError(
                'SMS operator returned SMS info about unknown uniq: {}'.format(
                    ', '.join(map(str, extra_uniq))))

        for uniq, sms_operator_state in parsed_response.items():
            sms = messages_dict[uniq]
            state = self.SMS_OPERATOR_STATES_MAPPING.get(sms_operator_state)
            error = (self.SMS_OPERATOR_STATES.get_label(sms_operator_state)
                     if state == OutputSMSMessage.STATE.ERROR_UPDATE else None)
            if is_sending:
                if error:
                    self._update_message_after_sending_error(
                        sms,
                        state=state,
                        error=error,
                        extra_sender_data={'sender_state': sms_operator_state},
                        **change_sms_kwargs)
                else:
                    self._update_message_after_sending(
                        sms,
                        state=state,
                        extra_sender_data={'sender_state': sms_operator_state},
                        **change_sms_kwargs)
            else:
                self._update_message(
                    sms,
                    state=state,
                    error=error,
                    extra_sender_data={'sender_state': sms_operator_state},
                    **change_sms_kwargs)

    def publish_message(self, message):
        try:
            self._send_requests([message],
                                request_type=self.REQUEST_TYPES.SMS,
                                is_sending=True,
                                sent_at=timezone.now())
        except self.SMSOperatorSendingError as ex:
            self._update_message_after_sending_error(
                message, state=OutputSMSMessage.STATE.ERROR, error=str(ex))
        except requests.exceptions.RequestException as ex:
            self._update_message_after_sending_error(message, error=str(ex))
            # Do not re-raise caught exception. Re-raise exception causes transaction rollback (lost of information
            # about exception).

    def publish_messages(self, messages):
        self._send_requests(messages,
                            request_type=self.REQUEST_TYPES.SMS,
                            is_sending=True,
                            sent_at=timezone.now())

    def _parse_response_codes(self, xml):
        """
        Finds all <dataitem> tags in the given XML and returns a mapping "uniq" -> "response code" for all SMS.
        In case of an error, the error is logged.
        :param xml: XML from the SMS operator response
        :return: dictionary with pair {SMS uniq: response status code}
        """

        soup = BeautifulSoup(xml, 'html.parser')

        return {
            int(item.smsid.string.lstrip(self.config.UNIQ_PREFIX + '-')):
            int(item.status.string)
            for item in soup.find_all('dataitem')
        }

    def update_sms_states(self, messages):
        self._send_requests(messages,
                            request_type=self.REQUEST_TYPES.DELIVERY_REQUEST)
Ejemplo n.º 13
0
from __future__ import unicode_literals

from chamber.utils.datastructures import Enum

LOGICAL_OPERATORS = Enum(
    'AND',
    'OR',
    'NOT',
)