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.
- fedctl-0.1.0/PKG-INFO +11 -0
- fedctl-0.1.0/pyproject.toml +29 -0
- fedctl-0.1.0/setup.cfg +4 -0
- fedctl-0.1.0/src/fedctl/__init__.py +4 -0
- fedctl-0.1.0/src/fedctl/__main__.py +11 -0
- fedctl-0.1.0/src/fedctl/build/__init__.py +19 -0
- fedctl-0.1.0/src/fedctl/build/build.py +33 -0
- fedctl-0.1.0/src/fedctl/build/dockerfile.py +15 -0
- fedctl-0.1.0/src/fedctl/build/errors.py +5 -0
- fedctl-0.1.0/src/fedctl/build/inspect.py +38 -0
- fedctl-0.1.0/src/fedctl/build/push.py +15 -0
- fedctl-0.1.0/src/fedctl/build/state.py +70 -0
- fedctl-0.1.0/src/fedctl/build/tagging.py +30 -0
- fedctl-0.1.0/src/fedctl/cli.py +610 -0
- fedctl-0.1.0/src/fedctl/commands/__init__.py +1 -0
- fedctl-0.1.0/src/fedctl/commands/address.py +88 -0
- fedctl-0.1.0/src/fedctl/commands/build.py +92 -0
- fedctl-0.1.0/src/fedctl/commands/configure.py +85 -0
- fedctl-0.1.0/src/fedctl/commands/deploy.py +401 -0
- fedctl-0.1.0/src/fedctl/commands/destroy.py +84 -0
- fedctl-0.1.0/src/fedctl/commands/discover.py +128 -0
- fedctl-0.1.0/src/fedctl/commands/doctor.py +105 -0
- fedctl-0.1.0/src/fedctl/commands/inspect.py +22 -0
- fedctl-0.1.0/src/fedctl/commands/local.py +264 -0
- fedctl-0.1.0/src/fedctl/commands/ping.py +59 -0
- fedctl-0.1.0/src/fedctl/commands/register.py +267 -0
- fedctl-0.1.0/src/fedctl/commands/run.py +137 -0
- fedctl-0.1.0/src/fedctl/commands/status.py +74 -0
- fedctl-0.1.0/src/fedctl/config/__init__.py +1 -0
- fedctl-0.1.0/src/fedctl/config/io.py +69 -0
- fedctl-0.1.0/src/fedctl/config/merge.py +44 -0
- fedctl-0.1.0/src/fedctl/config/paths.py +22 -0
- fedctl-0.1.0/src/fedctl/config/repo.py +27 -0
- fedctl-0.1.0/src/fedctl/config/schema.py +40 -0
- fedctl-0.1.0/src/fedctl/deploy/__init__.py +14 -0
- fedctl-0.1.0/src/fedctl/deploy/destroy.py +111 -0
- fedctl-0.1.0/src/fedctl/deploy/errors.py +5 -0
- fedctl-0.1.0/src/fedctl/deploy/naming.py +41 -0
- fedctl-0.1.0/src/fedctl/deploy/plan.py +86 -0
- fedctl-0.1.0/src/fedctl/deploy/render.py +432 -0
- fedctl-0.1.0/src/fedctl/deploy/resolve.py +280 -0
- fedctl-0.1.0/src/fedctl/deploy/spec.py +84 -0
- fedctl-0.1.0/src/fedctl/deploy/status.py +83 -0
- fedctl-0.1.0/src/fedctl/deploy/submit.py +36 -0
- fedctl-0.1.0/src/fedctl/nomad/__init__.py +1 -0
- fedctl-0.1.0/src/fedctl/nomad/client.py +146 -0
- fedctl-0.1.0/src/fedctl/nomad/errors.py +20 -0
- fedctl-0.1.0/src/fedctl/nomad/nodeview.py +44 -0
- fedctl-0.1.0/src/fedctl/project/__init__.py +6 -0
- fedctl-0.1.0/src/fedctl/project/errors.py +9 -0
- fedctl-0.1.0/src/fedctl/project/flwr_inspect.py +125 -0
- fedctl-0.1.0/src/fedctl/project/pyproject_patch.py +56 -0
- fedctl-0.1.0/src/fedctl/state/__init__.py +15 -0
- fedctl-0.1.0/src/fedctl/state/errors.py +5 -0
- fedctl-0.1.0/src/fedctl/state/manifest.py +75 -0
- fedctl-0.1.0/src/fedctl/state/store.py +43 -0
- fedctl-0.1.0/src/fedctl/util/console.py +17 -0
- fedctl-0.1.0/src/fedctl.egg-info/PKG-INFO +11 -0
- fedctl-0.1.0/src/fedctl.egg-info/SOURCES.txt +72 -0
- fedctl-0.1.0/src/fedctl.egg-info/dependency_links.txt +1 -0
- fedctl-0.1.0/src/fedctl.egg-info/entry_points.txt +2 -0
- fedctl-0.1.0/src/fedctl.egg-info/requires.txt +6 -0
- fedctl-0.1.0/src/fedctl.egg-info/top_level.txt +1 -0
- fedctl-0.1.0/tests/test_config.py +171 -0
- fedctl-0.1.0/tests/test_deploy_render.py +88 -0
- fedctl-0.1.0/tests/test_deploy_submit_resolve.py +86 -0
- fedctl-0.1.0/tests/test_discover.py +44 -0
- fedctl-0.1.0/tests/test_flwr_inspect.py +100 -0
- fedctl-0.1.0/tests/test_local.py +45 -0
- fedctl-0.1.0/tests/test_nomad_client.py +59 -0
- fedctl-0.1.0/tests/test_ping.py +27 -0
- fedctl-0.1.0/tests/test_pyproject_patch.py +42 -0
- fedctl-0.1.0/tests/test_smoke.py +10 -0
- 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,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,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")
|