kub-cli 0.2.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.
- kub_cli/__init__.py +17 -0
- kub_cli/__main__.py +12 -0
- kub_cli/cli.py +292 -0
- kub_cli/commands.py +164 -0
- kub_cli/config.py +555 -0
- kub_cli/errors.py +32 -0
- kub_cli/img_cli.py +306 -0
- kub_cli/img_integration.py +265 -0
- kub_cli/img_tools.py +434 -0
- kub_cli/logging_utils.py +31 -0
- kub_cli/runtime.py +463 -0
- kub_cli/versioning.py +175 -0
- kub_cli-0.2.0.dist-info/METADATA +344 -0
- kub_cli-0.2.0.dist-info/RECORD +17 -0
- kub_cli-0.2.0.dist-info/WHEEL +4 -0
- kub_cli-0.2.0.dist-info/entry_points.txt +6 -0
- kub_cli-0.2.0.dist-info/licenses/LICENSE +206 -0
kub_cli/__init__.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026 University of Strasbourg
|
|
2
|
+
# SPDX-FileContributor: Christophe Prud'homme
|
|
3
|
+
# SPDX-FileContributor: Cemosis
|
|
4
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
|
|
6
|
+
"""kub-cli package."""
|
|
7
|
+
|
|
8
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
try:
|
|
12
|
+
__version__ = version("kub-cli")
|
|
13
|
+
except PackageNotFoundError:
|
|
14
|
+
__version__ = "0.2.0"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
__all__ = ["__version__"]
|
kub_cli/__main__.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026 University of Strasbourg
|
|
2
|
+
# SPDX-FileContributor: Christophe Prud'homme
|
|
3
|
+
# SPDX-FileContributor: Cemosis
|
|
4
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
|
|
6
|
+
"""Module entrypoint for `python -m kub_cli`."""
|
|
7
|
+
|
|
8
|
+
from .cli import metaMain
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
if __name__ == "__main__":
|
|
12
|
+
metaMain()
|
kub_cli/cli.py
ADDED
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026 University of Strasbourg
|
|
2
|
+
# SPDX-FileContributor: Christophe Prud'homme
|
|
3
|
+
# SPDX-FileContributor: Cemosis
|
|
4
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
|
|
6
|
+
"""Typer CLI entrypoints for kub-cli wrapper commands."""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Sequence
|
|
12
|
+
|
|
13
|
+
import typer
|
|
14
|
+
|
|
15
|
+
from . import __version__
|
|
16
|
+
from .commands import WrapperOptions, runWrapperCommand
|
|
17
|
+
from .errors import KubCliError
|
|
18
|
+
from .versioning import bumpProjectVersion
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
CONTEXT_SETTINGS = {
|
|
22
|
+
"allow_extra_args": True,
|
|
23
|
+
"ignore_unknown_options": True,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def normalizeForwardedArgs(args: Sequence[str]) -> list[str]:
|
|
28
|
+
forwarded = list(args)
|
|
29
|
+
if forwarded and forwarded[0] == "--":
|
|
30
|
+
return forwarded[1:]
|
|
31
|
+
return forwarded
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def executeWrapperCommand(
|
|
35
|
+
*,
|
|
36
|
+
appName: str,
|
|
37
|
+
ctx: typer.Context,
|
|
38
|
+
runtime: str | None,
|
|
39
|
+
image: str | None,
|
|
40
|
+
bind: Sequence[str],
|
|
41
|
+
pwd: str | None,
|
|
42
|
+
runner: str | None,
|
|
43
|
+
dryRun: bool,
|
|
44
|
+
verbose: bool | None,
|
|
45
|
+
apptainerFlags: Sequence[str],
|
|
46
|
+
dockerFlags: Sequence[str],
|
|
47
|
+
envVars: Sequence[str],
|
|
48
|
+
showConfig: bool,
|
|
49
|
+
) -> None:
|
|
50
|
+
forwardedArgs = normalizeForwardedArgs(ctx.args)
|
|
51
|
+
options = WrapperOptions(
|
|
52
|
+
runtime=runtime,
|
|
53
|
+
image=image,
|
|
54
|
+
binds=tuple(bind),
|
|
55
|
+
pwd=pwd,
|
|
56
|
+
runner=runner,
|
|
57
|
+
dryRun=dryRun,
|
|
58
|
+
verbose=verbose,
|
|
59
|
+
apptainerFlags=tuple(apptainerFlags),
|
|
60
|
+
dockerFlags=tuple(dockerFlags),
|
|
61
|
+
envVars=tuple(envVars),
|
|
62
|
+
showConfig=showConfig,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
try:
|
|
66
|
+
exitCode = runWrapperCommand(
|
|
67
|
+
appName=appName,
|
|
68
|
+
forwardedArgs=forwardedArgs,
|
|
69
|
+
options=options,
|
|
70
|
+
)
|
|
71
|
+
except KubCliError as error:
|
|
72
|
+
typer.secho(str(error), fg=typer.colors.RED, err=True)
|
|
73
|
+
raise typer.Exit(code=error.exit_code) from error
|
|
74
|
+
|
|
75
|
+
raise typer.Exit(code=exitCode)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def createWrapperApp(*, appName: str, helpText: str) -> typer.Typer:
|
|
79
|
+
app = typer.Typer(
|
|
80
|
+
add_completion=False,
|
|
81
|
+
no_args_is_help=False,
|
|
82
|
+
help=helpText,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
@app.command(context_settings=CONTEXT_SETTINGS)
|
|
86
|
+
def wrapper(
|
|
87
|
+
ctx: typer.Context,
|
|
88
|
+
runtime: str | None = typer.Option(
|
|
89
|
+
None,
|
|
90
|
+
"--runtime",
|
|
91
|
+
metavar="{auto,apptainer,docker}",
|
|
92
|
+
help="Container runtime selection.",
|
|
93
|
+
),
|
|
94
|
+
image: str | None = typer.Option(
|
|
95
|
+
None,
|
|
96
|
+
"--image",
|
|
97
|
+
metavar="IMAGE",
|
|
98
|
+
help="Runtime image reference/path (runtime-dependent).",
|
|
99
|
+
),
|
|
100
|
+
bind: list[str] | None = typer.Option(
|
|
101
|
+
None,
|
|
102
|
+
"--bind",
|
|
103
|
+
metavar="SRC:DST",
|
|
104
|
+
help="Bind mount or volume mapping (repeatable).",
|
|
105
|
+
),
|
|
106
|
+
pwd: str | None = typer.Option(
|
|
107
|
+
None,
|
|
108
|
+
"--pwd",
|
|
109
|
+
metavar="PATH",
|
|
110
|
+
help="Working directory passed to runtime command.",
|
|
111
|
+
),
|
|
112
|
+
runner: str | None = typer.Option(
|
|
113
|
+
None,
|
|
114
|
+
"--runner",
|
|
115
|
+
metavar="PATH",
|
|
116
|
+
help="Runtime executable path or command name.",
|
|
117
|
+
),
|
|
118
|
+
dryRun: bool = typer.Option(
|
|
119
|
+
False,
|
|
120
|
+
"--dry-run",
|
|
121
|
+
help="Print the resolved command without running it.",
|
|
122
|
+
),
|
|
123
|
+
verbose: bool | None = typer.Option(
|
|
124
|
+
None,
|
|
125
|
+
"--verbose/--no-verbose",
|
|
126
|
+
help="Enable or disable verbose wrapper logs.",
|
|
127
|
+
),
|
|
128
|
+
apptainerFlags: list[str] | None = typer.Option(
|
|
129
|
+
None,
|
|
130
|
+
"--apptainer-flag",
|
|
131
|
+
metavar="FLAG",
|
|
132
|
+
help="Extra Apptainer flag (repeatable).",
|
|
133
|
+
),
|
|
134
|
+
dockerFlags: list[str] | None = typer.Option(
|
|
135
|
+
None,
|
|
136
|
+
"--docker-flag",
|
|
137
|
+
metavar="FLAG",
|
|
138
|
+
help="Extra Docker flag (repeatable).",
|
|
139
|
+
),
|
|
140
|
+
envVars: list[str] | None = typer.Option(
|
|
141
|
+
None,
|
|
142
|
+
"--env",
|
|
143
|
+
metavar="KEY=VALUE",
|
|
144
|
+
help="Environment variable assignment for the process/container (repeatable).",
|
|
145
|
+
),
|
|
146
|
+
showConfig: bool = typer.Option(
|
|
147
|
+
False,
|
|
148
|
+
"--show-config",
|
|
149
|
+
help="Print effective kub-cli configuration as JSON.",
|
|
150
|
+
),
|
|
151
|
+
version: bool = typer.Option(
|
|
152
|
+
False,
|
|
153
|
+
"--version",
|
|
154
|
+
is_eager=True,
|
|
155
|
+
help="Show kub-cli version and exit.",
|
|
156
|
+
),
|
|
157
|
+
) -> None:
|
|
158
|
+
if version:
|
|
159
|
+
typer.echo(f"kub-cli {__version__}")
|
|
160
|
+
raise typer.Exit(code=0)
|
|
161
|
+
|
|
162
|
+
executeWrapperCommand(
|
|
163
|
+
appName=appName,
|
|
164
|
+
ctx=ctx,
|
|
165
|
+
runtime=runtime,
|
|
166
|
+
image=image,
|
|
167
|
+
bind=bind or [],
|
|
168
|
+
pwd=pwd,
|
|
169
|
+
runner=runner,
|
|
170
|
+
dryRun=dryRun,
|
|
171
|
+
verbose=verbose,
|
|
172
|
+
apptainerFlags=apptainerFlags or [],
|
|
173
|
+
dockerFlags=dockerFlags or [],
|
|
174
|
+
envVars=envVars or [],
|
|
175
|
+
showConfig=showConfig,
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
return app
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def createMetaApp() -> typer.Typer:
|
|
182
|
+
app = typer.Typer(
|
|
183
|
+
add_completion=False,
|
|
184
|
+
no_args_is_help=False,
|
|
185
|
+
help="kub-cli meta command.",
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
@app.callback(invoke_without_command=True)
|
|
189
|
+
def meta(
|
|
190
|
+
ctx: typer.Context,
|
|
191
|
+
version: bool = typer.Option(
|
|
192
|
+
False,
|
|
193
|
+
"--version",
|
|
194
|
+
is_eager=True,
|
|
195
|
+
help="Show kub-cli version and exit.",
|
|
196
|
+
),
|
|
197
|
+
) -> None:
|
|
198
|
+
if version:
|
|
199
|
+
typer.echo(f"kub-cli {__version__}")
|
|
200
|
+
raise typer.Exit(code=0)
|
|
201
|
+
|
|
202
|
+
if ctx.invoked_subcommand is None:
|
|
203
|
+
typer.echo(
|
|
204
|
+
"kub-cli thin wrapper. Use: kub-dataset, kub-simulate, kub-dashboard, kub-img."
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
@app.command("bump")
|
|
208
|
+
def bumpCommand(
|
|
209
|
+
part: str = typer.Argument(
|
|
210
|
+
"patch",
|
|
211
|
+
metavar="PART",
|
|
212
|
+
help="Semantic version part to bump: major, minor, patch.",
|
|
213
|
+
),
|
|
214
|
+
toVersion: str | None = typer.Option(
|
|
215
|
+
None,
|
|
216
|
+
"--to",
|
|
217
|
+
metavar="VERSION",
|
|
218
|
+
help="Set an explicit semantic version (MAJOR.MINOR.PATCH).",
|
|
219
|
+
),
|
|
220
|
+
projectRoot: Path = typer.Option(
|
|
221
|
+
Path("."),
|
|
222
|
+
"--project-root",
|
|
223
|
+
metavar="PATH",
|
|
224
|
+
help="Project root containing pyproject.toml.",
|
|
225
|
+
),
|
|
226
|
+
dryRun: bool = typer.Option(
|
|
227
|
+
False,
|
|
228
|
+
"--dry-run",
|
|
229
|
+
help="Print planned version changes without writing files.",
|
|
230
|
+
),
|
|
231
|
+
) -> None:
|
|
232
|
+
try:
|
|
233
|
+
result = bumpProjectVersion(
|
|
234
|
+
projectRoot=projectRoot.resolve(),
|
|
235
|
+
part=part,
|
|
236
|
+
toVersion=toVersion,
|
|
237
|
+
dryRun=dryRun,
|
|
238
|
+
)
|
|
239
|
+
except KubCliError as error:
|
|
240
|
+
typer.secho(str(error), fg=typer.colors.RED, err=True)
|
|
241
|
+
raise typer.Exit(code=error.exit_code) from error
|
|
242
|
+
|
|
243
|
+
if result.changed:
|
|
244
|
+
action = "Planned" if dryRun else "Updated"
|
|
245
|
+
typer.echo(f"{action} version: {result.oldVersion} -> {result.newVersion}")
|
|
246
|
+
else:
|
|
247
|
+
typer.echo(f"Version unchanged: {result.newVersion}")
|
|
248
|
+
|
|
249
|
+
typer.echo(f"pyproject: {result.pyprojectPath}")
|
|
250
|
+
typer.echo(f"fallback: {result.initPath}")
|
|
251
|
+
|
|
252
|
+
return app
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
datasetApp = createWrapperApp(
|
|
256
|
+
appName="kub-dataset",
|
|
257
|
+
helpText=(
|
|
258
|
+
"Run the kub-dataset app inside the configured container runtime "
|
|
259
|
+
"(Apptainer/Docker)."
|
|
260
|
+
),
|
|
261
|
+
)
|
|
262
|
+
simulateApp = createWrapperApp(
|
|
263
|
+
appName="kub-simulate",
|
|
264
|
+
helpText=(
|
|
265
|
+
"Run the kub-simulate app inside the configured container runtime "
|
|
266
|
+
"(Apptainer/Docker)."
|
|
267
|
+
),
|
|
268
|
+
)
|
|
269
|
+
dashboardApp = createWrapperApp(
|
|
270
|
+
appName="kub-dashboard",
|
|
271
|
+
helpText=(
|
|
272
|
+
"Run the kub-dashboard app inside the configured container runtime "
|
|
273
|
+
"(Apptainer/Docker)."
|
|
274
|
+
),
|
|
275
|
+
)
|
|
276
|
+
metaApp = createMetaApp()
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def datasetMain() -> None:
|
|
280
|
+
datasetApp(prog_name="kub-dataset")
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def simulateMain() -> None:
|
|
284
|
+
simulateApp(prog_name="kub-simulate")
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def dashboardMain() -> None:
|
|
288
|
+
dashboardApp(prog_name="kub-dashboard")
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def metaMain() -> None:
|
|
292
|
+
metaApp(prog_name="kub-cli")
|
kub_cli/commands.py
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026 University of Strasbourg
|
|
2
|
+
# SPDX-FileContributor: Christophe Prud'homme
|
|
3
|
+
# SPDX-FileContributor: Cemosis
|
|
4
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
|
|
6
|
+
"""High-level command orchestration for kub-cli wrappers."""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
import json
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any, Mapping, Sequence
|
|
14
|
+
|
|
15
|
+
from .config import KubConfig, KubConfigOverrides, loadKubConfig
|
|
16
|
+
from .errors import ConfigError
|
|
17
|
+
from .img_integration import (
|
|
18
|
+
KubImgCommandRunner,
|
|
19
|
+
buildKubImgInfoRequest,
|
|
20
|
+
buildKubImgPullRequest,
|
|
21
|
+
)
|
|
22
|
+
from .logging_utils import configureLogging
|
|
23
|
+
from .runtime import KubAppRunner
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass(frozen=True)
|
|
27
|
+
class WrapperOptions:
|
|
28
|
+
"""Wrapper-specific options parsed from the CLI layer."""
|
|
29
|
+
|
|
30
|
+
runtime: str | None = None
|
|
31
|
+
image: str | None = None
|
|
32
|
+
binds: tuple[str, ...] = ()
|
|
33
|
+
pwd: str | None = None
|
|
34
|
+
runner: str | None = None
|
|
35
|
+
dryRun: bool = False
|
|
36
|
+
verbose: bool | None = None
|
|
37
|
+
apptainerFlags: tuple[str, ...] = ()
|
|
38
|
+
dockerFlags: tuple[str, ...] = ()
|
|
39
|
+
envVars: tuple[str, ...] = ()
|
|
40
|
+
showConfig: bool = False
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def runWrapperCommand(
|
|
44
|
+
*,
|
|
45
|
+
appName: str,
|
|
46
|
+
forwardedArgs: Sequence[str],
|
|
47
|
+
options: WrapperOptions,
|
|
48
|
+
cwd: Path | None = None,
|
|
49
|
+
env: Mapping[str, str] | None = None,
|
|
50
|
+
userConfigPath: Path | None = None,
|
|
51
|
+
) -> int:
|
|
52
|
+
"""Resolve config and execute one wrapped in-container app."""
|
|
53
|
+
|
|
54
|
+
effectiveConfig = resolveEffectiveConfig(
|
|
55
|
+
options=options,
|
|
56
|
+
cwd=cwd,
|
|
57
|
+
env=env,
|
|
58
|
+
userConfigPath=userConfigPath,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
configureLogging(effectiveConfig.verbose)
|
|
62
|
+
|
|
63
|
+
if options.showConfig:
|
|
64
|
+
print(json.dumps(effectiveConfig.toDict(), indent=2, sort_keys=True))
|
|
65
|
+
if not forwardedArgs and not options.dryRun:
|
|
66
|
+
return 0
|
|
67
|
+
|
|
68
|
+
runner = KubAppRunner(config=effectiveConfig)
|
|
69
|
+
return runner.run(appName=appName, forwardedArgs=forwardedArgs, dryRun=options.dryRun)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def pullSelectedRuntimeImage(
|
|
73
|
+
*,
|
|
74
|
+
options: WrapperOptions,
|
|
75
|
+
cwd: Path | None = None,
|
|
76
|
+
env: Mapping[str, str] | None = None,
|
|
77
|
+
userConfigPath: Path | None = None,
|
|
78
|
+
) -> int:
|
|
79
|
+
"""Invoke kub-img pull for the runtime/image selected by config precedence."""
|
|
80
|
+
|
|
81
|
+
effectiveConfig = resolveEffectiveConfig(
|
|
82
|
+
options=options,
|
|
83
|
+
cwd=cwd,
|
|
84
|
+
env=env,
|
|
85
|
+
userConfigPath=userConfigPath,
|
|
86
|
+
)
|
|
87
|
+
configureLogging(effectiveConfig.verbose)
|
|
88
|
+
|
|
89
|
+
request = buildKubImgPullRequest(effectiveConfig)
|
|
90
|
+
kubImgRunner = KubImgCommandRunner(verbose=effectiveConfig.verbose)
|
|
91
|
+
return kubImgRunner.pullImage(request, dryRun=options.dryRun)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def inspectSelectedRuntimeImage(
|
|
95
|
+
*,
|
|
96
|
+
options: WrapperOptions,
|
|
97
|
+
cwd: Path | None = None,
|
|
98
|
+
env: Mapping[str, str] | None = None,
|
|
99
|
+
userConfigPath: Path | None = None,
|
|
100
|
+
) -> dict[str, Any]:
|
|
101
|
+
"""Invoke kub-img info for the runtime/image selected by config precedence."""
|
|
102
|
+
|
|
103
|
+
effectiveConfig = resolveEffectiveConfig(
|
|
104
|
+
options=options,
|
|
105
|
+
cwd=cwd,
|
|
106
|
+
env=env,
|
|
107
|
+
userConfigPath=userConfigPath,
|
|
108
|
+
)
|
|
109
|
+
configureLogging(effectiveConfig.verbose)
|
|
110
|
+
|
|
111
|
+
request = buildKubImgInfoRequest(effectiveConfig)
|
|
112
|
+
kubImgRunner = KubImgCommandRunner(verbose=effectiveConfig.verbose)
|
|
113
|
+
return kubImgRunner.inspectImageInfo(request)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def resolveEffectiveConfig(
|
|
117
|
+
*,
|
|
118
|
+
options: WrapperOptions,
|
|
119
|
+
cwd: Path | None = None,
|
|
120
|
+
env: Mapping[str, str] | None = None,
|
|
121
|
+
userConfigPath: Path | None = None,
|
|
122
|
+
) -> KubConfig:
|
|
123
|
+
"""Build the effective config from defaults, files, env, then CLI overrides."""
|
|
124
|
+
|
|
125
|
+
overrideEnv = parseEnvAssignments(options.envVars)
|
|
126
|
+
overrides = KubConfigOverrides(
|
|
127
|
+
runtime=options.runtime,
|
|
128
|
+
image=options.image,
|
|
129
|
+
binds=options.binds,
|
|
130
|
+
workdir=options.pwd,
|
|
131
|
+
runner=options.runner,
|
|
132
|
+
verbose=options.verbose,
|
|
133
|
+
apptainerFlags=options.apptainerFlags,
|
|
134
|
+
dockerFlags=options.dockerFlags,
|
|
135
|
+
env=overrideEnv,
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
return loadKubConfig(
|
|
139
|
+
cwd=cwd,
|
|
140
|
+
env=env,
|
|
141
|
+
overrides=overrides,
|
|
142
|
+
userConfigPath=userConfigPath,
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def parseEnvAssignments(assignments: Sequence[str]) -> dict[str, str]:
|
|
147
|
+
envMapping: dict[str, str] = {}
|
|
148
|
+
|
|
149
|
+
for entry in assignments:
|
|
150
|
+
if "=" not in entry:
|
|
151
|
+
raise ConfigError(
|
|
152
|
+
f"Invalid --env assignment '{entry}'. Expected KEY=VALUE syntax."
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
key, value = entry.split("=", maxsplit=1)
|
|
156
|
+
normalizedKey = key.strip()
|
|
157
|
+
if not normalizedKey:
|
|
158
|
+
raise ConfigError(
|
|
159
|
+
f"Invalid --env assignment '{entry}'. Environment key cannot be empty."
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
envMapping[normalizedKey] = value
|
|
163
|
+
|
|
164
|
+
return envMapping
|