envread 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
envread/__init__.py ADDED
@@ -0,0 +1,32 @@
1
+ """Typed, zero-dependency reader for environment variables with mypy-inferred types."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from ._dotenv import load_dotenv, parse_dotenv, read_dotenv
6
+ from ._env import Env, env
7
+ from ._errors import (
8
+ DotenvError,
9
+ EnvError,
10
+ EnvValidationError,
11
+ InvalidEnvError,
12
+ MissingEnvError,
13
+ )
14
+ from ._secret import Secret
15
+ from ._settings import load, var
16
+ from ._version import __version__ as __version__
17
+
18
+ __all__ = [
19
+ "DotenvError",
20
+ "Env",
21
+ "EnvError",
22
+ "EnvValidationError",
23
+ "InvalidEnvError",
24
+ "MissingEnvError",
25
+ "Secret",
26
+ "env",
27
+ "load",
28
+ "load_dotenv",
29
+ "parse_dotenv",
30
+ "read_dotenv",
31
+ "var",
32
+ ]
envread/__main__.py ADDED
@@ -0,0 +1,7 @@
1
+ """Allow ``python -m envread`` as an alias for the ``envread`` command."""
2
+
3
+ import sys
4
+
5
+ from ._cli import main
6
+
7
+ sys.exit(main())
envread/_check.py ADDED
@@ -0,0 +1,68 @@
1
+ """Evaluate a list of variable specs against a reader, collecting every outcome."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Callable, Iterable
6
+ from dataclasses import dataclass
7
+ from typing import Any
8
+
9
+ from ._errors import InvalidEnvError, MissingEnvError
10
+ from ._reader import MISSING, Reader, parse_value
11
+
12
+
13
+ @dataclass(frozen=True)
14
+ class VarSpec:
15
+ """One variable to read: its name, parser and what to do when it is unset."""
16
+
17
+ name: str
18
+ type_name: str
19
+ parser: Callable[[str, str], Any]
20
+ required: bool = True
21
+ default: Any = MISSING
22
+ default_factory: Callable[[], Any] | None = None
23
+ secret: bool = False
24
+ attr: str = ""
25
+
26
+ def fallback(self) -> Any:
27
+ """The value used when the variable is unset and not required."""
28
+ if self.default_factory is not None:
29
+ return self.default_factory()
30
+ return None if self.default is MISSING else self.default
31
+
32
+
33
+ @dataclass(frozen=True)
34
+ class Outcome:
35
+ """The result of reading one spec.
36
+
37
+ ``status`` is ``"ok"`` (set and valid), ``"unset"`` (unset but optional, so
38
+ ``value`` is the fallback), ``"missing"`` or ``"invalid"`` (``error`` is set).
39
+ """
40
+
41
+ spec: VarSpec
42
+ full_name: str
43
+ status: str
44
+ value: Any = None
45
+ error: MissingEnvError | InvalidEnvError | None = None
46
+
47
+ @property
48
+ def failed(self) -> bool:
49
+ return self.error is not None
50
+
51
+
52
+ def evaluate_one(spec: VarSpec, reader: Reader) -> Outcome:
53
+ full = reader.full_name(spec.name)
54
+ raw = reader.raw(spec.name)
55
+ if raw is None:
56
+ if spec.required:
57
+ return Outcome(spec, full, "missing", error=MissingEnvError(full))
58
+ return Outcome(spec, full, "unset", spec.fallback())
59
+ try:
60
+ value = parse_value(spec.parser, full, raw, spec.secret)
61
+ except InvalidEnvError as exc:
62
+ return Outcome(spec, full, "invalid", error=exc)
63
+ return Outcome(spec, full, "ok", value)
64
+
65
+
66
+ def evaluate(specs: Iterable[VarSpec], reader: Reader) -> list[Outcome]:
67
+ """Read every spec, never stopping at the first failure."""
68
+ return [evaluate_one(spec, reader) for spec in specs]
envread/_cli.py ADDED
@@ -0,0 +1,152 @@
1
+ """The ``envread`` command: validate a spec against the environment, or read one value."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import importlib
7
+ import os
8
+ import sys
9
+ from collections import ChainMap
10
+ from collections.abc import Mapping, Sequence
11
+ from pathlib import Path
12
+ from typing import Any, NoReturn
13
+
14
+ from ._check import VarSpec, evaluate, evaluate_one
15
+ from ._dotenv import read_layer
16
+ from ._env import Env
17
+ from ._errors import EnvError
18
+ from ._reader import parse_value
19
+ from ._report import canonical, render_json, render_text, summary
20
+ from ._settings import specs_for
21
+ from ._specfile import TYPE_HELP, SpecError, parse_type, read_spec
22
+ from ._version import __version__
23
+
24
+ EPILOG = """\
25
+ examples:
26
+ envread check env.spec validate every variable in a spec file
27
+ envread check myapp.settings:Settings validate the fields of a dataclass
28
+ envread check env.spec --env-file .env layer a .env file under the environment
29
+ envread check env.spec --format json machine-readable report
30
+ envread get PORT --type int --default 8080 print one parsed value
31
+ envread get DEBUG --type bool prints true or false
32
+
33
+ spec file format (one variable per line, # starts a comment):
34
+ DATABASE_URL secret
35
+ PORT int optional
36
+ DEBUG bool optional
37
+ LOG_LEVEL choice:debug,info,warning
38
+ HOSTS list:;
39
+
40
+ check exits 0 when every variable is valid and 1 otherwise; values are only printed
41
+ with --show-values, and secrets never are. Errors go to stderr with exit code 1.
42
+ module:Class is imported with the current directory added to the import path.
43
+ """
44
+
45
+
46
+ class _Parser(argparse.ArgumentParser):
47
+ def error(self, message: str) -> NoReturn:
48
+ self.print_usage(sys.stderr)
49
+ self.exit(1, f"{self.prog}: error: {message}\n")
50
+
51
+
52
+ def _common(parser: argparse.ArgumentParser) -> None:
53
+ parser.add_argument("--prefix", default="", help="prepend PREFIX to every name")
54
+ parser.add_argument("--env-file", metavar="FILE", help="read FILE under the environment")
55
+ parser.add_argument(
56
+ "--empty-is-unset", action="store_true", help="treat variables set to '' as unset"
57
+ )
58
+
59
+
60
+ def build_parser() -> argparse.ArgumentParser:
61
+ parser = _Parser(
62
+ prog="envread",
63
+ description="Validate and read typed environment variables.",
64
+ epilog=EPILOG,
65
+ formatter_class=argparse.RawDescriptionHelpFormatter,
66
+ )
67
+ parser.add_argument("--version", action="version", version=f"envread {__version__}")
68
+ commands = parser.add_subparsers(dest="command", required=True, parser_class=_Parser)
69
+ check = commands.add_parser(
70
+ "check",
71
+ help="validate a spec or dataclass",
72
+ epilog=EPILOG,
73
+ formatter_class=argparse.RawDescriptionHelpFormatter,
74
+ )
75
+ check.add_argument("target", help="a spec file path, or module:Class for a dataclass")
76
+ _common(check)
77
+ check.add_argument("--format", choices=("text", "json"), default="text")
78
+ check.add_argument("--show-values", action="store_true", help="print values (not secrets)")
79
+ check.add_argument("--quiet", "-q", action="store_true", help="print nothing, exit code only")
80
+ get = commands.add_parser(
81
+ "get",
82
+ help="print one parsed value",
83
+ epilog=EPILOG,
84
+ formatter_class=argparse.RawDescriptionHelpFormatter,
85
+ )
86
+ get.add_argument("name", help="the variable name (without --prefix)")
87
+ get.add_argument("--type", default="str", help=f"one of {TYPE_HELP} (default str)")
88
+ get.add_argument("--default", help="value to use when the variable is unset")
89
+ get.add_argument("--secret", action="store_true", help="never show the value in errors")
90
+ _common(get)
91
+ return parser
92
+
93
+
94
+ def _reader(args: argparse.Namespace) -> Env:
95
+ source: Mapping[str, str] = os.environ
96
+ if args.env_file:
97
+ source = ChainMap(os.environ, read_layer(args.env_file))
98
+ return Env(source, prefix=args.prefix, empty_is_unset=args.empty_is_unset)
99
+
100
+
101
+ def _specs(target: str) -> list[VarSpec]:
102
+ if Path(target).is_file():
103
+ return read_spec(target)
104
+ module_name, colon, attr = target.partition(":")
105
+ if not colon or not module_name or not attr:
106
+ raise SpecError(f"{target!r} is neither a spec file nor module:Class")
107
+ if os.getcwd() not in sys.path:
108
+ sys.path.insert(0, os.getcwd())
109
+ obj: Any = importlib.import_module(module_name)
110
+ for part in attr.split("."):
111
+ obj = getattr(obj, part)
112
+ return specs_for(obj)
113
+
114
+
115
+ def _check(args: argparse.Namespace) -> int:
116
+ outcomes = evaluate(_specs(args.target), _reader(args))
117
+ failed = any(outcome.failed for outcome in outcomes)
118
+ if not args.quiet:
119
+ render = render_json if args.format == "json" else render_text
120
+ report = render(outcomes, args.show_values)
121
+ if report:
122
+ print(report)
123
+ if args.format == "text":
124
+ print(f"envread: {summary(outcomes)}", file=sys.stderr if failed else sys.stdout)
125
+ return 1 if failed else 0
126
+
127
+
128
+ def _get(args: argparse.Namespace) -> int:
129
+ parser, type_name = parse_type(args.type)
130
+ spec = VarSpec(args.name, type_name, parser, required=args.default is None, secret=args.secret)
131
+ outcome = evaluate_one(spec, _reader(args))
132
+ if outcome.error is not None:
133
+ raise outcome.error
134
+ value = outcome.value
135
+ if outcome.status == "unset":
136
+ value = parse_value(parser, f"--default for {outcome.full_name}", args.default, args.secret)
137
+ print("\n".join(value) if isinstance(value, list) else canonical(value))
138
+ return 0
139
+
140
+
141
+ def main(argv: Sequence[str] | None = None) -> int:
142
+ """Run the command line interface and return the process exit code."""
143
+ args = build_parser().parse_args(argv)
144
+ try:
145
+ return _check(args) if args.command == "check" else _get(args)
146
+ except (EnvError, SpecError, TypeError, ImportError, AttributeError, OSError) as exc:
147
+ print(f"envread: {exc}", file=sys.stderr)
148
+ return 1
149
+
150
+
151
+ if __name__ == "__main__": # pragma: no cover
152
+ sys.exit(main())
envread/_dotenv.py ADDED
@@ -0,0 +1,105 @@
1
+ r"""Read ``.env`` files: a documented subset of python-dotenv 1.x syntax.
2
+
3
+ Supported, matching ``dotenv_values`` / ``load_dotenv``: blank lines, ``#`` comment
4
+ lines, an optional ``export`` keyword, ``KEY=value`` with spaces allowed around ``=``;
5
+ unquoted values (trimmed, an inline comment needs whitespace before ``#``); single
6
+ quotes (literal, only ``\'`` and ``\\`` are escapes, and ``${`` is refused because
7
+ the two references disagree on it); double quotes (escapes
8
+ ``\\ \' \" \a \b \f \n \r \t \v``); and ``${NAME}`` / ``${NAME:-default}``
9
+ expansion in unquoted and double-quoted values. A bare ``KEY`` with no ``=`` is
10
+ skipped. Anything else, such as multi-line quoted values or other ``${...}`` forms,
11
+ raises :class:`DotenvError` naming the line, instead of being guessed at.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import os
17
+ import re
18
+ import sys
19
+ from collections.abc import Mapping
20
+ from pathlib import Path
21
+
22
+ from ._dotenv_value import Expand, parse_value
23
+ from ._errors import DotenvError
24
+
25
+ _LINE = re.compile(r"\s*(?:export\s+)?(?P<key>[^=#\s'\"]+)\s*(?:=(?P<rest>.*))?")
26
+ _WINDOWS_MAX = 32767
27
+
28
+
29
+ def _entries(text: str, path: str | None) -> list[tuple[int, str, str, bool]]:
30
+ """Return ``(line, key, value, expandable)`` for every assignment line."""
31
+ entries = []
32
+ for number, line in enumerate(text.splitlines(), start=1):
33
+ stripped = line.strip()
34
+ if not stripped or stripped.startswith("#"):
35
+ continue
36
+ match = _LINE.fullmatch(line)
37
+ if match is None:
38
+ raise DotenvError("expected KEY=value", number, path)
39
+ rest = match.group("rest")
40
+ if rest is None:
41
+ continue
42
+ try:
43
+ value, expandable = parse_value(rest)
44
+ except ValueError as exc:
45
+ raise DotenvError(str(exc), number, path) from None
46
+ entries.append((number, match.group("key"), value, expandable))
47
+ return entries
48
+
49
+
50
+ def _resolve(
51
+ text: str, path: str | None, environ: Mapping[str, str], file_first: bool
52
+ ) -> dict[str, str]:
53
+ values: dict[str, str] = {}
54
+ for number, key, raw, expandable in _entries(text, path):
55
+ lookup = Expand(values, environ, file_first)
56
+ try:
57
+ values[key] = lookup.expand(raw) if expandable else raw
58
+ except ValueError as exc:
59
+ raise DotenvError(str(exc), number, path) from None
60
+ return values
61
+
62
+
63
+ def parse_dotenv(text: str, environ: Mapping[str, str] | None = None) -> dict[str, str]:
64
+ """Parse ``.env`` text into a dict, like python-dotenv's ``dotenv_values``.
65
+
66
+ ``${NAME}`` resolves against earlier keys in the text first, then ``environ``
67
+ (``os.environ`` by default); an unknown name expands to ``""``.
68
+ """
69
+ return _resolve(text, None, os.environ if environ is None else environ, True)
70
+
71
+
72
+ def read_dotenv(
73
+ path: str | os.PathLike[str], environ: Mapping[str, str] | None = None
74
+ ) -> dict[str, str]:
75
+ """Read and parse a UTF-8 ``.env`` file without touching the environment."""
76
+ text = Path(path).read_text(encoding="utf-8-sig")
77
+ return _resolve(text, os.fspath(path), os.environ if environ is None else environ, True)
78
+
79
+
80
+ def load_dotenv(path: str | os.PathLike[str], *, override: bool = False) -> dict[str, str]:
81
+ """Copy the variables of a ``.env`` file into ``os.environ``; return what was set.
82
+
83
+ Like python-dotenv, variables that are already set win unless ``override=True``,
84
+ and the same precedence applies to ``${NAME}`` expansion. On Windows an entry
85
+ whose ``NAME=value`` exceeds 32,767 characters cannot be stored in the process
86
+ environment, so it raises :class:`DotenvError` before anything is written.
87
+ """
88
+ name = os.fspath(path)
89
+ text = Path(path).read_text(encoding="utf-8-sig")
90
+ values = _resolve(text, name, os.environ, override)
91
+ chosen = {k: v for k, v in values.items() if override or k not in os.environ}
92
+ if sys.platform == "win32":
93
+ lines = {key: number for number, key, *_ in _entries(text, name)}
94
+ for key, value in chosen.items():
95
+ if len(key) + 1 + len(value) > _WINDOWS_MAX:
96
+ reason = f"{key} is longer than the 32767 characters Windows allows"
97
+ raise DotenvError(reason, lines[key], name)
98
+ os.environ.update(chosen)
99
+ return chosen
100
+
101
+
102
+ def read_layer(path: str | os.PathLike[str]) -> dict[str, str]:
103
+ """Parse a file the way :func:`load_dotenv` would see it, without writing anything."""
104
+ text = Path(path).read_text(encoding="utf-8-sig")
105
+ return _resolve(text, os.fspath(path), os.environ, False)
@@ -0,0 +1,73 @@
1
+ """Decode the value part of one ``.env`` line and expand ``${NAME}`` references."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from collections.abc import Mapping
7
+
8
+ _SINGLE = re.compile(r"'((?:\\'|[^'])*)'")
9
+ _DOUBLE = re.compile(r'"((?:\\"|[^"])*)"')
10
+ _TAIL = re.compile(r"\s*(?:#.*)?")
11
+ _SINGLE_ESCAPES = re.compile(r"\\([\\'])")
12
+ _DOUBLE_ESCAPES = re.compile(r"\\([\\'\"abfnrtv])")
13
+ _CONTROL = {"a": "\a", "b": "\b", "f": "\f", "n": "\n", "r": "\r", "t": "\t", "v": "\v"}
14
+ _REFERENCE = re.compile(r"\$\{([^}]*)\}")
15
+ _PLAIN = re.compile(r"([A-Za-z_][A-Za-z0-9_]*)(?::-([^}]*))?")
16
+ # python-dotenv 1.x expands ${...} inside single quotes while docker compose keeps it
17
+ # literal, so the subset refuses the construct rather than pick one silently.
18
+ _AMBIGUOUS = "${ inside single quotes is read differently by python-dotenv and docker compose"
19
+
20
+
21
+ def parse_value(rest: str) -> tuple[str, bool]:
22
+ """Return ``(value, expandable)`` for the text after ``=``.
23
+
24
+ Raises ValueError with a reason that never quotes the value.
25
+ """
26
+ text = rest.lstrip()
27
+ if text.startswith(("'", '"')):
28
+ quote = text[0]
29
+ match = (_SINGLE if quote == "'" else _DOUBLE).match(text)
30
+ if match is None:
31
+ raise ValueError("unterminated quoted value (multi-line values are not supported)")
32
+ if _TAIL.fullmatch(text, match.end()) is None:
33
+ raise ValueError("unexpected text after the closing quote")
34
+ body = match.group(1)
35
+ if quote == "'":
36
+ if "${" in body:
37
+ raise ValueError(_AMBIGUOUS)
38
+ return _SINGLE_ESCAPES.sub(r"\1", body), False
39
+ return _DOUBLE_ESCAPES.sub(lambda m: _CONTROL.get(m.group(1), m.group(1)), body), True
40
+ return re.sub(r"\s+#.*", "", text).rstrip(), True
41
+
42
+
43
+ class Expand:
44
+ """Resolve ``${NAME}`` and ``${NAME:-default}`` against the file and an environment.
45
+
46
+ ``file_first`` decides who wins when a name exists in both: the earlier file
47
+ entries (``dotenv_values`` behaviour) or the environment (``load_dotenv`` without
48
+ ``override``). An unknown name with no default expands to ``""``; the default is
49
+ used only when the name is unset, never when it is set to ``""``.
50
+ """
51
+
52
+ def __init__(self, values: Mapping[str, str], environ: Mapping[str, str], file_first: bool):
53
+ self._first, self._second = (values, environ) if file_first else (environ, values)
54
+
55
+ def lookup(self, name: str) -> str | None:
56
+ value = self._first.get(name)
57
+ return value if value is not None else self._second.get(name)
58
+
59
+ def expand(self, raw: str) -> str:
60
+ if "${" not in raw:
61
+ return raw
62
+ if raw.count("${") != len(_REFERENCE.findall(raw)):
63
+ raise ValueError("unterminated ${ reference")
64
+ return _REFERENCE.sub(self._replace, raw)
65
+
66
+ def _replace(self, match: re.Match[str]) -> str:
67
+ inner = _PLAIN.fullmatch(match.group(1))
68
+ if inner is None:
69
+ raise ValueError("only ${NAME} and ${NAME:-default} references are supported")
70
+ value = self.lookup(inner.group(1))
71
+ if value is not None:
72
+ return value
73
+ return inner.group(2) or ""
envread/_duration.py ADDED
@@ -0,0 +1,57 @@
1
+ """Parse human durations such as ``90``, ``250ms``, ``1h30m`` or ``1.5h``."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from datetime import timedelta
7
+
8
+ from ._errors import InvalidEnvError
9
+
10
+ _UNITS = {
11
+ "ms": 0.001,
12
+ "s": 1.0,
13
+ "m": 60.0,
14
+ "h": 3600.0,
15
+ "d": 86400.0,
16
+ "w": 604800.0,
17
+ }
18
+ _PART = re.compile(r"(\d+(?:\.\d+)?|\.\d+)(ms|s|m|h|d|w)")
19
+ _REASON = "expected seconds (90) or amounts with units ms, s, m, h, d, w (1h30m, 250ms)"
20
+
21
+
22
+ def parse_duration(name: str, raw: str) -> timedelta:
23
+ """Return a non-negative timedelta.
24
+
25
+ A bare number is seconds. Otherwise the value is one or more ``<number><unit>``
26
+ parts with no spaces between them, each unit used at most once and from the
27
+ largest to the smallest, so ``1h30m`` is valid and ``30m1h`` is not.
28
+ """
29
+ text = raw.strip().lower()
30
+ try:
31
+ return timedelta(seconds=float(text)) if _is_number(text) else _parse_parts(text)
32
+ except (ValueError, OverflowError):
33
+ raise InvalidEnvError(name, raw, "duration", _REASON) from None
34
+
35
+
36
+ def _is_number(text: str) -> bool:
37
+ return re.fullmatch(r"\d+(?:\.\d+)?|\.\d+", text) is not None
38
+
39
+
40
+ def _parse_parts(text: str) -> timedelta:
41
+ if not text:
42
+ raise ValueError("empty")
43
+ order = list(_UNITS)[::-1]
44
+ position = 0
45
+ last_rank = -1
46
+ seconds = 0.0
47
+ while position < len(text):
48
+ match = _PART.match(text, position)
49
+ if match is None:
50
+ raise ValueError("bad part")
51
+ rank = order.index(match.group(2))
52
+ if rank <= last_rank:
53
+ raise ValueError("units out of order")
54
+ last_rank = rank
55
+ seconds += float(match.group(1)) * _UNITS[match.group(2)]
56
+ position = match.end()
57
+ return timedelta(seconds=seconds)
envread/_env.py ADDED
@@ -0,0 +1,136 @@
1
+ """The Env reader: typed getters whose return type mypy infers from the call."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from builtins import bool as _bool
6
+ from builtins import float as _float
7
+ from builtins import int as _int
8
+ from builtins import list as _list
9
+ from builtins import str as _str
10
+ from typing import Any, Literal, overload
11
+
12
+ from ._parsers import parse_bool, parse_float, parse_int, parse_json, parse_list, parse_str
13
+ from ._reader import MISSING
14
+ from ._rich import RichGetters
15
+
16
+ # The getters are named after the builtin types (env.int, env.str, ...), which shadows
17
+ # those names inside the class body, so annotations use these module-level aliases.
18
+
19
+
20
+ class Env(RichGetters):
21
+ """Read environment variables as typed values.
22
+
23
+ Each getter is overloaded so the static type follows the call: with no default
24
+ and no ``required=`` the result is non-optional and a missing variable raises;
25
+ passing ``None`` or ``required=False`` widens the result to include ``None``.
26
+ ``secret=True`` keeps the value out of any error message.
27
+ """
28
+
29
+ @overload
30
+ def str(self, name: _str, *, secret: _bool = ...) -> _str: ...
31
+ @overload
32
+ def str(self, name: _str, default: _str, *, secret: _bool = ...) -> _str: ...
33
+ @overload
34
+ def str(self, name: _str, default: None, *, secret: _bool = ...) -> _str | None: ...
35
+ @overload
36
+ def str(self, name: _str, *, required: Literal[True], secret: _bool = ...) -> _str: ...
37
+ @overload
38
+ def str(self, name: _str, *, required: Literal[False], secret: _bool = ...) -> _str | None: ...
39
+ def str(
40
+ self, name: _str, default: Any = MISSING, *, required: Any = MISSING, secret: _bool = False
41
+ ) -> _str | None:
42
+ """Return the raw value; an empty value is returned as ``""``."""
43
+ return self._get(name, parse_str, default, required, secret)
44
+
45
+ @overload
46
+ def int(self, name: _str, *, secret: _bool = ...) -> _int: ...
47
+ @overload
48
+ def int(self, name: _str, default: _int, *, secret: _bool = ...) -> _int: ...
49
+ @overload
50
+ def int(self, name: _str, default: None, *, secret: _bool = ...) -> _int | None: ...
51
+ @overload
52
+ def int(self, name: _str, *, required: Literal[True], secret: _bool = ...) -> _int: ...
53
+ @overload
54
+ def int(self, name: _str, *, required: Literal[False], secret: _bool = ...) -> _int | None: ...
55
+ def int(
56
+ self, name: _str, default: Any = MISSING, *, required: Any = MISSING, secret: _bool = False
57
+ ) -> _int | None:
58
+ """Parse what ``int()`` accepts in base 10 (``1_000`` too), ignoring whitespace."""
59
+ return self._get(name, parse_int, default, required, secret)
60
+
61
+ @overload
62
+ def float(self, name: _str, *, secret: _bool = ...) -> _float: ...
63
+ @overload
64
+ def float(self, name: _str, default: _float, *, secret: _bool = ...) -> _float: ...
65
+ @overload
66
+ def float(self, name: _str, default: None, *, secret: _bool = ...) -> _float | None: ...
67
+ @overload
68
+ def float(self, name: _str, *, required: Literal[True], secret: _bool = ...) -> _float: ...
69
+ @overload
70
+ def float(
71
+ self, name: _str, *, required: Literal[False], secret: _bool = ...
72
+ ) -> _float | None: ...
73
+ def float(
74
+ self, name: _str, default: Any = MISSING, *, required: Any = MISSING, secret: _bool = False
75
+ ) -> _float | None:
76
+ """Parse anything Python's ``float()`` accepts, ignoring surrounding whitespace."""
77
+ return self._get(name, parse_float, default, required, secret)
78
+
79
+ @overload
80
+ def bool(self, name: _str, *, secret: _bool = ...) -> _bool: ...
81
+ @overload
82
+ def bool(self, name: _str, default: _bool, *, secret: _bool = ...) -> _bool: ...
83
+ @overload
84
+ def bool(self, name: _str, default: None, *, secret: _bool = ...) -> _bool | None: ...
85
+ @overload
86
+ def bool(self, name: _str, *, required: Literal[True], secret: _bool = ...) -> _bool: ...
87
+ @overload
88
+ def bool(
89
+ self, name: _str, *, required: Literal[False], secret: _bool = ...
90
+ ) -> _bool | None: ...
91
+ def bool(
92
+ self, name: _str, default: Any = MISSING, *, required: Any = MISSING, secret: _bool = False
93
+ ) -> _bool | None:
94
+ """Accept true/false, yes/no, on/off, 1/0, y/n, t/f, enable(d)/disable(d), any case."""
95
+ return self._get(name, parse_bool, default, required, secret)
96
+
97
+ @overload
98
+ def list(self, name: _str, *, separator: _str = ",", secret: _bool = ...) -> _list[_str]: ...
99
+ @overload
100
+ def list(
101
+ self, name: _str, default: _list[_str], *, separator: _str = ",", secret: _bool = ...
102
+ ) -> _list[_str]: ...
103
+ @overload
104
+ def list(
105
+ self, name: _str, default: None, *, separator: _str = ",", secret: _bool = ...
106
+ ) -> _list[_str] | None: ...
107
+ @overload
108
+ def list(
109
+ self, name: _str, *, required: Literal[True], separator: _str = ",", secret: _bool = ...
110
+ ) -> _list[_str]: ...
111
+ @overload
112
+ def list(
113
+ self, name: _str, *, required: Literal[False], separator: _str = ",", secret: _bool = ...
114
+ ) -> _list[_str] | None: ...
115
+ def list(
116
+ self,
117
+ name: _str,
118
+ default: Any = MISSING,
119
+ *,
120
+ required: Any = MISSING,
121
+ separator: _str = ",",
122
+ secret: _bool = False,
123
+ ) -> _list[_str] | None:
124
+ """Split on ``separator`` and trim each item; an empty value is ``[]``."""
125
+ return self._get(
126
+ name, lambda n, raw: parse_list(n, raw, separator), default, required, secret
127
+ )
128
+
129
+ def json(
130
+ self, name: _str, default: Any = MISSING, *, required: Any = MISSING, secret: _bool = False
131
+ ) -> Any:
132
+ """Parse the value as JSON. Return type is Any, so annotate the call site."""
133
+ return self._get(name, parse_json, default, required, secret)
134
+
135
+
136
+ env = Env()