robotframework-desktop 0.1.0__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.
@@ -0,0 +1,180 @@
1
+ """Screen overlays (highlight and interactive region selection) using Tkinter.
2
+
3
+ All Tk objects live in a dedicated daemon thread; other threads talk to it
4
+ through a queue.
5
+ """
6
+
7
+ import queue
8
+ import threading
9
+ import uuid
10
+
11
+ from .geometry import Region
12
+
13
+ _TRANSPARENT = '#ff00fe'
14
+
15
+
16
+ class Overlay:
17
+ def __init__(self):
18
+ self._queue: queue.Queue = queue.Queue()
19
+ self._thread: threading.Thread | None = None
20
+ self._ready = threading.Event()
21
+ self._windows: dict[str, object] = {}
22
+ self._keys: dict[str, list[str]] = {}
23
+ self._root = None
24
+
25
+ # ---- thread ---------------------------------------------------------
26
+ def _ensure(self):
27
+ if self._thread and self._thread.is_alive():
28
+ return
29
+ self._ready.clear()
30
+ self._thread = threading.Thread(
31
+ target=self._run, name='DesktopLibraryOverlay', daemon=True
32
+ )
33
+ self._thread.start()
34
+ self._ready.wait(5)
35
+
36
+ def _run(self):
37
+ import tkinter as tk
38
+
39
+ self._root = tk.Tk()
40
+ self._root.withdraw()
41
+ self._ready.set()
42
+ self._root.after(30, self._poll)
43
+ self._root.mainloop()
44
+
45
+ def _poll(self):
46
+ try:
47
+ while True:
48
+ func, args = self._queue.get_nowait()
49
+ try:
50
+ func(*args)
51
+ except Exception:
52
+ pass
53
+ except queue.Empty:
54
+ pass
55
+ self._root.after(30, self._poll)
56
+
57
+ def _call(self, func, *args):
58
+ self._ensure()
59
+ self._queue.put((func, args))
60
+
61
+ # ---- highlight --------------------------------------------------------
62
+ def highlight(
63
+ self,
64
+ region: Region,
65
+ seconds: float = 0,
66
+ key: str = '',
67
+ color: str = 'red',
68
+ width: int = 3,
69
+ ) -> str:
70
+ hid = uuid.uuid4().hex
71
+ self._keys.setdefault(key or hid, []).append(hid)
72
+ self._call(
73
+ self._create, hid, region, float(seconds or 0), color, int(width)
74
+ )
75
+ return hid
76
+
77
+ def _create(self, hid, region: Region, seconds, color, width):
78
+ import tkinter as tk
79
+
80
+ win = tk.Toplevel(self._root)
81
+ win.overrideredirect(True)
82
+ win.attributes('-topmost', True)
83
+ try:
84
+ win.attributes('-transparentcolor', _TRANSPARENT)
85
+ except tk.TclError:
86
+ win.attributes('-alpha', 0.4)
87
+ pad = width
88
+ win.geometry(
89
+ f'{region.width + 2 * pad}x{region.height + 2 * pad}+{region.x - pad}+{region.y - pad}'
90
+ )
91
+ canvas = tk.Canvas(win, bg=_TRANSPARENT, highlightthickness=0)
92
+ canvas.pack(fill='both', expand=True)
93
+ canvas.create_rectangle(
94
+ pad // 2,
95
+ pad // 2,
96
+ region.width + pad * 1.5,
97
+ region.height + pad * 1.5,
98
+ outline=color,
99
+ width=width,
100
+ )
101
+ self._windows[hid] = win
102
+ if seconds > 0:
103
+ self._root.after(int(seconds * 1000), lambda: self._destroy(hid))
104
+
105
+ def _destroy(self, hid):
106
+ win = self._windows.pop(hid, None)
107
+ if win is not None:
108
+ win.destroy()
109
+
110
+ def clear(self, key: str):
111
+ for hid in self._keys.pop(key, []):
112
+ self._call(self._destroy, hid)
113
+
114
+ def clear_all(self):
115
+ self._keys.clear()
116
+ for hid in list(self._windows):
117
+ self._call(self._destroy, hid)
118
+
119
+ # ---- region selection ---------------------------------------------------
120
+ def select_region(
121
+ self, message: str, bounds: Region, timeout: float = 300
122
+ ) -> Region | None:
123
+ done = threading.Event()
124
+ result: dict = {}
125
+ self._call(self._select, message, bounds, result, done)
126
+ done.wait(timeout)
127
+ return result.get('region')
128
+
129
+ def _select(self, message, bounds: Region, result, done):
130
+ import tkinter as tk
131
+
132
+ win = tk.Toplevel(self._root)
133
+ win.overrideredirect(True)
134
+ win.attributes('-topmost', True)
135
+ win.attributes('-alpha', 0.3)
136
+ win.geometry(f'{bounds.width}x{bounds.height}+{bounds.x}+{bounds.y}')
137
+ canvas = tk.Canvas(
138
+ win, bg='black', cursor='cross', highlightthickness=0
139
+ )
140
+ canvas.pack(fill='both', expand=True)
141
+ canvas.create_text(
142
+ bounds.width // 2,
143
+ 40,
144
+ text=message or 'Select a region (ESC to cancel)',
145
+ fill='white',
146
+ font=('Segoe UI', 18, 'bold'),
147
+ )
148
+ state = {'start': None, 'rect': None}
149
+
150
+ def press(e):
151
+ state['start'] = (e.x, e.y)
152
+ state['rect'] = canvas.create_rectangle(
153
+ e.x, e.y, e.x, e.y, outline='red', width=3
154
+ )
155
+
156
+ def drag(e):
157
+ if state['rect']:
158
+ sx, sy = state['start']
159
+ canvas.coords(state['rect'], sx, sy, e.x, e.y)
160
+
161
+ def release(e):
162
+ if state['start']:
163
+ sx, sy = state['start']
164
+ x1, x2 = sorted((sx, e.x))
165
+ y1, y2 = sorted((sy, e.y))
166
+ if x2 - x1 > 0 and y2 - y1 > 0:
167
+ result['region'] = Region(
168
+ bounds.x + x1, bounds.y + y1, x2 - x1, y2 - y1
169
+ )
170
+ finish()
171
+
172
+ def finish(_=None):
173
+ win.destroy()
174
+ done.set()
175
+
176
+ canvas.bind('<ButtonPress-1>', press)
177
+ canvas.bind('<B1-Motion>', drag)
178
+ canvas.bind('<ButtonRelease-1>', release)
179
+ win.bind('<Escape>', finish)
180
+ win.focus_force()
@@ -0,0 +1,215 @@
1
+ Metadata-Version: 2.4
2
+ Name: robotframework-desktop
3
+ Version: 0.1.0
4
+ Summary: Robot Framework library for Windows desktop automation with hybrid image (pyautogui) and UI Automation (pywinauto) engines.
5
+ License: Apache-2.0
6
+ Keywords: robotframework,desktop,automation,pyautogui,pywinauto,uia,wpf,winforms
7
+ Classifier: Framework :: Robot Framework :: Library
8
+ Classifier: Operating System :: Microsoft :: Windows
9
+ Classifier: Programming Language :: Python :: 3
10
+ Requires-Python: >=3.10
11
+ Description-Content-Type: text/markdown
12
+ Requires-Dist: robotframework>=6.1
13
+ Requires-Dist: robotframework-pythonlibcore>=4.2
14
+ Requires-Dist: pyautogui>=0.9.54
15
+ Requires-Dist: opencv-python>=4.8
16
+ Requires-Dist: pillow>=10.0
17
+ Requires-Dist: mss>=9.0
18
+ Requires-Dist: pywinauto>=0.6.8
19
+ Requires-Dist: pyperclip>=1.8
20
+ Requires-Dist: rapidocr>=3.0
21
+ Requires-Dist: onnxruntime>=1.17
22
+ Provides-Extra: ocr
23
+ Requires-Dist: rapidocr>=3.0; extra == "ocr"
24
+ Requires-Dist: onnxruntime>=1.17; extra == "ocr"
25
+ Provides-Extra: dev
26
+ Requires-Dist: pytest>=7; extra == "dev"
27
+ Requires-Dist: rapidocr>=3.0; extra == "dev"
28
+ Requires-Dist: onnxruntime>=1.17; extra == "dev"
29
+ Requires-Dist: PyYAML>=6.0; extra == "dev"
30
+ Requires-Dist: blue>=0.9; extra == "dev"
31
+
32
+ <div align="center">
33
+
34
+ # 🖥️ robotframework-desktop
35
+
36
+ **Hybrid Windows desktop automation for [Robot Framework](https://robotframework.org)**
37
+ Image recognition · UI Automation · OCR — one keyword set, chosen by the locator.
38
+
39
+ ![Python](https://img.shields.io/badge/python-3.10%2B-3776AB?logo=python&logoColor=white)
40
+ ![Robot Framework](https://img.shields.io/badge/Robot%20Framework-6.1%2B-000000?logo=robotframework&logoColor=white)
41
+ ![Platform](https://img.shields.io/badge/platform-Windows-0078D6?logo=windows&logoColor=white)
42
+ ![License](https://img.shields.io/badge/license-Apache--2.0-green)
43
+ ![Code style: blue](https://img.shields.io/badge/code%20style-blue-1f6feb)
44
+
45
+ [Install](#-install) · [Quick start](#-quick-start) · [Locators](#-locators) · [YAML locators](#-locators-in-a-yaml-file) · [OCR](#-ocr) · [Keywords](docs/DesktopLibrary.html)
46
+
47
+ </div>
48
+
49
+ ---
50
+
51
+ ## ✨ Features
52
+
53
+ - 🔀 **Hybrid keywords** – `Click`, `Input Text`, `Drag And Drop`, ... work with images, UI elements or regions; the engine is picked from the `element` argument.
54
+ - 🖼️ **Image recognition** – pyautogui + OpenCV, per-call similarity, multiple candidate images, offsets and search regions.
55
+ - 🌳 **UI Automation** – pywinauto (`uia` and `win32` backends) for WPF, WinForms, UWP and Win32 apps.
56
+ - 🔤 **OCR** – [RapidOCR](https://github.com/RapidAI/RapidOCR) on onnxruntime, models bundled, **no external binary**; accent and case insensitive text search.
57
+ - ⏳ **Busy cursor aware** – actions wait automatically while the app shows the busy cursor.
58
+ - 🧩 **SikuliLibrary compatible** – implements all its actions, plus UIA keywords (`Attach To Window`, `Get Element Text`, `Set Element Value`, `Select Combobox Item`, ...).
59
+
60
+ ## 📦 Install
61
+
62
+ ```bash
63
+ pip install robotframework-desktop # core
64
+ pip install robotframework-desktop[ocr] # + OCR (rapidocr + onnxruntime)
65
+ ```
66
+
67
+ ## 🚀 Quick start
68
+
69
+ ```robot
70
+ *** Settings ***
71
+ Library DesktopLibrary image_paths=${CURDIR}/images;${CURDIR}/shared similarity=0.6
72
+
73
+ *** Test Cases ***
74
+ Write In Notepad
75
+ Open Application notepad.exe
76
+ Attach To Window name_re:.*Notepad
77
+ Input Text class:Edit Hello
78
+ Click ${{ {"path": ["save.png", "save_dark.png"], "similarity": 0.85} }}
79
+ Wait Until Screen Contain done.png 10s
80
+ [Teardown] Close Application notepad.exe
81
+ ```
82
+
83
+ > [!TIP]
84
+ > Relative image paths are searched in the `image_paths` folders (in order, extendable with `Add Image Path`) and then in the current working directory. The extension can be omitted.
85
+
86
+ ## 🎯 Locators
87
+
88
+ | Locator | Engine |
89
+ |---|---|
90
+ | `save.png` · `save.png=0.85` · `image:C:/refs/save.png` | 🖼️ Image |
91
+ | `{"path": [...], "similarity": 0.8, "offset": {x, y}, "region": {x, y, width, height}}` | 🖼️ Image |
92
+ | `automation_id:btnSave` · `name:OK AND control_type:Button` | 🌳 UIA |
93
+ | `win32:class:Button` · `{"name": "Save", "backend": "win32"}` | 🌳 Win32 |
94
+ | `{"automation_id": "btnSave", "offset": {"x": 5, "y": 0}}` | 🌳 UIA |
95
+ | `[x, y, w, h]` · `{"region": {...}}` · region/match returned by keywords | 📐 Coordinates |
96
+
97
+ ## 🗂️ Locators in a YAML file
98
+
99
+ Keep all element locators in one variable file ([full example](tests/atest/elements.yml)):
100
+
101
+ ```yaml
102
+ APP:
103
+ BTN_SAVE: "automation_id:btnSave"
104
+ TXT_NAME:
105
+ automation_id: txtName
106
+ control_type: Edit
107
+ BTN_SAVE_WIN32:
108
+ name: Save
109
+ class_name_re: ".*BUTTON.*"
110
+ backend: win32
111
+ IMG_TARGET_OFFSET:
112
+ path: target.png
113
+ offset:
114
+ x: 30
115
+ y: 20
116
+ IMG_IN_REGION:
117
+ path: save.png
118
+ region:
119
+ x: 0
120
+ y: 0
121
+ width: 800
122
+ height: 600
123
+ ```
124
+
125
+ ```robot
126
+ *** Settings ***
127
+ Library DesktopLibrary
128
+ Variables elements.yml # requires PyYAML
129
+
130
+ *** Test Cases ***
131
+ Save
132
+ Input Text ${APP.TXT_NAME} Robot
133
+ Click ${APP.BTN_SAVE}
134
+ Click ${APP.IMG_TARGET_OFFSET}
135
+ ```
136
+
137
+ ## 🔤 OCR
138
+
139
+ No language setup needed — the default RapidOCR models read Latin text (accented letters may come out unaccented, so matching ignores accents and case).
140
+
141
+ ```robot
142
+ ${text}= Get Text automation_id:lblStatus
143
+ ${text}= Read Text From Region [10, 400, 300, 30]
144
+ Click Text Clear
145
+ Set ROI [0, 0, 600, 400]
146
+ Region Click Text Clear
147
+ ```
148
+
149
+ ## 🔁 Looping over elements
150
+
151
+ ```robot
152
+ @{items}= Find Elements control_type:ListItem # [] when nothing is found
153
+ FOR ${item} IN @{items}
154
+ ${text}= Get Element Text ${item}
155
+ IF $text == "Item 3" Click ${item}
156
+ END
157
+ @{icons}= Find Elements icon.png sort_by=column min_count=3
158
+ @{rows}= Find Child Elements automation_id:grid control_type:DataItem depth=1
159
+ ```
160
+
161
+ ## ⏳ Busy cursor handling
162
+
163
+ Action keywords wait while the cursor is busy (`wait` / `appstarting`) until it stays normal for 0.5 s — **before** searching, before acting and **after** the action (watched for 0.2 s). On timeout (default 30 s) the keyword fails, or only warns with `busy_cursor_fail=False` / `ignore_busy_error=True`. Use `show_warnings=False` to log warnings as INFO.
164
+
165
+ ```robot
166
+ Library DesktopLibrary busy_cursor_timeout=1 min busy_cursor_after_delay=0.3s
167
+
168
+ Click automation_id:btnLoad ignore_busy_error=True
169
+ Set Busy Cursor Wait False
170
+ Set Busy Cursor Types wait,appstarting,custom
171
+ Wait Until Cursor Is Not Busy 2 min
172
+ ${cursor}= Get Current Cursor Type
173
+ ```
174
+
175
+ ## 📚 Documentation
176
+
177
+ Full keyword reference: **[docs/DesktopLibrary.html](docs/DesktopLibrary.html)** (`just docs` to regenerate).
178
+
179
+ ## 🛠️ Development
180
+
181
+ Requires [uv](https://docs.astral.sh/uv/) and [just](https://github.com/casey/just) (PowerShell recipes).
182
+
183
+ | Command | Description |
184
+ |---|---|
185
+ | `just setup` | Create `.venv` and install the library (editable + dev deps) |
186
+ | `just fmt` | Format code with [blue](https://github.com/grantjenks/blue) |
187
+ | `just test` | Unit tests (pytest) |
188
+ | `just atest` | Acceptance tests (Robot Framework) |
189
+ | `just docs` | Regenerate libdoc |
190
+ | `just build` | Build both wheels |
191
+ | `just build-full` | `dist/full` – OCR deps required |
192
+ | `just build-no-ocr` | `dist/no-ocr` – OCR optional (`[ocr]` extra) |
193
+ | `just check` | Show dependencies of the built wheels |
194
+
195
+ ### Release
196
+
197
+ CI ([.github/workflows/ci.yml](.github/workflows/ci.yml)) runs lint, unit and acceptance tests on every push and pull request.
198
+ To publish a release, bump `version` in `pyproject.toml`, then:
199
+
200
+ ```powershell
201
+ git tag v0.2.0
202
+ git push --tags
203
+ ```
204
+
205
+ [.github/workflows/release.yml](.github/workflows/release.yml) runs the tests, builds both wheels and creates a GitHub Release with them attached:
206
+
207
+ - `robotframework-desktop` - OCR optional (`[ocr]` extra)
208
+ - `robotframework-desktop-full` - OCR deps required
209
+
210
+ > [!WARNING]
211
+ > The acceptance suite launches a small WinForms app ([tests/app/SampleApp.ps1](tests/app/SampleApp.ps1)) and generates its reference images at runtime. **Don't use the mouse or keyboard while it runs.**
212
+
213
+ ## 📄 License
214
+
215
+ [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0)
@@ -0,0 +1,25 @@
1
+ DesktopLibrary/__init__.py,sha256=v6_IvClm3ij_ve6ffFA4TfP2Em3nOnQkH_Rl-BcRa2U,11729
2
+ DesktopLibrary/context.py,sha256=Myqoe-ACr91XP3g41YxmUjfT951JI6P5gebiSZz_QzA,10125
3
+ DesktopLibrary/cursor.py,sha256=ACpv5kZgEZVsqL88LehMEB2iZhF3QT-eGrMgmfJKRGk,8573
4
+ DesktopLibrary/geometry.py,sha256=C4M6tLB16iQ-Vy9VU3m63SnVNWdRCA6f6xxxw6O3UcE,6722
5
+ DesktopLibrary/locator.py,sha256=ZjeT7MBbCbkgKToNzhF3h0V176PeUvI_ib_WlgZ_8PA,7383
6
+ DesktopLibrary/overlay.py,sha256=FFTmMPOJcXqv3EK_Pp7DKZuYJczjPYJGqrqydIrpjxM,5707
7
+ DesktopLibrary/engines/__init__.py,sha256=H2e7e6Q0aAqOcKVTnjnv_AucouoZuQHD_oVJvMpUpTk,139
8
+ DesktopLibrary/engines/image.py,sha256=oEj0vSe0recQX4cFSlzJ5ec__9yR8vAPUFQwa1ZJJp4,6152
9
+ DesktopLibrary/engines/ui.py,sha256=u9S--vIiE-JDfl6YGrw1Uvu4mRJHzRtHn2b6nn-L69o,6210
10
+ DesktopLibrary/keywords/__init__.py,sha256=DvYDv9uKyoa6LzTF9QGCidALVEGteTqTxnXtK_YMbEo,608
11
+ DesktopLibrary/keywords/application.py,sha256=4em6Bjyy802bBSe56TbBzg73Wr5iNMW5T4lpw49ZGB8,5073
12
+ DesktopLibrary/keywords/base.py,sha256=nOzEGtOpGgzSVIjzCqKBALOJLtJG0lRFFEN9fNYaMEE,2986
13
+ DesktopLibrary/keywords/config.py,sha256=DPaLerkHU0Q8QPmgmxqvz9cvbLfH2b3WP12iEoGv9OU,4901
14
+ DesktopLibrary/keywords/cursor.py,sha256=svRADiYSFX2N4woxK1KtxgMtQDhx6N1yTMEGp6wy6HA,7689
15
+ DesktopLibrary/keywords/element.py,sha256=ieAIUh2wcHdfr5oCn61nGqEzQZSjeh8SH47NaRgz4Wc,8991
16
+ DesktopLibrary/keywords/find.py,sha256=SxK25gFYCbuSPSRfVggZfkZlcIyV8NjqHlISt7Sl010,5446
17
+ DesktopLibrary/keywords/keyboard.py,sha256=11lPEFTLxeNSZGRD7YBli2wplYzSYfbRfxZDVjKWTPk,4756
18
+ DesktopLibrary/keywords/mouse.py,sha256=zZDX2mrHCQfDX_B4GOwKkwiRMecSevX-YC_fdJXnAI0,12314
19
+ DesktopLibrary/keywords/ocr.py,sha256=zXy9HbetD5vNB6zd95hrqpFlSLdMQHmaK51rm50u8vA,5442
20
+ DesktopLibrary/keywords/region.py,sha256=CTwysDYAx7d2WYKJuML20r5WtnqkqM3anEZSkavUYHQ,10308
21
+ DesktopLibrary/keywords/wait.py,sha256=DIfbUrgxicOTFmvf7DOuAYBne2BkFbw58pdMaV7YEgM,6523
22
+ robotframework_desktop-0.1.0.dist-info/METADATA,sha256=SvMaaX--oZT6jFLHh_HvKyy4kSGBQjDLPUqFyQitg5s,8437
23
+ robotframework_desktop-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
24
+ robotframework_desktop-0.1.0.dist-info/top_level.txt,sha256=5eYvVeCi3VKx9Lg4zk4yqN9m5A60Nxlima2FQIDCLKo,15
25
+ robotframework_desktop-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ DesktopLibrary