cnos 1.11.4__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.
cnos/__init__.py ADDED
@@ -0,0 +1,181 @@
1
+ """CNOS Python runtime — public API.
2
+
3
+ Module-level functions mirror Go's singleton.go delegating functions.
4
+ Call cnos.ready() or cnos.load() before using the module-level read functions.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ from typing import Any, Dict, List, Optional, Tuple
9
+
10
+ # Re-export the main public types
11
+ from cnos.errors import CnosError
12
+ from cnos.exports import (
13
+ config_hash,
14
+ format_message,
15
+ log_message,
16
+ to_env,
17
+ to_namespace,
18
+ to_object,
19
+ to_public_env,
20
+ to_server_projection,
21
+ )
22
+ from cnos.inspect import InspectResult
23
+ from cnos.loader import (
24
+ CnosOptions,
25
+ bootstrap_default_runtime,
26
+ default_runtime,
27
+ load,
28
+ load_projection,
29
+ load_projection_file,
30
+ ready,
31
+ set_default_runtime,
32
+ )
33
+ from cnos.projection import ServerProjection
34
+ from cnos.runtime import CnosRuntime
35
+ from cnos.types import (
36
+ ConfigOrigin,
37
+ DerivedFormula,
38
+ SecretReference,
39
+ SecretVaultProvider,
40
+ SecretVaultProviderFactory,
41
+ ToEnvOptions,
42
+ ToPublicEnvOptions,
43
+ VaultAuthConfig,
44
+ VaultAuthDefinition,
45
+ VaultAuthSource,
46
+ VaultDefinition,
47
+ )
48
+
49
+ # ---------------------------------------------------------------------------
50
+ # Module-level singleton API — mirror Go's package-level functions
51
+ # ---------------------------------------------------------------------------
52
+
53
+ def read(key: str) -> Tuple[Any, bool]:
54
+ return default_runtime().read(key)
55
+
56
+
57
+ def require(key: str) -> Any:
58
+ return default_runtime().require(key)
59
+
60
+
61
+ def read_or(key: str, fallback: Any) -> Any:
62
+ return default_runtime().read_or(key, fallback)
63
+
64
+
65
+ def value(path: str) -> Tuple[Any, bool]:
66
+ return default_runtime().value(path)
67
+
68
+
69
+ def secret(path: str) -> Tuple[Any, bool]:
70
+ return default_runtime().secret(path)
71
+
72
+
73
+ def meta(path: str) -> Tuple[Any, bool]:
74
+ return default_runtime().meta(path)
75
+
76
+
77
+ def public(path: str) -> Tuple[Any, bool]:
78
+ return default_runtime().public(path)
79
+
80
+
81
+ def inspect(key: str) -> InspectResult:
82
+ return default_runtime().inspect(key)
83
+
84
+
85
+ def get_to_object() -> Dict[str, Any]:
86
+ return to_object(default_runtime())
87
+
88
+
89
+ def get_to_namespace(namespace: str) -> Dict[str, Any]:
90
+ return to_namespace(default_runtime(), namespace)
91
+
92
+
93
+ def get_to_env(options: Optional[ToEnvOptions] = None) -> Dict[str, str]:
94
+ return to_env(default_runtime(), options)
95
+
96
+
97
+ def get_to_public_env(options: Optional[ToPublicEnvOptions] = None) -> Dict[str, str]:
98
+ return to_public_env(default_runtime(), options)
99
+
100
+
101
+ def get_to_server_projection() -> ServerProjection:
102
+ return to_server_projection(default_runtime())
103
+
104
+
105
+ def get_format(message: str) -> str:
106
+ return format_message(default_runtime(), message)
107
+
108
+
109
+ def get_log(message: str) -> str:
110
+ return log_message(default_runtime(), message)
111
+
112
+
113
+ def refresh_secrets() -> None:
114
+ default_runtime().refresh_secrets()
115
+
116
+
117
+ def refresh_secret(path: str) -> None:
118
+ default_runtime().refresh_secret(path)
119
+
120
+
121
+ def register_runtime_provider(namespace: str, provider: Any) -> None:
122
+ default_runtime().register_runtime_provider(namespace, provider)
123
+
124
+
125
+ def register_secret_vault_providers(*factories: SecretVaultProviderFactory) -> None:
126
+ default_runtime().register_secret_vault_providers(*factories)
127
+
128
+
129
+ # Bootstrap eagerly (mirrors Go's init())
130
+ bootstrap_default_runtime()
131
+
132
+ __all__ = [
133
+ # Error
134
+ "CnosError",
135
+ # Main classes
136
+ "CnosRuntime",
137
+ "CnosOptions",
138
+ "ServerProjection",
139
+ "InspectResult",
140
+ # Types
141
+ "DerivedFormula",
142
+ "SecretReference",
143
+ "VaultDefinition",
144
+ "VaultAuthConfig",
145
+ "VaultAuthDefinition",
146
+ "VaultAuthSource",
147
+ "ConfigOrigin",
148
+ "ToEnvOptions",
149
+ "ToPublicEnvOptions",
150
+ "SecretVaultProvider",
151
+ "SecretVaultProviderFactory",
152
+ # Loader
153
+ "load",
154
+ "load_projection",
155
+ "load_projection_file",
156
+ "ready",
157
+ "set_default_runtime",
158
+ "default_runtime",
159
+ # Module-level read API
160
+ "read",
161
+ "require",
162
+ "read_or",
163
+ "value",
164
+ "secret",
165
+ "meta",
166
+ "public",
167
+ "inspect",
168
+ # Export API
169
+ "get_to_object",
170
+ "get_to_namespace",
171
+ "get_to_env",
172
+ "get_to_public_env",
173
+ "get_to_server_projection",
174
+ "get_format",
175
+ "get_log",
176
+ "config_hash",
177
+ "refresh_secrets",
178
+ "refresh_secret",
179
+ "register_runtime_provider",
180
+ "register_secret_vault_providers",
181
+ ]
@@ -0,0 +1 @@
1
+ # internal package
@@ -0,0 +1,31 @@
1
+ """Internal runtime entry and provenance types."""
2
+ from __future__ import annotations
3
+
4
+ from dataclasses import dataclass, field
5
+ from typing import Any, List, Optional
6
+
7
+ from cnos.types import ConfigOrigin, SecretReference
8
+
9
+
10
+ @dataclass
11
+ class RuntimeProvenance:
12
+ source_id: str = ""
13
+ plugin_id: str = ""
14
+ workspace_id: str = ""
15
+ value: Any = None
16
+ origin: Optional[ConfigOrigin] = None
17
+
18
+
19
+ @dataclass
20
+ class RuntimeEntry:
21
+ key: str = ""
22
+ namespace: str = ""
23
+ value: Any = None
24
+ alias_to: str = ""
25
+ promoted_from: str = ""
26
+ formula: Optional[Any] = None # ParsedFormula — avoid circular import
27
+ formula_cached: bool = False
28
+ formula_cache: Any = None
29
+ secret_ref: Optional[SecretReference] = None
30
+ winner: RuntimeProvenance = field(default_factory=RuntimeProvenance)
31
+ overridden: List[RuntimeProvenance] = field(default_factory=list)
cnos/derive.py ADDED
@@ -0,0 +1,399 @@
1
+ """Derived formula parser and evaluator — mirrors Go's derive.go exactly."""
2
+ from __future__ import annotations
3
+
4
+ from dataclasses import dataclass, field
5
+ from typing import Any, Callable, Dict, List, Optional, Set, Tuple
6
+
7
+ from cnos.errors import CnosError
8
+ from cnos.jscompat import js_stringify_value, js_strict_equal
9
+ from cnos.types import DerivedFormula
10
+
11
+
12
+ # ---------------------------------------------------------------------------
13
+ # AST node
14
+ # ---------------------------------------------------------------------------
15
+
16
+ @dataclass
17
+ class ExprNode:
18
+ kind: str = "" # "literal" | "ref" | "call"
19
+ value: Any = None # for kind="literal"
20
+ path: str = "" # for kind="ref"
21
+ name: str = "" # for kind="call"
22
+ args: List["ExprNode"] = field(default_factory=list)
23
+
24
+
25
+ # ---------------------------------------------------------------------------
26
+ # Parsed formula (internal)
27
+ # ---------------------------------------------------------------------------
28
+
29
+ @dataclass
30
+ class ParsedFormula:
31
+ raw: str = ""
32
+ refs: List[str] = field(default_factory=list)
33
+ deps: List[str] = field(default_factory=list)
34
+ runtime_refs: List[str] = field(default_factory=list)
35
+ runtime_dependent: bool = False
36
+ ast: Optional[ExprNode] = None
37
+
38
+
39
+ _DERIVE_BUILTINS: Set[str] = {"concat", "coalesce", "when", "exists", "eq", "ne"}
40
+
41
+
42
+ # ---------------------------------------------------------------------------
43
+ # Parser state
44
+ # ---------------------------------------------------------------------------
45
+
46
+ class _ParserState:
47
+ def __init__(self, source: str) -> None:
48
+ self.source = source
49
+ self.index = 0
50
+
51
+ def errorf(self, message: str) -> CnosError:
52
+ return CnosError(f"cnos: {message} at position {self.index + 1}")
53
+
54
+
55
+ def _is_whitespace(ch: str) -> bool:
56
+ return ch in " \n\r\t"
57
+
58
+
59
+ def _skip_whitespace(state: _ParserState) -> None:
60
+ while state.index < len(state.source) and _is_whitespace(state.source[state.index]):
61
+ state.index += 1
62
+
63
+
64
+ def _is_identifier_start(ch: str) -> bool:
65
+ return ch.isalpha() or ch == "_"
66
+
67
+
68
+ def _is_identifier_part(ch: str) -> bool:
69
+ return ch.isalpha() or ch.isdigit() or ch in "._-"
70
+
71
+
72
+ def _is_valid_template_ref(value: str) -> bool:
73
+ if not value or not _is_identifier_start(value[0]):
74
+ return False
75
+ for ch in value[1:]:
76
+ if not _is_identifier_part(ch):
77
+ return False
78
+ return True
79
+
80
+
81
+ # ---------------------------------------------------------------------------
82
+ # Parser functions
83
+ # ---------------------------------------------------------------------------
84
+
85
+ def _parse_string_literal(state: _ParserState) -> ExprNode:
86
+ state.index += 1 # consume opening '
87
+ chars: List[str] = []
88
+ while state.index < len(state.source):
89
+ ch = state.source[state.index]
90
+ if ch == "\\":
91
+ if state.index + 1 >= len(state.source):
92
+ raise state.errorf("Unterminated escape sequence")
93
+ chars.append(state.source[state.index + 1])
94
+ state.index += 2
95
+ continue
96
+ if ch == "'":
97
+ state.index += 1
98
+ return ExprNode(kind="literal", value="".join(chars))
99
+ chars.append(ch)
100
+ state.index += 1
101
+ raise state.errorf("Unterminated string literal")
102
+
103
+
104
+ def _parse_number_literal(state: _ParserState) -> ExprNode:
105
+ start = state.index
106
+ while state.index < len(state.source) and state.source[state.index].isdigit():
107
+ state.index += 1
108
+ if state.index < len(state.source) and state.source[state.index] == ".":
109
+ state.index += 1
110
+ while state.index < len(state.source) and state.source[state.index].isdigit():
111
+ state.index += 1
112
+ raw = state.source[start:state.index]
113
+ try:
114
+ return ExprNode(kind="literal", value=float(raw))
115
+ except ValueError as exc:
116
+ raise CnosError(f"cnos: invalid number literal: {raw}") from exc
117
+
118
+
119
+ def _parse_identifier(state: _ParserState) -> str:
120
+ if state.index >= len(state.source) or not _is_identifier_start(state.source[state.index]):
121
+ raise state.errorf("Expected identifier")
122
+ start = state.index
123
+ state.index += 1
124
+ while state.index < len(state.source) and _is_identifier_part(state.source[state.index]):
125
+ state.index += 1
126
+ return state.source[start:state.index]
127
+
128
+
129
+ def _parse_arguments(state: _ParserState) -> List[ExprNode]:
130
+ args: List[ExprNode] = []
131
+ _skip_whitespace(state)
132
+ if state.index < len(state.source) and state.source[state.index] == ")":
133
+ state.index += 1
134
+ return args
135
+ while state.index < len(state.source):
136
+ node = _parse_expression_node(state)
137
+ args.append(node)
138
+ _skip_whitespace(state)
139
+ if state.index >= len(state.source):
140
+ break
141
+ ch = state.source[state.index]
142
+ if ch == ",":
143
+ state.index += 1
144
+ _skip_whitespace(state)
145
+ elif ch == ")":
146
+ state.index += 1
147
+ return args
148
+ else:
149
+ raise state.errorf('Expected "," or ")"')
150
+ raise state.errorf("Unterminated function call")
151
+
152
+
153
+ def _parse_identifier_or_call(state: _ParserState) -> ExprNode:
154
+ identifier = _parse_identifier(state)
155
+ _skip_whitespace(state)
156
+ if state.index < len(state.source) and state.source[state.index] == "(":
157
+ if identifier not in _DERIVE_BUILTINS:
158
+ raise CnosError(f"cnos: unknown derive function: {identifier}")
159
+ state.index += 1 # consume '('
160
+ args = _parse_arguments(state)
161
+ return ExprNode(kind="call", name=identifier, args=args)
162
+ # keywords or ref
163
+ if identifier == "true":
164
+ return ExprNode(kind="literal", value=True)
165
+ if identifier == "false":
166
+ return ExprNode(kind="literal", value=False)
167
+ if identifier == "null":
168
+ return ExprNode(kind="literal", value=None)
169
+ return ExprNode(kind="ref", path=identifier)
170
+
171
+
172
+ def _parse_expression_node(state: _ParserState) -> ExprNode:
173
+ _skip_whitespace(state)
174
+ if state.index >= len(state.source):
175
+ raise state.errorf("Unexpected token")
176
+ ch = state.source[state.index]
177
+ if ch == "'":
178
+ return _parse_string_literal(state)
179
+ if ch.isdigit():
180
+ return _parse_number_literal(state)
181
+ if _is_identifier_start(ch):
182
+ return _parse_identifier_or_call(state)
183
+ raise state.errorf("Unexpected token")
184
+
185
+
186
+ def _parse_template(source: str) -> ExprNode:
187
+ parts: List[ExprNode] = []
188
+ cursor = 0
189
+ while cursor < len(source):
190
+ start = source.find("${", cursor)
191
+ if start == -1:
192
+ if cursor < len(source):
193
+ parts.append(ExprNode(kind="literal", value=source[cursor:]))
194
+ break
195
+ if start > cursor:
196
+ parts.append(ExprNode(kind="literal", value=source[cursor:start]))
197
+ end = source.find("}", start + 2)
198
+ if end == -1:
199
+ raise CnosError(
200
+ f"cnos: invalid derivation template: unclosed ${{...}} at position {start + 1}"
201
+ )
202
+ ref = source[start + 2:end].strip()
203
+ if not ref:
204
+ raise CnosError(
205
+ f"cnos: invalid derivation template: empty reference at position {start + 1}"
206
+ )
207
+ if not _is_valid_template_ref(ref):
208
+ raise CnosError(
209
+ f"cnos: invalid derivation template reference {ref!r}"
210
+ )
211
+ parts.append(ExprNode(kind="ref", path=ref))
212
+ cursor = end + 1
213
+
214
+ if not parts:
215
+ return ExprNode(kind="literal", value="")
216
+ if len(parts) == 1:
217
+ return parts[0]
218
+ return ExprNode(kind="call", name="concat", args=parts)
219
+
220
+
221
+ def parse_derived_source(source: str) -> ExprNode:
222
+ if "${" in source:
223
+ return _parse_template(source)
224
+ state = _ParserState(source)
225
+ node = _parse_expression_node(state)
226
+ _skip_whitespace(state)
227
+ if state.index != len(state.source):
228
+ raise state.errorf("Unexpected trailing input")
229
+ return node
230
+
231
+
232
+ def parse_derived_formula(formula: DerivedFormula) -> ParsedFormula:
233
+ ast = parse_derived_source(formula.expr)
234
+ refs = list(formula.deps) + list(formula.runtime_refs)
235
+ unique_refs = _unique_sorted(refs)
236
+ return ParsedFormula(
237
+ raw=formula.expr,
238
+ refs=unique_refs,
239
+ deps=list(formula.deps),
240
+ runtime_refs=list(formula.runtime_refs),
241
+ runtime_dependent=len(formula.runtime_refs) > 0,
242
+ ast=ast,
243
+ )
244
+
245
+
246
+ def parse_raw_derived_value(value: Any) -> ParsedFormula:
247
+ """Parse a $derive value from a graph entry."""
248
+ source = _derive_source_from_value(value)
249
+ ast = parse_derived_source(source)
250
+ refs = _extract_refs(ast, [])
251
+ unique_refs = _unique_sorted(refs)
252
+ return ParsedFormula(
253
+ raw=source,
254
+ refs=unique_refs,
255
+ deps=unique_refs,
256
+ ast=ast,
257
+ )
258
+
259
+
260
+ def _derive_source_from_value(value: Any) -> str:
261
+ if not isinstance(value, dict):
262
+ raise CnosError(
263
+ "cnos: derived value requires either a template string or { expr } object"
264
+ )
265
+ raw = value.get("$derive")
266
+ if raw is None:
267
+ raise CnosError(
268
+ "cnos: derived value requires either a template string or { expr } object"
269
+ )
270
+ if isinstance(raw, str):
271
+ return raw
272
+ if isinstance(raw, dict):
273
+ source = raw.get("expr", "")
274
+ if not isinstance(source, str) or not source.strip():
275
+ raise CnosError(
276
+ "cnos: derived value requires either a template string or { expr } object"
277
+ )
278
+ return source
279
+ raise CnosError("cnos: derived value requires either a template string or { expr } object")
280
+
281
+
282
+ def is_derived_value(value: Any) -> bool:
283
+ return isinstance(value, dict) and "$derive" in value
284
+
285
+
286
+ def _extract_refs(node: ExprNode, refs: List[str]) -> List[str]:
287
+ if node.kind == "ref":
288
+ refs.append(node.path)
289
+ elif node.kind == "call":
290
+ for arg in node.args:
291
+ refs = _extract_refs(arg, refs)
292
+ return refs
293
+
294
+
295
+ # ---------------------------------------------------------------------------
296
+ # Evaluator
297
+ # ---------------------------------------------------------------------------
298
+
299
+ ResolveRef = Callable[[str], Tuple[Any, bool]]
300
+
301
+
302
+ def evaluate_derived_formula(
303
+ key: str,
304
+ formula: ParsedFormula,
305
+ resolve_ref: ResolveRef,
306
+ ) -> Any:
307
+ value, found, err = _evaluate_node(formula.ast, resolve_ref)
308
+ if err:
309
+ raise CnosError(err)
310
+ if formula.ast is not None and formula.ast.kind == "ref" and not found:
311
+ raise CnosError(
312
+ f"cnos: unable to resolve derived config key {key} because {formula.ast.path} is missing"
313
+ )
314
+ return value
315
+
316
+
317
+ def _evaluate_node(
318
+ node: ExprNode,
319
+ resolve_ref: ResolveRef,
320
+ ) -> Tuple[Any, bool, Optional[str]]:
321
+ if node.kind == "literal":
322
+ return node.value, True, None
323
+ if node.kind == "ref":
324
+ val, found = resolve_ref(node.path)
325
+ return val, found, None
326
+ if node.kind == "call":
327
+ values: List[Any] = []
328
+ flags: List[bool] = []
329
+ for arg in node.args:
330
+ val, found, err = _evaluate_node(arg, resolve_ref)
331
+ if err:
332
+ return None, False, err
333
+ values.append(val)
334
+ flags.append(found)
335
+ val, found, err = _evaluate_call(node.name, values, flags)
336
+ return val, found, err
337
+ return None, False, f"cnos: unsupported derive AST node {node.kind!r}"
338
+
339
+
340
+ def _evaluate_call(
341
+ name: str, values: List[Any], flags: List[bool]
342
+ ) -> Tuple[Any, bool, Optional[str]]:
343
+ if name == "concat":
344
+ parts = [js_stringify_value(v) for v in values]
345
+ return "".join(parts), True, None
346
+ if name == "coalesce":
347
+ for v in values:
348
+ if v is not None:
349
+ return v, True, None
350
+ return None, True, None
351
+ if name == "when":
352
+ when_true = values[1] if len(values) > 1 else None
353
+ when_false = values[2] if len(values) > 2 else None
354
+ if _is_truthy(values[0] if values else None):
355
+ return when_true, True, None
356
+ return when_false, True, None
357
+ if name == "exists":
358
+ if not values:
359
+ return False, True, None
360
+ return (flags[0] and values[0] is not None), True, None
361
+ if name == "eq":
362
+ left = values[0] if values else None
363
+ right = values[1] if len(values) > 1 else None
364
+ return js_strict_equal(left, right), True, None
365
+ if name == "ne":
366
+ left = values[0] if values else None
367
+ right = values[1] if len(values) > 1 else None
368
+ return not js_strict_equal(left, right), True, None
369
+ return None, False, f"cnos: unknown derive function: {name}"
370
+
371
+
372
+ def _is_truthy(value: Any) -> bool:
373
+ if value is None:
374
+ return False
375
+ if isinstance(value, bool):
376
+ return value
377
+ if isinstance(value, str):
378
+ return bool(value)
379
+ if isinstance(value, (int, float)):
380
+ return value != 0
381
+ return True
382
+
383
+
384
+ def _unique_sorted(values: List[str]) -> List[str]:
385
+ seen: Set[str] = set()
386
+ result: List[str] = []
387
+ for v in values:
388
+ if v and v not in seen:
389
+ seen.add(v)
390
+ result.append(v)
391
+ return sorted(result)
392
+
393
+
394
+ def formula_type(formula: Optional[ParsedFormula]) -> str:
395
+ if formula is None:
396
+ return ""
397
+ if formula.raw and "${" in formula.raw:
398
+ return "template"
399
+ return "expression"
cnos/discover.py ADDED
@@ -0,0 +1,49 @@
1
+ """Projection file discovery — mirrors Go's discover.go."""
2
+ from __future__ import annotations
3
+
4
+ import os
5
+ from typing import Optional
6
+
7
+ PROJECTION_FILE_NAME = ".cnos-server.json"
8
+
9
+
10
+ def find_projection_path(working_dir: str = "") -> Optional[str]:
11
+ """Find .cnos-server.json by checking cwd then walking up 3 levels near .cnosrc.yml."""
12
+ cwd = _resolve_working_dir(working_dir)
13
+
14
+ # Direct candidate at cwd
15
+ direct = os.path.join(cwd, PROJECTION_FILE_NAME)
16
+ if _file_exists(direct):
17
+ return direct
18
+
19
+ # Walk up looking for .cnosrc.yml alongside .cnos-server.json
20
+ current = cwd
21
+ for _ in range(4): # depth 0..3
22
+ rc_candidate = os.path.join(current, ".cnosrc.yml")
23
+ if _file_exists(rc_candidate):
24
+ proj_candidate = os.path.join(current, PROJECTION_FILE_NAME)
25
+ if _file_exists(proj_candidate):
26
+ return proj_candidate
27
+ parent = os.path.dirname(current)
28
+ if parent == current:
29
+ break
30
+ current = parent
31
+
32
+ return None
33
+
34
+
35
+ def _resolve_working_dir(working_dir: str) -> str:
36
+ if working_dir:
37
+ return os.path.abspath(working_dir)
38
+ return os.getcwd()
39
+
40
+
41
+ def _file_exists(path: str) -> bool:
42
+ return os.path.isfile(path)
43
+
44
+
45
+ def resolve_path_from_working_dir(working_dir: str, target: str) -> str:
46
+ if os.path.isabs(target):
47
+ return target
48
+ base = _resolve_working_dir(working_dir)
49
+ return os.path.join(base, target)
cnos/env.py ADDED
@@ -0,0 +1,38 @@
1
+ """Environment abstraction — mirrors Go's environment struct."""
2
+ from __future__ import annotations
3
+
4
+ import os
5
+ from typing import Dict, Optional, Tuple
6
+
7
+
8
+ class Environment:
9
+ """Wraps real or injected environment variables."""
10
+
11
+ def __init__(self, override: Optional[Dict[str, str]] = None) -> None:
12
+ if override is None:
13
+ self._override: Optional[Dict[str, str]] = None
14
+ self._use_os = True
15
+ else:
16
+ self._override = dict(override)
17
+ self._use_os = False
18
+
19
+ def get(self, key: str) -> Tuple[Optional[str], bool]:
20
+ """Return (value, found)."""
21
+ if self._use_os:
22
+ value = os.environ.get(key)
23
+ return value, value is not None
24
+ if self._override is not None:
25
+ if key in self._override:
26
+ return self._override[key], True
27
+ return None, False
28
+
29
+ def process_env(self) -> Dict[str, str]:
30
+ """Return a merged dict of os.environ + any override."""
31
+ values: Dict[str, str] = dict(os.environ)
32
+ if not self._use_os and self._override:
33
+ values.update(self._override)
34
+ return values
35
+
36
+ @property
37
+ def use_os(self) -> bool:
38
+ return self._use_os