pal-found-cli 0.1.2__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.
- pal_found_cli/__init__.py +1 -0
- pal_found_cli/_version.py +24 -0
- pal_found_cli/admin/__init__.py +2 -0
- pal_found_cli/admin/scripts/__init__.py +2 -0
- pal_found_cli/admin/scripts/pal_found_admin_cli.py +600 -0
- pal_found_cli/aip_agents/__init__.py +1 -0
- pal_found_cli/aip_agents/metadata-allow-list.md +20 -0
- pal_found_cli/aip_agents/scripts/__init__.py +1 -0
- pal_found_cli/aip_agents/scripts/pal_found_aip_agents_cli.py +704 -0
- pal_found_cli/audit/__init__.py +1 -0
- pal_found_cli/audit/metadata-allow-list.md +4 -0
- pal_found_cli/audit/scripts/__init__.py +1 -0
- pal_found_cli/audit/scripts/pal_found_audit_cli.py +387 -0
- pal_found_cli/checkpoints/__init__.py +9 -0
- pal_found_cli/checkpoints/metadata-allow-list.md +7 -0
- pal_found_cli/checkpoints/scripts/__init__.py +1 -0
- pal_found_cli/checkpoints/scripts/pal_found_checkpoints_cli.py +527 -0
- pal_found_cli/common/__init__.py +56 -0
- pal_found_cli/common/access_control_guard.py +591 -0
- pal_found_cli/common/async_client_factory.py +161 -0
- pal_found_cli/common/auth_provider.py +82 -0
- pal_found_cli/common/binary_download_handler.py +288 -0
- pal_found_cli/common/config_loader.py +388 -0
- pal_found_cli/common/error_serializer.py +412 -0
- pal_found_cli/common/log_setup.py +241 -0
- pal_found_cli/common/metadata-allow-list.md +169 -0
- pal_found_cli/common/output_formatter.py +267 -0
- pal_found_cli/common/pagination_helper.py +212 -0
- pal_found_cli/common/retry.py +617 -0
- pal_found_cli/common/sdk_error_utils.py +112 -0
- pal_found_cli/common/session_manager.py +461 -0
- pal_found_cli/common/tracing_provider.py +107 -0
- pal_found_cli/connectivity/__init__.py +9 -0
- pal_found_cli/connectivity/metadata-allow-list.md +24 -0
- pal_found_cli/connectivity/scripts/__init__.py +1 -0
- pal_found_cli/connectivity/scripts/pal_found_connectivity_cli.py +720 -0
- pal_found_cli/data_health/__init__.py +9 -0
- pal_found_cli/data_health/metadata-allow-list.md +10 -0
- pal_found_cli/data_health/scripts/__init__.py +1 -0
- pal_found_cli/data_health/scripts/pal_found_data_health_cli.py +458 -0
- pal_found_cli/datasets/__init__.py +1 -0
- pal_found_cli/datasets/scripts/__init__.py +1 -0
- pal_found_cli/datasets/scripts/pal_found_datasets_cli.py +732 -0
- pal_found_cli/filesystem/__init__.py +2 -0
- pal_found_cli/filesystem/scripts/__init__.py +2 -0
- pal_found_cli/filesystem/scripts/pal_found_filesystem_cli.py +659 -0
- pal_found_cli/functions/__init__.py +2 -0
- pal_found_cli/functions/scripts/__init__.py +2 -0
- pal_found_cli/functions/scripts/pal_found_functions_cli.py +389 -0
- pal_found_cli/language_models/__init__.py +1 -0
- pal_found_cli/language_models/metadata-allow-list.md +6 -0
- pal_found_cli/language_models/scripts/__init__.py +1 -0
- pal_found_cli/language_models/scripts/pal_found_language_models_cli.py +342 -0
- pal_found_cli/media_sets/__init__.py +9 -0
- pal_found_cli/media_sets/metadata-allow-list.md +23 -0
- pal_found_cli/media_sets/scripts/__init__.py +1 -0
- pal_found_cli/media_sets/scripts/pal_found_media_sets_cli.py +642 -0
- pal_found_cli/models/__init__.py +5 -0
- pal_found_cli/models/metadata-allow-list.md +27 -0
- pal_found_cli/models/scripts/__init__.py +1 -0
- pal_found_cli/models/scripts/pal_found_models_cli.py +714 -0
- pal_found_cli/ontologies/__init__.py +1 -0
- pal_found_cli/ontologies/scripts/__init__.py +1 -0
- pal_found_cli/ontologies/scripts/pal_found_ontologies_cli.py +1225 -0
- pal_found_cli/orchestration/__init__.py +9 -0
- pal_found_cli/orchestration/metadata-allow-list.md +24 -0
- pal_found_cli/orchestration/scripts/__init__.py +1 -0
- pal_found_cli/orchestration/scripts/pal_found_orchestration_cli.py +640 -0
- pal_found_cli/sql_queries/__init__.py +9 -0
- pal_found_cli/sql_queries/metadata-allow-list.md +9 -0
- pal_found_cli/sql_queries/scripts/__init__.py +1 -0
- pal_found_cli/sql_queries/scripts/pal_found_sql_queries_cli.py +514 -0
- pal_found_cli/streams/__init__.py +9 -0
- pal_found_cli/streams/metadata-allow-list.md +19 -0
- pal_found_cli/streams/scripts/__init__.py +1 -0
- pal_found_cli/streams/scripts/pal_found_streams_cli.py +615 -0
- pal_found_cli/third_party_applications/__init__.py +9 -0
- pal_found_cli/third_party_applications/metadata-allow-list.md +13 -0
- pal_found_cli/third_party_applications/scripts/__init__.py +1 -0
- pal_found_cli/third_party_applications/scripts/pal_found_third_party_applications_cli.py +547 -0
- pal_found_cli/widgets/__init__.py +9 -0
- pal_found_cli/widgets/metadata-allow-list.md +12 -0
- pal_found_cli/widgets/scripts/__init__.py +1 -0
- pal_found_cli/widgets/scripts/pal_found_widgets_cli.py +571 -0
- pal_found_cli-0.1.2.dist-info/METADATA +74 -0
- pal_found_cli-0.1.2.dist-info/RECORD +89 -0
- pal_found_cli-0.1.2.dist-info/WHEEL +5 -0
- pal_found_cli-0.1.2.dist-info/entry_points.txt +19 -0
- pal_found_cli-0.1.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
"""ConfigLoader — .env file search path (ADR-006).
|
|
2
|
+
|
|
3
|
+
Loads configuration from .env file following ADR-006 search path order:
|
|
4
|
+
1. Explicit path via FOUNDRY_AGENTIC_CLI_ENV_FILE
|
|
5
|
+
2. Git root .env (walk up from CWD to find .git)
|
|
6
|
+
3. CWD .env fallback (non-git deployment)
|
|
7
|
+
4. Environment variables only (no .env file)
|
|
8
|
+
|
|
9
|
+
Never searches home directory.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import os
|
|
13
|
+
from enum import Enum
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import TypeVar
|
|
16
|
+
|
|
17
|
+
from dotenv import load_dotenv
|
|
18
|
+
|
|
19
|
+
from pal_found_cli.common.log_setup import DEFAULT_LOG_LEVEL, ENV_LOG_LEVEL
|
|
20
|
+
|
|
21
|
+
# Environment variable names
|
|
22
|
+
ENV_ENV_FILE = "FOUNDRY_AGENTIC_CLI_ENV_FILE"
|
|
23
|
+
ENV_TOKEN = "FOUNDRY_TOKEN" # nosec B105 - environment variable name, not a secret value.
|
|
24
|
+
ENV_HOSTNAME = "FOUNDRY_HOSTNAME"
|
|
25
|
+
ENV_TIMEOUT_S = "FOUNDRY_AGENTIC_CLI_TIMEOUT_S"
|
|
26
|
+
ENV_DEFAULT_FORMAT = "FOUNDRY_AGENTIC_CLI_DEFAULT_FORMAT"
|
|
27
|
+
ENV_ENABLED = "FOUNDRY_AGENTIC_CLI_ENABLED"
|
|
28
|
+
ENV_READONLY = "FOUNDRY_AGENTIC_CLI_READONLY"
|
|
29
|
+
ENV_METADATA_ONLY = "FOUNDRY_AGENTIC_CLI_METADATA_ONLY"
|
|
30
|
+
ENV_ENABLE_ATTRIBUTION = "FOUNDRY_AGENTIC_CLI_ENABLE_ATTRIBUTION"
|
|
31
|
+
ENV_ATTRIBUTION_RIDS = "FOUNDRY_AGENTIC_CLI_ATTRIBUTION_RIDS"
|
|
32
|
+
ENV_ENABLE_TRACING = "FOUNDRY_AGENTIC_CLI_ENABLE_TRACING"
|
|
33
|
+
ENV_MAX_DOWNLOAD_BYTES = "FOUNDRY_AGENTIC_CLI_MAX_DOWNLOAD_BYTES"
|
|
34
|
+
ENV_DOWNLOAD_PATH = "FOUNDRY_AGENTIC_CLI_DOWNLOAD_PATH"
|
|
35
|
+
ENV_SESSION_PATH = "FOUNDRY_AGENTIC_CLI_SESSION_PATH"
|
|
36
|
+
|
|
37
|
+
# Defaults
|
|
38
|
+
DEFAULT_TIMEOUT_S = 30
|
|
39
|
+
DEFAULT_MIN_TIMEOUT_S = 1
|
|
40
|
+
DEFAULT_MAX_TIMEOUT_S = 3600
|
|
41
|
+
DEFAULT_FORMAT = "auto"
|
|
42
|
+
DEFAULT_MAX_DOWNLOAD_BYTES = 1_572_864
|
|
43
|
+
DEFAULT_DOWNLOAD_PATH = Path(".foundry-data/downloads")
|
|
44
|
+
DEFAULT_SESSION_PATH = Path(".foundry-data/sessions")
|
|
45
|
+
|
|
46
|
+
# Exit code per ADR-001
|
|
47
|
+
EXIT_CONFIGURATION = 9
|
|
48
|
+
|
|
49
|
+
# Truthy boolean string set — kept consistent across property accessors
|
|
50
|
+
# and get_bool() so the parsing contract is identical everywhere.
|
|
51
|
+
_TRUTHY = ("true", "1", "yes", "on")
|
|
52
|
+
|
|
53
|
+
_E = TypeVar("_E", bound=Enum)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class ConfigLoader:
|
|
57
|
+
"""Loads configuration from .env file following ADR-006 search path.
|
|
58
|
+
|
|
59
|
+
Search path order:
|
|
60
|
+
1. Explicit: FOUNDRY_AGENTIC_CLI_ENV_FILE
|
|
61
|
+
2. Git root: Walk up from CWD to find .git, load .env there
|
|
62
|
+
3. Env vars only: No .env file, use shell environment
|
|
63
|
+
|
|
64
|
+
Usage
|
|
65
|
+
-----
|
|
66
|
+
>>> cfg = ConfigLoader()
|
|
67
|
+
>>> cfg.load()
|
|
68
|
+
>>> token = cfg.token
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
def __init__(self) -> None:
|
|
72
|
+
self._loaded_file: str | None = None
|
|
73
|
+
|
|
74
|
+
def load(self) -> None:
|
|
75
|
+
"""Load configuration following ADR-006 search path."""
|
|
76
|
+
# Order 1 — Explicit override
|
|
77
|
+
explicit_path = os.environ.get(ENV_ENV_FILE)
|
|
78
|
+
if explicit_path:
|
|
79
|
+
path = Path(explicit_path)
|
|
80
|
+
if not path.exists():
|
|
81
|
+
raise ConfigurationError(
|
|
82
|
+
f"Explicit .env file not found: {explicit_path}"
|
|
83
|
+
)
|
|
84
|
+
load_dotenv(str(path), override=False)
|
|
85
|
+
self._loaded_file = str(path)
|
|
86
|
+
return
|
|
87
|
+
|
|
88
|
+
# Order 2 — Git root .env
|
|
89
|
+
git_root = self._find_git_root()
|
|
90
|
+
if git_root:
|
|
91
|
+
env_path = git_root / ".env"
|
|
92
|
+
if env_path.exists():
|
|
93
|
+
load_dotenv(str(env_path), override=False)
|
|
94
|
+
self._loaded_file = str(env_path)
|
|
95
|
+
return
|
|
96
|
+
|
|
97
|
+
# Order 3 — CWD .env fallback
|
|
98
|
+
cwd_env = Path.cwd() / ".env"
|
|
99
|
+
if cwd_env.exists():
|
|
100
|
+
load_dotenv(str(cwd_env), override=False)
|
|
101
|
+
self._loaded_file = str(cwd_env)
|
|
102
|
+
return
|
|
103
|
+
|
|
104
|
+
# Order 4 — Env vars only
|
|
105
|
+
# No error — credentials may be in shell environment
|
|
106
|
+
|
|
107
|
+
def _find_git_root(self, max_depth: int = 20) -> Path | None:
|
|
108
|
+
"""Walk up from CWD to find .git directory.
|
|
109
|
+
|
|
110
|
+
Parameters
|
|
111
|
+
----------
|
|
112
|
+
max_depth : int
|
|
113
|
+
Maximum directory levels to walk up.
|
|
114
|
+
|
|
115
|
+
Returns
|
|
116
|
+
-------
|
|
117
|
+
Path or None
|
|
118
|
+
Git root directory or None if not found.
|
|
119
|
+
"""
|
|
120
|
+
current = Path.cwd()
|
|
121
|
+
depth = 0
|
|
122
|
+
while depth < max_depth:
|
|
123
|
+
if (current / ".git").exists():
|
|
124
|
+
return current
|
|
125
|
+
parent = current.parent
|
|
126
|
+
if parent == current:
|
|
127
|
+
break
|
|
128
|
+
current = parent
|
|
129
|
+
depth += 1
|
|
130
|
+
return None
|
|
131
|
+
|
|
132
|
+
# --- Typed accessors (generic, follow story AC) ---
|
|
133
|
+
|
|
134
|
+
def get_str(self, name: str, default: str | None = None) -> str | None:
|
|
135
|
+
"""Get string environment variable value.
|
|
136
|
+
|
|
137
|
+
Parameters
|
|
138
|
+
----------
|
|
139
|
+
name : str
|
|
140
|
+
Environment variable name.
|
|
141
|
+
default : str, optional
|
|
142
|
+
Default value if not set.
|
|
143
|
+
|
|
144
|
+
Returns
|
|
145
|
+
-------
|
|
146
|
+
str or None
|
|
147
|
+
Environment variable value, stripped of surrounding whitespace,
|
|
148
|
+
or ``default`` if unset.
|
|
149
|
+
"""
|
|
150
|
+
val = os.environ.get(name)
|
|
151
|
+
if val is None:
|
|
152
|
+
return default
|
|
153
|
+
return val.strip()
|
|
154
|
+
|
|
155
|
+
def get_bool(self, name: str, default: bool = False) -> bool:
|
|
156
|
+
"""Get boolean environment variable value.
|
|
157
|
+
|
|
158
|
+
Truthy values (case-insensitive): ``true``, ``1``, ``yes``, ``on``.
|
|
159
|
+
Everything else (including unset) evaluates per ``default``.
|
|
160
|
+
|
|
161
|
+
Parameters
|
|
162
|
+
----------
|
|
163
|
+
name : str
|
|
164
|
+
Environment variable name.
|
|
165
|
+
default : bool
|
|
166
|
+
Default value when the variable is unset.
|
|
167
|
+
|
|
168
|
+
Returns
|
|
169
|
+
-------
|
|
170
|
+
bool
|
|
171
|
+
Parsed boolean value.
|
|
172
|
+
"""
|
|
173
|
+
val = os.environ.get(name)
|
|
174
|
+
if val is None:
|
|
175
|
+
return default
|
|
176
|
+
return val.strip().lower() in _TRUTHY
|
|
177
|
+
|
|
178
|
+
def get_int(self, name: str, default: int | None = None) -> int | None:
|
|
179
|
+
"""Get integer environment variable value.
|
|
180
|
+
|
|
181
|
+
Parameters
|
|
182
|
+
----------
|
|
183
|
+
name : str
|
|
184
|
+
Environment variable name.
|
|
185
|
+
default : int, optional
|
|
186
|
+
Default value when unset or non-numeric.
|
|
187
|
+
|
|
188
|
+
Returns
|
|
189
|
+
-------
|
|
190
|
+
int or None
|
|
191
|
+
Parsed integer, or ``default`` when unset / unparsable.
|
|
192
|
+
"""
|
|
193
|
+
val = os.environ.get(name)
|
|
194
|
+
if val is None or val.strip() == "":
|
|
195
|
+
return default
|
|
196
|
+
try:
|
|
197
|
+
return int(val.strip())
|
|
198
|
+
except ValueError:
|
|
199
|
+
return default
|
|
200
|
+
|
|
201
|
+
def get_float(self, name: str, default: float | None = None) -> float | None:
|
|
202
|
+
"""Get float environment variable value.
|
|
203
|
+
|
|
204
|
+
Parameters
|
|
205
|
+
----------
|
|
206
|
+
name : str
|
|
207
|
+
Environment variable name.
|
|
208
|
+
default : float, optional
|
|
209
|
+
Default value when unset or non-numeric.
|
|
210
|
+
|
|
211
|
+
Returns
|
|
212
|
+
-------
|
|
213
|
+
float or None
|
|
214
|
+
Parsed float, or ``default`` when unset / unparsable.
|
|
215
|
+
"""
|
|
216
|
+
val = os.environ.get(name)
|
|
217
|
+
if val is None or val.strip() == "":
|
|
218
|
+
return default
|
|
219
|
+
try:
|
|
220
|
+
return float(val.strip())
|
|
221
|
+
except ValueError:
|
|
222
|
+
return default
|
|
223
|
+
|
|
224
|
+
def get_enum(
|
|
225
|
+
self, name: str, enum_cls: type[_E], default: _E | None = None
|
|
226
|
+
) -> _E | None:
|
|
227
|
+
"""Get enum environment variable value (case-insensitive).
|
|
228
|
+
|
|
229
|
+
Parameters
|
|
230
|
+
----------
|
|
231
|
+
name : str
|
|
232
|
+
Environment variable name.
|
|
233
|
+
enum_cls : Type[Enum]
|
|
234
|
+
Enum class to coerce into.
|
|
235
|
+
default : Enum member, optional
|
|
236
|
+
Default value when unset or not a valid member.
|
|
237
|
+
|
|
238
|
+
Returns
|
|
239
|
+
-------
|
|
240
|
+
Enum member or None
|
|
241
|
+
Matching enum member, or ``default``.
|
|
242
|
+
"""
|
|
243
|
+
val = os.environ.get(name)
|
|
244
|
+
if val is None or val.strip() == "":
|
|
245
|
+
return default
|
|
246
|
+
key = val.strip()
|
|
247
|
+
# Try name match first (case-insensitive)
|
|
248
|
+
for member in enum_cls:
|
|
249
|
+
if member.name.lower() == key.lower():
|
|
250
|
+
return member
|
|
251
|
+
# Fall back to value match (case-sensitive — values may be meaningful)
|
|
252
|
+
for member in enum_cls:
|
|
253
|
+
if member.value == key:
|
|
254
|
+
return member
|
|
255
|
+
return default
|
|
256
|
+
|
|
257
|
+
# --- Legacy helper aliases (kept for backward compatibility) ---
|
|
258
|
+
|
|
259
|
+
def get_env(self, name: str, default: str | None = None) -> str | None:
|
|
260
|
+
"""Get raw environment variable value (alias of :meth:`get_str`)."""
|
|
261
|
+
return self.get_str(name, default)
|
|
262
|
+
|
|
263
|
+
def get_env_bool(self, name: str, default: bool = False) -> bool:
|
|
264
|
+
"""Get boolean environment variable value (alias of :meth:`get_bool`)."""
|
|
265
|
+
return self.get_bool(name, default)
|
|
266
|
+
|
|
267
|
+
# --- Property accessors ---
|
|
268
|
+
|
|
269
|
+
@property
|
|
270
|
+
def token(self) -> str | None:
|
|
271
|
+
"""FOUNDRY_TOKEN value."""
|
|
272
|
+
return os.environ.get(ENV_TOKEN)
|
|
273
|
+
|
|
274
|
+
@property
|
|
275
|
+
def hostname(self) -> str | None:
|
|
276
|
+
"""FOUNDRY_HOSTNAME value."""
|
|
277
|
+
return os.environ.get(ENV_HOSTNAME)
|
|
278
|
+
|
|
279
|
+
@property
|
|
280
|
+
def timeout_s(self) -> int:
|
|
281
|
+
"""Call timeout in seconds (ADR-002)."""
|
|
282
|
+
val = os.environ.get(ENV_TIMEOUT_S)
|
|
283
|
+
if val is None:
|
|
284
|
+
return DEFAULT_TIMEOUT_S
|
|
285
|
+
try:
|
|
286
|
+
timeout = int(val)
|
|
287
|
+
return max(DEFAULT_MIN_TIMEOUT_S, min(DEFAULT_MAX_TIMEOUT_S, timeout))
|
|
288
|
+
except ValueError:
|
|
289
|
+
return DEFAULT_TIMEOUT_S
|
|
290
|
+
|
|
291
|
+
@property
|
|
292
|
+
def default_format(self) -> str:
|
|
293
|
+
"""Default output format."""
|
|
294
|
+
return os.environ.get(ENV_DEFAULT_FORMAT, DEFAULT_FORMAT)
|
|
295
|
+
|
|
296
|
+
@property
|
|
297
|
+
def log_level(self) -> str:
|
|
298
|
+
"""Log level (ADR-005)."""
|
|
299
|
+
return os.environ.get(ENV_LOG_LEVEL, DEFAULT_LOG_LEVEL)
|
|
300
|
+
|
|
301
|
+
@property
|
|
302
|
+
def global_readonly(self) -> bool:
|
|
303
|
+
"""Global READONLY flag."""
|
|
304
|
+
return os.environ.get(ENV_READONLY, "").lower() in _TRUTHY
|
|
305
|
+
|
|
306
|
+
@property
|
|
307
|
+
def global_enabled(self) -> bool:
|
|
308
|
+
"""Global CLI enablement flag, enabled unless explicitly false."""
|
|
309
|
+
return os.environ.get(ENV_ENABLED, "true").lower() not in (
|
|
310
|
+
"false",
|
|
311
|
+
"0",
|
|
312
|
+
"no",
|
|
313
|
+
"off",
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
@property
|
|
317
|
+
def global_metadata_only(self) -> bool:
|
|
318
|
+
"""Global METADATA_ONLY flag."""
|
|
319
|
+
return os.environ.get(ENV_METADATA_ONLY, "").lower() in _TRUTHY
|
|
320
|
+
|
|
321
|
+
@property
|
|
322
|
+
def enable_attribution(self) -> bool:
|
|
323
|
+
"""Attribution enabled flag."""
|
|
324
|
+
return os.environ.get(ENV_ENABLE_ATTRIBUTION, "").lower() in _TRUTHY
|
|
325
|
+
|
|
326
|
+
@property
|
|
327
|
+
def attribution_rids(self) -> str | None:
|
|
328
|
+
"""Comma-separated attribution RIDs."""
|
|
329
|
+
return os.environ.get(ENV_ATTRIBUTION_RIDS)
|
|
330
|
+
|
|
331
|
+
@property
|
|
332
|
+
def enable_tracing(self) -> bool:
|
|
333
|
+
"""Tracing enabled flag."""
|
|
334
|
+
return os.environ.get(ENV_ENABLE_TRACING, "").lower() in _TRUTHY
|
|
335
|
+
|
|
336
|
+
@property
|
|
337
|
+
def max_download_bytes(self) -> int:
|
|
338
|
+
"""Maximum persisted binary response size in bytes."""
|
|
339
|
+
raw = os.environ.get(ENV_MAX_DOWNLOAD_BYTES)
|
|
340
|
+
if raw is None:
|
|
341
|
+
return DEFAULT_MAX_DOWNLOAD_BYTES
|
|
342
|
+
try:
|
|
343
|
+
value = int(raw.strip())
|
|
344
|
+
except ValueError as exc:
|
|
345
|
+
raise ConfigurationError(
|
|
346
|
+
f"{ENV_MAX_DOWNLOAD_BYTES} must be a positive integer"
|
|
347
|
+
) from exc
|
|
348
|
+
if value <= 0:
|
|
349
|
+
raise ConfigurationError(
|
|
350
|
+
f"{ENV_MAX_DOWNLOAD_BYTES} must be a positive integer"
|
|
351
|
+
)
|
|
352
|
+
return value
|
|
353
|
+
|
|
354
|
+
@property
|
|
355
|
+
def download_path(self) -> Path:
|
|
356
|
+
"""Validated base path for binary downloads."""
|
|
357
|
+
return self._configured_path(ENV_DOWNLOAD_PATH, DEFAULT_DOWNLOAD_PATH)
|
|
358
|
+
|
|
359
|
+
@property
|
|
360
|
+
def session_path(self) -> Path:
|
|
361
|
+
"""Validated base path for persisted sessions."""
|
|
362
|
+
return self._configured_path(ENV_SESSION_PATH, DEFAULT_SESSION_PATH)
|
|
363
|
+
|
|
364
|
+
@staticmethod
|
|
365
|
+
def _configured_path(name: str, default: Path) -> Path:
|
|
366
|
+
raw = os.environ.get(name)
|
|
367
|
+
if raw is None:
|
|
368
|
+
return default
|
|
369
|
+
value = raw.strip()
|
|
370
|
+
if not value or "\x00" in value:
|
|
371
|
+
raise ConfigurationError(f"{name} must be a non-empty filesystem path")
|
|
372
|
+
return Path(value)
|
|
373
|
+
|
|
374
|
+
@property
|
|
375
|
+
def loaded_file(self) -> str | None:
|
|
376
|
+
"""Path to loaded .env file, or None."""
|
|
377
|
+
return self._loaded_file
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
class ConfigurationError(Exception):
|
|
381
|
+
"""Configuration loading error (exit code 9 per ADR-001).
|
|
382
|
+
|
|
383
|
+
Carries the canonical exit code so callers (CLI entrypoints, error
|
|
384
|
+
serializers) can map the exception to the correct process exit status
|
|
385
|
+
without maintaining a separate lookup table.
|
|
386
|
+
"""
|
|
387
|
+
|
|
388
|
+
exit_code: int = EXIT_CONFIGURATION
|