#88 #89 : use pytest + reorganise settings (#92)

- !88 pytest on existing tests
- !89 reorganise settings (! see notes for deployment)

Co-authored-by: bkfox <thomas bkfox net>
Reviewed-on: rc/aircox#92
This commit is contained in:
Thomas Kairos 2023-03-28 14:40:49 +02:00
parent 4bebc56a28
commit 0e183099ed
31 changed files with 511 additions and 368 deletions

View File

@ -12,17 +12,11 @@ repos:
args:
- --line-length=79
- --exclude="""\.git|\.__pycache__|venv|_build|buck-out|build|dist"""
- repo: https://github.com/PyCQA/autoflake.git
rev: v2.0.2
hooks:
- id: autoflake
args:
- --remove-all-unused-imports
- repo: https://github.com/PyCQA/flake8.git
rev: 6.0.0
hooks:
- id: flake8
exclude: instance/sample_settings.py
exclude: instance/settings/
- repo: https://github.com/PyCQA/docformatter.git
rev: v1.5.1
hooks:

180
aircox/conf.py Executable file
View File

@ -0,0 +1,180 @@
import os
import inspect
from django.conf import settings as d_settings
__all__ = ("Settings", "settings")
# code from django-fox
class BaseSettings:
"""Utility class used to load and save settings, can be used as model.
Some members are excluded from being configuration:
- Protected/private members;
- On django model, "objects" and "Meta";
- Class declaration and callables
Example:
```
class MySettings(Settings):
a = 13
b = 12
my_settings = MySettings().load('MY_SETTINGS_KEY')
print(my_settings.a, my_settings.get('b'))
```
This will load values from django project settings.
"""
def __init__(self, key, module=None):
self.load(key, module)
def load(self, key, module=None):
"""Load settings from module's item specified by its member name. When
no module is provided, uses ``django.conf.settings``.
:param str key: module member name.
:param module: configuration object.
:returns self
"""
if module is None:
module = d_settings
settings = getattr(module, key, None)
if settings:
self.update(settings)
return self
def update(self, settings):
"""Update self's values from provided settings. ``settings`` can be an
iterable of ``(key, value)``.
:param dict|Settings|iterable settings: value to update from.
"""
if isinstance(settings, (dict, Settings)):
settings = settings.items()
for key, value in settings:
if self.is_config_item(key, value):
setattr(self, key, value)
def get(self, key, default=None):
"""Return settings' value for provided key."""
return getattr(self, key, default)
def items(self):
"""Iterate over items members, as tupple of ``key, value``."""
for key in dir(self):
value = getattr(self, key)
if self.is_config_item(key, value):
yield key, value
def is_config_item(self, key, value):
"""Return True if key/value item is a configuration setting."""
if key.startswith("_") or callable(value) or inspect.isclass(value):
return False
return True
class Settings(BaseSettings):
# --- Global & misc
DEFAULT_USER_GROUPS = {
"radio hosts": (
# TODO include content_type in order to avoid clash with potential
# extra applications
# aircox
"change_program",
"change_episode",
"change_diffusion",
"add_comment",
"change_comment",
"delete_comment",
"add_article",
"change_article",
"delete_article",
"change_sound",
"add_track",
"change_track",
"delete_track",
# taggit
"add_tag",
"change_tag",
"delete_tag",
# filer
"add_folder",
"change_folder",
"delete_folder",
"can_use_directory_listing",
"add_image",
"change_image",
"delete_image",
),
}
"""Groups to assign to users at their creation, along with the permissions
to add to each group."""
PROGRAMS_DIR = "programs"
"""Directory for the programs data."""
@property
def PROGRAMS_DIR_ABS(self):
return os.path.join(d_settings.MEDIA_ROOT, self.PROGRAMS_DIR)
# --- Programs & episodes
EPISODE_TITLE = "{program.title} - {date}"
"""Default title for episodes."""
EPISODE_TITLE_DATE_FORMAT = "%-d %B %Y"
"""Date format in episode title (python's strftime)"""
# --- Logs & archives
LOGS_ARCHIVES_DIR = "logs/archives"
"""Directory where to save logs' archives."""
@property
def LOGS_ARCHIVES_DIR_ABS(self):
return os.path.join(d_settings.PROJECT_ROOT, self.LOGS_ARCHIVES_DIR)
LOGS_ARCHIVES_AGE = 60
"""In days, minimal age of a log before it is archived."""
# --- Sounds
SOUND_ARCHIVES_SUBDIR = "archives"
"""Sub directory used for the complete episode sounds."""
SOUND_EXCERPTS_SUBDIR = "excerpts"
"""Sub directory used for the excerpts of the episode."""
SOUND_QUALITY = {
"attribute": "RMS lev dB",
"range": (-18.0, -8.0),
"sample_length": 120,
}
"""Quality attributes passed to sound_quality_check from sounds_monitor
(Soxi parameters)."""
SOUND_FILE_EXT = (".ogg", ".flac", ".wav", ".mp3", ".opus")
"""Extension of sound files."""
SOUND_KEEP_DELETED = False
"""Tag sounds as deleted instead of deleting them when file has been
removed from filesystem (sound monitoring)."""
# --- Streamer & Controllers
CONTROLLERS_WORKING_DIR = "/tmp/aircox"
"""Controllers working directory."""
# --- Playlist import from CSV
IMPORT_PLAYLIST_CSV_COLS = (
"artist",
"title",
"minutes",
"seconds",
"tags",
"info",
)
"""Columns for CSV file."""
IMPORT_PLAYLIST_CSV_DELIMITER = ";"
"""Column delimiter of csv text files."""
IMPORT_PLAYLIST_CSV_TEXT_QUOTE = '"'
"""Text delimiter of csv text files."""
settings = Settings("AIRCOX")

View File

@ -9,7 +9,7 @@ from argparse import RawTextHelpFormatter
from django.core.management.base import BaseCommand
from django.utils import timezone as tz
import aircox.settings as settings
from aircox.conf import settings
from aircox.models import Log
from aircox.models.log import LogArchiver
@ -29,9 +29,9 @@ class Command(BaseCommand):
"-a",
"--age",
type=int,
default=settings.AIRCOX_LOGS_ARCHIVES_AGE,
default=settings.LOGS_ARCHIVES_AGE,
help="minimal age in days of logs to archive. Default is "
"settings.AIRCOX_LOGS_ARCHIVES_AGE",
"settings.LOGS_ARCHIVES_AGE",
)
group.add_argument(
"-k",

View File

@ -2,9 +2,9 @@
sound.
Playlists are in CSV format, where columns are separated with a
'{settings.AIRCOX_IMPORT_PLAYLIST_CSV_DELIMITER}'. Text quote is
{settings.AIRCOX_IMPORT_PLAYLIST_CSV_TEXT_QUOTE}.
The order of the elements is: {settings.AIRCOX_IMPORT_PLAYLIST_CSV_COLS}
'{settings.IMPORT_PLAYLIST_CSV_DELIMITER}'. Text quote is
{settings.IMPORT_PLAYLIST_CSV_TEXT_QUOTE}.
The order of the elements is: {settings.IMPORT_PLAYLIST_CSV_COLS}
If 'minutes' or 'seconds' are given, position will be expressed as timed
position, instead of position in playlist.
@ -16,7 +16,7 @@ from argparse import RawTextHelpFormatter
from django.core.management.base import BaseCommand
from aircox import settings
from aircox.conf import settings
from aircox.models import Sound, Track
__doc__ = __doc__.format(settings=settings)
@ -61,9 +61,9 @@ class PlaylistImport:
)
and row.strip()
),
fieldnames=settings.AIRCOX_IMPORT_PLAYLIST_CSV_COLS,
delimiter=settings.AIRCOX_IMPORT_PLAYLIST_CSV_DELIMITER,
quotechar=settings.AIRCOX_IMPORT_PLAYLIST_CSV_TEXT_QUOTE,
fieldnames=settings.IMPORT_PLAYLIST_CSV_COLS,
delimiter=settings.IMPORT_PLAYLIST_CSV_DELIMITER,
quotechar=settings.IMPORT_PLAYLIST_CSV_TEXT_QUOTE,
)
)
@ -80,7 +80,7 @@ class PlaylistImport:
)
return
maps = settings.AIRCOX_IMPORT_PLAYLIST_CSV_COLS
maps = settings.IMPORT_PLAYLIST_CSV_COLS
tracks = []
logger.info("parse csv file " + self.path)

View File

@ -20,7 +20,7 @@ Where:
To check quality of files, call the command sound_quality_check using the
parameters given by the setting AIRCOX_SOUND_QUALITY. This script requires
parameters given by the setting SOUND_QUALITY. This script requires
Sox (and soxi).
"""
import atexit
@ -33,7 +33,7 @@ from argparse import RawTextHelpFormatter
from django.core.management.base import BaseCommand
from watchdog.observers import Observer
from aircox import settings
from aircox.conf import settings
from aircox.management.sound_file import SoundFile
from aircox.management.sound_monitor import MonitorHandler
from aircox.models import Program, Sound
@ -67,12 +67,12 @@ class Command(BaseCommand):
logger.info("#%d %s", program.id, program.title)
self.scan_for_program(
program,
settings.AIRCOX_SOUND_ARCHIVES_SUBDIR,
settings.SOUND_ARCHIVES_SUBDIR,
type=Sound.TYPE_ARCHIVE,
)
self.scan_for_program(
program,
settings.AIRCOX_SOUND_EXCERPTS_SUBDIR,
settings.SOUND_EXCERPTS_SUBDIR,
type=Sound.TYPE_EXCERPT,
)
dirs.append(os.path.join(program.abspath))
@ -91,7 +91,7 @@ class Command(BaseCommand):
# sounds in directory
for path in os.listdir(subdir):
path = os.path.join(subdir, path)
if not path.endswith(settings.AIRCOX_SOUND_FILE_EXT):
if not path.endswith(settings.SOUND_FILE_EXT):
continue
sound_file = SoundFile(path)
@ -115,12 +115,12 @@ class Command(BaseCommand):
"""Run in monitor mode."""
with futures.ThreadPoolExecutor() as pool:
archives_handler = MonitorHandler(
settings.AIRCOX_SOUND_ARCHIVES_SUBDIR,
settings.SOUND_ARCHIVES_SUBDIR,
pool,
type=Sound.TYPE_ARCHIVE,
)
excerpts_handler = MonitorHandler(
settings.AIRCOX_SOUND_EXCERPTS_SUBDIR,
settings.SOUND_EXCERPTS_SUBDIR,
pool,
type=Sound.TYPE_EXCERPT,
)
@ -128,12 +128,12 @@ class Command(BaseCommand):
observer = Observer()
observer.schedule(
archives_handler,
settings.AIRCOX_PROGRAMS_DIR_ABS,
settings.PROGRAMS_DIR_ABS,
recursive=True,
)
observer.schedule(
excerpts_handler,
settings.AIRCOX_PROGRAMS_DIR_ABS,
settings.PROGRAMS_DIR_ABS,
recursive=True,
)
observer.start()

View File

@ -17,7 +17,7 @@ Where:
Sound Quality
=============
To check quality of files, call the command sound_quality_check using the
parameters given by the setting AIRCOX_SOUND_QUALITY. This script requires
parameters given by the setting SOUND_QUALITY. This script requires
Sox (and soxi).
"""
import logging
@ -175,7 +175,7 @@ class SoundFile:
at = tz.datetime(
year, month, day, pi.get("hour", 0), pi.get("minute", 0)
)
at = tz.get_current_timezone().localize(at)
at = tz.make_aware(at)
else:
at = date(year, month, day)

View File

@ -20,7 +20,7 @@ Where:
To check quality of files, call the command sound_quality_check using the
parameters given by the setting AIRCOX_SOUND_QUALITY. This script requires
parameters given by the setting SOUND_QUALITY. This script requires
Sox (and soxi).
"""
import logging
@ -29,7 +29,7 @@ from datetime import datetime, timedelta
from watchdog.events import PatternMatchingEventHandler
from aircox import settings
from aircox.conf import settings
from aircox.models import Sound
from .sound_file import SoundFile
@ -121,7 +121,7 @@ class MonitorHandler(PatternMatchingEventHandler):
def __init__(self, subdir, pool, **sync_kw):
"""
:param str subdir: sub-directory in program dirs to monitor \
(AIRCOX_SOUND_ARCHIVES_SUBDIR or AIRCOX_SOUND_EXCERPTS_SUBDIR);
(SOUND_ARCHIVES_SUBDIR or SOUND_EXCERPTS_SUBDIR);
:param concurrent.futures.Executor pool: pool executing jobs on file
change;
:param **sync_kw: kwargs passed to `SoundFile.sync`;
@ -132,7 +132,7 @@ class MonitorHandler(PatternMatchingEventHandler):
patterns = [
"*/{}/*{}".format(self.subdir, ext)
for ext in settings.AIRCOX_SOUND_FILE_EXT
for ext in settings.SOUND_FILE_EXT
]
super().__init__(patterns=patterns, ignore_directories=True)

View File

@ -1,4 +1,3 @@
from . import signals
from .article import Article
from .episode import Diffusion, DiffusionQuerySet, Episode
from .log import Log, LogArchiver, LogQuerySet

View File

@ -7,7 +7,8 @@ from django.utils.functional import cached_property
from django.utils.translation import gettext_lazy as _
from easy_thumbnails.files import get_thumbnailer
from aircox import settings, utils
from aircox.conf import settings
from aircox import utils
from .page import Page
from .program import (
@ -70,18 +71,18 @@ class Episode(Page):
@classmethod
def get_default_title(cls, page, date):
return settings.AIRCOX_EPISODE_TITLE.format(
return settings.EPISODE_TITLE.format(
program=page,
date=date.strftime(settings.AIRCOX_EPISODE_TITLE_DATE_FORMAT),
date=date.strftime(settings.EPISODE_TITLE_DATE_FORMAT),
)
@classmethod
def get_init_kwargs_from(cls, page, date, title=None, **kwargs):
"""Get default Episode's title."""
title = (
settings.AIRCOX_EPISODE_TITLE.format(
settings.EPISODE_TITLE.format(
program=page,
date=date.strftime(settings.AIRCOX_EPISODE_TITLE_DATE_FORMAT),
date=date.strftime(settings.EPISODE_TITLE_DATE_FORMAT),
)
if title is None
else title

View File

@ -10,8 +10,9 @@ from django.utils import timezone as tz
from django.utils.functional import cached_property
from django.utils.translation import gettext_lazy as _
from aircox import settings
from aircox.conf import settings
__all__ = ("Settings", "settings")
from .episode import Diffusion
from .sound import Sound, Track
from .station import Station
@ -251,7 +252,7 @@ class LogArchiver:
@staticmethod
def get_path(station, date):
return os.path.join(
settings.AIRCOX_LOGS_ARCHIVES_DIR,
settings.LOGS_ARCHIVES_DIR_ABS,
"{}_{}.log.gz".format(date.strftime("%Y%m%d"), station.pk),
)
@ -264,7 +265,7 @@ class LogArchiver:
if not qs.exists():
return 0
os.makedirs(settings.AIRCOX_LOGS_ARCHIVES_DIR, exist_ok=True)
os.makedirs(settings.LOGS_ARCHIVES_DIR_ABS, exist_ok=True)
count = qs.count()
logs = self.sort_logs(qs)

View File

@ -15,7 +15,8 @@ from django.utils import timezone as tz
from django.utils.functional import cached_property
from django.utils.translation import gettext_lazy as _
from aircox import settings, utils
from aircox import utils
from aircox.conf import settings
from .page import Page, PageQuerySet
from .station import Station
@ -77,9 +78,7 @@ class Program(Page):
@property
def path(self):
"""Return program's directory path."""
return os.path.join(
settings.AIRCOX_PROGRAMS_DIR, self.slug.replace("-", "_")
)
return os.path.join(settings.PROGRAMS_DIR, self.slug.replace("-", "_"))
@property
def abspath(self):
@ -88,11 +87,11 @@ class Program(Page):
@property
def archives_path(self):
return os.path.join(self.path, settings.AIRCOX_SOUND_ARCHIVES_SUBDIR)
return os.path.join(self.path, settings.SOUND_ARCHIVES_SUBDIR)
@property
def excerpts_path(self):
return os.path.join(self.path, settings.AIRCOX_SOUND_ARCHIVES_SUBDIR)
return os.path.join(self.path, settings.SOUND_ARCHIVES_SUBDIR)
def __init__(self, *kargs, **kwargs):
super().__init__(*kargs, **kwargs)
@ -107,8 +106,8 @@ class Program(Page):
We assume the path has been given in a previous time by this
model (Program.path getter).
"""
if path.startswith(settings.AIRCOX_PROGRAMS_DIR_ABS):
path = path.replace(settings.AIRCOX_PROGRAMS_DIR_ABS, "")
if path.startswith(settings.PROGRAMS_DIR_ABS):
path = path.replace(settings.PROGRAMS_DIR_ABS, "")
while path[0] == "/":
path = path[1:]
path = path[: path.index("/")]

View File

@ -4,7 +4,8 @@ from django.db.models import signals
from django.dispatch import receiver
from django.utils import timezone as tz
from .. import settings, utils
from aircox import utils
from aircox.conf import settings
from . import Diffusion, Episode, Page, Program, Schedule
@ -20,7 +21,7 @@ def user_default_groups(sender, instance, created, *args, **kwargs):
if not created or instance.is_superuser:
return
for group_name, permissions in settings.AIRCOX_DEFAULT_USER_GROUPS.items():
for group_name, permissions in settings.DEFAULT_USER_GROUPS.items():
if instance.groups.filter(name=group_name).count():
continue

View File

@ -8,7 +8,7 @@ from django.utils import timezone as tz
from django.utils.translation import gettext_lazy as _
from taggit.managers import TaggableManager
from aircox import settings
from aircox.conf import settings
from .episode import Episode
from .program import Program
@ -123,9 +123,9 @@ class Sound(models.Model):
def _upload_to(self, filename):
subdir = (
settings.AIRCOX_SOUND_ARCHIVES_SUBDIR
settings.SOUND_ARCHIVES_SUBDIR
if self.type == self.TYPE_ARCHIVE
else settings.AIRCOX_SOUND_EXCERPTS_SUBDIR
else settings.SOUND_EXCERPTS_SUBDIR
)
return os.path.join(self.program.path, subdir, filename)

View File

@ -5,7 +5,7 @@ from django.utils.functional import cached_property
from django.utils.translation import gettext_lazy as _
from filer.fields.image import FilerImageField
from .. import settings
from aircox.conf import settings
__all__ = ("Station", "StationQuerySet", "Port")
@ -92,7 +92,7 @@ class Station(models.Model):
def save(self, make_sources=True, *args, **kwargs):
if not self.path:
self.path = os.path.join(
settings.AIRCOX_CONTROLLERS_WORKING_DIR,
settings.CONTROLLERS_WORKING_DIR,
self.slug.replace("-", "_"),
)

168
aircox/settings.py Executable file → Normal file
View File

@ -1,125 +1,73 @@
import os
import inspect
from django.conf import settings
from django.conf import settings as django_settings
from django.db import models
def ensure(key, default):
value = getattr(settings, key, default)
globals()[key] = value
return value
class Settings:
"""Utility class used to load and save settings, can be used as model.
Some members are excluded from being configuration:
- Protected/private members;
- On django model, "objects" and "Meta";
- Class declaration and callables
########################################################################
# Global & misc
########################################################################
# group to assign to users at their creation, along with the permissions
# to add to each group.
ensure(
"AIRCOX_DEFAULT_USER_GROUPS",
{
"radio hosts": (
# TODO include content_type in order to avoid clash with potential
# extra applications
# aircox
"change_program",
"change_episode",
"change_diffusion",
"add_comment",
"change_comment",
"delete_comment",
"add_article",
"change_article",
"delete_article",
"change_sound",
"add_track",
"change_track",
"delete_track",
# taggit
"add_tag",
"change_tag",
"delete_tag",
# filer
"add_folder",
"change_folder",
"delete_folder",
"can_use_directory_listing",
"add_image",
"change_image",
"delete_image",
),
},
)
Example:
# Directory for the programs data
AIRCOX_PROGRAMS_DIR = ensure("AIRCOX_PROGRAMS_DIR", "programs")
ensure(
"AIRCOX_PROGRAMS_DIR_ABS",
os.path.join(settings.MEDIA_ROOT, AIRCOX_PROGRAMS_DIR),
)
```
class MySettings(Settings):
a = 13
b = 12
my_settings = MySettings().load('MY_SETTINGS_KEY')
print(my_settings.a, my_settings.get('b'))
```
########################################################################
# Programs & Episodes
########################################################################
# default title for episodes
ensure("AIRCOX_EPISODE_TITLE", "{program.title} - {date}")
# date format in episode title (python's strftime)
ensure("AIRCOX_EPISODE_TITLE_DATE_FORMAT", "%-d %B %Y")
This will load values from django project settings.
"""
########################################################################
# Logs & Archives
########################################################################
# Directory where to save logs' archives
ensure(
"AIRCOX_LOGS_ARCHIVES_DIR",
os.path.join(settings.PROJECT_ROOT, "logs/archives"),
)
# In days, minimal age of a log before it is archived
ensure("AIRCOX_LOGS_ARCHIVES_AGE", 60)
def load(self, key, module=None):
"""Load settings from module's item specified by its member name. When
no module is provided, uses ``django.conf.settings``.
:param str key: module member name.
:param module: configuration object.
:returns self
"""
if module is None:
module = django_settings
settings = getattr(module, key, None)
if settings:
self.update(settings)
return self
########################################################################
# Sounds
########################################################################
# Sub directory used for the complete episode sounds
ensure("AIRCOX_SOUND_ARCHIVES_SUBDIR", "archives")
# Sub directory used for the excerpts of the episode
ensure("AIRCOX_SOUND_EXCERPTS_SUBDIR", "excerpts")
def update(self, settings):
"""Update self's values from provided settings. ``settings`` can be an
iterable of ``(key, value)``.
# Quality attributes passed to sound_quality_check from sounds_monitor
ensure(
"AIRCOX_SOUND_QUALITY",
{
"attribute": "RMS lev dB",
"range": (-18.0, -8.0),
"sample_length": 120,
},
)
:param dict|Settings|iterable settings: value to update from.
"""
if isinstance(settings, (dict, Settings)):
settings = settings.items()
for key, value in settings:
if hasattr(self, key) and self.is_config_item(key, value):
setattr(self, key, value)
# Extension of sound files
ensure("AIRCOX_SOUND_FILE_EXT", (".ogg", ".flac", ".wav", ".mp3", ".opus"))
def get(self, key, default=None):
"""Return settings' value for provided key."""
return getattr(self, key, default)
# Tag sounds as deleted instead of deleting them when file has been removed
# from filesystem (sound monitoring)
ensure("AIRCOX_SOUND_KEEP_DELETED", False)
def items(self):
"""Iterate over items members, as tupple of ``key, value``."""
for key in dir(self):
value = getattr(self, key)
if self.is_config_item(key, value):
yield key, value
########################################################################
# Streamer & Controllers
########################################################################
# Controllers working directory
ensure("AIRCOX_CONTROLLERS_WORKING_DIR", "/tmp/aircox")
########################################################################
# Playlist import from CSV
########################################################################
# Columns for CSV file
ensure(
"AIRCOX_IMPORT_PLAYLIST_CSV_COLS",
("artist", "title", "minutes", "seconds", "tags", "info"),
)
# Column delimiter of csv text files
ensure("AIRCOX_IMPORT_PLAYLIST_CSV_DELIMITER", ";")
# Text delimiter of csv text files
ensure("AIRCOX_IMPORT_PLAYLIST_CSV_TEXT_QUOTE", '"')
def is_config_item(self, key, value):
"""Return True if key/value item is a configuration setting."""
if key.startswith("_") or callable(value) or inspect.isclass(value):
return False
if isinstance(self, models.Model) and key == "object":
return False
return True

View File

@ -1,15 +0,0 @@
from .sound_file import SoundFileTestCase
from .sound_monitor import (
ModifiedHandlerTestCase,
MonitorHandlerTestCase,
MoveHandlerTestCase,
NotifyHandlerTestCase,
)
__all__ = (
"SoundFileTestCase",
"NotifyHandlerTestCase",
"MoveHandlerTestCase",
"ModifiedHandlerTestCase",
"MonitorHandlerTestCase",
)

View File

@ -1,103 +0,0 @@
from datetime import timedelta
from django.conf import settings as conf
from django.test import TestCase
from django.utils import timezone as tz
from aircox import models
from aircox.management.sound_file import SoundFile
__all__ = ("SoundFileTestCase",)
class SoundFileTestCase(TestCase):
path_infos = {
"test/20220101_10h13_1_sample_1.mp3": {
"year": 2022,
"month": 1,
"day": 1,
"hour": 10,
"minute": 13,
"n": 1,
"name": "Sample 1",
},
"test/20220102_10h13_sample_2.mp3": {
"year": 2022,
"month": 1,
"day": 2,
"hour": 10,
"minute": 13,
"name": "Sample 2",
},
"test/20220103_1_sample_3.mp3": {
"year": 2022,
"month": 1,
"day": 3,
"n": 1,
"name": "Sample 3",
},
"test/20220104_sample_4.mp3": {
"year": 2022,
"month": 1,
"day": 4,
"name": "Sample 4",
},
"test/20220105.mp3": {
"year": 2022,
"month": 1,
"day": 5,
"name": "20220105",
},
}
subdir_prefix = "test"
sound_files = {
k: r
for k, r in (
(path, SoundFile(conf.MEDIA_ROOT + "/" + path))
for path in path_infos.keys()
)
}
def test_sound_path(self):
for path, sound_file in self.sound_files.items():
self.assertEqual(path, sound_file.sound_path)
def test_read_path(self):
for path, sound_file in self.sound_files.items():
expected = self.path_infos[path]
result = sound_file.read_path(path)
# remove None values
result = {k: v for k, v in result.items() if v is not None}
self.assertEqual(expected, result, "path: {}".format(path))
def _setup_diff(self, program, info):
episode = models.Episode(program=program, title="test-episode")
at = tz.datetime(
**{
k: info[k]
for k in ("year", "month", "day", "hour", "minute")
if info.get(k)
}
)
at = tz.make_aware(at)
diff = models.Diffusion(
episode=episode, start=at, end=at + timedelta(hours=1)
)
episode.save()
diff.save()
return diff
def test_find_episode(self):
station = models.Station(name="test-station")
program = models.Program(station=station, title="test")
station.save()
program.save()
for path, sound_file in self.sound_files.items():
infos = sound_file.read_path(path)
diff = self._setup_diff(program, infos)
sound = models.Sound(program=diff.program, file=path)
result = sound_file.find_episode(sound, infos)
self.assertEquals(diff.episode, result)
# TODO: find_playlist, sync

View File

@ -0,0 +1,111 @@
import pytest
from datetime import timedelta
from django.conf import settings as conf
from django.utils import timezone as tz
from aircox import models
from aircox.management.sound_file import SoundFile
@pytest.fixture
def path_infos():
return {
"test/20220101_10h13_1_sample_1.mp3": {
"year": 2022,
"month": 1,
"day": 1,
"hour": 10,
"minute": 13,
"n": 1,
"name": "Sample 1",
},
"test/20220102_10h13_sample_2.mp3": {
"year": 2022,
"month": 1,
"day": 2,
"hour": 10,
"minute": 13,
"name": "Sample 2",
},
"test/20220103_1_sample_3.mp3": {
"year": 2022,
"month": 1,
"day": 3,
"n": 1,
"name": "Sample 3",
},
"test/20220104_sample_4.mp3": {
"year": 2022,
"month": 1,
"day": 4,
"name": "Sample 4",
},
"test/20220105.mp3": {
"year": 2022,
"month": 1,
"day": 5,
"name": "20220105",
},
}
@pytest.fixture
def sound_files(path_infos):
return {
k: r
for k, r in (
(path, SoundFile(conf.MEDIA_ROOT + "/" + path))
for path in path_infos.keys()
)
}
def test_sound_path(sound_files):
for path, sound_file in sound_files.items():
assert path == sound_file.sound_path
def test_read_path(path_infos, sound_files):
for path, sound_file in sound_files.items():
expected = path_infos[path]
result = sound_file.read_path(path)
# remove None values
result = {k: v for k, v in result.items() if v is not None}
assert expected == result, "path: {}".format(path)
def _setup_diff(program, info):
episode = models.Episode(program=program, title="test-episode")
at = tz.datetime(
**{
k: info[k]
for k in ("year", "month", "day", "hour", "minute")
if info.get(k)
}
)
at = tz.make_aware(at)
diff = models.Diffusion(
episode=episode, start=at, end=at + timedelta(hours=1)
)
episode.save()
diff.save()
return diff
@pytest.mark.django_db(transaction=True)
def test_find_episode(sound_files):
station = models.Station(name="test-station")
program = models.Program(station=station, title="test")
station.save()
program.save()
for path, sound_file in sound_files.items():
infos = sound_file.read_path(path)
diff = _setup_diff(program, infos)
sound = models.Sound(program=diff.program, file=path)
result = sound_file.find_episode(sound, infos)
assert diff.episode == result
# TODO: find_playlist, sync

View File

@ -1,22 +1,15 @@
import pytest
import concurrent.futures as futures
import time
from datetime import datetime, timedelta
from django.test import TestCase
from aircox.management.sound_monitor import (
ModifiedHandler,
MonitorHandler,
NotifyHandler,
)
__all__ = (
"NotifyHandlerTestCase",
"MoveHandlerTestCase",
"ModifiedHandlerTestCase",
"MonitorHandlerTestCase",
)
class FakeEvent:
def __init__(self, **kwargs):
@ -31,22 +24,28 @@ class WaitHandler(NotifyHandler):
pass
class NotifyHandlerTestCase(TestCase):
@pytest.fixture
def monitor():
pool = futures.ThreadPoolExecutor(2)
return MonitorHandler("archives", pool)
class TestNotifyHandler:
pass
class MoveHandlerTestCase(TestCase):
class TestMoveHandler:
pass
class ModifiedHandlerTestCase(TestCase):
class TestModifiedHandler:
def test_wait(self):
handler = ModifiedHandler()
handler.timeout_delta = timedelta(seconds=0.1)
start = datetime.now()
handler.wait()
delta = datetime.now() - start
self.assertTrue(delta < handler.timeout_delta + timedelta(seconds=0.1))
assert delta < handler.timeout_delta + timedelta(seconds=0.1)
def test_wait_ping(self):
pool = futures.ThreadPoolExecutor(1)
@ -57,28 +56,24 @@ class ModifiedHandlerTestCase(TestCase):
time.sleep(0.3)
handler.ping()
time.sleep(0.3)
self.assertTrue(future.running())
assert future.running()
class MonitorHandlerTestCase(TestCase):
def setUp(self):
pool = futures.ThreadPoolExecutor(2)
self.monitor = MonitorHandler("archives", pool)
def test_submit_new_job(self):
class TestMonitorHandler:
def test_submit_new_job(self, monitor):
event = FakeEvent(src_path="dummy_src")
handler = NotifyHandler()
result, _ = self.monitor._submit(handler, event, "up")
self.assertIs(handler, result)
self.assertIsInstance(handler.future, futures.Future)
self.monitor.pool.shutdown()
result, _ = monitor._submit(handler, event, "up")
assert handler == result
assert isinstance(handler.future, futures.Future)
monitor.pool.shutdown()
def test_submit_job_exists(self):
def test_submit_job_exists(self, monitor):
event = FakeEvent(src_path="dummy_src")
job_1, new_1 = self.monitor._submit(WaitHandler(), event, "up")
job_2, new_2 = self.monitor._submit(NotifyHandler(), event, "up")
self.assertIs(job_1, job_2)
self.assertTrue(new_1)
self.assertFalse(new_2)
self.monitor.pool.shutdown()
job_1, new_1 = monitor._submit(WaitHandler(), event, "up")
job_2, new_2 = monitor._submit(NotifyHandler(), event, "up")
assert job_1 == job_2
assert new_1
assert not new_2
monitor.pool.shutdown()

View File

@ -1,8 +1,7 @@
import datetime
import django.utils.timezone as tz
__all__ = [
__all__ = (
"Redirect",
"redirect",
"date_range",
@ -10,9 +9,10 @@ __all__ = [
"date_or_default",
"to_timedelta",
"seconds_to_time",
]
)
# FIXME: usage & why we don't use Django's
class Redirect(Exception):
"""Redirect exception -- see `redirect()`."""

View File

@ -11,7 +11,7 @@ from django.template.loader import render_to_string
from django.utils import timezone as tz
from django.utils.translation import gettext_lazy as _
from aircox import settings
from aircox.conf import settings
from aircox.utils import to_seconds
from .connector import Connector

View File

View File

@ -1,15 +1,3 @@
"""Django and Aircox instance settings. This file should be saved as
`settings.py` in the same directory as this one.
User MUST define the following values: `SECRET_KEY`, `ALLOWED_HOSTS`, `DATABASES`
The following environment variables are used in settings:
* `AIRCOX_DEBUG` (`DEBUG`): enable/disable debugging
For Django settings see:
https://docs.djangoproject.com/en/3.1/topics/settings/
https://docs.djangoproject.com/en/3.1/ref/settings/
"""
import os
import sys
@ -19,7 +7,8 @@ from django.utils import timezone
sys.path.insert(1, os.path.dirname(os.path.realpath(__file__)))
# Project root directory
PROJECT_ROOT = os.path.dirname(os.path.dirname(__file__))
PROJECT_ROOT = os.path.abspath(__file__ + "/../../../")
# DEBUG mode
DEBUG = (
(os.environ["AIRCOX_DEBUG"].lower() in ("true", 1))
@ -38,12 +27,6 @@ LC_LOCALE = "en_US.UTF-8"
TIME_ZONE = os.environ.get("TZ") or "UTC"
########################################################################
#
# You MUST configure those values
#
########################################################################
# Secret key: you MUST put a consistent secret key. You can generate one
# at https://djecrety.ir/
SECRET_KEY = ""
@ -56,15 +39,11 @@ DATABASES = {
"TIMEZONE": TIME_ZONE,
}
}
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
# Allowed host for HTTP requests
ALLOWED_HOSTS = ("127.0.0.1",)
########################################################################
#
# You CAN configure starting from here
#
########################################################################
# Assets and medias:
# In production, user MUST configure webserver in order to serve static
@ -81,26 +60,6 @@ STATIC_ROOT = os.path.join(PROJECT_ROOT, "static")
# Path to media directory (by default in static's directory)
MEDIA_ROOT = os.path.join(STATIC_ROOT, "media")
# Include specific configuration depending of DEBUG
if DEBUG:
from .dev import *
else:
from .prod import *
# Enable caching using memcache
CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.memcached.MemcachedCache",
"LOCATION": "127.0.0.1:11211",
}
}
########################################################################
#
# You don't really need to configure what is happening below
#
########################################################################
# Enables internationalization and timezone
USE_I18N = True
USE_L10N = True
@ -112,7 +71,7 @@ try:
import locale
locale.setlocale(locale.LC_ALL, LC_LOCALE)
except:
except Exception:
print(
"Can not set locale {LC}. Is it available on you system? Hint: "
"Check /etc/locale.gen and rerun locale-gen as sudo if needed.".format(

View File

@ -1,5 +1,13 @@
import os
from .base import *
try:
from .settings import *
except ImportError:
pass
LOCALE_PATHS = ["aircox/locale", "aircox_streamer/locale"]
LOGGING = {

View File

@ -1,5 +1,10 @@
import os
try:
from .settings import *
except ImportError:
pass
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
@ -30,3 +35,12 @@ LOGGING = {
},
},
}
# Enable caching using memcache
CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.memcached.MemcachedCache",
"LOCATION": "127.0.0.1:11211",
}
}

View File

@ -0,0 +1,43 @@
"""Django and Aircox instance settings. This file should be saved as
`settings.py` in the same directory as this one.
User MUST define the following values: `SECRET_KEY`, `ALLOWED_HOSTS`, `DATABASES`
The following environment variables are used in settings:
* `AIRCOX_DEBUG` (`DEBUG`): enable/disable debugging
For Django settings see:
https://docs.djangoproject.com/en/3.1/topics/settings/
https://docs.djangoproject.com/en/3.1/ref/settings/
"""
# Debug mode: set to True for dev
# DEBUG = False
LANGUAGE_CODE = "fr-BE"
LC_LOCALE = "fr_BE.UTF-8"
# Secret key: you MUST put a consistent secret key. You can generate one
# at https://djecrety.ir/
SECRET_KEY = ""
# Database configuration: defaults to db.sqlite3
# DATABASES
# Allowed host for HTTP requests
# ALLOWED_HOSTS = ('127.0.0.1',)
# When LC_LOCALE is set here, this code activates it.
# Otherwise it can be removed
try:
import locale
locale.setlocale(locale.LC_ALL, LC_LOCALE)
except Exception:
print(
"Can not set locale {LC}. Is it available on you system? Hint: "
"Check /etc/locale.gen and rerun locale-gen as sudo if needed.".format(
LC=LANGUAGE_CODE
)
)
pass

4
pytest.ini Normal file
View File

@ -0,0 +1,4 @@
[pytest]
DJANGO_SETTINGS_MODULE = instance.settings
# -- recommended but optional:
python_files = tests.py test_*.py *_tests.py

View File

@ -3,6 +3,7 @@ djangorestframework~=3.13
django-model-utils>=4.2
django-filter~=22.1
django-content-editor~=6.3
django-filer~=2.2
django-honeypot~=1.0
django-taggit~=3.0

2
requirements_tests.txt Normal file
View File

@ -0,0 +1,2 @@
pytest~=7.2
pytest-django~=4.5

View File

@ -1,4 +1,5 @@
#! /bin/sh
export DJANGO_SETTINGS_MODULE=instance.settings.prod
# aircox daily tasks:
# - diffusions monitoring for the current month

View File

@ -21,7 +21,7 @@ autostart = true
autorestart = true
stdout_logfile = /srv/www/aircox/logs/server.log
redirect_stderr = true
environment=AIRCOX_DEBUG="False"
environment=AIRCOX_DEBUG="False",DJANGO_SETTINGS_MODULE=instance.settings.prod
[program:aircox_sounds_monitor]
command = /srv/www/aircox/scripts/launch_in_venv ./manage.py sounds_monitor -qsm
@ -31,7 +31,7 @@ autostart = true
autorestart = true
stdout_logfile = /srv/www/aircox/logs/sounds_monitor.log
redirect_stderr = true
environment=AIRCOX_DEBUG="False"
environment=AIRCOX_DEBUG="False",DJANGO_SETTINGS_MODULE=instance.settings.prod
[program:aircox_streamer]
command = /srv/www/aircox/scripts/launch_in_venv ./manage.py streamer -crm
@ -41,4 +41,4 @@ autostart = true
autorestart = true
stdout_logfile = /srv/www/aircox/logs/streamer.log
redirect_stderr = true
environment=AIRCOX_DEBUG="False"
environment=AIRCOX_DEBUG="False",DJANGO_SETTINGS_MODULE=instance.settings.prod