Skip to main content

Naz is an SMPP client.

Project description

naz

Codacy Badge Build Status codecov Code style: black

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
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.

Installation

pip install naz

Usage

1. As a library

import asyncio
import naz

loop = asyncio.get_event_loop()
outboundqueue = naz.q.SimpleOutboundQueue(maxsize=1000, loop=loop)
cli = naz.Client(
    async_loop=loop,
    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)
    item_to_enqueue = {
        "version": "1",
        "smpp_event": "submit_sm",
        "short_message": "Hello World-{0}".format(str(i)),
        "correlation_id": "myid12345",
        "source_addr": "254722111111",
        "destination_addr": "254722999999",
    }
    loop.run_until_complete(outboundqueue.enqueue(item_to_enqueue))

# 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.send_forever(), 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.close()
NB:
(a) For more information about all the parameters that naz.Client can take, consult the documentation here
(c) if you need a SMSC server/gateway to test with, you can use the docker-compose file in this repo 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 json config file, eg;
/tmp/my_config.json
{
  "smsc_host": "127.0.0.1",
  "smsc_port": 2775,
  "system_id": "smppclient1",
  "password": "password",
  "outboundqueue": "myfile.ExampleQueue"
}

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

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 --config /tmp/my_config.json
     Naz: the SMPP client.

{'event': 'connect', 'stage': 'start'} {'smsc_host': '127.0.0.1', 'system_id': 'smppclient1'}
{'event': 'connect', 'stage': 'end'} {'smsc_host': '127.0.0.1', 'system_id': 'smppclient1'}
{'event': 'tranceiver_bind', 'stage': 'start'} {'smsc_host': '127.0.0.1', 'system_id': 'smppclient1'}
{'event': 'send_data', 'stage': 'start', 'smpp_command': 'bind_transceiver', 'correlation_id': None} {'smsc_host': '127.0.0.1', 'system_id': 'smppclient1'}
{'event': 'SimpleHook.request', 'stage': 'start', 'correlation_id': None} {'smsc_host': '127.0.0.1', 'system_id': 'smppclient1'}
{'event': 'send_data', 'stage': 'end', 'smpp_command': 'bind_transceiver', 'correlation_id': None} {'smsc_host': '127.0.0.1', 'system_id': 'smppclient1'}
{'event': 'tranceiver_bind', 'stage': 'end'} {'smsc_host': '127.0.0.1', 'system_id': 'smppclient1'}
{'event': 'send_forever', 'stage': 'start'} {'smsc_host': '127.0.0.1', 'system_id': 'smppclient1'}
NB:
(a) For more information about the naz config file, consult the documentation here
(b) More examples can be found here. As an example, start the SMSC simulator(docker-compose up) then in another terminal run, naz-cli --config examples/example_config.json

To see help:

naz-cli --help

naz is an SMPP client.
example usage: naz-cli --config /path/to/my_config.json

optional arguments:
  -h, --help            show this help message and exit
  --version             The currently installed naz version.
  --loglevel {DEBUG,INFO,WARNING,ERROR,CRITICAL}
                        The log level to output log messages at. eg: --loglevel DEBUG
  --config CONFIG       The config file to use. eg: --config /path/to/my_config.json

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. And if you do not like python’s inbuilt event loop, you can bring your own. eg; to use uvloop;
import naz
import asyncio
import uvloop

asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
loop = asyncio.get_event_loop()
outboundqueue = naz.q.SimpleOutboundQueue(maxsize=1000, loop=loop)
cli = naz.Client(
    async_loop=loop,
    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.
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 and system_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.
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 setup;
import naz
from prometheus_client import Counter

class MyPrometheusHook(naz.hooks.BaseHook):
    async def request(self, smpp_event, correlation_id):
        c = Counter('my_requests', 'Description of counter')
        c.inc() # Increment by 1
    async def response(self, smpp_event, correlation_id):
        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;

import sqlite3
import naz

class SetMessageStateHook(naz.hooks.BaseHook):
    async def request(self, smpp_event, correlation_id):
        pass
    async def response(self, smpp_event, correlation_id):
        if smpp_event == "deliver_sm":
            conn = sqlite3.connect('mySmsDB.db')
            c = conn.cursor()
            t = (correlation_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,
)

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.
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 )
To customize the default ratelimiter, for example to send at a rate of 35 requests per second.
import logging
import naz
logger = logging.getLogger("naz.rateLimiter")

myLimiter = naz.ratelimiter.SimpleRateLimiter(logger=logger, 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
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;
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
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:
{
    "version": "1",
    "smpp_event": "submit_sm",
    "short_message": string,
    "correlation_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

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;
import asyncio
import naz

loop = asyncio.get_event_loop()
my_queue = naz.q.SimpleOutboundQueue(maxsize=1000, loop=loop) # can hold upto 1000 items
cli = naz.Client(
    ...
    async_loop=loop,
    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.send_forever(), 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.close()

then in your application, queue items to the queue;

# queue messages to send
for i in range(0, 4):
    item_to_enqueue = {
        "version": "1",
        "smpp_event": "submit_sm",
        "short_message": "Hello World-{0}".format(str(i)),
        "correlation_id": "myid12345",
        "source_addr": "254722111111",
        "destination_addr": "254722999999",
    }
    loop.run_until_complete(outboundqueue.enqueue(item_to_enqueue))

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

import json
import asyncio
import naz
import redis

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
    Note that in practice, you would probaly want to use a non-blocking redis
    client eg https://github.com/aio-libs/aioredis
    """
    def __init__(self):
        self.redis_instance = redis.StrictRedis(host="localhost", port=6379, db=0)
        self.queue_name = "myqueue"
    async def enqueue(self, item):
        self.redis_instance.lpush(self.queue_name, json.dumps(item))
    async def dequeue(self):
        x = self.redis_instance.brpop(self.queue_name)
        dequed_item = json.loads(x[1].decode())
        return dequed_item

loop = asyncio.get_event_loop()
outboundqueue = RedisExampleQueue()
cli = naz.Client(
    async_loop=loop,
    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.send_forever(), 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.close()

then queue on your application side;

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

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

Development setup

## TODO

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Filename, size & hash SHA256 hash help File type Python version Upload date
naz-0.0.6-py3-none-any.whl (28.5 kB) Copy SHA256 hash SHA256 Wheel py3
naz-0.0.6.tar.gz (33.2 kB) Copy SHA256 hash SHA256 Source None

Supported by

Elastic Elastic Search Pingdom Pingdom Monitoring Google Google BigQuery Sentry Sentry Error logging AWS AWS Cloud computing DataDog DataDog Monitoring Fastly Fastly CDN SignalFx SignalFx Supporter DigiCert DigiCert EV certificate StatusPage StatusPage Status page