comicbox.formats.base.online.cli_overrides

[docs] module comicbox.formats.base.online.cli_overrides

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
"""
Parse the repeatable per-source CLI ``--auth`` overrides.

The CLI flag ``--auth`` is repeatable with the form
``<source>:<field>=<value>``. Each occurrence is collected and turned
into a per-source field dict the credential resolver consumes.

Examples:
    --auth metron:user=AJSlater
    --auth metron:pass='hunter2'
    --auth metron:url=https://metron.cloud
    --auth comicvine:key=ABCD1234
    --auth comicvine:url=https://comicvine.gamespot.com/api

"""

from __future__ import annotations

from dataclasses import dataclass, field
from typing import TYPE_CHECKING

from comicbox.formats.base.online import SOURCE_NAMES

if TYPE_CHECKING:
    from collections.abc import Iterable, Mapping

_VALID_AUTH_FIELDS = frozenset({"user", "pass", "key", "url"})


def _parse_auth_entry(raw: str) -> tuple[str, str, str]:
    """Parse a single ``<source>:<field>=<value>`` entry."""
    if ":" not in raw or "=" not in raw:
        reason = f"--auth expects <source>:<field>=<value>, got {raw!r}"
        raise ValueError(reason)
    head, _, value = raw.partition("=")
    source, _, cred_field = head.partition(":")
    source = source.strip().lower()
    cred_field = cred_field.strip().lower()
    if source not in SOURCE_NAMES:
        reason = f"--auth: unknown source {source!r}; known: {', '.join(SOURCE_NAMES)}"
        raise ValueError(reason)
    if cred_field not in _VALID_AUTH_FIELDS:
        reason = (
            f"--auth: unknown field {cred_field!r}; "
            f"valid: {', '.join(sorted(_VALID_AUTH_FIELDS))}"
        )
        raise ValueError(reason)
    return source, cred_field, value


@dataclass(frozen=True, slots=True)
class CliOverrides:
    """
    CLI flag values for per-source credentials.

    ``per_source`` maps source name → {field_name: value}. Only fields
    explicitly provided on the CLI appear; missing fields fall through
    to env/config/keyring.
    """

    per_source: Mapping[str, Mapping[str, str]] = field(default_factory=dict)

    @classmethod
    def from_auth_list(cls, entries: Iterable[str]) -> CliOverrides:
        """Build from a list of parsed ``--auth`` strings."""
        per_source: dict[str, dict[str, str]] = {}
        for raw in entries or ():
            if not raw:
                continue
            source, cred_field, value = _parse_auth_entry(raw)
            per_source.setdefault(source, {})[cred_field] = value
        return cls(per_source={k: dict(v) for k, v in per_source.items()})