Initial commit

This commit is contained in:
Chris Caron 2019-10-26 23:53:40 -04:00
commit 4a8921abb8
64 changed files with 27784 additions and 0 deletions

2
.github/FUNDING.yml vendored Normal file
View File

@ -0,0 +1,2 @@
github: caronc
custom: ['https://www.paypal.me/lead2gold', 'https://beerpay.io/caronc/apprise']

22
.github/ISSUE_TEMPLATE/1_bug_report.md vendored Normal file
View File

@ -0,0 +1,22 @@
---
name: 🐛 Bug Report
about: Report any errors and problems
title: ''
labels: 'bug'
assignees: ''
---
:beetle: **Describe the bug**
<!-- A clear and concise description of the problem. -->
:bulb: **Screenshots and Logs**
<!-- If applicable, add screenshots or share logs help explain your problem. -->
<!-- be careful not to reveal anything personal in the logs such as a password or api key. -->
:computer: **Your System Details:**
- OS: [e.g. RedHat v8.0]
- Python Version: [e.g. Python v2.7]
:crystal_ball: **Additional context**
<!-- Is the issue easy to reproduce? if so how? -->

View File

@ -0,0 +1,15 @@
---
name: 💡 Enhancement Request
about: Got a great idea? Let us know!
title: ''
labels: 'enhancement'
assignees: ''
---
:bulb: **The Idea**
<!-- Share your thoughts; try to be detailed if you can -->
:hammer: **Breaking Feature**
<!-- Would your idea disrupt or drastically change the flow
of Apprise or how it currently works? If so explain it here. -->

11
.github/ISSUE_TEMPLATE/3_question.md vendored Normal file
View File

@ -0,0 +1,11 @@
---
name: ❓ Support Question
about: Ask a question about Apprise
title: ''
labels: 'question'
assignees: ''
---
:question: **Question**
<!-- Go ahead and ask your question here :) -->

9
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View File

@ -0,0 +1,9 @@
## Description:
**Related issue (if applicable):** fixes #<!--apprise issue number goes here-->
## Checklist
<!-- The following must be completed or your PR can't be merged -->
* [ ] The code change is tested and works locally.
* [ ] There is no commented out code in this PR.
* [ ] No lint errors (use `flake8`)
* [ ] tests added

70
.gitignore vendored Normal file
View File

@ -0,0 +1,70 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# vi swap files
.*.sw?
# Distribution / packaging / virtualenv
.Python
env/
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib64/
lib/
include/
bin/
parts/
sdist/
pyvenv.cfg
*.egg-info/
.installed.cfg
*.egg
pip-selfcheck.json
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Allow RPM SPEC files despite pyInstaller ignore
!packaging/redhat/*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*,cover
.hypothesis/
# Translations
*.mo
# Django stuff:
*.log
# PyBuilder
target/
#Ipython Notebook
.ipynb_checkpoints
#PyCharm
.idea
# Apprise Gateway Variable/Test Configuration
apprise_gw/var/*

22
Dockerfile-gunicorn Normal file
View File

@ -0,0 +1,22 @@
# pull official base image
FROM python:3.8.0-alpine
# set work directory
WORKDIR /opt/apprise
# set environment variables
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
ENV APPRISE_CONFIG_DIR /var/apprise/config
# install dependencies
RUN pip install --upgrade pip
COPY ./requirements.txt etc/requirements.txt
RUN pip install -r etc/requirements.txt \
gunicorn
# copy project
COPY apprise_api/ webapp
EXPOSE 8000
CMD ["gunicorn", "-c", "/opt/apprise/webapp/gunicorn.conf.py", "core.wsgi"]

9
Dockerfile-nginx Normal file
View File

@ -0,0 +1,9 @@
# pull official base image
FROM nginx
# Copy our customized NginX configuration (for container usage)
COPY apprise_api/nginx.conf /etc/nginx/nginx.conf
COPY apprise_api/apprise_api.conf /etc/nginx/conf.d/default.conf
# Copy our static content in place
COPY apprise_api/static /usr/share/nginx/html/s/

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2019 Chris Caron <lead2gold@gmail.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

150
README.md Normal file
View File

@ -0,0 +1,150 @@
# Apprise API
Take advantage of [Apprise](https://github.com/caronc/apprise) through your network with a user-friendly API.
- Send notifications to more then 50+ services.
- An incredibly lightweight gateway to Apprise.
- A production ready micro-service at your disposal.
Apprise API was designed easily fit into existing (and new) eco-systems that are looking for a simple notification solution.
[![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)
## API Details
| Path | Description |
|------------- | ----------- |
| `/add/{KEY}` | Saves Apprise Configuration (or set of URLs) to the persistent store.<br/>*Parameters*<br/>:small_red_triangle: **urls**: Used to define one or more Apprise URL(s). Use a comma and/or space to separate one URL from the next.<br/>:small_red_triangle: **config**: Provide the contents of either a YAML or TEXT based Apprise configuration.<br/>:small_red_triangle: **format**: This field is only required if you've specified the _config_ parameter. Used to tell the server which of the supported (Apprise) configuration types you are passing. Valid options are _text_ and _yaml_.
| `/del/{KEY}` | Removes Apprise Configuration from the persistent store.
| `/get/{KEY}` | Returns the Apprise Configuration from the persistent store. This can be directly used with the *Apprise CLI* and/or the *AppriseConfig()* object ([see here for details](https://github.com/caronc/apprise/wiki/config)).
| `/notify/{KEY}` | Sends a notification based on the Apprise Configuration associated with the specified *{KEY}*.<br/>*Parameters*<br/>:small_red_triangle: **body**: Your message body. This is the *only* required field.<br/>:small_red_triangle: **title**: Optionally define a title to go along with the *body*.<br/>:small_red_triangle: **type**: Defines the message type you want to send as. The valid options are `info`, `success`, `warning`, and `error`. If no *type* is specified then `info` is the default value used.<br/>:small_red_triangle: **tag**: Optionally notify only those tagged accordingly.
### API Notes
- `{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.
### Environment Variables
The use of environment variables allow you to provide over-rides to default settings.
| Variable | Description |
|--------------------- | ----------- |
| `APPRISE_CONFIG_DIR` | Defines the persistent store location of all configuration files saved. By default:<br/> - Content is written to the `apprise_api/var/config` directory when just using the _Django_ `manage runserver` script.
| `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`).
## Container Support
A `docker-compose.yml` file is already set up to grant you an instant production ready simulated environment:
```bash
# Docker Compose
docker-compose up
```
You can now access the API at: `http://localhost:8000/` from your browser.
## Development Environment
The following should get you a working development environment to test with:
```bash
# Create a virtual environment in the same directory you
# cloned this repository to:
python -m venv .
# Activate it now:
. ./bin/activate
# install dependencies
pip install -r dev-requirements.txt -r requirements.txt
# Run a dev server (debug mode):
./manage.py runserver
```
You can now access the API at: `http://localhost:8000/` from your browser.
Some other useful development notes:
```bash
# Check for any lint errors
flake8 apprise_api
# Run unit tests
pytest apprise_api
```
## Micro-Service Integration
Perhaps you run your own service and the only goal you have is to add notification support to it. Here is an example:
```python
import requests
import os
# Get your URLs from your end users. They just need
# to be a comma/space separated list (if there is more than one).
# Perhaps they're located in an environment variable:
urls = os.environ.get('NOTIFICATION_URLS', 'windows://')
if urls:
# Think of a key that best describes your purpose and/or program.
# Alternatively; you can make the key based on the users so they
# can each store their configuration.
key = 'my-program-name'
# POST our data:
requests.post(
'http://localhost:8000/add/{}'.format(key),
data={'urls': urls},
)
```
Now when you want to trigger a notification (sent from the Apprise API server), just do the following:
```python
# The minimum notify requirements are to have just provided the 'body':
requests.post(
'http://localhost:8000/notify/{}'.format(key),
data={'body': 'test message'},
)
```
## Apprise Integration
### Apprise CLI Pull Example
A scenario where you want to poll the API for your configuration:
```bash
# A simple example of the Apprise CLI
# pulling down previously stored configuration
apprise -body="test message" --config=http://localhost:8000/get/{KEY}
```
### AppriseConfig() Pull Example
Using the Apprise Library through Python, you can easily pull your saved configuration off of the API to use for future notifications.
```python
import apprise
# Point our configuration to this API server:
config = apprise.AppriseConfig()
config.add('http://localhost:8000/get/{KEY}')
# Create our Apprise Instance
a = apprise.Apprise()
# Store our new configuration
a.add(config)
# Send a test message
a.notify('test message')
```

10
apprise_api/.coveragerc Normal file
View File

@ -0,0 +1,10 @@
[run]
omit =
*apps.py,
*migrations/*,
*settings*,
*tests/*,
*urls.py,
*wsgi/*,
gunicorn.conf.py,
manage.py

View File

29
apprise_api/api/apps.py Normal file
View File

@ -0,0 +1,29 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2019 Chris Caron <lead2gold@gmail.com>
# All rights reserved.
#
# This code is licensed under the MIT License.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files(the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions :
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
from django.apps import AppConfig
class ApiConfig(AppConfig):
name = 'api'

137
apprise_api/api/forms.py Normal file
View File

@ -0,0 +1,137 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2019 Chris Caron <lead2gold@gmail.com>
# All rights reserved.
#
# This code is licensed under the MIT License.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files(the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions :
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
import apprise
from django import forms
from django.utils.translation import ugettext_lazy as _
# Define our potential configuration types
CONFIG_FORMATS = (
(apprise.ConfigFormat.TEXT, _('TEXT')),
(apprise.ConfigFormat.YAML, _('YAML')),
)
NOTIFICATION_TYPES = (
(apprise.NotifyType.INFO, _('Info')),
(apprise.NotifyType.SUCCESS, _('Success')),
(apprise.NotifyType.WARNING, _('Warning')),
(apprise.NotifyType.FAILURE, _('Failure')),
)
class AddByUrlForm(forms.Form):
"""
Form field for adding entries simply by passing in a string
of one or more URLs that have been deliminted by either a
comma and/or a space.
This content can just be directly fed straight into Apprise
"""
urls = forms.CharField(
label=_('URLs'),
widget=forms.TextInput(
attrs={
'placeholder': 'mailto://user:pass@domain.com, '
'slack://tokena/tokenb/tokenc, ...'}),
max_length=1024,
)
class AddByConfigForm(forms.Form):
"""
This is the reading in of a configuration file which contains
potential asset information (if yaml file) and tag details.
"""
format = forms.ChoiceField(
label=_('Format'),
choices=CONFIG_FORMATS,
)
config = forms.CharField(
label=_('Configuration'),
widget=forms.Textarea(),
max_length=4096,
)
class NotifyForm(forms.Form):
"""
This is the reading in of a configuration file which contains
potential asset information (if yaml file) and tag details.
"""
type = forms.ChoiceField(
label=_('Type'),
choices=NOTIFICATION_TYPES,
initial=NOTIFICATION_TYPES[0][0],
required=False,
)
title = forms.CharField(
label=_('Title'),
widget=forms.TextInput(attrs={'placeholder': _('Optional Title')}),
max_length=apprise.NotifyBase.title_maxlen,
required=False,
)
body = forms.CharField(
label=_('Body'),
widget=forms.Textarea(),
max_length=apprise.NotifyBase.body_maxlen,
)
tag = forms.ChoiceField(
label=_('Tags'),
widget=forms.TextInput(
attrs={'placeholder': _('Optional_Tag1, Optional_Tag2, ...')}),
required=False,
)
def clean_type(self):
"""
We just ensure there is a type always set
"""
data = self.cleaned_data['type']
if not data:
# Always set a type
data = apprise.NotifyType.INFO
return data
class NotifyByUrlForm(AddByUrlForm, NotifyForm):
"""
Same as the NotifyForm but additionally processes a string of URLs to
notify directly.
"""
pass
class NotifyByConfigForm(AddByConfigForm, NotifyForm):
"""
Same as the NotifyForm but additionally process a configuration file as
well.
"""
pass

View File

View File

View File

@ -0,0 +1,61 @@
{% load static %}
{% load i18n %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<!--Let browser know website is optimized for mobile-->
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="{% static 'css/materialize.min.css' %}"/>
<link rel="stylesheet" href="{% static 'iconfont/material-icons.css' %}">
<link rel="stylesheet" href="{% static 'css/base.css' %}"/>
<link rel="shortcut icon" type="image/png" href="{% static 'favicon.ico' %}" />
<script src="{% static 'js/materialize.min.js' %}"></script>
<title>{% block title %}{% trans "Apprise API" %}{% endblock %}</title>
</head>
<body>
<div class="content">
<!-- Title -->
<div class="nav row teal lighten-5 z-depth-2">
<div class="col s12">
<a href="{% url 'welcome' %}">
<img class="left" src="{% static "logo.png" %}" alt="{% trans "Apprise Logo" %}" />
</a>
<h1>{% trans "Apprise API" %}</h1>
</div>
</div>
<!-- Page Layout here -->
<div class="row">
<div class="col s3">
<ul class="collection z-depth-1">
<a class="collection-item" href="{% url 'config' 'apprise' %}"><i class="tiny material-icons">settings</i> {% trans "Configuration Manager" %}</a>
</ul>
<ul class="collection z-depth-1">
<a class="collection-item" target="_blank" href="https://github.com/caronc/apprise/wiki#notification-services">📣 {% trans "Notification Services" %}</a>
<a class="collection-item" target="_blank" href="https://github.com/caronc/apprise/wiki/config"><i class="tiny material-icons">local_library</i> {% trans "Configuration Help" %}</a>
<a class="collection-item" target="_blank" href="https://github.com/caronc/apprise/wiki/Troubleshooting"><i class="tiny material-icons">build</i> {% trans "Troubleshooting" %}</a>
<a class="collection-item" target="_blank" href="https://github.com/caronc/apprise/wiki/CLI_Usage"><i class="tiny material-icons">lightbulb_outline</i> {% trans "Using the CLI" %}</a>
</ul>
<ul class="collection z-depth-1">
<a class="collection-item" target="_blank" href="https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=MHANV39UZNQ5E"><i class="tiny material-icons">favorite</i> {% trans "Support Apprise" %}</a>
<a class="collection-item" target="_blank" href="https://github.com/sponsors/caronc"><i class="tiny material-icons">favorite</i> {% trans "Sponsor Developer" %}</a>
</ul>
{% block menu %}{% endblock %}
</div>
<div class="col s9">
{% block body %}{% endblock %}
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
M.AutoInit();
});
</script>
</body>
</html>

View File

@ -0,0 +1,76 @@
{% extends 'base.html' %}
{% load i18n %}
{% block body %}
<h3>{% trans "Management for:" %} <em>{{ key }}</em></h3>
<div class="row">
<div class="col s12">
<ul class="tabs">
<li class="tab col s4"><a class="active" href="#overview">{% trans "Overview" %}</a></li>
<li class="tab col s4"><a href="#config">{%trans "Configuration" %}</a></li>
<li class="tab col s4"><a href="#notify">{%trans "Notifications" %}</a></li>
</ul>
</div>
<div id="overview" class="col s12">
<p>
{% blocktrans %}
Here is where you can store your configuration so that it can be accessed by Apprise. You can always refer to the <a href="https://github.com/caronc/apprise/wiki#notification-services">Apprise Wiki</a> if you're having troubles assembling your URL(s).
You have chosen to associate your configuration with the key <code>{{key}}</code>. If anything was previously associated with this key, it will be replaced if you continue.
{% endblocktrans %}
</p>
<p>
{% blocktrans %}
In the future you can return to this configuration screen at any time by placing the following into your browser:{% endblocktrans %}
<br/><code>{{request.scheme}}://{{request.META.HTTP_HOST}}{{request.path}}</code>
</p>
<div class="section">
{% blocktrans %}For example, the following command would cause apprise to retrieve the configuration loaded and send a test notification to all of your added services:{% endblocktrans %}
<br/><code>apprise --body="Test Message" --tag=all --config={{request.scheme}}://{{request.META.HTTP_HOST}}{% url "get" key %}</code>
</div>
</div>
<div id="config" class="col s12">
<div class="section">
<h5>{% trans "Option 1: Add By URL" %}</h5>
<p>
{% blocktrans %}
Use a comma and/or space to separate one Apprise URL from another.
{% endblocktrans %}
<form action="#" method="post">
{{ form_url }}
<button class="btn waves-effect waves-light" type="submit" name="action">{% trans "Submit" %}
<i class="material-icons right">send</i>
</button>
</form>
</p>
</div>
<div class="section">
<h5>{% trans "Option 2: Add By Config" %}</h5>
<p>
{% blocktrans %}
This option grants you a bit more flexability because you can additionally associate tags with your URLs. Those using YAML configuration can also alter the Apprise Asset object as well for a more customized look and feel.
{% endblocktrans %}
<form action="#" method="post">
{{ form_cfg }}
<button class="btn waves-effect waves-light" type="submit" name="action">{% trans "Submit" %}
<i class="material-icons right">send</i>
</button>
</form>
</p>
</div>
</div>
<div id="notify" class="col s12">
<p>
{% blocktrans %}
You can send a notification using the loaded configuration:
{% endblocktrans %}
<form action="#" method="post">
{{ form_notify }}
<button class="btn waves-effect waves-light" type="submit" name="action">{% trans "Submit" %}
<i class="material-icons right">send</i>
</button>
</form>
</p>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,198 @@
{% extends 'base.html' %}
{% load i18n %}
{% block body %}
<h4>{% trans "The Apprise API" %}</h4>
<p>
{% blocktrans %}
<a target="_blank" href="https://github.com/caronc/apprise">Apprise</a> allows you to send a notification to almost
all of the most popular notification services available to us today such as: <em>Telegram</em>, <em>Discord</em>,
<em>Slack</em>, <em>Amazon SNS</em>, <em>Gotify</em>, etc.
This API provides a simple gateway to directly access it via an HTTP interface.
<ul>
<li><i class="tiny material-icons">chevron_right</i>This project was designed to be incredibly light weight.</li>
<li><i class="tiny material-icons">chevron_right</i>Configuration can be persistently stored for retrieval.</li>
</ul>
{% endblocktrans %}
</p>
<div class="section">
<h4>{% trans "Endpoints" %}</h4>
<p>{% blocktrans %}All endpoints that expect posted data can be received in either JSON or in it's standard encoding.
You must pass along the <code>Content-Type</code> as <code>application/json</code> in order for it to be interpreted
properly.{% endblocktrans %}</p>
<table class="highlighted">
<thead>
<tr>
<th>{% trans "URL" %}</th>
<th>{% trans "Description" %}</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>/add/<em>{% trans "KEY" %}</em></code></td>
<td>
{% blocktrans %}Used to add a new Apprise configuration or a set of URLs and associates them with the
specified <em>KEY</em>. See the <a target="_blank"
href="https://github.com/caronc/apprise/wiki#notification-services">Apprise Wiki</a> if you need help
constructing your URLs.{% endblocktrans %}
<div class="section">
<table>
<thead>
<tr>
<th>{% trans "Parameter" %}</th>
<th>{% trans "Description" %}</th>
</tr>
</thead>
<tbody>
<tr>
<td>urls</td>
<td>{% blocktrans %}Used to define one or more Apprise URL(s). Use a comma and/or space to separate
one URL from the next.{% endblocktrans %}</td>
</tr>
<tr>
<td>config</td>
<td>{% blocktrans %}Provide the contents of either a YAML or TEXT based Apprise
configuration.{% endblocktrans %}</td>
</tr>
<tr>
<td>format</td>
<td>{% blocktrans %}This field is only required if you've specified the config option. It's purpose is
to tell the server which of the supported (Apprise) configuration types you are passing. Valid
options are:{% endblocktrans %}
<ol>
<li><code>{% trans "yaml" %}</code></li>
<li><code>{% trans "text" %}</code></li>
</ol>
<ul>
<li>{% blocktrans %}You must specify either the <code>urls</code> parameter or the
<code>config</code>.{% endblocktrans %}</li>
<li>{% blocktrans %}The <code>urls</code> takes priority over the <code>config</code> if both were
specified.{% endblocktrans %}</li>
<li>{% blocktrans %}The <code>format</code> parameter is only required if the <code>config</code>
parameter was also specified.{% endblocktrans %}</li>
</ul>
</td>
</tr>
</tbody>
</table>
<ul class="collapsible">
<li>
<div class="collapsible-header"><i class="material-icons">code</i>curl example</div>
<div class="collapsible-body">
<code>#
{% blocktrans %}Load a single URL and assign it to the <em>KEY</em> of abc123{% endblocktrans %}<br/>
curl -X POST -d '{"urls":"mailto://user:pass@gmail.com"}' \<br/>
&nbsp;&nbsp;&nbsp;&nbsp;-H "Content-Type: application/json" \<br/>
&nbsp;&nbsp;&nbsp;&nbsp;http://localhost:8000/add/<em>abc123</em></code>
<code><br/>
<br/>#{% blocktrans %}Load a simple TEXT config entry <em>KEY</em> of abc123{% endblocktrans %}<br/>
curl -X POST -d '{"format":"text","config":"devops=mailto://user:pass@gmail.com"}' \<br/>
&nbsp;&nbsp;&nbsp;&nbsp;-H "Content-Type: application/json" \<br/>
&nbsp;&nbsp;&nbsp;&nbsp;http://localhost:8000/add/abc123/
</code>
</div>
</li>
<li>
<div class="collapsible-header"><i class="material-icons">code</i>python example</div>
<div class="collapsible-body"><code>Coming Soon</code></div>
<li>
<li>
<div class="collapsible-header"><i class="material-icons">code</i>php example</div>
<div class="collapsible-body"><code>Coming Soon</code></div>
</li>
</ul>
</div>
</td>
</tr>
<tr>
<td><code>/del/<em>{% trans "KEY" %}</em></code></td>
<td>{% blocktrans %}There are no arguments required. If the <em>KEY</em> exists and has data associated with it,
it will be removed.{% endblocktrans %}</td>
</tr>
<tr>
<td><code>/get/<em>{% trans "KEY" %}</em></code></td>
<td>{% blocktrans %}This feature can be used by Apprise itself. It provides a means of remotely fetching it's
configuration.{% endblocktrans %}
<p><strong>{% trans "As an example:" %}</strong><br /><code>apprise --body="test message" --config={{ request.scheme }}://{{request.META.HTTP_HOST}}{{ request.path }}<em>{% trans "KEY" %}</em></p>
<ul class="collapsible">
<li>
<div class="collapsible-header"><i class="material-icons">code</i>curl example</div>
<div class="collapsible-body"><code>
# {% blocktrans %}Load a single URL and assign it to the <em>KEY</em> of abc123{% endblocktrans %}</br>
curl -X POST -H "Content-Type: application/json" \<br/>
&nbsp;&nbsp;&nbsp;&nbsp;http://localhost:8000/get/<em>abc123</em></code>
</div>
</li>
<li>
<div class="collapsible-header"><i class="material-icons">code</i>python example</div>
<div class="collapsible-body"><code>Coming Soon</code></div>
<li>
<li>
<div class="collapsible-header"><i class="material-icons">code</i>php example</div>
<div class="collapsible-body"><code>Coming Soon</code></div>
</li>
</ul>
</td>
</tr>
<tr>
<td><code>/notify/<em>{% trans "KEY" %}</em></code></td>
<td>{% blocktrans %}Notifies the URLs associated with the specified <em>KEY</em>.{% endblocktrans %}
<div class="section">
<table>
<thead>
<tr>
<th>{% trans "Parameter" %}</th>
<th>{% trans "Description" %}</th>
</tr>
</thead>
<tbody>
<tr>
<td>body</td>
<td>{% blocktrans %}Defines the message body. This field is required!{% endblocktrans %}</td>
</tr>
<tr>
<td>title</td>
<td>{% blocktrans %}The title to include in the notification. This is an optional field.{% endblocktrans %}
</td>
</tr>
<tr>
<td>type</td>
<td>{% blocktrans %}This optional field defines the notification type. The possible options
are:{% endblocktrans %}
<ol>
<li><code>{% trans "info" %}</code> - <i>{% blocktrans %}this is the default option if a type isn't
specified.{% endblocktrans %}</i></li>
<li><code>{% trans "success" %}</code></li>
<li><code>{% trans "warning" %}</code></li>
<li><code>{% trans "failure" %}</code></li>
</ol>
</td>
<tr>
<td>tags</td>
<td>{% blocktrans %}Apply tagging logic to the further filter your URLs. This is an optional
field.{% endblocktrans %}</td>
</tr>
</tbody>
</table>
</div>
</td>
</tr>
</tbody>
</table>
<div class="section">
<h5>{% trans "Endpoint Notes" %}</h5>
<p>
The <em>KEY</em> you plan to associate your configuration with:
<ol>
<li>Can not have spaces and/or special characters in it. Both a dash (<code>-</code>) and underscore
(<code>_</code>) are the only exceptions to this rule.</li>
<li>Must start with at least 2 alpha/numeric characters.</li>
<li>Can not exceed 64 characters in total length.</li>
</ol>
</p>
</div>
</div>
{% endblock %}

View File

View File

@ -0,0 +1,185 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2019 Chris Caron <lead2gold@gmail.com>
# All rights reserved.
#
# This code is licensed under the MIT License.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files(the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions :
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
from django.test import SimpleTestCase
from apprise import ConfigFormat
from unittest.mock import patch
import json
class AddTests(SimpleTestCase):
def test_add_invalid_key_status_code(self):
"""
Test GET requests to invalid key
"""
response = self.client.get('/add/**invalid-key**')
assert response.status_code == 404
def test_save_config_by_urls(self):
"""
Test adding an configuration by URLs
"""
# our key to use
key = 'test_save_config_by_urls'
# GET returns 405 (not allowed)
response = self.client.get('/add/{}'.format(key))
assert response.status_code == 405
# no data
response = self.client.post('/add/{}'.format(key))
assert response.status_code == 400
# No entries specified
response = self.client.post(
'/add/{}'.format(key), {'urls': ''})
assert response.status_code == 400
# Added successfully
response = self.client.post(
'/add/{}'.format(key), {'urls': 'mailto://user:pass@yahoo.ca'})
assert response.status_code == 200
# URL is actually not a valid one (invalid Slack tokens specified
# below)
response = self.client.post(
'/add/{}'.format(key), {'urls': 'slack://TokenA/TokenB/TokenC'})
assert response.status_code == 400
# Test with JSON
response = self.client.post(
'/add/{}'.format(key),
data=json.dumps({'urls': 'mailto://user:pass@yahoo.ca'}),
content_type='application/json',
)
assert response.status_code == 200
# Invalid JSON
response = self.client.post(
'/add/{}'.format(key),
data='{',
content_type='application/json',
)
assert response.status_code == 400
# Test the handling of underlining disk/write exceptions
with patch('gzip.open') as mock_open:
mock_open.side_effect = OSError()
# We'll fail to write our key now
response = self.client.post(
'/add/{}'.format(key),
data=json.dumps({'urls': 'mailto://user:pass@yahoo.ca'}),
content_type='application/json',
)
# internal errors are correctly identified
assert response.status_code == 500
def test_save_config_by_config(self):
"""
Test adding an configuration by a config file
"""
# our key to use
key = 'test_save_config_by_config'
# Empty Text Configuration
config = """
""" # noqa W293
response = self.client.post(
'/add/{}'.format(key), {
'format': ConfigFormat.TEXT, 'config': config})
assert response.status_code == 400
# Valid Text Configuration
config = """
browser,media=notica://VToken
home=mailto://user:pass@hotmail.com
"""
response = self.client.post(
'/add/{}'.format(key),
{'format': ConfigFormat.TEXT, 'config': config})
assert response.status_code == 200
# Test with JSON
response = self.client.post(
'/add/{}'.format(key),
data=json.dumps({'format': ConfigFormat.TEXT, 'config': config}),
content_type='application/json',
)
assert response.status_code == 200
# Test invalid config format
response = self.client.post(
'/add/{}'.format(key),
data=json.dumps({'format': 'INVALID', 'config': config}),
content_type='application/json',
)
assert response.status_code == 400
with patch('tempfile._TemporaryFileWrapper') as mock_ntf:
mock_ntf.side_effect = OSError()
# we won't be able to write our retrieved configuration
# to disk for processing; we'll get a 500 error
response = self.client.post(
'/add/{}'.format(key),
data=json.dumps(
{'format': ConfigFormat.TEXT, 'config': config}),
content_type='application/json',
)
# internal errors are correctly identified
assert response.status_code == 500
# Test the handling of underlining disk/write exceptions
with patch('gzip.open') as mock_open:
mock_open.side_effect = OSError()
# We'll fail to write our key now
response = self.client.post(
'/add/{}'.format(key),
data=json.dumps(
{'format': ConfigFormat.TEXT, 'config': config}),
content_type='application/json',
)
# internal errors are correctly identified
assert response.status_code == 500
def test_save_with_bad_input(self):
"""
Test adding with bad input in general
"""
# our key to use
key = 'test_save_with_bad_input'
# Test with JSON
response = self.client.post(
'/add/{}'.format(key),
data=json.dumps({'garbage': 'input'}),
content_type='application/json',
)
assert response.status_code == 400

View File

@ -0,0 +1,105 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2019 Chris Caron <lead2gold@gmail.com>
# All rights reserved.
#
# This code is licensed under the MIT License.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files(the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions :
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
import os
from ..utils import AppriseConfigCache
from apprise import ConfigFormat
from unittest.mock import patch
import errno
def test_apprise_config_io(tmpdir):
"""
Test Apprise Config Disk Put/Get
"""
content = 'mailto://test:pass@gmail.com'
key = 'test_apprise_config_io'
# Create our object to work with
acc_obj = AppriseConfigCache(str(tmpdir))
# Verify that the content doesn't already exist
assert acc_obj.get(key) == (None, '')
# Write our content assigned to our key
assert acc_obj.put(key, content, ConfigFormat.TEXT)
# Test the handling of underlining disk/write exceptions
with patch('gzip.open') as mock_open:
mock_open.side_effect = OSError()
# We'll fail to write our key now
assert not acc_obj.put(key, content, ConfigFormat.TEXT)
# Get path details
conf_dir, _ = acc_obj.path(key)
# List content of directory
contents = os.listdir(conf_dir)
# There should be just 1 new file in this directory
assert len(contents) == 1
assert contents[0].endswith('.{}'.format(ConfigFormat.TEXT))
# Verify that the content is retrievable
assert acc_obj.get(key) == (content, ConfigFormat.TEXT)
# Test the handling of underlining disk/read exceptions
with patch('gzip.open') as mock_open:
mock_open.side_effect = OSError()
# We'll fail to read our key now
assert acc_obj.get(key) == (None, None)
# Tidy up our content
assert acc_obj.clear(key) is True
# But the second time is okay as it no longer exists
assert acc_obj.clear(key) is None
with patch('os.remove') as mock_remove:
mock_remove.side_effect = OSError(errno.EPERM)
# OSError
assert acc_obj.clear(key) is False
# Now test with YAML file
content = """
version: 1
urls:
- windows://
"""
# Write our content assigned to our key
# This should gracefully clear the TEXT entry that was
# previously in the spot
assert acc_obj.put(key, content, ConfigFormat.YAML)
# List content of directory
contents = os.listdir(conf_dir)
# There should STILL be just 1 new file in this directory
assert len(contents) == 1
assert contents[0].endswith('.{}'.format(ConfigFormat.YAML))
# Verify that the content is retrievable
assert acc_obj.get(key) == (content, ConfigFormat.YAML)

View File

@ -0,0 +1,61 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2019 Chris Caron <lead2gold@gmail.com>
# All rights reserved.
#
# This code is licensed under the MIT License.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files(the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions :
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
from django.test import SimpleTestCase
class GetTests(SimpleTestCase):
def test_get_invalid_key_status_code(self):
"""
Test GET requests to invalid key
"""
response = self.client.get('/get/**invalid-key**')
assert response.status_code == 404
def test_get_config(self):
"""
Test retrieving configuration
"""
# our key to use
key = 'test_get_config'
# GET returns 405 (not allowed)
response = self.client.get('/get/{}'.format(key))
assert response.status_code == 405
# No content saved to the location yet
response = self.client.post('/get/{}'.format(key))
assert response.status_code == 204
# Add some content
response = self.client.post(
'/add/{}'.format(key),
{'urls': 'mailto://user:pass@yahoo.ca'})
assert response.status_code == 200
# Now we should be able to see our content
response = self.client.post('/get/{}'.format(key))
assert response.status_code == 200

View File

@ -0,0 +1,47 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2019 Chris Caron <lead2gold@gmail.com>
# All rights reserved.
#
# This code is licensed under the MIT License.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files(the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions :
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
from django.test import SimpleTestCase
class ManagerPageTests(SimpleTestCase):
"""
Manager Webpage testing
"""
def test_manage_status_code(self):
"""
General testing of management page
"""
# No key was specified
response = self.client.get('/cfg/')
assert response.status_code == 404
# An invalid key was specified
response = self.client.get('/cfg/**invalid-key**')
assert response.status_code == 404
# An invalid key was specified
response = self.client.get('/cfg/valid-key')
assert response.status_code == 200

View File

@ -0,0 +1,197 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2019 Chris Caron <lead2gold@gmail.com>
# All rights reserved.
#
# This code is licensed under the MIT License.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files(the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions :
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
from django.test import SimpleTestCase
from unittest.mock import patch
from ..forms import NotifyForm
import json
import apprise
class NotifyTests(SimpleTestCase):
"""
Test notifications
"""
@patch('apprise.Apprise.notify')
def test_notify_by_loaded_urls(self, mock_notify):
"""
Test adding a simple notification and notifying it
"""
# Set our return value
mock_notify.return_value = True
# our key to use
key = 'test_notify_by_loaded_urls'
# Add some content
response = self.client.post(
'/add/{}'.format(key),
{'urls': 'mailto://user:pass@yahoo.ca'})
assert response.status_code == 200
# Preare our form data
form_data = {
'body': 'test notifiction',
}
# At a minimum, just a body is required
form = NotifyForm(data=form_data)
assert form.is_valid()
# we always set a type if one wasn't done so already
assert form.cleaned_data['type'] == apprise.NotifyType.INFO
# Send our notification
response = self.client.post(
'/notify/{}'.format(key), form.cleaned_data)
assert response.status_code == 200
assert mock_notify.call_count == 1
@patch('apprise.Apprise.notify')
def test_notify_by_loaded_urls_with_json(self, mock_notify):
"""
Test adding a simple notification and notifying it using JSON
"""
# Set our return value
mock_notify.return_value = True
# our key to use
key = 'test_notify_by_loaded_urls_with_json'
# Add some content
response = self.client.post(
'/add/{}'.format(key),
{'urls': 'mailto://user:pass@yahoo.ca'})
assert response.status_code == 200
# Preare our JSON data
json_data = {
'body': 'test notifiction',
'type': apprise.NotifyType.WARNING,
}
# Send our notification as a JSON object
response = self.client.post(
'/notify/{}'.format(key),
data=json.dumps(json_data),
content_type='application/json',
)
# Still supported
assert response.status_code == 200
assert mock_notify.call_count == 1
# Reset our count
mock_notify.reset_mock()
# Test referencing a key that doesn't exist
response = self.client.post(
'/notify/non-existant-key',
data=json.dumps(json_data),
content_type='application/json',
)
# Nothing notified
assert response.status_code == 204
assert mock_notify.call_count == 0
# Test sending a garbage JSON object
response = self.client.post(
'/notify/{}'.format(key),
data="{",
content_type='application/json',
)
assert response.status_code == 400
assert mock_notify.call_count == 0
# Test sending with an invalid content type
response = self.client.post(
'/notify/{}'.format(key),
data="{}",
content_type='application/xml',
)
assert response.status_code == 400
assert mock_notify.call_count == 0
# Test sending without any content at all
response = self.client.post(
'/notify/{}'.format(key),
data="{}",
content_type='application/json',
)
assert response.status_code == 400
assert mock_notify.call_count == 0
# Test sending without a body
json_data = {
'type': apprise.NotifyType.WARNING,
}
response = self.client.post(
'/notify/{}'.format(key),
data=json.dumps(json_data),
content_type='application/json',
)
assert response.status_code == 400
assert mock_notify.call_count == 0
# Test inability to prepare writing config to disk
json_data = {
'body': 'test message'
}
with patch('tempfile._TemporaryFileWrapper') as mock_ntf:
mock_ntf.side_effect = OSError()
# we won't be able to write our retrieved configuration
# to disk for processing; we'll get a 500 error
response = self.client.post(
'/notify/{}'.format(key),
data=json.dumps(json_data),
content_type='application/json',
)
# internal errors are correctly identified
assert response.status_code == 500
assert mock_notify.call_count == 0
# Test the handling of underlining disk/write exceptions
with patch('gzip.open') as mock_open:
mock_open.side_effect = OSError()
# We'll fail to write our key now
response = self.client.post(
'/notify/{}'.format(key),
data=json.dumps(json_data),
content_type='application/json',
)
# internal errors are correctly identified
assert response.status_code == 500
assert mock_notify.call_count == 0

View File

@ -0,0 +1,32 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2019 Chris Caron <lead2gold@gmail.com>
# All rights reserved.
#
# This code is licensed under the MIT License.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files(the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions :
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
from django.test import SimpleTestCase
class WelcomePageTests(SimpleTestCase):
def test_welcome_page_status_code(self):
response = self.client.get('/')
assert response.status_code == 200

47
apprise_api/api/urls.py Normal file
View File

@ -0,0 +1,47 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2019 Chris Caron <lead2gold@gmail.com>
# All rights reserved.
#
# This code is licensed under the MIT License.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files(the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions :
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
from django.urls import re_path
from . import views
urlpatterns = [
re_path(
r'^$',
views.WelcomeView.as_view(), name='welcome'),
re_path(
r'^cfg/(?P<key>[\w_-]{1,64})/?',
views.ConfigView.as_view(), name='config'),
re_path(
r'^add/(?P<key>[\w_-]{1,64})/?',
views.AddView.as_view(), name='add'),
re_path(
r'^del/(?P<key>[\w_-]{1,64})/?',
views.DelView.as_view(), name='del'),
re_path(
r'^get/(?P<key>[\w_-]{1,64})/?',
views.GetView.as_view(), name='get'),
re_path(
r'^notify/(?P<key>[\w_-]{1,64})/?',
views.NotifyView.as_view(), name='notify'),
]

196
apprise_api/api/utils.py Normal file
View File

@ -0,0 +1,196 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2019 Chris Caron <lead2gold@gmail.com>
# All rights reserved.
#
# This code is licensed under the MIT License.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files(the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions :
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
import os
import tempfile
import shutil
import gzip
import apprise
import hashlib
import errno
from django.conf import settings
class AppriseConfigCache(object):
"""
Designed to make it easy to store/read contact back from disk in a cache
type structure that is fast.
"""
def __init__(self, cache_root, salt="apprise"):
"""
Works relative to the cache_root
"""
self.root = cache_root
self.salt = salt.encode()
def put(self, key, content, fmt):
"""
Based on the key specified, content is written to disk (compressed)
key: is an alphanumeric string needed to write and read back this
file being written.
content: the content to be written to disk
fmt: the content config format (of type apprise.ConfigFormat)
"""
# There isn't a lot of error handling done here as it is presumed most
# of the checking has been done higher up.
# First two characters are reserved for cache level directory writing.
path, filename = self.path(key)
os.makedirs(path, exist_ok=True)
# Write our file to a temporary file
_, tmp_path = tempfile.mkstemp(suffix='.tmp', dir=path)
try:
with gzip.open(tmp_path, 'wb') as f:
# Write our content to disk
f.write(content.encode())
except OSError:
# Handle failure
os.remove(tmp_path)
return False
# If we reach here we successfully wrote the content. We now safely
# move our configuration into place. The following writes our content
# to disk as /xx/key.fmt
shutil.move(tmp_path, os.path.join(
path, '{}.{}'.format(filename, fmt)))
# perform tidy of any other lingering files of other type in case
# configuration changed from TEXT -> YAML or YAML -> TEXT
if self.clear(key, set(apprise.CONFIG_FORMATS) - {fmt}) is False:
# We couldn't remove an existing entry; clear what we just created
self.clear(key, {fmt})
# fail
return False
return True
def get(self, key):
"""
Based on the key specified, content is written to disk (compressed)
key: is an alphanumeric string needed to write and read back this
file being written.
The function returns a tuple of (content, fmt) where the content
is the uncompressed content found in the file and fmt is the
content representation (of type apprise.ConfigFormat).
If no data was found, then (None, None) is returned.
"""
# There isn't a lot of error handling done here as it is presumed most
# of the checking has been done higher up.
# First two characters are reserved for cache level directory writing.
path, filename = self.path(key)
# prepare our format to return
fmt = None
# Test the only possible hashed files we expect to find
text_file = os.path.join(
path, '{}.{}'.format(filename, apprise.ConfigFormat.TEXT))
yaml_file = os.path.join(
path, '{}.{}'.format(filename, apprise.ConfigFormat.YAML))
if os.path.isfile(text_file):
fmt = apprise.ConfigFormat.TEXT
path = text_file
elif os.path.isfile(yaml_file):
fmt = apprise.ConfigFormat.YAML
path = yaml_file
else:
# Not found; we set the fmt to something other than none as
# an indication for the upstream handling to know that we didn't
# fail on error
return (None, '')
# Initialize our content
content = None
try:
with gzip.open(path, 'rb') as f:
# Write our content to disk
content = f.read().decode()
except OSError:
# all none return means to let upstream know we had a hard failure
return (None, None)
# return our read content
return (content, fmt)
def clear(self, key, formats=None):
"""
Removes any content associated with the specified key should it
exist.
None is returned if there was nothing to clear
True is returned if content was cleared
False is returned if an internal error prevented data from being
cleared
"""
# Default our response None
response = None
if formats is None:
formats = apprise.CONFIG_FORMATS
path, filename = self.path(key)
for fmt in formats:
# Eliminate any existing content if present
try:
# Handle failure
os.remove(os.path.join(path, '{}.{}'.format(filename, fmt)))
# If we reach here, an element was removed
response = True
except OSError as e:
if e.errno != errno.ENOENT:
# We were unable to remove the file
response = False
return response
def path(self, key):
"""
returns the path and filename content should be written to based on the
specified key
"""
encoded_key = hashlib.sha224(self.salt + key.encode()).hexdigest()
path = os.path.join(self.root, encoded_key[0:2])
return (path, encoded_key[2:])
# Initialize our singleton
ConfigCache = AppriseConfigCache(
settings.APPRISE_CONFIG_DIR, salt=settings.SECRET_KEY)

419
apprise_api/api/views.py Normal file
View File

@ -0,0 +1,419 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2019 Chris Caron <lead2gold@gmail.com>
# All rights reserved.
#
# This code is licensed under the MIT License.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files(the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions :
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
from django.shortcuts import render
from django.http import HttpResponse
from django.views import View
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 .utils import ConfigCache
from .forms import AddByUrlForm
from .forms import AddByConfigForm
from .forms import NotifyForm
from tempfile import NamedTemporaryFile
import apprise
import json
import re
# Content-Type Parsing
FORM_CTYPE_RE = re.compile('^(.*form-(data|urlencoded))$', re.I)
JSON_CTYPE_RE = re.compile('^.*json$', re.I)
class ResponseCode(object):
"""
These codes are based on those provided by the requests object
"""
okay = 200
no_content = 204
bad_request = 400
not_found = 404
method_not_allowed = 405
internal_server_error = 500
class WelcomeView(View):
"""
A simple welcome/index page
"""
template_name = 'welcome.html'
def get(self, request):
return render(request, self.template_name, {})
@method_decorator(never_cache, name='dispatch')
class ConfigView(View):
"""
A Django view used to manage configuration
"""
template_name = 'config.html'
def get(self, request, key):
"""
Handle a GET request
"""
return render(request, self.template_name, {
'key': key,
'form_url': AddByUrlForm(),
'form_cfg': AddByConfigForm(),
'form_notify': NotifyForm(),
})
@method_decorator(never_cache, name='dispatch')
class AddView(View):
"""
A Django view used to store Apprise configuration
"""
def post(self, request, key):
"""
Handle a POST request
"""
# Our default response type
content_type = 'text/plain'
# our content
content = {}
if FORM_CTYPE_RE.match(request.content_type):
content = {}
form = AddByConfigForm(request.POST)
if form.is_valid():
content.update(form.clean())
form = AddByUrlForm(request.POST)
if form.is_valid():
content.update(form.clean())
elif JSON_CTYPE_RE.match(request.content_type):
# Prepare our default response
try:
# load our JSON content
content = json.loads(request.body)
except (AttributeError, ValueError):
# could not parse JSON response...
return HttpResponse(
_('Invalid JSON specified.'),
content_type=content_type,
status=ResponseCode.bad_request)
if not content:
return HttpResponse(
_('The message format is not supported.'),
content_type=content_type,
status=ResponseCode.bad_request)
# Create ourselves an apprise object to work with
a_obj = apprise.Apprise()
if 'urls' in content:
# Load our content
a_obj.add(content['urls'])
if not len(a_obj):
# No URLs were loaded
return HttpResponse(
_('No valid URLs were found.'),
content_type=content_type,
status=ResponseCode.bad_request)
if not ConfigCache.put(
key, '\r\n'.join([s.url() for s in a_obj]),
apprise.ConfigFormat.TEXT):
return HttpResponse(
_('The configuration could not be saved.'),
content_type=content_type,
status=ResponseCode.internal_server_error,
)
elif 'config' in content:
fmt = content.get('format', '').lower()
if fmt not in apprise.CONFIG_FORMATS:
# Format must be one supported by apprise
return HttpResponse(
_('The format specified is invalid.'),
content_type=content_type,
status=ResponseCode.bad_request)
# prepare our apprise config object
ac_obj = apprise.AppriseConfig()
try:
# Write our file to a temporary file
with NamedTemporaryFile() as f:
# Write our content to disk
f.write(content['config'].encode())
f.flush()
if not ac_obj.add(
'file://{}?format={}'.format(f.name, fmt)):
# Bad Configuration
return HttpResponse(
_('The configuration specified is invalid.'),
content_type=content_type,
status=ResponseCode.bad_request)
# Add our configuration
a_obj.add(ac_obj)
if not len(a_obj):
# No specified URL(s) were loaded due to
# mis-configuration on the caller's part
return HttpResponse(
_('No valid URL(s) were specified.'),
content_type=content_type,
status=ResponseCode.bad_request)
except OSError:
# We could not write the temporary file to disk
return HttpResponse(
_('The configuration could not be loaded.'),
content_type=content_type,
status=ResponseCode.internal_server_error)
if not ConfigCache.put(key, content['config'], fmt=fmt):
# Something went very wrong; return 500
return HttpResponse(
_('An error occured saving configuration.'),
content_type=content_type,
status=ResponseCode.internal_server_error,
)
else:
# No configuration specified; we're done
return HttpResponse(
_('No configuration specified.'),
content_type=content_type, status=ResponseCode.bad_request)
# If we reach here; we successfully loaded the configuration so we can
# go ahead and write it to disk and alert our caller of the success.
return HttpResponse(
_('Successfully saved configuration.'),
content_type=content_type, status=ResponseCode.okay)
@method_decorator(never_cache, name='dispatch')
class DelView(View):
"""
A Django view for removing content associated with a key
"""
def post(self, request, key):
"""
Handle a POST request
"""
# Our default response type
content_type = 'text/plain'
# Clear the key
result = ConfigCache.clear(key)
if result is None:
return HttpResponse(
_('There was no configuration to remove.'),
content_type=content_type,
status=ResponseCode.no_content,
)
elif result is False:
# There was a failure at the os level
return HttpResponse(
_('The configuration could not be removed.'),
content_type=content_type,
status=ResponseCode.internal_server_error,
)
# Removed content
return HttpResponse(
_('Successfully removed configuration.'),
content_type=content_type,
status=ResponseCode.okay,
)
@method_decorator((gzip_page, never_cache), name='dispatch')
class GetView(View):
"""
A Django view used to retrieve previously stored Apprise configuration
"""
def post(self, request, key):
"""
Handle a POST request
"""
# Our default response type
content_type = 'text/plain'
config, format = ConfigCache.get(key)
if config is None:
# The returned value of config and format tell a rather cryptic
# story; this portion could probably be updated in the future.
# but for now it reads like this:
# config == None and format == None: We had an internal error
# config == None and format != None: we simply have no data
# config != None: we simply have no data
if format is not None:
# no content to return
return HttpResponse(
_('There was no configuration found.'),
content_type=content_type,
status=ResponseCode.no_content,
)
# Something went very wrong; return 500
return HttpResponse(
_('An error occured accessing configuration.'),
content_type=content_type,
status=ResponseCode.internal_server_error,
)
# Our configuration was retrieved; now our response varies on whether
# we are a YAML configuration or a TEXT based one. This allows us to
# be compatible with those using the AppriseConfig() library or the
# reference to it through the --config (-c) option in the CLI
if format == apprise.ConfigFormat.YAML:
# update our return content type from the default text
content_type = 'text/yaml'
# Return our retrieved content
return HttpResponse(
config, content_type=content_type, status=ResponseCode.okay)
@method_decorator((gzip_page, never_cache), name='dispatch')
class NotifyView(View):
"""
A Django view for sending a notification
"""
def post(self, request, key):
"""
Handle a POST request
"""
# Our default response type
content_type = 'text/plain'
# our content
content = {}
if FORM_CTYPE_RE.match(request.content_type):
content = {}
form = NotifyForm(request.POST)
if form.is_valid():
content.update(form.clean())
elif JSON_CTYPE_RE.match(request.content_type):
# Prepare our default response
try:
# load our JSON content
content = json.loads(request.body)
except (AttributeError, ValueError):
# could not parse JSON response...
return HttpResponse(
_('Invalid JSON specified.'),
content_type=content_type,
status=ResponseCode.bad_request)
if not content:
# We could not handle the Content-Type
return HttpResponse(
_('The message format is not supported.'),
content_type=content_type,
status=ResponseCode.bad_request)
# Some basic error checking
if not content.get('body') or \
content.get('type', apprise.NotifyType.INFO) \
not in apprise.NOTIFY_TYPES:
return HttpResponse(
_('An invalid payload was specified.'),
content_type=content_type,
status=ResponseCode.bad_request)
# If we get here, we have enough information to generate a notification
# with.
config, format = ConfigCache.get(key)
if config is None:
# The returned value of config and format tell a rather cryptic
# story; this portion could probably be updated in the future.
# but for now it reads like this:
# config == None and format == None: We had an internal error
# config == None and format != None: we simply have no data
# config != None: we simply have no data
if format is not None:
# no content to return
return HttpResponse(
_('There was no configuration found.'),
content_type=content_type,
status=ResponseCode.no_content,
)
# Something went very wrong; return 500
return HttpResponse(
_('An error occured accessing configuration.'),
content_type=content_type,
status=ResponseCode.internal_server_error,
)
# Prepare our apprise object
a_obj = apprise.Apprise()
# Create an apprise config object
ac_obj = apprise.AppriseConfig()
try:
# Write our file to a temporary file containing our configuration
# so that we can read it back. In the future a change will be to
# Apprise so that we can just directly write the configuration as
# is to the AppriseConfig() object... but for now...
with NamedTemporaryFile() as f:
# Write our content to disk
f.write(config.encode())
f.flush()
# Read our configuration back in to our configuration
ac_obj.add('file://{}?format={}'.format(f.name, format))
# Add our configuration
a_obj.add(ac_obj)
# Perform our notification at this point
a_obj.notify(
content.get('body'),
title=content.get('title', ''),
notify_type=content.get('type', apprise.NotifyType.INFO),
tag=content.get('tag'),
)
except OSError:
# We could not write the temporary file to disk
return HttpResponse(
_('The configuration could not be loaded.'),
content_type=content_type,
status=ResponseCode.internal_server_error)
# Return our retrieved content
return HttpResponse(
_('Notification(s) sent.'),
content_type=content_type, status=ResponseCode.okay)

View File

@ -0,0 +1,36 @@
# The backend API server
upstream apprise_backend {
server backend:8000 fail_timeout=0;
}
server {
listen 80;
server_name _;
charset utf-8;
# Main Website
location / {
include uwsgi_params;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass http://apprise_backend;
}
# Static Content
location /s/ {
root /usr/share/nginx/html;
index index.html;
}
# 404 error handling
error_page 404 /404.html;
# redirect server error pages to the static page /50x.html
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}

View File

View File

@ -0,0 +1,85 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2019 Chris Caron <lead2gold@gmail.com>
# All rights reserved.
#
# This code is licensed under the MIT License.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files(the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions :
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
import os
# Base Directory (relative to settings)
BASE_DIR = os.path.dirname(
os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = os.environ.get(
'SECRET_KEY', '+reua88v8rs4j!bcfdtinb-f0edxazf!$x_q1g7jtgckxd7gi=')
# SECURITY WARNING: don't run with debug turned on in production!
# If you want to run this app in DEBUG mode, run the following:
#
# ./manage.py runserver --settings=core.settings.debug
#
# Or alternatively run:
#
# export DJANGO_SETTINGS_MODULE=core.settings.debug
# ./manage.py runserver
DEBUG = bool(os.environ.get("DEBUG", False))
# allow all hosts by default otherwise read from the
# ALLOWED_HOSTS environment variable
ALLOWED_HOSTS = os.environ.get('ALLOWED_HOSTS', '*').split(' ')
# Application definition
INSTALLED_APPS = [
'django.contrib.contenttypes',
'django.contrib.staticfiles',
# Apprise API
'api',
]
MIDDLEWARE = [
'django.middleware.common.CommonMiddleware',
]
ROOT_URLCONF = 'core.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.request',
],
},
},
]
WSGI_APPLICATION = 'core.wsgi.application'
# Static files relative path (CSS, JavaScript, Images)
STATIC_URL = '/s/'
# The location to store Apprise configuration files
APPRISE_CONFIG_DIR = os.environ.get(
'APPRISE_CONFIG_DIR', os.path.join(BASE_DIR, 'var', 'config'))

View File

@ -0,0 +1,43 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2019 Chris Caron <lead2gold@gmail.com>
# All rights reserved.
#
# This code is licensed under the MIT License.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files(the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions :
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
# To create a valid debug settings.py we need to intentionally pollute our
# file with all of the content found in the master configuration.
import os
from .. import * # noqa F403
# Debug is always on when running in debug mode
DEBUG = True
# Allowed hosts is not required in debug mode
ALLOWED_HOSTS = []
# Over-ride the default URLConf for debugging
ROOT_URLCONF = 'core.settings.debug.urls'
# Our static paths directory for serving
STATICFILES_DIRS = (
os.path.join(BASE_DIR, 'static'), # noqa F405
)

View File

@ -0,0 +1,30 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2019 Chris Caron <lead2gold@gmail.com>
# All rights reserved.
#
# This code is licensed under the MIT License.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files(the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions :
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
from django.conf import settings
from django.conf.urls.static import static
from ...urls import * # noqa F403
# Extend our patterns
urlpatterns += static(settings.STATIC_URL) # noqa F405

View File

@ -0,0 +1,41 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2019 Chris Caron <lead2gold@gmail.com>
# All rights reserved.
#
# This code is licensed under the MIT License.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files(the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions :
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
# To create a valid debug settings.py we need to intentionally pollute our
# file with all of the content found in the master configuration.
from tempfile import TemporaryDirectory
from .. import * # noqa F403
# Debug is always on when running in debug mode
DEBUG = True
# Allowed hosts is not required in debug mode
ALLOWED_HOSTS = []
# A temporary directory to work in for unit testing
APPRISE_CONFIG_DIR = TemporaryDirectory().name
# Setup our runner
TEST_RUNNER = 'core.settings.pytest.runner.PytestTestRunner'

View File

@ -0,0 +1,59 @@
# -*- coding: utf-8 -*-
# -*- coding: utf-8 -*-
#
# Copyright (C) 2019 Chris Caron <lead2gold@gmail.com>
# All rights reserved.
#
# This code is licensed under the MIT License.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files(the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions :
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
# To create a valid debug settings.py we need to intentionally pollute our
# file with all of the content found in the master configuration.
class PytestTestRunner(object):
"""Runs pytest to discover and run tests."""
def __init__(self, verbosity=1, failfast=False, keepdb=False, **kwargs):
self.verbosity = verbosity
self.failfast = failfast
self.keepdb = keepdb
def run_tests(self, test_labels):
"""Run pytest and return the exitcode.
It translates some of Django's test command option to pytest's.
"""
import pytest
argv = []
if self.verbosity == 0:
argv.append('--quiet')
if self.verbosity == 2:
argv.append('--verbose')
if self.verbosity == 3:
argv.append('-vv')
if self.failfast:
argv.append('--exitfirst')
if self.keepdb:
argv.append('--reuse-db')
argv.extend(test_labels)
return pytest.main(argv)

32
apprise_api/core/urls.py Normal file
View File

@ -0,0 +1,32 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2019 Chris Caron <lead2gold@gmail.com>
# All rights reserved.
#
# This code is licensed under the MIT License.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files(the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions :
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
from django.urls import path
from django.conf.urls import include
from api import urls as api_urls
urlpatterns = [
path('', include(api_urls)),
]

29
apprise_api/core/wsgi.py Normal file
View File

@ -0,0 +1,29 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2019 Chris Caron <lead2gold@gmail.com>
# All rights reserved.
#
# This code is licensed under the MIT License.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files(the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions :
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings')
application = get_wsgi_application()

View File

@ -0,0 +1,50 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2019 Chris Caron <lead2gold@gmail.com>
# All rights reserved.
#
# This code is licensed under the MIT License.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files(the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions :
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
import multiprocessing
# This file is launched with the call:
# gunicorn --config <this file> core.wsgi:application
raw_env = [
'LANG=en_US.UTF-8',
'DJANGO_SETTINGS_MODULE=core.settings',
]
# This is the path as prepared in the docker compose
pythonpath = '/opt/apprise/webapp'
# bind to port 8000
bind = [
'0.0.0.0:8000',
]
# Workers are relative to the number of CPU's provided by hosting server
workers = multiprocessing.cpu_count() * 2 + 1
# Logging
# '-' means log to stdout.
errorlog = '-'
accesslog = '-'
loglevel = 'warn'

44
apprise_api/manage.py Executable file
View File

@ -0,0 +1,44 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Copyright (C) 2019 Chris Caron <lead2gold@gmail.com>
# All rights reserved.
#
# This code is licensed under the MIT License.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files(the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions :
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
import os
import sys
def main():
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings.debug')
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)
if __name__ == '__main__':
main()

31
apprise_api/nginx.conf Normal file
View File

@ -0,0 +1,31 @@
#
# Apprise Docker NginX Configuration
#
error_log /dev/stdout info;
user nginx;
worker_processes 1;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /dev/stdout main;
sendfile on;
#tcp_nopush on;
keepalive_timeout 65;
gzip on;
include /etc/nginx/conf.d/*.conf;
}

4
apprise_api/pytest.ini Normal file
View File

@ -0,0 +1,4 @@
[pytest]
DJANGO_SETTINGS_MODULE = core.settings.pytest
addopts = --nomigrations --cov=. --cov-report=html
python_files = tests.py test_*.py *_tests.py

View File

@ -0,0 +1,61 @@
.nav h1 {
margin: 0;
font-size: 3.1rem;
font-weight: bold;
text-transform: uppercase;
}
code {
background-color: #eee;
font-family: monospace;
white-space: normal;
padding: 0.2rem;
}
table code {
background-color: inherit;
}
h1, h2, h3, h4, h5 {
margin-top: 0;
}
td, th {
vertical-align: top;
padding-top: 0;
}
.api-details ol {
margin-top: 0;
margin-bottom: 0;
}
em {
color: #004d40;
font-weight:bold;
}
textarea {
height: 16rem;
}
.collapsible-body {
padding: 1rem 2rem;
}
.tabs .tab a{
color:#2bbbad;
}
.collection a.collection-item:not(.active):hover,
.tabs .tab a:focus, .tabs .tab a:focus.active {
background-color: #eee;
}
.tabs .tab a:hover,.tabs .tab a.active {
background-color:transparent;
color:#2bbbad;
}
.tabs .tab.disabled a,.tabs .tab.disabled a:hover {
color:rgba(102,147,153,0.7);
}
.tabs .indicator {
background-color:#004d40;
}

9067
apprise_api/static/css/materialize.css vendored Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,9 @@
The recommended way to use the Material Icons font is by linking to the web font hosted on Google Fonts:
```html
<link href="https://fonts.googleapis.com/icon?family=Material+Icons"
rel="stylesheet">
```
Read more in our full usage guide:
http://google.github.io/material-design-icons/#icon-font-for-the-web

View File

@ -0,0 +1,932 @@
3d_rotation e84d
ac_unit eb3b
access_alarm e190
access_alarms e191
access_time e192
accessibility e84e
accessible e914
account_balance e84f
account_balance_wallet e850
account_box e851
account_circle e853
adb e60e
add e145
add_a_photo e439
add_alarm e193
add_alert e003
add_box e146
add_circle e147
add_circle_outline e148
add_location e567
add_shopping_cart e854
add_to_photos e39d
add_to_queue e05c
adjust e39e
airline_seat_flat e630
airline_seat_flat_angled e631
airline_seat_individual_suite e632
airline_seat_legroom_extra e633
airline_seat_legroom_normal e634
airline_seat_legroom_reduced e635
airline_seat_recline_extra e636
airline_seat_recline_normal e637
airplanemode_active e195
airplanemode_inactive e194
airplay e055
airport_shuttle eb3c
alarm e855
alarm_add e856
alarm_off e857
alarm_on e858
album e019
all_inclusive eb3d
all_out e90b
android e859
announcement e85a
apps e5c3
archive e149
arrow_back e5c4
arrow_downward e5db
arrow_drop_down e5c5
arrow_drop_down_circle e5c6
arrow_drop_up e5c7
arrow_forward e5c8
arrow_upward e5d8
art_track e060
aspect_ratio e85b
assessment e85c
assignment e85d
assignment_ind e85e
assignment_late e85f
assignment_return e860
assignment_returned e861
assignment_turned_in e862
assistant e39f
assistant_photo e3a0
attach_file e226
attach_money e227
attachment e2bc
audiotrack e3a1
autorenew e863
av_timer e01b
backspace e14a
backup e864
battery_alert e19c
battery_charging_full e1a3
battery_full e1a4
battery_std e1a5
battery_unknown e1a6
beach_access eb3e
beenhere e52d
block e14b
bluetooth e1a7
bluetooth_audio e60f
bluetooth_connected e1a8
bluetooth_disabled e1a9
bluetooth_searching e1aa
blur_circular e3a2
blur_linear e3a3
blur_off e3a4
blur_on e3a5
book e865
bookmark e866
bookmark_border e867
border_all e228
border_bottom e229
border_clear e22a
border_color e22b
border_horizontal e22c
border_inner e22d
border_left e22e
border_outer e22f
border_right e230
border_style e231
border_top e232
border_vertical e233
branding_watermark e06b
brightness_1 e3a6
brightness_2 e3a7
brightness_3 e3a8
brightness_4 e3a9
brightness_5 e3aa
brightness_6 e3ab
brightness_7 e3ac
brightness_auto e1ab
brightness_high e1ac
brightness_low e1ad
brightness_medium e1ae
broken_image e3ad
brush e3ae
bubble_chart e6dd
bug_report e868
build e869
burst_mode e43c
business e0af
business_center eb3f
cached e86a
cake e7e9
call e0b0
call_end e0b1
call_made e0b2
call_merge e0b3
call_missed e0b4
call_missed_outgoing e0e4
call_received e0b5
call_split e0b6
call_to_action e06c
camera e3af
camera_alt e3b0
camera_enhance e8fc
camera_front e3b1
camera_rear e3b2
camera_roll e3b3
cancel e5c9
card_giftcard e8f6
card_membership e8f7
card_travel e8f8
casino eb40
cast e307
cast_connected e308
center_focus_strong e3b4
center_focus_weak e3b5
change_history e86b
chat e0b7
chat_bubble e0ca
chat_bubble_outline e0cb
check e5ca
check_box e834
check_box_outline_blank e835
check_circle e86c
chevron_left e5cb
chevron_right e5cc
child_care eb41
child_friendly eb42
chrome_reader_mode e86d
class e86e
clear e14c
clear_all e0b8
close e5cd
closed_caption e01c
cloud e2bd
cloud_circle e2be
cloud_done e2bf
cloud_download e2c0
cloud_off e2c1
cloud_queue e2c2
cloud_upload e2c3
code e86f
collections e3b6
collections_bookmark e431
color_lens e3b7
colorize e3b8
comment e0b9
compare e3b9
compare_arrows e915
computer e30a
confirmation_number e638
contact_mail e0d0
contact_phone e0cf
contacts e0ba
content_copy e14d
content_cut e14e
content_paste e14f
control_point e3ba
control_point_duplicate e3bb
copyright e90c
create e150
create_new_folder e2cc
credit_card e870
crop e3be
crop_16_9 e3bc
crop_3_2 e3bd
crop_5_4 e3bf
crop_7_5 e3c0
crop_din e3c1
crop_free e3c2
crop_landscape e3c3
crop_original e3c4
crop_portrait e3c5
crop_rotate e437
crop_square e3c6
dashboard e871
data_usage e1af
date_range e916
dehaze e3c7
delete e872
delete_forever e92b
delete_sweep e16c
description e873
desktop_mac e30b
desktop_windows e30c
details e3c8
developer_board e30d
developer_mode e1b0
device_hub e335
devices e1b1
devices_other e337
dialer_sip e0bb
dialpad e0bc
directions e52e
directions_bike e52f
directions_boat e532
directions_bus e530
directions_car e531
directions_railway e534
directions_run e566
directions_subway e533
directions_transit e535
directions_walk e536
disc_full e610
dns e875
do_not_disturb e612
do_not_disturb_alt e611
do_not_disturb_off e643
do_not_disturb_on e644
dock e30e
domain e7ee
done e876
done_all e877
donut_large e917
donut_small e918
drafts e151
drag_handle e25d
drive_eta e613
dvr e1b2
edit e3c9
edit_location e568
eject e8fb
email e0be
enhanced_encryption e63f
equalizer e01d
error e000
error_outline e001
euro_symbol e926
ev_station e56d
event e878
event_available e614
event_busy e615
event_note e616
event_seat e903
exit_to_app e879
expand_less e5ce
expand_more e5cf
explicit e01e
explore e87a
exposure e3ca
exposure_neg_1 e3cb
exposure_neg_2 e3cc
exposure_plus_1 e3cd
exposure_plus_2 e3ce
exposure_zero e3cf
extension e87b
face e87c
fast_forward e01f
fast_rewind e020
favorite e87d
favorite_border e87e
featured_play_list e06d
featured_video e06e
feedback e87f
fiber_dvr e05d
fiber_manual_record e061
fiber_new e05e
fiber_pin e06a
fiber_smart_record e062
file_download e2c4
file_upload e2c6
filter e3d3
filter_1 e3d0
filter_2 e3d1
filter_3 e3d2
filter_4 e3d4
filter_5 e3d5
filter_6 e3d6
filter_7 e3d7
filter_8 e3d8
filter_9 e3d9
filter_9_plus e3da
filter_b_and_w e3db
filter_center_focus e3dc
filter_drama e3dd
filter_frames e3de
filter_hdr e3df
filter_list e152
filter_none e3e0
filter_tilt_shift e3e2
filter_vintage e3e3
find_in_page e880
find_replace e881
fingerprint e90d
first_page e5dc
fitness_center eb43
flag e153
flare e3e4
flash_auto e3e5
flash_off e3e6
flash_on e3e7
flight e539
flight_land e904
flight_takeoff e905
flip e3e8
flip_to_back e882
flip_to_front e883
folder e2c7
folder_open e2c8
folder_shared e2c9
folder_special e617
font_download e167
format_align_center e234
format_align_justify e235
format_align_left e236
format_align_right e237
format_bold e238
format_clear e239
format_color_fill e23a
format_color_reset e23b
format_color_text e23c
format_indent_decrease e23d
format_indent_increase e23e
format_italic e23f
format_line_spacing e240
format_list_bulleted e241
format_list_numbered e242
format_paint e243
format_quote e244
format_shapes e25e
format_size e245
format_strikethrough e246
format_textdirection_l_to_r e247
format_textdirection_r_to_l e248
format_underlined e249
forum e0bf
forward e154
forward_10 e056
forward_30 e057
forward_5 e058
free_breakfast eb44
fullscreen e5d0
fullscreen_exit e5d1
functions e24a
g_translate e927
gamepad e30f
games e021
gavel e90e
gesture e155
get_app e884
gif e908
golf_course eb45
gps_fixed e1b3
gps_not_fixed e1b4
gps_off e1b5
grade e885
gradient e3e9
grain e3ea
graphic_eq e1b8
grid_off e3eb
grid_on e3ec
group e7ef
group_add e7f0
group_work e886
hd e052
hdr_off e3ed
hdr_on e3ee
hdr_strong e3f1
hdr_weak e3f2
headset e310
headset_mic e311
healing e3f3
hearing e023
help e887
help_outline e8fd
high_quality e024
highlight e25f
highlight_off e888
history e889
home e88a
hot_tub eb46
hotel e53a
hourglass_empty e88b
hourglass_full e88c
http e902
https e88d
image e3f4
image_aspect_ratio e3f5
import_contacts e0e0
import_export e0c3
important_devices e912
inbox e156
indeterminate_check_box e909
info e88e
info_outline e88f
input e890
insert_chart e24b
insert_comment e24c
insert_drive_file e24d
insert_emoticon e24e
insert_invitation e24f
insert_link e250
insert_photo e251
invert_colors e891
invert_colors_off e0c4
iso e3f6
keyboard e312
keyboard_arrow_down e313
keyboard_arrow_left e314
keyboard_arrow_right e315
keyboard_arrow_up e316
keyboard_backspace e317
keyboard_capslock e318
keyboard_hide e31a
keyboard_return e31b
keyboard_tab e31c
keyboard_voice e31d
kitchen eb47
label e892
label_outline e893
landscape e3f7
language e894
laptop e31e
laptop_chromebook e31f
laptop_mac e320
laptop_windows e321
last_page e5dd
launch e895
layers e53b
layers_clear e53c
leak_add e3f8
leak_remove e3f9
lens e3fa
library_add e02e
library_books e02f
library_music e030
lightbulb_outline e90f
line_style e919
line_weight e91a
linear_scale e260
link e157
linked_camera e438
list e896
live_help e0c6
live_tv e639
local_activity e53f
local_airport e53d
local_atm e53e
local_bar e540
local_cafe e541
local_car_wash e542
local_convenience_store e543
local_dining e556
local_drink e544
local_florist e545
local_gas_station e546
local_grocery_store e547
local_hospital e548
local_hotel e549
local_laundry_service e54a
local_library e54b
local_mall e54c
local_movies e54d
local_offer e54e
local_parking e54f
local_pharmacy e550
local_phone e551
local_pizza e552
local_play e553
local_post_office e554
local_printshop e555
local_see e557
local_shipping e558
local_taxi e559
location_city e7f1
location_disabled e1b6
location_off e0c7
location_on e0c8
location_searching e1b7
lock e897
lock_open e898
lock_outline e899
looks e3fc
looks_3 e3fb
looks_4 e3fd
looks_5 e3fe
looks_6 e3ff
looks_one e400
looks_two e401
loop e028
loupe e402
low_priority e16d
loyalty e89a
mail e158
mail_outline e0e1
map e55b
markunread e159
markunread_mailbox e89b
memory e322
menu e5d2
merge_type e252
message e0c9
mic e029
mic_none e02a
mic_off e02b
mms e618
mode_comment e253
mode_edit e254
monetization_on e263
money_off e25c
monochrome_photos e403
mood e7f2
mood_bad e7f3
more e619
more_horiz e5d3
more_vert e5d4
motorcycle e91b
mouse e323
move_to_inbox e168
movie e02c
movie_creation e404
movie_filter e43a
multiline_chart e6df
music_note e405
music_video e063
my_location e55c
nature e406
nature_people e407
navigate_before e408
navigate_next e409
navigation e55d
near_me e569
network_cell e1b9
network_check e640
network_locked e61a
network_wifi e1ba
new_releases e031
next_week e16a
nfc e1bb
no_encryption e641
no_sim e0cc
not_interested e033
note e06f
note_add e89c
notifications e7f4
notifications_active e7f7
notifications_none e7f5
notifications_off e7f6
notifications_paused e7f8
offline_pin e90a
ondemand_video e63a
opacity e91c
open_in_browser e89d
open_in_new e89e
open_with e89f
pages e7f9
pageview e8a0
palette e40a
pan_tool e925
panorama e40b
panorama_fish_eye e40c
panorama_horizontal e40d
panorama_vertical e40e
panorama_wide_angle e40f
party_mode e7fa
pause e034
pause_circle_filled e035
pause_circle_outline e036
payment e8a1
people e7fb
people_outline e7fc
perm_camera_mic e8a2
perm_contact_calendar e8a3
perm_data_setting e8a4
perm_device_information e8a5
perm_identity e8a6
perm_media e8a7
perm_phone_msg e8a8
perm_scan_wifi e8a9
person e7fd
person_add e7fe
person_outline e7ff
person_pin e55a
person_pin_circle e56a
personal_video e63b
pets e91d
phone e0cd
phone_android e324
phone_bluetooth_speaker e61b
phone_forwarded e61c
phone_in_talk e61d
phone_iphone e325
phone_locked e61e
phone_missed e61f
phone_paused e620
phonelink e326
phonelink_erase e0db
phonelink_lock e0dc
phonelink_off e327
phonelink_ring e0dd
phonelink_setup e0de
photo e410
photo_album e411
photo_camera e412
photo_filter e43b
photo_library e413
photo_size_select_actual e432
photo_size_select_large e433
photo_size_select_small e434
picture_as_pdf e415
picture_in_picture e8aa
picture_in_picture_alt e911
pie_chart e6c4
pie_chart_outlined e6c5
pin_drop e55e
place e55f
play_arrow e037
play_circle_filled e038
play_circle_outline e039
play_for_work e906
playlist_add e03b
playlist_add_check e065
playlist_play e05f
plus_one e800
poll e801
polymer e8ab
pool eb48
portable_wifi_off e0ce
portrait e416
power e63c
power_input e336
power_settings_new e8ac
pregnant_woman e91e
present_to_all e0df
print e8ad
priority_high e645
public e80b
publish e255
query_builder e8ae
question_answer e8af
queue e03c
queue_music e03d
queue_play_next e066
radio e03e
radio_button_checked e837
radio_button_unchecked e836
rate_review e560
receipt e8b0
recent_actors e03f
record_voice_over e91f
redeem e8b1
redo e15a
refresh e5d5
remove e15b
remove_circle e15c
remove_circle_outline e15d
remove_from_queue e067
remove_red_eye e417
remove_shopping_cart e928
reorder e8fe
repeat e040
repeat_one e041
replay e042
replay_10 e059
replay_30 e05a
replay_5 e05b
reply e15e
reply_all e15f
report e160
report_problem e8b2
restaurant e56c
restaurant_menu e561
restore e8b3
restore_page e929
ring_volume e0d1
room e8b4
room_service eb49
rotate_90_degrees_ccw e418
rotate_left e419
rotate_right e41a
rounded_corner e920
router e328
rowing e921
rss_feed e0e5
rv_hookup e642
satellite e562
save e161
scanner e329
schedule e8b5
school e80c
screen_lock_landscape e1be
screen_lock_portrait e1bf
screen_lock_rotation e1c0
screen_rotation e1c1
screen_share e0e2
sd_card e623
sd_storage e1c2
search e8b6
security e32a
select_all e162
send e163
sentiment_dissatisfied e811
sentiment_neutral e812
sentiment_satisfied e813
sentiment_very_dissatisfied e814
sentiment_very_satisfied e815
settings e8b8
settings_applications e8b9
settings_backup_restore e8ba
settings_bluetooth e8bb
settings_brightness e8bd
settings_cell e8bc
settings_ethernet e8be
settings_input_antenna e8bf
settings_input_component e8c0
settings_input_composite e8c1
settings_input_hdmi e8c2
settings_input_svideo e8c3
settings_overscan e8c4
settings_phone e8c5
settings_power e8c6
settings_remote e8c7
settings_system_daydream e1c3
settings_voice e8c8
share e80d
shop e8c9
shop_two e8ca
shopping_basket e8cb
shopping_cart e8cc
short_text e261
show_chart e6e1
shuffle e043
signal_cellular_4_bar e1c8
signal_cellular_connected_no_internet_4_bar e1cd
signal_cellular_no_sim e1ce
signal_cellular_null e1cf
signal_cellular_off e1d0
signal_wifi_4_bar e1d8
signal_wifi_4_bar_lock e1d9
signal_wifi_off e1da
sim_card e32b
sim_card_alert e624
skip_next e044
skip_previous e045
slideshow e41b
slow_motion_video e068
smartphone e32c
smoke_free eb4a
smoking_rooms eb4b
sms e625
sms_failed e626
snooze e046
sort e164
sort_by_alpha e053
spa eb4c
space_bar e256
speaker e32d
speaker_group e32e
speaker_notes e8cd
speaker_notes_off e92a
speaker_phone e0d2
spellcheck e8ce
star e838
star_border e83a
star_half e839
stars e8d0
stay_current_landscape e0d3
stay_current_portrait e0d4
stay_primary_landscape e0d5
stay_primary_portrait e0d6
stop e047
stop_screen_share e0e3
storage e1db
store e8d1
store_mall_directory e563
straighten e41c
streetview e56e
strikethrough_s e257
style e41d
subdirectory_arrow_left e5d9
subdirectory_arrow_right e5da
subject e8d2
subscriptions e064
subtitles e048
subway e56f
supervisor_account e8d3
surround_sound e049
swap_calls e0d7
swap_horiz e8d4
swap_vert e8d5
swap_vertical_circle e8d6
switch_camera e41e
switch_video e41f
sync e627
sync_disabled e628
sync_problem e629
system_update e62a
system_update_alt e8d7
tab e8d8
tab_unselected e8d9
tablet e32f
tablet_android e330
tablet_mac e331
tag_faces e420
tap_and_play e62b
terrain e564
text_fields e262
text_format e165
textsms e0d8
texture e421
theaters e8da
thumb_down e8db
thumb_up e8dc
thumbs_up_down e8dd
time_to_leave e62c
timelapse e422
timeline e922
timer e425
timer_10 e423
timer_3 e424
timer_off e426
title e264
toc e8de
today e8df
toll e8e0
tonality e427
touch_app e913
toys e332
track_changes e8e1
traffic e565
train e570
tram e571
transfer_within_a_station e572
transform e428
translate e8e2
trending_down e8e3
trending_flat e8e4
trending_up e8e5
tune e429
turned_in e8e6
turned_in_not e8e7
tv e333
unarchive e169
undo e166
unfold_less e5d6
unfold_more e5d7
update e923
usb e1e0
verified_user e8e8
vertical_align_bottom e258
vertical_align_center e259
vertical_align_top e25a
vibration e62d
video_call e070
video_label e071
video_library e04a
videocam e04b
videocam_off e04c
videogame_asset e338
view_agenda e8e9
view_array e8ea
view_carousel e8eb
view_column e8ec
view_comfy e42a
view_compact e42b
view_day e8ed
view_headline e8ee
view_list e8ef
view_module e8f0
view_quilt e8f1
view_stream e8f2
view_week e8f3
vignette e435
visibility e8f4
visibility_off e8f5
voice_chat e62e
voicemail e0d9
volume_down e04d
volume_mute e04e
volume_off e04f
volume_up e050
vpn_key e0da
vpn_lock e62f
wallpaper e1bc
warning e002
watch e334
watch_later e924
wb_auto e42c
wb_cloudy e42d
wb_incandescent e42e
wb_iridescent e436
wb_sunny e430
wc e63d
web e051
web_asset e069
weekend e16b
whatshot e80e
widgets e1bd
wifi e63e
wifi_lock e1e1
wifi_tethering e1e2
work e8f9
wrap_text e25b
youtube_searched_for e8fa
zoom_in e8ff
zoom_out e900
zoom_out_map e56b

View File

@ -0,0 +1,36 @@
@font-face {
font-family: 'Material Icons';
font-style: normal;
font-weight: 400;
src: url(MaterialIcons-Regular.eot); /* For IE6-8 */
src: local('Material Icons'),
local('MaterialIcons-Regular'),
url(MaterialIcons-Regular.woff2) format('woff2'),
url(MaterialIcons-Regular.woff) format('woff'),
url(MaterialIcons-Regular.ttf) format('truetype');
}
.material-icons {
font-family: 'Material Icons';
font-weight: normal;
font-style: normal;
font-size: 24px; /* Preferred icon size */
display: inline-block;
line-height: 1;
text-transform: none;
letter-spacing: normal;
word-wrap: normal;
white-space: nowrap;
direction: ltr;
/* Support for all WebKit browsers. */
-webkit-font-smoothing: antialiased;
/* Support for Safari and Chrome. */
text-rendering: optimizeLegibility;
/* Support for Firefox. */
-moz-osx-font-smoothing: grayscale;
/* Support for IE. */
font-feature-settings: 'liga';
}

12374
apprise_api/static/js/materialize.js vendored Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2014-2018 Materialize
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

BIN
apprise_api/static/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

4
dev-requirements.txt Normal file
View File

@ -0,0 +1,4 @@
pytest
flake8
pytest-django
pytest-cov

19
docker-compose.yml Normal file
View File

@ -0,0 +1,19 @@
version: '3.3'
services:
backend:
build:
context: .
dockerfile: Dockerfile-gunicorn
volumes:
- ./var:/var/apprise
restart: always
frontend:
build:
context: .
dockerfile: Dockerfile-nginx
ports:
- "8000:80"
links:
- backend

49
manage.py Executable file
View File

@ -0,0 +1,49 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Copyright (C) 2019 Chris Caron <lead2gold@gmail.com>
# All rights reserved.
#
# This code is licensed under the MIT License.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files(the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions :
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
import os
import sys
# Update our path so it will see our apprise_api content
sys.path.insert(
0, os.path.join(os.path.dirname(os.path.abspath(__file__)), 'apprise_api'))
def main():
# Unless otherwise specified, default to a debug mode
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings.debug')
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)
if __name__ == '__main__':
main()

2
requirements.txt Normal file
View File

@ -0,0 +1,2 @@
django
apprise