fedctl 0.1.0__tar.gz

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 (74) hide show
  1. fedctl-0.1.0/PKG-INFO +11 -0
  2. fedctl-0.1.0/pyproject.toml +29 -0
  3. fedctl-0.1.0/setup.cfg +4 -0
  4. fedctl-0.1.0/src/fedctl/__init__.py +4 -0
  5. fedctl-0.1.0/src/fedctl/__main__.py +11 -0
  6. fedctl-0.1.0/src/fedctl/build/__init__.py +19 -0
  7. fedctl-0.1.0/src/fedctl/build/build.py +33 -0
  8. fedctl-0.1.0/src/fedctl/build/dockerfile.py +15 -0
  9. fedctl-0.1.0/src/fedctl/build/errors.py +5 -0
  10. fedctl-0.1.0/src/fedctl/build/inspect.py +38 -0
  11. fedctl-0.1.0/src/fedctl/build/push.py +15 -0
  12. fedctl-0.1.0/src/fedctl/build/state.py +70 -0
  13. fedctl-0.1.0/src/fedctl/build/tagging.py +30 -0
  14. fedctl-0.1.0/src/fedctl/cli.py +610 -0
  15. fedctl-0.1.0/src/fedctl/commands/__init__.py +1 -0
  16. fedctl-0.1.0/src/fedctl/commands/address.py +88 -0
  17. fedctl-0.1.0/src/fedctl/commands/build.py +92 -0
  18. fedctl-0.1.0/src/fedctl/commands/configure.py +85 -0
  19. fedctl-0.1.0/src/fedctl/commands/deploy.py +401 -0
  20. fedctl-0.1.0/src/fedctl/commands/destroy.py +84 -0
  21. fedctl-0.1.0/src/fedctl/commands/discover.py +128 -0
  22. fedctl-0.1.0/src/fedctl/commands/doctor.py +105 -0
  23. fedctl-0.1.0/src/fedctl/commands/inspect.py +22 -0
  24. fedctl-0.1.0/src/fedctl/commands/local.py +264 -0
  25. fedctl-0.1.0/src/fedctl/commands/ping.py +59 -0
  26. fedctl-0.1.0/src/fedctl/commands/register.py +267 -0
  27. fedctl-0.1.0/src/fedctl/commands/run.py +137 -0
  28. fedctl-0.1.0/src/fedctl/commands/status.py +74 -0
  29. fedctl-0.1.0/src/fedctl/config/__init__.py +1 -0
  30. fedctl-0.1.0/src/fedctl/config/io.py +69 -0
  31. fedctl-0.1.0/src/fedctl/config/merge.py +44 -0
  32. fedctl-0.1.0/src/fedctl/config/paths.py +22 -0
  33. fedctl-0.1.0/src/fedctl/config/repo.py +27 -0
  34. fedctl-0.1.0/src/fedctl/config/schema.py +40 -0
  35. fedctl-0.1.0/src/fedctl/deploy/__init__.py +14 -0
  36. fedctl-0.1.0/src/fedctl/deploy/destroy.py +111 -0
  37. fedctl-0.1.0/src/fedctl/deploy/errors.py +5 -0
  38. fedctl-0.1.0/src/fedctl/deploy/naming.py +41 -0
  39. fedctl-0.1.0/src/fedctl/deploy/plan.py +86 -0
  40. fedctl-0.1.0/src/fedctl/deploy/render.py +432 -0
  41. fedctl-0.1.0/src/fedctl/deploy/resolve.py +280 -0
  42. fedctl-0.1.0/src/fedctl/deploy/spec.py +84 -0
  43. fedctl-0.1.0/src/fedctl/deploy/status.py +83 -0
  44. fedctl-0.1.0/src/fedctl/deploy/submit.py +36 -0
  45. fedctl-0.1.0/src/fedctl/nomad/__init__.py +1 -0
  46. fedctl-0.1.0/src/fedctl/nomad/client.py +146 -0
  47. fedctl-0.1.0/src/fedctl/nomad/errors.py +20 -0
  48. fedctl-0.1.0/src/fedctl/nomad/nodeview.py +44 -0
  49. fedctl-0.1.0/src/fedctl/project/__init__.py +6 -0
  50. fedctl-0.1.0/src/fedctl/project/errors.py +9 -0
  51. fedctl-0.1.0/src/fedctl/project/flwr_inspect.py +125 -0
  52. fedctl-0.1.0/src/fedctl/project/pyproject_patch.py +56 -0
  53. fedctl-0.1.0/src/fedctl/state/__init__.py +15 -0
  54. fedctl-0.1.0/src/fedctl/state/errors.py +5 -0
  55. fedctl-0.1.0/src/fedctl/state/manifest.py +75 -0
  56. fedctl-0.1.0/src/fedctl/state/store.py +43 -0
  57. fedctl-0.1.0/src/fedctl/util/console.py +17 -0
  58. fedctl-0.1.0/src/fedctl.egg-info/PKG-INFO +11 -0
  59. fedctl-0.1.0/src/fedctl.egg-info/SOURCES.txt +72 -0
  60. fedctl-0.1.0/src/fedctl.egg-info/dependency_links.txt +1 -0
  61. fedctl-0.1.0/src/fedctl.egg-info/entry_points.txt +2 -0
  62. fedctl-0.1.0/src/fedctl.egg-info/requires.txt +6 -0
  63. fedctl-0.1.0/src/fedctl.egg-info/top_level.txt +1 -0
  64. fedctl-0.1.0/tests/test_config.py +171 -0
  65. fedctl-0.1.0/tests/test_deploy_render.py +88 -0
  66. fedctl-0.1.0/tests/test_deploy_submit_resolve.py +86 -0
  67. fedctl-0.1.0/tests/test_discover.py +44 -0
  68. fedctl-0.1.0/tests/test_flwr_inspect.py +100 -0
  69. fedctl-0.1.0/tests/test_local.py +45 -0
  70. fedctl-0.1.0/tests/test_nomad_client.py +59 -0
  71. fedctl-0.1.0/tests/test_ping.py +27 -0
  72. fedctl-0.1.0/tests/test_pyproject_patch.py +42 -0
  73. fedctl-0.1.0/tests/test_smoke.py +10 -0
  74. fedctl-0.1.0/tests/test_state_store.py +55 -0
fedctl-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,11 @@
1
+ Metadata-Version: 2.4
2
+ Name: fedctl
3
+ Version: 0.1.0
4
+ Summary: Federated control CLI
5
+ Requires-Python: >=3.11
6
+ Requires-Dist: typer>=0.12.0
7
+ Requires-Dist: tomlkit>=0.12.0
8
+ Requires-Dist: rich>=13.0.0
9
+ Requires-Dist: httpx>=0.27.0
10
+ Requires-Dist: jinja2>=3.1.0
11
+ Requires-Dist: PyYAML>=6.0.0
@@ -0,0 +1,29 @@
1
+ [build-system]
2
+ requires = ["setuptools>=65", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "fedctl"
7
+ version = "0.1.0"
8
+ description = "Federated control CLI"
9
+ requires-python = ">=3.11"
10
+ dependencies = [
11
+ "typer>=0.12.0",
12
+ "tomlkit>=0.12.0",
13
+ "rich>=13.0.0",
14
+ "httpx>=0.27.0",
15
+ "jinja2>=3.1.0",
16
+ "PyYAML>=6.0.0",
17
+ ]
18
+
19
+ [project.scripts]
20
+ fedctl = "fedctl.cli:app"
21
+
22
+ [tool.setuptools]
23
+ package-dir = {"" = "src"}
24
+
25
+ [tool.setuptools.packages.find]
26
+ where = ["src"]
27
+
28
+ [tool.pytest.ini_options]
29
+ testpaths = ["tests"]
fedctl-0.1.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,4 @@
1
+ """fedctl package."""
2
+
3
+ __all__ = ["__version__"]
4
+ __version__ = "0.0.1"
@@ -0,0 +1,11 @@
1
+ """Module entrypoint for `python -m fedctl`."""
2
+
3
+ from .cli import app
4
+
5
+
6
+ def main() -> None:
7
+ app()
8
+
9
+
10
+ if __name__ == "__main__":
11
+ main()
@@ -0,0 +1,19 @@
1
+ """Build pipeline for SuperExec images."""
2
+
3
+ from .build import build_image
4
+ from .dockerfile import render_dockerfile
5
+ from .errors import BuildError
6
+ from .inspect import inspect_project
7
+ from .state import BuildMetadata, load_latest_build, write_latest_build
8
+ from .tagging import default_image_tag
9
+
10
+ __all__ = [
11
+ "BuildError",
12
+ "BuildMetadata",
13
+ "build_image",
14
+ "default_image_tag",
15
+ "inspect_project",
16
+ "load_latest_build",
17
+ "render_dockerfile",
18
+ "write_latest_build",
19
+ ]
@@ -0,0 +1,33 @@
1
+ from __future__ import annotations
2
+
3
+ import subprocess
4
+ from pathlib import Path
5
+
6
+ from .errors import BuildError
7
+
8
+
9
+ def build_image(
10
+ *,
11
+ image: str,
12
+ dockerfile_path: Path,
13
+ context_dir: Path,
14
+ no_cache: bool = False,
15
+ platform: str | None = None,
16
+ quiet: bool = False,
17
+ ) -> None:
18
+ cmd = ["docker", "build", "-t", image, "-f", str(dockerfile_path)]
19
+ if no_cache:
20
+ cmd.append("--no-cache")
21
+ if quiet:
22
+ cmd.append("--quiet")
23
+ if platform:
24
+ cmd.extend(["--platform", platform])
25
+ cmd.append(str(context_dir))
26
+
27
+ try:
28
+ result = subprocess.run(cmd, check=False)
29
+ except FileNotFoundError as exc:
30
+ raise BuildError("Docker is not installed or not on PATH.") from exc
31
+
32
+ if result.returncode != 0:
33
+ raise BuildError(f"Docker build failed with exit code {result.returncode}.")
@@ -0,0 +1,15 @@
1
+ from __future__ import annotations
2
+
3
+
4
+ def render_dockerfile(flwr_version: str) -> str:
5
+ return (
6
+ f"FROM flwr/superexec:{flwr_version}\n"
7
+ "\n"
8
+ "WORKDIR /app\n"
9
+ "\n"
10
+ "COPY pyproject.toml .\n"
11
+ "RUN sed -i 's/.*flwr\\[simulation\\].*//' pyproject.toml \\\n"
12
+ " && python -m pip install -U --no-cache-dir .\n"
13
+ "\n"
14
+ 'ENTRYPOINT ["flower-superexec"]\n'
15
+ )
@@ -0,0 +1,5 @@
1
+ from __future__ import annotations
2
+
3
+
4
+ class BuildError(Exception):
5
+ """User-facing build errors."""
@@ -0,0 +1,38 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from pathlib import Path
5
+
6
+ from fedctl.project.flwr_inspect import FlwrProjectInfo, inspect_flwr_project, load_pyproject
7
+
8
+
9
+ @dataclass(frozen=True)
10
+ class BuildProjectInfo:
11
+ root: Path
12
+ pyproject_path: Path
13
+ project_name: str
14
+ project_version: str | None
15
+ flwr_info: FlwrProjectInfo
16
+
17
+
18
+ def inspect_project(path: Path) -> BuildProjectInfo:
19
+ root, doc = load_pyproject(path)
20
+ pyproject_path = root / "pyproject.toml"
21
+ flwr_info = inspect_flwr_project(root)
22
+
23
+ project = doc.get("project", {}) if isinstance(doc.get("project"), dict) else {}
24
+ name = project.get("name")
25
+ if not isinstance(name, str) or not name:
26
+ raise ValueError("Missing [project].name in pyproject.toml.")
27
+
28
+ version = project.get("version")
29
+ if not isinstance(version, str):
30
+ version = None
31
+
32
+ return BuildProjectInfo(
33
+ root=root,
34
+ pyproject_path=pyproject_path,
35
+ project_name=name,
36
+ project_version=version,
37
+ flwr_info=flwr_info,
38
+ )
@@ -0,0 +1,15 @@
1
+ from __future__ import annotations
2
+
3
+ import subprocess
4
+
5
+ from .errors import BuildError
6
+
7
+
8
+ def push_image(image: str) -> None:
9
+ try:
10
+ result = subprocess.run(["docker", "push", image], check=False)
11
+ except FileNotFoundError as exc:
12
+ raise BuildError("Docker is not installed or not on PATH.") from exc
13
+
14
+ if result.returncode != 0:
15
+ raise BuildError(f"Docker push failed with exit code {result.returncode}.")
@@ -0,0 +1,70 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ from dataclasses import dataclass
6
+ from datetime import datetime, timezone
7
+ from pathlib import Path
8
+
9
+ from fedctl.config.paths import user_config_dir
10
+ from .errors import BuildError
11
+
12
+
13
+ @dataclass(frozen=True)
14
+ class BuildMetadata:
15
+ image: str
16
+ project: str
17
+ flwr_version: str
18
+ timestamp: str
19
+
20
+ def to_dict(self) -> dict[str, str]:
21
+ return {
22
+ "image": self.image,
23
+ "project": self.project,
24
+ "flwr_version": self.flwr_version,
25
+ "timestamp": self.timestamp,
26
+ }
27
+
28
+
29
+ def new_timestamp() -> str:
30
+ return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
31
+
32
+
33
+ def latest_build_path() -> Path:
34
+ return user_config_dir() / "builds" / "latest.json"
35
+
36
+
37
+ def write_latest_build(metadata: BuildMetadata) -> Path:
38
+ path = latest_build_path()
39
+ path.parent.mkdir(parents=True, exist_ok=True)
40
+ payload = json.dumps(metadata.to_dict(), indent=2, sort_keys=True)
41
+ tmp_path = path.with_suffix(path.suffix + ".tmp")
42
+ try:
43
+ tmp_path.write_text(payload, encoding="utf-8")
44
+ os.replace(tmp_path, path)
45
+ except OSError as exc:
46
+ raise BuildError(f"Failed to write build metadata: {exc}") from exc
47
+ return path
48
+
49
+
50
+ def load_latest_build() -> BuildMetadata:
51
+ path = latest_build_path()
52
+ if not path.exists():
53
+ raise BuildError(f"No build metadata found at {path}.")
54
+ try:
55
+ raw = json.loads(path.read_text(encoding="utf-8"))
56
+ except json.JSONDecodeError as exc:
57
+ raise BuildError(f"Build metadata at {path} is invalid JSON.") from exc
58
+
59
+ image = raw.get("image")
60
+ project = raw.get("project")
61
+ flwr_version = raw.get("flwr_version")
62
+ timestamp = raw.get("timestamp")
63
+ if not all(isinstance(val, str) and val for val in [image, project, flwr_version, timestamp]):
64
+ raise BuildError(f"Build metadata at {path} is missing required fields.")
65
+ return BuildMetadata(
66
+ image=image,
67
+ project=project,
68
+ flwr_version=flwr_version,
69
+ timestamp=timestamp,
70
+ )
@@ -0,0 +1,30 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import datetime, timezone
4
+ from pathlib import Path
5
+ import subprocess
6
+
7
+
8
+ def default_image_tag(project_name: str, *, repo_root: Path | None = None) -> str:
9
+ suffix = _git_sha(repo_root) or _timestamp()
10
+ base = project_name.strip() or "superexec"
11
+ return f"{base}-superexec:{suffix}"
12
+
13
+
14
+ def _git_sha(repo_root: Path | None) -> str | None:
15
+ try:
16
+ result = subprocess.run(
17
+ ["git", "rev-parse", "--short", "HEAD"],
18
+ cwd=str(repo_root) if repo_root else None,
19
+ check=True,
20
+ capture_output=True,
21
+ text=True,
22
+ )
23
+ except (subprocess.SubprocessError, FileNotFoundError):
24
+ return None
25
+ sha = result.stdout.strip()
26
+ return sha if sha else None
27
+
28
+
29
+ def _timestamp() -> str:
30
+ return datetime.now(timezone.utc).strftime("%Y%m%d%H%M%S")