alltoml 0.0.0__tar.gz

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.
alltoml-0.0.0/LICENSE ADDED
@@ -0,0 +1,16 @@
1
+ Copyright (c) 2025 Erik Soma
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
4
+ associated documentation files (the "Software"), to deal in the Software without restriction,
5
+ including without limitation the rights to use, copy, modify, merge, publish, distribute,
6
+ sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
7
+ furnished to do so, subject to the following conditions:
8
+
9
+ The above copyright notice and this permission notice shall be included in all copies or
10
+ substantial portions of the Software.
11
+
12
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
13
+ NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
14
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
15
+ DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT
16
+ OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
alltoml-0.0.0/PKG-INFO ADDED
@@ -0,0 +1,12 @@
1
+ Metadata-Version: 2.3
2
+ Name: alltoml
3
+ Version: 0.0.0
4
+ Summary: TOML configuration everywhere
5
+ Author: Erik Soma
6
+ Author-email: stillusingirc@gmail.com
7
+ Requires-Python: >=3.12
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.12
10
+ Classifier: Programming Language :: Python :: 3.13
11
+ Requires-Dist: deep-chainmap (>=0.1.3,<0.2.0)
12
+ Requires-Dist: platformdirs (>=4.4.0,<5.0.0)
@@ -0,0 +1,28 @@
1
+ [project]
2
+ name = "alltoml"
3
+ requires-python = ">=3.12"
4
+ version = "0.0.0"
5
+ description = "TOML configuration everywhere"
6
+ authors = [
7
+ {name="Erik Soma", email="stillusingirc@gmail.com"}
8
+ ]
9
+
10
+ [tool.poetry.dependencies]
11
+ python = "^3.12"
12
+ platformdirs = "^4.4.0"
13
+ deep-chainmap = "^0.1.3"
14
+
15
+ [tool.poetry.group.dev.dependencies]
16
+ pytest = "7.4.3"
17
+ pytest-cov = "4.1.0"
18
+
19
+ [build-system]
20
+ requires = ["poetry-core", "setuptools==69.0.2"]
21
+ build-backend = "poetry.core.masonry.api"
22
+
23
+ [virtualenvs]
24
+ in-project = true
25
+
26
+ [tool.pyright]
27
+ venvPath = "."
28
+ venv = ".venv"
@@ -0,0 +1,7 @@
1
+ __all__ = ["load", "load_from_argv", "load_from_environ", "load_from_file"]
2
+
3
+
4
+ from ._argv import load_from_argv
5
+ from ._environ import load_from_environ
6
+ from ._file import load_from_file
7
+ from ._load import load
@@ -0,0 +1,41 @@
1
+ __all__ = ["load_from_argv"]
2
+
3
+ import sys
4
+ from itertools import islice
5
+ from typing import Any
6
+ from typing import Callable
7
+ from typing import Iterable
8
+
9
+ from ._parse import store_settings
10
+
11
+
12
+ def load_from_argv(
13
+ argv: Iterable[str] | None = None,
14
+ *,
15
+ on_extra: Callable[[str], None] = lambda n: None,
16
+ on_failure: Callable[[str, str | None], None] = lambda n, v: None,
17
+ prefix: str = "--config.",
18
+ ) -> dict[str, Any]:
19
+ settings: dict[str, Any] = {}
20
+
21
+ if argv is None:
22
+ argv = sys.argv[1:]
23
+
24
+ argv_i = iter(argv)
25
+ while True:
26
+ try:
27
+ arg = next(argv_i)
28
+ except StopIteration:
29
+ break
30
+ if arg.startswith(prefix):
31
+ raw_key = arg[len(prefix) :]
32
+ try:
33
+ raw_value = next(argv_i)
34
+ except StopIteration:
35
+ on_failure(arg, None)
36
+ continue
37
+ store_settings(settings, raw_key, raw_value, lambda: on_failure(arg, raw_value))
38
+ else:
39
+ on_extra(arg)
40
+
41
+ return settings
@@ -0,0 +1,37 @@
1
+ __all__ = ["load_from_environ"]
2
+
3
+ import os
4
+ from itertools import islice
5
+ from os import environ
6
+ from re import sub as re_sub
7
+ from types import NoneType
8
+ from types import UnionType
9
+ from typing import Any
10
+ from typing import Callable
11
+ from typing import Final
12
+ from typing import Mapping
13
+ from typing import Sequence
14
+ from typing import Union
15
+ from typing import get_args as get_typing_args
16
+ from typing import get_origin as get_typing_origin
17
+
18
+ from ._parse import store_settings
19
+
20
+
21
+ def load_from_environ(
22
+ environ: Mapping[str, str] | None = None,
23
+ *,
24
+ prefix: str = "CONFIG.",
25
+ on_failure: Callable[[str, str], None] = lambda n, v: None,
26
+ ) -> dict[str, Any]:
27
+ settings: dict[str, Any] = {}
28
+
29
+ if environ is None:
30
+ environ = os.environ
31
+
32
+ for key, raw_value in environ.items():
33
+ if key.startswith(prefix):
34
+ raw_key = key[len(prefix) :]
35
+ store_settings(settings, raw_key, raw_value, lambda: on_failure(key, raw_value))
36
+
37
+ return settings
@@ -0,0 +1,21 @@
1
+ __all__ = ["load_from_file"]
2
+
3
+ import tomllib
4
+ from pathlib import Path
5
+ from typing import Any
6
+ from typing import Callable
7
+
8
+
9
+ def load_from_file(
10
+ base_path: Path,
11
+ *,
12
+ name: Path = Path("config.toml"),
13
+ on_failure: Callable[[Path], None] = lambda p: None,
14
+ ) -> dict[str, Any]:
15
+ file_path = base_path / name
16
+ try:
17
+ with open(file_path, "rb") as file:
18
+ return tomllib.load(file)
19
+ except (OSError, tomllib.TOMLDecodeError):
20
+ on_failure(file_path)
21
+ return {}
@@ -0,0 +1,100 @@
1
+ __all__ = ["load"]
2
+
3
+ import os
4
+ import re
5
+ import sys
6
+ from logging import getLogger
7
+ from pathlib import Path
8
+ from typing import Any
9
+ from typing import Mapping
10
+
11
+ from deep_chainmap import DeepChainMap
12
+ from platformdirs import user_data_dir
13
+
14
+ from ._argv import load_from_argv
15
+ from ._environ import load_from_environ
16
+ from ._file import load_from_file
17
+
18
+ _log = getLogger("alltoml")
19
+
20
+
21
+ def load(
22
+ application_name: str,
23
+ application_author: str,
24
+ *,
25
+ default_settings: Mapping[str, Any] | None = None,
26
+ ) -> Mapping[str, Any]:
27
+ if default_settings is None:
28
+ default_settings = {}
29
+ else:
30
+ default_settings = {**default_settings}
31
+ assert isinstance(default_settings, dict)
32
+
33
+ if application_name.strip():
34
+ base_env_prefix = re.sub(r"[\-\s_]+", "_", application_name.strip()).upper()
35
+ environ_prefix = f"{base_env_prefix}_CONFIG."
36
+ file_environ_key = f"{base_env_prefix}_CONFIG"
37
+ else:
38
+ environ_prefix = "CONFIG."
39
+ file_environ_key = "CONFIG"
40
+
41
+ file_path: Path | None = None
42
+ # try to find the file path in the environ
43
+ try:
44
+ file_path = Path(os.environ[file_environ_key])
45
+ except KeyError:
46
+ pass
47
+ # try to find the file path in the argv, this will take precedence of the one found in environ
48
+ #
49
+ # we remove the arguments from the argv list that load_from_argv will scan so that we don't get
50
+ # errors about extra arguments
51
+ argv = sys.argv[1:]
52
+ for i in range(len(argv)):
53
+ if argv[i] == "--config":
54
+ try:
55
+ file_path = Path(argv[i + 1])
56
+ except IndexError:
57
+ _log.error("argument %r has no value", "--config")
58
+ sys.exit(1)
59
+ argv = [*argv[:i], *argv[i + 2 :]]
60
+ break
61
+ # try to load file settings from the path specified by either the environ or argv
62
+ if file_path is None:
63
+ file_settings = {}
64
+ else:
65
+ file_base_path = file_path.parent
66
+ file_name = Path(file_path.name)
67
+ file_settings = load_from_file(file_base_path, name=file_name, on_failure=_file_on_failure)
68
+
69
+ user_file_settings = load_from_file(
70
+ Path(user_data_dir(application_name, application_author)), on_failure=_file_on_failure
71
+ )
72
+ cwd_file_settings = load_from_file(Path("."), on_failure=_file_on_failure)
73
+ environ_settings = load_from_environ(prefix=environ_prefix, on_failure=_environ_on_failure)
74
+ argv_settings = load_from_argv(argv, on_extra=_argv_on_extra, on_failure=_argv_on_failure)
75
+
76
+ return DeepChainMap(
77
+ argv_settings,
78
+ file_settings,
79
+ cwd_file_settings,
80
+ user_file_settings,
81
+ environ_settings,
82
+ default_settings,
83
+ )
84
+
85
+
86
+ def _environ_on_failure(key: str, value: str) -> None:
87
+ _log.warning("ignoring invalid environment variable: %r", key)
88
+
89
+
90
+ def _file_on_failure(file_path: Path) -> None:
91
+ _log.warning("ignoring invalid config file: %r", str(file_path))
92
+
93
+
94
+ def _argv_on_extra(argument: str) -> None:
95
+ _log.error("argument %r was unexpected", argument)
96
+ sys.exit(1)
97
+
98
+
99
+ def _argv_on_failure(argument: str, value: str | None) -> None:
100
+ _log.warning("ignoring invalid argument: %r", argument)
@@ -0,0 +1,60 @@
1
+ __all__ = ["store_settings"]
2
+
3
+ from itertools import islice
4
+ from tomllib import TOMLDecodeError
5
+ from tomllib import loads as toml_loads
6
+ from typing import Any
7
+ from typing import Callable
8
+ from typing import Generator
9
+
10
+
11
+ def store_settings(
12
+ settings: dict[str, Any], raw_key: str, raw_value: str, fail: Callable[[], None]
13
+ ) -> None:
14
+ try:
15
+ key = tuple(_convert_key(raw_key))
16
+ value = _convert_value(raw_value)
17
+ except ValueError:
18
+ fail()
19
+ return
20
+
21
+ target = settings
22
+ for name in islice(key, len(key) - 1):
23
+ try:
24
+ target = target[name]
25
+ if not isinstance(target, dict):
26
+ fail()
27
+ return
28
+ except KeyError:
29
+ target[name] = target = {}
30
+ if key[-1] in target:
31
+ fail()
32
+ return
33
+ target[key[-1]] = value
34
+
35
+
36
+ def _convert_value(raw_value: str) -> Any:
37
+ try:
38
+ result = toml_loads(f"value = {raw_value}")
39
+ except TOMLDecodeError:
40
+ raise ValueError(raw_value)
41
+ if set(result.keys()) != {"value"}:
42
+ raise ValueError(raw_value)
43
+ return result["value"]
44
+
45
+
46
+ def _convert_key(raw_key: str) -> Generator[str, None, None]:
47
+ try:
48
+ result = toml_loads(f"{raw_key} = 0")
49
+ except TOMLDecodeError:
50
+ raise ValueError(raw_key)
51
+ while True:
52
+ if len(result) > 1:
53
+ raise ValueError(raw_key)
54
+ for key, value in result.items():
55
+ yield key
56
+ result = value
57
+ if not isinstance(result, dict):
58
+ if result == 0:
59
+ return
60
+ raise ValueError(raw_key)
File without changes