mainboard 0.0.1__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 (59) hide show
  1. mainboard/__init__.py +113 -0
  2. mainboard/__main__.py +2 -0
  3. mainboard/assets/banner.svg +54 -0
  4. mainboard/cli.py +24 -0
  5. mainboard/enums.py +78 -0
  6. mainboard/gpu.py +178 -0
  7. mainboard/host.py +133 -0
  8. mainboard/machine.py +127 -0
  9. mainboard/models/__init__.py +58 -0
  10. mainboard/models/base.py +26 -0
  11. mainboard/models/board.py +58 -0
  12. mainboard/models/clock.py +20 -0
  13. mainboard/models/clock_info.py +14 -0
  14. mainboard/models/compiler_info.py +48 -0
  15. mainboard/models/compute_capability.py +20 -0
  16. mainboard/models/cuda_python_info.py +11 -0
  17. mainboard/models/dimm_card.py +34 -0
  18. mainboard/models/disk.py +102 -0
  19. mainboard/models/drive_info.py +72 -0
  20. mainboard/models/energy_interval.py +34 -0
  21. mainboard/models/energy_reading.py +19 -0
  22. mainboard/models/environment.py +72 -0
  23. mainboard/models/gpu_snapshot.py +28 -0
  24. mainboard/models/host_disk.py +36 -0
  25. mainboard/models/host_memory.py +99 -0
  26. mainboard/models/machine_snapshot.py +68 -0
  27. mainboard/models/mem_info.py +35 -0
  28. mainboard/models/memory_card.py +122 -0
  29. mainboard/models/memory_usage.py +46 -0
  30. mainboard/models/partition_info.py +86 -0
  31. mainboard/models/pcie_info.py +14 -0
  32. mainboard/models/process_info.py +14 -0
  33. mainboard/models/system_compilers.py +73 -0
  34. mainboard/models/thermal_state.py +35 -0
  35. mainboard/models/thermal_tracker.py +36 -0
  36. mainboard/models/throttle_reason.py +30 -0
  37. mainboard/models/unit_snapshot.py +38 -0
  38. mainboard/models/utilization.py +14 -0
  39. mainboard/npu.py +23 -0
  40. mainboard/providers/__init__.py +23 -0
  41. mainboard/providers/amd.py +23 -0
  42. mainboard/providers/apple.py +176 -0
  43. mainboard/providers/intel.py +41 -0
  44. mainboard/providers/nvidia.py +304 -0
  45. mainboard/providers/qualcomm.py +41 -0
  46. mainboard/py.typed +0 -0
  47. mainboard/shell/__init__.py +15 -0
  48. mainboard/shell/run.py +18 -0
  49. mainboard/shell/sysctl.py +17 -0
  50. mainboard/shell/sysfs.py +29 -0
  51. mainboard/shell/system_profiler.py +25 -0
  52. mainboard/unit.py +138 -0
  53. mainboard/visual.py +289 -0
  54. mainboard-0.0.1.dist-info/METADATA +100 -0
  55. mainboard-0.0.1.dist-info/RECORD +59 -0
  56. mainboard-0.0.1.dist-info/WHEEL +5 -0
  57. mainboard-0.0.1.dist-info/entry_points.txt +2 -0
  58. mainboard-0.0.1.dist-info/licenses/LICENSE +202 -0
  59. mainboard-0.0.1.dist-info/top_level.txt +1 -0
mainboard/__init__.py ADDED
@@ -0,0 +1,113 @@
1
+ from __future__ import annotations
2
+
3
+ from .enums import (
4
+ CmakeBuildType,
5
+ CompilerKind,
6
+ CudaPythonVariant,
7
+ DiskKind,
8
+ Scheduler,
9
+ SystemFamily,
10
+ UnitKind,
11
+ Vendor,
12
+ )
13
+ from .gpu import GPU
14
+ from .host import Host
15
+ from .machine import Machine
16
+ from .models import (
17
+ Board,
18
+ Clock,
19
+ ClockInfo,
20
+ CompilerInfo,
21
+ ComputeCapability,
22
+ CpuSnapshot,
23
+ CudaPythonInfo,
24
+ DriveInfo,
25
+ EnergyInterval,
26
+ EnergyReading,
27
+ Environment,
28
+ GPUSnapshot,
29
+ HostDisk,
30
+ HostMemory,
31
+ MachineSnapshot,
32
+ MemInfo,
33
+ MemoryCard,
34
+ MemoryUsage,
35
+ PartitionInfo,
36
+ PcieInfo,
37
+ ProcessInfo,
38
+ SystemCompilers,
39
+ ThermalState,
40
+ ThermalTracker,
41
+ ThrottleReason,
42
+ UnitSnapshot,
43
+ Utilization,
44
+ )
45
+ from .npu import NPU
46
+ from .providers import (
47
+ AMDGPU,
48
+ AppleGPU,
49
+ AppleNPU,
50
+ IntelGPU,
51
+ IntelNPU,
52
+ NvidiaGPU,
53
+ QualcommGPU,
54
+ QualcommNPU,
55
+ )
56
+ from .unit import CPU, Unit
57
+ from .visual import MachineView
58
+
59
+ NAME = __name__ # the package folder is the single source of the name
60
+
61
+ __all__ = [
62
+ "NAME",
63
+ "CPU",
64
+ "GPU",
65
+ "NPU",
66
+ "AMDGPU",
67
+ "AppleGPU",
68
+ "AppleNPU",
69
+ "Board",
70
+ "Clock",
71
+ "ClockInfo",
72
+ "CmakeBuildType",
73
+ "CompilerInfo",
74
+ "CompilerKind",
75
+ "ComputeCapability",
76
+ "CpuSnapshot",
77
+ "CudaPythonInfo",
78
+ "CudaPythonVariant",
79
+ "DiskKind",
80
+ "DriveInfo",
81
+ "EnergyInterval",
82
+ "EnergyReading",
83
+ "Environment",
84
+ "GPUSnapshot",
85
+ "Host",
86
+ "HostDisk",
87
+ "HostMemory",
88
+ "IntelGPU",
89
+ "IntelNPU",
90
+ "Machine",
91
+ "MachineSnapshot",
92
+ "MachineView",
93
+ "MemInfo",
94
+ "MemoryCard",
95
+ "MemoryUsage",
96
+ "NvidiaGPU",
97
+ "PartitionInfo",
98
+ "PcieInfo",
99
+ "ProcessInfo",
100
+ "QualcommGPU",
101
+ "QualcommNPU",
102
+ "Scheduler",
103
+ "SystemCompilers",
104
+ "SystemFamily",
105
+ "ThermalState",
106
+ "ThermalTracker",
107
+ "ThrottleReason",
108
+ "Unit",
109
+ "UnitKind",
110
+ "UnitSnapshot",
111
+ "Utilization",
112
+ "Vendor",
113
+ ]
mainboard/__main__.py ADDED
@@ -0,0 +1,2 @@
1
+ if __name__ == "__main__":
2
+ __import__(f"{__package__}.cli", fromlist=["main"]).main()
@@ -0,0 +1,54 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="1280" height="420" viewBox="0 0 1280 420" role="img" aria-labelledby="title desc">
2
+ <title id="title">Maquina</title>
3
+ <desc id="desc">CPU, GPU, and NPU hardware topology for Python.</desc>
4
+ <defs>
5
+ <linearGradient id="bg" x1="0" x2="1" y1="0" y2="1">
6
+ <stop offset="0" stop-color="#020617"/>
7
+ <stop offset="0.46" stop-color="#0f172a"/>
8
+ <stop offset="1" stop-color="#1e3a8a"/>
9
+ </linearGradient>
10
+ <linearGradient id="unit" x1="0" x2="1" y1="0" y2="1">
11
+ <stop offset="0" stop-color="#38bdf8"/>
12
+ <stop offset="1" stop-color="#2563eb"/>
13
+ </linearGradient>
14
+ <filter id="glow" x="-50%" y="-50%" width="200%" height="200%">
15
+ <feGaussianBlur stdDeviation="10" result="blur"/>
16
+ <feMerge>
17
+ <feMergeNode in="blur"/>
18
+ <feMergeNode in="SourceGraphic"/>
19
+ </feMerge>
20
+ </filter>
21
+ </defs>
22
+
23
+ <rect width="1280" height="420" rx="42" fill="url(#bg)"/>
24
+ <circle cx="1030" cy="70" r="210" fill="#2563eb" opacity="0.18"/>
25
+ <circle cx="230" cy="380" r="260" fill="#38bdf8" opacity="0.10"/>
26
+ <path d="M0 310 C170 245 270 370 430 310 C610 242 725 160 920 220 C1070 268 1120 160 1280 185 L1280 420 L0 420 Z" fill="#0ea5e9" opacity="0.08"/>
27
+
28
+ <g transform="translate(760 92)" filter="url(#glow)">
29
+ <rect x="0" y="0" width="360" height="220" rx="28" fill="#020617" stroke="#38bdf8" stroke-opacity="0.42" stroke-width="2"/>
30
+ <rect x="38" y="44" width="82" height="58" rx="16" fill="url(#unit)"/>
31
+ <rect x="139" y="44" width="82" height="58" rx="16" fill="url(#unit)" opacity="0.82"/>
32
+ <rect x="240" y="44" width="82" height="58" rx="16" fill="url(#unit)" opacity="0.66"/>
33
+ <rect x="38" y="128" width="284" height="34" rx="17" fill="#0f172a" stroke="#60a5fa" stroke-opacity="0.32"/>
34
+ <path d="M72 198 H288" stroke="#38bdf8" stroke-width="8" stroke-linecap="round" opacity="0.55"/>
35
+ <path d="M106 18 V-18 M180 18 V-18 M254 18 V-18 M106 238 V202 M180 238 V202 M254 238 V202" stroke="#38bdf8" stroke-width="8" stroke-linecap="round" opacity="0.34"/>
36
+ <text x="79" y="81" text-anchor="middle" font-family="Inter, ui-sans-serif, system-ui, sans-serif" font-size="22" font-weight="800" fill="#e0f2fe">CPU</text>
37
+ <text x="180" y="81" text-anchor="middle" font-family="Inter, ui-sans-serif, system-ui, sans-serif" font-size="22" font-weight="800" fill="#e0f2fe">GPU</text>
38
+ <text x="281" y="81" text-anchor="middle" font-family="Inter, ui-sans-serif, system-ui, sans-serif" font-size="22" font-weight="800" fill="#e0f2fe">NPU</text>
39
+ </g>
40
+
41
+ <g transform="translate(96 92)">
42
+ <text x="0" y="0" font-family="Inter, ui-sans-serif, system-ui, sans-serif" font-size="86" font-weight="900" fill="#f8fafc" letter-spacing="-3">Maquina</text>
43
+ <text x="3" y="58" font-family="Inter, ui-sans-serif, system-ui, sans-serif" font-size="28" font-weight="600" fill="#bfdbfe">Hardware topology for heterogeneous Python systems.</text>
44
+ <text x="3" y="112" font-family="JetBrains Mono, ui-monospace, SFMono-Regular, Menlo, monospace" font-size="22" fill="#93c5fd">from maquina import Machine</text>
45
+ <g transform="translate(3 154)">
46
+ <rect x="0" y="0" width="88" height="38" rx="19" fill="#0f172a" stroke="#60a5fa" stroke-opacity="0.35"/>
47
+ <rect x="104" y="0" width="88" height="38" rx="19" fill="#0f172a" stroke="#60a5fa" stroke-opacity="0.35"/>
48
+ <rect x="208" y="0" width="88" height="38" rx="19" fill="#0f172a" stroke="#60a5fa" stroke-opacity="0.35"/>
49
+ <text x="44" y="25" text-anchor="middle" font-family="Inter, ui-sans-serif, system-ui, sans-serif" font-size="16" font-weight="800" fill="#dbeafe">CPU</text>
50
+ <text x="148" y="25" text-anchor="middle" font-family="Inter, ui-sans-serif, system-ui, sans-serif" font-size="16" font-weight="800" fill="#dbeafe">GPU</text>
51
+ <text x="252" y="25" text-anchor="middle" font-family="Inter, ui-sans-serif, system-ui, sans-serif" font-size="16" font-weight="800" fill="#dbeafe">NPU</text>
52
+ </g>
53
+ </g>
54
+ </svg>
mainboard/cli.py ADDED
@@ -0,0 +1,24 @@
1
+ from __future__ import annotations
2
+
3
+ from cyclopts import App
4
+
5
+ from . import NAME
6
+ from .machine import Machine
7
+ from .visual import MachineView
8
+
9
+ app = App(name=NAME, help="Inspect CPU, GPU, and NPU hardware topology.")
10
+
11
+
12
+ @app.default
13
+ def show(color: bool = True) -> None:
14
+ """Render a Rich terminal view of the current machine."""
15
+ MachineView(Machine()).print(color=color)
16
+
17
+
18
+ def main() -> None:
19
+ """Run the Mainboard command-line interface."""
20
+ app()
21
+
22
+
23
+ if __name__ == "__main__":
24
+ main()
mainboard/enums.py ADDED
@@ -0,0 +1,78 @@
1
+ from __future__ import annotations
2
+
3
+ from enum import StrEnum
4
+
5
+
6
+ class CompilerKind(StrEnum):
7
+ """Normalized host compiler kind."""
8
+
9
+ CLANG_GRACE = "clang-grace"
10
+ GCC = "gcc"
11
+ CLANG = "clang"
12
+ NVCC = "nvcc"
13
+ UNKNOWN = "unknown"
14
+
15
+
16
+ class SystemFamily(StrEnum):
17
+ """Machine-family grouping used to guide build choices."""
18
+
19
+ GRACE_HOPPER = "grace-hopper"
20
+ GRACE_BLACKWELL = "grace-blackwell"
21
+ HOPPER = "hopper"
22
+ BLACKWELL = "blackwell"
23
+ GENERIC = "generic"
24
+
25
+
26
+ class CudaPythonVariant(StrEnum):
27
+ """CUDA Python stack family required on the current system."""
28
+
29
+ CU12 = "cu12"
30
+ CU13 = "cu13"
31
+
32
+
33
+ class CmakeBuildType(StrEnum):
34
+ """CMake build type string."""
35
+
36
+ RELEASE = "Release"
37
+
38
+
39
+ class DiskKind(StrEnum):
40
+ """Drive technology for a mounted disk or partition."""
41
+
42
+ NVME = "nvme"
43
+ SSD = "ssd"
44
+ HDD = "hdd"
45
+ RAMDISK = "ramdisk"
46
+ UNKNOWN = "unknown"
47
+
48
+
49
+ class UnitKind(StrEnum):
50
+ """Schedulable execution-resource category."""
51
+
52
+ CPU = "cpu"
53
+ GPU = "gpu"
54
+ NPU = "npu"
55
+ DSP = "dsp"
56
+ MEDIA = "media"
57
+ UNKNOWN = "unknown"
58
+
59
+
60
+ class Vendor(StrEnum):
61
+ """Normalized hardware vendor."""
62
+
63
+ ARM = "arm"
64
+ APPLE = "apple"
65
+ NVIDIA = "nvidia"
66
+ QUALCOMM = "qualcomm"
67
+ AMD = "amd"
68
+ INTEL = "intel"
69
+ UNKNOWN = "unknown"
70
+
71
+
72
+ class Scheduler(StrEnum):
73
+ """Job scheduler detected on the host's PATH."""
74
+
75
+ SLURM = "slurm"
76
+ PBS = "pbs"
77
+ PUEUE = "pueue"
78
+ NONE = "none"
mainboard/gpu.py ADDED
@@ -0,0 +1,178 @@
1
+ from __future__ import annotations
2
+
3
+ from functools import cached_property
4
+ from typing import TYPE_CHECKING, ClassVar
5
+
6
+ from pydantic import Field
7
+
8
+ from .enums import UnitKind, Vendor
9
+ from .models.clock import Clock
10
+ from .models.clock_info import ClockInfo
11
+ from .models.energy_reading import EnergyReading
12
+ from .models.gpu_snapshot import GPUSnapshot
13
+ from .models.mem_info import MemInfo
14
+ from .models.memory_usage import MemoryUsage
15
+ from .models.pcie_info import PcieInfo
16
+ from .models.thermal_state import ThermalState
17
+ from .models.utilization import Utilization
18
+ from .unit import Unit
19
+
20
+ if TYPE_CHECKING:
21
+ from .models.process_info import ProcessInfo
22
+
23
+
24
+ class GPU(Unit):
25
+ """GPU with telemetry and legacy profiling sensor accessors."""
26
+
27
+ providers: ClassVar[tuple[type[GPU], ...]] = ()
28
+
29
+ index: int = 0
30
+ kind: ClassVar[UnitKind] = UnitKind.GPU
31
+ vendor: Vendor = Field(default=Vendor.UNKNOWN)
32
+ backend: str = "none"
33
+
34
+ @classmethod
35
+ def register_providers(cls, *providers: type[GPU]) -> None:
36
+ """Register concrete providers used by `GPU.all`."""
37
+ cls.providers = providers
38
+
39
+ @classmethod
40
+ def all(cls) -> tuple[GPU, ...]:
41
+ """Return GPUs visible to supported providers."""
42
+ return tuple(gpu for provider in cls.providers for gpu in provider.all())
43
+
44
+ @cached_property
45
+ def name(self) -> str:
46
+ """Human-readable GPU name."""
47
+ return "unknown"
48
+
49
+ @cached_property
50
+ def uuid(self) -> str:
51
+ """Stable GPU identifier when the provider exposes one."""
52
+ return ""
53
+
54
+ @cached_property
55
+ def architecture(self) -> str:
56
+ """Human-readable architecture or generation name."""
57
+ return "unknown"
58
+
59
+ @cached_property
60
+ def total_memory_bytes(self) -> int:
61
+ """Total memory managed by this accelerator."""
62
+ return 0
63
+
64
+ @cached_property
65
+ def peak_bandwidth_gbs(self) -> float:
66
+ """Theoretical peak memory bandwidth in GB/s when known."""
67
+ return 0.0
68
+
69
+ @cached_property
70
+ def driver_version(self) -> tuple[int, int] | None:
71
+ """Driver or runtime version as `(major, minor)` when known."""
72
+ return None
73
+
74
+ @property
75
+ def mem_info(self) -> MemInfo:
76
+ """Current accelerator memory state."""
77
+ return MemInfo(total_bytes=self.total_memory_bytes)
78
+
79
+ @property
80
+ def memory_readings(self) -> tuple[MemoryUsage, ...]:
81
+ """Memory regions visible to this GPU."""
82
+ mem = self.mem_info
83
+ return (
84
+ MemoryUsage(
85
+ scope="device",
86
+ total_bytes=mem.total_bytes,
87
+ used_bytes=mem.used_bytes,
88
+ free_bytes=mem.free_bytes,
89
+ source=self.backend,
90
+ supported=mem.total_bytes > 0,
91
+ ),
92
+ )
93
+
94
+ @property
95
+ def clocks(self) -> ClockInfo:
96
+ """Current compute and memory clocks."""
97
+ return ClockInfo()
98
+
99
+ @property
100
+ def clock_readings(self) -> tuple[Clock, ...]:
101
+ """Clock readings grouped by hardware domain."""
102
+ clocks = self.clocks
103
+ return (
104
+ Clock(
105
+ domain="gpu_compute",
106
+ current_mhz=clocks.sm_mhz or None,
107
+ source=self.backend,
108
+ supported=clocks.sm_mhz > 0,
109
+ ),
110
+ Clock(
111
+ domain="memory",
112
+ current_mhz=clocks.memory_mhz or None,
113
+ source=self.backend,
114
+ supported=clocks.memory_mhz > 0,
115
+ ),
116
+ )
117
+
118
+ @property
119
+ def utilization(self) -> Utilization:
120
+ """Current compute and memory-controller utilization."""
121
+ return Utilization()
122
+
123
+ @property
124
+ def thermal(self) -> ThermalState:
125
+ """Current thermal state."""
126
+ return ThermalState()
127
+
128
+ @property
129
+ def energy(self) -> EnergyReading:
130
+ """Current power and cumulative energy reading."""
131
+ return EnergyReading()
132
+
133
+ @property
134
+ def pcie(self) -> PcieInfo:
135
+ """Current host interconnect throughput."""
136
+ return PcieInfo()
137
+
138
+ @property
139
+ def fan_speed_pct(self) -> int:
140
+ """Fan speed percentage, or zero when unavailable."""
141
+ return 0
142
+
143
+ @property
144
+ def temperature_c(self) -> int:
145
+ """Current GPU temperature in Celsius."""
146
+ return self.thermal.temperature_c
147
+
148
+ @property
149
+ def gpu_util_pct(self) -> int:
150
+ """Current compute utilization percentage."""
151
+ return self.utilization.gpu_pct
152
+
153
+ @property
154
+ def processes(self) -> list[ProcessInfo]:
155
+ """Processes using this accelerator."""
156
+ return []
157
+
158
+ def snapshot(self, name: str = "") -> GPUSnapshot:
159
+ """Point-in-time reading of all common sensor properties.
160
+
161
+ name: profiling region label to embed in the snapshot.
162
+ """
163
+ return GPUSnapshot(
164
+ name=name,
165
+ unit_name=self.name,
166
+ kind=self.kind,
167
+ vendor=self.vendor,
168
+ memory=self.memory_readings,
169
+ clocks=self.clock_readings,
170
+ utilization=self.utilization,
171
+ energy=self.energy,
172
+ thermal=self.thermal,
173
+ gpu_memory=self.mem_info,
174
+ gpu_clocks=self.clocks,
175
+ pcie=self.pcie,
176
+ fan_speed_pct=self.fan_speed_pct,
177
+ processes=self.processes,
178
+ )
mainboard/host.py ADDED
@@ -0,0 +1,133 @@
1
+ from __future__ import annotations
2
+
3
+ import platform
4
+ import re
5
+ from collections import Counter
6
+ from functools import cached_property
7
+
8
+ import psutil
9
+
10
+ from . import shell
11
+ from .enums import Vendor
12
+ from .models.host_disk import HostDisk
13
+ from .models.host_memory import HostMemory
14
+
15
+ _CPU_MODEL_RE = re.compile(r"^(?:model name|hardware)\s*:\s*(.+)$", re.IGNORECASE | re.MULTILINE)
16
+ _CPU_IMPLEMENTER_RE = re.compile(r"^CPU implementer\s*:\s*(.+)$", re.MULTILINE)
17
+ _CPU_PART_RE = re.compile(r"^CPU part\s*:\s*(.+)$", re.MULTILINE)
18
+ _ARM_IMPLEMENTERS = {
19
+ "0x41": "Arm",
20
+ "0x4e": "NVIDIA",
21
+ "0x51": "Qualcomm",
22
+ "0x61": "Apple",
23
+ "0xc0": "Ampere",
24
+ }
25
+ _VENDOR_BY_IMPLEMENTER = {
26
+ "0x41": Vendor.ARM,
27
+ "0x4e": Vendor.NVIDIA,
28
+ "0x51": Vendor.QUALCOMM,
29
+ "0x61": Vendor.APPLE,
30
+ }
31
+ _ARM_PARTS = {
32
+ ("0x41", "0xd85"): "Cortex-X925",
33
+ ("0x41", "0xd87"): "Cortex-A725",
34
+ }
35
+
36
+
37
+ class Host:
38
+ """Host CPU, memory, and disk identity for the current machine."""
39
+
40
+ @cached_property
41
+ def arch(self) -> str:
42
+ """CPU architecture string, e.g. `x86_64` or `aarch64`."""
43
+ return platform.machine().lower()
44
+
45
+ @cached_property
46
+ def cpu(self) -> str:
47
+ """CPU model name from `platform` or `/proc/cpuinfo`."""
48
+ if platform.system() == "Darwin" and (brand := shell.sysctl("machdep.cpu.brand_string")):
49
+ return brand
50
+ text = self.cpuinfo_text
51
+ if not text:
52
+ return platform.processor() or "unknown"
53
+ if m := _CPU_MODEL_RE.search(text):
54
+ return m.group(1).strip()
55
+ if name := self.arm_cpu_name:
56
+ return name
57
+ return platform.processor() or "unknown"
58
+
59
+ @cached_property
60
+ def cpu_vendor(self) -> Vendor:
61
+ """CPU core vendor inferred from OS identity records."""
62
+ if platform.system() == "Darwin":
63
+ return Vendor.APPLE
64
+ for record in self.cpuinfo_records:
65
+ implementer = record.get("CPU implementer", "").lower()
66
+ if vendor := _VENDOR_BY_IMPLEMENTER.get(implementer):
67
+ return vendor
68
+ return Vendor.UNKNOWN
69
+
70
+ @cached_property
71
+ def cpuinfo_text(self) -> str:
72
+ """Raw Linux `/proc/cpuinfo`, or an empty string when unavailable."""
73
+ return shell.read("/proc/cpuinfo")
74
+
75
+ @cached_property
76
+ def cpuinfo_records(self) -> tuple[dict[str, str], ...]:
77
+ """Parsed Linux `/proc/cpuinfo` records."""
78
+ return tuple(
79
+ {
80
+ key.strip(): value.strip()
81
+ for line in block.splitlines()
82
+ if ":" in line
83
+ for key, value in [line.split(":", 1)]
84
+ }
85
+ for block in self.cpuinfo_text.strip().split("\n\n")
86
+ if block.strip()
87
+ )
88
+
89
+ @cached_property
90
+ def arm_cpu_name(self) -> str:
91
+ """Human-readable ARM CPU core mix from MIDR implementer and part IDs."""
92
+ pairs = [
93
+ (record.get("CPU implementer", "").lower(), record.get("CPU part", "").lower())
94
+ for record in self.cpuinfo_records
95
+ if record.get("CPU implementer") and record.get("CPU part")
96
+ ]
97
+ counts = Counter(pairs)
98
+ names = []
99
+ for implementer, part in dict.fromkeys(pairs):
100
+ vendor = _ARM_IMPLEMENTERS.get(implementer, f"implementer {implementer}")
101
+ model = _ARM_PARTS.get((implementer, part), f"part {part}")
102
+ count = counts[(implementer, part)]
103
+ names.append(f"{count}x {vendor} {model}" if count > 1 else f"{vendor} {model}")
104
+ return " + ".join(names)
105
+
106
+ @cached_property
107
+ def logical_cpus(self) -> int:
108
+ """Number of logical CPU threads (including hyperthreading)."""
109
+ return psutil.cpu_count(logical=True) or 0
110
+
111
+ @cached_property
112
+ def physical_cpus(self) -> int:
113
+ """Number of physical CPU cores."""
114
+ return psutil.cpu_count(logical=False) or 0
115
+
116
+ @property
117
+ def cpu_freq_mhz(self) -> float | None:
118
+ """Current CPU frequency in MHz; `None` if the platform cannot report it."""
119
+ try:
120
+ freq = psutil.cpu_freq()
121
+ except (AttributeError, NotImplementedError, OSError):
122
+ return None
123
+ return freq.current if freq else None
124
+
125
+ @property
126
+ def memory(self) -> HostMemory:
127
+ """Live system memory snapshot; lazily samples psutil on first field access."""
128
+ return HostMemory()
129
+
130
+ @cached_property
131
+ def disk(self) -> HostDisk:
132
+ """All physical drives with their mounted partitions."""
133
+ return HostDisk()