base.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481
  1. """
  2. Base Command Classes for Trixy Network Protocol
  3. This module defines the base classes for all network commands in the Trixy
  4. system. These classes provide:
  5. - Common interface for all commands
  6. - Serialization/deserialization support
  7. - Validation and error handling
  8. - Response management
  9. - Type safety and documentation
  10. All network commands inherit from these base classes to ensure consistency
  11. and proper integration with the Trixy protocol.
  12. """
  13. import time
  14. import uuid
  15. from typing import Any, Dict, Optional, Union, List
  16. from dataclasses import dataclass, field
  17. from enum import Enum, IntEnum
  18. from abc import ABC, abstractmethod
  19. import json
  20. def pprint(message: str) -> None:
  21. """Base command logging function."""
  22. print(f"[CMD.BASE] {message}")
  23. class CommandStatus(IntEnum):
  24. """Status codes for command execution."""
  25. SUCCESS = 0
  26. ERROR = 1
  27. PENDING = 2
  28. TIMEOUT = 3
  29. INVALID = 4
  30. UNAUTHORIZED = 5
  31. NOT_FOUND = 6
  32. CONFLICT = 7
  33. class ResponseType(Enum):
  34. """Types of command responses."""
  35. SUCCESS = "success"
  36. ERROR = "error"
  37. ACK = "acknowledgment"
  38. DATA = "data"
  39. PARTIAL = "partial"
  40. class CommandError(Exception):
  41. """Base exception for command-related errors."""
  42. def __init__(self, message: str, status: CommandStatus = CommandStatus.ERROR):
  43. super().__init__(message)
  44. self.status = status
  45. class ValidationError(CommandError):
  46. """Raised when command validation fails."""
  47. def __init__(self, message: str):
  48. super().__init__(message, CommandStatus.INVALID)
  49. @dataclass
  50. class TrixyCommand(ABC):
  51. """
  52. Base class for all Trixy network commands.
  53. This abstract base class defines the common interface that all commands
  54. must implement. It provides basic functionality for:
  55. - Unique command identification
  56. - Timestamp tracking
  57. - Serialization support
  58. - Validation
  59. - Response handling
  60. All command classes must inherit from this class and implement the
  61. required abstract methods.
  62. """
  63. # Core fields that all commands have
  64. command_id: str = field(default_factory=lambda: str(uuid.uuid4()))
  65. timestamp: float = field(default_factory=time.time)
  66. version: str = field(default="1.0.0")
  67. source: Optional[str] = field(default=None)
  68. destination: Optional[str] = field(default=None)
  69. def __post_init__(self):
  70. """Post-initialization validation and setup."""
  71. self.validate()
  72. @abstractmethod
  73. def validate(self) -> bool:
  74. """
  75. Validate the command data.
  76. Returns:
  77. bool: True if valid
  78. Raises:
  79. ValidationError: If validation fails
  80. """
  81. pass
  82. @abstractmethod
  83. def get_command_name(self) -> str:
  84. """
  85. Get the command name for protocol identification.
  86. Returns:
  87. str: Command name
  88. """
  89. pass
  90. def to_dict(self) -> Dict[str, Any]:
  91. """
  92. Convert command to dictionary for serialization.
  93. Returns:
  94. Dict[str, Any]: Command data as dictionary
  95. """
  96. result = {}
  97. for key, value in self.__dict__.items():
  98. if not key.startswith('_'):
  99. result[key] = value
  100. return result
  101. @classmethod
  102. def from_dict(cls, data: Dict[str, Any]) -> 'TrixyCommand':
  103. """
  104. Create command instance from dictionary.
  105. Args:
  106. data: Dictionary with command data
  107. Returns:
  108. TrixyCommand: Command instance
  109. """
  110. return cls(**data)
  111. def to_json(self) -> str:
  112. """
  113. Convert command to JSON string.
  114. Returns:
  115. str: JSON representation
  116. """
  117. return json.dumps(self.to_dict())
  118. @classmethod
  119. def from_json(cls, json_str: str) -> 'TrixyCommand':
  120. """
  121. Create command from JSON string.
  122. Args:
  123. json_str: JSON string
  124. Returns:
  125. TrixyCommand: Command instance
  126. """
  127. data = json.loads(json_str)
  128. return cls.from_dict(data)
  129. def get_response_class(self):
  130. """
  131. Get the expected response class for this command.
  132. Returns:
  133. Class: Response class or None if no response expected
  134. """
  135. return None
  136. def requires_response(self) -> bool:
  137. """
  138. Check if this command requires a response.
  139. Returns:
  140. bool: True if response is required
  141. """
  142. return self.get_response_class() is not None
  143. def get_timeout(self) -> float:
  144. """
  145. Get the timeout for this command in seconds.
  146. Returns:
  147. float: Timeout in seconds
  148. """
  149. return 30.0 # Default 30 second timeout
  150. def is_expired(self) -> bool:
  151. """
  152. Check if this command has expired based on its timeout.
  153. Returns:
  154. bool: True if expired
  155. """
  156. return time.time() - self.timestamp > self.get_timeout()
  157. def get_priority(self) -> int:
  158. """
  159. Get command priority (lower numbers = higher priority).
  160. Returns:
  161. int: Priority value (0-100)
  162. """
  163. return 50 # Normal priority
  164. def __str__(self) -> str:
  165. """String representation of the command."""
  166. return f"{self.get_command_name()}({self.command_id[:8]}...)"
  167. def __repr__(self) -> str:
  168. """Detailed string representation."""
  169. return f"{self.__class__.__name__}(id={self.command_id}, timestamp={self.timestamp})"
  170. @dataclass
  171. class CommandResponse(ABC):
  172. """
  173. Base class for all command responses.
  174. This class provides the standard interface for responses to commands.
  175. It includes status information, timing data, and optional response data.
  176. """
  177. # Response identification
  178. response_id: str = field(default_factory=lambda: str(uuid.uuid4()))
  179. command_id: str = field(default="")
  180. # Status and timing
  181. status: CommandStatus = field(default=CommandStatus.SUCCESS)
  182. response_type: ResponseType = field(default=ResponseType.SUCCESS)
  183. timestamp: float = field(default_factory=time.time)
  184. # Optional fields
  185. message: Optional[str] = field(default=None)
  186. error_details: Optional[Dict[str, Any]] = field(default=None)
  187. data: Optional[Dict[str, Any]] = field(default=None)
  188. def __post_init__(self):
  189. """Post-initialization setup."""
  190. self.validate()
  191. @abstractmethod
  192. def validate(self) -> bool:
  193. """
  194. Validate the response data.
  195. Returns:
  196. bool: True if valid
  197. Raises:
  198. ValidationError: If validation fails
  199. """
  200. pass
  201. @abstractmethod
  202. def get_response_name(self) -> str:
  203. """
  204. Get the response name for protocol identification.
  205. Returns:
  206. str: Response name
  207. """
  208. pass
  209. def is_success(self) -> bool:
  210. """
  211. Check if the response indicates success.
  212. Returns:
  213. bool: True if successful
  214. """
  215. return self.status == CommandStatus.SUCCESS
  216. def is_error(self) -> bool:
  217. """
  218. Check if the response indicates an error.
  219. Returns:
  220. bool: True if error
  221. """
  222. return self.status != CommandStatus.SUCCESS
  223. def get_error_message(self) -> str:
  224. """
  225. Get error message if response is an error.
  226. Returns:
  227. str: Error message or empty string
  228. """
  229. if self.is_error():
  230. return self.message or f"Command failed with status: {self.status.name}"
  231. return ""
  232. def to_dict(self) -> Dict[str, Any]:
  233. """
  234. Convert response to dictionary.
  235. Returns:
  236. Dict[str, Any]: Response data as dictionary
  237. """
  238. result = {}
  239. for key, value in self.__dict__.items():
  240. if not key.startswith('_'):
  241. if isinstance(value, Enum):
  242. result[key] = value.value
  243. else:
  244. result[key] = value
  245. return result
  246. @classmethod
  247. def from_dict(cls, data: Dict[str, Any]) -> 'CommandResponse':
  248. """
  249. Create response from dictionary.
  250. Args:
  251. data: Dictionary with response data
  252. Returns:
  253. CommandResponse: Response instance
  254. """
  255. # Handle enum fields
  256. if 'status' in data and isinstance(data['status'], (str, int)):
  257. data['status'] = CommandStatus(data['status'])
  258. if 'response_type' in data and isinstance(data['response_type'], str):
  259. data['response_type'] = ResponseType(data['response_type'])
  260. return cls(**data)
  261. def __str__(self) -> str:
  262. """String representation of the response."""
  263. return f"{self.get_response_name()}({self.status.name})"
  264. # Concrete response implementations for common cases
  265. @dataclass
  266. class SuccessResponse(CommandResponse):
  267. """Generic success response."""
  268. def validate(self) -> bool:
  269. """Validate success response."""
  270. if self.status != CommandStatus.SUCCESS:
  271. raise ValidationError("SuccessResponse must have SUCCESS status")
  272. return True
  273. def get_response_name(self) -> str:
  274. """Get response name."""
  275. return "SuccessResponse"
  276. @dataclass
  277. class ErrorResponse(CommandResponse):
  278. """Generic error response."""
  279. def __post_init__(self):
  280. """Set error status if not already set."""
  281. if self.status == CommandStatus.SUCCESS:
  282. self.status = CommandStatus.ERROR
  283. self.response_type = ResponseType.ERROR
  284. super().__post_init__()
  285. def validate(self) -> bool:
  286. """Validate error response."""
  287. if self.status == CommandStatus.SUCCESS:
  288. raise ValidationError("ErrorResponse cannot have SUCCESS status")
  289. if not self.message:
  290. raise ValidationError("ErrorResponse must have an error message")
  291. return True
  292. def get_response_name(self) -> str:
  293. """Get response name."""
  294. return "ErrorResponse"
  295. @dataclass
  296. class AckResponse(CommandResponse):
  297. """Simple acknowledgment response."""
  298. def __post_init__(self):
  299. """Set ACK response type."""
  300. self.response_type = ResponseType.ACK
  301. super().__post_init__()
  302. def validate(self) -> bool:
  303. """Validate ACK response."""
  304. return True
  305. def get_response_name(self) -> str:
  306. """Get response name."""
  307. return "AckResponse"
  308. # Utility functions
  309. def create_success_response(
  310. command_id: str,
  311. message: str = "Command completed successfully",
  312. data: Optional[Dict[str, Any]] = None
  313. ) -> SuccessResponse:
  314. """
  315. Create a success response for a command.
  316. Args:
  317. command_id: ID of the command being responded to
  318. message: Success message
  319. data: Optional response data
  320. Returns:
  321. SuccessResponse: Success response
  322. """
  323. return SuccessResponse(
  324. command_id=command_id,
  325. message=message,
  326. data=data
  327. )
  328. def create_error_response(
  329. command_id: str,
  330. message: str,
  331. status: CommandStatus = CommandStatus.ERROR,
  332. error_details: Optional[Dict[str, Any]] = None
  333. ) -> ErrorResponse:
  334. """
  335. Create an error response for a command.
  336. Args:
  337. command_id: ID of the command being responded to
  338. message: Error message
  339. status: Error status code
  340. error_details: Optional error details
  341. Returns:
  342. ErrorResponse: Error response
  343. """
  344. return ErrorResponse(
  345. command_id=command_id,
  346. message=message,
  347. status=status,
  348. error_details=error_details
  349. )
  350. def create_ack_response(command_id: str, message: str = "Command acknowledged") -> AckResponse:
  351. """
  352. Create an acknowledgment response for a command.
  353. Args:
  354. command_id: ID of the command being responded to
  355. message: ACK message
  356. Returns:
  357. AckResponse: Acknowledgment response
  358. """
  359. return AckResponse(
  360. command_id=command_id,
  361. message=message
  362. )
  363. # Module exports
  364. __all__ = [
  365. "TrixyCommand",
  366. "CommandResponse",
  367. "CommandStatus",
  368. "ResponseType",
  369. "CommandError",
  370. "ValidationError",
  371. "SuccessResponse",
  372. "ErrorResponse",
  373. "AckResponse",
  374. "create_success_response",
  375. "create_error_response",
  376. "create_ack_response",
  377. ]