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 +16 -0
- alltoml-0.0.0/PKG-INFO +12 -0
- alltoml-0.0.0/pyproject.toml +28 -0
- alltoml-0.0.0/src/alltoml/__init__.py +7 -0
- alltoml-0.0.0/src/alltoml/_argv.py +41 -0
- alltoml-0.0.0/src/alltoml/_environ.py +37 -0
- alltoml-0.0.0/src/alltoml/_file.py +21 -0
- alltoml-0.0.0/src/alltoml/_load.py +100 -0
- alltoml-0.0.0/src/alltoml/_parse.py +60 -0
- alltoml-0.0.0/src/alltoml/py.typed +0 -0
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,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
|