Skip to content

Commit

Permalink
Issues/94 - the interfaces should be abc.ABC (#95)
Browse files Browse the repository at this point in the history
What:
- change all interfaces to use `abc.ABC`

Why:
- idiomatic constraints
- closes #94
  • Loading branch information
komuw authored Feb 2, 2019
1 parent dbc7d0f commit ecb6527
Show file tree
Hide file tree
Showing 11 changed files with 97 additions and 41 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@
- Start tracking changes in a changelog
- Add more type hints and also run `mypy` across the entire repo: https://github.com/komuw/naz/pull/92
- It's now possible to bring your own logger: https://github.com/komuw/naz/pull/93
- Made the various interfaces in `naz` to inherit from `abc.ABC`: https://github.com/komuw/naz/pull/95
4 changes: 1 addition & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@ upload:


VERSION_STRING=$$(cat naz/__version__.py | grep "__version__" | sed -e 's/"__version__"://' | sed -e 's/,//g' | sed -e 's/"//g' | sed -e 's/ //g')
LAST_TAG=$$(git describe --tags --abbrev=0)
COMMIT_MESSAGES_SINCE_LAST_TAG=$$(git log "$(LAST_TAG)"...master)
uploadprod:
@rm -rf build
@rm -rf dist
Expand All @@ -19,7 +17,7 @@ uploadprod:
@python setup.py bdist_wheel
@twine upload dist/*
@printf "\n creating git tag: $(VERSION_STRING) \n"
@printf "\n with commit message $(COMMIT_MESSAGES_SINCE_LAST_TAG) \n" && git tag -a "$(VERSION_STRING)" -m "$(COMMIT_MESSAGES_SINCE_LAST_TAG)"
@printf "\n with commit message, see Changelong: https://github.com/komuw/naz/blob/master/CHANGELOG.md \n" && git tag -a "$(VERSION_STRING)" -m "see Changelong: https://github.com/komuw/naz/blob/master/CHANGELOG.md"
@printf "\n git push the tag::\n" && git push --all -u --follow-tags
@pip install -U naz

Expand Down
87 changes: 60 additions & 27 deletions documentation/sphinx-docs/introduction.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@
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
``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 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.

Expand Down Expand Up @@ -78,14 +78,14 @@ 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 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.
* (c) if you need an SMSC server/gateway to test with, you can use the `docker-compose <https://github.com/komuw/naz/blob/master/docker-compose.yml>`_ file in the ``naz`` 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 `naz.q.BaseOutboundQueue`.



2.2 As a cli app
=====================
``naz`` also ships with a commandline interface app called ``naz-cli``.
``naz`` also ships with a commandline interface app called ``naz-cli`` (it is also installed by default when you `pip install naz`).

create a json config file, eg; `/tmp/my_config.json`

Expand Down Expand Up @@ -131,8 +131,9 @@ NB:

3.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;
| 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;
.. code-block:: python
Expand All @@ -156,8 +157,8 @@ It thus makes sense to write your SMPP client in an async manner. We leverage py

3.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.
| 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-block:: python
Expand All @@ -167,8 +168,38 @@ So, for example if you wanted to annotate all log-events with a release version
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
| 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
``naz`` also gives you the ability to supply your own logger.
For example if you wanted ``naz`` to use key=value style of logging, then just create a logger that does just that:

.. code-block:: python
import naz
class KVlogger(naz.logger.BaseLogger):
def __init__(self):
self.logger = logging.getLogger("myKVlogger")
handler = logging.StreamHandler()
formatter = logging.Formatter("%(message)s")
handler.setFormatter(formatter)
if not self.logger.handlers:
self.logger.addHandler(handler)
self.logger.setLevel("DEBUG")
def bind(self, loglevel, log_metadata):
pass
def log(self, level, log_data):
# implementation of key=value log renderer
message = ", ".join("{0}={1}".format(k, v) for k, v in log_data.items())
self.logger.log(level, message)
kvLog = KVlogger()
cli = naz.Client(
...
log_handler=kvLog,
)
``naz`` also gives you the ability to supply your own logger.
For example if you wanted ``naz`` to use key=value style of logging, then just create a logger that does just that:
Expand Down Expand Up @@ -202,10 +233,10 @@ For example if you wanted ``naz`` to use key=value style of logging, then just c
3.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;
| A hook is a class with two methods `request` and `response`, ie it implements naz's ``naz.hooks.BaseHook`` interface.
| ``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 just logs the request and response.
| If you wanted, for example to keep metrics of all requests and responses to SMSC in your prometheus setup;
.. code-block:: python
Expand Down Expand Up @@ -263,9 +294,10 @@ another example is if you want to update a database record whenever you get a de
3.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.
| 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 in ``naz.ratelimiter.SimpleRateLimiter``
| 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 ``naz.ratelimiter.BaseRateLimiter`` interface)
| To customize the default ratelimiter, for example to send at a rate of 35 requests per second.
.. code-block:: python
Expand All @@ -281,9 +313,8 @@ To customize the default ratelimiter, for example to send at a rate of 35 reques
3.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 ¯_(ツ)_/¯
| 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 ``naz.BaseThrottleHandler``
Expand All @@ -308,10 +339,10 @@ As an example if you want to deny outgoing requests if the percentage of throttl

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 ``naz.BaseOutboundQueue``
You can implement the queuing mechanism any way you like, so long as it satisfies the ``naz.q.BaseOutboundQueue``

Your application should call that class's enqueue method to enqueue messages.
Your application should enqueue a dictionary/json object with any parameters but the following are mandatory:
| Your application should call that class's enqueue method to enqueue messages.
| Your application should enqueue a dictionary/json object with any parameters but the following are mandatory:
.. code-block:: bash
Expand All @@ -326,8 +357,10 @@ Your application should enqueue a dictionary/json object with any parameters but
For more information about all the parameters that are needed in the enqueued json object, consult the `documentation <https://github.com/komuw/naz/blob/master/documentation/config.md#2-naz-enqueued-message-protocol>`_

naz ships with a simple queue implementation called ``naz.q.SimpleOutboundQueue``
An example of using that;
| naz ships with a simple queue implementation called ``naz.q.SimpleOutboundQueue``
| **NB:** ``naz.q.SimpleOutboundQueue`` should only be used for demo/test purposes.
An example of using that queue;

.. code-block:: python
Expand Down
5 changes: 4 additions & 1 deletion naz/correlater.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import abc
import time
from typing import Tuple


class BaseCorrelater:
class BaseCorrelater(abc.ABC):
"""
Interface that must be implemented to satisfy naz's Correlater.
User implementations should inherit this class and
Expand All @@ -15,6 +16,7 @@ class BaseCorrelater:
One reason is that the SMPP specifiation mandates sequence numbers to wrap around after ≈ 2billion.
"""

@abc.abstractmethod
async def put(self, sequence_number: int, log_id: str, hook_metadata: str) -> None:
"""
called by naz to put/store the correlation of a given SMPP sequence number to log_id and/or hook_metadata.
Expand All @@ -26,6 +28,7 @@ async def put(self, sequence_number: int, log_id: str, hook_metadata: str) -> No
"""
raise NotImplementedError("put method must be implemented.")

@abc.abstractmethod
async def get(self, sequence_number: int) -> Tuple[str, str]:
"""
called by naz to get the correlation of a given SMPP sequence number to log_id and/or hook_metadata.
Expand Down
5 changes: 4 additions & 1 deletion naz/hooks.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import abc
import logging
from typing import TYPE_CHECKING

if TYPE_CHECKING:
import naz # noqa: F401


class BaseHook:
class BaseHook(abc.ABC):
"""
Interface that must be implemented to satisfy naz's hooks.
User implementations should inherit this class and
Expand All @@ -15,6 +16,7 @@ class BaseHook:
just after a response is received from SMSC.
"""

@abc.abstractmethod
async def request(self, smpp_command: str, log_id: str, hook_metadata: str) -> None:
"""
called before a request is sent to SMSC.
Expand All @@ -26,6 +28,7 @@ async def request(self, smpp_command: str, log_id: str, hook_metadata: str) -> N
"""
raise NotImplementedError("request method must be implemented.")

@abc.abstractmethod
async def response(
self, smpp_command: str, log_id: str, hook_metadata: str, smsc_response: "naz.CommandStatus"
) -> None:
Expand Down
5 changes: 4 additions & 1 deletion naz/logger.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import abc
import typing
import logging


class BaseLogger:
class BaseLogger(abc.ABC):
"""
Interface that must be implemented to satisfy naz's logger.
User implementations should inherit this class and
Expand All @@ -12,6 +13,7 @@ class BaseLogger:
This enables developers to implement logging in any way that they want.
"""

@abc.abstractmethod
def bind(self, loglevel: str, log_metadata: dict) -> None:
"""
called when a naz client is been instantiated so that the logger can be
Expand All @@ -24,6 +26,7 @@ def bind(self, loglevel: str, log_metadata: dict) -> None:
"""
raise NotImplementedError("bind method must be implemented.")

@abc.abstractmethod
def log(self, level: int, log_data: dict) -> None:
"""
called by naz everytime it wants to log something.
Expand Down
8 changes: 5 additions & 3 deletions naz/nazcodec.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,9 @@
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.


import codecs
import abc
import sys
import codecs


# An alternative to using this codec module is to use: https://github.com/dsch/gsm0338
Expand Down Expand Up @@ -160,7 +160,7 @@ def decode(self, input, errors="strict"):
return codecs.utf_16_be_decode(input, errors)


class BaseNazCodec:
class BaseNazCodec(abc.ABC):
"""
This is the interface that must be implemented to satisfy naz's encoding/decoding.
User implementations should inherit this class and
Expand All @@ -169,6 +169,7 @@ class BaseNazCodec:
naz calls an implementation of this class to encode/decode messages.
"""

@abc.abstractmethod
def encode(self, string_to_encode: str, encoding: str, errors: str) -> bytes:
"""
return an encoded version of the string as a bytes object
Expand All @@ -183,6 +184,7 @@ def encode(self, string_to_encode: str, encoding: str, errors: str) -> bytes:
"""
raise NotImplementedError("encode method must be implemented.")

@abc.abstractmethod
def decode(self, byte_string: bytes, encoding: str, errors: str) -> str:
"""
return a string decoded from the given bytes.
Expand Down
6 changes: 4 additions & 2 deletions naz/q.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import abc
import asyncio

import typing


class BaseOutboundQueue:
class BaseOutboundQueue(abc.ABC):
"""
This is the interface that must be implemented to satisfy naz's outbound queue.
User implementations should inherit this class and
Expand All @@ -12,6 +12,7 @@ class BaseOutboundQueue:
naz calls an implementation of this class to enqueue and/or dequeue an item.
"""

@abc.abstractmethod
async def enqueue(self, item: dict) -> None:
"""
enqueue/save an item.
Expand All @@ -21,6 +22,7 @@ async def enqueue(self, item: dict) -> None:
"""
raise NotImplementedError("enqueue method must be implemented.")

@abc.abstractmethod
async def dequeue(self) -> typing.Dict[typing.Any, typing.Any]:
"""
dequeue an item.
Expand Down
4 changes: 3 additions & 1 deletion naz/ratelimiter.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import abc
import time
import asyncio
import logging


class BaseRateLimiter:
class BaseRateLimiter(abc.ABC):
"""
This is the interface that must be implemented to satisfy naz's rate limiting.
User implementations should inherit this class and
Expand All @@ -13,6 +14,7 @@ class BaseRateLimiter:
naz lets you do this, by allowing you to specify a custom rate limiter.
"""

@abc.abstractmethod
async def limit(self) -> None:
"""
rate limit sending of messages to SMSC.
Expand Down
6 changes: 5 additions & 1 deletion naz/sequence.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
class BaseSequenceGenerator:
import abc


class BaseSequenceGenerator(abc.ABC):
"""
Interface that must be implemented to satisfy naz's sequence generator.
User implementations should inherit this class and
Expand All @@ -10,6 +13,7 @@ class BaseSequenceGenerator:
The sequence_number should wrap around when it reaches the maximum allowed by SMPP specification.
"""

@abc.abstractmethod
def next_sequence(self) -> int:
"""
method that returns a monotonically increasing Integer in the range 1 - 2,147,483,647
Expand Down
Loading

0 comments on commit ecb6527

Please sign in to comment.