Skip to main content

Bi-directional SMS gateway with pluggable providers

Project description

Build Status

SMSframework

SMS framework with pluggable providers.

Key features:

  • Send messages

  • Receive messages

  • Delivery confirmations

  • Handle multiple pluggable providers with a single gateway

  • Synchronous message receipt through events

  • Reliable message handling

  • Supports provider APIs (like getting the balance)

  • Providers use Flask microframework for message receivers (not required)

  • 0 dependencies

  • Unit-tested

Table of Contents

  • Supported Providers

  • Installation

  • Gateway

    • Providers

      • Gateway.add_provider(name, Provider, **config):IProvider

      • Gateway.default_provider

      • Gateway.get_provider(name):IProvider

    • Sending Messages

      • Gateway.send(message):OutgoingMessage

    • Event Hooks

      • Gateway.onSend

      • Gateway.onReceive

      • Gateway.onStatus

  • Data Objects

    • IncomingMessage

    • OutgoingMessage

    • MessageStatus

    • Exceptions

  • Provider HTTP Receivers

    • Gateway.receiver_blueprint_for(name): flask.Blueprint

    • Gateway.receiver_blueprints():(name, flask.Blueprint)*

    • Gateway.receiver_blueprints_register(app, prefix=’/’):flask.Flask

  • Message Routing

  • Bundled Providers

    • NullProvider

    • LogProvider

    • LoopbackProvider

      • LoopbackProvider.get_traffic():list

      • LoopbackProvider.received(src, body):IncomingMessage

      • LoopbackProvider.subscribe(number, callback):IProvider

Supported Providers

SMSframework supports the following bundled providers:

  • log: log provider for testing. Bundled.

  • null: null provider for testing. Bundled.

  • loopback: loopback provider for testing. Bundled.

Supported providers list:

Also see the full list of providers.

Installation

Install from pypi:

$ pip install smsframework

Install with some additional providers:

$ pip install smsframework[clickatell]

To receive SMS messages, you need to ensure that Flask microframework is also installed:

$ pip install smsframework[clickatell,receiver]

Gateway

SMSframework handles the whole messaging thing with a single Gateway object.

Let’s start with initializing a gateway:

from smsframework import Gateway

gateway = Gateway()

The Gateway() constructor currently has no arguments.

Providers

A Provider is a package which implements the logic for a specific SMS provider.

Each provider reside in an individual package smsframework_*. You’ll probably want to install some of these first.

Gateway.add_provider(name, Provider, **config):IProvider

Register a provider on the gateway

Arguments:

  • provider: str Provider name that will be used to uniquely identify it

  • Provider: type Provider class that inherits from smsframework.IProvider You’ll use this string in order to send messages via a specific provider.

  • **config Provider configuration. Please refer to the Provider documentation.

from smsframework.providers import NullProvider
from smsframework_clickatell import ClickatellProvider

gateway.add_provider('main', ClickatellProvider)  # the default ont
gateway.add_provider('null', NullProvider)

The first provider defined becomes the default one: used in case the routing function has no better idea. See: Message Routing.

Gateway.default_provider

Property which contains the default provider name. You can change it to something else:

gateway.default_provider = 'null'

Gateway.get_provider(name):IProvider

Get a provider by name

You don’t normally need this, unless the provider has some public API: refer to the provider documentation for the details.

Sending Messages

Gateway.send(message):OutgoingMessage

To send a message, you first create the `OutgoingMessage <#outgoingmessage>`__ object and then pass it as the first argument.

Arguments:

  • message: OutgoingMessage: The messasge to send

Exceptions:

  • AssertionError: Wrong provider name encountered (returned by the router, or provided to OutgoingMessage)

  • ProviderError: Generic provider error

  • ConnectionError: Connection failed

  • MessageSendError: Generic sending error

  • RequestError: Request error: likely, validation errors

  • UnsupportedError: The requested operation is not supported

  • ServerError: Server error: sevice unavailable, etc

  • AuthError: Provider authentication failed

  • LimitsError: Sending limits exceeded

  • CreditError: Not enough money on the account

Returns: the same OutgoingMessage, with some additional fields populated: msgid, meta, ..

from smsframework import OutgoingMessage

msg = gateway.send(OutgoingMessage('+123456789', 'hi there!'))

A message sending fail when the provider raises an exception. This typically occurs when the wrapped HTTP API has returned an immediate error. Note that some errors occur later, and are typically reported with status messages: see `MessageStatus <#messagestatus>`__

Event Hooks

The Gateway object has three events you can subscribe to.

The event is a simple object that implements the += and -= operators which allow you to subscribe to the event and unsubscribe respectively.

Event hook is a python callable which accepts arguments explained in the further sections.

Note that if you accidentally replace the hook with a callable (using the = operator instead of +=), you’ll end up having a single hook, but smsframework will continue to work normally: thanks to the implementation.

See smsframework/lib/events.py.

Gateway.onSend

Outgoing Message: a message that was successfully sent.

Arguments:

  • message: OutgoingMessage: The message that was sent. See OutgoingMessage.

The message object is populated with the additional information from the provider, namely, the msgid and meta fields.

Note that if the hook raises an Exception, it will propagate to the place where Gateway.send() was called!

def on_send(message):
    """ :type message: OutgoingMessage """
    print message

gw.onSend += on_send

Gateway.onReceive

Incoming Message: a message that was received from the provider.

Arguments:

Note that if the hook raises an Exception, the Provider will report the error to the sms service. Most services will retry the message delivery with increasing delays.

def on_receive(message):
    """ :type message: IncomingMessage """
    print message

gw.onReceive += on_receive

Gateway.onStatus

Message Status: a message status reported by the provider.

A status report is only delivered when explicitly requested with OutgoingMessage.options(status_report=True).

Arguments:

  • status: MessageStatus: The status info. See MessageStatus and its subclasses.

Note that if the hook raises an Exception, the Provider will report the error to the sms service. Most services will retry the status delivery with increasing delays.

def on_status(status):
    """ :type status: MessageStatus """
    print status

gw.onStatys += status

Data Objects

SMSframework uses the following objects to represent message flows.

Note that internally all non-digit characters are removed from all phone numbers, both outgoing and incoming. Phone numbers are typically provided in international formats, though some local providers may be less strict with this.

IncomingMessage

A messsage received from the provider.

Source: smsframework/data/IncomingMessage.py.

OutgoingMessage

A message being sent.

Source: smsframework/data/OutgoingMessage.py.

MessageStatus

A status report received from the provider.

Source: smsframework/data/MessageStatus.py.

Exceptions

Source: smsframework/exc.py.

Provider HTTP Receivers

Note: the whole receiver feature is optional. Skip this section if you only need to send messages.

In order to receive messages, most providers need an HTTP handler.

To get standardized, by default providers use Flask microframework for this: a provider defines a Blueprint which can be registered on your Flask application as the receiver endpoint.

The resources are provider-dependent: refer to the provider documentation for the details. The recommended approach is to use /im for incoming messages, and /status for status reports.

Gateway.receiver_blueprint_for(name): flask.Blueprint

Get a Flask blueprint for the named provider that handles incoming messages & status reports.

Returns: flask.Blueprint

Errors:

  • KeyError: provider not found

  • NotImplementedError: Provider does not implement a receiver

This method is mostly internal, as the following ones are usually much more convenient.

Gateway.receiver_blueprints():(name, flask.Blueprint)*

Get Flask blueprints for every provider that supports it.

The method is a generator that yields (name, blueprint) tuples, where blueprint is flask.Blueprint for provider named name.

Use this method to register your receivers manually:

from flask import Flask

app = Flask()

for name, bp in gateway.receiver_blueprints():
    app.register_blueprint(bp, url_prefix='/sms/'+name)

With the example above, each receivers will be registered under /name prefix.

Assuming the ‘clickatell’ provider defines /im and /status receivers and your app is running on http://localhost:5000/, you will configure the SMS service to send messages to:

Gateway.receiver_blueprints_register(app, prefix=’/’):flask.Flask

Register all provider receivers on the provided Flask application under ‘/{prefix}/provider-name’.

This is a convenience method to register all blueprints at once using the following recommended rules:

  • If prefix is provided, all blueprints are registered under this prefix

  • Provider receivers are registered under ‘/provider-name’ path

It’s adviced to mount the receivers under some difficult-to-guess prefix: otherwise, attackers can send fake messages into your system!

Secure example:

gateway.receiver_blueprints_register(app, '/24fb0d6963f/');

NOTE: Other mechanisms, such as basic authentication, are not typically useful as some services do not support that.

Message Routing

SMSframework requires you to explicitly specify the provider for each message: otherwise, it uses the first defined provider by default.

In real world conditions with multiple providers, you may want a router function that decides on which provider to use and which options to pick.

In order to achieve flexible message routing, we need to associate some metadata with each message, for instance:

  • module: name of the sending module: e.g. “users”

  • type: type of the message: e.g. “notification”

These 2 arbitrary strings need to be standardized in the application code, thus offering the possibility to define complex routing rules.

When creating the message, use OutgoingMessage.route() function to specify these values:

gateway.send(OutgoingMessage('+1234', 'hi').route('users', 'notification'))

Now, set a router function on the gateway: a function which gets an outgoing message + some additional routing values, and decides on the provider to use:

gateway.add_provider('primary', ClickatellProvider, ...)
gateway.add_provider('quick', ClickatellProvider, ...)
gateway.add_provider('usa', ClickatellProvider, ...)

def router(message, module, type):
    """ Custom router function """
    if message.dst.startswith('1'):
        return 'usa'  # Use 'usa' for all messages sent to the United States
    elif type == 'notification':
        return 'quick'  # use the 'quick' for all notifications
    else:
        return None  # Use the default provider ('primary') for everything else

    self.gw.router = router

Router function is also the right place to specify provider-specific options.

Bundled Providers

The following providers are bundled with SMSframework and thus require no additional packages.

NullProvider

Source: smsframework/providers/null.py

The 'null' provider just ignores all outgoing messages.

Configuration: none

Sending: does nothing, but increments message.msgid

Receipt: Not implemented

Status: Not implemented

from smsframework.providers import NullProvider

gw.add_provider('null', NullProvider)

LogProvider

Source: smsframework/providers/log.py

Logs the outgoing messages to a python logger provided as the config option.

Configuration:

  • logger: logging.Logger: The logger to use

Sending: does nothing, increments message.msgid, prints the message to the log

Receipt: Not implemented

Status: Not implemented

Example:

import logging
from smsframework.providers import LogProvider

gw.add_provider('log', LogProvider, logger=logging.getLogger(__name__))

LoopbackProvider

Source: smsframework/providers/loopback.py

The 'loopback' provider is used as a dummy for testing purposes.

All messages are stored in the local log and can be retrieved as a list.

The provider even supports status & delivery notifications.

In addition, is supports virtual subscribers: callbacks bound to some phone numbers which are called when any simulated message is sent to their phone number. Replies are also supported!

Configuration: none

Sending: sends message to a registered subscriber (see: :meth:LoopbackProvider.subscribe), silently ignores other messages.

Receipt: simulation with a method

Status: always reports success

LoopbackProvider.get_traffic():list

LoopbackProvider stores all messages that go through it: both IncomingMessage and OutgoingMessage.

To get those messages, call .get_traffic(). This method empties the message log and returns its previous state:

from smsframework.providers import LoopbackProvider

gateway.add_provider('lo', LoopbackProvider);
gateway.send(OutgoingMessage('+123', 'hi'))

traffic = gateway.get_provider('lo').get_traffic()
print traffic[0].body  #-> 'hi'

LoopbackProvider.received(src, body):IncomingMessage

Simulate an incoming message.

The message is reported to the Gateway as if it has been received from the sms service.

Arguments:

  • src: str: Source number

  • body: str | unicode: Message text

Returns: IncomingMessage

LoopbackProvider.subscribe(number, callback):IProvider

Register a virtual subscriber which receives messages to the matching number.

Arguments:

  • number: str: Subscriber phone number

  • callback:: A callback(OutgoingMessage) which handles the messages directed to the subscriber. The message object is augmented with the .reply(str) method which allows to send a reply easily!

def subscriber(message):
    print message  #-> OutgoingMessage('1', 'obey me')
    message.reply('got it')  # use the augmented reply method

provider = gateway.get_provider('lo')
provider.subscribe('+1', subscriber)  # register the subscriber

gateway.send('+1', 'obey me')

Project details


Download files

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

Source Distribution

smsframework-0.0.2-1.tar.gz (18.6 kB view details)

Uploaded Source

Built Distribution

smsframework-0.0.2_1-py2.7.egg (33.0 kB view details)

Uploaded Source

File details

Details for the file smsframework-0.0.2-1.tar.gz.

File metadata

File hashes

Hashes for smsframework-0.0.2-1.tar.gz
Algorithm Hash digest
SHA256 90ffd381879e0daaf7d03a92e2abf9f9821afc25c1cdad0b52c584c8fcc580a6
MD5 a0ad6c9e0a13158f2f4014f134f2751c
BLAKE2b-256 55a606a8d403339392e2825d937a692b76caae81de0d356233baed01560f065a

See more details on using hashes here.

File details

Details for the file smsframework-0.0.2_1-py2.7.egg.

File metadata

File hashes

Hashes for smsframework-0.0.2_1-py2.7.egg
Algorithm Hash digest
SHA256 7465c607714f07d17f34bd2ab8027cf41d84749df7c0ad153cb07333bfa2d220
MD5 a262af931764285428f20565ce59f804
BLAKE2b-256 c5ba26b9bf931467408856f55ae408ab10361d15d4cc8cc8fb33953e1fdd921f

See more details on using hashes here.

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page