PyAlgoEngine 0.7.5.post2__tar.gz → 0.7.6.post1__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 (53) hide show
  1. {pyalgoengine-0.7.5.post2 → pyalgoengine-0.7.6.post1}/PKG-INFO +1 -1
  2. {pyalgoengine-0.7.5.post2 → pyalgoengine-0.7.6.post1}/PyAlgoEngine.egg-info/PKG-INFO +1 -1
  3. {pyalgoengine-0.7.5.post2 → pyalgoengine-0.7.6.post1}/PyAlgoEngine.egg-info/SOURCES.txt +5 -0
  4. {pyalgoengine-0.7.5.post2 → pyalgoengine-0.7.6.post1}/algo_engine/__init__.py +1 -1
  5. pyalgoengine-0.7.6.post1/algo_engine/apps/sim_input/__init__.py +23 -0
  6. pyalgoengine-0.7.6.post1/algo_engine/apps/sim_input/client.py +244 -0
  7. pyalgoengine-0.7.6.post1/algo_engine/apps/sim_input/sim_keyboard.py +88 -0
  8. pyalgoengine-0.7.6.post1/algo_engine/apps/sim_input/sim_mouse.py +137 -0
  9. pyalgoengine-0.7.6.post1/algo_engine/apps/sim_input/window.py +162 -0
  10. {pyalgoengine-0.7.5.post2 → pyalgoengine-0.7.6.post1}/LICENSE +0 -0
  11. {pyalgoengine-0.7.5.post2 → pyalgoengine-0.7.6.post1}/PyAlgoEngine.egg-info/dependency_links.txt +0 -0
  12. {pyalgoengine-0.7.5.post2 → pyalgoengine-0.7.6.post1}/PyAlgoEngine.egg-info/requires.txt +0 -0
  13. {pyalgoengine-0.7.5.post2 → pyalgoengine-0.7.6.post1}/PyAlgoEngine.egg-info/top_level.txt +0 -0
  14. {pyalgoengine-0.7.5.post2 → pyalgoengine-0.7.6.post1}/README.md +0 -0
  15. {pyalgoengine-0.7.5.post2 → pyalgoengine-0.7.6.post1}/algo_engine/apps/__init__.py +0 -0
  16. {pyalgoengine-0.7.5.post2 → pyalgoengine-0.7.6.post1}/algo_engine/apps/backtest/__init__.py +0 -0
  17. {pyalgoengine-0.7.5.post2 → pyalgoengine-0.7.6.post1}/algo_engine/apps/backtest/doc_server.py +0 -0
  18. {pyalgoengine-0.7.5.post2 → pyalgoengine-0.7.6.post1}/algo_engine/apps/backtest/tester.py +0 -0
  19. {pyalgoengine-0.7.5.post2 → pyalgoengine-0.7.6.post1}/algo_engine/apps/backtest/web_app.py +0 -0
  20. {pyalgoengine-0.7.5.post2 → pyalgoengine-0.7.6.post1}/algo_engine/apps/bokeh_server.py +0 -0
  21. {pyalgoengine-0.7.5.post2 → pyalgoengine-0.7.6.post1}/algo_engine/apps/demo/__init__.py +0 -0
  22. {pyalgoengine-0.7.5.post2 → pyalgoengine-0.7.6.post1}/algo_engine/apps/demo/test.py +0 -0
  23. {pyalgoengine-0.7.5.post2 → pyalgoengine-0.7.6.post1}/algo_engine/backtest/__init__.py +0 -0
  24. {pyalgoengine-0.7.5.post2 → pyalgoengine-0.7.6.post1}/algo_engine/backtest/__main__.py +0 -0
  25. {pyalgoengine-0.7.5.post2 → pyalgoengine-0.7.6.post1}/algo_engine/backtest/metrics.py +0 -0
  26. {pyalgoengine-0.7.5.post2 → pyalgoengine-0.7.6.post1}/algo_engine/backtest/replay.py +0 -0
  27. {pyalgoengine-0.7.5.post2 → pyalgoengine-0.7.6.post1}/algo_engine/backtest/sim_match.py +0 -0
  28. {pyalgoengine-0.7.5.post2 → pyalgoengine-0.7.6.post1}/algo_engine/base/__init__.py +0 -0
  29. {pyalgoengine-0.7.5.post2 → pyalgoengine-0.7.6.post1}/algo_engine/base/console_utils.py +0 -0
  30. {pyalgoengine-0.7.5.post2 → pyalgoengine-0.7.6.post1}/algo_engine/base/finance_decimal.py +0 -0
  31. {pyalgoengine-0.7.5.post2 → pyalgoengine-0.7.6.post1}/algo_engine/base/market_buffer.py +0 -0
  32. {pyalgoengine-0.7.5.post2 → pyalgoengine-0.7.6.post1}/algo_engine/base/market_utils.py +0 -0
  33. {pyalgoengine-0.7.5.post2 → pyalgoengine-0.7.6.post1}/algo_engine/base/market_utils_nt.py +0 -0
  34. {pyalgoengine-0.7.5.post2 → pyalgoengine-0.7.6.post1}/algo_engine/base/market_utils_posix.py +0 -0
  35. {pyalgoengine-0.7.5.post2 → pyalgoengine-0.7.6.post1}/algo_engine/base/technical_analysis.py +0 -0
  36. {pyalgoengine-0.7.5.post2 → pyalgoengine-0.7.6.post1}/algo_engine/base/telemetrics.py +0 -0
  37. {pyalgoengine-0.7.5.post2 → pyalgoengine-0.7.6.post1}/algo_engine/base/trade_utils.py +0 -0
  38. {pyalgoengine-0.7.5.post2 → pyalgoengine-0.7.6.post1}/algo_engine/engine/__init__.py +0 -0
  39. {pyalgoengine-0.7.5.post2 → pyalgoengine-0.7.6.post1}/algo_engine/engine/algo_engine.py +0 -0
  40. {pyalgoengine-0.7.5.post2 → pyalgoengine-0.7.6.post1}/algo_engine/engine/event_engine.py +0 -0
  41. {pyalgoengine-0.7.5.post2 → pyalgoengine-0.7.6.post1}/algo_engine/engine/market_engine.py +0 -0
  42. {pyalgoengine-0.7.5.post2 → pyalgoengine-0.7.6.post1}/algo_engine/engine/trade_engine.py +0 -0
  43. {pyalgoengine-0.7.5.post2 → pyalgoengine-0.7.6.post1}/algo_engine/monitor/__init__.py +0 -0
  44. {pyalgoengine-0.7.5.post2 → pyalgoengine-0.7.6.post1}/algo_engine/monitor/advanced_data_interface.py +0 -0
  45. {pyalgoengine-0.7.5.post2 → pyalgoengine-0.7.6.post1}/algo_engine/profile/__init__.py +0 -0
  46. {pyalgoengine-0.7.5.post2 → pyalgoengine-0.7.6.post1}/algo_engine/profile/cn.py +0 -0
  47. {pyalgoengine-0.7.5.post2 → pyalgoengine-0.7.6.post1}/algo_engine/strategy/__init__.py +0 -0
  48. {pyalgoengine-0.7.5.post2 → pyalgoengine-0.7.6.post1}/algo_engine/strategy/strategy_engine.py +0 -0
  49. {pyalgoengine-0.7.5.post2 → pyalgoengine-0.7.6.post1}/algo_engine/utils/__init__.py +0 -0
  50. {pyalgoengine-0.7.5.post2 → pyalgoengine-0.7.6.post1}/algo_engine/utils/commit_regularizer.py +0 -0
  51. {pyalgoengine-0.7.5.post2 → pyalgoengine-0.7.6.post1}/algo_engine/utils/data_utils.py +0 -0
  52. {pyalgoengine-0.7.5.post2 → pyalgoengine-0.7.6.post1}/setup.cfg +0 -0
  53. {pyalgoengine-0.7.5.post2 → pyalgoengine-0.7.6.post1}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: PyAlgoEngine
3
- Version: 0.7.5.post2
3
+ Version: 0.7.6.post1
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.5.post2
3
+ Version: 0.7.6.post1
4
4
  Summary: Basic algo engine
5
5
  Home-page: https://github.com/BolunHan/PyAlgoEngine
6
6
  Author: Bolun.Han
@@ -15,6 +15,11 @@ algo_engine/apps/backtest/tester.py
15
15
  algo_engine/apps/backtest/web_app.py
16
16
  algo_engine/apps/demo/__init__.py
17
17
  algo_engine/apps/demo/test.py
18
+ algo_engine/apps/sim_input/__init__.py
19
+ algo_engine/apps/sim_input/client.py
20
+ algo_engine/apps/sim_input/sim_keyboard.py
21
+ algo_engine/apps/sim_input/sim_mouse.py
22
+ algo_engine/apps/sim_input/window.py
18
23
  algo_engine/backtest/__init__.py
19
24
  algo_engine/backtest/__main__.py
20
25
  algo_engine/backtest/metrics.py
@@ -1,4 +1,4 @@
1
- __version__ = "0.7.5.post2"
1
+ __version__ = "0.7.6.post1"
2
2
 
3
3
  import logging
4
4
  import os
@@ -0,0 +1,23 @@
1
+ import platform
2
+
3
+ from .. import LOGGER
4
+
5
+
6
+ def check_windows_version(min_version=(6, 1)):
7
+ """
8
+ Ensure the script is running on a compatible Windows version.
9
+ :param min_version: Minimum required version as a tuple (major, minor).
10
+ Default is (6, 1) for Windows 7 (major=6, minor=1).
11
+ """
12
+ # Get the Windows version
13
+ if not platform.system() == "Windows":
14
+ raise EnvironmentError("This script only works on Windows.")
15
+
16
+ version_str = platform.version()
17
+ version_tuple = tuple(map(int, platform.win32_ver()[1].split('.')))
18
+
19
+ if version_tuple < min_version:
20
+ raise EnvironmentError(f"Unsupported Windows version: {version_str}. Minimum required is {min_version[0]}.{min_version[1]}.")
21
+
22
+
23
+ LOGGER = LOGGER.getChild('SimInput')
@@ -0,0 +1,244 @@
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
+ # self.style = ttk.Style()
94
+ # self.style.theme_use("vista") # Choose a modern theme like 'clam', 'vista', or 'alt'
95
+
96
+ @abc.abstractmethod
97
+ def listen_signal(self) -> int:
98
+ ...
99
+
100
+ def register_action(self, action: Action, signal: int):
101
+ if signal in self.actions:
102
+ self.actions[signal].append(action)
103
+ else:
104
+ self.actions[signal] = [action]
105
+
106
+ def toggle_auto_work(self):
107
+ """Toggle the daemon thread to start or stop auto work."""
108
+ if not self.running:
109
+ self.running = True
110
+ self.button.config(text="Release Control")
111
+ self.worker_thread = threading.Thread(target=self.auto_work, daemon=True)
112
+ self.worker_thread.start()
113
+ else:
114
+ self.running = False
115
+ self.button.config(text="Takeover Input")
116
+
117
+ def auto_work(self):
118
+ """Generate data for the table in a loop."""
119
+ while self.running:
120
+ signal = self.listen_signal()
121
+ actions = self.actions.get(signal, [])
122
+
123
+ # Update table for the current signal
124
+ self.update_table(actions)
125
+
126
+ for action_idx, action in enumerate(actions):
127
+ # Update action status to "Executing"
128
+ self.update_status(parent_id=action_idx, status="Executing")
129
+
130
+ for step_idx, step in enumerate(action.steps):
131
+ # Update step status to "Executing"
132
+ self.update_status(parent_id=action_idx, child_id=step_idx, status="Executing")
133
+
134
+ # Execute the step
135
+ step["procedure"](*step.get("args", []), **step.get("kwargs", {}))
136
+
137
+ # Mark step as "Done"
138
+ self.update_status(parent_id=action_idx, child_id=step_idx, status="Done")
139
+
140
+ # Mark action as "Done" after all steps
141
+ self.update_status(parent_id=action_idx, status="Done")
142
+
143
+ def update_table(self, actions: list[Action]):
144
+ """Render the table based on the provided actions."""
145
+ self.table.delete(*self.table.get_children()) # Clear existing rows
146
+
147
+ for action_idx, action in enumerate(actions):
148
+ prefix = chr(0x250C)
149
+ # Insert the parent row
150
+ parent_id = self.table.insert(
151
+ "", "end", iid=action_idx, values=(f"{prefix} {action.name}", "Pending", "")
152
+ )
153
+
154
+ # Insert child rows for steps
155
+ for step_idx, step in enumerate(action.steps):
156
+ prefix = f'{chr(0x251C) if step_idx < len(action.steps) - 1 else chr(0x2514)}{chr(0x2500)}'
157
+ self.table.insert(
158
+ parent_id,
159
+ "end",
160
+ iid=f"{action_idx}-{step_idx}",
161
+ text=f"Step {step_idx + 1}",
162
+ values=(f"{prefix} Step {step_idx + 1}", "Pending", step.get("comments", "")),
163
+ )
164
+
165
+ # Expand parent row by default
166
+ self.table.item(parent_id, open=True)
167
+
168
+ # Auto-adjust column widths
169
+ self.adjust_column_widths()
170
+
171
+ def update_status(self, parent_id: int, child_id: int = None, status: str = "Pending"):
172
+ """Update the status of the specified row."""
173
+ row_id = f"{parent_id}" if child_id is None else f"{parent_id}-{child_id}"
174
+ values = self.table.item(row_id, "values")
175
+
176
+ # Select the row if the status is "Executing"
177
+ match status:
178
+ case "Executing":
179
+ self.table.selection_set(row_id)
180
+ self.table.see(row_id) # Ensure the row is visible
181
+
182
+ self.table.item(row_id, values=(values[0], status, values[2]))
183
+
184
+ def adjust_column_widths(self):
185
+ """Automatically adjust column widths based on content."""
186
+
187
+ self.table.column("#0", width=30, minwidth=30, stretch=False)
188
+
189
+ for col in self.table["columns"]:
190
+ max_width = max(
191
+ [len(str(self.table.set(item, col))) for item in self.table.get_children()] + [len(col)]
192
+ )
193
+ self.table.column(col, width=max_width * 10, minwidth=30, stretch=True) # Scale width for readability
194
+
195
+
196
+ class ExampleClient(AutoWorkClient):
197
+ """An example implementation of the AutoWorkClient."""
198
+ dummy_signal = 0
199
+ is_init = False
200
+
201
+ @staticmethod
202
+ def dummy_action(name: str, msg: str) -> None:
203
+ LOGGER.info(f'start working on {name}, {msg}')
204
+ time.sleep(2)
205
+ LOGGER.info('working completed!')
206
+
207
+ def listen_signal(self) -> int:
208
+ """Simulate listening for a signal (e.g., return random signal)."""
209
+ import random
210
+ if self.is_init:
211
+ delay = 5 + 5 * random.random()
212
+ time.sleep(delay)
213
+ self.dummy_signal += 1
214
+ self.is_init = True
215
+ return 1 + self.dummy_signal % 2
216
+
217
+
218
+ def main():
219
+ client = ExampleClient()
220
+
221
+ # Example usage: Register actions for signal 1
222
+ action1 = Action("Example Action 1")
223
+ action1.append(name='S1A1s1', action=ExampleClient.dummy_action, args=("Signal 1 Action 1", "Step 1"))
224
+ action1.append(name='S1A1s2', action=ExampleClient.dummy_action, args=("Signal 1 Action 1", "Step 2"))
225
+
226
+ action2 = Action("Example Action 2")
227
+ action2.append(name='S1A2s1', action=ExampleClient.dummy_action, args=("Signal 1 Action 2", "Step 1"))
228
+ action2.append(name='S1A2s2', action=ExampleClient.dummy_action, args=("Signal 1 Action 2", "Step 2"))
229
+
230
+ client.register_action(action1, signal=1)
231
+ client.register_action(action2, signal=1)
232
+
233
+ action3 = Action("Example Action 3")
234
+ action3.append(name='S2A1s1', action=ExampleClient.dummy_action, args=("Signal 2 Action 1", "Step 1"))
235
+ action3.append(name='S2A1s2', action=ExampleClient.dummy_action, args=("Signal 2 Action 1", "Step 2"))
236
+ action3.append(name='S2A1s3', action=ExampleClient.dummy_action, args=("Signal 2 Action 1", "Step 3"))
237
+
238
+ client.register_action(action3, signal=2)
239
+
240
+ client.root.mainloop()
241
+
242
+
243
+ if __name__ == "__main__":
244
+ main()
@@ -0,0 +1,88 @@
1
+ __package__ = 'algo_engine.apps.sim_input'
2
+
3
+ import ctypes
4
+ import enum
5
+ import time
6
+
7
+ from . import LOGGER, check_windows_version
8
+
9
+ check_windows_version((6, 1))
10
+ LOGGER.getChild('Keyboard')
11
+
12
+
13
+ # Constants for key event types
14
+ class KeyEvent(enum.IntEnum):
15
+ F_KEYDOWN = 0x0000
16
+ F_KEYUP = 0x0002
17
+
18
+
19
+ # Windows API functions
20
+ user32 = ctypes.windll.user32
21
+
22
+
23
+ # Map character to virtual key code and shift state
24
+ def get_keycode(char: str):
25
+ """
26
+ Get the virtual key code (VK_CODE) and shift state for a given character.
27
+
28
+ Returns:
29
+ tuple: (VK_CODE, needs_shift)
30
+ """
31
+ vk_combo = user32.VkKeyScanW(ord(char))
32
+ vk_code = vk_combo & 0xFF # Lower byte is VK_CODE
33
+ shift_state = (vk_combo >> 8) & 0xFF # Higher byte indicates shift state
34
+ needs_shift = shift_state & 0x01 # Check if Shift key is required
35
+ return vk_code, needs_shift
36
+
37
+
38
+ # Simulate key press
39
+ def press_key(vk_code):
40
+ """Press a key using its VK_CODE."""
41
+ ctypes.windll.user32.keybd_event(vk_code, 0, KeyEvent.F_KEYDOWN, 0)
42
+
43
+
44
+ # Simulate key release
45
+ def release_key(vk_code):
46
+ """Release a key using its VK_CODE."""
47
+ ctypes.windll.user32.keybd_event(vk_code, 0, KeyEvent.F_KEYUP, 0)
48
+
49
+
50
+ # Advanced function to simulate a key press for a given duration
51
+ def simulate_keypress(char, ts=0.1):
52
+ """
53
+ Simulate pressing a key for a given duration.
54
+
55
+ Args:
56
+ char (str): The character to simulate key press for.
57
+ ts (float): The duration (in seconds) to hold the key. Default is 0.1s.
58
+ """
59
+ vk_code, needs_shift = get_keycode(char) # Get VK_CODE and shift state
60
+ if vk_code == 0xFF:
61
+ raise ValueError(f"Invalid character '{char}'. Cannot map to a virtual key code.")
62
+
63
+ # If the character needs Shift, press Shift first
64
+ if needs_shift:
65
+ press_key(0x10) # VK_CODE for Shift
66
+
67
+ press_key(vk_code) # Press the key
68
+ time.sleep(ts) # Hold the key for the given duration
69
+ release_key(vk_code) # Release the key
70
+
71
+ # If Shift was pressed, release it
72
+ if needs_shift:
73
+ release_key(0x10) # VK_CODE for Shift
74
+
75
+
76
+ def main():
77
+ LOGGER.info('Staring sim input sequence in 5 seconds...')
78
+ time.sleep(5)
79
+ key_strikes = list('hellow world!')
80
+ for char in key_strikes:
81
+ LOGGER.info(f'Pressing {char}...')
82
+ simulate_keypress(char=char, ts=.5)
83
+ time.sleep(.5)
84
+
85
+
86
+ # Example usage
87
+ if __name__ == "__main__":
88
+ main()
@@ -0,0 +1,137 @@
1
+ __package__ = 'algo_engine.apps.sim_input'
2
+
3
+ import ctypes
4
+ import enum
5
+ import time
6
+ from ctypes import wintypes
7
+ from typing import Literal
8
+
9
+ from . import LOGGER, check_windows_version
10
+
11
+ check_windows_version((6, 1))
12
+ LOGGER.getChild('Mouse')
13
+
14
+
15
+ # Constants for mouse events
16
+ class MouseEvent(enum.IntEnum):
17
+ MOUSEEVENTF_MOVE = 0x0001
18
+ MOUSEEVENTF_LEFTDOWN = 0x0002
19
+ MOUSEEVENTF_LEFTUP = 0x0004
20
+ MOUSEEVENTF_RIGHTDOWN = 0x0008
21
+ MOUSEEVENTF_RIGHTUP = 0x0010
22
+ MOUSEEVENTF_ABSOLUTE = 0x8000
23
+
24
+
25
+ # Structure to store point coordinates
26
+ class POINT(ctypes.Structure):
27
+ _fields_ = [("x", wintypes.LONG), ("y", wintypes.LONG)]
28
+
29
+
30
+ # Windows API functions
31
+ user32 = ctypes.windll.user32
32
+
33
+
34
+ # Get the current mouse location
35
+ def get_location() -> POINT:
36
+ """
37
+ Get the current mouse cursor position.
38
+
39
+ Returns:
40
+ tuple: (x, y) coordinates of the mouse cursor.
41
+ """
42
+ point = POINT()
43
+ user32.GetCursorPos(ctypes.byref(point))
44
+ return point
45
+
46
+
47
+ def move_mouse(x: int, y: int):
48
+ """
49
+ Move the mouse cursor to a specific screen location.
50
+
51
+ Args:
52
+ x (int): The target x-coordinate.
53
+ y (int): The target y-coordinate.
54
+ """
55
+ user32.SetCursorPos(x, y)
56
+
57
+
58
+ # Simulate mouse press
59
+ def press_mouse(button: Literal['l', 'r', 'left', 'right'] = 'l'):
60
+ """
61
+ Simulate a mouse button press.
62
+
63
+ Args:
64
+ button (str): The button to press ("left" or "right").
65
+ """
66
+ match button:
67
+ case 'l' | "left":
68
+ user32.mouse_event(MouseEvent.MOUSEEVENTF_LEFTDOWN, 0, 0, 0, 0)
69
+ case 'r' | "right":
70
+ user32.mouse_event(MouseEvent.MOUSEEVENTF_RIGHTDOWN, 0, 0, 0, 0)
71
+ case _:
72
+ raise ValueError("Invalid button. Use 'left' or 'right'.")
73
+
74
+
75
+ # Simulate mouse release
76
+ def release_mouse(button: Literal['l', 'r', 'left', 'right'] = 'l'):
77
+ """
78
+ Simulate a mouse button release.
79
+
80
+ Args:
81
+ button (str): The button to release ("left" or "right").
82
+ """
83
+
84
+ match button:
85
+ case 'l' | "left":
86
+ user32.mouse_event(MouseEvent.MOUSEEVENTF_LEFTUP, 0, 0, 0, 0)
87
+ case 'r' | "right":
88
+ user32.mouse_event(MouseEvent.MOUSEEVENTF_RIGHTUP, 0, 0, 0, 0)
89
+ case _:
90
+ raise ValueError("Invalid button. Use 'left' or 'right'.")
91
+
92
+
93
+ # Simulate a mouse click
94
+ def click_mouse(button: Literal['l', 'r', 'left', 'right'] = 'l'):
95
+ """
96
+ Simulate a mouse click (press + release).
97
+
98
+ Args:
99
+ button (str): The button to click ("left" or "right").
100
+ """
101
+ press_mouse(button)
102
+ release_mouse(button)
103
+
104
+
105
+ # Simulate a mouse double click
106
+ def double_click_mouse(button: Literal['l', 'r', 'left', 'right'] = 'l', interval: float = 0.1):
107
+ """
108
+ Simulate a mouse double click.
109
+
110
+ Args:
111
+ button (str): The button to double-click ("left" or "right").
112
+ interval (float): Time between clicks (in seconds). Default is 0.1s.
113
+ """
114
+ click_mouse(button)
115
+ time.sleep(interval)
116
+ click_mouse(button)
117
+
118
+
119
+ # Example usage
120
+ if __name__ == "__main__":
121
+ LOGGER.info('Staring sim mouse input sequence in 5 seconds...')
122
+ time.sleep(5)
123
+
124
+ p = get_location()
125
+ LOGGER.info(f"Current mouse location: {p.x, p.y}")
126
+
127
+ LOGGER.info('Move to diagonal location...')
128
+ move_mouse(x=p.y, y=p.x)
129
+
130
+ LOGGER.info(f"Simulating left click...")
131
+ click_mouse(button="l")
132
+
133
+ LOGGER.info(f"Simulating right click...")
134
+ click_mouse(button="r")
135
+
136
+ LOGGER.info("Simulating double click...")
137
+ double_click_mouse()
@@ -0,0 +1,162 @@
1
+ __package__ = 'algo_engine.apps.sim_input'
2
+
3
+ import ctypes
4
+ import dataclasses
5
+ import enum
6
+ import os
7
+ from typing import Literal
8
+
9
+ from . import LOGGER
10
+
11
+ LOGGER.getChild('Window')
12
+
13
+ # Define necessary Windows API functions and constants
14
+ user32 = ctypes.windll.user32
15
+ kernel32 = ctypes.windll.kernel32
16
+ psapi = ctypes.windll.psapi
17
+
18
+ # Define constants for access rights
19
+ PROCESS_QUERY_INFORMATION = 0x0400
20
+ PROCESS_VM_READ = 0x0010
21
+
22
+ # Define necessary Windows API function prototypes
23
+ WNDENUMPROC = ctypes.WINFUNCTYPE(ctypes.c_bool, ctypes.c_int, ctypes.POINTER(ctypes.c_int))
24
+
25
+
26
+ @dataclasses.dataclass(frozen=True)
27
+ class WindowInfo:
28
+ window_name: str
29
+ pid: int
30
+ hwnd: int
31
+ executable_name: str
32
+ executable_path: str
33
+
34
+
35
+ class WindowState(enum.IntEnum):
36
+ SW_SHOWNORMAL = 1 # Show the window normally
37
+ SW_SHOWMINIMIZED = 2 # Minimize the window
38
+ SW_SHOWMAXIMIZED = 3 # Maximize the window
39
+ SW_SHOWNOACTIVATE = 4 # Show the window without activating it
40
+ SW_SHOW = 5 # Show the window and bring it to the foreground
41
+
42
+
43
+ # Function to get PID from window handle
44
+ def get_pid(hwnd):
45
+ pid = ctypes.c_ulong()
46
+ user32.GetWindowThreadProcessId(hwnd, ctypes.byref(pid))
47
+ return pid.value
48
+
49
+
50
+ # Function to retrieve the executable path and name of a process
51
+ def get_executable_info(pid):
52
+ # Open the process to get information
53
+ h_process = kernel32.OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, False, pid)
54
+ if not h_process:
55
+ return None, None
56
+
57
+ # Buffer to hold the path
58
+ path_buffer = ctypes.create_unicode_buffer(1024)
59
+
60
+ # Get the full executable path using GetModuleFileNameEx
61
+ if psapi.GetModuleFileNameExW(h_process, 0, path_buffer, ctypes.byref(ctypes.c_ulong(1024))):
62
+ executable_path = path_buffer.value
63
+ executable_name = os.path.basename(executable_path)
64
+ else:
65
+ executable_path = None
66
+ executable_name = None
67
+
68
+ # Close the process handle
69
+ kernel32.CloseHandle(h_process)
70
+
71
+ return executable_name, executable_path
72
+
73
+
74
+ # Function to check if a window is visible (including minimized)
75
+ def is_window_visible(hwnd):
76
+ return user32.IsWindowVisible(hwnd)
77
+
78
+
79
+ # Function to enumerate windows
80
+ def get_windows() -> list[WindowInfo]:
81
+ windows = []
82
+
83
+ def enum_windows_proc(hwnd, _):
84
+ # Only include visible windows
85
+ if not is_window_visible(hwnd):
86
+ return True
87
+
88
+ # Get window title (for name)
89
+ length = user32.GetWindowTextLengthW(hwnd)
90
+ if length > 0:
91
+ buffer = ctypes.create_unicode_buffer(length + 1)
92
+ user32.GetWindowTextW(hwnd, buffer, length + 1)
93
+ window_name = buffer.value
94
+ else:
95
+ window_name = "Untitled"
96
+
97
+ # Get the PID for the window
98
+ pid = get_pid(hwnd)
99
+
100
+ # Get executable name and path
101
+ executable_name, executable_path = get_executable_info(pid)
102
+
103
+ # Append to the windows list as a tuple (window_name, pid, hwnd, executable_name, executable_path)
104
+ windows.append(WindowInfo(window_name=window_name, pid=pid, hwnd=hwnd, executable_name=executable_name, executable_path=executable_path))
105
+ return True
106
+
107
+ # Enumerate all windows (including child windows)
108
+ user32.EnumWindows(WNDENUMPROC(enum_windows_proc), 0)
109
+
110
+ return windows
111
+
112
+
113
+ def find_window(name: str = None, executable: str = None) -> list[WindowInfo]:
114
+ windows = get_windows()
115
+ matched = []
116
+
117
+ for window in windows:
118
+ if name is not None and name.lower() not in window.name.lower():
119
+ continue
120
+
121
+ if executable is not None and executable.lower() not in window.executable_name.lower():
122
+ continue
123
+
124
+ matched.append(window)
125
+
126
+ return matched
127
+
128
+
129
+ # Function to set the window action (top, maximize, minimize)
130
+ def set_window(window_info: WindowInfo, action: Literal['top', 'maximize', 'minimize', 'max', 'min']):
131
+ hwnd = window_info.hwnd
132
+
133
+ match action:
134
+ case "top":
135
+ # Bring the window to the front (top)
136
+ user32.ShowWindow(hwnd, WindowState.SW_SHOWNORMAL)
137
+ user32.SetForegroundWindow(hwnd)
138
+ case "maximize" | 'max':
139
+ # Maximize the window
140
+ user32.ShowWindow(hwnd, WindowState.SW_SHOWMAXIMIZED)
141
+ case "minimize" | 'min':
142
+ # Minimize the window
143
+ user32.ShowWindow(hwnd, WindowState.SW_SHOWMINIMIZED)
144
+ case _:
145
+ raise ValueError(f"Unknown action: {action}")
146
+
147
+
148
+ def main():
149
+ # Example usage
150
+ windows = get_windows()
151
+ for _ in windows:
152
+ LOGGER.debug(_)
153
+
154
+ firefox = find_window(executable="firefox")[0]
155
+ LOGGER.info(firefox)
156
+
157
+ set_window(window_info=firefox, action='top')
158
+ set_window(window_info=firefox, action='maximize')
159
+
160
+
161
+ if __name__ == "__main__":
162
+ main()