feat(api): load config using shared helpers
- add django settings module documentation - use default for previously required fields BREAKING CHANGE: The API command line interface require the configuration file to be present. The default configuration file path is `/etc/airtime/airtime.conf`
This commit is contained in:
parent
9af717ef7f
commit
2dcc654b70
|
@ -192,6 +192,7 @@ jobs:
|
||||||
postgres:
|
postgres:
|
||||||
image: postgres
|
image: postgres
|
||||||
env:
|
env:
|
||||||
|
POSTGRES_USER: libretime
|
||||||
POSTGRES_PASSWORD: libretime
|
POSTGRES_PASSWORD: libretime
|
||||||
options: >-
|
options: >-
|
||||||
--health-cmd pg_isready
|
--health-cmd pg_isready
|
||||||
|
@ -204,7 +205,8 @@ jobs:
|
||||||
run:
|
run:
|
||||||
shell: bash
|
shell: bash
|
||||||
env:
|
env:
|
||||||
LIBRETIME_CONFIG_FILEPATH: /tmp/libretime-test.conf
|
LIBRETIME_GENERAL_API_KEY: test_key
|
||||||
|
LIBRETIME_DATABASE_HOST: postgres
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
|
@ -216,20 +218,6 @@ jobs:
|
||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-pip-${{ matrix.context }}
|
${{ runner.os }}-pip-${{ matrix.context }}
|
||||||
|
|
||||||
- name: Setup libretime configuration
|
|
||||||
run: |
|
|
||||||
cat <<EOF > $LIBRETIME_CONFIG_FILEPATH
|
|
||||||
[general]
|
|
||||||
api_key = test_key
|
|
||||||
[database]
|
|
||||||
host = postgres
|
|
||||||
port = 5432
|
|
||||||
name = libretime
|
|
||||||
user = postgres
|
|
||||||
password = libretime
|
|
||||||
EOF
|
|
||||||
cat $LIBRETIME_CONFIG_FILEPATH
|
|
||||||
|
|
||||||
- name: Test
|
- name: Test
|
||||||
run: make test
|
run: make test
|
||||||
working-directory: ${{ matrix.context }}
|
working-directory: ${{ matrix.context }}
|
||||||
|
|
|
@ -7,6 +7,7 @@ NotifyAccess=all
|
||||||
KillSignal=SIGQUIT
|
KillSignal=SIGQUIT
|
||||||
|
|
||||||
Environment=LIBRETIME_LOG_FILEPATH=/var/log/libretime/api.log
|
Environment=LIBRETIME_LOG_FILEPATH=/var/log/libretime/api.log
|
||||||
|
Environment=LIBRETIME_CONFIG_FILEPATH=/etc/airtime/airtime.conf
|
||||||
|
|
||||||
ExecStart=/usr/bin/uwsgi /etc/airtime/libretime-api.ini
|
ExecStart=/usr/bin/uwsgi /etc/airtime/libretime-api.ini
|
||||||
User=libretime-api
|
User=libretime-api
|
||||||
|
|
|
@ -7,6 +7,7 @@ import sys
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "libretime_api.settings")
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "libretime_api.settings")
|
||||||
|
os.environ.setdefault("LIBRETIME_CONFIG_FILEPATH", "/etc/airtime/airtime.conf")
|
||||||
try:
|
try:
|
||||||
from django.core.management import execute_from_command_line
|
from django.core.management import execute_from_command_line
|
||||||
except ImportError as exc:
|
except ImportError as exc:
|
||||||
|
|
|
@ -55,7 +55,7 @@ def check_authorization_header(request):
|
||||||
|
|
||||||
if auth_header.startswith("Api-Key"):
|
if auth_header.startswith("Api-Key"):
|
||||||
token = auth_header.split()[1]
|
token = auth_header.split()[1]
|
||||||
if token == settings.CONFIG.get("general", "api_key"):
|
if token == settings.CONFIG.general.api_key:
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
# Django settings
|
||||||
|
|
||||||
|
The structure of the django settings module is the following:
|
||||||
|
|
||||||
|
- the `__init__.py` (`libretime_api.settings`) module is the django settings entrypoint. The module contains bindings between the user configuration and the django settings. **Advanced users** may edit this file to better integrate the LibreTime API in their setup.
|
||||||
|
- the `_internal.py` module contains application settings for django.
|
||||||
|
- the `_schema.py` module contains the schema for the user configuration parsing and validation.
|
|
@ -0,0 +1,46 @@
|
||||||
|
# pylint: disable=unused-import
|
||||||
|
from os import getenv
|
||||||
|
|
||||||
|
from ._internal import (
|
||||||
|
AUTH_PASSWORD_VALIDATORS,
|
||||||
|
AUTH_USER_MODEL,
|
||||||
|
DEBUG,
|
||||||
|
INSTALLED_APPS,
|
||||||
|
MIDDLEWARE,
|
||||||
|
REST_FRAMEWORK,
|
||||||
|
ROOT_URLCONF,
|
||||||
|
TEMPLATES,
|
||||||
|
TEST_RUNNER,
|
||||||
|
WSGI_APPLICATION,
|
||||||
|
setup_logger,
|
||||||
|
)
|
||||||
|
from ._schema import Config
|
||||||
|
|
||||||
|
API_VERSION = "2.0.0"
|
||||||
|
|
||||||
|
LIBRETIME_LOG_FILEPATH = getenv("LIBRETIME_LOG_FILEPATH")
|
||||||
|
LIBRETIME_CONFIG_FILEPATH = getenv("LIBRETIME_CONFIG_FILEPATH")
|
||||||
|
|
||||||
|
CONFIG = Config(filepath=LIBRETIME_CONFIG_FILEPATH)
|
||||||
|
|
||||||
|
SECRET_KEY = CONFIG.general.api_key
|
||||||
|
ALLOWED_HOSTS = ["*"]
|
||||||
|
|
||||||
|
DATABASES = {
|
||||||
|
"default": {
|
||||||
|
"ENGINE": "django.db.backends.postgresql",
|
||||||
|
"HOST": CONFIG.database.host,
|
||||||
|
"PORT": CONFIG.database.port,
|
||||||
|
"NAME": CONFIG.database.name,
|
||||||
|
"USER": CONFIG.database.user,
|
||||||
|
"PASSWORD": CONFIG.database.password,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LANGUAGE_CODE = "en-us"
|
||||||
|
TIME_ZONE = "UTC"
|
||||||
|
USE_I18N = True
|
||||||
|
USE_L10N = True
|
||||||
|
USE_TZ = True
|
||||||
|
|
||||||
|
LOGGING = setup_logger(LIBRETIME_LOG_FILEPATH)
|
|
@ -1,32 +1,8 @@
|
||||||
import os
|
from os import getenv
|
||||||
|
from typing import Optional
|
||||||
from .utils import get_random_string, read_config_file
|
|
||||||
|
|
||||||
API_VERSION = "2.0.0"
|
|
||||||
|
|
||||||
LIBRETIME_LOG_FILEPATH = os.getenv("LIBRETIME_LOG_FILEPATH")
|
|
||||||
LIBRETIME_CONFIG_FILEPATH = os.getenv(
|
|
||||||
"LIBRETIME_CONFIG_FILEPATH",
|
|
||||||
"/etc/airtime/airtime.conf",
|
|
||||||
)
|
|
||||||
LIBRETIME_STATIC_ROOT = os.getenv(
|
|
||||||
"LIBRETIME_STATIC_ROOT",
|
|
||||||
"/usr/share/airtime/api",
|
|
||||||
)
|
|
||||||
CONFIG = read_config_file(LIBRETIME_CONFIG_FILEPATH)
|
|
||||||
|
|
||||||
|
|
||||||
# Quick-start development settings - unsuitable for production
|
|
||||||
# See https://docs.djangoproject.com/en/3.0/howto/deployment/checklist/
|
|
||||||
|
|
||||||
# SECURITY WARNING: keep the secret key used in production secret!
|
|
||||||
SECRET_KEY = get_random_string(CONFIG.get("general", "api_key", fallback=""))
|
|
||||||
|
|
||||||
# SECURITY WARNING: don't run with debug turned on in production!
|
# SECURITY WARNING: don't run with debug turned on in production!
|
||||||
DEBUG = os.getenv("LIBRETIME_DEBUG", False)
|
DEBUG = getenv("LIBRETIME_DEBUG")
|
||||||
|
|
||||||
ALLOWED_HOSTS = ["*"]
|
|
||||||
|
|
||||||
|
|
||||||
# Application definition
|
# Application definition
|
||||||
|
|
||||||
|
@ -72,22 +48,6 @@ TEMPLATES = [
|
||||||
|
|
||||||
WSGI_APPLICATION = "libretime_api.wsgi.application"
|
WSGI_APPLICATION = "libretime_api.wsgi.application"
|
||||||
|
|
||||||
|
|
||||||
# Database
|
|
||||||
# https://docs.djangoproject.com/en/3.0/ref/settings/#databases
|
|
||||||
|
|
||||||
DATABASES = {
|
|
||||||
"default": {
|
|
||||||
"ENGINE": "django.db.backends.postgresql",
|
|
||||||
"NAME": CONFIG.get("database", "name", fallback="libretime"),
|
|
||||||
"USER": CONFIG.get("database", "user", fallback="libretime"),
|
|
||||||
"PASSWORD": CONFIG.get("database", "password", fallback="libretime"),
|
|
||||||
"HOST": CONFIG.get("database", "host", fallback="localhost"),
|
|
||||||
"PORT": CONFIG.get("database", "port", fallback="5432"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# Password validation
|
# Password validation
|
||||||
# https://docs.djangoproject.com/en/3.0/ref/settings/#auth-password-validators
|
# https://docs.djangoproject.com/en/3.0/ref/settings/#auth-password-validators
|
||||||
|
|
||||||
|
@ -129,66 +89,53 @@ REST_FRAMEWORK = {
|
||||||
"URL_FIELD_NAME": "item_url",
|
"URL_FIELD_NAME": "item_url",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
# Internationalization
|
|
||||||
# https://docs.djangoproject.com/en/3.0/topics/i18n/
|
|
||||||
|
|
||||||
LANGUAGE_CODE = "en-us"
|
|
||||||
|
|
||||||
TIME_ZONE = "UTC"
|
|
||||||
|
|
||||||
USE_I18N = True
|
|
||||||
|
|
||||||
USE_L10N = True
|
|
||||||
|
|
||||||
USE_TZ = True
|
|
||||||
|
|
||||||
|
|
||||||
AUTH_USER_MODEL = "libretime_api.User"
|
AUTH_USER_MODEL = "libretime_api.User"
|
||||||
|
|
||||||
TEST_RUNNER = "libretime_api.tests.runners.ManagedModelTestRunner"
|
TEST_RUNNER = "libretime_api.tests.runners.ManagedModelTestRunner"
|
||||||
|
|
||||||
|
|
||||||
LOGGING_HANDLERS = {
|
# Logging
|
||||||
"console": {
|
def setup_logger(log_filepath: Optional[str]):
|
||||||
"level": "INFO",
|
logging_handlers = {
|
||||||
"class": "logging.StreamHandler",
|
"console": {
|
||||||
"formatter": "simple",
|
"level": "INFO",
|
||||||
},
|
"class": "logging.StreamHandler",
|
||||||
}
|
"formatter": "simple",
|
||||||
|
},
|
||||||
if LIBRETIME_LOG_FILEPATH is not None:
|
|
||||||
LOGGING_HANDLERS["file"] = {
|
|
||||||
"level": "DEBUG",
|
|
||||||
"class": "logging.FileHandler",
|
|
||||||
"filename": LIBRETIME_LOG_FILEPATH,
|
|
||||||
"formatter": "verbose",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
LOGGING = {
|
if log_filepath is not None:
|
||||||
"version": 1,
|
logging_handlers["file"] = {
|
||||||
"disable_existing_loggers": False,
|
"level": "DEBUG",
|
||||||
"formatters": {
|
"class": "logging.FileHandler",
|
||||||
"simple": {
|
"filename": log_filepath,
|
||||||
"format": "{levelname} {message}",
|
"formatter": "verbose",
|
||||||
"style": "{",
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"version": 1,
|
||||||
|
"disable_existing_loggers": False,
|
||||||
|
"formatters": {
|
||||||
|
"simple": {
|
||||||
|
"format": "{levelname} {message}",
|
||||||
|
"style": "{",
|
||||||
|
},
|
||||||
|
"verbose": {
|
||||||
|
"format": "{asctime} {module} {levelname} {message}",
|
||||||
|
"style": "{",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"verbose": {
|
"handlers": logging_handlers,
|
||||||
"format": "{asctime} {module} {levelname} {message}",
|
"loggers": {
|
||||||
"style": "{",
|
"django": {
|
||||||
|
"handlers": logging_handlers.keys(),
|
||||||
|
"level": "INFO",
|
||||||
|
"propagate": True,
|
||||||
|
},
|
||||||
|
"libretime_api": {
|
||||||
|
"handlers": logging_handlers.keys(),
|
||||||
|
"level": "INFO",
|
||||||
|
"propagate": True,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
}
|
||||||
"handlers": LOGGING_HANDLERS,
|
|
||||||
"loggers": {
|
|
||||||
"django": {
|
|
||||||
"handlers": LOGGING_HANDLERS.keys(),
|
|
||||||
"level": "INFO",
|
|
||||||
"propagate": True,
|
|
||||||
},
|
|
||||||
"libretime_api": {
|
|
||||||
"handlers": LOGGING_HANDLERS.keys(),
|
|
||||||
"level": "INFO",
|
|
||||||
"propagate": True,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
from libretime_shared.config import (
|
||||||
|
BaseConfig,
|
||||||
|
DatabaseConfig,
|
||||||
|
GeneralConfig,
|
||||||
|
RabbitMQConfig,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Config(BaseConfig):
|
||||||
|
general: GeneralConfig
|
||||||
|
database: DatabaseConfig = DatabaseConfig()
|
||||||
|
rabbitmq: RabbitMQConfig = RabbitMQConfig()
|
|
@ -33,7 +33,7 @@ class TestIsSystemTokenOrUser(APITestCase):
|
||||||
self.assertFalse(allowed)
|
self.assertFalse(allowed)
|
||||||
|
|
||||||
def test_token_correct(self):
|
def test_token_correct(self):
|
||||||
token = settings.CONFIG.get("general", "api_key")
|
token = settings.CONFIG.general.api_key
|
||||||
request = APIRequestFactory().get(self.path)
|
request = APIRequestFactory().get(self.path)
|
||||||
request.user = AnonymousUser()
|
request.user = AnonymousUser()
|
||||||
request.META["Authorization"] = f"Api-Key {token}"
|
request.META["Authorization"] = f"Api-Key {token}"
|
||||||
|
|
|
@ -14,7 +14,7 @@ class TestFileViewSet(APITestCase):
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpTestData(cls):
|
def setUpTestData(cls):
|
||||||
cls.path = "/api/v2/files/{id}/download/"
|
cls.path = "/api/v2/files/{id}/download/"
|
||||||
cls.token = settings.CONFIG.get("general", "api_key")
|
cls.token = settings.CONFIG.general.api_key
|
||||||
|
|
||||||
def test_invalid(self):
|
def test_invalid(self):
|
||||||
path = self.path.format(id="a")
|
path = self.path.format(id="a")
|
||||||
|
@ -49,7 +49,7 @@ class TestScheduleViewSet(APITestCase):
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpTestData(cls):
|
def setUpTestData(cls):
|
||||||
cls.path = "/api/v2/schedule/"
|
cls.path = "/api/v2/schedule/"
|
||||||
cls.token = settings.CONFIG.get("general", "api_key")
|
cls.token = settings.CONFIG.general.api_key
|
||||||
|
|
||||||
def test_schedule_item_full_length(self):
|
def test_schedule_item_full_length(self):
|
||||||
music_dir = baker.make(
|
music_dir = baker.make(
|
||||||
|
|
|
@ -1,30 +0,0 @@
|
||||||
import random
|
|
||||||
import string
|
|
||||||
import sys
|
|
||||||
from configparser import ConfigParser
|
|
||||||
|
|
||||||
|
|
||||||
def read_config_file(config_filepath):
|
|
||||||
"""Parse the application's config file located at config_path."""
|
|
||||||
config = ConfigParser()
|
|
||||||
try:
|
|
||||||
with open(config_filepath, encoding="utf-8") as config_file:
|
|
||||||
config.read_file(config_file)
|
|
||||||
except OSError as error:
|
|
||||||
print(
|
|
||||||
f"Unable to read config file at {config_filepath}: {error.strerror}",
|
|
||||||
file=sys.stderr,
|
|
||||||
)
|
|
||||||
return ConfigParser()
|
|
||||||
except Exception as error:
|
|
||||||
print(error, file=sys.stderr)
|
|
||||||
raise error
|
|
||||||
return config
|
|
||||||
|
|
||||||
|
|
||||||
def get_random_string(seed):
|
|
||||||
"""Generates a random string based on the given seed"""
|
|
||||||
choices = string.ascii_letters + string.digits + string.punctuation
|
|
||||||
seed = seed.encode("utf-8")
|
|
||||||
rand = random.Random(seed)
|
|
||||||
return [rand.choice(choices) for i in range(16)]
|
|
|
@ -43,6 +43,7 @@ setup(
|
||||||
"dev": [
|
"dev": [
|
||||||
"model_bakery",
|
"model_bakery",
|
||||||
"psycopg2-binary",
|
"psycopg2-binary",
|
||||||
|
f"libretime-shared @ file://localhost/{here.parent / 'shared'}#egg=libretime_shared",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
Loading…
Reference in New Issue