locum 0.1.0__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.
- locum-0.1.0/.gitignore +13 -0
- locum-0.1.0/LICENSE +21 -0
- locum-0.1.0/PKG-INFO +510 -0
- locum-0.1.0/README.md +464 -0
- locum-0.1.0/docs/ARCHITECTURE.md +189 -0
- locum-0.1.0/docs/COMPARISON.md +94 -0
- locum-0.1.0/docs/LLM.md +473 -0
- locum-0.1.0/examples/basic_automation.py +89 -0
- locum-0.1.0/examples/data/invoices.csv +6 -0
- locum-0.1.0/examples/data/sales.csv +5 -0
- locum-0.1.0/examples/excel_report_flow.yaml +57 -0
- locum-0.1.0/examples/llm_agent.py +131 -0
- locum-0.1.0/examples/mcp_config.json +12 -0
- locum-0.1.0/examples/pyautogui_migration.py +85 -0
- locum-0.1.0/examples/sap_style_data_entry.py +262 -0
- locum-0.1.0/pyproject.toml +63 -0
- locum-0.1.0/src/locum/__init__.py +397 -0
- locum-0.1.0/src/locum/__main__.py +10 -0
- locum-0.1.0/src/locum/_nodeinfo.py +235 -0
- locum-0.1.0/src/locum/backends/__init__.py +98 -0
- locum-0.1.0/src/locum/backends/_x11clip.py +373 -0
- locum-0.1.0/src/locum/backends/base.py +373 -0
- locum-0.1.0/src/locum/backends/macos.py +2924 -0
- locum-0.1.0/src/locum/backends/mock.py +775 -0
- locum-0.1.0/src/locum/backends/windows.py +2848 -0
- locum-0.1.0/src/locum/backends/x11.py +2462 -0
- locum-0.1.0/src/locum/cli.py +1286 -0
- locum-0.1.0/src/locum/compat/__init__.py +41 -0
- locum-0.1.0/src/locum/compat/pyautogui.py +1100 -0
- locum-0.1.0/src/locum/desktop.py +1200 -0
- locum-0.1.0/src/locum/element.py +1374 -0
- locum-0.1.0/src/locum/errors.py +208 -0
- locum-0.1.0/src/locum/failsafe.py +331 -0
- locum-0.1.0/src/locum/keyboard.py +404 -0
- locum-0.1.0/src/locum/keys.py +258 -0
- locum-0.1.0/src/locum/llm/__init__.py +64 -0
- locum-0.1.0/src/locum/llm/agent.py +783 -0
- locum-0.1.0/src/locum/llm/computer_use.py +813 -0
- locum-0.1.0/src/locum/llm/mcp_server.py +368 -0
- locum-0.1.0/src/locum/llm/observe.py +540 -0
- locum-0.1.0/src/locum/llm/safety.py +2113 -0
- locum-0.1.0/src/locum/llm/tools.py +1891 -0
- locum-0.1.0/src/locum/motion.py +372 -0
- locum-0.1.0/src/locum/mouse.py +327 -0
- locum-0.1.0/src/locum/ocr.py +673 -0
- locum-0.1.0/src/locum/rpa/__init__.py +61 -0
- locum-0.1.0/src/locum/rpa/flow.py +1516 -0
- locum-0.1.0/src/locum/rpa/queue.py +1524 -0
- locum-0.1.0/src/locum/rpa/recorder.py +1237 -0
- locum-0.1.0/src/locum/rpa/report.py +486 -0
- locum-0.1.0/src/locum/rpa/robot.py +968 -0
- locum-0.1.0/src/locum/screen.py +345 -0
- locum-0.1.0/src/locum/selectors.py +1801 -0
- locum-0.1.0/src/locum/selftest.py +571 -0
- locum-0.1.0/src/locum/types.py +239 -0
- locum-0.1.0/src/locum/vision.py +1156 -0
- locum-0.1.0/src/locum/wait.py +323 -0
- locum-0.1.0/src/locum/window.py +398 -0
- locum-0.1.0/tests/_queue_workers.py +37 -0
- locum-0.1.0/tests/_stub_desktop.py +65 -0
- locum-0.1.0/tests/conftest.py +38 -0
- locum-0.1.0/tests/test_api_docs_review7_fixes.py +369 -0
- locum-0.1.0/tests/test_cli.py +681 -0
- locum-0.1.0/tests/test_compat_pyautogui.py +468 -0
- locum-0.1.0/tests/test_core_correctness_fixes.py +418 -0
- locum-0.1.0/tests/test_desktop.py +645 -0
- locum-0.1.0/tests/test_element.py +438 -0
- locum-0.1.0/tests/test_element_focus_container.py +155 -0
- locum-0.1.0/tests/test_element_focus_proof.py +284 -0
- locum-0.1.0/tests/test_element_review2_fixes.py +387 -0
- locum-0.1.0/tests/test_element_wait_fail_fast.py +144 -0
- locum-0.1.0/tests/test_failsafe.py +138 -0
- locum-0.1.0/tests/test_input_review2_fixes.py +217 -0
- locum-0.1.0/tests/test_integration_fixes.py +333 -0
- locum-0.1.0/tests/test_keyboard.py +351 -0
- locum-0.1.0/tests/test_llm_agent.py +367 -0
- locum-0.1.0/tests/test_llm_computer_use.py +307 -0
- locum-0.1.0/tests/test_llm_observe.py +160 -0
- locum-0.1.0/tests/test_llm_review2_fixes.py +310 -0
- locum-0.1.0/tests/test_llm_rpa_review6_fixes.py +286 -0
- locum-0.1.0/tests/test_llm_rpa_safety_fixes.py +454 -0
- locum-0.1.0/tests/test_llm_safety.py +163 -0
- locum-0.1.0/tests/test_llm_tools.py +408 -0
- locum-0.1.0/tests/test_llm_tools_review3_fixes.py +335 -0
- locum-0.1.0/tests/test_macos_backend.py +1612 -0
- locum-0.1.0/tests/test_mcp_server.py +135 -0
- locum-0.1.0/tests/test_mock_examples_review4_fixes.py +368 -0
- locum-0.1.0/tests/test_motion.py +142 -0
- locum-0.1.0/tests/test_mouse.py +254 -0
- locum-0.1.0/tests/test_ocr.py +443 -0
- locum-0.1.0/tests/test_package.py +192 -0
- locum-0.1.0/tests/test_package_review5_fixes.py +246 -0
- locum-0.1.0/tests/test_packaging_metadata.py +71 -0
- locum-0.1.0/tests/test_policy_allow_rule_scope.py +271 -0
- locum-0.1.0/tests/test_policy_allowlist.py +351 -0
- locum-0.1.0/tests/test_policy_nameless_targets.py +205 -0
- locum-0.1.0/tests/test_queue_concurrency.py +159 -0
- locum-0.1.0/tests/test_queue_durability.py +196 -0
- locum-0.1.0/tests/test_queue_leases.py +197 -0
- locum-0.1.0/tests/test_queue_write_failures.py +174 -0
- locum-0.1.0/tests/test_readme_claims.py +42 -0
- locum-0.1.0/tests/test_review6_fixes.py +510 -0
- locum-0.1.0/tests/test_rpa_flow.py +384 -0
- locum-0.1.0/tests/test_rpa_flow_cli_review3_fixes.py +411 -0
- locum-0.1.0/tests/test_rpa_policy_gate.py +322 -0
- locum-0.1.0/tests/test_rpa_queue.py +222 -0
- locum-0.1.0/tests/test_rpa_recorder.py +344 -0
- locum-0.1.0/tests/test_rpa_review2_fixes.py +406 -0
- locum-0.1.0/tests/test_rpa_robot.py +441 -0
- locum-0.1.0/tests/test_screen.py +299 -0
- locum-0.1.0/tests/test_selectors.py +581 -0
- locum-0.1.0/tests/test_selectors_review2_fixes.py +293 -0
- locum-0.1.0/tests/test_selftest.py +82 -0
- locum-0.1.0/tests/test_type_verify.py +208 -0
- locum-0.1.0/tests/test_vision.py +628 -0
- locum-0.1.0/tests/test_wait.py +239 -0
- locum-0.1.0/tests/test_wait_fail_fast.py +350 -0
- locum-0.1.0/tests/test_window.py +252 -0
- locum-0.1.0/tests/test_windows_backend.py +1771 -0
- locum-0.1.0/tests/test_x11_backend.py +1352 -0
locum-0.1.0/.gitignore
ADDED
locum-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 locum contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
locum-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,510 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: locum
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Desktop automation for Python (Windows first; macOS and Linux/X11 backends): input, vision, accessibility selectors, RPA workflows, and LLM agent control.
|
|
5
|
+
Project-URL: Homepage, https://github.com/israerusan/locum
|
|
6
|
+
Project-URL: Repository, https://github.com/israerusan/locum
|
|
7
|
+
Project-URL: Issues, https://github.com/israerusan/locum/issues
|
|
8
|
+
Project-URL: Documentation, https://github.com/israerusan/locum/blob/main/docs/LLM.md
|
|
9
|
+
Author: israerusan
|
|
10
|
+
License: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: agent,automation,computer-use,desktop,gui,llm,mcp,pyautogui,rpa
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Operating System :: MacOS
|
|
15
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
16
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Topic :: Desktop Environment
|
|
19
|
+
Classifier: Topic :: Software Development :: Testing
|
|
20
|
+
Requires-Python: >=3.9
|
|
21
|
+
Requires-Dist: comtypes>=1.2; sys_platform == 'win32'
|
|
22
|
+
Requires-Dist: mss>=9.0
|
|
23
|
+
Requires-Dist: numpy>=1.22
|
|
24
|
+
Requires-Dist: pillow>=9.0
|
|
25
|
+
Requires-Dist: pyobjc-framework-applicationservices>=9.0; sys_platform == 'darwin'
|
|
26
|
+
Requires-Dist: pyobjc-framework-cocoa>=9.0; sys_platform == 'darwin'
|
|
27
|
+
Requires-Dist: pyobjc-framework-quartz>=9.0; sys_platform == 'darwin'
|
|
28
|
+
Requires-Dist: python-xlib>=0.33; sys_platform == 'linux'
|
|
29
|
+
Provides-Extra: all
|
|
30
|
+
Requires-Dist: anthropic>=0.40; extra == 'all'
|
|
31
|
+
Requires-Dist: pynput>=1.7; extra == 'all'
|
|
32
|
+
Requires-Dist: pytesseract>=0.3.10; extra == 'all'
|
|
33
|
+
Requires-Dist: pyyaml>=6.0; extra == 'all'
|
|
34
|
+
Provides-Extra: dev
|
|
35
|
+
Requires-Dist: pytest>=7; extra == 'dev'
|
|
36
|
+
Requires-Dist: pyyaml>=6.0; extra == 'dev'
|
|
37
|
+
Provides-Extra: llm
|
|
38
|
+
Requires-Dist: anthropic>=0.40; extra == 'llm'
|
|
39
|
+
Provides-Extra: ocr
|
|
40
|
+
Requires-Dist: pytesseract>=0.3.10; extra == 'ocr'
|
|
41
|
+
Provides-Extra: record
|
|
42
|
+
Requires-Dist: pynput>=1.7; extra == 'record'
|
|
43
|
+
Provides-Extra: yaml
|
|
44
|
+
Requires-Dist: pyyaml>=6.0; extra == 'yaml'
|
|
45
|
+
Description-Content-Type: text/markdown
|
|
46
|
+
|
|
47
|
+
# locum
|
|
48
|
+
|
|
49
|
+
**Desktop automation for Python that understands the screen.** One library for
|
|
50
|
+
mouse/keyboard input, screenshots and template matching, accessibility
|
|
51
|
+
selectors, RPA robots, and LLM-agent control. Windows is the most-validated
|
|
52
|
+
platform; macOS and Linux (X11) backends exist but have been tested less (see
|
|
53
|
+
below). **Native Wayland is not supported**: on Linux, locum drives X11
|
|
54
|
+
sessions and XWayland windows only.
|
|
55
|
+
|
|
56
|
+
| Platform | Backend | How it is tested today |
|
|
57
|
+
|----------|---------|------------------------|
|
|
58
|
+
| Windows 10/11 | SendInput + **UI Automation** | unit tests against a fake Win32/UIA API; validated on a real Windows 11 machine by the built-in self-test |
|
|
59
|
+
| macOS 12+ | Quartz events + **AX API** | unit tests against a fake Quartz/AX API only — **not yet validated on a real Mac** |
|
|
60
|
+
| Linux (X11 / XWayland) | XTEST + **AT-SPI** | unit tests plus X11 tests under Xvfb in CI; native Wayland input/capture **not supported** |
|
|
61
|
+
|
|
62
|
+
* **Selectors, not coordinates.** `d.click('button "Save"')` finds the element
|
|
63
|
+
through UI Automation / AX / AT-SPI, survives window moves, DPI and theme
|
|
64
|
+
changes, and falls back to the native *invoke* action when the control is
|
|
65
|
+
covered. UiPath-style XML selectors work too.
|
|
66
|
+
* **Vision when there is no accessibility tree.** FFT-based normalized
|
|
67
|
+
cross-correlation, multi-scale, multi-monitor, **alpha channel = mask**, OCR
|
|
68
|
+
hooks, wait-for-change / wait-until-stable.
|
|
69
|
+
* **Input that behaves like a person** (optional): Fitts's-law timing,
|
|
70
|
+
Bézier mouse paths with overshoot, Unicode keystroke injection that does not
|
|
71
|
+
depend on the keyboard layout (with read-back verification for text fields,
|
|
72
|
+
see [below](#humanized-layout-independent-input)), clipboard paste,
|
|
73
|
+
stuck-modifier-proof hotkeys, a corner fail-safe and a kill hotkey.
|
|
74
|
+
* **RPA built in.** Robots with steps/retries/evidence screenshots and HTML
|
|
75
|
+
reports, resumable file-backed work queues with business exceptions that
|
|
76
|
+
several worker processes on one machine can drain together (locked, leased,
|
|
77
|
+
fsynced), YAML flows, and a recorder that writes selectors instead of raw
|
|
78
|
+
coordinates.
|
|
79
|
+
* **LLM-native.** Set-of-marks observations; a Claude (Anthropic) agent loop
|
|
80
|
+
and computer-use adapter; tool schemas exportable in Anthropic, OpenAI and
|
|
81
|
+
MCP formats (locum ships no OpenAI agent runtime — bring your own loop);
|
|
82
|
+
a zero-dependency MCP server; and a safety policy with human confirmation,
|
|
83
|
+
dry-run and audit logs.
|
|
84
|
+
* **A drop-in PyAutoGUI layer** (`import locum.compat.pyautogui as pyautogui`)
|
|
85
|
+
so existing scripts move over with one line.
|
|
86
|
+
|
|
87
|
+
> Status: **0.1 alpha** — usable, tested, but APIs may still change between
|
|
88
|
+
> releases. The design is in [`docs/ARCHITECTURE.md`](https://github.com/israerusan/locum/blob/main/docs/ARCHITECTURE.md).
|
|
89
|
+
> See [`docs/COMPARISON.md`](https://github.com/israerusan/locum/blob/main/docs/COMPARISON.md) and [`docs/LLM.md`](https://github.com/israerusan/locum/blob/main/docs/LLM.md).
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## Install
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
pip install locum # core: pillow, numpy, mss (+ python-xlib / comtypes / pyobjc per OS)
|
|
97
|
+
pip install "locum[llm]" # + anthropic SDK for the Agent
|
|
98
|
+
pip install "locum[ocr]" # + pytesseract only (install the Tesseract binary too)
|
|
99
|
+
pip install "locum[record]" # + pynput: live recorder and global kill hotkey
|
|
100
|
+
pip install "locum[yaml]" # + PyYAML for YAML flows (JSON flows need nothing)
|
|
101
|
+
pip install "locum[all]" # everything above
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
From a checkout: `pip install -e ".[dev]"` and `python -m pytest -q`.
|
|
105
|
+
|
|
106
|
+
Platform notes: on **Linux** the clipboard works out of the box (native X11
|
|
107
|
+
selection handling; `xclip`/`xsel` are used if present) and, for
|
|
108
|
+
accessibility selectors, install PyGObject with AT-SPI (`python3-gi gir1.2-atspi-2.0`
|
|
109
|
+
on Debian/Ubuntu). On **macOS** grant your terminal/IDE *Accessibility* and
|
|
110
|
+
*Screen Recording* permission (System Settings → Privacy & Security).
|
|
111
|
+
|
|
112
|
+
## Check it on your machine
|
|
113
|
+
|
|
114
|
+
Before automating real work, run the self-test. It opens a fresh, empty
|
|
115
|
+
editor tab (Notepad / TextEdit / gedit), types into it, uses the clipboard and
|
|
116
|
+
moves that window, then cleans up and restores your clipboard and mouse. It
|
|
117
|
+
refuses to type into an editor that already contains text:
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
python -m locum.selftest # report in ./locum_selftest_<timestamp>/
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Don't touch the mouse or keyboard while it runs (about a minute); slam the
|
|
124
|
+
mouse into a screen corner to abort.
|
|
125
|
+
|
|
126
|
+
## 60-second quickstart
|
|
127
|
+
|
|
128
|
+
locum ships with a simulated desktop (an ERP form, a spreadsheet and a text
|
|
129
|
+
editor), so you can try everything without touching your real screen:
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
from locum import Desktop
|
|
133
|
+
from locum.cli import DemoBackend
|
|
134
|
+
|
|
135
|
+
d = Desktop(DemoBackend()) # Desktop() drives your real screen instead
|
|
136
|
+
|
|
137
|
+
form = d.window("Invoice Entry*") # exact title, substring, glob or regex
|
|
138
|
+
form.find("edit#vendor").set_text("ACME Corp") # native set-value, falls back to typing;
|
|
139
|
+
# read back: refused/truncated input raises
|
|
140
|
+
form.find('edit "Invoice No"').set_text("INV-42")
|
|
141
|
+
form.find("edit#amount").set_text("1250.00")
|
|
142
|
+
d.click('button "Save"') # selector -> element -> click
|
|
143
|
+
print(d.find("text#status").text) # Saved invoice INV-42
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
The same from the shell (`--backend mock` = the demo desktop):
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
locum --backend mock windows
|
|
150
|
+
locum --backend mock tree --window "Invoice Entry"
|
|
151
|
+
locum --backend mock click 'button "Save"'
|
|
152
|
+
locum --backend mock observe # what an LLM agent sees
|
|
153
|
+
locum --backend mock run examples/excel_report_flow.yaml
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
On your real desktop the same calls work against real applications — e.g.
|
|
157
|
+
`locum inspect` prints a robust selector for the element under the mouse.
|
|
158
|
+
|
|
159
|
+
## Feature tour
|
|
160
|
+
|
|
161
|
+
The snippets below continue the quickstart (`d` is the demo desktop) and are
|
|
162
|
+
executed by the test-suite, so they stay correct.
|
|
163
|
+
|
|
164
|
+
### Selectors: CSS-like, UiPath XML or dicts
|
|
165
|
+
|
|
166
|
+
```python
|
|
167
|
+
d.find('window[name%="Invoice*"] button:has-text("save")') # glob, case-insensitive text
|
|
168
|
+
d.find("<wnd app='erp' title='Invoice Entry*' /><ctrl automationid='amount' role='editable text' />")
|
|
169
|
+
d.find({"role": "button", "name": "Clear", "within": {"role": "window", "name": "Invoice Entry - ERP"}})
|
|
170
|
+
d.find('window "Book1 - Excel" > edit "Name Box"') # child combinator
|
|
171
|
+
cells = d.find_all('edit[name~="^[A-E]1$"]') # regex attribute match
|
|
172
|
+
assert [c.name for c in cells] == ["A1", "B1", "C1", "D1", "E1"]
|
|
173
|
+
|
|
174
|
+
from locum import selector_for
|
|
175
|
+
el = d.element_at(370, 171) # what is under this point?
|
|
176
|
+
print(selector_for(el.node, roots=d.roots())) # window "Invoice Entry - ERP" edit#amount
|
|
177
|
+
print(d.tree(window="Invoice Entry*", interactive_only=True)) # readable tree dump
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Operators: `=` `!=` `*=` `^=` `$=` `~=` (regex) `%=` (glob), `i` flag for
|
|
181
|
+
case-insensitive; pseudo-classes `:enabled :visible :focused :checked :unchecked :mixed
|
|
182
|
+
:has-text() :has() :not() :nth() :first :last`. Every lookup waits up to
|
|
183
|
+
`default_timeout` and failures say what *was* there:
|
|
184
|
+
`No element matching 'button "Sav"'; did you mean: button "Save" #save`.
|
|
185
|
+
|
|
186
|
+
### Elements and windows
|
|
187
|
+
|
|
188
|
+
```python
|
|
189
|
+
amount = d.find("edit#amount")
|
|
190
|
+
amount.set_text("99.90").press("tab") # actions chain
|
|
191
|
+
d.find("checkbox#urgent").check()
|
|
192
|
+
assert d.find("checkbox#urgent").is_checked
|
|
193
|
+
d.wait('button "Save"', state="enabled", timeout=5)
|
|
194
|
+
|
|
195
|
+
excel = d.window("Book1*")
|
|
196
|
+
excel.activate()
|
|
197
|
+
print(excel.rect, excel.app, excel.is_active)
|
|
198
|
+
d.window("Invoice Entry*").activate()
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### Vision: template matching with alpha masks
|
|
202
|
+
|
|
203
|
+
```python
|
|
204
|
+
needle = d.screenshot((180, 268, 100, 32)) # a crop of the Save button
|
|
205
|
+
match = d.locate(needle, confidence=0.95)
|
|
206
|
+
print(match.rect, round(match.confidence, 3)) # Rect(left=180, top=268, ...) 1.0
|
|
207
|
+
|
|
208
|
+
masked = needle.convert("RGBA")
|
|
209
|
+
masked.paste((255, 0, 255, 0), (0, 0, 100, 8)) # transparent pixels are ignored when matching
|
|
210
|
+
assert d.locate(masked, region=(0, 0, 700, 600)).center == match.center
|
|
211
|
+
d.click_image(needle, timeout=2) # wait for it, then click its center
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
`locate_all` (non-max suppression), `scales=(0.8, 1.0, 1.25)` for DPI
|
|
215
|
+
differences, `grayscale=True`, `region=`, `d.wait_until_stable()` and
|
|
216
|
+
`d.wait_for_change()` are all there. With an OCR engine installed,
|
|
217
|
+
`d.find_text("Total")`, `d.click_text("Submit")` and `d.read_text(region)` work
|
|
218
|
+
on any pixels. `locum[ocr]` installs only `pytesseract` (plus the Tesseract
|
|
219
|
+
binary from your OS); RapidOCR (`pip install rapidocr-onnxruntime`) and EasyOCR
|
|
220
|
+
(`pip install easyocr`) are optional, separate installs that locum picks up
|
|
221
|
+
when present.
|
|
222
|
+
|
|
223
|
+
### Humanized, layout-independent input
|
|
224
|
+
|
|
225
|
+
```python
|
|
226
|
+
d.move('button "Clear"', humanize=True, duration=0.2) # Bezier path with jitter and overshoot
|
|
227
|
+
d.click("edit#vendor")
|
|
228
|
+
d.type("Café Zoë ✓") # Unicode injection, no keymap needed
|
|
229
|
+
d.hotkey("mod+a") # ctrl+a, or cmd+a on macOS
|
|
230
|
+
d.press("backspace")
|
|
231
|
+
with d.hold("shift"):
|
|
232
|
+
d.press("tab")
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
How reliable is typing? `d.type()` injects Unicode keystrokes (no keymap
|
|
236
|
+
lookup, so the keyboard layout does not matter), paced per character. That
|
|
237
|
+
does not make it infallible: some applications drop or garble synthesized
|
|
238
|
+
keystrokes while busy — seen intermittently with Windows 11 Notepad. For text
|
|
239
|
+
fields, `d.find("edit#vendor").type("…")` (`Element.type`, `verify=True` by
|
|
240
|
+
default) reads the field back afterwards and repairs a garbled result, or
|
|
241
|
+
raises instead of reporting success; `set_text()` sets the value natively and
|
|
242
|
+
verifies it; `d.type(text, mode="clipboard")` pastes instead of typing. Use one
|
|
243
|
+
of those when the exact text matters.
|
|
244
|
+
|
|
245
|
+
`Desktop(humanize=True)` (or `Humanize(speed=..., jitter=..., seed=...)`) makes
|
|
246
|
+
every move human-like; `pause=` mirrors PyAutoGUI's `PAUSE`. Moving the mouse
|
|
247
|
+
into a screen corner raises `FailSafeTriggered`; `locum.install_kill_hotkey()`
|
|
248
|
+
(needs `pynput`) aborts from anywhere.
|
|
249
|
+
|
|
250
|
+
### RPA: robots, work queues, flows, recorder
|
|
251
|
+
|
|
252
|
+
```python
|
|
253
|
+
import re
|
|
254
|
+
from locum import FailSafeTriggered
|
|
255
|
+
from locum.rpa import BusinessException, Robot, WorkQueue
|
|
256
|
+
|
|
257
|
+
robot = Robot(d, name="invoices", run_dir="locum_runs")
|
|
258
|
+
queue = WorkQueue("invoices", path="locum_runs/invoices.queue.json") # persistent: runs resume
|
|
259
|
+
queue.add({"vendor": "Initech", "invoice_no": "INV-43", "amount": "75"}, reference="Initech|INV-43")
|
|
260
|
+
queue.add({"vendor": "Umbrella", "invoice_no": "INV-44", "amount": "-1"}, reference="Umbrella|INV-44")
|
|
261
|
+
|
|
262
|
+
def enter_invoice(item):
|
|
263
|
+
if not re.fullmatch(r"\d+(\.\d{1,2})?", item["amount"]) or float(item["amount"]) <= 0:
|
|
264
|
+
raise BusinessException(f"invalid amount {item['amount']!r}") # recorded, never retried
|
|
265
|
+
d.click('button "Clear"') # known state: status "Cleared"
|
|
266
|
+
d.find("edit#vendor").set_text(item["vendor"]) # errors up to here are retried
|
|
267
|
+
d.find("edit#invoice_no").set_text(item["invoice_no"])
|
|
268
|
+
d.find("edit#amount").set_text(item["amount"])
|
|
269
|
+
|
|
270
|
+
def answered(): # the old status is still on screen
|
|
271
|
+
status = d.find("text#status").text
|
|
272
|
+
return status if status not in ("", "Cleared") else None
|
|
273
|
+
|
|
274
|
+
d.click('button "Save"')
|
|
275
|
+
try: # after Save, a retry could post twice
|
|
276
|
+
status = d.wait_until(answered, timeout=10)
|
|
277
|
+
except FailSafeTriggered:
|
|
278
|
+
raise # the operator stopped the robot
|
|
279
|
+
except Exception as exc:
|
|
280
|
+
raise BusinessException(f"needs review: no answer after Save ({exc})") from exc
|
|
281
|
+
if status.lower().startswith("error"):
|
|
282
|
+
raise BusinessException(status) # rejected by the app: never "success"
|
|
283
|
+
return {"status": status}
|
|
284
|
+
|
|
285
|
+
@robot.task(retries=1)
|
|
286
|
+
def process():
|
|
287
|
+
with robot.step("enter invoices"):
|
|
288
|
+
robot.vars["stats"] = queue.process(enter_invoice, retries=2)
|
|
289
|
+
|
|
290
|
+
report = robot.run() # timings, logs, error screenshots, report.json + report.html
|
|
291
|
+
print(report.summary(), robot.vars["stats"]["success"], robot.vars["stats"]["business_exception"])
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
Declarative flows (YAML/JSON) cover the same ground without Python —
|
|
295
|
+
`click, type, set_text, wait, if_exists, for_each (csv/json/items), try/catch,
|
|
296
|
+
repeat, call, read_value, assert_exists, ...` with `${var}` interpolation; see
|
|
297
|
+
[`examples/excel_report_flow.yaml`](https://github.com/israerusan/locum/blob/main/examples/excel_report_flow.yaml):
|
|
298
|
+
|
|
299
|
+
```python
|
|
300
|
+
from locum.rpa import run_flow
|
|
301
|
+
|
|
302
|
+
report = run_flow({"steps": [
|
|
303
|
+
{"set_text": {"target": "edit#vendor", "text": "${vendor}"}},
|
|
304
|
+
{"assert_exists": {"target": "edit#vendor", "equals": "Hooli"}},
|
|
305
|
+
]}, d, variables={"vendor": "Hooli"})
|
|
306
|
+
assert report.ok
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
The `Recorder` turns live input (or anything locum itself does) into flows
|
|
310
|
+
or Python **with selectors instead of coordinates**:
|
|
311
|
+
|
|
312
|
+
```python
|
|
313
|
+
from locum.rpa import Recorder
|
|
314
|
+
|
|
315
|
+
rec = Recorder(d).start(source="backend") # "auto" = global hooks via pynput
|
|
316
|
+
d.click("edit#due_date")
|
|
317
|
+
d.type("2026-12-31")
|
|
318
|
+
rec.stop()
|
|
319
|
+
print(rec.to_python()) # d.click('window "Invoice Entry - ERP" edit#due_date') ...
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
### LLM control: tools, computer use, MCP, agents
|
|
323
|
+
|
|
324
|
+
```python
|
|
325
|
+
from locum.llm import DesktopToolkit, Policy
|
|
326
|
+
|
|
327
|
+
kit = DesktopToolkit(d, policy=Policy()) # every call is policy-checked and audited
|
|
328
|
+
tools = kit.to_anthropic() # also .to_openai() / .to_mcp()
|
|
329
|
+
obs = kit.call("observe", {"include_screenshot": False})
|
|
330
|
+
print(obs.text.splitlines()[-1]) # [N] ... numbered elements (set-of-marks)
|
|
331
|
+
print(kit.call("set_value", {"selector": "edit#vendor", "value": "Pied Piper"}).text)
|
|
332
|
+
print(kit.call("click", {"selector": 'button "Submit Payment"'}).is_error) # True: needs confirmation
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
<!-- norun -->
|
|
336
|
+
```python
|
|
337
|
+
from locum import Desktop
|
|
338
|
+
from locum.llm import Agent, AuditLog, Policy, console_confirm
|
|
339
|
+
|
|
340
|
+
agent = Agent(Desktop(), policy=Policy(confirm=console_confirm), audit=AuditLog("audit.jsonl"))
|
|
341
|
+
result = agent.run("Open the Q3 report in Excel and e-mail the totals to Dana")
|
|
342
|
+
print(result.success, result.final_text)
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
* `mode="tools"` (default) gives Claude the toolkit above: element ids,
|
|
346
|
+
selectors, OCR, keyboard/mouse, windows, apps, clipboard, and a `done` tool.
|
|
347
|
+
* `mode="computer_use"` uses Claude's computer-use toolset, executed by
|
|
348
|
+
`ComputerUseAdapter` (screenshot scaling and coordinate mapping included).
|
|
349
|
+
* `locum mcp` exposes the same tools to Claude Desktop, Claude Code or any
|
|
350
|
+
MCP client — config in [`examples/mcp_config.json`](https://github.com/israerusan/locum/blob/main/examples/mcp_config.json):
|
|
351
|
+
|
|
352
|
+
```json
|
|
353
|
+
{"mcpServers": {"locum": {"command": "locum", "args": ["mcp", "--policy", "default"]}}}
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
or `claude mcp add locum -- locum mcp --policy default` for Claude Code.
|
|
357
|
+
Details, prompting tips and the safety model: [`docs/LLM.md`](https://github.com/israerusan/locum/blob/main/docs/LLM.md).
|
|
358
|
+
|
|
359
|
+
### Safety policy
|
|
360
|
+
|
|
361
|
+
```python
|
|
362
|
+
from locum.llm import CARD_NUMBER, Policy
|
|
363
|
+
|
|
364
|
+
policy = Policy(
|
|
365
|
+
allowed_apps=["erp", "excel"], # act (and launch) only inside these apps
|
|
366
|
+
deny_text_patterns=[CARD_NUMBER], # never type card numbers (Luhn-checked; IBANs pass)
|
|
367
|
+
confirm=lambda description, details: input(f"Allow {description}? [y/N] ") == "y",
|
|
368
|
+
max_actions=200, max_actions_per_minute=30,
|
|
369
|
+
region=(0, 0, 1920, 1080), # pointer must stay in this rectangle
|
|
370
|
+
dry_run=False, # True: describe instead of act
|
|
371
|
+
)
|
|
372
|
+
strict = Policy.strict() # no launching/clipboard, shells blocked, broad confirmations
|
|
373
|
+
authorized = Policy.strict(allow=[ # default-deny: only these run without a human
|
|
374
|
+
'window "Invoice Entry - ERP" edit',
|
|
375
|
+
'window "Invoice Entry - ERP" button "Save"',
|
|
376
|
+
])
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
The default policy is a **heuristic tripwire**, not authorization. A human
|
|
380
|
+
`confirm` is needed (and without a callback the action is **blocked**) when the
|
|
381
|
+
target looks like a commit — *pay, transfer, wire, submit, send, delete, approve,
|
|
382
|
+
post, purchase, release, …*, their plurals and German SAP labels such as *Buchen*
|
|
383
|
+
and *Überweisen* — when Ctrl/Cmd+Enter or Ctrl/Cmd+S is pressed, or when a click,
|
|
384
|
+
Enter or field entry reaches a control that cannot be identified (nothing under
|
|
385
|
+
the pointer, or no name, automation id, value or named child: an icon-only
|
|
386
|
+
button). Keyword matching misses other languages, "Continue"/"Apply"-style
|
|
387
|
+
commits and shortcuts it was not told about.
|
|
388
|
+
|
|
389
|
+
For money and data actions use **allow-list mode**: with `Policy(allow=[...])` /
|
|
390
|
+
`Policy.strict(allow=[...])` every consequential action (clicks, text entry,
|
|
391
|
+
invoke, launching, clipboard writes, non-navigation keys) needs a human unless one
|
|
392
|
+
of your rules — a selector, `"window=..."`, `"app=..."`, `"launch=..."`,
|
|
393
|
+
`"keys=..."` or `"tool=..."` — matches what really receives it: the control the
|
|
394
|
+
mouse actually presses (not the container around it), or the default button that
|
|
395
|
+
Enter presses. A rule without `keys=` covers only typing, editing and navigation
|
|
396
|
+
keys and Enter/Space on the control it names; application shortcuts such as
|
|
397
|
+
Escape, F-keys or Alt+F4 need a `keys=` rule. Robots and flows are gated only when
|
|
398
|
+
you pass a policy (`Robot(policy=...)`, `run_flow(..., policy=...)`); without one
|
|
399
|
+
they run ungated as before. Text typed into password fields is masked in audit
|
|
400
|
+
logs and recordings. Details: [`docs/LLM.md`](https://github.com/israerusan/locum/blob/main/docs/LLM.md#6-safety-model).
|
|
401
|
+
|
|
402
|
+
### PyAutoGUI compatibility
|
|
403
|
+
|
|
404
|
+
<!-- norun -->
|
|
405
|
+
```python
|
|
406
|
+
import locum.compat.pyautogui as pyautogui # was: import pyautogui
|
|
407
|
+
|
|
408
|
+
pyautogui.moveTo(100, 200, duration=0.5, tween=pyautogui.easeInOutQuad)
|
|
409
|
+
pyautogui.write("Hello!\n", interval=0.05)
|
|
410
|
+
box = pyautogui.locateOnScreen("button.png", confidence=0.9)
|
|
411
|
+
pyautogui.click(pyautogui.center(box))
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
See [`examples/pyautogui_migration.py`](https://github.com/israerusan/locum/blob/main/examples/pyautogui_migration.py) for
|
|
415
|
+
the before/after.
|
|
416
|
+
|
|
417
|
+
## How it compares
|
|
418
|
+
|
|
419
|
+
| | PyAutoGUI | UiPath | **locum** |
|
|
420
|
+
|-----------------------------------|:---------:|:------:|:-----------:|
|
|
421
|
+
| Mouse/keyboard/screenshots | ✓ | ✓ | ✓ |
|
|
422
|
+
| Accessibility selectors (UIA/AX/AT-SPI) | — | ✓ (Windows-first) | ✓ |
|
|
423
|
+
| UiPath-style XML selectors | — | ✓ | ✓ (compatible subset) |
|
|
424
|
+
| Template matching w/ alpha masks, multi-scale | confidence needs OpenCV | ✓ | ✓ (numpy; OpenCV optional) |
|
|
425
|
+
| OCR | — | ✓ | pluggable: Tesseract via `locum[ocr]`; RapidOCR / EasyOCR as separate installs |
|
|
426
|
+
| Native invoke / set-value (works when covered) | — | ✓ | ✓ |
|
|
427
|
+
| Humanized motion, Unicode typing | partial | — | ✓ |
|
|
428
|
+
| Robots, retries, evidence reports | — | ✓ | ✓ (code-first) |
|
|
429
|
+
| Transaction queues | — | ✓ (Orchestrator: central, multi-machine) | ✓ (local JSON file: resumable, multi-process on one machine, leases; no network shares or multi-machine) |
|
|
430
|
+
| Recorder | — | ✓ (Studio) | ✓ (emits Python/YAML with selectors) |
|
|
431
|
+
| Visual designer, Orchestrator, enterprise governance, support | — | ✓ | — |
|
|
432
|
+
| LLM tools, MCP server, computer-use adapter, agent loop | — | add-ons | ✓ built in |
|
|
433
|
+
| Price / license | free, BSD-3 | commercial (free Community tier) | free, MIT |
|
|
434
|
+
|
|
435
|
+
UiPath is a full enterprise platform — Studio's visual designer, Orchestrator
|
|
436
|
+
scheduling and credential vaults, Action Center, governance and vendor
|
|
437
|
+
support. locum does not replace that; it is a **code-first, free,
|
|
438
|
+
LLM-native** library for developers who want UiPath-grade robustness inside
|
|
439
|
+
plain Python. Full write-up: [`docs/COMPARISON.md`](https://github.com/israerusan/locum/blob/main/docs/COMPARISON.md).
|
|
440
|
+
|
|
441
|
+
## Platform details
|
|
442
|
+
|
|
443
|
+
How each platform is tested is in the table at the top; this is what each
|
|
444
|
+
backend uses.
|
|
445
|
+
|
|
446
|
+
| Platform | Input | Screenshots | Windows | Accessibility | Notes |
|
|
447
|
+
|----------|-------|-------------|---------|---------------|-------|
|
|
448
|
+
| Windows 10/11 | SendInput (Unicode, DPI-aware, multi-monitor) | mss / GDI | Win32 | **UI Automation** (comtypes) | run as the same integrity level as the target app (UAC); text locum puts on the clipboard is kept out of Clipboard History (Win+V) and cloud sync (`WindowsBackend(clipboard_history=True)` allows it) |
|
|
449
|
+
| macOS 12+ | Quartz events | Quartz (Retina-aware) | Quartz / AppKit | **AX API** | needs Accessibility + Screen Recording permission; not yet validated on a real Mac |
|
|
450
|
+
| Linux X11 | XTEST (Unicode via spare keycodes) | mss / XGetImage | EWMH | **AT-SPI** (PyGObject) | native X11 clipboard (xclip/xsel optional) |
|
|
451
|
+
| Linux Wayland | XWayland apps only | XWayland only | XWayland only | AT-SPI works | see below |
|
|
452
|
+
| Any (tests, CI, evals) | `MockBackend` / `DemoBackend` | ✓ | ✓ | ✓ | deterministic, no display |
|
|
453
|
+
|
|
454
|
+
**Wayland:** Wayland deliberately prevents applications from
|
|
455
|
+
injecting input into, or capturing, other applications. The X11 backend works
|
|
456
|
+
inside an X11 session and with XWayland windows; native Wayland windows cannot
|
|
457
|
+
be driven or captured through it. Use an X11 session (“GNOME on Xorg”) for
|
|
458
|
+
full-desktop automation; portal-based (`xdg-desktop-portal` RemoteDesktop /
|
|
459
|
+
ScreenCast) support is not implemented yet.
|
|
460
|
+
|
|
461
|
+
## Safety notes for LLM control
|
|
462
|
+
|
|
463
|
+
* An agent with desktop access can do anything **you** can: send e-mail, move
|
|
464
|
+
money, delete files. Run unattended agents in a VM or a dedicated user
|
|
465
|
+
account, never on a machine with privileged sessions open.
|
|
466
|
+
* Keep the default policy (or `Policy.strict()`): consequential actions need
|
|
467
|
+
a human `confirm`; without one they are blocked. Use `allowed_apps` to fence
|
|
468
|
+
the agent into the applications the task needs, and `dry_run=True` to
|
|
469
|
+
rehearse.
|
|
470
|
+
* Keep the fail-safe on (mouse to a corner aborts) and arm the kill hotkey
|
|
471
|
+
(`locum --kill-hotkey agent ...` or `install_kill_hotkey()`).
|
|
472
|
+
* Treat on-screen text as untrusted: a web page or document can contain
|
|
473
|
+
instructions aimed at the model (prompt injection). The policy enforces
|
|
474
|
+
limits regardless of what the model was told.
|
|
475
|
+
* Keep the audit log (`AuditLog(path)` / `--audit FILE`): every tool call, its
|
|
476
|
+
arguments, the policy decision and the result are recorded as JSON lines.
|
|
477
|
+
|
|
478
|
+
## CLI
|
|
479
|
+
|
|
480
|
+
```text
|
|
481
|
+
locum [--backend NAME] [--timeout S] [--json] [--no-failsafe] [--humanize] [--kill-hotkey] COMMAND
|
|
482
|
+
info | screenshot [-o PNG] [--region L,T,W,H] [--monitor N] | windows | tree [--window T] [--depth N] [--all]
|
|
483
|
+
find SELECTOR [--all] | click TARGET | move TARGET | type TEXT [-e] [--into SEL] | hotkey COMBO | press KEY...
|
|
484
|
+
scroll DY [--at TARGET] | wait SELECTOR [--state S] | locate IMAGE [--confidence C] [--click]
|
|
485
|
+
observe [--json] [--screenshot PNG] | inspect [X,Y]
|
|
486
|
+
mcp [--policy strict|default|permissive] [--dry-run] [--audit FILE]
|
|
487
|
+
agent TASK [--model M] [--max-steps N] [--dry-run] [--mode tools|computer_use] [--replay FILE]
|
|
488
|
+
run FLOW.yaml [--var k=v] [--report-dir DIR] [--validate] | record [-o out.py|out.yaml] [--duration S]
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
Exit codes: 0 ok · 1 failed (not found, timeout, policy, failed flow/agent) ·
|
|
492
|
+
2 usage/invalid input · 3 unavailable (backend, capability, missing extra) ·
|
|
493
|
+
4 fail-safe abort · 130 interrupted.
|
|
494
|
+
|
|
495
|
+
## Examples
|
|
496
|
+
|
|
497
|
+
| File | What it shows |
|
|
498
|
+
|------|---------------|
|
|
499
|
+
| [`basic_automation.py`](https://github.com/israerusan/locum/blob/main/examples/basic_automation.py) | windows, selectors, typing, screenshots, template matching |
|
|
500
|
+
| [`sap_style_data_entry.py`](https://github.com/israerusan/locum/blob/main/examples/sap_style_data_entry.py) | robot + CSV work queue entering invoices via selectors, business exceptions, reports |
|
|
501
|
+
| [`excel_report_flow.yaml`](https://github.com/israerusan/locum/blob/main/examples/excel_report_flow.yaml) | YAML flow filling a spreadsheet from CSV |
|
|
502
|
+
| [`llm_agent.py`](https://github.com/israerusan/locum/blob/main/examples/llm_agent.py) | Claude agent with a policy, confirmation callback and audit log |
|
|
503
|
+
| [`mcp_config.json`](https://github.com/israerusan/locum/blob/main/examples/mcp_config.json) | Claude Desktop / Claude Code MCP configuration |
|
|
504
|
+
| [`pyautogui_migration.py`](https://github.com/israerusan/locum/blob/main/examples/pyautogui_migration.py) | one-line PyAutoGUI switch, then the upgrade |
|
|
505
|
+
|
|
506
|
+
All of them run on the demo desktop: `LOCUM_BACKEND=mock python examples/<file>`.
|
|
507
|
+
|
|
508
|
+
## License
|
|
509
|
+
|
|
510
|
+
MIT.
|