scitex-container 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.
Files changed (35) hide show
  1. scitex_container/__init__.py +10 -0
  2. scitex_container/__main__.py +6 -0
  3. scitex_container/_cli/__init__.py +141 -0
  4. scitex_container/_cli/_apptainer.py +352 -0
  5. scitex_container/_cli/_docker.py +48 -0
  6. scitex_container/_cli/_env_snapshot.py +149 -0
  7. scitex_container/_cli/_host.py +109 -0
  8. scitex_container/_cli/_mcp.py +256 -0
  9. scitex_container/_cli/_sandbox.py +231 -0
  10. scitex_container/_cli/_status.py +151 -0
  11. scitex_container/_mcp/__init__.py +38 -0
  12. scitex_container/_mcp/handlers.py +337 -0
  13. scitex_container/_snapshot.py +249 -0
  14. scitex_container/apptainer/__init__.py +74 -0
  15. scitex_container/apptainer/_build.py +116 -0
  16. scitex_container/apptainer/_command_builder.py +301 -0
  17. scitex_container/apptainer/_freeze.py +90 -0
  18. scitex_container/apptainer/_sandbox.py +253 -0
  19. scitex_container/apptainer/_sandbox_versioning.py +298 -0
  20. scitex_container/apptainer/_status.py +86 -0
  21. scitex_container/apptainer/_utils.py +78 -0
  22. scitex_container/apptainer/_verify.py +249 -0
  23. scitex_container/apptainer/_versioning.py +328 -0
  24. scitex_container/docker/__init__.py +16 -0
  25. scitex_container/docker/_compose.py +247 -0
  26. scitex_container/docker/_mounts.py +67 -0
  27. scitex_container/host/__init__.py +18 -0
  28. scitex_container/host/_mounts.py +195 -0
  29. scitex_container/host/_packages.py +185 -0
  30. scitex_container/mcp_server.py +315 -0
  31. scitex_container-0.1.2.dist-info/METADATA +222 -0
  32. scitex_container-0.1.2.dist-info/RECORD +35 -0
  33. scitex_container-0.1.2.dist-info/WHEEL +4 -0
  34. scitex_container-0.1.2.dist-info/entry_points.txt +3 -0
  35. scitex_container-0.1.2.dist-info/licenses/LICENSE +661 -0
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env python3
2
+ # Timestamp: "2026-02-25"
3
+ # File: src/scitex_container/__init__.py
4
+ """scitex-container: Unified container management for Apptainer and Docker."""
5
+
6
+ from . import apptainer, docker, host
7
+ from ._snapshot import env_snapshot
8
+
9
+ __version__ = "0.1.1"
10
+ __all__ = ["apptainer", "docker", "host", "env_snapshot"]
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env python3
2
+ """Allow running as: python -m scitex_container."""
3
+
4
+ from ._cli import main
5
+
6
+ main()
@@ -0,0 +1,141 @@
1
+ #!/usr/bin/env python3
2
+ # Timestamp: "2026-02-25"
3
+ # File: src/scitex_container/_cli/__init__.py
4
+ """CLI package for scitex-container.
5
+
6
+ Entry point: scitex-container (maps to main() here)
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import inspect
12
+
13
+ import click
14
+
15
+ from ._apptainer import (
16
+ build,
17
+ cleanup,
18
+ deploy,
19
+ freeze,
20
+ list_containers,
21
+ rollback,
22
+ switch,
23
+ verify,
24
+ )
25
+ from ._docker import docker
26
+ from ._env_snapshot import env_snapshot_cmd
27
+ from ._host import host
28
+ from ._mcp import mcp
29
+ from ._sandbox import sandbox
30
+ from ._status import status
31
+
32
+
33
+ def _print_help_recursive(ctx, group, prefix="scitex-container"):
34
+ """Recursively print help for a group and all its subcommands/subgroups."""
35
+ click.secho(f"━━━ {prefix} ━━━", fg="cyan", bold=True)
36
+ click.echo(group.get_help(ctx))
37
+
38
+ commands = group.list_commands(ctx) or []
39
+ for name in sorted(commands):
40
+ cmd = group.get_command(ctx, name)
41
+ if cmd is None:
42
+ continue
43
+ sub_prefix = f"{prefix} {name}"
44
+ with click.Context(cmd, info_name=name, parent=ctx) as sub_ctx:
45
+ click.echo()
46
+ if isinstance(cmd, click.Group):
47
+ _print_help_recursive(sub_ctx, cmd, prefix=sub_prefix)
48
+ else:
49
+ click.secho(f"━━━ {sub_prefix} ━━━", fg="cyan", bold=True)
50
+ click.echo(cmd.get_help(sub_ctx))
51
+
52
+
53
+ @click.group()
54
+ @click.version_option(package_name="scitex-container")
55
+ @click.option(
56
+ "--help-recursive", is_flag=True, help="Show help for all commands recursively"
57
+ )
58
+ @click.pass_context
59
+ def main(ctx, help_recursive):
60
+ """scitex-container: Unified container management (Apptainer + Docker + host)."""
61
+ if help_recursive:
62
+ _print_help_recursive(ctx, main)
63
+ ctx.exit(0)
64
+
65
+
66
+ # Apptainer commands (top-level)
67
+ main.add_command(build)
68
+ main.add_command(freeze)
69
+ main.add_command(list_containers)
70
+ main.add_command(switch)
71
+ main.add_command(rollback)
72
+ main.add_command(deploy)
73
+ main.add_command(cleanup)
74
+ main.add_command(verify)
75
+
76
+ # Sub-groups
77
+ main.add_command(sandbox)
78
+ main.add_command(docker)
79
+ main.add_command(host)
80
+ main.add_command(mcp)
81
+
82
+ # Unified status dashboard
83
+ main.add_command(status)
84
+
85
+ # Clew reproducibility snapshot
86
+ main.add_command(env_snapshot_cmd)
87
+
88
+
89
+ @main.command("list-python-apis")
90
+ @click.option(
91
+ "-v", "--verbose", count=True, help="Verbosity: -v with signatures, -vv +docstring"
92
+ )
93
+ def list_python_apis(verbose: int):
94
+ """List scitex_container Python APIs (apptainer, docker, host modules)."""
95
+ import scitex_container.apptainer as apptainer_mod
96
+ import scitex_container.docker as docker_mod
97
+ import scitex_container.host as host_mod
98
+
99
+ modules = [
100
+ ("apptainer", apptainer_mod),
101
+ ("docker", docker_mod),
102
+ ("host", host_mod),
103
+ ]
104
+
105
+ for mod_name, mod in modules:
106
+ public_names = [n for n in dir(mod) if not n.startswith("_")]
107
+ click.secho(f"{mod_name}: {len(public_names)} APIs", fg="green", bold=True)
108
+
109
+ for name in sorted(public_names):
110
+ obj = getattr(mod, name, None)
111
+ if obj is None:
112
+ continue
113
+
114
+ if callable(obj) and not isinstance(obj, type):
115
+ if verbose == 0:
116
+ click.echo(f" {name}")
117
+ elif verbose >= 1:
118
+ try:
119
+ sig_str = str(inspect.signature(obj))
120
+ except (ValueError, TypeError):
121
+ sig_str = "()"
122
+ click.echo(f" {click.style(name, fg='white', bold=True)}{sig_str}")
123
+ if verbose >= 2:
124
+ doc = inspect.getdoc(obj)
125
+ if doc:
126
+ first_line = doc.split("\n")[0].strip()
127
+ click.echo(f" {first_line}")
128
+ elif isinstance(obj, type):
129
+ click.echo(f" {name} [class]")
130
+ else:
131
+ if verbose >= 1:
132
+ click.echo(f" {name} = {obj!r}")
133
+ else:
134
+ click.echo(f" {name}")
135
+
136
+ click.echo()
137
+
138
+
139
+ __all__ = ["main"]
140
+
141
+ # EOF
@@ -0,0 +1,352 @@
1
+ #!/usr/bin/env python3
2
+ # Timestamp: "2026-02-25"
3
+ # File: src/scitex_container/_cli/_apptainer.py
4
+ """CLI commands for Apptainer container operations."""
5
+
6
+ from __future__ import annotations
7
+
8
+ import click
9
+
10
+
11
+ def register(main: click.Group) -> None:
12
+ """Register all Apptainer commands onto main."""
13
+ main.add_command(build)
14
+ main.add_command(freeze)
15
+ main.add_command(list_containers)
16
+ main.add_command(switch)
17
+ main.add_command(rollback)
18
+ main.add_command(deploy)
19
+ main.add_command(cleanup)
20
+ main.add_command(verify)
21
+
22
+
23
+ @click.command()
24
+ @click.argument("name", default="scitex-final")
25
+ @click.option(
26
+ "--sandbox", is_flag=True, help="Build a sandbox directory instead of SIF."
27
+ )
28
+ @click.option("--base", is_flag=True, help="Build the base image instead of final.")
29
+ @click.option("--force", "-f", is_flag=True, help="Force rebuild even if up-to-date.")
30
+ @click.option("--output-dir", "-o", type=click.Path(), help="Output directory.")
31
+ def build(name, sandbox, base, force, output_dir):
32
+ """Build a SIF or sandbox from a .def file."""
33
+ from scitex_container.apptainer import build as do_build
34
+
35
+ try:
36
+ output_path = do_build(
37
+ def_name=name,
38
+ output_dir=output_dir,
39
+ force=force,
40
+ sandbox=sandbox,
41
+ )
42
+ click.secho(f"Built: {output_path}", fg="green")
43
+ except FileNotFoundError as exc:
44
+ click.secho(str(exc), fg="red", err=True)
45
+ raise SystemExit(1)
46
+ except RuntimeError as exc:
47
+ click.secho(f"Build failed: {exc}", fg="red", err=True)
48
+ click.secho(
49
+ "Tip: check that apptainer is installed and the .def file exists.",
50
+ fg="yellow",
51
+ err=True,
52
+ )
53
+ raise SystemExit(1)
54
+
55
+
56
+ @click.command()
57
+ @click.argument("sif_path", type=click.Path(exists=True))
58
+ @click.option(
59
+ "--output-dir", "-o", type=click.Path(), help="Output directory for lock files."
60
+ )
61
+ def freeze(sif_path, output_dir):
62
+ """Extract pinned package versions (pip, dpkg, npm) from a built SIF."""
63
+ from scitex_container.apptainer import freeze as do_freeze
64
+
65
+ try:
66
+ lock_files = do_freeze(sif_path=sif_path, output_dir=output_dir)
67
+ click.secho("Lock files generated:", fg="green")
68
+ for kind, path in lock_files.items():
69
+ click.echo(f" {kind}: {path}")
70
+ except FileNotFoundError as exc:
71
+ click.secho(str(exc), fg="red", err=True)
72
+ raise SystemExit(1)
73
+ except RuntimeError as exc:
74
+ click.secho(f"Freeze failed: {exc}", fg="red", err=True)
75
+ raise SystemExit(1)
76
+
77
+
78
+ @click.command(name="list")
79
+ @click.option(
80
+ "--dir", "-d", "containers_dir", type=click.Path(), help="Containers directory."
81
+ )
82
+ def list_containers(containers_dir):
83
+ """List available container versions."""
84
+ from pathlib import Path
85
+
86
+ from scitex_container.apptainer import (
87
+ find_containers_dir,
88
+ get_active_version,
89
+ list_versions,
90
+ )
91
+
92
+ try:
93
+ cdir = Path(containers_dir) if containers_dir else find_containers_dir()
94
+ except FileNotFoundError as exc:
95
+ click.secho(str(exc), fg="red", err=True)
96
+ raise SystemExit(1)
97
+
98
+ versions = list_versions(cdir)
99
+ if not versions:
100
+ click.echo(f"No versioned SIFs found in {cdir}")
101
+ return
102
+
103
+ active = get_active_version(cdir)
104
+ click.secho(f"Container versions in {cdir}:", fg="cyan")
105
+ for v in versions:
106
+ marker = click.style(" *", fg="green") if v["active"] else " "
107
+ version_str = click.style(v["version"], fg="green" if v["active"] else "white")
108
+ click.echo(f" {marker} {version_str} {v['size']} {v['date']}")
109
+
110
+ if active:
111
+ click.echo()
112
+ click.echo(f" Active: {click.style(active, fg='green', bold=True)}")
113
+
114
+
115
+ @click.command()
116
+ @click.argument("version")
117
+ @click.option(
118
+ "--dir", "-d", "containers_dir", type=click.Path(), help="Containers directory."
119
+ )
120
+ @click.option(
121
+ "--sudo", "use_sudo", is_flag=True, help="Use sudo for symlink operations."
122
+ )
123
+ def switch(version, containers_dir, use_sudo):
124
+ """Switch active container to VERSION."""
125
+ from pathlib import Path
126
+
127
+ from scitex_container.apptainer import (
128
+ find_containers_dir,
129
+ get_active_version,
130
+ switch_version,
131
+ )
132
+
133
+ try:
134
+ cdir = Path(containers_dir) if containers_dir else find_containers_dir()
135
+ except FileNotFoundError as exc:
136
+ click.secho(str(exc), fg="red", err=True)
137
+ raise SystemExit(1)
138
+
139
+ old_version = get_active_version(cdir)
140
+
141
+ try:
142
+ switch_version(version, cdir, use_sudo=use_sudo)
143
+ except FileNotFoundError as exc:
144
+ click.secho(str(exc), fg="red", err=True)
145
+ click.secho(
146
+ "Tip: run 'scitex-container list' to see available versions.",
147
+ fg="yellow",
148
+ err=True,
149
+ )
150
+ raise SystemExit(1)
151
+ except RuntimeError as exc:
152
+ click.secho(f"Switch failed: {exc}", fg="red", err=True)
153
+ raise SystemExit(1)
154
+
155
+ if old_version:
156
+ click.secho(f"Switched {old_version} -> {version}", fg="green")
157
+ else:
158
+ click.secho(f"Activated version {version}", fg="green")
159
+
160
+
161
+ @click.command()
162
+ @click.option(
163
+ "--dir", "-d", "containers_dir", type=click.Path(), help="Containers directory."
164
+ )
165
+ @click.option(
166
+ "--sudo", "use_sudo", is_flag=True, help="Use sudo for symlink operations."
167
+ )
168
+ def rollback(containers_dir, use_sudo):
169
+ """Revert to the previous container version."""
170
+ from pathlib import Path
171
+
172
+ from scitex_container.apptainer import find_containers_dir, get_active_version
173
+ from scitex_container.apptainer import rollback as do_rollback
174
+
175
+ try:
176
+ cdir = Path(containers_dir) if containers_dir else find_containers_dir()
177
+ except FileNotFoundError as exc:
178
+ click.secho(str(exc), fg="red", err=True)
179
+ raise SystemExit(1)
180
+
181
+ old_version = get_active_version(cdir)
182
+
183
+ try:
184
+ new_version = do_rollback(cdir, use_sudo=use_sudo)
185
+ except RuntimeError as exc:
186
+ click.secho(f"Rollback failed: {exc}", fg="red", err=True)
187
+ click.secho(
188
+ "Tip: rollback requires at least two versions to be present.",
189
+ fg="yellow",
190
+ err=True,
191
+ )
192
+ raise SystemExit(1)
193
+
194
+ click.secho(f"Rolled back {old_version} -> {new_version}", fg="green")
195
+
196
+
197
+ @click.command()
198
+ @click.option(
199
+ "--target",
200
+ "-t",
201
+ "target_dir",
202
+ type=click.Path(),
203
+ default="/opt/scitex/singularity",
204
+ show_default=True,
205
+ help="Deployment target directory.",
206
+ )
207
+ @click.option(
208
+ "--dir",
209
+ "-d",
210
+ "containers_dir",
211
+ type=click.Path(),
212
+ help="Source containers directory.",
213
+ )
214
+ def deploy(target_dir, containers_dir):
215
+ """Copy active SIF to production target directory."""
216
+ from pathlib import Path
217
+
218
+ from scitex_container.apptainer import find_containers_dir
219
+ from scitex_container.apptainer import deploy as do_deploy
220
+
221
+ try:
222
+ cdir = Path(containers_dir) if containers_dir else find_containers_dir()
223
+ except FileNotFoundError as exc:
224
+ click.secho(str(exc), fg="red", err=True)
225
+ raise SystemExit(1)
226
+
227
+ try:
228
+ do_deploy(source_dir=cdir, target_dir=Path(target_dir))
229
+ except (FileNotFoundError, RuntimeError) as exc:
230
+ click.secho(f"Deploy failed: {exc}", fg="red", err=True)
231
+ click.secho(
232
+ "Tip: ensure an active version is set (run 'scitex-container switch').",
233
+ fg="yellow",
234
+ err=True,
235
+ )
236
+ raise SystemExit(1)
237
+
238
+ click.secho(f"Deployed to {target_dir}", fg="green")
239
+
240
+
241
+ @click.command()
242
+ @click.option(
243
+ "--keep",
244
+ "-k",
245
+ type=int,
246
+ default=3,
247
+ show_default=True,
248
+ help="Number of recent versions to keep.",
249
+ )
250
+ @click.option(
251
+ "--dir", "-d", "containers_dir", type=click.Path(), help="Containers directory."
252
+ )
253
+ def cleanup(keep, containers_dir):
254
+ """Remove old container versions, keeping the N most recent."""
255
+ from pathlib import Path
256
+
257
+ from scitex_container.apptainer import find_containers_dir
258
+ from scitex_container.apptainer import cleanup as do_cleanup
259
+
260
+ try:
261
+ cdir = Path(containers_dir) if containers_dir else find_containers_dir()
262
+ except FileNotFoundError as exc:
263
+ click.secho(str(exc), fg="red", err=True)
264
+ raise SystemExit(1)
265
+
266
+ removed = do_cleanup(cdir, keep=keep)
267
+
268
+ if removed:
269
+ click.secho(f"Removed {len(removed)} old version(s):", fg="yellow")
270
+ for path in removed:
271
+ click.echo(f" {path.name}")
272
+ else:
273
+ click.secho("No old versions to remove.", fg="green")
274
+
275
+
276
+ @click.command()
277
+ @click.argument("sif_path", required=False)
278
+ @click.option("--def", "def_path", help="Path to .def file to verify against.")
279
+ @click.option("--lock-dir", help="Directory containing lock files.")
280
+ @click.option("--json", "as_json", is_flag=True, help="Output raw JSON.")
281
+ def verify(sif_path, def_path, lock_dir, as_json):
282
+ """Verify SIF integrity: hash, .def origin, and lock file consistency."""
283
+ import json as json_mod
284
+
285
+ from scitex_container.apptainer import (
286
+ find_containers_dir,
287
+ get_active_version,
288
+ verify as do_verify,
289
+ )
290
+
291
+ if not sif_path:
292
+ try:
293
+ cdir = find_containers_dir()
294
+ active = get_active_version(cdir)
295
+ if active:
296
+ sif_path = str(cdir / f"scitex-v{active}.sif")
297
+ else:
298
+ click.secho(
299
+ "No active SIF found. Provide SIF_PATH argument.",
300
+ fg="red",
301
+ err=True,
302
+ )
303
+ raise SystemExit(1)
304
+ except FileNotFoundError as exc:
305
+ click.secho(str(exc), fg="red", err=True)
306
+ raise SystemExit(1)
307
+
308
+ result = do_verify(sif_path=sif_path, def_path=def_path, lock_dir=lock_dir)
309
+
310
+ if as_json:
311
+ click.echo(json_mod.dumps(result, indent=2))
312
+ else:
313
+ _print_verify_result(result)
314
+
315
+ raise SystemExit(0 if result["overall"] == "pass" else 1)
316
+
317
+
318
+ def _print_verify_result(result: dict) -> None:
319
+ """Pretty-print verification results."""
320
+ status_colors = {"pass": "green", "fail": "red", "skip": "yellow"}
321
+
322
+ click.secho("Container Verification Report", fg="cyan", bold=True)
323
+ click.echo()
324
+
325
+ # SIF
326
+ sif = result["sif"]
327
+ click.echo(f" SIF: {sif['path']}")
328
+ if sif["exists"]:
329
+ click.echo(f" SHA256: {sif['sha256']}")
330
+ else:
331
+ click.secho(" NOT FOUND", fg="red")
332
+ click.echo()
333
+
334
+ # Checks
335
+ for check_name, label in [
336
+ ("def_origin", ".def Origin"),
337
+ ("pip_lock", "pip Packages"),
338
+ ("dpkg_lock", "dpkg Packages"),
339
+ ]:
340
+ check = result[check_name]
341
+ status = check["status"]
342
+ color = status_colors.get(status, "white")
343
+ badge = click.style(f"[{status.upper()}]", fg=color, bold=True)
344
+ click.echo(f" {badge} {label}: {check['detail']}")
345
+
346
+ click.echo()
347
+ overall = result["overall"]
348
+ color = "green" if overall == "pass" else "red"
349
+ click.secho(f" Overall: {overall.upper()}", fg=color, bold=True)
350
+
351
+
352
+ # EOF
@@ -0,0 +1,48 @@
1
+ #!/usr/bin/env python3
2
+ # Timestamp: "2026-02-25"
3
+ # File: src/scitex_container/_cli/_docker.py
4
+ """CLI docker sub-group for Docker Compose management."""
5
+
6
+ from __future__ import annotations
7
+
8
+ import click
9
+
10
+
11
+ @click.group()
12
+ def docker():
13
+ """Manage Docker Compose services."""
14
+
15
+
16
+ @docker.command(name="rebuild")
17
+ @click.option(
18
+ "--env", "-e", default="dev", show_default=True, help="Environment (dev/prod)."
19
+ )
20
+ def docker_rebuild(env):
21
+ """Rebuild Docker containers without cache."""
22
+ from scitex_container.docker import rebuild as do_rebuild
23
+
24
+ click.secho(f"Rebuilding Docker containers for env={env}...", fg="cyan")
25
+ rc = do_rebuild(env=env)
26
+ if rc != 0:
27
+ click.secho(f"docker compose build exited with code {rc}", fg="red", err=True)
28
+ raise SystemExit(rc)
29
+ click.secho("Rebuild complete.", fg="green")
30
+
31
+
32
+ @docker.command(name="restart")
33
+ @click.option(
34
+ "--env", "-e", default="dev", show_default=True, help="Environment (dev/prod)."
35
+ )
36
+ def docker_restart(env):
37
+ """Restart Docker containers (down then up -d)."""
38
+ from scitex_container.docker import restart as do_restart
39
+
40
+ click.secho(f"Restarting Docker containers for env={env}...", fg="cyan")
41
+ rc = do_restart(env=env)
42
+ if rc != 0:
43
+ click.secho(f"docker compose restart exited with code {rc}", fg="red", err=True)
44
+ raise SystemExit(rc)
45
+ click.secho("Containers restarted.", fg="green")
46
+
47
+
48
+ # EOF