Support a /json/urls/{key} query to provide loaded details (#4)

This commit is contained in:
Chris Caron 2020-01-05 18:19:09 -05:00 committed by GitHub
parent d571a8e186
commit 4b9c072d6b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 333 additions and 68 deletions

View File

@ -4,6 +4,8 @@ omit =
*migrations/*,
*settings*,
*tests/*,
lib/*,
lib64/*,
*urls.py,
*core/wsgi.py,
gunicorn.conf.py,

View File

@ -15,7 +15,7 @@ Apprise API was designed to easily fit into existing (and new) eco-systems that
[![Docker Pulls](https://img.shields.io/docker/pulls/caronc/apprise.svg?style=flat-square)](https://hub.docker.com/r/caronc/apprise)
## Screenshots
There is a small built-in *Configuration Manager* that can be accesed using `/cfg/{KEY}`:<br/>
There is a small built-in *Configuration Manager* that can be optionally accessed through your web browser accessible via `/cfg/{KEY}`:<br/>
![Screenshot of GUI - Using Keys](https://raw.githubusercontent.com/caronc/apprise-api/master/Screenshot-1.png)<br/>
Below is a screenshot of how you can set either a series of URL's to your `{KEY}`, or set your YAML and/or TEXT configuration below.
@ -47,18 +47,39 @@ docker-compose up
## 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.
| `/notify/` | Similar to the `notify` API identified above except this one sends a stateless notification and requires no reference to a `{KEY}`. <br/>*Parameters*<br/>:small_red_triangle: **urls**: One or more URLs identifying where the notification should be sent to. If this field isn't specified then it automatically assumes the `settings.APPRISE_STATELESS_URLS` value or `APPRISE_STATELESS_URLS` environment variable.<br/>:small_red_triangle: **body**: Your message body. This is a 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.
| Path | Method | Description |
|------------- | ------ | ----------- |
| `/add/{KEY}` | POST | 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}` | POST | Removes Apprise Configuration from the persistent store.
| `/get/{KEY}` | POST | 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}` | POST | 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.
| `/notify/` | POST | Similar to the `notify` API identified above except this one sends a stateless notification and requires no reference to a `{KEY}`. <br/>*Parameters*<br/>:small_red_triangle: **urls**: One or more URLs identifying where the notification should be sent to. If this field isn't specified then it automatically assumes the `settings.APPRISE_STATELESS_URLS` value or `APPRISE_STATELESS_URLS` environment variable.<br/>:small_red_triangle: **body**: Your message body. This is a 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.
| `/json/urls/{KEY}` | GET | Returns a JSON response object that contains all of the URLS and Tags associated with the key specified.
The `/json/urls/{KEY}` response might look like this:
```json
{
"tags": ["devops", "admin", "me"],
"urls": [
{
"url": "slack://TokenA/TokenB/TokenC",
"tags": ["devops", "admin"]
},
{
"url": "discord://WebhookID/WebhookToken",
"tags": ["devops"]
},
{
"url": "mailto://user:pass@gmail.com",
"tags": ["me"]
}
]
}
```
### 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 (or SSL encryption) required to use this API; this is by design. The intentio here to be a lightweight and fast micro-service that can be parked behind another tier that was designed to handle security.
- There are no additional dependencies should you choose to use the optional persistent store (mounted as `/config`).
@ -69,8 +90,8 @@ The use of environment variables allow you to provide over-rides to default sett
| Variable | Description |
|--------------------- | ----------- |
| `APPRISE_CONFIG_DIR` | Defines the persistent store location of all configuration files saved. By default:<br/> - Configuration is written to the `apprise_api/var/config` directory when just using the _Django_ `manage runserver` script.
| `APPRISE_STATELESS_URLS` | A default set of URLs to notify when using the stateless `/notify` reference (no reference to `{KEY}` variables). Use this option if you don't intend to use the persistent store at all.
| `APPRISE_CONFIG_DIR` | Defines an (optional) persistent store location of all configuration files saved. By default:<br/> - Configuration is written to the `apprise_api/var/config` directory when just using the _Django_ `manage runserver` script. However for the path for containers is `/config`
| `APPRISE_STATELESS_URLS` | For a non-persistent solution, you can take avantage of this global variable. Use this to define a default set of Apprise URLs to notify when using API calls to `/notify`. If no `{KEY}` is defined when calling `/notify` then the URLs defined here are used instead.
| `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`).

View File

@ -77,11 +77,6 @@ class GetTests(SimpleTestCase):
- dbus://"""})
assert response.status_code == 200
# Verify that the correct Content-Type is set in the header of the
# response
assert 'Content-Type' in response
assert response['Content-Type'].startswith('text/plain')
# Now retrieve our YAML configuration
response = self.client.post('/get/{}'.format(key))
assert response.status_code == 200

View File

@ -0,0 +1,148 @@
# -*- 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
class JsonUrlsTests(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_post_not_supported(self):
"""
Test POST requests with key
"""
response = self.client.post('/json/urls/test')
# 405 as posting is not allowed
assert response.status_code == 405
def test_json_urls_config(self):
"""
Test retrieving configuration
"""
# our key to use
key = 'test_json_urls_config'
# Nothing to return
response = self.client.get('/json/urls/{}'.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
# Handle case when we try to retrieve our content but we have no idea
# what the format is in. Essentialy there had to have been disk
# corruption here or someone meddling with the backend.
with patch('gzip.open', side_effect=OSError):
response = self.client.get('/json/urls/{}'.format(key))
assert response.status_code == 500
assert response['Content-Type'].startswith('application/json')
assert 'tags' in response.json()
assert 'urls' in response.json()
# has error directive
assert 'error' in response.json()
# entries exist by are empty
assert len(response.json()['tags']) == 0
assert len(response.json()['urls']) == 0
# Now we should be able to see our content
response = self.client.get('/json/urls/{}'.format(key))
assert response.status_code == 200
assert response['Content-Type'].startswith('application/json')
assert 'tags' in response.json()
assert 'urls' in response.json()
# No errors occured, therefore no error entry
assert 'error' not in response.json()
# No tags (but can be assumed "all") is always present
assert len(response.json()['tags']) == 0
# One URL loaded
assert len(response.json()['urls']) == 1
assert 'url' in response.json()['urls'][0]
assert 'tags' in response.json()['urls'][0]
assert len(response.json()['urls'][0]['tags']) == 0
# Add a YAML file
response = self.client.post(
'/add/{}'.format(key), {
'format': 'yaml',
'config': """
urls:
- dbus://:
- tag: tag1, tag2"""})
assert response.status_code == 200
# Now retrieve our JSON resonse
response = self.client.get('/json/urls/{}'.format(key))
assert response.status_code == 200
# No errors occured, therefore no error entry
assert 'error' not in response.json()
# No tags (but can be assumed "all") is always present
assert len(response.json()['tags']) == 2
# One URL loaded
assert len(response.json()['urls']) == 1
assert 'url' in response.json()['urls'][0]
assert 'tags' in response.json()['urls'][0]
assert len(response.json()['urls'][0]['tags']) == 2
# Handle case when we try to retrieve our content but we have no idea
# what the format is in. Essentialy there had to have been disk
# corruption here or someone meddling with the backend.
with patch('tempfile._TemporaryFileWrapper') as mock_ntf:
mock_ntf.side_effect = OSError()
# Now retrieve our JSON resonse
response = self.client.get('/json/urls/{}'.format(key))
assert response.status_code == 500
assert response['Content-Type'].startswith('application/json')
assert 'tags' in response.json()
assert 'urls' in response.json()
# has error directive
assert 'error' in response.json()
# entries exist by are empty
assert len(response.json()['tags']) == 0
assert len(response.json()['urls']) == 0
# Verify that the correct Content-Type is set in the header of the
# response
assert 'Content-Type' in response
assert response['Content-Type'].startswith('application/json')

View File

@ -47,4 +47,7 @@ urlpatterns = [
re_path(
r'^notify/?',
views.StatelessNotifyView.as_view(), name='s_notify'),
re_path(
r'^json/urls/(?P<key>[\w_-]{1,64})/?',
views.JsonUrlView.as_view(), name='json_urls'),
]

View File

@ -24,12 +24,15 @@
# THE SOFTWARE.
from django.shortcuts import render
from django.http import HttpResponse
from django.http import JsonResponse
from django.views import View
from django.conf import settings
from django.utils.decorators import method_decorator
from django.views.decorators.cache import never_cache
from django.views.decorators.gzip import gzip_page
from django.utils.translation import gettext_lazy as _
from django.core.serializers.json import DjangoJSONEncoder
from .utils import ConfigCache
from .forms import AddByUrlForm
from .forms import AddByConfigForm
@ -57,6 +60,17 @@ MIME_IS_JSON = re.compile(
r'(text|application)/(x-)?json', re.I)
class JSONEncoder(DjangoJSONEncoder):
"""
A wrapper to the DjangoJSONEncoder to support
sets() (converting them to lists).
"""
def default(self, obj):
if isinstance(obj, set):
return list(obj)
return super().default(obj)
class ResponseCode(object):
"""
These codes are based on those provided by the requests object
@ -108,9 +122,6 @@ class AddView(View):
"""
Handle a POST request
"""
# Our default response type
content_type = 'text/plain; charset=utf-8'
# our content
content = {}
if MIME_IS_FORM.match(request.content_type):
@ -133,13 +144,11 @@ class AddView(View):
# 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
@ -151,8 +160,8 @@ class AddView(View):
# No URLs were loaded
return HttpResponse(
_('No valid URLs were found.'),
content_type=content_type,
status=ResponseCode.bad_request)
status=ResponseCode.bad_request,
)
if not ConfigCache.put(
key, '\r\n'.join([s.url() for s in a_obj]),
@ -160,7 +169,6 @@ class AddView(View):
return HttpResponse(
_('The configuration could not be saved.'),
content_type=content_type,
status=ResponseCode.internal_server_error,
)
@ -170,8 +178,8 @@ class AddView(View):
# Format must be one supported by apprise
return HttpResponse(
_('The format specified is invalid.'),
content_type=content_type,
status=ResponseCode.bad_request)
status=ResponseCode.bad_request,
)
# prepare our apprise config object
ac_obj = apprise.AppriseConfig()
@ -185,11 +193,12 @@ class AddView(View):
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)
status=ResponseCode.bad_request,
)
# Add our configuration
a_obj.add(ac_obj)
@ -199,34 +208,35 @@ class AddView(View):
# mis-configuration on the caller's part
return HttpResponse(
_('No valid URL(s) were specified.'),
content_type=content_type,
status=ResponseCode.bad_request)
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)
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)
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)
status=ResponseCode.okay,
)
@method_decorator(never_cache, name='dispatch')
@ -238,15 +248,11 @@ class DelView(View):
"""
Handle a POST request
"""
# Our default response type
content_type = 'text/plain; charset=utf-8'
# 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,
)
@ -254,14 +260,12 @@ class DelView(View):
# 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,
)
@ -275,8 +279,6 @@ class GetView(View):
"""
Handle a POST request
"""
# Our default response type
content_type = 'text/plain; charset=utf-8'
config, format = ConfigCache.get(key)
if config is None:
@ -290,28 +292,29 @@ class GetView(View):
# 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; charset=utf-8'
# reference to it through the --config (-c) option in the CLI.
content_type = 'text/yaml; charset=utf-8' \
if format == apprise.ConfigFormat.YAML \
else 'text/html; charset=utf-8'
# Return our retrieved content
return HttpResponse(
config, content_type=content_type, status=ResponseCode.okay)
config,
content_type=content_type,
status=ResponseCode.okay,
)
@method_decorator((gzip_page, never_cache), name='dispatch')
@ -323,9 +326,6 @@ class NotifyView(View):
"""
Handle a POST request
"""
# Our default response type
content_type = 'text/plain; charset=utf-8'
# our content
content = {}
if MIME_IS_FORM.match(request.content_type):
@ -344,14 +344,12 @@ class NotifyView(View):
# 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
@ -361,7 +359,6 @@ class NotifyView(View):
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
@ -378,14 +375,12 @@ class NotifyView(View):
# 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,
)
@ -423,13 +418,13 @@ class NotifyView(View):
# 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)
status=ResponseCode.okay
)
@method_decorator((gzip_page, never_cache), name='dispatch')
@ -441,9 +436,6 @@ class StatelessNotifyView(View):
"""
Handle a POST request
"""
# Our default response type
content_type = 'text/plain; charset=utf-8'
# our content
content = {}
if MIME_IS_FORM.match(request.content_type):
@ -462,14 +454,12 @@ class StatelessNotifyView(View):
# 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)
if not content.get('urls') and settings.APPRISE_STATELESS_URLS:
@ -484,7 +474,6 @@ class StatelessNotifyView(View):
return HttpResponse(
_('An invalid payload was specified.'),
content_type=content_type,
status=ResponseCode.bad_request)
# Prepare our apprise object
@ -495,7 +484,6 @@ class StatelessNotifyView(View):
if not len(a_obj):
return HttpResponse(
_('There was no services to notify.'),
content_type=content_type,
status=ResponseCode.no_content,
)
@ -510,4 +498,112 @@ class StatelessNotifyView(View):
# Return our retrieved content
return HttpResponse(
_('Notification(s) sent.'),
content_type=content_type, status=ResponseCode.okay)
status=ResponseCode.okay,
)
@method_decorator((gzip_page, never_cache), name='dispatch')
class JsonUrlView(View):
"""
A Django view that lists all loaded tags and URLs for a given key
"""
def get(self, request, key):
"""
Handle a POST request
"""
# Now build our tag response that identifies all of the tags
# and the URL's they're associated with
# {
# "tags": ["tag1', "tag2", "tag3"],
# "urls": [
# {
# "url": "windows://",
# "tags": [],
# },
# {
# "url": "mailto://user:pass@gmail.com"
# "tags": ["tag1", "tag2", "tag3"]
# }
# ]
# }
response = {
'tags': set(),
'urls': [],
}
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 JsonResponse(
response,
encoder=JSONEncoder,
safe=False,
status=ResponseCode.no_content,
)
# Something went very wrong; return 500
response['error'] = _('There was no configuration found.')
return JsonResponse(
response,
encoder=JSONEncoder,
safe=False,
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)
for notification in a_obj:
# Set Notification
response['urls'].append({
'url': notification.url(privacy=False),
'tags': notification.tags,
})
# Store Tags
response['tags'] |= notification.tags
except OSError:
# We could not write the temporary file to disk
response['error'] = _('The configuration could not be loaded.'),
return JsonResponse(
response,
encoder=JSONEncoder,
safe=False,
status=ResponseCode.internal_server_error,
)
# Return our retrieved content
return JsonResponse(
response,
encoder=JSONEncoder,
safe=False,
status=ResponseCode.okay
)