danielutils 1.1.28__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 (287) hide show
  1. danielutils/__init__.py +50 -0
  2. danielutils/abstractions/__init__.py +3 -0
  3. danielutils/abstractions/db/__init__.py +10 -0
  4. danielutils/abstractions/db/database.py +206 -0
  5. danielutils/abstractions/db/database_definitions.py +219 -0
  6. danielutils/abstractions/db/database_exceptions.py +34 -0
  7. danielutils/abstractions/db/database_factory.py +70 -0
  8. danielutils/abstractions/db/database_initializer.py +261 -0
  9. danielutils/abstractions/db/database_models.py +213 -0
  10. danielutils/abstractions/db/dependencies.py +6 -0
  11. danielutils/abstractions/db/implementations/__init__.py +4 -0
  12. danielutils/abstractions/db/implementations/in_memory_database.py +365 -0
  13. danielutils/abstractions/db/implementations/persistent_in_memory_database.py +174 -0
  14. danielutils/abstractions/db/implementations/redis_database.py +403 -0
  15. danielutils/abstractions/db/implementations/sqlite_database.py +491 -0
  16. danielutils/abstractions/multiprogramming/__init__.py +3 -0
  17. danielutils/abstractions/multiprogramming/multi_id.py +26 -0
  18. danielutils/abstractions/multiprogramming/worker.py +104 -0
  19. danielutils/abstractions/multiprogramming/worker_pool.py +98 -0
  20. danielutils/abstractions/repl.py +125 -0
  21. danielutils/aliases.py +174 -0
  22. danielutils/async_/__init__.py +7 -0
  23. danielutils/async_/async_cmd.py +28 -0
  24. danielutils/async_/async_command.py +879 -0
  25. danielutils/async_/async_layered_command.py +125 -0
  26. danielutils/async_/async_retry_executor.py +86 -0
  27. danielutils/async_/async_worker_pool.py +110 -0
  28. danielutils/async_/time_strategy.py +73 -0
  29. danielutils/async_/utils.py +57 -0
  30. danielutils/better_builtins/__init__.py +3 -0
  31. danielutils/better_builtins/counter.py +48 -0
  32. danielutils/better_builtins/frange.py +266 -0
  33. danielutils/better_builtins/typed_builtins/__init__.py +4 -0
  34. danielutils/better_builtins/typed_builtins/factory.py +126 -0
  35. danielutils/better_builtins/typed_builtins/tdict.py +36 -0
  36. danielutils/better_builtins/typed_builtins/tlist.py +223 -0
  37. danielutils/better_builtins/typed_builtins/tset.py +67 -0
  38. danielutils/better_builtins/typed_builtins/ttuple.py +15 -0
  39. danielutils/colors.py +203 -0
  40. danielutils/context_managers/__init__.py +5 -0
  41. danielutils/context_managers/attr_context.py +40 -0
  42. danielutils/context_managers/multi_context.py +34 -0
  43. danielutils/context_managers/optional_context.py +33 -0
  44. danielutils/context_managers/state_context.py +25 -0
  45. danielutils/context_managers/temporary_file.py +108 -0
  46. danielutils/convenience.py +12 -0
  47. danielutils/conversions/__init__.py +2 -0
  48. danielutils/conversions/main_conversions.py +131 -0
  49. danielutils/conversions/specialized_conversions/__init__.py +2 -0
  50. danielutils/conversions/specialized_conversions/to_hex.py +25 -0
  51. danielutils/conversions/specialized_conversions/to_int.py +19 -0
  52. danielutils/custom_types.py +18 -0
  53. danielutils/data_structures/__init__.py +8 -0
  54. danielutils/data_structures/algorithms.py +52 -0
  55. danielutils/data_structures/comparer.py +47 -0
  56. danielutils/data_structures/default_dict.py +23 -0
  57. danielutils/data_structures/functions.py +35 -0
  58. danielutils/data_structures/graph/__init__.py +4 -0
  59. danielutils/data_structures/graph/binary_node.py +71 -0
  60. danielutils/data_structures/graph/graph.py +208 -0
  61. danielutils/data_structures/graph/multinode.py +101 -0
  62. danielutils/data_structures/graph/node.py +60 -0
  63. danielutils/data_structures/heap/__init__.py +3 -0
  64. danielutils/data_structures/heap/heap.py +119 -0
  65. danielutils/data_structures/heap/max_heap.py +18 -0
  66. danielutils/data_structures/heap/min_heap.py +18 -0
  67. danielutils/data_structures/queue/__init__.py +3 -0
  68. danielutils/data_structures/queue/atomic_queue.py +15 -0
  69. danielutils/data_structures/queue/priority_queue.py +110 -0
  70. danielutils/data_structures/queue/queue.py +92 -0
  71. danielutils/data_structures/stack.py +102 -0
  72. danielutils/data_structures/trees/__init__.py +2 -0
  73. danielutils/data_structures/trees/binary_syntax_tree.py +84 -0
  74. danielutils/data_structures/trees/binary_tree.py +63 -0
  75. danielutils/date.py +43 -0
  76. danielutils/date_time.py +23 -0
  77. danielutils/decorators/__init__.py +18 -0
  78. danielutils/decorators/atomic.py +45 -0
  79. danielutils/decorators/attach.py +61 -0
  80. danielutils/decorators/chain_decorators.py +39 -0
  81. danielutils/decorators/decorate_conditionally.py +32 -0
  82. danielutils/decorators/delay_call.py +47 -0
  83. danielutils/decorators/deprecate.py +46 -0
  84. danielutils/decorators/final.py +39 -0
  85. danielutils/decorators/limit_recursion.py +63 -0
  86. danielutils/decorators/memo.py +68 -0
  87. danielutils/decorators/normalize_decorator.py +55 -0
  88. danielutils/decorators/overload.py +204 -0
  89. danielutils/decorators/partially_implemented.py +32 -0
  90. danielutils/decorators/processify.py +74 -0
  91. danielutils/decorators/property.py +42 -0
  92. danielutils/decorators/singleton.py +59 -0
  93. danielutils/decorators/threadify.py +40 -0
  94. danielutils/decorators/timeout.py +82 -0
  95. danielutils/decorators/total_ordering.py +82 -0
  96. danielutils/decorators/validate.py +227 -0
  97. danielutils/exceptions.py +90 -0
  98. danielutils/functions/__init__.py +13 -0
  99. danielutils/functions/areoneof.py +47 -0
  100. danielutils/functions/check_foreach.py +39 -0
  101. danielutils/functions/factorial.py +24 -0
  102. danielutils/functions/flatten.py +33 -0
  103. danielutils/functions/foreach.py +20 -0
  104. danielutils/functions/isoftype.py +475 -0
  105. danielutils/functions/isoneof.py +71 -0
  106. danielutils/functions/multiloop.py +51 -0
  107. danielutils/functions/parallel_for.py +47 -0
  108. danielutils/functions/partition.py +45 -0
  109. danielutils/functions/powerset.py +31 -0
  110. danielutils/functions/subseteq.py +25 -0
  111. danielutils/functions/types_subseteq.py +51 -0
  112. danielutils/generators/__init__.py +3 -0
  113. danielutils/generators/conditional_generator.py +59 -0
  114. danielutils/generators/generator_from_stream.py +31 -0
  115. danielutils/generators/join_generators.py +109 -0
  116. danielutils/internet.py +92 -0
  117. danielutils/io_.py +451 -0
  118. danielutils/java/__init__.py +18 -0
  119. danielutils/java/interfaces/__init__.py +1 -0
  120. danielutils/java/interfaces/comparable.py +31 -0
  121. danielutils/java/java_interface.py +59 -0
  122. danielutils/logging_/__init__.py +2 -0
  123. danielutils/logging_/_impl/__init__.py +4 -0
  124. danielutils/logging_/_impl/builtin_impls/__init__.py +2 -0
  125. danielutils/logging_/_impl/builtin_impls/file_logger.py +35 -0
  126. danielutils/logging_/_impl/builtin_impls/print_logger.py +21 -0
  127. danielutils/logging_/_impl/log_level.py +13 -0
  128. danielutils/logging_/_impl/logger.py +109 -0
  129. danielutils/logging_/_impl/logger_strategy_impl_base.py +37 -0
  130. danielutils/logging_/utils.py +199 -0
  131. danielutils/lombok/__init__.py +1 -0
  132. danielutils/lombok/builder.py +68 -0
  133. danielutils/math_/__init__.py +2 -0
  134. danielutils/math_/constants.py +103 -0
  135. danielutils/math_/functions.py +34 -0
  136. danielutils/math_/math_print.py +52 -0
  137. danielutils/math_/math_symbols.py +125 -0
  138. danielutils/metaclasses/__init__.py +5 -0
  139. danielutils/metaclasses/atomic_class_meta.py +36 -0
  140. danielutils/metaclasses/implicit_data_deleter_meta.py +87 -0
  141. danielutils/metaclasses/instance_cache_meta.py +66 -0
  142. danielutils/metaclasses/interface.py +328 -0
  143. danielutils/metaclasses/overload_meta.py +48 -0
  144. danielutils/mock_/__init__.py +1 -0
  145. danielutils/mock_/mock_module.py +27 -0
  146. danielutils/path.py +55 -0
  147. danielutils/print_.py +123 -0
  148. danielutils/progress_bar/__init__.py +3 -0
  149. danielutils/progress_bar/ascii_progress_bar.py +161 -0
  150. danielutils/progress_bar/progress_bar.py +75 -0
  151. danielutils/progress_bar/progress_bar_pool.py +72 -0
  152. danielutils/protocols/__init__.py +3 -0
  153. danielutils/protocols/dictable.py +38 -0
  154. danielutils/protocols/evaluable.py +26 -0
  155. danielutils/protocols/serializable.py +51 -0
  156. danielutils/py.typed +0 -0
  157. danielutils/random_.py +47 -0
  158. danielutils/reflection/__init__.py +6 -0
  159. danielutils/reflection/declarations.py +288 -0
  160. danielutils/reflection/file/__init__.py +1 -0
  161. danielutils/reflection/file/file_reflection.py +85 -0
  162. danielutils/reflection/function/__init__.py +1 -0
  163. danielutils/reflection/function/function_reflections.py +152 -0
  164. danielutils/reflection/info_classes/__init__.py +6 -0
  165. danielutils/reflection/info_classes/argument_info.py +278 -0
  166. danielutils/reflection/info_classes/class_info.py +204 -0
  167. danielutils/reflection/info_classes/decorator_info.py +54 -0
  168. danielutils/reflection/info_classes/file_info.py +1180 -0
  169. danielutils/reflection/info_classes/folder_info.py +592 -0
  170. danielutils/reflection/info_classes/function_info.py +551 -0
  171. danielutils/reflection/info_classes/import_info.py +207 -0
  172. danielutils/reflection/interpreter/__init__.py +9 -0
  173. danielutils/reflection/interpreter/callstack.py +57 -0
  174. danielutils/reflection/interpreter/get_traceback.py +27 -0
  175. danielutils/reflection/interpreter/interpreter.py +41 -0
  176. danielutils/reflection/interpreter/is_debugging.py +13 -0
  177. danielutils/reflection/interpreter/os_.py +33 -0
  178. danielutils/reflection/interpreter/packages.py +33 -0
  179. danielutils/reflection/interpreter/python_version.py +33 -0
  180. danielutils/reflection/interpreter/signals.py +18 -0
  181. danielutils/reflection/interpreter/tracer.py +175 -0
  182. danielutils/reflection/module/__init__.py +3 -0
  183. danielutils/reflection/module/lazy_module.py +104 -0
  184. danielutils/reflection/module/module_reflections.py +34 -0
  185. danielutils/reflection/module/package_reflection.py +161 -0
  186. danielutils/retry_executor/__init__.py +3 -0
  187. danielutils/retry_executor/backoff_strategies/__init__.py +6 -0
  188. danielutils/retry_executor/backoff_strategies/constant_backoff.py +25 -0
  189. danielutils/retry_executor/backoff_strategies/exponential_backoff.py +26 -0
  190. danielutils/retry_executor/backoff_strategies/functional_backoff.py +29 -0
  191. danielutils/retry_executor/backoff_strategies/linear_backoff.py +23 -0
  192. danielutils/retry_executor/backoff_strategies/multiplicative_backoff.py +23 -0
  193. danielutils/retry_executor/backoff_strategies/no_backoff.py +17 -0
  194. danielutils/retry_executor/backoff_strategy.py +28 -0
  195. danielutils/retry_executor/retry_executor.py +55 -0
  196. danielutils/snippets/__init__.py +1 -0
  197. danielutils/snippets/try_get.py +28 -0
  198. danielutils/system/__init__.py +3 -0
  199. danielutils/system/independent.py +197 -0
  200. danielutils/system/layered_command.py +130 -0
  201. danielutils/system/windows/__init__.py +2 -0
  202. danielutils/system/windows/utils/__init__.py +1 -0
  203. danielutils/system/windows/utils/filetime.py +19 -0
  204. danielutils/system/windows/win32_ctime.py +183 -0
  205. danielutils/system/windows/windows.py +78 -0
  206. danielutils/testing/__init__.py +1 -0
  207. danielutils/testing/unittest_/__init__.py +2 -0
  208. danielutils/testing/unittest_/always_teardown_testcase.py +75 -0
  209. danielutils/testing/unittest_/auto_cwd_testcase.py +114 -0
  210. danielutils/text.py +169 -0
  211. danielutils/time.py +64 -0
  212. danielutils/tools/__init__.py +1 -0
  213. danielutils/tools/unittest_test_runner/__init__.py +9 -0
  214. danielutils/tools/unittest_test_runner/cli.py +70 -0
  215. danielutils/tools/unittest_test_runner/core/__init__.py +17 -0
  216. danielutils/tools/unittest_test_runner/core/discovery.py +322 -0
  217. danielutils/tools/unittest_test_runner/core/execution.py +244 -0
  218. danielutils/tools/unittest_test_runner/core/parser.py +163 -0
  219. danielutils/tools/unittest_test_runner/core/runner.py +561 -0
  220. danielutils/tools/unittest_test_runner/models.py +82 -0
  221. danielutils/university/__init__.py +7 -0
  222. danielutils/university/computability_and_complexity/__init__.py +2 -0
  223. danielutils/university/computability_and_complexity/discreate_finite_automaton.py +89 -0
  224. danielutils/university/computability_and_complexity/languages/__init__.py +0 -0
  225. danielutils/university/computability_and_complexity/languages/language.py +17 -0
  226. danielutils/university/computability_and_complexity/languages/sat.py +71 -0
  227. danielutils/university/computability_and_complexity/turing_machine.py +47 -0
  228. danielutils/university/databases/__init__.py +1 -0
  229. danielutils/university/databases/all.py +756 -0
  230. danielutils/university/image_proccesing/__init__.py +1 -0
  231. danielutils/university/image_proccesing/encoding/__init__.py +3 -0
  232. danielutils/university/image_proccesing/encoding/encoding.py +35 -0
  233. danielutils/university/image_proccesing/encoding/lossless/__init__.py +4 -0
  234. danielutils/university/image_proccesing/encoding/lossless/huffman.py +0 -0
  235. danielutils/university/image_proccesing/encoding/lossless/lossless_encoding.py +10 -0
  236. danielutils/university/image_proccesing/encoding/lossless/lzw.py +0 -0
  237. danielutils/university/image_proccesing/encoding/lossless/run_length.py +22 -0
  238. danielutils/university/image_proccesing/encoding/lossy/__init__.py +1 -0
  239. danielutils/university/image_proccesing/encoding/lossy/lossy_encoding.py +10 -0
  240. danielutils/university/image_proccesing/tansformations/__init__.py +0 -0
  241. danielutils/university/image_proccesing/tansformations/gaussian.py +0 -0
  242. danielutils/university/image_proccesing/tansformations/gradient.py +0 -0
  243. danielutils/university/image_proccesing/tansformations/hough.py +0 -0
  244. danielutils/university/image_proccesing/tansformations/laplacian.py +0 -0
  245. danielutils/university/image_proccesing/tansformations/transformation.py +11 -0
  246. danielutils/university/linear_algebra/__init__.py +1 -0
  247. danielutils/university/linear_algebra/matrix.py +404 -0
  248. danielutils/university/machine_learning/__init__.py +0 -0
  249. danielutils/university/machine_learning/activation_functions/__init__.py +2 -0
  250. danielutils/university/machine_learning/activation_functions/activation_function.py +11 -0
  251. danielutils/university/machine_learning/activation_functions/relu.py +11 -0
  252. danielutils/university/machine_learning/neuron.py +15 -0
  253. danielutils/university/oop/__init__.py +1 -0
  254. danielutils/university/oop/observer.py +31 -0
  255. danielutils/university/oop/strategy.py +9 -0
  256. danielutils/university/probability/__init__.py +7 -0
  257. danielutils/university/probability/conditional_variable/__init__.py +3 -0
  258. danielutils/university/probability/conditional_variable/conditional_variable.py +138 -0
  259. danielutils/university/probability/conditional_variable/continuous/__init__.py +0 -0
  260. danielutils/university/probability/conditional_variable/discrete/__init__.py +8 -0
  261. danielutils/university/probability/conditional_variable/discrete/bernoulli.py +38 -0
  262. danielutils/university/probability/conditional_variable/discrete/bernoulli_sum.py +31 -0
  263. danielutils/university/probability/conditional_variable/discrete/binomial.py +40 -0
  264. danielutils/university/probability/conditional_variable/discrete/conditional_from_discrete_probability_func.py +36 -0
  265. danielutils/university/probability/conditional_variable/discrete/discrete.py +42 -0
  266. danielutils/university/probability/conditional_variable/discrete/geometric.py +43 -0
  267. danielutils/university/probability/conditional_variable/discrete/poisson.py +32 -0
  268. danielutils/university/probability/conditional_variable/discrete/uniform.py +33 -0
  269. danielutils/university/probability/distributions.py +35 -0
  270. danielutils/university/probability/expressions/__init__.py +2 -0
  271. danielutils/university/probability/expressions/accumulation_expression.py +291 -0
  272. danielutils/university/probability/expressions/probability_expression.py +110 -0
  273. danielutils/university/probability/funcs/__init__.py +4 -0
  274. danielutils/university/probability/funcs/covariance.py +12 -0
  275. danielutils/university/probability/funcs/expected_value.py +41 -0
  276. danielutils/university/probability/funcs/probability_function.py +14 -0
  277. danielutils/university/probability/funcs/variance.py +17 -0
  278. danielutils/university/probability/operator.py +79 -0
  279. danielutils/university/probability/protocols.py +32 -0
  280. danielutils/university/probability/supp.py +101 -0
  281. danielutils/university/probability/transformation.py +44 -0
  282. danielutils/versioned_imports.py +31 -0
  283. danielutils-1.1.28.dist-info/METADATA +390 -0
  284. danielutils-1.1.28.dist-info/RECORD +287 -0
  285. danielutils-1.1.28.dist-info/WHEEL +5 -0
  286. danielutils-1.1.28.dist-info/licenses/LICENSE +21 -0
  287. danielutils-1.1.28.dist-info/top_level.txt +1 -0
@@ -0,0 +1,879 @@
1
+ """
2
+ Generic AsyncCommand class for standardized async command execution.
3
+
4
+ This module provides a comprehensive AsyncCommand class that encapsulates command execution
5
+ with state tracking, result handling, and CLI/GUI strategy support using async patterns.
6
+ """
7
+
8
+ import asyncio
9
+ import functools
10
+ import logging
11
+ import os
12
+ import subprocess
13
+ from dataclasses import dataclass
14
+ from enum import Enum
15
+ from pathlib import Path
16
+ from typing import List, Optional, Union, Callable, Dict, Literal
17
+ from datetime import datetime
18
+ from ..logging_.utils import get_logger
19
+
20
+ logger = get_logger(__name__)
21
+
22
+
23
+ async def _to_thread(func: Callable, /, *args, **kwargs):
24
+ if hasattr(asyncio, "to_thread"):
25
+ return await asyncio.to_thread(func, *args, **kwargs)
26
+ loop = asyncio.get_event_loop()
27
+ return await loop.run_in_executor(None, functools.partial(func, *args, **kwargs))
28
+
29
+
30
+ @dataclass
31
+ class CommandResult:
32
+ """Comprehensive result object for command execution."""
33
+ args: List[str]
34
+ returncode: int
35
+ stdout: str
36
+ stderr: str
37
+ pid: int
38
+ start_time: str
39
+ end_time: str
40
+ duration_seconds: float
41
+ timeout_occurred: bool
42
+ killed: bool
43
+ success: bool
44
+ command: str # The original command string
45
+
46
+
47
+ @dataclass
48
+ class CommandResponse:
49
+ success: bool
50
+ output: str
51
+ error: Optional[str] = None
52
+ result: Optional[CommandResult] = None
53
+
54
+
55
+ class CommandState(Enum):
56
+ """Enumeration of possible command states."""
57
+ PENDING = "pending"
58
+ RUNNING = "running"
59
+ COMPLETED = "completed"
60
+ FAILED = "failed"
61
+ TIMEOUT = "timeout"
62
+ KILLED = "killed"
63
+
64
+
65
+ class CommandType(Enum):
66
+ """Enumeration of command types."""
67
+ CLI = "cli"
68
+ GUI = "gui"
69
+
70
+
71
+ @dataclass
72
+ class CommandExecutionResult:
73
+ """Result of command execution with comprehensive details."""
74
+ command: 'AsyncCommand'
75
+ success: bool
76
+ return_code: int
77
+ stdout: str
78
+ stderr: str
79
+ execution_time: float
80
+ state: CommandState
81
+ killed: bool = False
82
+ timeout_occurred: bool = False
83
+ start_time: Optional[datetime] = None
84
+ end_time: Optional[datetime] = None
85
+ pid: Optional[int] = None
86
+ command_type: CommandType = CommandType.CLI
87
+ exception: Optional[Exception] = None
88
+
89
+ def __str__(self) -> str:
90
+ """String representation of the result."""
91
+ status = "SUCCESS" if self.success else "FAILED"
92
+ if self.timeout_occurred:
93
+ status = "TIMEOUT"
94
+ elif self.killed:
95
+ status = "KILLED"
96
+
97
+ return f"CommandExecutionResult(command={self.command}, success={self.success}, return_code={self.return_code}, stdout='{self.stdout}', stderr='{self.stderr}', execution_time={self.execution_time}, state={self.state}, killed={self.killed}, timeout_occurred={self.timeout_occurred}, start_time={self.start_time}, end_time={self.end_time}, pid={self.pid}, command_type={self.command_type}, exception={self.exception}) - {status}"
98
+
99
+
100
+ class AsyncCommand:
101
+ """
102
+ Generic async command execution class with comprehensive state tracking and result handling.
103
+
104
+ This class provides a unified interface for executing commands with support for:
105
+ - Async execution with proper event loop management
106
+ - State tracking (pending, running, completed, failed, timeout, killed)
107
+ - Comprehensive result reporting (stdout, stderr, return code, execution time)
108
+ - CLI/GUI execution strategies
109
+ - Callback support for lifecycle events
110
+ - Timeout handling
111
+ - Process management (kill, wait)
112
+ """
113
+ logger: logging.Logger = logging.getLogger(__name__)
114
+
115
+ def __init__(
116
+ self,
117
+ args: List[str],
118
+ command_type: CommandType = CommandType.CLI,
119
+ timeout: Optional[float] = None,
120
+ cwd: Optional[Union[str, Path]] = None,
121
+ env: Optional[Dict[str, str]] = None,
122
+ on_start: Optional[Callable[['AsyncCommand'], None]] = None,
123
+ on_complete: Optional[Callable[['AsyncCommand', CommandExecutionResult], None]] = None,
124
+ on_error: Optional[Callable[['AsyncCommand', Exception], None]] = None,
125
+ ):
126
+ """
127
+ Initialize the AsyncCommand.
128
+
129
+ Args:
130
+ args: Command arguments as a list of strings
131
+ command_type: Type of command (CLI or GUI)
132
+ timeout: Timeout in seconds (None for no timeout)
133
+ cwd: Working directory for command execution
134
+ env: Environment variables for command execution
135
+ on_start: Callback called when command starts
136
+ on_complete: Callback called when command completes
137
+ on_error: Callback called when command fails
138
+ """
139
+ logger.debug("Initializing AsyncCommand with args=%s, command_type=%s, timeout=%s", args, command_type, timeout)
140
+ self.args = args
141
+ self.command_type = command_type
142
+ if timeout is not None:
143
+ if timeout <= 0.0:
144
+ raise ValueError("timeout must be strictly positive")
145
+ self.timeout = timeout
146
+ self.cwd = Path(cwd) if cwd else None
147
+ self.env = env or {}
148
+ self.on_start = on_start
149
+ self.on_complete = on_complete
150
+ self.on_error = on_error
151
+
152
+ # State management
153
+ self._state = CommandState.PENDING
154
+ self._process: Optional[subprocess.Popen] = None
155
+ self._result: Optional[CommandExecutionResult] = None
156
+ self._start_time: Optional[datetime] = None
157
+ logger.debug("AsyncCommand initialized successfully with state=%s", self._state)
158
+
159
+ @property
160
+ def state(self) -> CommandState:
161
+ """Get the current state of the command."""
162
+ return self._state
163
+
164
+ @property
165
+ def is_running(self) -> bool:
166
+ """Check if the command is currently running."""
167
+ return self._state == CommandState.RUNNING
168
+
169
+ @property
170
+ def is_completed(self) -> bool:
171
+ """Check if the command has completed (successfully or not)."""
172
+ return self._state in [CommandState.COMPLETED, CommandState.FAILED, CommandState.TIMEOUT, CommandState.KILLED]
173
+
174
+ @property
175
+ def result(self) -> Optional[CommandExecutionResult]:
176
+ """Get the execution result if available."""
177
+ return self._result
178
+
179
+ async def execute(self, timeout: Optional[float] = None) -> CommandExecutionResult:
180
+ """
181
+ Execute the command asynchronously.
182
+
183
+ Args:
184
+ timeout: Override the default timeout for this execution
185
+
186
+ Returns:
187
+ CommandExecutionResult: Comprehensive execution result
188
+
189
+ Raises:
190
+ RuntimeError: If command is not in pending state
191
+ """
192
+ # Log command execution start
193
+ self.logger.info("Executing command (async)", extra={
194
+ "data": {
195
+ "command": " ".join(self.args),
196
+ "command_args": self.args,
197
+ "command_type": self.command_type.value,
198
+ "timeout": timeout or self.timeout,
199
+ "cwd": str(self.cwd) if self.cwd else None,
200
+ "env_keys": list(self.env.keys()) if self.env else None
201
+ }
202
+ })
203
+
204
+ # Check if command is in pending state
205
+ if self._state != CommandState.PENDING:
206
+ raise RuntimeError(f"Command is not in pending state: {self._state}")
207
+
208
+ # Check if args is empty - this is considered an error
209
+ if not self.args:
210
+ end_time = datetime.now()
211
+ result = CommandExecutionResult(
212
+ command=self,
213
+ success=False,
214
+ return_code=-1,
215
+ stdout="",
216
+ stderr="Empty command arguments provided",
217
+ execution_time=0.0,
218
+ start_time=self._start_time,
219
+ end_time=end_time,
220
+ state=CommandState.FAILED
221
+ )
222
+ self._result = result
223
+ self._state = CommandState.FAILED
224
+ return result
225
+
226
+ # Set initial state
227
+ self._state = CommandState.RUNNING
228
+ self._start_time = datetime.now()
229
+
230
+ # Call on_start callback
231
+ if self.on_start:
232
+ self.on_start(self)
233
+
234
+ try:
235
+ # Execute based on command type
236
+ if self.command_type == CommandType.CLI:
237
+ result = await self._execute_cli_strategy(timeout)
238
+ else:
239
+ result = await self._execute_gui_strategy(timeout)
240
+
241
+ return result
242
+
243
+ except (OSError, ValueError, subprocess.SubprocessError, asyncio.TimeoutError) as e:
244
+ # Handle unexpected errors
245
+ end_time = datetime.now()
246
+ execution_time = (end_time - self._start_time).total_seconds()
247
+
248
+ result = CommandExecutionResult(
249
+ command=self,
250
+ success=False,
251
+ return_code=-1,
252
+ stdout="",
253
+ stderr=f"Command execution failed: {str(e)}",
254
+ execution_time=execution_time,
255
+ state=CommandState.FAILED,
256
+ start_time=self._start_time,
257
+ end_time=end_time,
258
+ command_type=self.command_type,
259
+ exception=e
260
+ )
261
+
262
+ self._state = CommandState.FAILED
263
+ self._result = result
264
+
265
+ if self.on_error:
266
+ self.on_error(self, e)
267
+
268
+ return result
269
+
270
+ async def _execute_cli_strategy(self, timeout: Optional[float] = None) -> CommandExecutionResult:
271
+ """Execute using CLI strategy asynchronously."""
272
+ start_time = self._start_time or datetime.now()
273
+ effective_timeout = timeout if timeout is not None else self.timeout
274
+
275
+ try:
276
+ # Create subprocess
277
+ # Merge environment variables with current environment
278
+ env = os.environ.copy()
279
+ if self.env:
280
+ env.update(self.env)
281
+
282
+ # Use asyncio.create_subprocess_exec instead of subprocess.Popen to avoid threading issues
283
+ # Set encoding to UTF-8 for proper unicode support
284
+ self._process = await asyncio.create_subprocess_exec(
285
+ *self.args,
286
+ stdout=asyncio.subprocess.PIPE,
287
+ stderr=asyncio.subprocess.PIPE,
288
+ cwd=str(self.cwd) if self.cwd else None,
289
+ env=env
290
+ )
291
+
292
+ pid = self._process.pid
293
+ self.logger.debug("CLI subprocess created (async)", extra={
294
+ "data": {
295
+ "pid": pid,
296
+ "command": " ".join(self.args),
297
+ "timeout": effective_timeout
298
+ }
299
+ })
300
+
301
+ # Wait for completion with timeout
302
+ try:
303
+ # Use asyncio.wait_for with the process directly instead of asyncio.to_thread
304
+ stdout, stderr = await asyncio.wait_for(self._process.communicate(),
305
+ timeout=effective_timeout)
306
+ returncode = self._process.returncode
307
+
308
+ # Decode bytes to strings
309
+ stdout = stdout.decode('utf8') if stdout else ""
310
+ stderr = stderr.decode('utf8') if stderr else ""
311
+
312
+ if returncode == 0:
313
+ self.logger.info("CLI subprocess completed successfully (async)", extra={
314
+ "data": {
315
+ "pid": pid,
316
+ "command": " ".join(self.args),
317
+ "returncode": returncode,
318
+ "stdout_length": len(stdout),
319
+ "stderr_length": len(stderr)
320
+ }
321
+ })
322
+ else:
323
+ self.logger.warning("CLI subprocess completed with non-zero exit code (async)", extra={
324
+ "data": {
325
+ "pid": pid,
326
+ "command": " ".join(self.args),
327
+ "returncode": returncode,
328
+ "stderr_length": len(stderr)
329
+ }
330
+ })
331
+
332
+ except asyncio.TimeoutError:
333
+ # Kill the process on timeout
334
+ self.logger.warning("CLI subprocess timed out, killing process (async)", extra={
335
+ "data": {
336
+ "pid": pid,
337
+ "command": " ".join(self.args),
338
+ "timeout": effective_timeout
339
+ }
340
+ })
341
+
342
+ try:
343
+ self._process.kill()
344
+ stdout, stderr = await self._process.communicate()
345
+ # Decode bytes to strings
346
+ stdout = stdout.decode('utf-8', errors='replace') if stdout else ""
347
+ stderr = stderr.decode('utf-8', errors='replace') if stderr else ""
348
+ except (OSError, subprocess.SubprocessError, BaseException):
349
+ stdout, stderr = "", ""
350
+ returncode = -1
351
+
352
+ self.logger.error("CLI subprocess killed due to timeout (async)", extra={
353
+ "data": {
354
+ "pid": pid,
355
+ "command": " ".join(self.args),
356
+ "timeout": effective_timeout,
357
+ "stderr_length": len(stderr),
358
+ "stdout_length": len(stdout)
359
+ }
360
+ })
361
+
362
+ # Set timeout state and return early
363
+ end_time = datetime.now()
364
+ execution_time = (end_time - start_time).total_seconds()
365
+
366
+ result = CommandExecutionResult(
367
+ command=self,
368
+ success=False,
369
+ return_code=returncode,
370
+ stdout=stdout or "",
371
+ stderr=stderr or "",
372
+ execution_time=execution_time,
373
+ state=CommandState.TIMEOUT,
374
+ timeout_occurred=True,
375
+ start_time=start_time,
376
+ end_time=end_time,
377
+ pid=pid,
378
+ command_type=self.command_type
379
+ )
380
+
381
+ self._state = CommandState.TIMEOUT
382
+ self._result = result
383
+
384
+ if self.on_complete:
385
+ self.on_complete(self, result)
386
+
387
+ return result
388
+
389
+ except (OSError, ValueError, subprocess.SubprocessError) as e:
390
+ returncode = -1
391
+ stdout = ""
392
+ stderr = f"CLI command failed to start: {str(e)}"
393
+
394
+ self.logger.error("CLI subprocess execution failed (async)", extra={
395
+ "data": {
396
+ "command": " ".join(self.args),
397
+ "error": str(e),
398
+ "timeout": effective_timeout
399
+ }
400
+ })
401
+
402
+ end_time = datetime.now()
403
+ execution_time = (end_time - start_time).total_seconds()
404
+ success = returncode == 0
405
+ state = CommandState.COMPLETED if success else CommandState.FAILED
406
+
407
+ # Check if this was a timeout
408
+ timeout_occurred = False
409
+ if returncode == -1 and effective_timeout is not None and execution_time >= effective_timeout * 0.9:
410
+ state = CommandState.TIMEOUT
411
+ timeout_occurred = True
412
+
413
+ result = CommandExecutionResult(
414
+ command=self,
415
+ success=success,
416
+ return_code=returncode,
417
+ stdout=stdout or "",
418
+ stderr=stderr or "",
419
+ execution_time=execution_time,
420
+ state=state,
421
+ timeout_occurred=timeout_occurred,
422
+ start_time=start_time,
423
+ end_time=end_time,
424
+ pid=self._process.pid if self._process else None,
425
+ command_type=self.command_type
426
+ )
427
+
428
+ self._state = state
429
+ self._result = result
430
+
431
+ # Call appropriate callback based on result
432
+ if not success and self.on_error:
433
+ # Create a generic exception for non-zero return codes
434
+ error = RuntimeError(f"Command failed with return code {returncode}")
435
+ self.on_error(self, error)
436
+ elif self.on_complete:
437
+ self.on_complete(self, result)
438
+
439
+ return result
440
+
441
+ async def _execute_gui_strategy(self, timeout: Optional[float] = None) -> CommandExecutionResult:
442
+ """Execute using GUI strategy asynchronously."""
443
+ start_time = self._start_time or datetime.now()
444
+ effective_timeout = timeout if timeout is not None else self.timeout
445
+
446
+ try:
447
+ # Create subprocess without capturing output
448
+ # Merge environment variables with current environment
449
+ env = os.environ.copy()
450
+ if self.env:
451
+ env.update(self.env)
452
+
453
+ self._process = subprocess.Popen(
454
+ self.args,
455
+ shell=True,
456
+ cwd=str(self.cwd) if self.cwd else None,
457
+ env=env
458
+ )
459
+
460
+ pid = self._process.pid
461
+ self.logger.debug("GUI subprocess created (async)", extra={
462
+ "data": {
463
+ "pid": pid,
464
+ "command": " ".join(self.args),
465
+ "timeout": effective_timeout
466
+ }
467
+ })
468
+
469
+ # Wait for completion with timeout
470
+ try:
471
+ returncode = await asyncio.wait_for(
472
+ _to_thread(self._process.wait),
473
+ timeout=effective_timeout
474
+ )
475
+
476
+ if returncode == 0:
477
+ self.logger.info("GUI subprocess completed successfully (async)", extra={
478
+ "data": {
479
+ "pid": pid,
480
+ "command": " ".join(self.args),
481
+ "returncode": returncode
482
+ }
483
+ })
484
+ else:
485
+ self.logger.warning("GUI subprocess completed with non-zero exit code (async)", extra={
486
+ "data": {
487
+ "pid": pid,
488
+ "command": " ".join(self.args),
489
+ "returncode": returncode
490
+ }
491
+ })
492
+
493
+ except asyncio.TimeoutError:
494
+ # Kill the process on timeout
495
+ self.logger.warning("GUI subprocess timed out, killing process (async)", extra={
496
+ "data": {
497
+ "pid": pid,
498
+ "command": " ".join(self.args),
499
+ "timeout": effective_timeout
500
+ }
501
+ })
502
+
503
+ try:
504
+ self._process.kill()
505
+ await _to_thread(self._process.wait)
506
+ except (OSError, subprocess.SubprocessError, BaseException):
507
+ pass
508
+ returncode = -1
509
+
510
+ self.logger.error("GUI subprocess killed due to timeout (async)", extra={
511
+ "data": {
512
+ "pid": pid,
513
+ "command": " ".join(self.args),
514
+ "timeout": effective_timeout
515
+ }
516
+ })
517
+
518
+ # Set timeout state and return early
519
+ end_time = datetime.now()
520
+ execution_time = (end_time - start_time).total_seconds()
521
+
522
+ result = CommandExecutionResult(
523
+ command=self,
524
+ success=False,
525
+ return_code=returncode,
526
+ stdout="", # GUI commands don't capture output
527
+ stderr="", # GUI commands don't capture output
528
+ execution_time=execution_time,
529
+ state=CommandState.TIMEOUT,
530
+ timeout_occurred=True,
531
+ start_time=start_time,
532
+ end_time=end_time,
533
+ pid=pid,
534
+ command_type=self.command_type
535
+ )
536
+
537
+ self._state = CommandState.TIMEOUT
538
+ self._result = result
539
+
540
+ if self.on_complete:
541
+ self.on_complete(self, result)
542
+
543
+ return result
544
+
545
+ except (OSError, ValueError, subprocess.SubprocessError) as e:
546
+ returncode = -1
547
+ self.logger.error("GUI subprocess execution failed (async)", extra={
548
+ "data": {
549
+ "command": " ".join(self.args),
550
+ "error": str(e),
551
+ "timeout": effective_timeout
552
+ }
553
+ })
554
+
555
+ end_time = datetime.now()
556
+ execution_time = (end_time - start_time).total_seconds()
557
+ success = returncode == 0
558
+ state = CommandState.COMPLETED if success else CommandState.FAILED
559
+
560
+ # Check if this was a timeout
561
+ timeout_occurred = False
562
+ if returncode == -1 and effective_timeout is not None and execution_time >= effective_timeout * 0.9:
563
+ state = CommandState.TIMEOUT
564
+ timeout_occurred = True
565
+
566
+ result = CommandExecutionResult(
567
+ command=self,
568
+ success=success,
569
+ return_code=returncode,
570
+ stdout="", # GUI commands don't capture output
571
+ stderr="", # GUI commands don't capture output
572
+ execution_time=execution_time,
573
+ state=state,
574
+ timeout_occurred=timeout_occurred,
575
+ start_time=start_time,
576
+ end_time=end_time,
577
+ pid=self._process.pid if self._process else None,
578
+ command_type=self.command_type
579
+ )
580
+
581
+ self._state = state
582
+ self._result = result
583
+
584
+ # Call appropriate callback based on result
585
+ if not success and self.on_error:
586
+ # Create a generic exception for non-zero return codes
587
+ error = RuntimeError(f"Command failed with return code {returncode}")
588
+ self.on_error(self, error)
589
+ elif self.on_complete:
590
+ self.on_complete(self, result)
591
+
592
+ return result
593
+
594
+ def kill(self) -> bool:
595
+ """
596
+ Kill the running command.
597
+
598
+ Returns:
599
+ bool: True if the command was killed, False otherwise
600
+ """
601
+ self.logger.info("Attempting to kill command", extra={
602
+ "data": {
603
+ "command": " ".join(self.args),
604
+ "state": self._state.value,
605
+ "has_process": self._process is not None
606
+ }
607
+ })
608
+
609
+ if not self._process or not self.is_running:
610
+ self.logger.warning("Attempted to kill command that is not running", extra={
611
+ "data": {
612
+ "command": " ".join(self.args),
613
+ "state": self._state.value,
614
+ "has_process": self._process is not None,
615
+ "is_running": self.is_running
616
+ }
617
+ })
618
+ return False
619
+
620
+ pid = self._process.pid
621
+ try:
622
+ self._process.kill()
623
+ self._state = CommandState.KILLED
624
+
625
+ self.logger.info("Command killed successfully", extra={
626
+ "data": {
627
+ "command": " ".join(self.args),
628
+ "pid": pid,
629
+ "state": self._state.value
630
+ }
631
+ })
632
+ return True
633
+ except (OSError, subprocess.SubprocessError) as e:
634
+ self.logger.error("Failed to kill command", extra={
635
+ "data": {
636
+ "command": " ".join(self.args),
637
+ "pid": pid,
638
+ "error": str(e),
639
+ "error_type": type(e).__name__
640
+ }
641
+ })
642
+ return False
643
+
644
+ def cleanup(self) -> None:
645
+ """Clean up resources and ensure proper shutdown."""
646
+ if self._process:
647
+ try:
648
+ if hasattr(self._process, "poll"):
649
+ if self._process.poll() is None:
650
+ self._process.kill()
651
+ self._process.wait(timeout=1.0)
652
+ elif getattr(self._process, "returncode", None) is None:
653
+ self._process.kill()
654
+ if hasattr(self._process, "wait"):
655
+ self._process.wait()
656
+ except (OSError, subprocess.SubprocessError, subprocess.TimeoutExpired):
657
+ pass
658
+ finally:
659
+ self._process = None
660
+
661
+ async def wait(self) -> CommandExecutionResult:
662
+ """
663
+ Wait for the command to complete.
664
+
665
+ Returns:
666
+ CommandExecutionResult: The execution result
667
+ """
668
+ self.logger.info("Waiting for command to complete", extra={
669
+ "data": {
670
+ "command": " ".join(self.args),
671
+ "state": self._state.value
672
+ }
673
+ })
674
+
675
+ if self._result:
676
+ self.logger.debug("Command already completed, returning cached result", extra={
677
+ "data": {
678
+ "command": " ".join(self.args),
679
+ "state": self._state.value,
680
+ "success": self._result.success
681
+ }
682
+ })
683
+ return self._result
684
+
685
+ # If not running, execute first
686
+ if self._state == CommandState.PENDING:
687
+ return await self.execute()
688
+
689
+ # Wait for completion
690
+ while not self.is_completed:
691
+ await asyncio.sleep(0.1)
692
+
693
+ self.logger.info("Command completed", extra={
694
+ "data": {
695
+ "command": " ".join(self.args),
696
+ "state": self._state.value,
697
+ "success": self._result.success if self._result else False
698
+ }
699
+ })
700
+
701
+ return self._result
702
+
703
+ def to_command_result(self) -> CommandResult:
704
+ """Convert to CommandResult schema."""
705
+ if not self._result:
706
+ return CommandResult(
707
+ success=False,
708
+ return_code=-1,
709
+ stdout="",
710
+ stderr="Command not executed",
711
+ execution_time=0.0
712
+ )
713
+
714
+ return CommandResult(
715
+ success=self._result.success,
716
+ return_code=self._result.return_code,
717
+ stdout=self._result.stdout,
718
+ stderr=self._result.stderr,
719
+ execution_time=self._result.execution_time
720
+ )
721
+
722
+ def to_command_response(self) -> CommandResponse:
723
+ """Convert to CommandResponse schema."""
724
+ if not self._result:
725
+ return CommandResponse(
726
+ success=False,
727
+ message="Command not executed",
728
+ data={}
729
+ )
730
+
731
+ return CommandResponse(
732
+ success=self._result.success,
733
+ message="Command executed successfully" if self._result.success else "Command failed",
734
+ data={
735
+ "return_code": self._result.return_code,
736
+ "stdout": self._result.stdout,
737
+ "stderr": self._result.stderr,
738
+ "execution_time": self._result.execution_time,
739
+ "state": self._state.value,
740
+ "pid": self._result.pid
741
+ }
742
+ )
743
+
744
+ @staticmethod
745
+ def _parse_command_string(command: str) -> List[str]:
746
+ """
747
+ Parse a command string into a list of arguments, handling quoted strings properly.
748
+
749
+ This method intelligently splits command strings while preserving spaces within
750
+ single or double quotes. It handles both single and double quotes correctly.
751
+
752
+ Args:
753
+ command: The command string to parse
754
+
755
+ Returns:
756
+ List[str]: List of parsed command arguments
757
+
758
+ Examples:
759
+ >>> _parse_command_string('echo "hello world"')
760
+ ['echo', 'hello world']
761
+ >>> _parse_command_string("git commit -m 'fix bug'")
762
+ ['git', 'commit', '-m', 'fix bug']
763
+ >>> _parse_command_string('cmd "arg with spaces" --flag value')
764
+ ['cmd', 'arg with spaces', '--flag', 'value']
765
+ """
766
+ if not command.strip():
767
+ return []
768
+
769
+ args = []
770
+ current_arg = ""
771
+ in_quotes = False
772
+ quote_char = None
773
+
774
+ i = 0
775
+ while i < len(command):
776
+ char = command[i]
777
+
778
+ if not in_quotes:
779
+ if char in ['"', "'"]:
780
+ # Start of quoted string
781
+ in_quotes = True
782
+ quote_char = char
783
+ elif char == ' ':
784
+ # Space outside quotes - end of current argument
785
+ if current_arg:
786
+ args.append(current_arg)
787
+ current_arg = ""
788
+ else:
789
+ # Regular character
790
+ current_arg += char
791
+ else:
792
+ if char == quote_char:
793
+ # End of quoted string
794
+ in_quotes = False
795
+ quote_char = None
796
+ else:
797
+ # Character inside quotes
798
+ current_arg += char
799
+
800
+ i += 1
801
+
802
+ # Add the last argument if there is one
803
+ if current_arg:
804
+ args.append(current_arg)
805
+
806
+ return args
807
+
808
+ @classmethod
809
+ def from_str(cls, command: str, **kwargs) -> 'AsyncCommand':
810
+ """
811
+ Create a shell command.
812
+
813
+ Args:
814
+ command: Shell command string
815
+ **kwargs: Additional arguments for AsyncCommand
816
+
817
+ Returns:
818
+ AsyncCommand: Configured command instance
819
+ """
820
+ kwargs['command_type'] = kwargs.get('command_type', CommandType.CLI)
821
+ if '"' in command or "'" in command:
822
+ cls.logger.warning(
823
+ "Command '%s' contains quotes which can lead to the command failing due to parsing. It is recommended to build explicitly using the constructor to be able to escape the quotes correctly.",
824
+ command)
825
+ return cls(
826
+ args=AsyncCommand._parse_command_string(command),
827
+ **kwargs
828
+ )
829
+
830
+ @classmethod
831
+ def cmd(cls, command: str, **kwargs) -> 'AsyncCommand':
832
+ """Create a Windows CMD command with proper argument handling."""
833
+ stripped_command = command.strip()
834
+ # For cmd, we pass the command directly to /c without additional quoting
835
+ # The command will be parsed by cmd.exe itself
836
+ args = ["cmd", "/c", stripped_command]
837
+ return cls(args, **kwargs)
838
+
839
+ @classmethod
840
+ def powershell(cls, command: str, **kwargs) -> 'AsyncCommand':
841
+ """Create a PowerShell command with proper argument handling."""
842
+ stripped_command = command.strip()
843
+ # For PowerShell, we pass the command directly to -Command without additional quoting
844
+ # PowerShell will handle the command parsing internally
845
+ args = ["powershell", "-Command", stripped_command]
846
+ return cls(args, **kwargs)
847
+
848
+ @classmethod
849
+ def wsl(cls, command: str, **kwargs) -> 'AsyncCommand':
850
+ """Create a WSL command with proper argument handling."""
851
+ stripped_command = command.strip()
852
+ # For WSL, we pass the command directly to bash -c
853
+ args = ["wsl", "bash", "-c", stripped_command]
854
+ return cls(args, **kwargs)
855
+
856
+ def __await__(self):
857
+ """Make AsyncCommand awaitable by delegating to execute()."""
858
+ return self.execute().__await__()
859
+
860
+ def __repr__(self) -> str:
861
+ """String representation of the command."""
862
+ return f"AsyncCommand(args={self.args}, state={self._state.value})"
863
+
864
+
865
+ class CommonCommands:
866
+ @staticmethod
867
+ def kill_pid(pid: int, **kwargs) -> 'AsyncCommand':
868
+ return AsyncCommand.powershell(f'taskkill /PID {pid} /F', **kwargs)
869
+
870
+
871
+ __all__ = [
872
+ "CommandResult",
873
+ "CommandResponse",
874
+ "CommandState",
875
+ "CommandType",
876
+ "CommandExecutionResult",
877
+ "AsyncCommand",
878
+ "CommonCommands"
879
+ ]