Source code for action0.celery_sched.sources
"""
What the loaders read from, and turning each kind of source into data.
A source is one of:
- a path (a ``str`` or any path-like) to a YAML or TOML file;
- an open stream — text or binary, as :py:mod:`tomllib` users are used to;
- a mapping that is already the parsed document, e.g. a table of a larger
TOML configuration, or a dict from a settings system.
"""
import os
from collections.abc import Mapping
from pathlib import Path
from typing import IO
from typing import Any
from typing import TypeAlias
from action0.celery_sched.errors import located
from action0.celery_sched.formats import FormatLike
from action0.celery_sched.formats import detect_format
from action0.celery_sched.formats import parser_for
#: what the load functions read from: a file path, an open stream, or parsed data
Source: TypeAlias = str | os.PathLike[str] | IO[str] | IO[bytes] | Mapping[str, Any]
[docs]
def read_source(source: Source, format: FormatLike | None = None) -> tuple[str | None, Any]:
"""
Read a source into its parsed document, and name it for error messages.
>>> read_source({"Poll": {"task": "myapp.tasks.poll"}})
(None, {'Poll': {'task': 'myapp.tasks.poll'}})
:param source: the source to read
:param format: the format of a file or stream, instead of guessing it from
the name (ignored for mappings, which are parsed already)
:returns: the source's name (the path, the stream's name, or ``None``) and
its document
:raises ValueError: if the format of a file or stream can't be told
:raises ImportError: for a YAML source, if PyYAML (the ``yaml`` extra) is
not installed
:raises DefinitionError: if the text is malformed
"""
if isinstance(source, Mapping):
return None, source
name = os.fspath(source) if isinstance(source, str | os.PathLike) else _stream_name(source)
# decided before reading, so a file of unknown format (or one whose parser
# isn't installed) fails without any I/O
parse = parser_for(detect_format(name, format))
text = _read_text(source)
with located(source=name):
return name, parse(text)
def _stream_name(stream: IO[str] | IO[bytes]) -> str | None:
"""The name of a stream, if it has a usable one (files opened with open() do)."""
name = getattr(stream, "name", None)
return name if isinstance(name, str) else None
def _read_text(source: str | os.PathLike[str] | IO[str] | IO[bytes]) -> str:
"""Read a file or a text or binary stream; schedules are UTF-8 (TOML requires it)."""
if isinstance(source, str | os.PathLike):
return Path(source).read_text(encoding="utf-8")
content = source.read()
return content.decode("utf-8") if isinstance(content, bytes) else content