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,59 @@
|
|
|
1
|
+
"""Domain error taxonomy shared across layers.
|
|
2
|
+
|
|
3
|
+
Codifies the error classes referenced throughout ``docs/systemdesign`` so the
|
|
4
|
+
application and adapter layers can communicate failures without depending on
|
|
5
|
+
concrete implementations.
|
|
6
|
+
|
|
7
|
+
Contents:
|
|
8
|
+
- ``ConfigError``: base class for every library-specific exception.
|
|
9
|
+
- ``InvalidFormatError``: raised when structured configuration cannot be parsed.
|
|
10
|
+
- ``ValidationError``: reserved for semantic validation of configuration
|
|
11
|
+
payloads once implemented.
|
|
12
|
+
- ``NotFoundError``: indicates optional configuration sources were absent.
|
|
13
|
+
|
|
14
|
+
System Role:
|
|
15
|
+
Adapters raise these exceptions; the composition root and CLI translate them
|
|
16
|
+
into operator-facing messages without leaking implementation details.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"ConfigError",
|
|
23
|
+
"InvalidFormatError",
|
|
24
|
+
"ValidationError",
|
|
25
|
+
"NotFoundError",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ConfigError(Exception):
|
|
30
|
+
"""Base class for all configuration-related errors in the library.
|
|
31
|
+
|
|
32
|
+
Centralises exception handling so callers can catch a single type when
|
|
33
|
+
operating at library boundaries.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class InvalidFormatError(ConfigError):
|
|
38
|
+
"""Raised when a configuration source cannot be parsed.
|
|
39
|
+
|
|
40
|
+
Typical sources include malformed TOML, JSON, YAML, or dotenv files. The
|
|
41
|
+
message should reference the offending path for operator debugging.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class ValidationError(ConfigError):
|
|
46
|
+
"""Placeholder for semantic configuration validation failures.
|
|
47
|
+
|
|
48
|
+
The current release does not perform semantic validation, but the class is
|
|
49
|
+
reserved so downstream integrations already depend on a stable type.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class NotFoundError(ConfigError):
|
|
54
|
+
"""Indicates an optional configuration source was not discovered.
|
|
55
|
+
|
|
56
|
+
Used when files, directory entries, or environment variable namespaces are
|
|
57
|
+
genuinely missing; callers generally treat this as informational rather than
|
|
58
|
+
fatal.
|
|
59
|
+
"""
|
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
"""Identifier validation and layer enumeration.
|
|
2
|
+
|
|
3
|
+
Provide safe identifier handling and layer name constants used throughout the
|
|
4
|
+
library, preventing path traversal attacks and ensuring cross-platform filesystem
|
|
5
|
+
compatibility.
|
|
6
|
+
|
|
7
|
+
Contents:
|
|
8
|
+
- ``Layer``: enumeration of configuration layer names.
|
|
9
|
+
- ``validate_path_segment``: core validation for filesystem path segments (strict, no spaces).
|
|
10
|
+
- ``validate_identifier``: validate slug/profile identifiers (strict, no spaces).
|
|
11
|
+
- ``validate_vendor_app``: validate vendor/app names (permissive, allows spaces).
|
|
12
|
+
- ``validate_profile``: validate optional profile names (strict, no spaces).
|
|
13
|
+
- ``validate_hostname``: validate hostname for filesystem paths (allows dots for FQDN).
|
|
14
|
+
|
|
15
|
+
Validation Strategy:
|
|
16
|
+
- **vendor/app**: Use ``validate_vendor_app()`` - allows spaces for macOS/Windows paths
|
|
17
|
+
(e.g., ``/Library/Application Support/Acme Corp/My App/``).
|
|
18
|
+
- **slug/profile**: Use ``validate_identifier()`` - strict, no spaces allowed
|
|
19
|
+
(used in Linux paths and environment variable prefixes).
|
|
20
|
+
- **hostname**: Use ``validate_hostname()`` - allows dots for FQDNs.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import re
|
|
26
|
+
from enum import Enum
|
|
27
|
+
|
|
28
|
+
# Windows reserved device names (case-insensitive)
|
|
29
|
+
_WINDOWS_RESERVED_NAMES: frozenset[str] = frozenset(
|
|
30
|
+
{
|
|
31
|
+
"CON",
|
|
32
|
+
"PRN",
|
|
33
|
+
"AUX",
|
|
34
|
+
"NUL",
|
|
35
|
+
"COM1",
|
|
36
|
+
"COM2",
|
|
37
|
+
"COM3",
|
|
38
|
+
"COM4",
|
|
39
|
+
"COM5",
|
|
40
|
+
"COM6",
|
|
41
|
+
"COM7",
|
|
42
|
+
"COM8",
|
|
43
|
+
"COM9",
|
|
44
|
+
"LPT1",
|
|
45
|
+
"LPT2",
|
|
46
|
+
"LPT3",
|
|
47
|
+
"LPT4",
|
|
48
|
+
"LPT5",
|
|
49
|
+
"LPT6",
|
|
50
|
+
"LPT7",
|
|
51
|
+
"LPT8",
|
|
52
|
+
"LPT9",
|
|
53
|
+
}
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
# Characters invalid in filenames on Windows and/or problematic on Linux
|
|
57
|
+
# < > : " | ? * / \ and null byte
|
|
58
|
+
_INVALID_CHARS_PATTERN: re.Pattern[str] = re.compile(r'[<>:"|?*\\/\x00]')
|
|
59
|
+
|
|
60
|
+
# Valid strict identifier pattern: ASCII alphanumeric, hyphen, underscore, dot (not at start)
|
|
61
|
+
# Used for: slug, profile
|
|
62
|
+
_VALID_IDENTIFIER_PATTERN: re.Pattern[str] = re.compile(r"^[a-zA-Z0-9][a-zA-Z0-9._-]*$")
|
|
63
|
+
|
|
64
|
+
# Valid permissive identifier pattern: ASCII alphanumeric, hyphen, underscore, dot, space
|
|
65
|
+
# Used for: vendor, app (which can have spaces on macOS/Windows paths)
|
|
66
|
+
_VALID_PERMISSIVE_PATTERN: re.Pattern[str] = re.compile(r"^[a-zA-Z0-9][a-zA-Z0-9._\- ]*$")
|
|
67
|
+
|
|
68
|
+
# Valid hostname pattern: ASCII alphanumeric, hyphen, dot (FQDN support)
|
|
69
|
+
# Hostnames can start with alphanumeric, contain hyphens and dots
|
|
70
|
+
_VALID_HOSTNAME_PATTERN: re.Pattern[str] = re.compile(r"^[a-zA-Z0-9][a-zA-Z0-9.-]*$")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# ============================================================================
|
|
74
|
+
# Validation Helper Functions (reduce cyclomatic complexity)
|
|
75
|
+
# ============================================================================
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _check_not_empty(value: str, name: str) -> None:
|
|
79
|
+
"""Raise ValueError if value is empty."""
|
|
80
|
+
if not value:
|
|
81
|
+
raise ValueError(f"{name} cannot be empty")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _check_ascii_only(value: str, name: str) -> None:
|
|
85
|
+
"""Raise ValueError if value contains non-ASCII characters."""
|
|
86
|
+
try:
|
|
87
|
+
value.encode("ascii")
|
|
88
|
+
except UnicodeEncodeError:
|
|
89
|
+
raise ValueError(f"{name} contains non-ASCII characters: {value}") from None
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _check_no_invalid_chars(value: str, name: str) -> None:
|
|
93
|
+
"""Raise ValueError if value contains filesystem-invalid characters."""
|
|
94
|
+
if _INVALID_CHARS_PATTERN.search(value):
|
|
95
|
+
raise ValueError(f"{name} contains invalid characters: {value}")
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _check_not_windows_reserved(value: str, name: str, *, split_on_space: bool = False) -> None:
|
|
99
|
+
"""Raise ValueError if value is a Windows reserved name."""
|
|
100
|
+
# Extract base name (before first dot, optionally before first space)
|
|
101
|
+
base_name = value.split(".")[0]
|
|
102
|
+
if split_on_space:
|
|
103
|
+
base_name = base_name.split()[0]
|
|
104
|
+
if base_name.upper() in _WINDOWS_RESERVED_NAMES:
|
|
105
|
+
raise ValueError(f"{name} is a Windows reserved name: {value}")
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _check_no_trailing_dot_or_space(value: str, name: str) -> None:
|
|
109
|
+
"""Raise ValueError if value ends with dot or space (Windows restriction)."""
|
|
110
|
+
if value.endswith(".") or value.endswith(" "):
|
|
111
|
+
raise ValueError(f"{name} cannot end with a dot or space: {value}")
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _check_permissive_pattern(value: str, name: str) -> None:
|
|
115
|
+
"""Check value matches permissive pattern (allows spaces for vendor/app)."""
|
|
116
|
+
if _VALID_PERMISSIVE_PATTERN.match(value):
|
|
117
|
+
return
|
|
118
|
+
if value.startswith("."):
|
|
119
|
+
raise ValueError(f"{name} cannot start with a dot: {value}")
|
|
120
|
+
if value.startswith("-") or value.startswith("_") or value.startswith(" "):
|
|
121
|
+
raise ValueError(f"{name} must start with an alphanumeric character: {value}")
|
|
122
|
+
raise ValueError(f"{name} contains invalid characters: {value}")
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _check_hostname_pattern(value: str, name: str) -> None:
|
|
126
|
+
"""Check value matches hostname pattern (alphanumeric, hyphen, dot)."""
|
|
127
|
+
if _VALID_HOSTNAME_PATTERN.match(value):
|
|
128
|
+
return
|
|
129
|
+
if value.startswith(".") or value.startswith("-"):
|
|
130
|
+
raise ValueError(f"{name} must start with an alphanumeric character: {value}")
|
|
131
|
+
raise ValueError(f"{name} contains invalid characters: {value}")
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _check_no_trailing_space(value: str, name: str) -> None:
|
|
135
|
+
"""Raise ValueError if value ends with space."""
|
|
136
|
+
if value.endswith(" "):
|
|
137
|
+
raise ValueError(f"{name} cannot end with a space: {value}")
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class Layer(str, Enum):
|
|
141
|
+
"""Configuration layer names in precedence order.
|
|
142
|
+
|
|
143
|
+
Replace magic strings with type-safe enumeration, enabling IDE completion
|
|
144
|
+
and preventing typos in layer name references.
|
|
145
|
+
|
|
146
|
+
Attributes:
|
|
147
|
+
DEFAULTS: Lowest precedence - bundled application defaults.
|
|
148
|
+
APP: System-wide application configuration.
|
|
149
|
+
HOST: Machine-specific overrides.
|
|
150
|
+
USER: Per-user preferences.
|
|
151
|
+
DOTENV: Project-local `.env` file values.
|
|
152
|
+
ENV: Environment variable overrides (highest precedence).
|
|
153
|
+
"""
|
|
154
|
+
|
|
155
|
+
DEFAULTS = "defaults"
|
|
156
|
+
APP = "app"
|
|
157
|
+
HOST = "host"
|
|
158
|
+
USER = "user"
|
|
159
|
+
DOTENV = "dotenv"
|
|
160
|
+
ENV = "env"
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def validate_path_segment(value: str, name: str, *, allow_dots: bool = False) -> str:
|
|
164
|
+
"""Validate a string for safe use as a filesystem path segment.
|
|
165
|
+
|
|
166
|
+
Ensures identifiers (vendor, app, slug, profile) are safe on Windows and Linux,
|
|
167
|
+
preventing path traversal attacks and encoding issues.
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
value: The string to validate.
|
|
171
|
+
name: Parameter name for error messages (e.g., "vendor", "slug").
|
|
172
|
+
allow_dots: If True, allow dots within the value (for hostnames).
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
The validated string (unchanged if valid).
|
|
176
|
+
|
|
177
|
+
Raises:
|
|
178
|
+
ValueError: When the value fails validation (empty, non-ASCII, invalid chars,
|
|
179
|
+
path separators, Windows reserved names, or trailing dot/space).
|
|
180
|
+
|
|
181
|
+
Examples:
|
|
182
|
+
>>> validate_path_segment("myapp", "slug")
|
|
183
|
+
'myapp'
|
|
184
|
+
>>> validate_path_segment("Acme", "vendor")
|
|
185
|
+
'Acme'
|
|
186
|
+
"""
|
|
187
|
+
_check_not_empty(value, name)
|
|
188
|
+
_check_ascii_only(value, name)
|
|
189
|
+
_check_no_invalid_chars(value, name)
|
|
190
|
+
_check_strict_pattern(value, name, allow_dots)
|
|
191
|
+
_check_not_windows_reserved(value, name)
|
|
192
|
+
_check_no_trailing_dot_or_space(value, name)
|
|
193
|
+
return value
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _check_strict_pattern(value: str, name: str, allow_dots: bool) -> None:
|
|
197
|
+
"""Check value matches strict identifier pattern (no spaces)."""
|
|
198
|
+
if allow_dots:
|
|
199
|
+
return
|
|
200
|
+
if _VALID_IDENTIFIER_PATTERN.match(value):
|
|
201
|
+
return
|
|
202
|
+
if value.startswith("."):
|
|
203
|
+
raise ValueError(f"{name} cannot start with a dot: {value}")
|
|
204
|
+
if value.startswith("-") or value.startswith("_"):
|
|
205
|
+
raise ValueError(f"{name} must start with an alphanumeric character: {value}")
|
|
206
|
+
raise ValueError(f"{name} contains invalid characters: {value}")
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def validate_identifier(value: str, name: str) -> str:
|
|
210
|
+
"""Validate a strict identifier (slug, profile) for filesystem safety.
|
|
211
|
+
|
|
212
|
+
Prevent path traversal attacks and ensure cross-platform filesystem compatibility
|
|
213
|
+
when identifiers are used to construct directory paths.
|
|
214
|
+
|
|
215
|
+
Note:
|
|
216
|
+
This is for strict identifiers (slug, profile) that should not contain spaces.
|
|
217
|
+
For vendor/app which allow spaces, use ``validate_vendor_app()``.
|
|
218
|
+
|
|
219
|
+
Args:
|
|
220
|
+
value: The identifier value to validate.
|
|
221
|
+
name: Parameter name for error messages (e.g., "slug", "profile").
|
|
222
|
+
|
|
223
|
+
Returns:
|
|
224
|
+
The validated identifier (unchanged if valid).
|
|
225
|
+
|
|
226
|
+
Raises:
|
|
227
|
+
ValueError: When the identifier contains invalid characters or patterns.
|
|
228
|
+
|
|
229
|
+
Examples:
|
|
230
|
+
>>> validate_identifier("myapp", "slug")
|
|
231
|
+
'myapp'
|
|
232
|
+
>>> validate_identifier("my-app_v2", "slug")
|
|
233
|
+
'my-app_v2'
|
|
234
|
+
>>> validate_identifier("../etc", "slug")
|
|
235
|
+
Traceback (most recent call last):
|
|
236
|
+
...
|
|
237
|
+
ValueError: slug contains invalid characters: ../etc
|
|
238
|
+
"""
|
|
239
|
+
return validate_path_segment(value, name, allow_dots=False)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def validate_vendor_app(value: str, name: str) -> str:
|
|
243
|
+
"""Validate vendor or app identifier for filesystem safety (allows spaces).
|
|
244
|
+
|
|
245
|
+
Vendor and app names are used in macOS and Windows paths which support spaces
|
|
246
|
+
(e.g., ``/Library/Application Support/Acme Corp/My App/``).
|
|
247
|
+
This function allows spaces while still preventing path traversal attacks.
|
|
248
|
+
|
|
249
|
+
Args:
|
|
250
|
+
value: The vendor or app value to validate.
|
|
251
|
+
name: Parameter name for error messages ("vendor" or "app").
|
|
252
|
+
|
|
253
|
+
Returns:
|
|
254
|
+
The validated value (unchanged if valid).
|
|
255
|
+
|
|
256
|
+
Raises:
|
|
257
|
+
ValueError: When the value contains invalid characters or patterns.
|
|
258
|
+
|
|
259
|
+
Examples:
|
|
260
|
+
>>> validate_vendor_app("Acme Corp", "vendor")
|
|
261
|
+
'Acme Corp'
|
|
262
|
+
>>> validate_vendor_app("My App", "app")
|
|
263
|
+
'My App'
|
|
264
|
+
>>> validate_vendor_app("Btx Fix Mcp", "app")
|
|
265
|
+
'Btx Fix Mcp'
|
|
266
|
+
>>> validate_vendor_app("../etc", "vendor")
|
|
267
|
+
Traceback (most recent call last):
|
|
268
|
+
...
|
|
269
|
+
ValueError: vendor contains invalid characters: ../etc
|
|
270
|
+
"""
|
|
271
|
+
_check_not_empty(value, name)
|
|
272
|
+
_check_ascii_only(value, name)
|
|
273
|
+
_check_no_invalid_chars(value, name)
|
|
274
|
+
_check_permissive_pattern(value, name)
|
|
275
|
+
_check_not_windows_reserved(value, name, split_on_space=True)
|
|
276
|
+
_check_no_trailing_dot_or_space(value, name)
|
|
277
|
+
return value
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def validate_profile(value: str | None) -> str | None:
|
|
281
|
+
"""Validate profile name or return None if not provided.
|
|
282
|
+
|
|
283
|
+
Profile names become path segments, so they must be validated against
|
|
284
|
+
path traversal attacks and ensured cross-platform filesystem compatibility.
|
|
285
|
+
|
|
286
|
+
Args:
|
|
287
|
+
value: The profile name to validate, or None for no profile.
|
|
288
|
+
|
|
289
|
+
Returns:
|
|
290
|
+
The validated profile name, or None if no profile.
|
|
291
|
+
|
|
292
|
+
Raises:
|
|
293
|
+
ValueError: When the profile name contains invalid characters.
|
|
294
|
+
|
|
295
|
+
Examples:
|
|
296
|
+
>>> validate_profile(None) is None
|
|
297
|
+
True
|
|
298
|
+
>>> validate_profile("test")
|
|
299
|
+
'test'
|
|
300
|
+
>>> validate_profile("prod-v1")
|
|
301
|
+
'prod-v1'
|
|
302
|
+
>>> validate_profile("../etc")
|
|
303
|
+
Traceback (most recent call last):
|
|
304
|
+
...
|
|
305
|
+
ValueError: profile contains invalid characters: ../etc
|
|
306
|
+
"""
|
|
307
|
+
if value is None:
|
|
308
|
+
return None
|
|
309
|
+
return validate_identifier(value, "profile")
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def validate_hostname(value: str) -> str:
|
|
313
|
+
"""Ensure hostname is safe for use in filesystem paths.
|
|
314
|
+
|
|
315
|
+
Hostnames are used to construct file paths like ``hosts/{hostname}.toml``.
|
|
316
|
+
While hostnames from ``socket.gethostname()`` are typically safe, defensive
|
|
317
|
+
validation prevents path traversal and ensures cross-platform safety.
|
|
318
|
+
|
|
319
|
+
Validation Rules:
|
|
320
|
+
1. Must not be empty
|
|
321
|
+
2. Must contain only ASCII characters
|
|
322
|
+
3. Must start with alphanumeric character
|
|
323
|
+
4. May contain alphanumeric, hyphen, and dot (for FQDNs)
|
|
324
|
+
5. Must not contain path separators or Windows-invalid characters
|
|
325
|
+
6. Must not be a Windows reserved name
|
|
326
|
+
|
|
327
|
+
Args:
|
|
328
|
+
value: The hostname to validate.
|
|
329
|
+
|
|
330
|
+
Returns:
|
|
331
|
+
The validated hostname (unchanged if valid).
|
|
332
|
+
|
|
333
|
+
Raises:
|
|
334
|
+
ValueError: When the hostname fails validation.
|
|
335
|
+
|
|
336
|
+
Examples:
|
|
337
|
+
>>> validate_hostname("web-server-01")
|
|
338
|
+
'web-server-01'
|
|
339
|
+
>>> validate_hostname("server.local")
|
|
340
|
+
'server.local'
|
|
341
|
+
>>> validate_hostname("../etc")
|
|
342
|
+
Traceback (most recent call last):
|
|
343
|
+
...
|
|
344
|
+
ValueError: hostname contains invalid characters: ../etc
|
|
345
|
+
>>> validate_hostname("café")
|
|
346
|
+
Traceback (most recent call last):
|
|
347
|
+
...
|
|
348
|
+
ValueError: hostname contains non-ASCII characters: café
|
|
349
|
+
"""
|
|
350
|
+
_check_not_empty(value, "hostname")
|
|
351
|
+
_check_ascii_only(value, "hostname")
|
|
352
|
+
_check_no_invalid_chars(value, "hostname")
|
|
353
|
+
_check_hostname_pattern(value, "hostname")
|
|
354
|
+
_check_not_windows_reserved(value, "hostname")
|
|
355
|
+
_check_no_trailing_space(value, "hostname")
|
|
356
|
+
return value
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
__all__ = [
|
|
360
|
+
"Layer",
|
|
361
|
+
"validate_path_segment",
|
|
362
|
+
"validate_identifier",
|
|
363
|
+
"validate_vendor_app",
|
|
364
|
+
"validate_profile",
|
|
365
|
+
"validate_hostname",
|
|
366
|
+
]
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Expose example-generation and deployment helpers as a tidy façade.
|
|
2
|
+
|
|
3
|
+
Purpose
|
|
4
|
+
Provide a single import point for notebooks and docs that showcase layered
|
|
5
|
+
configuration scenarios. Keeps consumers away from internal module layout.
|
|
6
|
+
|
|
7
|
+
Contents
|
|
8
|
+
- :func:`deploy_config`: copy template files into etc/xdg directories.
|
|
9
|
+
- :class:`ExampleSpec`: describes example assets to generate.
|
|
10
|
+
- :data:`DEFAULT_HOST_PLACEHOLDER`: default hostname marker for templates.
|
|
11
|
+
- :func:`generate_examples`: materialise example configs on disk.
|
|
12
|
+
|
|
13
|
+
System Integration
|
|
14
|
+
Re-exports live in the ``examples`` namespace so tutorials can call
|
|
15
|
+
``lib_layered_config.examples.generate_examples`` without traversing the
|
|
16
|
+
package internals.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
from .deploy import deploy_config
|
|
22
|
+
from .generate import DEFAULT_HOST_PLACEHOLDER, ExampleSpec, generate_examples
|
|
23
|
+
|
|
24
|
+
__all__ = (
|
|
25
|
+
"deploy_config",
|
|
26
|
+
"ExampleSpec",
|
|
27
|
+
"DEFAULT_HOST_PLACEHOLDER",
|
|
28
|
+
"generate_examples",
|
|
29
|
+
)
|