~kf5jwc/sms-printer

83f141d0bd34662859108a44509a56292fe3bc1d — Kyle Jones 2 years ago 6a2e6e2
Break out the library.

This works because gunicorn includes $CWD, so I can import packages
like this!

Parsers are now segregated, and organized in a way that allows them to
be autoimported and avoids special-casing modules.
13 files changed, 120 insertions(+), 66 deletions(-)

M Pipfile
M Pipfile.lock
A requirements.txt
A sms_broker_parsers/__init__.py
A sms_broker_parsers/errors.py
A sms_broker_parsers/parsers/__init__.py
A sms_broker_parsers/parsers/json/__init__.py
R sms_printer/sms/schemas/{bandwidth_v1.py => rs/json/bandwidth_v1.py}
R sms_printer/sms/schemas/{bandwidth_v2.py => rs/json/bandwidth_v2.py}
R sms_printer/sms/schemas/{types.py => .py}
M sms_printer/application.py
D sms_printer/sms/__init__.py
D sms_printer/sms/schemas/__init__.py
M Pipfile => Pipfile +1 -0
@@ 8,6 8,7 @@ gunicorn = "*"
flask = "*"
pyxdg = "*"
jsonschema = "*"
automodinit = "*"

[dev-packages]
isort = "*"

M Pipfile.lock => Pipfile.lock +9 -1
@@ 1,7 1,7 @@
{
    "_meta": {
        "hash": {
            "sha256": "4578baaf6d64898434be661bb76aace3ecb7d761e09333bfcb44cab47c1a9683"
            "sha256": "34049153fbec89bde332577a7fad2d381bbd3a4dc0c0c5b0838394853f2a4055"
        },
        "pipfile-spec": 6,
        "requires": {


@@ 16,6 16,13 @@
        ]
    },
    "default": {
        "automodinit": {
            "hashes": [
                "sha256:fc0d340865be7378fe591c8db162609dea713cfef2e25d9b15836f4cd690c831"
            ],
            "index": "pypi",
            "version": "==0.16"
        },
        "click": {
            "hashes": [
                "sha256:29f99fc6125fbc931b758dc053b3114e55c77a6e4c6c3a2674a2dc986016381d",


@@ 219,6 226,7 @@
                "sha256:edb04bdd45bfd76c8292c4d9654568efaedf76fe78eb246dde69bdb13b2dad87",
                "sha256:f19f2a4f547505fe9072e15f6f4ae714af51b5a681a97f187971f50c283193b6"
            ],
            "markers": "python_version < '3.7'",
            "version": "==1.1.0"
        },
        "typing": {

A requirements.txt => requirements.txt +3 -0
@@ 0,0 1,3 @@
-i https://pypi.python.org/simple
automodinit==0.16
jsonschema==2.6.0

A sms_broker_parsers/__init__.py => sms_broker_parsers/__init__.py +2 -0
@@ 0,0 1,2 @@
from . import errors, types
from .parsers import get_mimetype_parser

A sms_broker_parsers/errors.py => sms_broker_parsers/errors.py +10 -0
@@ 0,0 1,10 @@
class NoMatchingSchema(Exception):
    """We weren't able to find a parser for this message"""

    pass


class NoParserForMimeType(Exception):
    """We weren't able to find a parser for the type matching this header"""

    pass

A sms_broker_parsers/parsers/__init__.py => sms_broker_parsers/parsers/__init__.py +30 -0
@@ 0,0 1,30 @@
import automodinit
import logging
from typing import Any, Callable, Dict, List
from .. import types
from .. import errors

MIMETYPES: Dict[str, Any] = {}
__all__: List[str] = []


def get_mimetype_parser(mimetype) -> Callable[[str], List[types.sms]]:
    """Get the class of parser, so that we can ask it for the actual parser"""
    try:
        return MIMETYPES[mimetype]

    except KeyError:
        raise errors.NoParserForMimeType


# This sets our __all__ instead of returning a list
automodinit.automodinit(__name__, __file__, globals())

for module_name in __all__:
    mod = globals()[module_name]
    for mimetype in getattr(mod, "MIMETYPES"):
        MIMETYPES[mimetype] = getattr(mod, "parse_sms")

del automodinit

__all__ = ["get_mimetype_parser"]

A sms_broker_parsers/parsers/json/__init__.py => sms_broker_parsers/parsers/json/__init__.py +47 -0
@@ 0,0 1,47 @@
"""
This module provides the filtering and parsing. They currently only
provide a list of messages with the sender number and message text.

Each module has a `SCHEMA` and a `Parser(Parser_Base)`

The `SCHEMA` is matched against incoming messages to determine the
correct parser to use for a message.

Adding more is simple enough. Define a new parser and its schema in
its own module next to the others, and add it to `SCHEMAS` here.

I use `genson` to generate schemas based on an example message.
"""


import automodinit
from typing import Dict, Type
from jsonschema import validate
from jsonschema.exceptions import ValidationError
import json
from ...errors import NoMatchingSchema
from ...types import Parser_Base

MIMETYPES = ["application/json", "application/json+xml"]
SCHEMAS: Dict[str, Type[Parser_Base]] = {}
__all__ = []

automodinit.automodinit(__name__, __file__, globals())
del automodinit
for module_name in __all__:
    mod = globals()[module_name]
    SCHEMAS[getattr(mod, "SCHEMA")] = getattr(mod, "Parser")


def parse_sms(sms_input: str) -> Type[Parser_Base]:
    for schema, sms_parser in SCHEMAS.items():
        try:
            validate(sms_input, json.loads(schema))
            return sms_parser(sms_input)
        except ValidationError:
            continue

    raise NoMatchingSchema


__all__ = ["MIMETYPES", "parse_sms"]

R sms_printer/sms/schemas/bandwidth_v1.py => sms_broker_parsers/parsers/json/bandwidth_v1.py +1 -2
@@ 1,6 1,5 @@
from typing import Dict

from .types import Parser_Base, sms
from ...types import Parser_Base, sms


class Parser(Parser_Base):

R sms_printer/sms/schemas/bandwidth_v2.py => sms_broker_parsers/parsers/json/bandwidth_v2.py +1 -2
@@ 1,6 1,5 @@
from typing import List

from .types import Parser_Base, sms
from ...types import Parser_Base, sms


class Parser(Parser_Base):

R sms_printer/sms/schemas/types.py => sms_broker_parsers/types.py +3 -3
@@ 2,8 2,8 @@ from typing import List


class sms(object):
    sender: str
    text: str
    sender: str = ""
    text: str = ""

    def __init__(self, sender: str, text: str) -> None:
        self.sender = sender


@@ 11,7 11,7 @@ class sms(object):


class Parser_Base(object):
    messages: List[sms]
    messages: List[sms] = []

    def __init__(self):
        self.messages = []

M sms_printer/application.py => sms_printer/application.py +13 -3
@@ 2,7 2,8 @@ import logging

from flask import Flask, Response, json, request

from .sms import NoMatchingSchema, parse_sms
from sms_broker_parsers import get_mimetype_parser
from sms_broker_parsers.errors import NoMatchingSchema, NoParserForMimeType
from .utils import allowed_sender, log_request, print_message

APP = Flask(__name__)


@@ 20,11 21,20 @@ def sms_printer() -> Response:
    sms_input = request.get_json(force=True)

    try:
        sms_list = parse_sms(sms_input)
        parser = get_mimetype_parser(request.mimetype)

    except NoParserForMimeType:
        logging.warning("No parsers available for the given mimetype!")
        logging.debug("mimetype: {}".format(request.mimetype))
        logging.debug("header: {}".format(request.headers["Content-Type"]))
        return Response(status=415)

    try:
        sms_list = parser(sms_input)
        log_request(sms_input)

    except NoMatchingSchema:
        logging.error("Unrecognized message format!")
        logging.warning("Unrecognized message format!")
        logging.debug(json.dumps(sms_input))
        return Response(status=415)


D sms_printer/sms/__init__.py => sms_printer/sms/__init__.py +0 -24
@@ 1,24 0,0 @@
from jsonschema import validate
from jsonschema.exceptions import ValidationError
from flask import json

from .schemas import SCHEMAS
from .schemas.types import Parser_Base


class NoMatchingSchema(Exception):
    pass


def parse_sms(sms_input: str) -> Parser_Base:
    for schema, sms_parser in SCHEMAS.items():
        try:
            validate(sms_input, json.loads(schema))
            return sms_parser(sms_input)
        except ValidationError:
            continue

    raise NoMatchingSchema


__all__ = ["parse_sms", "NoMatchingSchema"]

D sms_printer/sms/schemas/__init__.py => sms_printer/sms/schemas/__init__.py +0 -31
@@ 1,31 0,0 @@
"""
This module provides the filtering and parsing. They currently only
provide a list of messages with the sender number and message text.

Each module has a `SCHEMA` and a `Parser(Parser_Base)`

The `SCHEMA` is matched against incoming messages to determine the
correct parser to use for a message.

Adding more is simple enough. Define a new parser and its schema in
its own module next to the others, and add it to `SCHEMAS` here.

I use `genson` to generate schemas based on an example message.
"""


from typing import Dict, Type

from . import types

from . import bandwidth_v1
from . import bandwidth_v2


SCHEMAS: Dict[str, Type[types.Parser_Base]] = {}

SCHEMAS[bandwidth_v1.SCHEMA] = bandwidth_v1.Parser
SCHEMAS[bandwidth_v2.SCHEMA] = bandwidth_v2.Parser


__all__ = ["SCHEMAS", "types"]