fbuild 1.2.8__py3-none-any.whl → 1.2.15__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.
- fbuild/__init__.py +5 -1
- fbuild/build/configurable_compiler.py +49 -6
- fbuild/build/configurable_linker.py +14 -9
- fbuild/build/orchestrator_esp32.py +6 -3
- fbuild/build/orchestrator_rp2040.py +6 -2
- fbuild/cli.py +300 -5
- fbuild/config/ini_parser.py +13 -1
- fbuild/daemon/__init__.py +11 -0
- fbuild/daemon/async_client.py +5 -4
- fbuild/daemon/async_client_lib.py +1543 -0
- fbuild/daemon/async_protocol.py +825 -0
- fbuild/daemon/async_server.py +2100 -0
- fbuild/daemon/client.py +425 -13
- fbuild/daemon/configuration_lock.py +13 -13
- fbuild/daemon/connection.py +508 -0
- fbuild/daemon/connection_registry.py +579 -0
- fbuild/daemon/daemon.py +517 -164
- fbuild/daemon/daemon_context.py +72 -1
- fbuild/daemon/device_discovery.py +477 -0
- fbuild/daemon/device_manager.py +821 -0
- fbuild/daemon/error_collector.py +263 -263
- fbuild/daemon/file_cache.py +332 -332
- fbuild/daemon/firmware_ledger.py +46 -123
- fbuild/daemon/lock_manager.py +508 -508
- fbuild/daemon/messages.py +431 -0
- fbuild/daemon/operation_registry.py +288 -288
- fbuild/daemon/processors/build_processor.py +34 -1
- fbuild/daemon/processors/deploy_processor.py +1 -3
- fbuild/daemon/processors/locking_processor.py +7 -7
- fbuild/daemon/request_processor.py +457 -457
- fbuild/daemon/shared_serial.py +7 -7
- fbuild/daemon/status_manager.py +238 -238
- fbuild/daemon/subprocess_manager.py +316 -316
- fbuild/deploy/docker_utils.py +182 -2
- fbuild/deploy/monitor.py +1 -1
- fbuild/deploy/qemu_runner.py +71 -13
- fbuild/ledger/board_ledger.py +46 -122
- fbuild/output.py +238 -2
- fbuild/packages/library_compiler.py +15 -5
- fbuild/packages/library_manager.py +12 -6
- fbuild-1.2.15.dist-info/METADATA +569 -0
- {fbuild-1.2.8.dist-info → fbuild-1.2.15.dist-info}/RECORD +46 -39
- fbuild-1.2.8.dist-info/METADATA +0 -468
- {fbuild-1.2.8.dist-info → fbuild-1.2.15.dist-info}/WHEEL +0 -0
- {fbuild-1.2.8.dist-info → fbuild-1.2.15.dist-info}/entry_points.txt +0 -0
- {fbuild-1.2.8.dist-info → fbuild-1.2.15.dist-info}/licenses/LICENSE +0 -0
- {fbuild-1.2.8.dist-info → fbuild-1.2.15.dist-info}/top_level.txt +0 -0
fbuild/daemon/messages.py
CHANGED
|
@@ -955,3 +955,434 @@ class ClientResponse:
|
|
|
955
955
|
total_clients=data.get("total_clients", 0),
|
|
956
956
|
timestamp=data.get("timestamp", time.time()),
|
|
957
957
|
)
|
|
958
|
+
|
|
959
|
+
|
|
960
|
+
@dataclass
|
|
961
|
+
class DaemonIdentity:
|
|
962
|
+
"""Identity information for a daemon instance.
|
|
963
|
+
|
|
964
|
+
This is returned when clients query daemon identity and is used
|
|
965
|
+
to distinguish between different daemon instances (e.g., dev vs prod).
|
|
966
|
+
|
|
967
|
+
Attributes:
|
|
968
|
+
name: Daemon name (e.g., "fbuild_daemon" or "fbuild_daemon_dev")
|
|
969
|
+
version: Daemon version string
|
|
970
|
+
started_at: Unix timestamp when daemon started
|
|
971
|
+
spawned_by_pid: PID of client that originally started the daemon
|
|
972
|
+
spawned_by_cwd: Working directory of client that started daemon
|
|
973
|
+
is_dev: Whether this is a development mode daemon
|
|
974
|
+
pid: Process ID of the daemon itself
|
|
975
|
+
"""
|
|
976
|
+
|
|
977
|
+
name: str
|
|
978
|
+
version: str
|
|
979
|
+
started_at: float
|
|
980
|
+
spawned_by_pid: int
|
|
981
|
+
spawned_by_cwd: str
|
|
982
|
+
is_dev: bool
|
|
983
|
+
pid: int
|
|
984
|
+
|
|
985
|
+
def to_dict(self) -> dict[str, Any]:
|
|
986
|
+
"""Convert to dictionary for JSON serialization."""
|
|
987
|
+
return asdict(self)
|
|
988
|
+
|
|
989
|
+
@classmethod
|
|
990
|
+
def from_dict(cls, data: dict[str, Any]) -> "DaemonIdentity":
|
|
991
|
+
"""Create DaemonIdentity from dictionary."""
|
|
992
|
+
return cls(
|
|
993
|
+
name=data["name"],
|
|
994
|
+
version=data["version"],
|
|
995
|
+
started_at=data["started_at"],
|
|
996
|
+
spawned_by_pid=data["spawned_by_pid"],
|
|
997
|
+
spawned_by_cwd=data["spawned_by_cwd"],
|
|
998
|
+
is_dev=data["is_dev"],
|
|
999
|
+
pid=data["pid"],
|
|
1000
|
+
)
|
|
1001
|
+
|
|
1002
|
+
|
|
1003
|
+
@dataclass
|
|
1004
|
+
class DaemonIdentityRequest:
|
|
1005
|
+
"""Client -> Daemon: Request daemon identity information.
|
|
1006
|
+
|
|
1007
|
+
Attributes:
|
|
1008
|
+
timestamp: Unix timestamp when request was created
|
|
1009
|
+
"""
|
|
1010
|
+
|
|
1011
|
+
timestamp: float = field(default_factory=time.time)
|
|
1012
|
+
|
|
1013
|
+
def to_dict(self) -> dict[str, Any]:
|
|
1014
|
+
"""Convert to dictionary for JSON serialization."""
|
|
1015
|
+
return asdict(self)
|
|
1016
|
+
|
|
1017
|
+
@classmethod
|
|
1018
|
+
def from_dict(cls, data: dict[str, Any]) -> "DaemonIdentityRequest":
|
|
1019
|
+
"""Create DaemonIdentityRequest from dictionary."""
|
|
1020
|
+
return cls(
|
|
1021
|
+
timestamp=data.get("timestamp", time.time()),
|
|
1022
|
+
)
|
|
1023
|
+
|
|
1024
|
+
|
|
1025
|
+
@dataclass
|
|
1026
|
+
class DaemonIdentityResponse:
|
|
1027
|
+
"""Daemon -> Client: Response with daemon identity.
|
|
1028
|
+
|
|
1029
|
+
Attributes:
|
|
1030
|
+
success: Whether the request succeeded
|
|
1031
|
+
message: Human-readable message
|
|
1032
|
+
identity: The daemon identity (if success)
|
|
1033
|
+
timestamp: Unix timestamp of response
|
|
1034
|
+
"""
|
|
1035
|
+
|
|
1036
|
+
success: bool
|
|
1037
|
+
message: str
|
|
1038
|
+
identity: DaemonIdentity | None = None
|
|
1039
|
+
timestamp: float = field(default_factory=time.time)
|
|
1040
|
+
|
|
1041
|
+
def to_dict(self) -> dict[str, Any]:
|
|
1042
|
+
"""Convert to dictionary for JSON serialization."""
|
|
1043
|
+
result = asdict(self)
|
|
1044
|
+
if self.identity:
|
|
1045
|
+
result["identity"] = self.identity.to_dict()
|
|
1046
|
+
return result
|
|
1047
|
+
|
|
1048
|
+
@classmethod
|
|
1049
|
+
def from_dict(cls, data: dict[str, Any]) -> "DaemonIdentityResponse":
|
|
1050
|
+
"""Create DaemonIdentityResponse from dictionary."""
|
|
1051
|
+
identity = None
|
|
1052
|
+
if data.get("identity"):
|
|
1053
|
+
identity = DaemonIdentity.from_dict(data["identity"])
|
|
1054
|
+
return cls(
|
|
1055
|
+
success=data["success"],
|
|
1056
|
+
message=data["message"],
|
|
1057
|
+
identity=identity,
|
|
1058
|
+
timestamp=data.get("timestamp", time.time()),
|
|
1059
|
+
)
|
|
1060
|
+
|
|
1061
|
+
|
|
1062
|
+
# =============================================================================
|
|
1063
|
+
# Device Management Messages (Resource Manager Expansion)
|
|
1064
|
+
# =============================================================================
|
|
1065
|
+
|
|
1066
|
+
|
|
1067
|
+
class DeviceLeaseType(Enum):
|
|
1068
|
+
"""Type of device lease."""
|
|
1069
|
+
|
|
1070
|
+
EXCLUSIVE = "exclusive" # For deploy/flash/reset (single holder)
|
|
1071
|
+
MONITOR = "monitor" # For read-only monitoring (multiple holders)
|
|
1072
|
+
|
|
1073
|
+
|
|
1074
|
+
@dataclass
|
|
1075
|
+
class DeviceLeaseRequest:
|
|
1076
|
+
"""Client -> Daemon: Request to acquire a device lease.
|
|
1077
|
+
|
|
1078
|
+
Used to acquire either exclusive access (for deploy/flash) or
|
|
1079
|
+
monitor access (for read-only serial monitoring).
|
|
1080
|
+
|
|
1081
|
+
Attributes:
|
|
1082
|
+
device_id: The stable device ID to lease
|
|
1083
|
+
lease_type: Type of lease ("exclusive" or "monitor")
|
|
1084
|
+
description: Human-readable description of the operation
|
|
1085
|
+
allows_monitors: For exclusive leases, whether monitors are allowed (default True)
|
|
1086
|
+
timeout: Maximum time in seconds to wait for the lease
|
|
1087
|
+
timestamp: Unix timestamp when request was created
|
|
1088
|
+
"""
|
|
1089
|
+
|
|
1090
|
+
device_id: str
|
|
1091
|
+
lease_type: str # "exclusive" or "monitor"
|
|
1092
|
+
description: str = ""
|
|
1093
|
+
allows_monitors: bool = True
|
|
1094
|
+
timeout: float = 300.0
|
|
1095
|
+
timestamp: float = field(default_factory=time.time)
|
|
1096
|
+
|
|
1097
|
+
def to_dict(self) -> dict[str, Any]:
|
|
1098
|
+
"""Convert to dictionary for JSON serialization."""
|
|
1099
|
+
return asdict(self)
|
|
1100
|
+
|
|
1101
|
+
@classmethod
|
|
1102
|
+
def from_dict(cls, data: dict[str, Any]) -> "DeviceLeaseRequest":
|
|
1103
|
+
"""Create DeviceLeaseRequest from dictionary."""
|
|
1104
|
+
return cls(
|
|
1105
|
+
device_id=data["device_id"],
|
|
1106
|
+
lease_type=data["lease_type"],
|
|
1107
|
+
description=data.get("description", ""),
|
|
1108
|
+
allows_monitors=data.get("allows_monitors", True),
|
|
1109
|
+
timeout=data.get("timeout", 300.0),
|
|
1110
|
+
timestamp=data.get("timestamp", time.time()),
|
|
1111
|
+
)
|
|
1112
|
+
|
|
1113
|
+
|
|
1114
|
+
@dataclass
|
|
1115
|
+
class DeviceReleaseRequest:
|
|
1116
|
+
"""Client -> Daemon: Request to release a device lease.
|
|
1117
|
+
|
|
1118
|
+
Attributes:
|
|
1119
|
+
lease_id: The lease ID to release
|
|
1120
|
+
timestamp: Unix timestamp when request was created
|
|
1121
|
+
"""
|
|
1122
|
+
|
|
1123
|
+
lease_id: str
|
|
1124
|
+
timestamp: float = field(default_factory=time.time)
|
|
1125
|
+
|
|
1126
|
+
def to_dict(self) -> dict[str, Any]:
|
|
1127
|
+
"""Convert to dictionary for JSON serialization."""
|
|
1128
|
+
return asdict(self)
|
|
1129
|
+
|
|
1130
|
+
@classmethod
|
|
1131
|
+
def from_dict(cls, data: dict[str, Any]) -> "DeviceReleaseRequest":
|
|
1132
|
+
"""Create DeviceReleaseRequest from dictionary."""
|
|
1133
|
+
return cls(
|
|
1134
|
+
lease_id=data["lease_id"],
|
|
1135
|
+
timestamp=data.get("timestamp", time.time()),
|
|
1136
|
+
)
|
|
1137
|
+
|
|
1138
|
+
|
|
1139
|
+
@dataclass
|
|
1140
|
+
class DevicePreemptRequest:
|
|
1141
|
+
"""Client -> Daemon: Request to preempt a device's exclusive holder.
|
|
1142
|
+
|
|
1143
|
+
Forcibly takes the exclusive lease from the current holder.
|
|
1144
|
+
The reason is REQUIRED and must not be empty.
|
|
1145
|
+
|
|
1146
|
+
Attributes:
|
|
1147
|
+
device_id: The device to preempt
|
|
1148
|
+
reason: REQUIRED reason for preemption (must not be empty)
|
|
1149
|
+
timestamp: Unix timestamp when request was created
|
|
1150
|
+
"""
|
|
1151
|
+
|
|
1152
|
+
device_id: str
|
|
1153
|
+
reason: str # REQUIRED - must not be empty
|
|
1154
|
+
timestamp: float = field(default_factory=time.time)
|
|
1155
|
+
|
|
1156
|
+
def to_dict(self) -> dict[str, Any]:
|
|
1157
|
+
"""Convert to dictionary for JSON serialization."""
|
|
1158
|
+
return asdict(self)
|
|
1159
|
+
|
|
1160
|
+
@classmethod
|
|
1161
|
+
def from_dict(cls, data: dict[str, Any]) -> "DevicePreemptRequest":
|
|
1162
|
+
"""Create DevicePreemptRequest from dictionary."""
|
|
1163
|
+
return cls(
|
|
1164
|
+
device_id=data["device_id"],
|
|
1165
|
+
reason=data["reason"],
|
|
1166
|
+
timestamp=data.get("timestamp", time.time()),
|
|
1167
|
+
)
|
|
1168
|
+
|
|
1169
|
+
|
|
1170
|
+
@dataclass
|
|
1171
|
+
class DeviceListRequest:
|
|
1172
|
+
"""Client -> Daemon: Request to list all devices.
|
|
1173
|
+
|
|
1174
|
+
Attributes:
|
|
1175
|
+
include_disconnected: Whether to include disconnected devices
|
|
1176
|
+
refresh: Whether to refresh device discovery before listing
|
|
1177
|
+
timestamp: Unix timestamp when request was created
|
|
1178
|
+
"""
|
|
1179
|
+
|
|
1180
|
+
include_disconnected: bool = False
|
|
1181
|
+
refresh: bool = False
|
|
1182
|
+
timestamp: float = field(default_factory=time.time)
|
|
1183
|
+
|
|
1184
|
+
def to_dict(self) -> dict[str, Any]:
|
|
1185
|
+
"""Convert to dictionary for JSON serialization."""
|
|
1186
|
+
return asdict(self)
|
|
1187
|
+
|
|
1188
|
+
@classmethod
|
|
1189
|
+
def from_dict(cls, data: dict[str, Any]) -> "DeviceListRequest":
|
|
1190
|
+
"""Create DeviceListRequest from dictionary."""
|
|
1191
|
+
return cls(
|
|
1192
|
+
include_disconnected=data.get("include_disconnected", False),
|
|
1193
|
+
refresh=data.get("refresh", False),
|
|
1194
|
+
timestamp=data.get("timestamp", time.time()),
|
|
1195
|
+
)
|
|
1196
|
+
|
|
1197
|
+
|
|
1198
|
+
@dataclass
|
|
1199
|
+
class DeviceStatusRequest:
|
|
1200
|
+
"""Client -> Daemon: Request for detailed device status.
|
|
1201
|
+
|
|
1202
|
+
Attributes:
|
|
1203
|
+
device_id: The device to get status for
|
|
1204
|
+
timestamp: Unix timestamp when request was created
|
|
1205
|
+
"""
|
|
1206
|
+
|
|
1207
|
+
device_id: str
|
|
1208
|
+
timestamp: float = field(default_factory=time.time)
|
|
1209
|
+
|
|
1210
|
+
def to_dict(self) -> dict[str, Any]:
|
|
1211
|
+
"""Convert to dictionary for JSON serialization."""
|
|
1212
|
+
return asdict(self)
|
|
1213
|
+
|
|
1214
|
+
@classmethod
|
|
1215
|
+
def from_dict(cls, data: dict[str, Any]) -> "DeviceStatusRequest":
|
|
1216
|
+
"""Create DeviceStatusRequest from dictionary."""
|
|
1217
|
+
return cls(
|
|
1218
|
+
device_id=data["device_id"],
|
|
1219
|
+
timestamp=data.get("timestamp", time.time()),
|
|
1220
|
+
)
|
|
1221
|
+
|
|
1222
|
+
|
|
1223
|
+
@dataclass
|
|
1224
|
+
class DeviceLeaseResponse:
|
|
1225
|
+
"""Daemon -> Client: Response to device lease operations.
|
|
1226
|
+
|
|
1227
|
+
Attributes:
|
|
1228
|
+
success: Whether the operation succeeded
|
|
1229
|
+
message: Human-readable status message
|
|
1230
|
+
lease_id: The lease ID (if acquired)
|
|
1231
|
+
device_id: The device ID
|
|
1232
|
+
lease_type: Type of lease acquired
|
|
1233
|
+
allows_monitors: Whether monitors are allowed (for exclusive leases)
|
|
1234
|
+
preempted_client_id: Client ID that was preempted (for preempt operations)
|
|
1235
|
+
timestamp: Unix timestamp of the response
|
|
1236
|
+
"""
|
|
1237
|
+
|
|
1238
|
+
success: bool
|
|
1239
|
+
message: str
|
|
1240
|
+
lease_id: str | None = None
|
|
1241
|
+
device_id: str | None = None
|
|
1242
|
+
lease_type: str | None = None
|
|
1243
|
+
allows_monitors: bool = True
|
|
1244
|
+
preempted_client_id: str | None = None
|
|
1245
|
+
timestamp: float = field(default_factory=time.time)
|
|
1246
|
+
|
|
1247
|
+
def to_dict(self) -> dict[str, Any]:
|
|
1248
|
+
"""Convert to dictionary for JSON serialization."""
|
|
1249
|
+
return asdict(self)
|
|
1250
|
+
|
|
1251
|
+
@classmethod
|
|
1252
|
+
def from_dict(cls, data: dict[str, Any]) -> "DeviceLeaseResponse":
|
|
1253
|
+
"""Create DeviceLeaseResponse from dictionary."""
|
|
1254
|
+
return cls(
|
|
1255
|
+
success=data["success"],
|
|
1256
|
+
message=data["message"],
|
|
1257
|
+
lease_id=data.get("lease_id"),
|
|
1258
|
+
device_id=data.get("device_id"),
|
|
1259
|
+
lease_type=data.get("lease_type"),
|
|
1260
|
+
allows_monitors=data.get("allows_monitors", True),
|
|
1261
|
+
preempted_client_id=data.get("preempted_client_id"),
|
|
1262
|
+
timestamp=data.get("timestamp", time.time()),
|
|
1263
|
+
)
|
|
1264
|
+
|
|
1265
|
+
|
|
1266
|
+
@dataclass
|
|
1267
|
+
class DeviceListResponse:
|
|
1268
|
+
"""Daemon -> Client: Response to device list request.
|
|
1269
|
+
|
|
1270
|
+
Attributes:
|
|
1271
|
+
success: Whether the operation succeeded
|
|
1272
|
+
message: Human-readable status message
|
|
1273
|
+
devices: List of device information dictionaries
|
|
1274
|
+
total_devices: Total number of devices
|
|
1275
|
+
connected_devices: Number of connected devices
|
|
1276
|
+
total_leases: Total number of active leases
|
|
1277
|
+
timestamp: Unix timestamp of the response
|
|
1278
|
+
"""
|
|
1279
|
+
|
|
1280
|
+
success: bool
|
|
1281
|
+
message: str
|
|
1282
|
+
devices: list[dict[str, Any]] = field(default_factory=list)
|
|
1283
|
+
total_devices: int = 0
|
|
1284
|
+
connected_devices: int = 0
|
|
1285
|
+
total_leases: int = 0
|
|
1286
|
+
timestamp: float = field(default_factory=time.time)
|
|
1287
|
+
|
|
1288
|
+
def to_dict(self) -> dict[str, Any]:
|
|
1289
|
+
"""Convert to dictionary for JSON serialization."""
|
|
1290
|
+
return asdict(self)
|
|
1291
|
+
|
|
1292
|
+
@classmethod
|
|
1293
|
+
def from_dict(cls, data: dict[str, Any]) -> "DeviceListResponse":
|
|
1294
|
+
"""Create DeviceListResponse from dictionary."""
|
|
1295
|
+
return cls(
|
|
1296
|
+
success=data["success"],
|
|
1297
|
+
message=data["message"],
|
|
1298
|
+
devices=data.get("devices", []),
|
|
1299
|
+
total_devices=data.get("total_devices", 0),
|
|
1300
|
+
connected_devices=data.get("connected_devices", 0),
|
|
1301
|
+
total_leases=data.get("total_leases", 0),
|
|
1302
|
+
timestamp=data.get("timestamp", time.time()),
|
|
1303
|
+
)
|
|
1304
|
+
|
|
1305
|
+
|
|
1306
|
+
@dataclass
|
|
1307
|
+
class DeviceStatusResponse:
|
|
1308
|
+
"""Daemon -> Client: Response to device status request.
|
|
1309
|
+
|
|
1310
|
+
Attributes:
|
|
1311
|
+
success: Whether the operation succeeded
|
|
1312
|
+
message: Human-readable status message
|
|
1313
|
+
device_id: The device ID
|
|
1314
|
+
exists: Whether the device exists in the inventory
|
|
1315
|
+
is_connected: Whether the device is currently connected
|
|
1316
|
+
device_info: Full device information dictionary
|
|
1317
|
+
exclusive_lease: Current exclusive lease info (if any)
|
|
1318
|
+
monitor_leases: List of monitor lease info dictionaries
|
|
1319
|
+
monitor_count: Number of active monitor leases
|
|
1320
|
+
is_available_for_exclusive: Whether exclusive lease can be acquired
|
|
1321
|
+
timestamp: Unix timestamp of the response
|
|
1322
|
+
"""
|
|
1323
|
+
|
|
1324
|
+
success: bool
|
|
1325
|
+
message: str
|
|
1326
|
+
device_id: str = ""
|
|
1327
|
+
exists: bool = False
|
|
1328
|
+
is_connected: bool = False
|
|
1329
|
+
device_info: dict[str, Any] | None = None
|
|
1330
|
+
exclusive_lease: dict[str, Any] | None = None
|
|
1331
|
+
monitor_leases: list[dict[str, Any]] = field(default_factory=list)
|
|
1332
|
+
monitor_count: int = 0
|
|
1333
|
+
is_available_for_exclusive: bool = False
|
|
1334
|
+
timestamp: float = field(default_factory=time.time)
|
|
1335
|
+
|
|
1336
|
+
def to_dict(self) -> dict[str, Any]:
|
|
1337
|
+
"""Convert to dictionary for JSON serialization."""
|
|
1338
|
+
return asdict(self)
|
|
1339
|
+
|
|
1340
|
+
@classmethod
|
|
1341
|
+
def from_dict(cls, data: dict[str, Any]) -> "DeviceStatusResponse":
|
|
1342
|
+
"""Create DeviceStatusResponse from dictionary."""
|
|
1343
|
+
return cls(
|
|
1344
|
+
success=data["success"],
|
|
1345
|
+
message=data["message"],
|
|
1346
|
+
device_id=data.get("device_id", ""),
|
|
1347
|
+
exists=data.get("exists", False),
|
|
1348
|
+
is_connected=data.get("is_connected", False),
|
|
1349
|
+
device_info=data.get("device_info"),
|
|
1350
|
+
exclusive_lease=data.get("exclusive_lease"),
|
|
1351
|
+
monitor_leases=data.get("monitor_leases", []),
|
|
1352
|
+
monitor_count=data.get("monitor_count", 0),
|
|
1353
|
+
is_available_for_exclusive=data.get("is_available_for_exclusive", False),
|
|
1354
|
+
timestamp=data.get("timestamp", time.time()),
|
|
1355
|
+
)
|
|
1356
|
+
|
|
1357
|
+
|
|
1358
|
+
@dataclass
|
|
1359
|
+
class DevicePreemptNotification:
|
|
1360
|
+
"""Daemon -> Client: Notification that device was preempted.
|
|
1361
|
+
|
|
1362
|
+
Sent to the client that was preempted from a device.
|
|
1363
|
+
|
|
1364
|
+
Attributes:
|
|
1365
|
+
device_id: The device that was preempted
|
|
1366
|
+
preempted_by: Client ID of the requester
|
|
1367
|
+
reason: Reason for preemption (required)
|
|
1368
|
+
timestamp: Unix timestamp when preemption occurred
|
|
1369
|
+
"""
|
|
1370
|
+
|
|
1371
|
+
device_id: str
|
|
1372
|
+
preempted_by: str
|
|
1373
|
+
reason: str
|
|
1374
|
+
timestamp: float = field(default_factory=time.time)
|
|
1375
|
+
|
|
1376
|
+
def to_dict(self) -> dict[str, Any]:
|
|
1377
|
+
"""Convert to dictionary for JSON serialization."""
|
|
1378
|
+
return asdict(self)
|
|
1379
|
+
|
|
1380
|
+
@classmethod
|
|
1381
|
+
def from_dict(cls, data: dict[str, Any]) -> "DevicePreemptNotification":
|
|
1382
|
+
"""Create DevicePreemptNotification from dictionary."""
|
|
1383
|
+
return cls(
|
|
1384
|
+
device_id=data["device_id"],
|
|
1385
|
+
preempted_by=data["preempted_by"],
|
|
1386
|
+
reason=data["reason"],
|
|
1387
|
+
timestamp=data.get("timestamp", time.time()),
|
|
1388
|
+
)
|