lib-layered-config 4.1.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.
- lib_layered_config/__init__.py +58 -0
- lib_layered_config/__init__conf__.py +74 -0
- lib_layered_config/__main__.py +18 -0
- lib_layered_config/_layers.py +310 -0
- lib_layered_config/_platform.py +166 -0
- lib_layered_config/adapters/__init__.py +13 -0
- lib_layered_config/adapters/_nested_keys.py +126 -0
- lib_layered_config/adapters/dotenv/__init__.py +1 -0
- lib_layered_config/adapters/dotenv/default.py +143 -0
- lib_layered_config/adapters/env/__init__.py +5 -0
- lib_layered_config/adapters/env/default.py +288 -0
- lib_layered_config/adapters/file_loaders/__init__.py +1 -0
- lib_layered_config/adapters/file_loaders/structured.py +376 -0
- lib_layered_config/adapters/path_resolvers/__init__.py +28 -0
- lib_layered_config/adapters/path_resolvers/_base.py +166 -0
- lib_layered_config/adapters/path_resolvers/_dotenv.py +74 -0
- lib_layered_config/adapters/path_resolvers/_linux.py +89 -0
- lib_layered_config/adapters/path_resolvers/_macos.py +93 -0
- lib_layered_config/adapters/path_resolvers/_windows.py +126 -0
- lib_layered_config/adapters/path_resolvers/default.py +194 -0
- lib_layered_config/application/__init__.py +12 -0
- lib_layered_config/application/merge.py +379 -0
- lib_layered_config/application/ports.py +115 -0
- lib_layered_config/cli/__init__.py +92 -0
- lib_layered_config/cli/common.py +381 -0
- lib_layered_config/cli/constants.py +12 -0
- lib_layered_config/cli/deploy.py +71 -0
- lib_layered_config/cli/fail.py +19 -0
- lib_layered_config/cli/generate.py +57 -0
- lib_layered_config/cli/info.py +29 -0
- lib_layered_config/cli/read.py +120 -0
- lib_layered_config/core.py +301 -0
- lib_layered_config/domain/__init__.py +7 -0
- lib_layered_config/domain/config.py +372 -0
- lib_layered_config/domain/errors.py +59 -0
- lib_layered_config/domain/identifiers.py +366 -0
- lib_layered_config/examples/__init__.py +29 -0
- lib_layered_config/examples/deploy.py +333 -0
- lib_layered_config/examples/generate.py +406 -0
- lib_layered_config/observability.py +209 -0
- lib_layered_config/py.typed +0 -0
- lib_layered_config/testing.py +46 -0
- lib_layered_config-4.1.0.dist-info/METADATA +3263 -0
- lib_layered_config-4.1.0.dist-info/RECORD +47 -0
- lib_layered_config-4.1.0.dist-info/WHEEL +4 -0
- lib_layered_config-4.1.0.dist-info/entry_points.txt +3 -0
- lib_layered_config-4.1.0.dist-info/licenses/LICENSE +22 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""Linux-specific path resolution strategy.
|
|
2
|
+
|
|
3
|
+
Implement path resolution following XDG Base Directory specification
|
|
4
|
+
and traditional ``/etc`` conventions.
|
|
5
|
+
|
|
6
|
+
Contents:
|
|
7
|
+
- ``LinuxStrategy``: yields paths for app, host, user, and dotenv layers.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from collections.abc import Iterable
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
from ._base import PlatformStrategy, collect_layer
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class LinuxStrategy(PlatformStrategy):
|
|
19
|
+
"""Resolve paths following Linux/XDG conventions.
|
|
20
|
+
|
|
21
|
+
Mirror the XDG specification and ``/etc`` conventions documented in the
|
|
22
|
+
system design.
|
|
23
|
+
|
|
24
|
+
Path Layouts:
|
|
25
|
+
- App: ``/etc/xdg/<slug>`` then ``/etc/<slug>``
|
|
26
|
+
- Host: ``<etc>/<slug>/hosts/<hostname>.toml``
|
|
27
|
+
- User: ``$XDG_CONFIG_HOME/<slug>`` or ``~/.config/<slug>``
|
|
28
|
+
- Dotenv: ``$XDG_CONFIG_HOME/<slug>/.env``
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def app_paths(self) -> Iterable[str]:
|
|
32
|
+
"""Yield Linux application-default configuration paths.
|
|
33
|
+
|
|
34
|
+
Provide deterministic discovery following XDG Base Directory specification.
|
|
35
|
+
Checks ``/etc/xdg/<slug>`` first (XDG system-wide default), then falls back
|
|
36
|
+
to ``/etc/<slug>`` for backwards compatibility.
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
Paths under ``/etc/xdg`` and ``/etc`` (or overridden root).
|
|
40
|
+
"""
|
|
41
|
+
etc_root = Path(self.ctx.env.get("LIB_LAYERED_CONFIG_ETC", "/etc"))
|
|
42
|
+
profile_seg = self._profile_segment()
|
|
43
|
+
# Check XDG-compliant location first
|
|
44
|
+
yield from collect_layer(etc_root / "xdg" / self.ctx.slug / profile_seg)
|
|
45
|
+
# Fall back to traditional /etc location for backwards compatibility
|
|
46
|
+
yield from collect_layer(etc_root / self.ctx.slug / profile_seg)
|
|
47
|
+
|
|
48
|
+
def host_paths(self) -> Iterable[str]:
|
|
49
|
+
"""Yield Linux host-specific configuration paths.
|
|
50
|
+
|
|
51
|
+
Allow installations to override defaults per hostname following XDG specification.
|
|
52
|
+
Checks ``/etc/xdg/<slug>/hosts`` first, then falls back to ``/etc/<slug>/hosts``.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
Host-level configuration paths (empty when missing).
|
|
56
|
+
"""
|
|
57
|
+
etc_root = Path(self.ctx.env.get("LIB_LAYERED_CONFIG_ETC", "/etc"))
|
|
58
|
+
profile_seg = self._profile_segment()
|
|
59
|
+
# Check XDG-compliant location first
|
|
60
|
+
xdg_candidate = etc_root / "xdg" / self.ctx.slug / profile_seg / "hosts" / f"{self.ctx.hostname}.toml"
|
|
61
|
+
if xdg_candidate.is_file():
|
|
62
|
+
yield str(xdg_candidate)
|
|
63
|
+
# Fall back to traditional /etc location for backwards compatibility
|
|
64
|
+
candidate = etc_root / self.ctx.slug / profile_seg / "hosts" / f"{self.ctx.hostname}.toml"
|
|
65
|
+
if candidate.is_file():
|
|
66
|
+
yield str(candidate)
|
|
67
|
+
|
|
68
|
+
def user_paths(self) -> Iterable[str]:
|
|
69
|
+
"""Yield Linux user-specific configuration paths.
|
|
70
|
+
|
|
71
|
+
Honour XDG directories while falling back to ``~/.config``.
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
User-level configuration paths.
|
|
75
|
+
"""
|
|
76
|
+
xdg = self.ctx.env.get("XDG_CONFIG_HOME")
|
|
77
|
+
base = Path(xdg) if xdg else Path.home() / ".config"
|
|
78
|
+
profile_seg = self._profile_segment()
|
|
79
|
+
yield from collect_layer(base / self.ctx.slug / profile_seg)
|
|
80
|
+
|
|
81
|
+
def dotenv_path(self) -> Path | None:
|
|
82
|
+
"""Return Linux-specific ``.env`` fallback path.
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
Path to ``$XDG_CONFIG_HOME/<slug>/.env``.
|
|
86
|
+
"""
|
|
87
|
+
base = Path(self.ctx.env.get("XDG_CONFIG_HOME", Path.home() / ".config"))
|
|
88
|
+
profile_seg = self._profile_segment()
|
|
89
|
+
return base / self.ctx.slug / profile_seg / ".env"
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""macOS-specific path resolution strategy.
|
|
2
|
+
|
|
3
|
+
Implement path resolution following macOS Application Support conventions.
|
|
4
|
+
|
|
5
|
+
Contents:
|
|
6
|
+
- ``MacOSStrategy``: yields paths for app, host, user, and dotenv layers.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from collections.abc import Iterable
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
from ._base import PlatformStrategy, collect_layer
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class MacOSStrategy(PlatformStrategy):
|
|
18
|
+
"""Resolve paths following macOS Application Support conventions.
|
|
19
|
+
|
|
20
|
+
Follow macOS conventions for vendor/app directories under
|
|
21
|
+
``/Library/Application Support`` and ``~/Library/Application Support``.
|
|
22
|
+
|
|
23
|
+
Path Layouts:
|
|
24
|
+
- App: ``/Library/Application Support/<Vendor>/<App>``
|
|
25
|
+
- Host: ``<app>/hosts/<hostname>.toml``
|
|
26
|
+
- User: ``~/Library/Application Support/<Vendor>/<App>``
|
|
27
|
+
- Dotenv: ``~/Library/Application Support/<Vendor>/<App>/.env``
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def _app_root(self) -> Path:
|
|
31
|
+
"""Return the base directory for system-wide Application Support.
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
``/Library/Application Support`` or overridden root.
|
|
35
|
+
"""
|
|
36
|
+
default_root = Path("/Library/Application Support")
|
|
37
|
+
return Path(self.ctx.env.get("LIB_LAYERED_CONFIG_MAC_APP_ROOT", default_root))
|
|
38
|
+
|
|
39
|
+
def _home_root(self) -> Path:
|
|
40
|
+
"""Return the base directory for user-level Application Support.
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
``~/Library/Application Support`` or overridden root.
|
|
44
|
+
"""
|
|
45
|
+
home_default = Path.home() / "Library/Application Support"
|
|
46
|
+
return Path(self.ctx.env.get("LIB_LAYERED_CONFIG_MAC_HOME_ROOT", home_default))
|
|
47
|
+
|
|
48
|
+
def app_paths(self) -> Iterable[str]:
|
|
49
|
+
"""Yield macOS application-default configuration paths.
|
|
50
|
+
|
|
51
|
+
Follow macOS Application Support directory conventions.
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
Application-level configuration paths.
|
|
55
|
+
"""
|
|
56
|
+
profile_seg = self._profile_segment()
|
|
57
|
+
yield from collect_layer(self._app_root() / self.ctx.vendor / self.ctx.app / profile_seg)
|
|
58
|
+
|
|
59
|
+
def host_paths(self) -> Iterable[str]:
|
|
60
|
+
"""Yield macOS host-specific configuration paths.
|
|
61
|
+
|
|
62
|
+
Support host overrides stored under ``hosts/<hostname>.toml`` within
|
|
63
|
+
Application Support.
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
Host-level macOS configuration paths (empty when missing).
|
|
67
|
+
"""
|
|
68
|
+
profile_seg = self._profile_segment()
|
|
69
|
+
candidate = (
|
|
70
|
+
self._app_root() / self.ctx.vendor / self.ctx.app / profile_seg / "hosts" / f"{self.ctx.hostname}.toml"
|
|
71
|
+
)
|
|
72
|
+
if candidate.is_file():
|
|
73
|
+
yield str(candidate)
|
|
74
|
+
|
|
75
|
+
def user_paths(self) -> Iterable[str]:
|
|
76
|
+
"""Yield macOS user-specific configuration paths.
|
|
77
|
+
|
|
78
|
+
Honour per-user Application Support directories with optional overrides.
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
User-level macOS configuration paths.
|
|
82
|
+
"""
|
|
83
|
+
profile_seg = self._profile_segment()
|
|
84
|
+
yield from collect_layer(self._home_root() / self.ctx.vendor / self.ctx.app / profile_seg)
|
|
85
|
+
|
|
86
|
+
def dotenv_path(self) -> Path | None:
|
|
87
|
+
"""Return macOS-specific ``.env`` fallback path.
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
Path to ``~/Library/Application Support/<Vendor>/<App>/.env``.
|
|
91
|
+
"""
|
|
92
|
+
profile_seg = self._profile_segment()
|
|
93
|
+
return self._home_root() / self.ctx.vendor / self.ctx.app / profile_seg / ".env"
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""Windows-specific path resolution strategy.
|
|
2
|
+
|
|
3
|
+
Implement path resolution following Windows ProgramData/AppData conventions.
|
|
4
|
+
|
|
5
|
+
Contents:
|
|
6
|
+
- ``WindowsStrategy``: yields paths for app, host, user, and dotenv layers.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from collections.abc import Iterable
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
from ._base import PlatformStrategy, collect_layer
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class WindowsStrategy(PlatformStrategy):
|
|
18
|
+
"""Resolve paths following Windows directory conventions.
|
|
19
|
+
|
|
20
|
+
Respect ``%ProgramData%`` and ``%APPDATA%/%LOCALAPPDATA%`` layouts with
|
|
21
|
+
override support for portable deployments.
|
|
22
|
+
|
|
23
|
+
Path Layouts:
|
|
24
|
+
- App: ``%ProgramData%/<Vendor>/<App>``
|
|
25
|
+
- Host: ``<app>/hosts/<hostname>.toml``
|
|
26
|
+
- User: ``%APPDATA%/<Vendor>/<App>`` (fallback to ``%LOCALAPPDATA%``)
|
|
27
|
+
- Dotenv: ``%APPDATA%/<Vendor>/<App>/.env``
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def _program_data_root(self) -> Path:
|
|
31
|
+
"""Return the base directory for ProgramData lookups.
|
|
32
|
+
|
|
33
|
+
Centralise overrides for ``%ProgramData%`` so tests can supply temporary roots.
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
Resolved ProgramData root directory.
|
|
37
|
+
"""
|
|
38
|
+
return Path(
|
|
39
|
+
self.ctx.env.get(
|
|
40
|
+
"LIB_LAYERED_CONFIG_PROGRAMDATA",
|
|
41
|
+
self.ctx.env.get("ProgramData", r"C:\ProgramData"),
|
|
42
|
+
)
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
def _appdata_root(self) -> Path:
|
|
46
|
+
"""Return the user AppData root used for ``%APPDATA%`` lookups.
|
|
47
|
+
|
|
48
|
+
Support overrides in tests or portable deployments.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
Resolved AppData root directory.
|
|
52
|
+
"""
|
|
53
|
+
return Path(
|
|
54
|
+
self.ctx.env.get(
|
|
55
|
+
"LIB_LAYERED_CONFIG_APPDATA",
|
|
56
|
+
self.ctx.env.get("APPDATA", Path.home() / "AppData" / "Roaming"),
|
|
57
|
+
)
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
def _localappdata_root(self) -> Path:
|
|
61
|
+
"""Return the fallback LocalAppData root.
|
|
62
|
+
|
|
63
|
+
Provide a deterministic fallback when ``%APPDATA%`` does not exist.
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
Resolved LocalAppData root directory.
|
|
67
|
+
"""
|
|
68
|
+
return Path(
|
|
69
|
+
self.ctx.env.get(
|
|
70
|
+
"LIB_LAYERED_CONFIG_LOCALAPPDATA",
|
|
71
|
+
self.ctx.env.get("LOCALAPPDATA", Path.home() / "AppData" / "Local"),
|
|
72
|
+
)
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
def app_paths(self) -> Iterable[str]:
|
|
76
|
+
"""Yield Windows application-default configuration paths.
|
|
77
|
+
|
|
78
|
+
Mirror ``%ProgramData%/<Vendor>/<App>`` layouts with override support.
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
Application-level Windows configuration paths.
|
|
82
|
+
"""
|
|
83
|
+
profile_seg = self._profile_segment()
|
|
84
|
+
base = self._program_data_root() / self.ctx.vendor / self.ctx.app / profile_seg
|
|
85
|
+
yield from collect_layer(base)
|
|
86
|
+
|
|
87
|
+
def host_paths(self) -> Iterable[str]:
|
|
88
|
+
"""Yield Windows host-specific configuration paths.
|
|
89
|
+
|
|
90
|
+
Enable host overrides within ``%ProgramData%/<Vendor>/<App>/hosts``.
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
Host-level Windows configuration paths.
|
|
94
|
+
"""
|
|
95
|
+
profile_seg = self._profile_segment()
|
|
96
|
+
base = self._program_data_root() / self.ctx.vendor / self.ctx.app / profile_seg
|
|
97
|
+
candidate = base / "hosts" / f"{self.ctx.hostname}.toml"
|
|
98
|
+
if candidate.is_file():
|
|
99
|
+
yield str(candidate)
|
|
100
|
+
|
|
101
|
+
def user_paths(self) -> Iterable[str]:
|
|
102
|
+
"""Yield Windows user-specific configuration paths.
|
|
103
|
+
|
|
104
|
+
Honour ``%APPDATA%`` with a fallback to ``%LOCALAPPDATA%`` for portable setups.
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
User-level Windows configuration paths.
|
|
108
|
+
"""
|
|
109
|
+
profile_seg = self._profile_segment()
|
|
110
|
+
roaming_base = self._appdata_root() / self.ctx.vendor / self.ctx.app / profile_seg
|
|
111
|
+
roaming_paths = list(collect_layer(roaming_base))
|
|
112
|
+
if roaming_paths:
|
|
113
|
+
yield from roaming_paths
|
|
114
|
+
return
|
|
115
|
+
|
|
116
|
+
local_base = self._localappdata_root() / self.ctx.vendor / self.ctx.app / profile_seg
|
|
117
|
+
yield from collect_layer(local_base)
|
|
118
|
+
|
|
119
|
+
def dotenv_path(self) -> Path | None:
|
|
120
|
+
"""Return Windows-specific ``.env`` fallback path.
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
Path to ``%APPDATA%/<Vendor>/<App>/.env``.
|
|
124
|
+
"""
|
|
125
|
+
profile_seg = self._profile_segment()
|
|
126
|
+
return self._appdata_root() / self.ctx.vendor / self.ctx.app / profile_seg / ".env"
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"""Filesystem path resolution using platform-specific strategies.
|
|
2
|
+
|
|
3
|
+
Implement the :class:`lib_layered_config.application.ports.PathResolver`
|
|
4
|
+
port using the Strategy pattern for clean platform-specific handling.
|
|
5
|
+
|
|
6
|
+
Contents:
|
|
7
|
+
- ``DefaultPathResolver``: public adapter consumed by the composition root.
|
|
8
|
+
- Platform strategies in ``_linux.py``, ``_macos.py``, ``_windows.py``.
|
|
9
|
+
|
|
10
|
+
System Integration:
|
|
11
|
+
Produces ordered path lists for the core merge pipeline. All filesystem
|
|
12
|
+
knowledge stays here so inner layers remain filesystem-agnostic.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import os
|
|
18
|
+
import socket
|
|
19
|
+
import sys
|
|
20
|
+
from collections.abc import Iterable
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
from ...domain.identifiers import (
|
|
24
|
+
validate_hostname,
|
|
25
|
+
validate_identifier,
|
|
26
|
+
validate_profile,
|
|
27
|
+
validate_vendor_app,
|
|
28
|
+
)
|
|
29
|
+
from ...observability import log_debug
|
|
30
|
+
from ._base import PlatformContext, PlatformStrategy
|
|
31
|
+
from ._dotenv import DotenvPathFinder
|
|
32
|
+
from ._linux import LinuxStrategy
|
|
33
|
+
from ._macos import MacOSStrategy
|
|
34
|
+
from ._windows import WindowsStrategy
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class DefaultPathResolver:
|
|
38
|
+
"""Resolve candidate paths for each configuration layer.
|
|
39
|
+
|
|
40
|
+
Centralise path discovery so the composition root stays platform-agnostic
|
|
41
|
+
and easy to test.
|
|
42
|
+
|
|
43
|
+
Architecture:
|
|
44
|
+
Uses the Strategy pattern to delegate platform-specific logic to dedicated
|
|
45
|
+
classes (``LinuxStrategy``, ``MacOSStrategy``, ``WindowsStrategy``),
|
|
46
|
+
keeping the main resolver focused on orchestration.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
def __init__(
|
|
50
|
+
self,
|
|
51
|
+
*,
|
|
52
|
+
vendor: str,
|
|
53
|
+
app: str,
|
|
54
|
+
slug: str,
|
|
55
|
+
profile: str | None = None,
|
|
56
|
+
cwd: Path | None = None,
|
|
57
|
+
env: dict[str, str] | None = None,
|
|
58
|
+
platform: str | None = None,
|
|
59
|
+
hostname: str | None = None,
|
|
60
|
+
) -> None:
|
|
61
|
+
"""Store context required to resolve filesystem locations.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
vendor / app / slug: Naming context injected into platform-specific directory structures.
|
|
65
|
+
profile: Optional profile name for environment-specific configurations
|
|
66
|
+
(e.g., "test", "production"). When set, paths include a
|
|
67
|
+
``profile/<name>/`` subdirectory.
|
|
68
|
+
cwd: Working directory to use when searching for ``.env`` files.
|
|
69
|
+
env: Optional environment mapping that overrides ``os.environ`` values
|
|
70
|
+
(useful for deterministic tests).
|
|
71
|
+
platform: Platform identifier (``sys.platform`` clone). Defaults to the
|
|
72
|
+
current interpreter platform.
|
|
73
|
+
hostname: Hostname used for host-specific configuration lookups.
|
|
74
|
+
|
|
75
|
+
Raises:
|
|
76
|
+
ValueError: When vendor, app, slug, profile, or hostname contain invalid path characters.
|
|
77
|
+
"""
|
|
78
|
+
self.vendor = validate_vendor_app(vendor, "vendor")
|
|
79
|
+
self.application = validate_vendor_app(app, "app")
|
|
80
|
+
self.slug = validate_identifier(slug, "slug")
|
|
81
|
+
self.profile = validate_profile(profile)
|
|
82
|
+
self.cwd = cwd or Path.cwd()
|
|
83
|
+
self.env = {**os.environ, **(env or {})}
|
|
84
|
+
self.platform = platform or sys.platform
|
|
85
|
+
self.hostname = validate_hostname(hostname or socket.gethostname())
|
|
86
|
+
|
|
87
|
+
# Build the platform context and select the appropriate strategy
|
|
88
|
+
self._ctx = PlatformContext(
|
|
89
|
+
vendor=self.vendor,
|
|
90
|
+
app=self.application,
|
|
91
|
+
slug=self.slug,
|
|
92
|
+
cwd=self.cwd,
|
|
93
|
+
env=self.env,
|
|
94
|
+
hostname=self.hostname,
|
|
95
|
+
profile=self.profile,
|
|
96
|
+
)
|
|
97
|
+
self._strategy = self._select_strategy()
|
|
98
|
+
self._dotenv_finder = DotenvPathFinder(self.cwd, self._strategy)
|
|
99
|
+
|
|
100
|
+
@property
|
|
101
|
+
def strategy(self) -> PlatformStrategy | None:
|
|
102
|
+
"""Return the current platform strategy for direct access in tests."""
|
|
103
|
+
return self._strategy
|
|
104
|
+
|
|
105
|
+
@property
|
|
106
|
+
def context(self) -> PlatformContext:
|
|
107
|
+
"""Return the platform context for direct access in tests."""
|
|
108
|
+
return self._ctx
|
|
109
|
+
|
|
110
|
+
def _select_strategy(self) -> PlatformStrategy | None:
|
|
111
|
+
"""Select the appropriate platform strategy based on the current platform."""
|
|
112
|
+
if self.platform.startswith("linux"):
|
|
113
|
+
return LinuxStrategy(self._ctx)
|
|
114
|
+
if self.platform == "darwin":
|
|
115
|
+
return MacOSStrategy(self._ctx)
|
|
116
|
+
if self.platform.startswith("win"):
|
|
117
|
+
return WindowsStrategy(self._ctx)
|
|
118
|
+
return None
|
|
119
|
+
|
|
120
|
+
def app(self) -> Iterable[str]:
|
|
121
|
+
"""Return candidate system-wide configuration paths.
|
|
122
|
+
|
|
123
|
+
Provide the lowest-precedence defaults shared across machines.
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
Ordered path strings for the application defaults layer.
|
|
127
|
+
|
|
128
|
+
Examples:
|
|
129
|
+
>>> import os
|
|
130
|
+
>>> from pathlib import Path
|
|
131
|
+
>>> from tempfile import TemporaryDirectory
|
|
132
|
+
>>> tmp = TemporaryDirectory()
|
|
133
|
+
>>> root = Path(tmp.name)
|
|
134
|
+
>>> (root / 'demo').mkdir(parents=True, exist_ok=True)
|
|
135
|
+
>>> body = os.linesep.join(['[settings]', 'value=1'])
|
|
136
|
+
>>> _ = (root / 'demo' / 'config.toml').write_text(body, encoding='utf-8')
|
|
137
|
+
>>> resolver = DefaultPathResolver(vendor='Acme', app='Demo', slug='demo', env={'LIB_LAYERED_CONFIG_ETC': str(root)}, platform='linux')
|
|
138
|
+
>>> [Path(p).name for p in resolver.app()]
|
|
139
|
+
['config.toml']
|
|
140
|
+
>>> tmp.cleanup()
|
|
141
|
+
"""
|
|
142
|
+
return self._iter_layer("app")
|
|
143
|
+
|
|
144
|
+
def host(self) -> Iterable[str]:
|
|
145
|
+
"""Return host-specific overrides.
|
|
146
|
+
|
|
147
|
+
Allow operators to tailor configuration to individual hosts (e.g.
|
|
148
|
+
``demo-host.toml``).
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
Ordered host-level configuration paths.
|
|
152
|
+
"""
|
|
153
|
+
return self._iter_layer("host")
|
|
154
|
+
|
|
155
|
+
def user(self) -> Iterable[str]:
|
|
156
|
+
"""Return user-level configuration locations.
|
|
157
|
+
|
|
158
|
+
Capture per-user preferences stored in XDG/macOS/Windows user config
|
|
159
|
+
directories.
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
Ordered user-level configuration paths.
|
|
163
|
+
"""
|
|
164
|
+
return self._iter_layer("user")
|
|
165
|
+
|
|
166
|
+
def dotenv(self) -> Iterable[str]:
|
|
167
|
+
"""Return candidate ``.env`` locations discovered during path resolution.
|
|
168
|
+
|
|
169
|
+
`.env` files often live near the project root; this helper provides the
|
|
170
|
+
ordered search list for the dotenv adapter.
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
Ordered `.env` path strings.
|
|
174
|
+
"""
|
|
175
|
+
return list(self._dotenv_finder.find_paths())
|
|
176
|
+
|
|
177
|
+
def _iter_layer(self, layer: str) -> list[str]:
|
|
178
|
+
"""Dispatch to the strategy for *layer* with logging."""
|
|
179
|
+
if self._strategy is None:
|
|
180
|
+
return []
|
|
181
|
+
|
|
182
|
+
method_map = {
|
|
183
|
+
"app": self._strategy.app_paths,
|
|
184
|
+
"host": self._strategy.host_paths,
|
|
185
|
+
"user": self._strategy.user_paths,
|
|
186
|
+
}
|
|
187
|
+
method = method_map.get(layer)
|
|
188
|
+
if method is None:
|
|
189
|
+
return []
|
|
190
|
+
|
|
191
|
+
paths = list(method())
|
|
192
|
+
if paths:
|
|
193
|
+
log_debug("path_candidates", layer=layer, path=None, count=len(paths))
|
|
194
|
+
return paths
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Application layer orchestrators (ports + merge policy).
|
|
2
|
+
|
|
3
|
+
Purpose
|
|
4
|
+
-------
|
|
5
|
+
Define pure coordination code that glues domain value objects to adapter
|
|
6
|
+
interfaces.
|
|
7
|
+
|
|
8
|
+
Contents
|
|
9
|
+
--------
|
|
10
|
+
* :mod:`lib_layered_config.application.ports`
|
|
11
|
+
* :mod:`lib_layered_config.application.merge`
|
|
12
|
+
"""
|