Package: naz
naznaz 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). -Wikipedianaz currently only supports SMPP version 3.4.naz has no third-party dependencies and it requires python version
3.7+naz is in active development and it’s API may change in backward
incompatible ways.https://pypi.python.org/pypi/nazComprehensive documetion is available ->DocumentationContents:InstallationUsage+As a library+As cli appFeatures+async everywhere+monitoring-and-observability+logging+hooks+integration with bug trackers(eg Sentry
)+Rate
limiting+Throttle handling+BrokerBenchmarksInstallationpipinstallnazUsage1. As a libraryimportasyncioimportnazloop=asyncio.get_event_loop()broker=naz.broker.SimpleBroker(maxsize=1000)cli=naz.Client(smsc_host="127.0.0.1",smsc_port=2775,system_id="smppclient1",password="password",broker=broker,)# queue messages to sendforiinrange(0,4):print("submit_sm round:",i)msg=naz.protocol.SubmitSM(short_message="Hello World-{0}".format(str(i)),log_id="myid12345",source_addr="254722111111",destination_addr="254722999999",)loop.run_until_complete(cli.send_message(msg))try:# 1. connect to the SMSC host# 2. bind to the SMSC host# 3. send any queued messages to SMSC# 4. read any data from SMSC# 5. continually check the state of the SMSCtasks=asyncio.gather(cli.connect(),cli.tranceiver_bind(),cli.dequeue_messages(),cli.receive_data(),cli.enquire_link(),)loop.run_until_complete(tasks)exceptExceptionase: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 thatnaz.Clientcan take, consult thedocumentation
here(b) Moreexamples can be found
here(c) if you need a SMSC server/gateway to test with, you can use thedocker-compose file in this
repoto 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 broker.2. As a cli appnaz also ships with a commandline interface app callednaz-cli.create a python config file, eg;/tmp/my_config.pyimportnazfrommyfileimportExampleBrokerclient=naz.Client(smsc_host="127.0.0.1",smsc_port=2775,system_id="smppclient1",password="password",broker=ExampleBroker())and a python file,myfile.py(in the current working directory) with
the contents:importasyncioimportnazclassExampleBroker(naz.broker.BaseBroker):def__init__(self):loop=asyncio.get_event_loop()self.queue=asyncio.Queue(maxsize=1000,loop=loop)asyncdefenqueue(self,message):self.queue.put_nowait(message)asyncdefdequeue(self):returnawaitself.queue.get()then run:naz-cli--clienttmp.my_config.clientNaz:theSMPPclient.{'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.to_smsc','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) Thenazconfig file(ie, the dotted path we pass in tonaz-cli--client) is any python file that has anaz.Client instance<https://komuw.github.io/naz/client.html>_
declared in it.(b) Moreexamples can be found
here. As an
example, start the SMSC simulator(docker-composeup) then in
another terminal run,naz-cli--clientexamples.example_config.clientTo see help:naz-cli--helpnazisanasyncSMPPclient.exampleusage:naz-cli--clientpath.to.my_config.clientoptionalarguments:-h,--helpshowthishelpmessageandexit--versionThecurrentlyinstallednazversion.--clientCLIENTTheconfigfiletouse.eg:--clientpath.to.my_config.clientFeatures1. async everywhereSMPP 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.importnazimportasyncioloop=asyncio.get_event_loop()broker=naz.broker.SimpleBroker(maxsize=1000)cli=naz.Client(smsc_host="127.0.0.1",smsc_port=2775,system_id="smppclient1",password="password",broker=broker,)2. monitoring and observabilityit’s a loaded term, I know.2.1 loggingInnazyou have the ability to annotate all the log events thatnazwill 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.importnazlogger=naz.log.SimpleLogger("naz.client",log_metadata={"environment":"production","release":"v5.6.8"})cli=naz.Client(...logger=logger,)and then these will show up in all log events.by default,nazannotates all log events withsmsc_host,system_idandclient_id2.2 hooksa hook is a class with two methodsto_smscandfrom_smsc, ie
it implementsnaz’s BaseHook interface asdefined
here.nazwill call theto_smscmethod just before sending data to
SMSC and also call thefrom_smscmethod just after getting data
from SMSC.the default hook thatnazuses isnaz.hooks.SimpleHookwhich
does nothing but logs.If you wanted, for example to keep metrics of all requests and
responses to SMSC in yourprometheussetup;importnazfromprometheus_clientimportCounterclassMyPrometheusHook(naz.hooks.BaseHook):asyncdefto_smsc(self,smpp_command,log_id,hook_metadata,pdu):c=Counter('my_requests','Description of counter')c.inc()# Increment by 1asyncdeffrom_smsc(self,smpp_command,log_id,hook_metadata,status,pdu):c=Counter('my_responses','Description of counter')c.inc()# Increment by 1myHook=MyPrometheusHook()cli=naz.Client(...hook=myHook,)another example is if you want to update a database record whenever you
get a delivery notification event;importsqlite3importnazclassSetMessageStateHook(naz.hooks.BaseHook):asyncdefto_smsc(self,smpp_command,log_id,hook_metadata,pdu):passasyncdeffrom_smsc(self,smpp_command,log_id,hook_metadata,status,pdu):ifsmpp_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 trackersIf you want to integratenazwith your bug/issue tracker of
choice, all you have to do is use their logging integrator.As an example, to integratenazwithsentry, 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.pyimportnazfrommyfileimportExampleBrokerimportsentry_sdk# import sentry SDKsentry_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",broker=ExampleBroker())then run thenaz-clias usual:naz-cli--clienttmp.my_config.my_naz_clientAnd just like that you are good to go. This is what errors fromnazwill look like on sentry(sans the emojis, ofcourse):naz integration with sentry3. Rate limitingSometimes you want to control the rate at which the client sends
requests to an SMSC/server.nazlets you do this, by allowing you
to specify a custom rate limiter. By default,nazuses a simple
token bucket rate limiting algorithmimplemented
here.You can customizenaz’s ratelimiter or even write your own
ratelimiter (if you decide to write your own, you just have to satisfy
theBaseRateLimiterinterfacefound
here)To customize the default ratelimiter, for example to send at a rate of
35 requests per second.importnazmyLimiter=naz.ratelimiter.SimpleRateLimiter(send_rate=35)cli=naz.Client(...rate_limiter=myLimiter,)4. Throttle handlingSometimes, when a client sends requests to an SMSC/server, the SMSC
may reply with anESME_RTHROTTLEDstatus.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 waynazhandles throtlling is via Throttle handlers.A throttle handler is a class that implements theBaseThrottleHandlerinterface asdefined
herenazcalls that class’sthrottledmethod everytime it gets a
throttled(ESME_RTHROTTLED) response from the SMSC and it also
calls that class’snot_throttledmethod everytime it gets a
response from the SMSC and the response is NOT a throttled response.nazwill also call that class’sallow_requestmethod just
before sending a request to SMSC. theallow_requestmethod should
returnTrueif requests should be allowed to SMSC else it should
returnFalseif requests should not be sent.By defaultnazuses`naz.throttle.SimpleThrottleHandler<https://github.com/komuw/naz/blob/master/naz/throttle.py>`__
to handle throttling.The waySimpleThrottleHandlerworks 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;importnazthrottler=naz.throttle.SimpleThrottleHandler(sampling_period=180,sample_size=45,deny_request_at=1.2)cli=naz.Client(...throttle_handler=throttler,)5. BrokerHow does your application and ``naz`` talk with each other?It’s via a broker interface. Your application queues messages to a
broker,nazconsumes from that broker and thennazsends those
messages to SMSC/server.You can implement the broker mechanism any way you like, so long as it
satisfies theBaseBrokerinterface asdefined
hereYour application should call that class’senqueuemethod to -you
guessed it- enqueue messages to the queue whilenazwill call the
class’sdequeuemethod to consume from the broker.nazships with a simple broker implementation called`naz.broker.SimpleBroker<https://github.com/komuw/naz/blob/master/naz/broker.py>`__.An example of using that;importasyncioimportnazloop=asyncio.get_event_loop()my_broker=naz.broker.SimpleBroker(maxsize=1000,)# can hold upto 1000 itemscli=naz.Client(...broker=my_broker,)try:# 1. connect to the SMSC host# 2. bind to the SMSC host# 3. send any queued messages to SMSC# 4. read any data from SMSC# 5. continually check the state of the SMSCtasks=asyncio.gather(cli.connect(),cli.tranceiver_bind(),cli.dequeue_messages(),cli.receive_data(),cli.enquire_link(),)loop.run_until_complete(tasks)exceptExceptionase: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;# queue messages to sendforiinrange(0,4):msg=naz.protocol.SubmitSM(short_message="Hello World-{0}".format(str(i)),log_id="myid12345",source_addr="254722111111",destination_addr="254722999999",)loop.run_until_complete(cli.send_message(msg))Here is another example, but where we now use redis for our broker;importjsonimportasyncioimportnazimportaioredisclassRedisExampleBroker(naz.broker.BaseBroker):"""
    use redis as our broker.
    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"asyncdefenqueue(self,item):_redis=awaitaioredis.create_redis_pool(address=("localhost",6379))await_redis.lpush(self.queue_name,json.dumps(item))asyncdefdequeue(self):_redis=awaitaioredis.create_redis_pool(address=("localhost",6379))x=await_redis.brpop(self.queue_name)dequed_item=json.loads(x[1].decode())returndequed_itemloop=asyncio.get_event_loop()broker=RedisExampleBroker()cli=naz.Client(smsc_host="127.0.0.1",smsc_port=2775,system_id="smppclient1",password="password",broker=broker,)try:# 1. connect to the SMSC host# 2. bind to the SMSC host# 3. send any queued messages to SMSC# 4. read any data from SMSC# 5. continually check the state of the SMSCtasks=asyncio.gather(cli.connect(),cli.tranceiver_bind(),cli.dequeue_messages(),cli.receive_data(),cli.enquire_link(),)tasks=asyncio.gather(cli.dequeue_messages(),cli.receive_data(),cli.enquire_link())loop.run_until_complete(tasks)exceptExceptionase:print("error={0}".format(str(e)))finally:loop.run_until_complete(cli.unbind())loop.stop()then queue on your application side;# queue messages to sendforiinrange(0,5):print("submit_sm round:",i)msg=naz.protocol.SubmitSM(short_message="Hello World-{0}".format(str(i)),log_id="myid12345",source_addr="254722111111",destination_addr="254722999999",)loop.run_until_complete(cli.send_message(msg))6. Well written(if I have to say so myself):Good test coveragePassing continous
integrationstatically analyzed
codeDevelopment setupseedocumentation on
contributingNB:I make no commitment of accepting your pull requests.## TODO