From ecb65277c4a0fb7530d76765874d50b1bf65f74e Mon Sep 17 00:00:00 2001 From: Komu Wairagu Date: Sat, 2 Feb 2019 11:46:44 +0300 Subject: [PATCH] Issues/94 - the interfaces should be abc.ABC (#95) What: - change all interfaces to use `abc.ABC` Why: - idiomatic constraints - closes https://github.com/komuw/naz/issues/94 --- CHANGELOG.md | 1 + Makefile | 4 +- documentation/sphinx-docs/introduction.rst | 87 +++++++++++++++------- naz/correlater.py | 5 +- naz/hooks.py | 5 +- naz/logger.py | 5 +- naz/nazcodec.py | 8 +- naz/q.py | 6 +- naz/ratelimiter.py | 4 +- naz/sequence.py | 6 +- naz/throttle.py | 7 +- 11 files changed, 97 insertions(+), 41 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f03e92f..2cad57ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/Makefile b/Makefile index 92665e72..592dea7d 100644 --- a/Makefile +++ b/Makefile @@ -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 @@ -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 diff --git a/documentation/sphinx-docs/introduction.rst b/documentation/sphinx-docs/introduction.rst index d8147702..f5163eee 100644 --- a/documentation/sphinx-docs/introduction.rst +++ b/documentation/sphinx-docs/introduction.rst @@ -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 `_ -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. @@ -78,14 +78,14 @@ NB: * (a) For more information about all the parameters that `naz.Client` can take, consult the `documentation here `_ * (b) More examples can be found `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. +* (c) if you need an SMSC server/gateway to test with, you can use the `docker-compose `_ 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` @@ -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 @@ -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 @@ -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: @@ -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 @@ -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 @@ -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`` @@ -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 @@ -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 `_ -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 diff --git a/naz/correlater.py b/naz/correlater.py index aba0331b..10d4225e 100644 --- a/naz/correlater.py +++ b/naz/correlater.py @@ -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 @@ -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. @@ -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. diff --git a/naz/hooks.py b/naz/hooks.py index 1dcfecf8..0eea1cdb 100644 --- a/naz/hooks.py +++ b/naz/hooks.py @@ -1,3 +1,4 @@ +import abc import logging from typing import TYPE_CHECKING @@ -5,7 +6,7 @@ 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 @@ -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. @@ -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: diff --git a/naz/logger.py b/naz/logger.py index 264177bf..8e8c757c 100644 --- a/naz/logger.py +++ b/naz/logger.py @@ -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 @@ -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 @@ -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. diff --git a/naz/nazcodec.py b/naz/nazcodec.py index c0f1f939..b8847eb8 100644 --- a/naz/nazcodec.py +++ b/naz/nazcodec.py @@ -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 @@ -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 @@ -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 @@ -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. diff --git a/naz/q.py b/naz/q.py index 2c6e90b7..982aee8c 100644 --- a/naz/q.py +++ b/naz/q.py @@ -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 @@ -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. @@ -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. diff --git a/naz/ratelimiter.py b/naz/ratelimiter.py index 4dc24b4a..db652c19 100644 --- a/naz/ratelimiter.py +++ b/naz/ratelimiter.py @@ -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 @@ -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. diff --git a/naz/sequence.py b/naz/sequence.py index f6a6d775..25bd0146 100644 --- a/naz/sequence.py +++ b/naz/sequence.py @@ -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 @@ -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 diff --git a/naz/throttle.py b/naz/throttle.py index 2eb83ab2..557bfec5 100644 --- a/naz/throttle.py +++ b/naz/throttle.py @@ -1,8 +1,9 @@ +import abc import time import logging -class BaseThrottleHandler: +class BaseThrottleHandler(abc.ABC): """ This is the interface that must be implemented to satisfy naz's throttle handling. User implementations should inherit this class and @@ -16,18 +17,21 @@ class BaseThrottleHandler: rate limiting itself. The way naz implements this self imposed self-regulation is via Throttle Handlers. """ + @abc.abstractmethod async def throttled(self) -> None: """ this method will be called by naz everytime we get a throttling response from SMSC. """ raise NotImplementedError("throttled method must be implemented.") + @abc.abstractmethod async def not_throttled(self) -> None: """ this method will be called by naz everytime we get any response from SMSC that is not a throttling response. """ raise NotImplementedError("not_throttled method must be implemented.") + @abc.abstractmethod async def allow_request(self) -> bool: """ this method will be called by naz just before sending a request to SMSC. @@ -35,6 +39,7 @@ async def allow_request(self) -> bool: """ raise NotImplementedError("allow_request method must be implemented.") + @abc.abstractmethod async def throttle_delay(self) -> float: """ if the last :func:`allow_request ` method call returned False(thus denying sending a request),