Skip to content

Commit

Permalink
feat: Allow configuration of config envvar
Browse files Browse the repository at this point in the history
  • Loading branch information
janw committed May 1, 2023
1 parent 4647755 commit a8879ad
Show file tree
Hide file tree
Showing 6 changed files with 101 additions and 11 deletions.
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,21 @@ After modifying the settings to your liking, `podcast-archiver` can be run with
podcast-archiver --config path/to/config.yaml
```

Alternatively (for example, if you're running `podcast-archiver` in Docker), you may point it to the config file using the `PODCAST_ARCHIVER_CONFIG=path/to/config.yaml` environment variable.

### Using environment variables

`podcast-archiver` uses [Pydantic](https://docs.pydantic.dev/latest/usage/settings/#parsing-environment-variable-values) to parse and validate its configuration. You can also prefix all configuration options (as used in the config file) with `PODCAST_ARCHIVER_` and export the results as environment variables to change the behavior, like so:

```bash
export PODCAST_ARCHIVER_FEEDS='[
"http://raumzeit-podcast.de/feed/m4a/",
"https://feeds.lagedernation.org/feeds/ldn-mp3.xml"
]' # Must be a string of a JSON array
export PODCAST_ARCHIVER_SLUGIFY_PATHS=true
export PODCAST_ARCHIVER_VERBOSE=2
```

## Excursion: Unicode Normalization in Slugify

The `--slugify` option removes all ambiguous characters from folders and filenames used in the archiving process. The removal includes unicode normalization according to [Compatibility Decomposition](http://unicode.org/reports/tr15/tr15-18.html#Decomposition). What? Yeah, me too. I figured this is best seen in an example, so here's a fictitious episode name, and how it would be translated to an target filename using the Archiver:
Expand Down
3 changes: 2 additions & 1 deletion podcast_archiver/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ def main(argv: Union[list[str], None] = None) -> None:
Settings.generate_example(args.config_generate)
sys.exit(0)

settings = Settings.load_and_merge(args.config, args)
settings = Settings.load_from_yaml(args.config)
settings.merge_argparser_args(args)
if not (settings.opml_files or settings.feeds):
parser.error("Must provide at least one of --feed or --opml")

Expand Down
33 changes: 24 additions & 9 deletions podcast_archiver/config.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

import textwrap
from os import environ
from pathlib import Path
from typing import TYPE_CHECKING, Any, Union

Expand Down Expand Up @@ -106,30 +107,44 @@ def normalize_opml_files(cls, v: Any) -> Path:

@classmethod
def load_from_yaml(cls, path: Union[Path, None]) -> Settings:
target = None
if path and path.is_file():
with path.open("r") as filep:
target = path
else:
target = cls._get_envvar_config_path()

if target:
with target.open("r") as filep:
content = safe_load(filep)
if content:
return cls.parse_obj(content)
return cls() # type: ignore[call-arg]

@classmethod
def load_and_merge(cls, path: Union[Path, None], args: argparse.Namespace):
settings = cls.load_from_yaml(path)
for name, field in settings.__fields__.items():
def _get_envvar_config_path(cls) -> Union[Path, None]:
if not (var_value := environ.get(f"{cls.Config.env_prefix}CONFIG")):
return None

if not (env_path := Path(var_value).expanduser()).is_file():
raise FileNotFoundError(f"{env_path} does not exist")

return env_path

def merge_argparser_args(self, args: argparse.Namespace):
for name, field in self.__fields__.items():
if (args_value := getattr(args, name, None)) is None:
continue

settings_value = getattr(settings, name)
settings_value = getattr(self, name)
if isinstance(settings_value, list) and isinstance(args_value, list):
setattr(settings, name, settings_value + args_value)
setattr(self, name, settings_value + args_value)
continue

if args_value != field.get_default():
setattr(settings, name, args_value)
setattr(self, name, args_value)

merged_settings = settings.dict(exclude_defaults=True, exclude_unset=True)
return cls.parse_obj(merged_settings)
merged_settings = self.dict(exclude_defaults=True, exclude_unset=True)
return self.parse_obj(merged_settings)

@classmethod
def generate_example(cls, path: Path) -> None:
Expand Down
20 changes: 19 additions & 1 deletion poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ pre-commit = "^3.2.2"
pytest = "^7.3.1"
pytest-cov = "^4.0.0"
pytest-responses = "^0.5.1"
pytest-env = "^0.8.1"

[tool.ruff]
line-length = 120
Expand Down Expand Up @@ -86,12 +87,19 @@ target-version = ['py39', 'py310', 'py311']
minversion = "6.0"
testpaths = ["tests",]
addopts = "--cov podcast_archiver --cov-report term --no-cov-on-fail"
env = [
"PODCAST_ARCHIVER_CONFIG=",
]

[tool.coverage.run]
omit = ["tests/*", "venv/*", ".venv/*"]

[tool.coverage.report]
precision = 1
exclude_lines = [
"pragma: no cover",
"if TYPE_CHECKING:",
]

[tool.mypy]
warn_unused_configs = true
Expand Down
33 changes: 33 additions & 0 deletions tests/test_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from pathlib import Path

import pytest

from podcast_archiver.config import Settings

DUMMY_FEED = "http://localhost/feed.rss"


def test_load_from_envvar_config_path(tmp_path_cd: Path, monkeypatch):
configfile = tmp_path_cd / "configtmp.yaml"
configfile.write_text(f"feeds: [{DUMMY_FEED}]")

monkeypatch.setenv("PODCAST_ARCHIVER_CONFIG", configfile)
settings = Settings.load_from_yaml(None)

assert DUMMY_FEED in settings.feeds


def test_load_from_envvar_config_path_nonexistent(monkeypatch):
monkeypatch.setenv("PODCAST_ARCHIVER_CONFIG", "nonexistent")
with pytest.raises(FileNotFoundError):
Settings.load_from_yaml(None)


def test_load_from_envvar_config_path_overridden_by_arg(tmp_path_cd: Path, monkeypatch):
configfile = tmp_path_cd / "configtmp.yaml"
configfile.write_text(f"feeds: [{DUMMY_FEED}]")

monkeypatch.setenv("PODCAST_ARCHIVER_CONFIG", "nonexistent")
settings = Settings.load_from_yaml(configfile)

assert DUMMY_FEED in settings.feeds

0 comments on commit a8879ad

Please sign in to comment.