aiosmpplib
==========
An asynchronous SMPP library for use with asyncio.
Inspired by `naz`_ library by Komu Wairagu. Initial intention was to add missing functionality
to existing library. But in the end, the code has been almost completely rewritten and released
as a separate library.
SMPP is a protocol designed for the transfer of short message data between External Short
Messaging Entities(ESMEs), Routing Entities(REs) and Short Message Service Center(SMSC).
- `Wikipedia <https://en.wikipedia.org/wiki/Short_Message_Peer-to-Peer>`_
Currently, only partial ESME functionality is implemented, and only SMPP version 3.4 is supported.
> :warning: **Version 0.7.0 introduces breaking changes to Correlator, due to implementation of message segmentation.**
Full documentation is not available at this time.
.. _naz: https://github.com/komuw/naz
Installation
------------
.. code-block:: shell
pip install aiosmpplib
Requirements
------------
Python 3.7+ is required. Currently, aiosmpplib does not have any third-party dependencies,
but it optionally uses `orjson`_ library for JSON serialization and logging.
.. _orjson: https://github.com/ijl/orjson
Quick start
-----------
.. code-block:: python
import asyncio
from aiosmpplib import ESME, PhoneNumber, SubmitSm
from aiosmpplib.log import DEBUG
async def main():
# Create ESME instance.
esme = ESME(
smsc_host='127.0.0.1',
smsc_port=2775,
system_id='test',
password='test',
log_level=DEBUG,
)
# Queue messages to send.
for i in range(0, 5):
msg = SubmitSm(
short_message=f'Test message {i}',
source=PhoneNumber('254722111111'),
destination=PhoneNumber('254722999999'),
log_id=f'id-{i}',
)
await esme.broker.enqueue(msg)
# Start ESME. It will run until stopped, automatically reconnecting if necessary.
# If you want to test connection beforehand, await esme.connect() first.
# It will raise an exception if connection is not successfull -
# typically SmppError, or one of transport errors (OSError, TimeoutError, socket.error etc).
asyncio.create_task(esme.start())
# Give it some time to send messages.
await asyncio.sleep(20)
# Stop ESME.
await esme.stop()
if __name__ == "__main__":
loop = asyncio.get_event_loop()
loop.run_until_complete(main())
loop.close()
Quick user guide
----------------
Your application interacts with ESME via three interfaces: broker, correlator and hook.
* Broker is a FIFO queue in which your application puts messages. ESME retrieves messages
from the broker and sends them to SMSC. Any type of SMPP message can be queued, but it really
only makes sense for **SubmitSm** (outgoing SMS). Subclass **AbstractBroker** in order to put and
get messages from persistent storage. The library provides ``json_encode`` and ``json_decode``
convenience methods which can be used to convert messages to/from JSON. Again, while any message
can be serialized, it probably only makes sense for **SubmitSm**, and possibly **DeliverSm**.
* Correlator is an interface that does four types of correlation:
* Outgoing SMPP requests are correlated with received responses.
* Parts of the segmented SubmitSm messages are correlated with original messages
* Parts of the segmented DeliverSm messages are correlated based on message reference number
* Outgoing SMS messages (SubmitSm) are correlated with delivery receipts (DeliverSm).
Delivery receipts may be received days after original message is sent, so this type of
correlation should be persisted. Subclass **SimpleCorrelator** and override ``put_delivery``,
``get_delivery`` and ``get_segmented`` methods. If you want to implement more efficient
request/response correlation, subclass **AbstractCorrelator** and also override
``get`` and ``put`` methods.
**SimpleCorrelator** can do a simple file persistence if ``directory`` parameter is provided.
* Hook is an interface with three async methods:
* ``sending``: Called before sending any message to SMSC.
* ``received``: Called after receiving any message from SMSC.
* ``send_error``: Called if error occured while sending a SubmitSm.
Subclass **AbstractHook** and implement all three methods. The latter two are essential for
reliable message tracking.
Incoming message flow
_____________________
Receiving messages is straightforward. The ``received`` hook will be called. If the
``smpp_message`` parameter is of type **DeliverSm** and its ``is_receipt`` method returns ``False``,
it is an incoming SMS. Store it as appropriate. If the message was segmented, segments will be
reassembled ba the correlator, and ``received`` hook called for the complete message only.
Outgoing message flow
_____________________
Sending messages is a lot more involved.
1. Create a **SubmitSm** message with unique ``log_id`` and optionally ``extra_data`` parameters.
Any message related to this message will have the same ``log_id`` and ``extra_data``,
provided that correlator did its job.
If encoded message text is longer than 254 bytes, it is handled as follows.
* If ``auto_message_payload`` parameter is True, text will be moved to
``message_payload`` optional parameter.
* If ``auto_message_payload`` parameter is False and seventh bit in ``esm_class`` parameter is
set (e.g. 0b01000000), the message will be segmented using UDH method.
* If ``auto_message_payload`` parameter is False and seventh bit in ``esm_class`` parameter is
not set, the message will be segmented using SAR (Segmentation And Reassembly) method.
Segmentation is transparent. Hooks will not be called for individual segmentsm but for
the complete message only.
1. Enqueue the message in broker.
2. If message could not be sent, ``send_error`` hook will be called. Original message is available
in ``smpp_message`` parameter. The ``error`` parameter contains exception that occured.
* ValueError indicates that the message couldn't be encoded to PDU (probably invalid parameters).
* Transport errors (OSError and its descendants) indicate a network problem.
* TimeoutError indicates that the response from SMSC was not received within timeout.
Timeout duration depends on correlator implementation.
Whichever error occured, the message will not be re-sent automatically.
User application must implement retry mechanism, if required.
3. If the SMSC does respond, check the response in ``received`` hook.
The ``smpp_message`` parameter will be either:
* **SubmitSmResp** - If ``command_status`` member is anything other than
``SmppCommandStatus.ESME_ROK``, the request has been rejected by SMSC.
* **GenericNack** - The request was not understood by SMSC, probably due to network error.
Again, if the message was rejected, it will not be re-sent automatically.
4. If the request was accepted, a delivery receipt should arrive after some time.
In ``received`` hook, look for **DeliverSm** message whose ``is_receipt`` method
returns ``True``. Then use ``parse_receipt`` method to get a dictionary with parsed data.
Receipt structure is SMSC-specific, but it usually has the following items:
.. code-block:: python
{
'id': str # Message ID allocated by the SMSC when submitted.
'sub': int # Number of short messages originally submitted.
'dlvrd': int # Number of short messages delivered.
'submit date': datetime # The time and date at which the message was submitted.
'done date': datetime # The time and date at which the message reached its final state.
'stat': str # The final status of the message.
'err': int # Network specific error code or an SMSC error code.
'text': str # The first 20 characters of the short message.
}
The ``err`` parameter should be 0 if no error occured.
The ``stat`` parameter should have one the following values:
* ``DELIVRD`` - Message is delivered to destination.
* ``EXPIRED`` - Message validity period has expired.
* ``DELETED`` - Message has been deleted.
* ``UNDELIV`` - Message is undeliverable.
* ``ACCEPTD`` - Message is in accepted state.
* ``UNKNOWN`` - Message is in invalid state.
* ``REJECTD`` - Message is in a rejected state.
For more details, check `SMPP specification <https://smpp.org/SMPP_v3_4_Issue1_2.pdf>`_.
Example hook implementation:
____________________________
.. code-block:: python
from aiosmpplib import AbstractHook, SmppCommandStatus
from aiosmpplib import DeliverSm, SubmitSm, SubmitSmResp, GenericNack, SmppMessage, Trackable
class MyHook(AbstractHook):
async def _save_result(self, msg: str, smpp_message: Trackable) -> None:
log_id: str = smpp_message.log_id
extra_data: str = smpp_message.extra_data
# Save data to database
async def sending(self, smpp_message: SmppMessage, pdu: bytes, client_id: str) -> None:
# Called for every sent message, includion individual segments of a segmented SubmitSM
pass # Or trace log
async def received(self, smpp_message: Optional[SmppMessage], pdu: bytes,
client_id: str) -> None:
# If SubmitSm was segmented, this will be only called once, after all segments
# are processed. This applies both to SubmitSmResp and delivery receipt.
if isinstance(smpp_message, GenericNack):
await self._save_result('Sending failed', smpp_message)
# Requeue if desired
if isinstance(smpp_message, SubmitSmResp):
if smpp_message.command_status == SmppCommandStatus.ESME_ROK:
await self._save_result('Message sent', smpp_message)
else:
await self._save_result('Sending failed', smpp_message)
# Requeue if desired
elif isinstance(smpp_message, DeliverSm):
if smpp_message.is_receipt():
# This is a delivery receipt
receipt: Dict[str, Any] = smpp_message.parse_receipt()
final_status: str = receipt.get('stat', '')
msg: str
if final_status == 'DELIVRD':
msg = 'Delivered to handset'
elif final_status == 'EXPIRED':
msg = 'Message expired'
elif final_status == 'DELETED':
msg = 'Message deleted by SC'
elif final_status == 'UNDELIV':
msg = 'Message undeliverable'
elif final_status == 'ACCEPTD':
msg = 'Message accepted'
elif final_status == 'REJECTD':
msg = 'Message rejected'
else:
msg = 'Unknown status'
await self._save_result(msg, smpp_message)
else:
pass
# This is an incoming SMS
# Process and save to database
async def send_error(self, smpp_message: SmppMessage, error: Exception, client_id: str) -> None:
if isinstance(smpp_message, SubmitSm):
await self._save_result('Sending failed', smpp_message)
# Requeue if desired
Bug Reporting
-------------
Bug reports and feature requests are welcome via `Github issues`_.
.. _Github issues: https://github.com/niksabaldun/aiosmpplib/issues
Raw data
{
"_id": null,
"home_page": "https://github.com/niksabaldun/aiosmpplib",
"name": "aiosmpplib",
"maintainer": null,
"docs_url": null,
"requires_python": null,
"maintainer_email": null,
"keywords": "aiosmpplib, smpp, smpp-client, smpp-protocol, smpp-library, esme, smsc",
"author": "Nik\u0161a Baldun",
"author_email": "niksa.baldun@gmail.com",
"download_url": "https://files.pythonhosted.org/packages/e7/e0/c21d8772d0b0033b9a502b15437fbf04c7be068e47b1f5d49eb49219b237/aiosmpplib-0.7.0.tar.gz",
"platform": null,
"description": "aiosmpplib\n==========\nAn asynchronous SMPP library for use with asyncio.\n\nInspired by `naz`_ library by Komu Wairagu. Initial intention was to add missing functionality\nto existing library. But in the end, the code has been almost completely rewritten and released\nas a separate library.\n\n SMPP is a protocol designed for the transfer of short message data between External Short\n Messaging Entities(ESMEs), Routing Entities(REs) and Short Message Service Center(SMSC).\n - `Wikipedia <https://en.wikipedia.org/wiki/Short_Message_Peer-to-Peer>`_\n\nCurrently, only partial ESME functionality is implemented, and only SMPP version 3.4 is supported.\n\n> :warning: **Version 0.7.0 introduces breaking changes to Correlator, due to implementation of message segmentation.**\n\nFull documentation is not available at this time.\n\n.. _naz: https://github.com/komuw/naz\n\nInstallation\n------------\n.. code-block:: shell\n\n pip install aiosmpplib\n\n\nRequirements\n------------\nPython 3.7+ is required. Currently, aiosmpplib does not have any third-party dependencies,\nbut it optionally uses `orjson`_ library for JSON serialization and logging.\n\n.. _orjson: https://github.com/ijl/orjson\n\n\nQuick start\n-----------\n\n.. code-block:: python\n\n import asyncio\n from aiosmpplib import ESME, PhoneNumber, SubmitSm\n from aiosmpplib.log import DEBUG\n\n async def main():\n # Create ESME instance.\n esme = ESME(\n smsc_host='127.0.0.1',\n smsc_port=2775,\n system_id='test',\n password='test',\n log_level=DEBUG,\n )\n\n # Queue messages to send.\n for i in range(0, 5):\n msg = SubmitSm(\n short_message=f'Test message {i}',\n source=PhoneNumber('254722111111'),\n destination=PhoneNumber('254722999999'),\n log_id=f'id-{i}',\n )\n await esme.broker.enqueue(msg)\n\n # Start ESME. It will run until stopped, automatically reconnecting if necessary.\n # If you want to test connection beforehand, await esme.connect() first.\n # It will raise an exception if connection is not successfull -\n # typically SmppError, or one of transport errors (OSError, TimeoutError, socket.error etc).\n asyncio.create_task(esme.start())\n # Give it some time to send messages.\n await asyncio.sleep(20)\n # Stop ESME.\n await esme.stop()\n\n if __name__ == \"__main__\":\n loop = asyncio.get_event_loop()\n loop.run_until_complete(main())\n loop.close()\n\n\nQuick user guide\n----------------\nYour application interacts with ESME via three interfaces: broker, correlator and hook.\n\n* Broker is a FIFO queue in which your application puts messages. ESME retrieves messages\n from the broker and sends them to SMSC. Any type of SMPP message can be queued, but it really\n only makes sense for **SubmitSm** (outgoing SMS). Subclass **AbstractBroker** in order to put and\n get messages from persistent storage. The library provides ``json_encode`` and ``json_decode``\n convenience methods which can be used to convert messages to/from JSON. Again, while any message\n can be serialized, it probably only makes sense for **SubmitSm**, and possibly **DeliverSm**.\n* Correlator is an interface that does four types of correlation:\n\n * Outgoing SMPP requests are correlated with received responses.\n * Parts of the segmented SubmitSm messages are correlated with original messages\n * Parts of the segmented DeliverSm messages are correlated based on message reference number\n * Outgoing SMS messages (SubmitSm) are correlated with delivery receipts (DeliverSm).\n\n Delivery receipts may be received days after original message is sent, so this type of\n correlation should be persisted. Subclass **SimpleCorrelator** and override ``put_delivery``,\n ``get_delivery`` and ``get_segmented`` methods. If you want to implement more efficient\n request/response correlation, subclass **AbstractCorrelator** and also override\n ``get`` and ``put`` methods.\n\n **SimpleCorrelator** can do a simple file persistence if ``directory`` parameter is provided.\n* Hook is an interface with three async methods:\n\n * ``sending``: Called before sending any message to SMSC.\n * ``received``: Called after receiving any message from SMSC.\n * ``send_error``: Called if error occured while sending a SubmitSm.\n\n Subclass **AbstractHook** and implement all three methods. The latter two are essential for\n reliable message tracking.\n\nIncoming message flow\n_____________________\nReceiving messages is straightforward. The ``received`` hook will be called. If the\n``smpp_message`` parameter is of type **DeliverSm** and its ``is_receipt`` method returns ``False``,\nit is an incoming SMS. Store it as appropriate. If the message was segmented, segments will be\nreassembled ba the correlator, and ``received`` hook called for the complete message only.\n\nOutgoing message flow\n_____________________\nSending messages is a lot more involved.\n\n1. Create a **SubmitSm** message with unique ``log_id`` and optionally ``extra_data`` parameters.\n Any message related to this message will have the same ``log_id`` and ``extra_data``,\n provided that correlator did its job.\n If encoded message text is longer than 254 bytes, it is handled as follows.\n\n * If ``auto_message_payload`` parameter is True, text will be moved to\n ``message_payload`` optional parameter.\n * If ``auto_message_payload`` parameter is False and seventh bit in ``esm_class`` parameter is\n set (e.g. 0b01000000), the message will be segmented using UDH method.\n * If ``auto_message_payload`` parameter is False and seventh bit in ``esm_class`` parameter is\n not set, the message will be segmented using SAR (Segmentation And Reassembly) method.\n\n Segmentation is transparent. Hooks will not be called for individual segmentsm but for\n the complete message only.\n\n1. Enqueue the message in broker.\n2. If message could not be sent, ``send_error`` hook will be called. Original message is available\n in ``smpp_message`` parameter. The ``error`` parameter contains exception that occured.\n\n * ValueError indicates that the message couldn't be encoded to PDU (probably invalid parameters).\n * Transport errors (OSError and its descendants) indicate a network problem.\n * TimeoutError indicates that the response from SMSC was not received within timeout.\n Timeout duration depends on correlator implementation.\n\n Whichever error occured, the message will not be re-sent automatically.\n User application must implement retry mechanism, if required.\n3. If the SMSC does respond, check the response in ``received`` hook.\n The ``smpp_message`` parameter will be either:\n\n * **SubmitSmResp** - If ``command_status`` member is anything other than\n ``SmppCommandStatus.ESME_ROK``, the request has been rejected by SMSC.\n * **GenericNack** - The request was not understood by SMSC, probably due to network error.\n\n Again, if the message was rejected, it will not be re-sent automatically.\n4. If the request was accepted, a delivery receipt should arrive after some time.\n In ``received`` hook, look for **DeliverSm** message whose ``is_receipt`` method\n returns ``True``. Then use ``parse_receipt`` method to get a dictionary with parsed data.\n Receipt structure is SMSC-specific, but it usually has the following items:\n\n .. code-block:: python\n\n {\n 'id': str # Message ID allocated by the SMSC when submitted.\n 'sub': int # Number of short messages originally submitted.\n 'dlvrd': int # Number of short messages delivered.\n 'submit date': datetime # The time and date at which the message was submitted.\n 'done date': datetime # The time and date at which the message reached its final state.\n 'stat': str # The final status of the message.\n 'err': int # Network specific error code or an SMSC error code.\n 'text': str # The first 20 characters of the short message.\n }\n\n The ``err`` parameter should be 0 if no error occured.\n\n The ``stat`` parameter should have one the following values:\n\n * ``DELIVRD`` - Message is delivered to destination.\n * ``EXPIRED`` - Message validity period has expired.\n * ``DELETED`` - Message has been deleted.\n * ``UNDELIV`` - Message is undeliverable.\n * ``ACCEPTD`` - Message is in accepted state.\n * ``UNKNOWN`` - Message is in invalid state.\n * ``REJECTD`` - Message is in a rejected state.\n\n For more details, check `SMPP specification <https://smpp.org/SMPP_v3_4_Issue1_2.pdf>`_.\n\nExample hook implementation:\n____________________________\n\n.. code-block:: python\n\n from aiosmpplib import AbstractHook, SmppCommandStatus\n from aiosmpplib import DeliverSm, SubmitSm, SubmitSmResp, GenericNack, SmppMessage, Trackable\n\n class MyHook(AbstractHook):\n async def _save_result(self, msg: str, smpp_message: Trackable) -> None:\n log_id: str = smpp_message.log_id\n extra_data: str = smpp_message.extra_data\n # Save data to database\n\n async def sending(self, smpp_message: SmppMessage, pdu: bytes, client_id: str) -> None:\n # Called for every sent message, includion individual segments of a segmented SubmitSM\n pass # Or trace log\n\n async def received(self, smpp_message: Optional[SmppMessage], pdu: bytes,\n client_id: str) -> None:\n # If SubmitSm was segmented, this will be only called once, after all segments\n # are processed. This applies both to SubmitSmResp and delivery receipt.\n if isinstance(smpp_message, GenericNack):\n await self._save_result('Sending failed', smpp_message)\n # Requeue if desired\n if isinstance(smpp_message, SubmitSmResp):\n if smpp_message.command_status == SmppCommandStatus.ESME_ROK:\n await self._save_result('Message sent', smpp_message)\n else:\n await self._save_result('Sending failed', smpp_message)\n # Requeue if desired\n elif isinstance(smpp_message, DeliverSm):\n if smpp_message.is_receipt():\n # This is a delivery receipt\n receipt: Dict[str, Any] = smpp_message.parse_receipt()\n final_status: str = receipt.get('stat', '')\n msg: str\n if final_status == 'DELIVRD':\n msg = 'Delivered to handset'\n elif final_status == 'EXPIRED':\n msg = 'Message expired'\n elif final_status == 'DELETED':\n msg = 'Message deleted by SC'\n elif final_status == 'UNDELIV':\n msg = 'Message undeliverable'\n elif final_status == 'ACCEPTD':\n msg = 'Message accepted'\n elif final_status == 'REJECTD':\n msg = 'Message rejected'\n else:\n msg = 'Unknown status'\n await self._save_result(msg, smpp_message)\n else:\n pass\n # This is an incoming SMS\n # Process and save to database\n\n async def send_error(self, smpp_message: SmppMessage, error: Exception, client_id: str) -> None:\n if isinstance(smpp_message, SubmitSm):\n await self._save_result('Sending failed', smpp_message)\n # Requeue if desired\n\n\nBug Reporting\n-------------\nBug reports and feature requests are welcome via `Github issues`_.\n\n.. _Github issues: https://github.com/niksabaldun/aiosmpplib/issues\n",
"bugtrack_url": null,
"license": "MIT",
"summary": "Python asyncio SMPP client library.",
"version": "0.7.0",
"project_urls": {
"Homepage": "https://github.com/niksabaldun/aiosmpplib"
},
"split_keywords": [
"aiosmpplib",
" smpp",
" smpp-client",
" smpp-protocol",
" smpp-library",
" esme",
" smsc"
],
"urls": [
{
"comment_text": "",
"digests": {
"blake2b_256": "e7e0c21d8772d0b0033b9a502b15437fbf04c7be068e47b1f5d49eb49219b237",
"md5": "cdb16f45f8f5735b2c795c98419db35e",
"sha256": "f5496df6ac6439e517f30b6e43743ddcd9af4b96698c5fc6e4cd5bad6a5cfa3c"
},
"downloads": -1,
"filename": "aiosmpplib-0.7.0.tar.gz",
"has_sig": false,
"md5_digest": "cdb16f45f8f5735b2c795c98419db35e",
"packagetype": "sdist",
"python_version": "source",
"requires_python": null,
"size": 64047,
"upload_time": "2024-12-08T18:06:08",
"upload_time_iso_8601": "2024-12-08T18:06:08.619780Z",
"url": "https://files.pythonhosted.org/packages/e7/e0/c21d8772d0b0033b9a502b15437fbf04c7be068e47b1f5d49eb49219b237/aiosmpplib-0.7.0.tar.gz",
"yanked": false,
"yanked_reason": null
}
],
"upload_time": "2024-12-08 18:06:08",
"github": true,
"gitlab": false,
"bitbucket": false,
"codeberg": false,
"github_user": "niksabaldun",
"github_project": "aiosmpplib",
"travis_ci": false,
"coveralls": false,
"github_actions": false,
"tox": true,
"lcname": "aiosmpplib"
}