Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
url='https://github.com/python-smpplib/python-smpplib',
description='SMPP library for python',
packages=find_packages(),
install_requires=['six'],
install_requires=['six', 'monotonic'],
extras_require=dict(
tests=('typing; python_version < "3.5"', 'pytest', 'mock'),
),
Expand Down
75 changes: 73 additions & 2 deletions smpplib/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,14 @@
"""SMPP client module"""

import binascii
import collections
import logging
import select
import socket
import struct
import warnings

from monotonic import monotonic
from smpplib import consts, exceptions, smpp


Expand Down Expand Up @@ -91,7 +93,6 @@ def __init__(
else:
self.allow_unknown_opt_params = allow_unknown_opt_params


self._socket = self._create_socket()

def __enter__(self):
Expand Down Expand Up @@ -301,7 +302,7 @@ def set_message_received_handler(self, func):
def set_message_sent_handler(self, func):
"""Set new function to handle message sent event"""
self.message_sent_handler = func

def set_query_resp_handler(self, func):
"""Set new function to handle query resp event"""
self.query_resp_handler = func
Expand Down Expand Up @@ -421,3 +422,73 @@ def query_message(self, **kwargs):
qsm = smpp.make_pdu('query_sm', client=self, **kwargs)
self.send_pdu(qsm)
return qsm


class ThreadSafeClient(Client):
should_stop = False

def __init__(self, *args, **kwargs):
# Socket polling period
select_timeout = kwargs.get('select_timeout', 1.0)

super(ThreadSafeClient, self).__init__(*args, **kwargs)

self._select_timeout = select_timeout

self._send_queue = collections.deque()
self._read_sock, self._send_sock = socket.socketpair()

# It will help not to spam the server
self._last_active_time = 0.0

def accept(self, obj):
"""Accept an object"""
raise NotImplementedError('not implemented')

def send_pdu(self, pdu, send_later=False):
if send_later:
self._send_queue.append(pdu)
self._send_sock.send(b'\x00')
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we sending a 00? Is it a no-op? If so, why are we sending it?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is some sort of notification frame. We put it into the send socket to wake up our read socket

return True
else:
pdu_sent = super(ThreadSafeClient, self).send_pdu(pdu)
self._last_active_time = monotonic()
return pdu_sent

def send_message(self, send_later=True, **kwargs):
submit_sm_pdu = smpp.make_pdu('submit_sm', client=self, **kwargs)
self.send_pdu(submit_sm_pdu, send_later=send_later)
return submit_sm_pdu

def _should_prolong_session(self):
# We need some time to send enquire_link before the next `select` call comes
passed_from_last_message = monotonic() - self._last_active_time

return self.timeout - self._select_timeout <= passed_from_last_message

def observe(self, ignore_error_codes=None, auto_send_enquire_link=True):
while not self.should_stop:
rlist, _, _ = select.select(
[self._socket, self._read_sock], [], [], self._select_timeout,
)

if self.should_stop:
break

if not rlist:
if self._should_prolong_session():
if not auto_send_enquire_link:
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO: check auto_send_enquire_link before self._should_prolong_session() call

raise exceptions.SessionProlongationDisabled()

self.logger.debug('Sending enquire_link')
pdu = smpp.make_pdu('enquire_link', client=self)
self.send_pdu(pdu)
else:
for ready_socket in rlist:
if ready_socket is self._socket:
self.read_once(ignore_error_codes, auto_send_enquire_link)
else:
self._read_sock.recv(1)
self.send_pdu(self._send_queue.pop())

self.logger.info('Finished observing...')
4 changes: 4 additions & 0 deletions smpplib/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,7 @@ class PDUError(RuntimeError):

class MessageTooLong(ValueError):
"""Text too long to fit 255 SMS"""


class SessionProlongationDisabled(Exception):
"""Server send nothing and we do not want to continue"""
19 changes: 16 additions & 3 deletions smpplib/tests/test_client.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import time
import warnings

import pytest
from mock import Mock, call
from mock import call, Mock
from monotonic import monotonic

from smpplib.client import Client
from smpplib.smpp import make_pdu
from smpplib import consts
from smpplib import exceptions
from smpplib.client import Client, ThreadSafeClient
from smpplib.smpp import make_pdu


def test_client_construction_allow_unknown_opt_params_warning():
Expand Down Expand Up @@ -44,3 +47,13 @@ def test_client_error_pdu_custom_handler():
client.read_once()

assert mock_error_pdu_handler.mock_calls == [call(error_pdu)]


def test_prolongation():
client = ThreadSafeClient("localhost", 5679)
client._last_active_time = monotonic()
assert not client._should_prolong_session()

time.sleep(client.timeout - client._select_timeout)

assert client._should_prolong_session()