kubechain 0.1.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.
kubechain/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """Decide whether a blockchain workload on Kubernetes is production ready."""
2
+
3
+ __version__ = "0.1.0"
kubechain/__main__.py ADDED
@@ -0,0 +1,5 @@
1
+ import sys
2
+
3
+ from kubechain.cli import main
4
+
5
+ sys.exit(main())
kubechain/catalog.py ADDED
@@ -0,0 +1,152 @@
1
+ """Built-in client profiles and a local file that overrides one of them."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from dataclasses import dataclass, replace
7
+ from importlib.resources import files
8
+ from pathlib import Path
9
+
10
+
11
+ class ProfileError(Exception):
12
+ """The profile file cannot be used."""
13
+
14
+
15
+ ALLOWED = {
16
+ "comm",
17
+ "role",
18
+ "images",
19
+ "p2p_ports",
20
+ "rpc_ports",
21
+ "engine_port",
22
+ "jwt",
23
+ "data_volume",
24
+ "cpu",
25
+ "memory",
26
+ "storage",
27
+ "grace_seconds",
28
+ "exempt_probes",
29
+ "source",
30
+ }
31
+
32
+
33
+ @dataclass(frozen=True)
34
+ class Profile:
35
+ comm: str
36
+ role: str
37
+ images: tuple[str, ...]
38
+ p2p_ports: tuple[int, ...]
39
+ rpc_ports: tuple[int, ...]
40
+ engine_port: int | None
41
+ jwt: bool
42
+ data_volume: str
43
+ cpu: str
44
+ memory: str
45
+ storage: str
46
+ grace_seconds: int
47
+ exempt_probes: bool
48
+ source: str
49
+
50
+
51
+ def catalog() -> dict[str, Profile]:
52
+ raw = json.loads(files("kubechain").joinpath("clients.json").read_text())
53
+ return {name: _profile(body) for name, body in raw.items()}
54
+
55
+
56
+ def load_profile(path: Path) -> dict:
57
+ try:
58
+ data = json.loads(path.read_text())
59
+ except json.JSONDecodeError as exc:
60
+ raise ProfileError(f"profile is not JSON: {path}") from exc
61
+ if not isinstance(data, dict):
62
+ raise ProfileError("profile must be a JSON object")
63
+ extra = sorted(set(data) - ALLOWED)
64
+ if extra:
65
+ raise ProfileError(f"unknown profile field: {extra[0]}")
66
+ if "comm" not in data:
67
+ raise ProfileError("profile needs comm")
68
+ return data
69
+
70
+
71
+ def resolve(container: dict, profiles: dict[str, Profile], overlay: dict | None) -> Profile | None:
72
+ name = str(container.get("name") or "")
73
+ image = str(container.get("image") or "")
74
+ chosen = profiles.get(name) or _by_image(image, profiles)
75
+ if overlay and overlay.get("comm") == name:
76
+ return _merge(chosen, overlay)
77
+ return chosen
78
+
79
+
80
+ def _by_image(image: str, profiles: dict[str, Profile]) -> Profile | None:
81
+ path = image.split("@", 1)[0]
82
+ slash = path.rfind("/")
83
+ colon = path.rfind(":")
84
+ if colon > slash:
85
+ path = path[:colon]
86
+ segments = path.split("/")
87
+ best: Profile | None = None
88
+ best_len = -1
89
+ for profile in profiles.values():
90
+ for hint in profile.images:
91
+ if hint in segments and len(hint) > best_len:
92
+ best = profile
93
+ best_len = len(hint)
94
+ return best
95
+
96
+
97
+ def _merge(base: Profile | None, overlay: dict) -> Profile:
98
+ if base is None:
99
+ base = Profile(
100
+ comm=str(overlay["comm"]),
101
+ role=str(overlay.get("role") or "unknown"),
102
+ images=(),
103
+ p2p_ports=(),
104
+ rpc_ports=(),
105
+ engine_port=None,
106
+ jwt=bool(overlay.get("jwt", False)),
107
+ data_volume=str(overlay.get("data_volume") or ""),
108
+ cpu=str(overlay.get("cpu") or ""),
109
+ memory=str(overlay.get("memory") or ""),
110
+ storage=str(overlay.get("storage") or ""),
111
+ grace_seconds=int(overlay.get("grace_seconds") or 30),
112
+ exempt_probes=bool(overlay.get("exempt_probes", False)),
113
+ source="Local profile.",
114
+ )
115
+ values = {}
116
+ for key in ("role", "data_volume", "cpu", "memory", "storage", "source"):
117
+ if key in overlay:
118
+ values[key] = overlay[key]
119
+ if "engine_port" in overlay:
120
+ values["engine_port"] = overlay["engine_port"]
121
+ if "jwt" in overlay:
122
+ values["jwt"] = bool(overlay["jwt"])
123
+ if "exempt_probes" in overlay:
124
+ values["exempt_probes"] = bool(overlay["exempt_probes"])
125
+ if "grace_seconds" in overlay:
126
+ values["grace_seconds"] = int(overlay["grace_seconds"])
127
+ if "p2p_ports" in overlay:
128
+ values["p2p_ports"] = tuple(int(port) for port in overlay["p2p_ports"])
129
+ if "rpc_ports" in overlay:
130
+ values["rpc_ports"] = tuple(int(port) for port in overlay["rpc_ports"])
131
+ if "images" in overlay:
132
+ values["images"] = tuple(overlay["images"])
133
+ return replace(base, **values)
134
+
135
+
136
+ def _profile(body: dict) -> Profile:
137
+ return Profile(
138
+ comm=body["comm"],
139
+ role=body["role"],
140
+ images=tuple(body.get("images") or ()),
141
+ p2p_ports=tuple(body.get("p2p_ports") or ()),
142
+ rpc_ports=tuple(body.get("rpc_ports") or ()),
143
+ engine_port=body.get("engine_port"),
144
+ jwt=bool(body.get("jwt")),
145
+ data_volume=body.get("data_volume") or "",
146
+ cpu=body.get("cpu") or "",
147
+ memory=body.get("memory") or "",
148
+ storage=body.get("storage") or "",
149
+ grace_seconds=int(body.get("grace_seconds") or 30),
150
+ exempt_probes=bool(body.get("exempt_probes")),
151
+ source=body.get("source") or "KubeChain floor.",
152
+ )