PyAlgoEngine 0.7.7a1__tar.gz → 0.7.7a3__tar.gz

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 (54) hide show
  1. {pyalgoengine-0.7.7a1 → pyalgoengine-0.7.7a3}/PKG-INFO +1 -1
  2. {pyalgoengine-0.7.7a1 → pyalgoengine-0.7.7a3}/PyAlgoEngine.egg-info/PKG-INFO +1 -1
  3. {pyalgoengine-0.7.7a1 → pyalgoengine-0.7.7a3}/algo_engine/__init__.py +1 -1
  4. pyalgoengine-0.7.7a3/algo_engine/apps/sim_input/client.py +394 -0
  5. pyalgoengine-0.7.7a1/algo_engine/apps/sim_input/client.py +0 -253
  6. {pyalgoengine-0.7.7a1 → pyalgoengine-0.7.7a3}/LICENSE +0 -0
  7. {pyalgoengine-0.7.7a1 → pyalgoengine-0.7.7a3}/PyAlgoEngine.egg-info/SOURCES.txt +0 -0
  8. {pyalgoengine-0.7.7a1 → pyalgoengine-0.7.7a3}/PyAlgoEngine.egg-info/dependency_links.txt +0 -0
  9. {pyalgoengine-0.7.7a1 → pyalgoengine-0.7.7a3}/PyAlgoEngine.egg-info/requires.txt +0 -0
  10. {pyalgoengine-0.7.7a1 → pyalgoengine-0.7.7a3}/PyAlgoEngine.egg-info/top_level.txt +0 -0
  11. {pyalgoengine-0.7.7a1 → pyalgoengine-0.7.7a3}/README.md +0 -0
  12. {pyalgoengine-0.7.7a1 → pyalgoengine-0.7.7a3}/algo_engine/apps/__init__.py +0 -0
  13. {pyalgoengine-0.7.7a1 → pyalgoengine-0.7.7a3}/algo_engine/apps/backtest/__init__.py +0 -0
  14. {pyalgoengine-0.7.7a1 → pyalgoengine-0.7.7a3}/algo_engine/apps/backtest/doc_server.py +0 -0
  15. {pyalgoengine-0.7.7a1 → pyalgoengine-0.7.7a3}/algo_engine/apps/backtest/tester.py +0 -0
  16. {pyalgoengine-0.7.7a1 → pyalgoengine-0.7.7a3}/algo_engine/apps/backtest/web_app.py +0 -0
  17. {pyalgoengine-0.7.7a1 → pyalgoengine-0.7.7a3}/algo_engine/apps/bokeh_server.py +0 -0
  18. {pyalgoengine-0.7.7a1 → pyalgoengine-0.7.7a3}/algo_engine/apps/demo/__init__.py +0 -0
  19. {pyalgoengine-0.7.7a1 → pyalgoengine-0.7.7a3}/algo_engine/apps/demo/test.py +0 -0
  20. {pyalgoengine-0.7.7a1 → pyalgoengine-0.7.7a3}/algo_engine/apps/sim_input/__init__.py +0 -0
  21. {pyalgoengine-0.7.7a1 → pyalgoengine-0.7.7a3}/algo_engine/apps/sim_input/sim_keyboard.py +0 -0
  22. {pyalgoengine-0.7.7a1 → pyalgoengine-0.7.7a3}/algo_engine/apps/sim_input/sim_mouse.py +0 -0
  23. {pyalgoengine-0.7.7a1 → pyalgoengine-0.7.7a3}/algo_engine/apps/sim_input/window.py +0 -0
  24. {pyalgoengine-0.7.7a1 → pyalgoengine-0.7.7a3}/algo_engine/backtest/__init__.py +0 -0
  25. {pyalgoengine-0.7.7a1 → pyalgoengine-0.7.7a3}/algo_engine/backtest/__main__.py +0 -0
  26. {pyalgoengine-0.7.7a1 → pyalgoengine-0.7.7a3}/algo_engine/backtest/metrics.py +0 -0
  27. {pyalgoengine-0.7.7a1 → pyalgoengine-0.7.7a3}/algo_engine/backtest/replay.py +0 -0
  28. {pyalgoengine-0.7.7a1 → pyalgoengine-0.7.7a3}/algo_engine/backtest/sim_match.py +0 -0
  29. {pyalgoengine-0.7.7a1 → pyalgoengine-0.7.7a3}/algo_engine/base/__init__.py +0 -0
  30. {pyalgoengine-0.7.7a1 → pyalgoengine-0.7.7a3}/algo_engine/base/console_utils.py +0 -0
  31. {pyalgoengine-0.7.7a1 → pyalgoengine-0.7.7a3}/algo_engine/base/finance_decimal.py +0 -0
  32. {pyalgoengine-0.7.7a1 → pyalgoengine-0.7.7a3}/algo_engine/base/market_buffer.py +0 -0
  33. {pyalgoengine-0.7.7a1 → pyalgoengine-0.7.7a3}/algo_engine/base/market_utils.py +0 -0
  34. {pyalgoengine-0.7.7a1 → pyalgoengine-0.7.7a3}/algo_engine/base/market_utils_nt.py +0 -0
  35. {pyalgoengine-0.7.7a1 → pyalgoengine-0.7.7a3}/algo_engine/base/market_utils_posix.py +0 -0
  36. {pyalgoengine-0.7.7a1 → pyalgoengine-0.7.7a3}/algo_engine/base/technical_analysis.py +0 -0
  37. {pyalgoengine-0.7.7a1 → pyalgoengine-0.7.7a3}/algo_engine/base/telemetrics.py +0 -0
  38. {pyalgoengine-0.7.7a1 → pyalgoengine-0.7.7a3}/algo_engine/base/trade_utils.py +0 -0
  39. {pyalgoengine-0.7.7a1 → pyalgoengine-0.7.7a3}/algo_engine/engine/__init__.py +0 -0
  40. {pyalgoengine-0.7.7a1 → pyalgoengine-0.7.7a3}/algo_engine/engine/algo_engine.py +0 -0
  41. {pyalgoengine-0.7.7a1 → pyalgoengine-0.7.7a3}/algo_engine/engine/event_engine.py +0 -0
  42. {pyalgoengine-0.7.7a1 → pyalgoengine-0.7.7a3}/algo_engine/engine/market_engine.py +0 -0
  43. {pyalgoengine-0.7.7a1 → pyalgoengine-0.7.7a3}/algo_engine/engine/trade_engine.py +0 -0
  44. {pyalgoengine-0.7.7a1 → pyalgoengine-0.7.7a3}/algo_engine/monitor/__init__.py +0 -0
  45. {pyalgoengine-0.7.7a1 → pyalgoengine-0.7.7a3}/algo_engine/monitor/advanced_data_interface.py +0 -0
  46. {pyalgoengine-0.7.7a1 → pyalgoengine-0.7.7a3}/algo_engine/profile/__init__.py +0 -0
  47. {pyalgoengine-0.7.7a1 → pyalgoengine-0.7.7a3}/algo_engine/profile/cn.py +0 -0
  48. {pyalgoengine-0.7.7a1 → pyalgoengine-0.7.7a3}/algo_engine/strategy/__init__.py +0 -0
  49. {pyalgoengine-0.7.7a1 → pyalgoengine-0.7.7a3}/algo_engine/strategy/strategy_engine.py +0 -0
  50. {pyalgoengine-0.7.7a1 → pyalgoengine-0.7.7a3}/algo_engine/utils/__init__.py +0 -0
  51. {pyalgoengine-0.7.7a1 → pyalgoengine-0.7.7a3}/algo_engine/utils/commit_regularizer.py +0 -0
  52. {pyalgoengine-0.7.7a1 → pyalgoengine-0.7.7a3}/algo_engine/utils/data_utils.py +0 -0
  53. {pyalgoengine-0.7.7a1 → pyalgoengine-0.7.7a3}/setup.cfg +0 -0
  54. {pyalgoengine-0.7.7a1 → pyalgoengine-0.7.7a3}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: PyAlgoEngine
3
- Version: 0.7.7a1
3
+ Version: 0.7.7a3
4
4
  Summary: Basic algo engine
5
5
  Home-page: https://github.com/BolunHan/PyAlgoEngine
6
6
  Author: Bolun.Han
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: PyAlgoEngine
3
- Version: 0.7.7a1
3
+ Version: 0.7.7a3
4
4
  Summary: Basic algo engine
5
5
  Home-page: https://github.com/BolunHan/PyAlgoEngine
6
6
  Author: Bolun.Han
@@ -1,4 +1,4 @@
1
- __version__ = "0.7.7.alpha1"
1
+ __version__ = "0.7.7.alpha3"
2
2
 
3
3
  import logging
4
4
  import os
@@ -0,0 +1,394 @@
1
+ __package__ = 'algo_engine.apps.sim_input'
2
+
3
+ import abc
4
+ import time
5
+ import tkinter
6
+ from collections.abc import Callable, Iterable
7
+ from threading import Thread
8
+ from tkinter import ttk
9
+ from typing import TypedDict, NotRequired, Any
10
+
11
+ from . import LOGGER
12
+
13
+ LOGGER.getChild('Client')
14
+
15
+
16
+ class Action(object):
17
+ class Step(TypedDict):
18
+ name: str
19
+ procedure: Callable
20
+ args: NotRequired[Iterable]
21
+ kwargs: NotRequired[dict[str, Any]]
22
+ comments: NotRequired[str]
23
+
24
+ def __init__(self, name: str):
25
+ self.name = name
26
+ self.steps: list[Action.Step] = []
27
+
28
+ def append(self, name: str, action: Callable[[..., ...], None], args=None, kwargs=None, comments: str = None) -> None:
29
+ step = Action.Step(name=name, procedure=action)
30
+
31
+ if args is not None:
32
+ step['args'] = args
33
+
34
+ if kwargs is not None:
35
+ step['kwargs'] = kwargs
36
+
37
+ if comments is not None:
38
+ step['comments'] = comments
39
+
40
+ self.steps.append(step)
41
+
42
+ def __call__(self, ignore_error: bool = False) -> None:
43
+ n = len(self.steps)
44
+ for i, step in enumerate(self.steps, start=1):
45
+ procedure = step['procedure']
46
+ args = step.get('args', [])
47
+ kwargs = step.get('kwargs', {})
48
+ comments = step.get('comments', '')
49
+ try:
50
+ procedure(*args, **kwargs)
51
+ except Exception as e:
52
+ LOGGER.error(f'<Action {self.name}> <step {comments}>({i} / {n}) Failed!')
53
+
54
+ if not ignore_error:
55
+ raise e
56
+
57
+ LOGGER.debug(f'<Action {self.name}> <step {comments}>({i} / {n}) Completed!')
58
+
59
+ LOGGER.info(f'<Action {self.name}> completed!')
60
+
61
+ def __len__(self):
62
+ return len(self.steps)
63
+
64
+
65
+ class AutoWorkClient(object, metaclass=abc.ABCMeta):
66
+ def __init__(self, root=None, **kwargs):
67
+ self.root = root if root is not None else tkinter.Tk()
68
+ self.root.title(kwargs.get('title', "Structured GUI Client"))
69
+
70
+ self.actions: dict[int, list[Action]] = {}
71
+ self.layout = {}
72
+
73
+ # State variables
74
+ self.worker_thread = None
75
+ self.running = False
76
+ self.recording = False
77
+
78
+ self.render_layout()
79
+
80
+ def render_layout(self):
81
+ # No fixed geometry, letting it resize
82
+ # self.root.geometry("600x500")
83
+
84
+ # Grid(0, 0): Create button
85
+ button_takeover = self.layout['button_takeover'] = ttk.Button(self.root, text="Takeover Input", command=self.toggle_auto_work)
86
+ button_takeover.grid(row=0, column=0, pady=10, sticky="ew")
87
+
88
+ # Grid(0, 1): Mock button
89
+ button_record = self.layout['button_record'] = ttk.Button(self.root, text="Record Action", command=self.record_action)
90
+ button_record.grid(row=0, column=1, pady=10, sticky="ew")
91
+
92
+ # Grid(1, 0): Mock button
93
+ button_mock = self.layout['button_mock'] = ttk.Button(self.root, text="Mock Action", command=self.toggle_mock)
94
+ button_mock.grid(row=1, column=0, padx=10, pady=10, sticky="ew")
95
+
96
+ # Grid(1, 1): Dropdown for action selection
97
+ action_selected = self.layout['action_selected'] = tkinter.StringVar()
98
+ action_dropdown = self.layout['action_dropdown'] = ttk.Combobox(self.root, textvariable=action_selected, state="readonly")
99
+ action_dropdown.grid(row=1, column=1, padx=10, pady=10, sticky="ew")
100
+
101
+ # Grid(2, 0): Create table
102
+ action_table = self.layout['action_table'] = ttk.Treeview(self.root, columns=("Action", "Status", "Comments"), show="tree headings")
103
+ action_table.heading("Action", text="Action")
104
+ action_table.heading("Status", text="Status")
105
+ action_table.heading("Comments", text="Comments")
106
+ action_table.grid(row=2, column=0, columnspan=2, padx=10, pady=10, sticky="nsew")
107
+
108
+ # Make the window resize properly by configuring grid weights
109
+ self.root.grid_columnconfigure(0, weight=1, minsize=200) # Adjust minsize for column 0
110
+ self.root.grid_columnconfigure(1, weight=1, minsize=200) # Adjust minsize for column 1
111
+ self.root.grid_rowconfigure(2, weight=1) # Make row 2 expand with the window
112
+
113
+ @abc.abstractmethod
114
+ def listen_signal(self) -> int:
115
+ ...
116
+
117
+ def register_action(self, action: Action, signal: int):
118
+ self.actions.setdefault(signal, []).append(action)
119
+
120
+ # Update the dropdown with all registered actions.
121
+ all_actions = [f"Signal {signal} - {action.name}" for signal, actions in self.actions.items() for action in actions]
122
+ action_dropdown = self.layout['action_dropdown']
123
+ action_dropdown["values"] = all_actions
124
+
125
+ def mock_action(self, action: Action, timeout: float = 3.):
126
+ """Mock the execution of an action on a transparent test ground."""
127
+ self.root.iconify()
128
+
129
+ # Create a borderless, transparent test ground window
130
+ testground = tkinter.Toplevel(self.root)
131
+ # testground.overrideredirect(True) # Remove all window borders and decorations
132
+
133
+ # Get screen dimensions and set the testground window to cover the entire screen
134
+ # screen_width = self.root.winfo_screenwidth()
135
+ # screen_height = self.root.winfo_screenheight()
136
+ # testground.geometry(f"{screen_width}x{screen_height}+0+0")
137
+
138
+ # Make the window semi-transparent and ensure it stays on top
139
+ testground.attributes("-alpha", 0.5) # Set transparency to 50%
140
+ testground.attributes("-fullscreen", True)
141
+ testground.attributes("-topmost", True) # Ensure it stays above other windows
142
+
143
+ # Force the window manager to render the window completely
144
+ testground.lift() # Bring it to the front
145
+ testground.focus_force() # Force focus to this window
146
+
147
+ # Add a label to display input history
148
+ input_history = tkinter.Listbox(testground, font=("Courier", 14))
149
+ input_history.pack(fill="both", expand=True, padx=20, pady=20)
150
+ input_history.insert("active", f"Mocking {action.name}")
151
+ testground.update_idletasks()
152
+
153
+ # Simulate action execution
154
+ for step in action.steps:
155
+ # Display the step name in the input history
156
+ input_history.insert("end", f"Executing Step: {step['name']}")
157
+ input_history.insert("end", f"Comments: {step.get('comments', 'No comments')}")
158
+ input_history.insert("end", "-" * 50)
159
+ input_history.see("end")
160
+ testground.update_idletasks()
161
+
162
+ # Simulate mouse and keyboard inputs
163
+ procedure = step['procedure']
164
+ args = step.get('args', [])
165
+ kwargs = step.get('kwargs', {})
166
+ try:
167
+ procedure(*args, **kwargs)
168
+ except Exception as e:
169
+ input_history.insert("end", f"Error: {str(e)}")
170
+ input_history.see("end")
171
+
172
+ # Simulate delay between steps
173
+
174
+ # Wait 3 seconds, then close the test ground and restore the client window
175
+ time.sleep(timeout)
176
+ testground.destroy()
177
+ self.root.deiconify()
178
+
179
+ def toggle_auto_work(self):
180
+ """Toggle the daemon thread to start or stop auto work."""
181
+ if not self.running:
182
+ self.running = True
183
+ self.layout['button_takeover'].config(text="Release Control")
184
+ self.worker_thread = Thread(target=self.auto_work, daemon=True)
185
+ self.worker_thread.start()
186
+ else:
187
+ self.running = False
188
+ self.layout['button_takeover'].config(text="Takeover Input")
189
+
190
+ def toggle_mock(self):
191
+ """Mock the action selected in the dropdown."""
192
+ selected_name = self.layout['action_selected'].get()
193
+ if not selected_name:
194
+ LOGGER.warning("No action selected to mock!")
195
+ return
196
+
197
+ selected_action = None
198
+ for signal, actions in self.actions.items():
199
+ for action in actions:
200
+ if f"Signal {signal} - {action.name}" == selected_name:
201
+ selected_action = action
202
+ break
203
+
204
+ if selected_action is None:
205
+ LOGGER.warning(f"Action '{selected_name}' not found!")
206
+ return
207
+
208
+ self.mock_action(action=selected_action)
209
+
210
+ def auto_work(self):
211
+ """Generate data for the table in a loop."""
212
+ while self.running:
213
+ signal = self.listen_signal()
214
+ actions = self.actions.get(signal, [])
215
+
216
+ # Update table for the current signal
217
+ self.update_table(actions)
218
+
219
+ for action_idx, action in enumerate(actions):
220
+ if not self.running:
221
+ break
222
+
223
+ # Update action status to "Executing"
224
+ self.update_status(parent_id=action_idx, status="Executing")
225
+ action_done = False
226
+
227
+ for step_idx, step in enumerate(action.steps):
228
+ if not self.running:
229
+ break
230
+
231
+ # Update step status to "Executing"
232
+ self.update_status(parent_id=action_idx, child_id=step_idx, status="Executing")
233
+
234
+ # Execute the step
235
+ step["procedure"](*step.get("args", []), **step.get("kwargs", {}))
236
+
237
+ # Mark step as "Done"
238
+ self.update_status(parent_id=action_idx, child_id=step_idx, status="Done")
239
+ else:
240
+ # Mark action as "Done" after all steps
241
+ self.update_status(parent_id=action_idx, status="Done")
242
+ action_done = True
243
+
244
+ if not action_done:
245
+ self.update_status(parent_id=action_idx, status="Stopped")
246
+
247
+ def update_table(self, actions: list[Action]):
248
+ """Render the table based on the provided actions."""
249
+ action_table = self.layout['action_table']
250
+ action_table.delete(*action_table.get_children()) # Clear existing rows
251
+
252
+ for action_idx, action in enumerate(actions):
253
+ prefix = chr(0x250C)
254
+ # Insert the parent row
255
+ parent_id = action_table.insert(
256
+ "", "end", iid=action_idx, values=(f"{prefix} {action.name}", "Pending", "")
257
+ )
258
+
259
+ # Insert child rows for steps
260
+ for step_idx, step in enumerate(action.steps):
261
+ prefix = f'{chr(0x251C) if step_idx < len(action.steps) - 1 else chr(0x2514)}{chr(0x2500)}'
262
+ action_table.insert(
263
+ parent_id,
264
+ "end",
265
+ iid=f"{action_idx}-{step_idx}",
266
+ text=f"Step {step_idx + 1}",
267
+ values=(f"{prefix} Step {step_idx + 1}", "Pending", step.get("comments", "")),
268
+ )
269
+
270
+ # Expand parent row by default
271
+ action_table.item(parent_id, open=True)
272
+
273
+ # Auto-adjust column widths
274
+ self.adjust_column_widths()
275
+
276
+ def update_status(self, parent_id: int, child_id: int = None, status: str = "Pending"):
277
+ """Update the status of the specified row."""
278
+ row_id = f"{parent_id}" if child_id is None else f"{parent_id}-{child_id}"
279
+ action_table = self.layout['action_table']
280
+ values = action_table.item(row_id, "values")
281
+
282
+ # Select the row if the status is "Executing"
283
+ match status:
284
+ case "Executing":
285
+ action_table.selection_set(row_id)
286
+ action_table.see(row_id) # Ensure the row is visible
287
+
288
+ action_table.item(row_id, values=(values[0], status, values[2]))
289
+
290
+ def adjust_column_widths(self):
291
+ """Automatically adjust column widths based on content."""
292
+ action_table = self.layout['action_table']
293
+ action_table.column("#0", width=30, minwidth=30, stretch=False)
294
+
295
+ for col in action_table["columns"]:
296
+ max_width = max(
297
+ [len(str(action_table.set(item, col))) for item in action_table.get_children()] + [len(col)]
298
+ )
299
+ action_table.column(col, width=max_width * 10, minwidth=30, stretch=True) # Scale width for readability
300
+
301
+ def record_action(self):
302
+ """Capture and log mouse and keyboard events in the test ground."""
303
+ self.root.iconify()
304
+
305
+ # Create a semi-transparent test ground window
306
+ testground = tkinter.Toplevel(self.root)
307
+ testground.attributes("-alpha", 0.5) # Set transparency
308
+ testground.attributes("-fullscreen", True)
309
+ testground.attributes("-topmost", True)
310
+ testground.lift()
311
+ testground.focus_force()
312
+
313
+ # Listbox to display recorded events
314
+ event_log = tkinter.Listbox(testground, font=("Courier", 14), selectmode="none", state=tkinter.DISABLED)
315
+ # event_log.bindtags((event_log, self.root, "all"))
316
+ event_log.pack(fill="both", expand=True, padx=0, pady=0)
317
+
318
+ def log_event(event_type, event):
319
+ """Log the event details to the console and the event log."""
320
+ if event_type == "Key":
321
+ event_details = f"{event_type}: {event.keysym} (char: {event.char})"
322
+ else:
323
+ event_details = f"{event_type}: Button-{event.num} @ ({event.x}, {event.y})"
324
+ LOGGER.info(event_details)
325
+ event_log.configure(state=tkinter.NORMAL)
326
+ event_log.insert("end", event_details)
327
+ event_log.see("end")
328
+ event_log.configure(state=tkinter.DISABLED)
329
+
330
+ # Bind events for mouse clicks, double-clicks, and keyboard inputs
331
+ testground.bind("<Button-1>", lambda e: log_event("Click", e))
332
+ testground.bind("<Double-1>", lambda e: log_event("Double Click", e))
333
+ testground.bind("<Button-3>", lambda e: log_event("Right Click", e))
334
+ testground.bind("<Key>", lambda e: log_event("Key", e))
335
+
336
+ # Exit recording when the ESC key is pressed
337
+ def exit_record(event):
338
+ if event.keysym == "Escape":
339
+ LOGGER.info("Exiting recording mode.")
340
+ testground.destroy()
341
+ self.root.deiconify()
342
+
343
+ testground.bind("<Escape>", exit_record)
344
+
345
+ class ExampleClient(AutoWorkClient):
346
+ """An example implementation of the AutoWorkClient."""
347
+ dummy_signal = 0
348
+ is_init = False
349
+
350
+ @staticmethod
351
+ def dummy_action(name: str, msg: str) -> None:
352
+ LOGGER.info(f'start working on {name}, {msg}')
353
+ time.sleep(2)
354
+ LOGGER.info('working completed!')
355
+
356
+ def listen_signal(self) -> int:
357
+ """Simulate listening for a signal (e.g., return random signal)."""
358
+ import random
359
+ if self.is_init:
360
+ delay = 5 + 5 * random.random()
361
+ time.sleep(delay)
362
+ self.dummy_signal += 1
363
+ self.is_init = True
364
+ return 1 + self.dummy_signal % 2
365
+
366
+
367
+ def main():
368
+ client = ExampleClient()
369
+
370
+ # Example usage: Register actions for signal 1
371
+ action1 = Action("Example Action 1")
372
+ action1.append(name='S1A1s1', action=ExampleClient.dummy_action, args=("Signal 1 Action 1", "Step 1"))
373
+ action1.append(name='S1A1s2', action=ExampleClient.dummy_action, args=("Signal 1 Action 1", "Step 2"))
374
+
375
+ action2 = Action("Example Action 2")
376
+ action2.append(name='S1A2s1', action=ExampleClient.dummy_action, args=("Signal 1 Action 2", "Step 1"))
377
+ action2.append(name='S1A2s2', action=ExampleClient.dummy_action, args=("Signal 1 Action 2", "Step 2"))
378
+
379
+ client.register_action(action1, signal=1)
380
+ client.register_action(action2, signal=1)
381
+
382
+ action3 = Action("Example Action 3")
383
+ action3.append(name='S2A1s1', action=ExampleClient.dummy_action, args=("Signal 2 Action 1", "Step 1"))
384
+ action3.append(name='S2A1s2', action=ExampleClient.dummy_action, args=("Signal 2 Action 1", "Step 2"))
385
+ action3.append(name='S2A1s3', action=ExampleClient.dummy_action, args=("Signal 2 Action 1", "Step 3"))
386
+
387
+ client.register_action(action3, signal=2)
388
+
389
+ client.root.mainloop()
390
+
391
+
392
+ if __name__ == "__main__":
393
+ t = Thread(target=main, daemon=False)
394
+ t.start()
@@ -1,253 +0,0 @@
1
- __package__ = 'algo_engine.apps.sim_input'
2
-
3
- import abc
4
- import threading
5
- import time
6
- import tkinter
7
- from collections.abc import Callable, Iterable
8
- from tkinter import ttk
9
- from typing import TypedDict, NotRequired, Any
10
-
11
- from . import LOGGER
12
-
13
- LOGGER.getChild('Client')
14
-
15
-
16
- class Action(object):
17
- class Step(TypedDict):
18
- name: str
19
- procedure: Callable
20
- args: NotRequired[Iterable]
21
- kwargs: NotRequired[dict[str, Any]]
22
- comments: NotRequired[str]
23
-
24
- def __init__(self, name: str):
25
- self.name = name
26
- self.steps: list[Action.Step] = []
27
-
28
- def append(self, name: str, action: Callable[[..., ...], None], args=None, kwargs=None, comments: str = None) -> None:
29
- step = Action.Step(name=name, procedure=action)
30
-
31
- if args is not None:
32
- step['args'] = args
33
-
34
- if kwargs is not None:
35
- step['kwargs'] = kwargs
36
-
37
- if comments is not None:
38
- step['comments'] = comments
39
-
40
- self.steps.append(step)
41
-
42
- def __call__(self, ignore_error: bool = False) -> None:
43
- n = len(self.steps)
44
- for i, step in enumerate(self.steps, start=1):
45
- procedure = step['procedure']
46
- args = step.get('args', [])
47
- kwargs = step.get('kwargs', {})
48
- comments = step.get('comments', '')
49
- try:
50
- procedure(*args, **kwargs)
51
- except Exception as e:
52
- LOGGER.error(f'<Action {self.name}> <step {comments}>({i} / {n}) Failed!')
53
-
54
- if not ignore_error:
55
- raise e
56
-
57
- LOGGER.debug(f'<Action {self.name}> <step {comments}>({i} / {n}) Completed!')
58
-
59
- LOGGER.info(f'<Action {self.name}> completed!')
60
-
61
- def __len__(self):
62
- return len(self.steps)
63
-
64
-
65
- class AutoWorkClient(object, metaclass=abc.ABCMeta):
66
- def __init__(self, root=None):
67
- self.root = root if root is not None else tkinter.Tk()
68
- self.root.title("Structured GUI Client")
69
- self.root.geometry("600x500")
70
-
71
- # State variables
72
- self.worker_thread = None
73
- self.running = False
74
-
75
- # Create button
76
- self.button = ttk.Button(root, text="Takeover Input", command=self.toggle_auto_work)
77
- self.button.pack(pady=10)
78
-
79
- # Create table
80
- self.table = ttk.Treeview(
81
- root,
82
- columns=("Action", "Status", "Comments"),
83
- show="tree headings",
84
- height=20
85
- )
86
- self.table.heading("Action", text="Action")
87
- self.table.heading("Status", text="Status")
88
- self.table.heading("Comments", text="Comments")
89
- self.table.pack(padx=10, pady=10, fill="both", expand=True)
90
-
91
- self.actions: dict[int, list[Action]] = {}
92
-
93
- @abc.abstractmethod
94
- def listen_signal(self) -> int:
95
- ...
96
-
97
- def register_action(self, action: Action, signal: int):
98
- if signal in self.actions:
99
- self.actions[signal].append(action)
100
- else:
101
- self.actions[signal] = [action]
102
-
103
- def toggle_auto_work(self):
104
- """Toggle the daemon thread to start or stop auto work."""
105
- if not self.running:
106
- self.running = True
107
- self.button.config(text="Release Control")
108
- self.worker_thread = threading.Thread(target=self.auto_work, daemon=True)
109
- self.worker_thread.start()
110
- else:
111
- self.running = False
112
- self.button.config(text="Takeover Input")
113
-
114
- def auto_work(self):
115
- """Generate data for the table in a loop."""
116
- while self.running:
117
- signal = self.listen_signal()
118
- actions = self.actions.get(signal, [])
119
-
120
- # Update table for the current signal
121
- self.update_table(actions)
122
-
123
- for action_idx, action in enumerate(actions):
124
- if not self.running:
125
- break
126
-
127
- # Update action status to "Executing"
128
- self.update_status(parent_id=action_idx, status="Executing")
129
- action_done = False
130
-
131
- for step_idx, step in enumerate(action.steps):
132
- if not self.running:
133
- break
134
-
135
- # Update step status to "Executing"
136
- self.update_status(parent_id=action_idx, child_id=step_idx, status="Executing")
137
-
138
- # Execute the step
139
- step["procedure"](*step.get("args", []), **step.get("kwargs", {}))
140
-
141
- # Mark step as "Done"
142
- self.update_status(parent_id=action_idx, child_id=step_idx, status="Done")
143
- else:
144
- # Mark action as "Done" after all steps
145
- self.update_status(parent_id=action_idx, status="Done")
146
- action_done = True
147
-
148
- if not action_done:
149
- self.update_status(parent_id=action_idx, status="Stopped")
150
-
151
- def update_table(self, actions: list[Action]):
152
- """Render the table based on the provided actions."""
153
- self.table.delete(*self.table.get_children()) # Clear existing rows
154
-
155
- for action_idx, action in enumerate(actions):
156
- prefix = chr(0x250C)
157
- # Insert the parent row
158
- parent_id = self.table.insert(
159
- "", "end", iid=action_idx, values=(f"{prefix} {action.name}", "Pending", "")
160
- )
161
-
162
- # Insert child rows for steps
163
- for step_idx, step in enumerate(action.steps):
164
- prefix = f'{chr(0x251C) if step_idx < len(action.steps) - 1 else chr(0x2514)}{chr(0x2500)}'
165
- self.table.insert(
166
- parent_id,
167
- "end",
168
- iid=f"{action_idx}-{step_idx}",
169
- text=f"Step {step_idx + 1}",
170
- values=(f"{prefix} Step {step_idx + 1}", "Pending", step.get("comments", "")),
171
- )
172
-
173
- # Expand parent row by default
174
- self.table.item(parent_id, open=True)
175
-
176
- # Auto-adjust column widths
177
- self.adjust_column_widths()
178
-
179
- def update_status(self, parent_id: int, child_id: int = None, status: str = "Pending"):
180
- """Update the status of the specified row."""
181
- row_id = f"{parent_id}" if child_id is None else f"{parent_id}-{child_id}"
182
- values = self.table.item(row_id, "values")
183
-
184
- # Select the row if the status is "Executing"
185
- match status:
186
- case "Executing":
187
- self.table.selection_set(row_id)
188
- self.table.see(row_id) # Ensure the row is visible
189
-
190
- self.table.item(row_id, values=(values[0], status, values[2]))
191
-
192
- def adjust_column_widths(self):
193
- """Automatically adjust column widths based on content."""
194
-
195
- self.table.column("#0", width=30, minwidth=30, stretch=False)
196
-
197
- for col in self.table["columns"]:
198
- max_width = max(
199
- [len(str(self.table.set(item, col))) for item in self.table.get_children()] + [len(col)]
200
- )
201
- self.table.column(col, width=max_width * 10, minwidth=30, stretch=True) # Scale width for readability
202
-
203
-
204
- class ExampleClient(AutoWorkClient):
205
- """An example implementation of the AutoWorkClient."""
206
- dummy_signal = 0
207
- is_init = False
208
-
209
- @staticmethod
210
- def dummy_action(name: str, msg: str) -> None:
211
- LOGGER.info(f'start working on {name}, {msg}')
212
- time.sleep(2)
213
- LOGGER.info('working completed!')
214
-
215
- def listen_signal(self) -> int:
216
- """Simulate listening for a signal (e.g., return random signal)."""
217
- import random
218
- if self.is_init:
219
- delay = 5 + 5 * random.random()
220
- time.sleep(delay)
221
- self.dummy_signal += 1
222
- self.is_init = True
223
- return 1 + self.dummy_signal % 2
224
-
225
-
226
- def main():
227
- client = ExampleClient()
228
-
229
- # Example usage: Register actions for signal 1
230
- action1 = Action("Example Action 1")
231
- action1.append(name='S1A1s1', action=ExampleClient.dummy_action, args=("Signal 1 Action 1", "Step 1"))
232
- action1.append(name='S1A1s2', action=ExampleClient.dummy_action, args=("Signal 1 Action 1", "Step 2"))
233
-
234
- action2 = Action("Example Action 2")
235
- action2.append(name='S1A2s1', action=ExampleClient.dummy_action, args=("Signal 1 Action 2", "Step 1"))
236
- action2.append(name='S1A2s2', action=ExampleClient.dummy_action, args=("Signal 1 Action 2", "Step 2"))
237
-
238
- client.register_action(action1, signal=1)
239
- client.register_action(action2, signal=1)
240
-
241
- action3 = Action("Example Action 3")
242
- action3.append(name='S2A1s1', action=ExampleClient.dummy_action, args=("Signal 2 Action 1", "Step 1"))
243
- action3.append(name='S2A1s2', action=ExampleClient.dummy_action, args=("Signal 2 Action 1", "Step 2"))
244
- action3.append(name='S2A1s3', action=ExampleClient.dummy_action, args=("Signal 2 Action 1", "Step 3"))
245
-
246
- client.register_action(action3, signal=2)
247
-
248
- client.root.mainloop()
249
-
250
-
251
- if __name__ == "__main__":
252
- t = threading.Thread(target=main, daemon=False)
253
- t.start()
File without changes
File without changes
File without changes
File without changes