diff --git a/apprise_api/.coveragerc b/.coveragerc
similarity index 100%
rename from apprise_api/.coveragerc
rename to .coveragerc
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..617b209
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,32 @@
+language: python
+
+dist: xenial
+
+matrix:
+ include:
+ - python: "3.4"
+ env: TOXENV=py34
+ - python: "3.5"
+ env: TOXENV=py35
+ - python: "3.6"
+ env: TOXENV=py36
+ - python: "3.7"
+ env: TOXENV=py37
+ - python: "pypy3.5-6.0"
+ env: TOXENV=pypy3
+
+install:
+ - pip install codecov
+ - pip install -r dev-requirements.txt
+ - pip install -r requirements.txt
+
+# run tests
+script:
+ - tox
+
+after_success:
+ - tox -e coverage-report
+ - codecov
+
+notifications:
+ email: false
diff --git a/README.md b/README.md
index 70e8375..9cef82c 100644
--- a/README.md
+++ b/README.md
@@ -10,6 +10,8 @@ Apprise API was designed to easily fit into existing (and new) eco-systems that
[![Paypal](https://img.shields.io/badge/paypal-donate-green.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=MHANV39UZNQ5E)
[![Discord](https://img.shields.io/discord/558793703356104724.svg?colorB=7289DA&label=Discord&logo=Discord&logoColor=7289DA&style=flat-square)](https://discord.gg/MMPeN2D)
+[![Build Status](https://travis-ci.org/caronc/apprise-api.svg?branch=master)](https://travis-ci.org/caronc/apprise-api)
+[![CodeCov Status](https://codecov.io/github/caronc/apprise-pai/branch/master/graph/badge.svg)](https://codecov.io/github/caronc/apprise-api)
[![Docker Pulls](https://img.shields.io/docker/pulls/caronc/apprise.svg?style=flat-square)](https://hub.docker.com/r/caronc/apprise)
## Screenshots
@@ -58,8 +60,8 @@ docker-compose up
- `{KEY}` must be 1-64 alphanumeric characters in length. In addition to this, the underscore (`_`) and dash (`-`) are also accepted.
- You must `POST` to URLs defined above in order for them to respond.
- Specify the `Content-Type` of `application/json` to use the JSON support.
-- There is no authentication required to use this API; this is by design. It's intention is to be a lightweight and fast micro-service parked behind the systems designed to handle security.
-- There are no persistent store dependencies for the purpose of simplicity. Configuration is hashed and written straight to disk.
+- There is no authentication (or SSL encryption) required to use this API; this is by design. The intentio here to be a lightweight and fast micro-service that can be parked behind another tier that was designed to handle security.
+- There are no additional dependencies should you choose to use the optional persistent store (mounted as `/config`).
### Environment Variables
@@ -67,8 +69,8 @@ The use of environment variables allow you to provide over-rides to default sett
| Variable | Description |
|--------------------- | ----------- |
-| `APPRISE_CONFIG_DIR` | Defines the persistent store location of all configuration files saved. By default:
- Content is written to the `apprise_api/var/config` directory when just using the _Django_ `manage runserver` script.
-| `APPRISE_STATELESS_URLS` | A default set of URLs to notify when using the stateless `/notify` reference (no reference to `{KEY}` variables).
+| `APPRISE_CONFIG_DIR` | Defines the persistent store location of all configuration files saved. By default:
- Configuration is written to the `apprise_api/var/config` directory when just using the _Django_ `manage runserver` script.
+| `APPRISE_STATELESS_URLS` | A default set of URLs to notify when using the stateless `/notify` reference (no reference to `{KEY}` variables). Use this option if you don't intend to use the persistent store at all.
| `SECRET_KEY` | A Django variable acting as a *salt* for most things that require security. This API uses it for the hash sequences when writing the configuration files to disk.
| `ALLOWED_HOSTS` | A list of strings representing the host/domain names that this API can serve. This is a security measure to prevent HTTP Host header attacks, which are possible even under many seemingly-safe web server configurations. By default this is set to `*` allowing any host. Use space to delimit more then one host.
| `DEBUG` | This defaults to `False` however can be set to `True`if defined with a non-zero value (such as `1`).
diff --git a/apprise_api/api/forms.py b/apprise_api/api/forms.py
index 25178fe..4fbb359 100644
--- a/apprise_api/api/forms.py
+++ b/apprise_api/api/forms.py
@@ -25,7 +25,7 @@
import apprise
from django import forms
-from django.utils.translation import ugettext_lazy as _
+from django.utils.translation import gettext_lazy as _
# Define our potential configuration types
CONFIG_FORMATS = (
diff --git a/apprise_api/api/views.py b/apprise_api/api/views.py
index 45781b8..b9f4249 100644
--- a/apprise_api/api/views.py
+++ b/apprise_api/api/views.py
@@ -29,7 +29,7 @@ from django.conf import settings
from django.utils.decorators import method_decorator
from django.views.decorators.cache import never_cache
from django.views.decorators.gzip import gzip_page
-from django.utils.translation import ugettext_lazy as _
+from django.utils.translation import gettext_lazy as _
from .utils import ConfigCache
from .forms import AddByUrlForm
from .forms import AddByConfigForm
@@ -127,7 +127,7 @@ class AddView(View):
# Prepare our default response
try:
# load our JSON content
- content = json.loads(request.body)
+ content = json.loads(request.body.decode('utf-8'))
except (AttributeError, ValueError):
# could not parse JSON response...
@@ -338,7 +338,7 @@ class NotifyView(View):
# Prepare our default response
try:
# load our JSON content
- content = json.loads(request.body)
+ content = json.loads(request.body.decode('utf-8'))
except (AttributeError, ValueError):
# could not parse JSON response...
@@ -456,7 +456,7 @@ class StatelessNotifyView(View):
# Prepare our default response
try:
# load our JSON content
- content = json.loads(request.body)
+ content = json.loads(request.body.decode('utf-8'))
except (AttributeError, ValueError):
# could not parse JSON response...
diff --git a/dev-requirements.txt b/dev-requirements.txt
index fb6efbf..734ae31 100644
--- a/dev-requirements.txt
+++ b/dev-requirements.txt
@@ -1,4 +1,6 @@
-pytest
flake8
+mock
pytest-django
+pytest
pytest-cov
+tox
diff --git a/tox.ini b/tox.ini
new file mode 100644
index 0000000..6ba2371
--- /dev/null
+++ b/tox.ini
@@ -0,0 +1,62 @@
+[tox]
+envlist = py34,py35,py36,py37,pypy3,coverage-report
+skipsdist = true
+
+[testenv]
+# Prevent random setuptools/pip breakages like
+# https://github.com/pypa/setuptools/issues/1042 from breaking our builds.
+setenv =
+ VIRTUALENV_NO_DOWNLOAD=1
+deps=
+ -r{toxinidir}/requirements.txt
+ -r{toxinidir}/dev-requirements.txt
+commands =
+ coverage run --parallel -m pytest {posargs} apprise_api
+ flake8 apprise_api --count --show-source --statistics
+
+[testenv:py34]
+deps=
+ -r{toxinidir}/requirements.txt
+ -r{toxinidir}/dev-requirements.txt
+commands =
+ coverage run --parallel -m pytest {posargs} apprise_api
+ flake8 apprise_api --count --show-source --statistics
+
+[testenv:py35]
+deps=
+ -r{toxinidir}/requirements.txt
+ -r{toxinidir}/dev-requirements.txt
+commands =
+ coverage run --parallel -m pytest {posargs} apprise_api
+ flake8 apprise_api --count --show-source --statistics
+
+[testenv:py36]
+deps=
+ -r{toxinidir}/requirements.txt
+ -r{toxinidir}/dev-requirements.txt
+commands =
+ coverage run --parallel -m pytest {posargs} apprise_api
+ flake8 apprise_api --count --show-source --statistics
+
+[testenv:py37]
+deps=
+ -r{toxinidir}/requirements.txt
+ -r{toxinidir}/dev-requirements.txt
+commands =
+ coverage run --parallel -m pytest {posargs} apprise_api
+ flake8 apprise_api --count --show-source --statistics
+
+[testenv:pypy3]
+deps=
+ -r{toxinidir}/requirements.txt
+ -r{toxinidir}/dev-requirements.txt
+commands =
+ coverage run --parallel -m pytest {posargs} apprise_api
+ flake8 apprise_api --count --show-source --statistics
+
+[testenv:coverage-report]
+deps = coverage
+skip_install = true
+commands=
+ coverage combine apprise_api
+ coverage report apprise_api