Metadata-Version: 2.1
Name: naz
Version: 0.6.2
Summary: Naz is an async SMPP client.
Home-page: https://github.com/komuw/naz
Author: komuW
Author-email: komuw05@gmail.com
License: MIT
Keywords: naz,smpp,smpp-client,smpp-protocol,smpp-library
Platform: UNKNOWN
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Topic :: Software Development :: Build Tools
Classifier: Topic :: Internet :: WWW/HTTP
Classifier: Topic :: Security
Classifier: Topic :: System :: Installation/Setup
Classifier: Topic :: System :: Networking
Classifier: Topic :: System :: Systems Administration
Classifier: Topic :: Utilities
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3.6
Provides-Extra: benchmarks
Requires-Dist: asyncpg (==0.18.3) ; extra == 'benchmarks'
Requires-Dist: docker (==4.0.1) ; extra == 'benchmarks'
Requires-Dist: prometheus-client (==0.6.0) ; extra == 'benchmarks'
Requires-Dist: aioredis (==1.2.0) ; extra == 'benchmarks'
Provides-Extra: dev
Requires-Dist: coverage ; extra == 'dev'
Requires-Dist: pypandoc ; extra == 'dev'
Requires-Dist: twine ; extra == 'dev'
Requires-Dist: wheel ; extra == 'dev'
Requires-Dist: Sphinx (==2.1.1) ; extra == 'dev'
Requires-Dist: sphinx-autodoc-typehints (==1.6.0) ; extra == 'dev'
Requires-Dist: sphinx-rtd-theme (==0.4.3) ; extra == 'dev'
Requires-Dist: redis (==3.2.1) ; extra == 'dev'
Requires-Dist: pika (==1.0.1) ; extra == 'dev'
Provides-Extra: test
Requires-Dist: flake8 ; extra == 'test'
Requires-Dist: pylint ; extra == 'test'
Requires-Dist: black ; extra == 'test'
Requires-Dist: bandit ; extra == 'test'
Requires-Dist: docker (==4.0.1) ; extra == 'test'
Requires-Dist: mypy ; extra == 'test'

naz
---

`Codacy Badge <https://www.codacy.com/app/komuw/naz>`__ `Build
Status <https://travis-ci.com/komuw/naz>`__
`codecov <https://codecov.io/gh/komuw/naz>`__ `Code style:
black <https://github.com/komuw/naz>`__

| naz is an async SMPP client.
| It’s name is derived from Kenyan hip hop artiste, Nazizi.

   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>`__

| naz currently only supports SMPP version 3.4.
| naz has no third-party dependencies and it requires python version
  3.6+

| naz is in active development and it’s API may change in backward
  incompatible ways.
| https://pypi.python.org/pypi/naz

Comprehensive documetion is available ->
`Documentation <https://komuw.github.io/naz>`__

| **Contents:**
| `Installation <#installation>`__
| `Usage <#usage>`__
| + `As a library <#1-as-a-library>`__
| + `As cli app <#2-as-a-cli-app>`__

| `Features <#features>`__
| + `async everywhere <#1-async-everywhere>`__
| + `monitoring-and-observability <#2-monitoring-and-observability>`__
| + `logging <#21-logging>`__
| + `hooks <#22-hooks>`__ + `integration with bug trackers(eg Sentry
  ) <#23-integration-with-bug-trackers>`__ + `Rate
  limiting <#3-rate-limiting>`__
| + `Throttle handling <#4-throttle-handling>`__
| + `Queuing <#5-queuing>`__

`Benchmarks <./benchmarks/README.md>`__

Installation
------------

.. code:: shell

   pip install naz

Usage
-----

1. As a library
^^^^^^^^^^^^^^^

.. code:: python

   import asyncio
   import naz

   loop = asyncio.get_event_loop()
   outboundqueue = naz.q.SimpleOutboundQueue(maxsize=1000)
   cli = naz.Client(
       smsc_host="127.0.0.1",
       smsc_port=2775,
       system_id="smppclient1",
       password="password",
       outboundqueue=outboundqueue,
   )

   # queue messages to send
   for i in range(0, 4):
       print("submit_sm round:", i)
       loop.run_until_complete(
           cli.submit_sm(
               short_message="Hello World-{0}".format(str(i)),
               log_id="myid12345",
               source_addr="254722111111",
               destination_addr="254722999999",
           )
       )

   # connect to the SMSC host
   reader, writer = loop.run_until_complete(cli.connect())
   # bind to SMSC as a tranceiver
   loop.run_until_complete(cli.tranceiver_bind())

   try:
       # read any data from SMSC, send any queued messages to SMSC and continually check the state of the SMSC
       tasks = asyncio.gather(cli.dequeue_messages(), cli.receive_data(), cli.enquire_link())
       loop.run_until_complete(tasks)
       loop.run_forever()
   except Exception as e:
       print("exception occured. error={0}".format(str(e)))
   finally:
       loop.run_until_complete(cli.unbind())
       loop.stop()

| **NB:**
| (a) For more information about all the parameters that ``naz.Client``
  can take, consult the `documentation
  here <https://github.com/komuw/naz/blob/master/documentation/config.md>`__
| (b) More `examples can be found
  here <https://github.com/komuw/naz/tree/master/examples>`__
| (c) if you need a SMSC server/gateway to test with, you can use the
  `docker-compose file in this
  repo <https://github.com/komuw/naz/blob/master/docker-compose.yml>`__
  to bring up an SMSC simulator.
| That docker-compose file also has a redis and rabbitMQ container if
  you would like to use those as your outboundqueue.

2. As a cli app
^^^^^^^^^^^^^^^

| naz also ships with a commandline interface app called ``naz-cli``.
| create a python config file, eg;
| ``/tmp/my_config.py``

.. code:: python

   import naz
   from myfile import ExampleQueue

   client = naz.Client(
       smsc_host="127.0.0.1",
       smsc_port=2775,
       system_id="smppclient1",
       password="password",
       outboundqueue=ExampleQueue()
   )

and a python file, ``myfile.py`` (in the current working directory) with
the contents:

.. code:: python

   import asyncio
   import naz

   class ExampleQueue(naz.q.BaseOutboundQueue):
       def __init__(self):
           loop = asyncio.get_event_loop()
           self.queue = asyncio.Queue(maxsize=1000, loop=loop)
       async def enqueue(self, item):
           self.queue.put_nowait(item)
       async def dequeue(self):
           return await self.queue.get()

| then run:
| ``naz-cli --client tmp.my_config.client``

.. code:: shell

        Naz: the SMPP client.

   {'event': 'naz.Client.connect', 'stage': 'start', 'environment': 'production', 'release': 'canary', 'smsc_host': '127.0.0.1', 'system_id': 'smppclient1', 'client_id': '2VU55VT86KHWXTW7X'}
   {'event': 'naz.Client.connect', 'stage': 'end', 'environment': 'production', 'release': 'canary', 'smsc_host': '127.0.0.1', 'system_id': 'smppclient1', 'client_id': '2VU55VT86KHWXTW7X'}
   {'event': 'naz.Client.tranceiver_bind', 'stage': 'start', 'environment': 'production', 'release': 'canary', 'smsc_host': '127.0.0.1', 'system_id': 'smppclient1', 'client_id': '2VU55VT86KHWXTW7X'}
   {'event': 'naz.Client.send_data', 'stage': 'start', 'smpp_command': 'bind_transceiver', 'log_id': None, 'msg': 'hello', 'environment': 'production', 'release': 'canary', 'smsc_host': '127.0.0.1', 'system_id': 'smppclient1', 'client_id': '2VU55VT86KHWXTW7X'}
   {'event': 'naz.SimpleHook.request', 'stage': 'start', 'smpp_command': 'bind_transceiver', 'log_id': None, 'environment': 'production', 'release': 'canary', 'smsc_host': '127.0.0.1', 'system_id': 'smppclient1', 'client_id': '2VU55VT86KHWXTW7X'}
   {'event': 'naz.Client.send_data', 'stage': 'end', 'smpp_command': 'bind_transceiver', 'log_id': None, 'msg': 'hello', 'environment': 'production', 'release': 'canary', 'smsc_host': '127.0.0.1', 'system_id': 'smppclient1', 'client_id': '2VU55VT86KHWXTW7X'}
   {'event': 'naz.Client.tranceiver_bind', 'stage': 'end', 'environment': 'production', 'release': 'canary', 'smsc_host': '127.0.0.1', 'system_id': 'smppclient1', 'client_id': '2VU55VT86KHWXTW7X'}
   {'event': 'naz.Client.dequeue_messages', 'stage': 'start', 'environment': 'production', 'release': 'canary', 'smsc_host': '127.0.0.1', 'system_id': 'smppclient1', 'client_id': '2VU55VT86KHWXTW7X'}

| **NB:**
| (a) For more information about the ``naz`` config file, consult the
  `documentation
  here <https://github.com/komuw/naz/blob/master/documentation/config.md>`__
| (b) More `examples can be found
  here <https://github.com/komuw/naz/tree/master/examples>`__. As an
  example, start the SMSC simulator(\ ``docker-compose up``) then in
  another terminal run,
  ``naz-cli --client examples.example_config.client``

To see help:

``naz-cli --help``

.. code:: shell

   naz is an async SMPP client.     
   example usage: naz-cli --client path.to.my_config.client

   optional arguments:
     -h, --help            show this help message and exit
     --version             The currently installed naz version.
     --client CLIENT       The config file to use. eg: --client path.to.my_config.client

Features
--------

1. async everywhere
^^^^^^^^^^^^^^^^^^^

| SMPP is an async protocol; the client can send a request and only get
  a response from SMSC/server 20mins later out of band.
| It thus makes sense to write your SMPP client in an async manner. We
  leverage python3’s async/await to do so.

.. code:: python

   import naz
   import asyncio

   loop = asyncio.get_event_loop()
   outboundqueue = naz.q.SimpleOutboundQueue(maxsize=1000)
   cli = naz.Client(
       smsc_host="127.0.0.1",
       smsc_port=2775,
       system_id="smppclient1",
       password="password",
       outboundqueue=outboundqueue,
   )

2. monitoring and observability
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

it’s a loaded term, I know.

2.1 logging
'''''''''''

| In ``naz`` you have the ability to annotate all the log events that
  ``naz`` will generate with anything you want.
| So, for example if you wanted to annotate all log-events with a
  release version and your app’s running environment.

.. code:: python

   import naz

   cli = naz.Client(
       ...
       log_metadata={ "environment": "production", "release": "canary"},
   )

| and then these will show up in all log events.
| by default, ``naz`` annotates all log events with ``smsc_host``,
  ``system_id`` and ``client_id``

2.2 hooks
'''''''''

| a hook is a class with two methods ``request`` and ``response``, ie it
  implements ``naz``\ ’s BaseHook interface as `defined
  here <https://github.com/komuw/naz/blob/master/naz/hooks.py>`__.
| ``naz`` will call the ``request`` method just before sending request
  to SMSC and also call the ``response`` method just after getting
  response from SMSC.
| the default hook that ``naz`` uses is ``naz.hooks.SimpleHook`` which
  does nothing but logs.
| If you wanted, for example to keep metrics of all requests and
  responses to SMSC in your `prometheus <https://prometheus.io/>`__
  setup;

.. code:: python

   import naz
   from prometheus_client import Counter

   class MyPrometheusHook(naz.hooks.BaseHook):
       async def request(self, smpp_command, log_id, hook_metadata):
           c = Counter('my_requests', 'Description of counter')
           c.inc() # Increment by 1
       async def response(self,
                          smpp_command,
                          log_id,
                          hook_metadata,
                          smsc_response):
           c = Counter('my_responses', 'Description of counter')
           c.inc() # Increment by 1

   myHook = MyPrometheusHook()
   cli = naz.Client(
       ...
       hook=myHook,
   )

another example is if you want to update a database record whenever you
get a delivery notification event;

.. code:: python

   import sqlite3
   import naz

   class SetMessageStateHook(naz.hooks.BaseHook):
       async def request(self, smpp_command, log_id, hook_metadata):
           pass
       async def response(self,
                          smpp_command,
                          log_id,
                          hook_metadata,
                          smsc_response):
           if smpp_command == naz.SmppCommand.DELIVER_SM:
               conn = sqlite3.connect('mySmsDB.db')
               c = conn.cursor()
               t = (log_id,)
               # watch out for SQL injections!!
               c.execute("UPDATE SmsTable SET State='delivered' WHERE CorrelatinID=?", t)
               conn.commit()
               conn.close()

   stateHook = SetMessageStateHook()
   cli = naz.Client(
       ...
       hook=stateHook,
   )

2.3 integration with bug trackers
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

| If you want to integrate ``naz`` with your bug/issue tracker of
  choice, all you have to do is use their logging integrator.
| As an example, to integrate ``naz`` with
  `sentry <https://sentry.io/>`__, all you have to do is import and init
  the sentry sdk. A good place to do that would be in the naz config
  file, ie;
| ``/tmp/my_config.py``

.. code:: python

   import naz
   from myfile import ExampleQueue

   import sentry_sdk # import sentry SDK
   sentry_sdk.init("https://<YOUR_SENTRY_PUBLIC_KEY>@sentry.io/<YOUR_SENTRY_PROJECT_ID>")

   my_naz_client = naz.Client(
       smsc_host="127.0.0.1",
       smsc_port=2775,
       system_id="smppclient1",
       password="password",
       outboundqueue=ExampleQueue()
   )

| then run the ``naz-cli`` as usual:
| ``naz-cli --client tmp.my_config.my_naz_client``
| And just like that you are good to go. This is what errors from
  ``naz`` will look like on sentry(sans the emojis, ofcourse):

.. figure:: https://raw.githubusercontent.com/komuw/naz/master/documentation/sphinx-docs/naz-sentry.png
   :alt: naz integration with sentry

   naz integration with sentry

3. Rate limiting
^^^^^^^^^^^^^^^^

| Sometimes you want to control the rate at which the client sends
  requests to an SMSC/server. ``naz`` lets you do this, by allowing you
  to specify a custom rate limiter. By default, ``naz`` uses a simple
  token bucket rate limiting algorithm `implemented
  here <https://github.com/komuw/naz/blob/master/naz/ratelimiter.py>`__.
| You can customize ``naz``\ ’s ratelimiter or even write your own
  ratelimiter (if you decide to write your own, you just have to satisfy
  the ``BaseRateLimiter`` interface `found
  here <https://github.com/komuw/naz/blob/master/naz/ratelimiter.py>`__
  )
| To customize the default ratelimiter, for example to send at a rate of
  35 requests per second.

.. code:: python

   import naz

   myLimiter = naz.ratelimiter.SimpleRateLimiter(send_rate=35)
   cli = naz.Client(
       ...
       rateLimiter=myLimiter,
   )

4. Throttle handling
^^^^^^^^^^^^^^^^^^^^

| Sometimes, when a client sends requests to an SMSC/server, the SMSC
  may reply with an ``ESME_RTHROTTLED`` status.
| This can happen, say if the client has surpassed the rate at which it
  is supposed to send requests at, or the SMSC is under load or for
  whatever reason ¯_(ツ)_/¯
| The way ``naz`` handles throtlling is via Throttle handlers.
| A throttle handler is a class that implements the
  ``BaseThrottleHandler`` interface as `defined
  here <https://github.com/komuw/naz/blob/master/naz/throttle.py>`__
| ``naz`` calls that class’s ``throttled`` method everytime it gets a
  throttled(\ ``ESME_RTHROTTLED``) response from the SMSC and it also
  calls that class’s ``not_throttled`` method everytime it gets a
  response from the SMSC and the response is NOT a throttled response.
| ``naz`` will also call that class’s ``allow_request`` method just
  before sending a request to SMSC. the ``allow_request`` method should
  return ``True`` if requests should be allowed to SMSC else it should
  return ``False`` if requests should not be sent.
| By default ``naz`` uses
  ```naz.throttle.SimpleThrottleHandler`` <https://github.com/komuw/naz/blob/master/naz/throttle.py>`__
  to handle throttling.
| The way ``SimpleThrottleHandler`` works is, it calculates the
  percentage of responses that are throttle responses and then denies
  outgoing requests(towards SMSC) if percentage of responses that are
  throttles goes above a certain metric.
| As an example if you want to deny outgoing requests if the percentage
  of throttles is above 1.2% over a period of 180 seconds and the total
  number of responses from SMSC is greater than 45, then;

.. code:: python

   import naz

   throttler = naz.throttle.SimpleThrottleHandler(sampling_period=180,
                                                  sample_size=45,
                                                  deny_request_at=1.2)
   cli = naz.Client(
       ...
       throttle_handler=throttler,
   )

5. Queuing
^^^^^^^^^^

| **How does your application and ``naz`` talk with each other?**
| It’s via a queuing interface. Your application queues messages to a
  queue, ``naz`` consumes from that queue and then ``naz`` sends those
  messages to SMSC/server.
| You can implement the queuing mechanism any way you like, so long as
  it satisfies the ``BaseOutboundQueue`` interface as `defined
  here <https://github.com/komuw/naz/blob/master/naz/q.py>`__
| Your application should call that class’s ``enqueue`` method to -you
  guessed it- enqueue messages to the queue while ``naz`` will call the
  class’s ``dequeue`` method to consume from the queue.
| Your application should enqueue a dictionary/json object with any
  parameters but the following are mandatory:

.. code:: bash

   {
       "version": "1",
       "smpp_command": naz.SmppCommand.SUBMIT_SM,
       "short_message": string,
       "log_id": string,
       "source_addr": string,
       "destination_addr": string
   }

For more information about all the parameters that are needed in the
enqueued json object, consult the `documentation
here <https://github.com/komuw/naz/blob/master/documentation/config.md>`__

| ``naz`` ships with a simple queue implementation called
  ```naz.q.SimpleOutboundQueue`` <https://github.com/komuw/naz/blob/master/naz/q.py>`__.
| An example of using that;

.. code:: python

   import asyncio
   import naz

   loop = asyncio.get_event_loop()
   my_queue = naz.q.SimpleOutboundQueue(maxsize=1000,) # can hold upto 1000 items
   cli = naz.Client(
       ...
       outboundqueue=my_queue,
   )
   # connect to the SMSC host
   loop.run_until_complete(cli.connect())
   # bind to SMSC as a tranceiver
   loop.run_until_complete(cli.tranceiver_bind())

   try:
       # read any data from SMSC, send any queued messages to SMSC and continually check the state of the SMSC
       tasks = asyncio.gather(cli.dequeue_messages(), cli.receive_data(), cli.enquire_link())
       loop.run_until_complete(tasks)
       loop.run_forever()
   except Exception as e:
       print("exception occured. error={0}".format(str(e)))
   finally:
       loop.run_until_complete(cli.unbind())
       loop.stop()

then in your application, queue items to the queue;

.. code:: python

   # queue messages to send
   for i in range(0, 4):
       loop.run_until_complete(
           cli.submit_sm(
               short_message="Hello World-{0}".format(str(i)),
               log_id="myid12345",
               source_addr="254722111111",
               destination_addr="254722999999",
           )
       )

Here is another example, but where we now use redis for our queue;

.. code:: python

   import json
   import asyncio
   import naz
   import aioredis

   class RedisExampleQueue(naz.q.BaseOutboundQueue):
       """
       use redis as our queue.
       This implements a basic FIFO queue using redis.
       Basically we use the redis command LPUSH to push messages onto the queue and BRPOP to pull them off.
       https://redis.io/commands/lpush
       https://redis.io/commands/brpop
       You should use a non-blocking redis client eg https://github.com/aio-libs/aioredis
       """
       def __init__(self):
           self.queue_name = "myqueue"
       async def enqueue(self, item):
           _redis = await aioredis.create_redis_pool(address=("localhost", 6379))
           await _redis.lpush(self.queue_name, json.dumps(item))
       async def dequeue(self):
           _redis = await aioredis.create_redis_pool(address=("localhost", 6379))
           x = await _redis.brpop(self.queue_name)
           dequed_item = json.loads(x[1].decode())
           return dequed_item

   loop = asyncio.get_event_loop()
   outboundqueue = RedisExampleQueue()
   cli = naz.Client(
       smsc_host="127.0.0.1",
       smsc_port=2775,
       system_id="smppclient1",
       password="password",
       outboundqueue=outboundqueue,
   )
   # connect to the SMSC host
   reader, writer = loop.run_until_complete(cli.connect())
   # bind to SMSC as a tranceiver
   loop.run_until_complete(cli.tranceiver_bind())
   try:
       # read any data from SMSC, send any queued messages to SMSC and continually check the state of the SMSC
       tasks = asyncio.gather(cli.dequeue_messages(), cli.receive_data(), cli.enquire_link())
       loop.run_until_complete(tasks)
       loop.run_forever()
   except Exception as e:
       print("error={0}".format(str(e)))
   finally:
       loop.run_until_complete(cli.unbind())
       loop.stop()

then queue on your application side;

.. code:: python

   # queue messages to send
   for i in range(0, 5):
       print("submit_sm round:", i)
       loop.run_until_complete(
           cli.submit_sm(
               short_message="Hello World-{0}".format(str(i)),
               log_id="myid12345",
               source_addr="254722111111",
               destination_addr="254722999999",
           )
       )

6. Well written(if I have to say so myself):
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

-  `Good test coverage <https://codecov.io/gh/komuw/naz>`__
-  `Passing continous
   integration <https://travis-ci.com/komuw/naz/builds>`__
-  `statically analyzed
   code <https://www.codacy.com/app/komuw/naz/dashboard>`__

Development setup
-----------------

-  see `documentation on
   contributing <https://github.com/komuw/naz/blob/master/.github/CONTRIBUTING.md>`__
-  **NB:** I make no commitment of accepting your pull requests.

## TODO
-------


