smithy-engine 0.6.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.
- smithy_engine-0.6.0/.github/workflows/ci.yml +75 -0
- smithy_engine-0.6.0/.github/workflows/release.yml +54 -0
- smithy_engine-0.6.0/.gitignore +25 -0
- smithy_engine-0.6.0/CHANGELOG.md +144 -0
- smithy_engine-0.6.0/PKG-INFO +372 -0
- smithy_engine-0.6.0/README.md +344 -0
- smithy_engine-0.6.0/bot.py +31 -0
- smithy_engine-0.6.0/examples/basic_bot.py +30 -0
- smithy_engine-0.6.0/examples/config_demo.py +49 -0
- smithy_engine-0.6.0/examples/config_demo.toml +18 -0
- smithy_engine-0.6.0/examples/config_demo_broken.toml +3 -0
- smithy_engine-0.6.0/examples/custom_tool.py +36 -0
- smithy_engine-0.6.0/examples/notepad_click.json +30 -0
- smithy_engine-0.6.0/examples/reframework_bot.py +126 -0
- smithy_engine-0.6.0/examples/reframework_bot.toml +14 -0
- smithy_engine-0.6.0/examples/reframework_invoices.csv +4 -0
- smithy_engine-0.6.0/flow.json +154 -0
- smithy_engine-0.6.0/my.flow.json +15 -0
- smithy_engine-0.6.0/pyproject.toml +59 -0
- smithy_engine-0.6.0/recording.json +1430 -0
- smithy_engine-0.6.0/requirements.lock +89 -0
- smithy_engine-0.6.0/schemas/flow-v2.schema.json +116 -0
- smithy_engine-0.6.0/src/smithy/__init__.py +75 -0
- smithy_engine-0.6.0/src/smithy/core/__init__.py +28 -0
- smithy_engine-0.6.0/src/smithy/core/blocking.py +53 -0
- smithy_engine-0.6.0/src/smithy/core/config.py +171 -0
- smithy_engine-0.6.0/src/smithy/core/errors.py +105 -0
- smithy_engine-0.6.0/src/smithy/core/events.py +67 -0
- smithy_engine-0.6.0/src/smithy/core/http_queue.py +298 -0
- smithy_engine-0.6.0/src/smithy/core/logging.py +73 -0
- smithy_engine-0.6.0/src/smithy/core/queue.py +503 -0
- smithy_engine-0.6.0/src/smithy/core/registry.py +66 -0
- smithy_engine-0.6.0/src/smithy/core/retry.py +79 -0
- smithy_engine-0.6.0/src/smithy/core/schema.py +92 -0
- smithy_engine-0.6.0/src/smithy/core/tool.py +114 -0
- smithy_engine-0.6.0/src/smithy/core/transactions.py +523 -0
- smithy_engine-0.6.0/src/smithy/facade.py +652 -0
- smithy_engine-0.6.0/src/smithy/py.typed +0 -0
- smithy_engine-0.6.0/src/smithy/windows/__init__.py +0 -0
- smithy_engine-0.6.0/src/smithy/windows/element.py +84 -0
- smithy_engine-0.6.0/src/smithy/windows/selector.py +320 -0
- smithy_engine-0.6.0/src/smithy/windows/selector_rank.py +280 -0
- smithy_engine-0.6.0/src/smithy/windows/tools/__init__.py +72 -0
- smithy_engine-0.6.0/src/smithy/windows/tools/_resolve.py +139 -0
- smithy_engine-0.6.0/src/smithy/windows/tools/click.py +147 -0
- smithy_engine-0.6.0/src/smithy/windows/tools/clipboard.py +82 -0
- smithy_engine-0.6.0/src/smithy/windows/tools/delay.py +55 -0
- smithy_engine-0.6.0/src/smithy/windows/tools/drag.py +82 -0
- smithy_engine-0.6.0/src/smithy/windows/tools/exists.py +59 -0
- smithy_engine-0.6.0/src/smithy/windows/tools/get_element.py +59 -0
- smithy_engine-0.6.0/src/smithy/windows/tools/get_text.py +76 -0
- smithy_engine-0.6.0/src/smithy/windows/tools/highlight.py +122 -0
- smithy_engine-0.6.0/src/smithy/windows/tools/hover.py +62 -0
- smithy_engine-0.6.0/src/smithy/windows/tools/input_text.py +77 -0
- smithy_engine-0.6.0/src/smithy/windows/tools/keyboard.py +270 -0
- smithy_engine-0.6.0/src/smithy/windows/tools/list_elements.py +92 -0
- smithy_engine-0.6.0/src/smithy/windows/tools/process.py +239 -0
- smithy_engine-0.6.0/src/smithy/windows/tools/screenshot.py +201 -0
- smithy_engine-0.6.0/src/smithy/windows/tools/scroll.py +100 -0
- smithy_engine-0.6.0/src/smithy/windows/tools/select.py +67 -0
- smithy_engine-0.6.0/src/smithy/windows/tools/selector_capture/__init__.py +28 -0
- smithy_engine-0.6.0/src/smithy/windows/tools/selector_capture/__main__.py +5 -0
- smithy_engine-0.6.0/src/smithy/windows/tools/selector_capture/capture.py +387 -0
- smithy_engine-0.6.0/src/smithy/windows/tools/selector_capture/cli.py +149 -0
- smithy_engine-0.6.0/src/smithy/windows/tools/selector_capture/emit.py +151 -0
- smithy_engine-0.6.0/src/smithy/windows/tools/selector_capture/generate.py +274 -0
- smithy_engine-0.6.0/src/smithy/windows/tools/selector_capture/recorder.py +739 -0
- smithy_engine-0.6.0/src/smithy/windows/tools/set_text.py +110 -0
- smithy_engine-0.6.0/src/smithy/windows/tools/wait.py +150 -0
- smithy_engine-0.6.0/src/smithy/windows/tools/window.py +147 -0
- smithy_engine-0.6.0/test.txt +1 -0
- smithy_engine-0.6.0/tests/conftest.py +11 -0
- smithy_engine-0.6.0/tests/core/__init__.py +0 -0
- smithy_engine-0.6.0/tests/core/test_config.py +129 -0
- smithy_engine-0.6.0/tests/core/test_errors.py +95 -0
- smithy_engine-0.6.0/tests/core/test_events.py +217 -0
- smithy_engine-0.6.0/tests/core/test_facade.py +340 -0
- smithy_engine-0.6.0/tests/core/test_http_queue.py +355 -0
- smithy_engine-0.6.0/tests/core/test_jsonl_logging.py +99 -0
- smithy_engine-0.6.0/tests/core/test_queue.py +202 -0
- smithy_engine-0.6.0/tests/core/test_retry.py +106 -0
- smithy_engine-0.6.0/tests/core/test_schema.py +169 -0
- smithy_engine-0.6.0/tests/core/test_tool_registry.py +150 -0
- smithy_engine-0.6.0/tests/core/test_transactions.py +363 -0
- smithy_engine-0.6.0/tests/test_audit_regressions.py +201 -0
- smithy_engine-0.6.0/tests/windows/__init__.py +0 -0
- smithy_engine-0.6.0/tests/windows/test_emit.py +141 -0
- smithy_engine-0.6.0/tests/windows/test_gui_tools.py +712 -0
- smithy_engine-0.6.0/tests/windows/test_input_text.py +17 -0
- smithy_engine-0.6.0/tests/windows/test_keyboard.py +52 -0
- smithy_engine-0.6.0/tests/windows/test_process_allowlist.py +60 -0
- smithy_engine-0.6.0/tests/windows/test_selector.py +157 -0
- smithy_engine-0.6.0/tests/windows/test_selector_capture.py +772 -0
- smithy_engine-0.6.0/tests/windows/test_selector_rank.py +397 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [master, main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [master, main]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
lint:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
steps:
|
|
13
|
+
- uses: actions/checkout@v4
|
|
14
|
+
- uses: astral-sh/setup-uv@v5
|
|
15
|
+
with:
|
|
16
|
+
version: "0.11"
|
|
17
|
+
- uses: actions/setup-python@v5
|
|
18
|
+
with:
|
|
19
|
+
python-version: "3.14"
|
|
20
|
+
- run: uv pip install --system ruff
|
|
21
|
+
- name: Ruff check
|
|
22
|
+
run: ruff check src/ tests/ examples/
|
|
23
|
+
- name: Ruff format check
|
|
24
|
+
run: ruff format --check src/ tests/ examples/
|
|
25
|
+
|
|
26
|
+
typecheck:
|
|
27
|
+
runs-on: ubuntu-latest
|
|
28
|
+
steps:
|
|
29
|
+
- uses: actions/checkout@v4
|
|
30
|
+
- uses: astral-sh/setup-uv@v5
|
|
31
|
+
with:
|
|
32
|
+
version: "0.11"
|
|
33
|
+
- uses: actions/setup-python@v5
|
|
34
|
+
with:
|
|
35
|
+
python-version: "3.14"
|
|
36
|
+
- run: uv pip install --system -e ".[dev]"
|
|
37
|
+
- name: Mypy strict
|
|
38
|
+
run: mypy src/smithy --strict
|
|
39
|
+
|
|
40
|
+
test:
|
|
41
|
+
strategy:
|
|
42
|
+
fail-fast: false
|
|
43
|
+
matrix:
|
|
44
|
+
os: [ubuntu-latest, windows-latest]
|
|
45
|
+
python-version: ["3.11", "3.14"]
|
|
46
|
+
runs-on: ${{ matrix.os }}
|
|
47
|
+
steps:
|
|
48
|
+
- uses: actions/checkout@v4
|
|
49
|
+
- uses: astral-sh/setup-uv@v5
|
|
50
|
+
with:
|
|
51
|
+
version: "0.11"
|
|
52
|
+
- uses: actions/setup-python@v5
|
|
53
|
+
with:
|
|
54
|
+
python-version: ${{ matrix.python-version }}
|
|
55
|
+
- name: Install (with windows extras on windows runners)
|
|
56
|
+
run: uv pip install --system -e ".[dev,windows]"
|
|
57
|
+
- name: Pytest
|
|
58
|
+
run: pytest tests/ -q
|
|
59
|
+
|
|
60
|
+
build:
|
|
61
|
+
runs-on: ubuntu-latest
|
|
62
|
+
steps:
|
|
63
|
+
- uses: actions/checkout@v4
|
|
64
|
+
- uses: astral-sh/setup-uv@v5
|
|
65
|
+
with:
|
|
66
|
+
version: "0.11"
|
|
67
|
+
- uses: actions/setup-python@v5
|
|
68
|
+
with:
|
|
69
|
+
python-version: "3.14"
|
|
70
|
+
- name: Build sdist and wheel
|
|
71
|
+
run: uv build
|
|
72
|
+
- uses: actions/upload-artifact@v4
|
|
73
|
+
with:
|
|
74
|
+
name: dist
|
|
75
|
+
path: dist/
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags: ["v*"]
|
|
6
|
+
|
|
7
|
+
permissions:
|
|
8
|
+
id-token: write # PyPI trusted publishing
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
build:
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v4
|
|
15
|
+
- uses: astral-sh/setup-uv@v5
|
|
16
|
+
with:
|
|
17
|
+
version: "0.11"
|
|
18
|
+
- uses: actions/setup-python@v5
|
|
19
|
+
with:
|
|
20
|
+
python-version: "3.14"
|
|
21
|
+
- name: Build sdist and wheel
|
|
22
|
+
run: uv build
|
|
23
|
+
- uses: actions/upload-artifact@v4
|
|
24
|
+
with:
|
|
25
|
+
name: dist
|
|
26
|
+
path: dist/
|
|
27
|
+
|
|
28
|
+
publish:
|
|
29
|
+
needs: build
|
|
30
|
+
runs-on: ubuntu-latest
|
|
31
|
+
environment: pypi
|
|
32
|
+
steps:
|
|
33
|
+
- uses: actions/download-artifact@v4
|
|
34
|
+
with:
|
|
35
|
+
name: dist
|
|
36
|
+
path: dist/
|
|
37
|
+
- name: Publish to PyPI
|
|
38
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
39
|
+
|
|
40
|
+
github-release:
|
|
41
|
+
needs: build
|
|
42
|
+
runs-on: ubuntu-latest
|
|
43
|
+
permissions:
|
|
44
|
+
contents: write
|
|
45
|
+
steps:
|
|
46
|
+
- uses: actions/download-artifact@v4
|
|
47
|
+
with:
|
|
48
|
+
name: dist
|
|
49
|
+
path: dist/
|
|
50
|
+
- name: Create GitHub release
|
|
51
|
+
uses: softprops/action-gh-release@v2
|
|
52
|
+
with:
|
|
53
|
+
files: dist/*
|
|
54
|
+
generate_release_notes: true
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
__pycache__/
|
|
2
|
+
*.py[cod]
|
|
3
|
+
*.egg-info/
|
|
4
|
+
dist/
|
|
5
|
+
build/
|
|
6
|
+
.mypy_cache/
|
|
7
|
+
.ruff_cache/
|
|
8
|
+
.pytest_cache/
|
|
9
|
+
.venv/
|
|
10
|
+
venv/
|
|
11
|
+
*.egg
|
|
12
|
+
.env
|
|
13
|
+
|
|
14
|
+
# Test artifacts
|
|
15
|
+
selectors.json
|
|
16
|
+
selectors1.json
|
|
17
|
+
selectors2.json
|
|
18
|
+
selectors3.json
|
|
19
|
+
test_output.txt
|
|
20
|
+
|
|
21
|
+
# Lockfile (library, not app)
|
|
22
|
+
uv.lock
|
|
23
|
+
|
|
24
|
+
# Smith Studio (private, paid product)
|
|
25
|
+
apps/smith-studio/
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.5.0
|
|
4
|
+
|
|
5
|
+
Playwright-style codegen: recorded flows render as runnable bot scripts.
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Code generation (`windows/tools/selector_capture/emit.py`): any
|
|
10
|
+
capture file (`single`/`series`/`record` — same `nodes` shape)
|
|
11
|
+
renders as a `Smithy(tools=windows_tools())` script with one
|
|
12
|
+
`await bot.*` call per node. New `emit` CLI subcommand
|
|
13
|
+
(`emit -i flow.json -o bot.py [--clip]`) plus `--emit BOT.py` on
|
|
14
|
+
every record mode for one-pass record-to-code.
|
|
15
|
+
- Honest placeholders instead of silent gaps: a `TODO` header for the
|
|
16
|
+
unseen app launch (`process_run` + PID scoping), `text="TODO: fill
|
|
17
|
+
in"` for series-mode `input_text` (keys are never captured), and
|
|
18
|
+
`WARNING` comments from static selector-fragility scoring.
|
|
19
|
+
|
|
20
|
+
## 0.4.1
|
|
21
|
+
|
|
22
|
+
Unified capture output: every recorder mode writes the same flow shape.
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
|
|
26
|
+
- `single` mode now writes `{"tool": "selector-capture", "nodes":
|
|
27
|
+
[...]}` with one ranked node (same shape as `series`/`record`)
|
|
28
|
+
instead of the divergent `captures`/`best_selector` format. The node
|
|
29
|
+
`args` is the ranked minimal selector, `full_path` is attached, and
|
|
30
|
+
numeric control types are translated — previously `best_selector`
|
|
31
|
+
carried the raw `"50011"` the runtime rejects. The `-d`/
|
|
32
|
+
`--description` flag is still accepted but only logged, not persisted.
|
|
33
|
+
|
|
34
|
+
### Fixed
|
|
35
|
+
|
|
36
|
+
- Series-mode `windows.input_text` nodes were missing `full_path`
|
|
37
|
+
(keyboard flushes built nodes without the last clicked element's
|
|
38
|
+
path) — now every node carries it. Known limitation, now documented:
|
|
39
|
+
series mode captures the input *target*, not the typed text itself.
|
|
40
|
+
|
|
41
|
+
## 0.4.0
|
|
42
|
+
|
|
43
|
+
Playwright-style selector engine for the desktop: ranked selectors with
|
|
44
|
+
uniqueness checks and honest confidence instead of all-fields dumps.
|
|
45
|
+
|
|
46
|
+
### Added
|
|
47
|
+
|
|
48
|
+
- Selector ranking (`windows/selector_rank.py`): candidate generation in
|
|
49
|
+
priority order (automation ID → name + type → class + type, minimal
|
|
50
|
+
first), static stability scoring (dynamic digits/dates, wildcards, hex
|
|
51
|
+
runs, long names), live-desktop uniqueness check, `high`/`medium`/`low`
|
|
52
|
+
confidence with warnings. Low confidence means "add an anchor", never
|
|
53
|
+
a made-up stable selector.
|
|
54
|
+
- `ElementSelector.count_from_desktop(limit)` — bounded tree walk for
|
|
55
|
+
counting matches (strict-mode primitive; dev-time helper, not a runtime
|
|
56
|
+
search path).
|
|
57
|
+
- `resolve_element(..., strict=True)` — fail on ambiguous selectors
|
|
58
|
+
(2+ matches → `InvalidInput`) instead of silently taking the first.
|
|
59
|
+
- `generate_nodes_from_config()` — flow nodes from an already-ranked
|
|
60
|
+
minimal config.
|
|
61
|
+
- Record mode now ranks every capture: logs the winning selector,
|
|
62
|
+
confidence, and warnings, and emits nodes from the ranked config
|
|
63
|
+
(falls back to the unranked dump if the UIA walk fails).
|
|
64
|
+
|
|
65
|
+
### Fixed
|
|
66
|
+
|
|
67
|
+
- Real captures carry numeric control types (`"50000"`) which the
|
|
68
|
+
runtime rejects — `build_inline_selector` now translates them to names
|
|
69
|
+
(`"button"`) and drops untranslatable ones.
|
|
70
|
+
|
|
71
|
+
## 0.3.0
|
|
72
|
+
|
|
73
|
+
GUI batch: comfortable desktop automation on top of the 0.2.0 core.
|
|
74
|
+
|
|
75
|
+
### Added
|
|
76
|
+
|
|
77
|
+
- Ten new Windows tools: `windows.scroll` (wheel over element/point),
|
|
78
|
+
`windows.hover` (menus, tooltips), `windows.exists` (single-lookup
|
|
79
|
+
boolean), `windows.get_text` (ValuePattern → Name fallback),
|
|
80
|
+
`windows.window` (activate/minimize/maximize/restore/move/close by PID
|
|
81
|
+
via Win32), `windows.select` (dropdown/combobox/list via
|
|
82
|
+
SelectionItemPattern), `windows.drag` (two endpoints, coordinates or
|
|
83
|
+
`from_*`/`to_*` selectors), `windows.clipboard` (get/set via
|
|
84
|
+
`pyperclip`), `windows.list_elements` (direct-children dump for
|
|
85
|
+
discovering automation IDs), `windows.highlight` (colored rectangle
|
|
86
|
+
flash for debugging selectors).
|
|
87
|
+
- `Smithy` facade methods for every new tool (`scroll`, `hover`,
|
|
88
|
+
`exists`, `get_text`, `window`, `select`, `drag`, `clipboard`,
|
|
89
|
+
`list_elements`, `highlight`), all accepting an optional `handle` for
|
|
90
|
+
PID scoping.
|
|
91
|
+
- Shared `_resolve.py` helpers: `resolve_point` (coordinates win over
|
|
92
|
+
selectors) and `resolve_element`.
|
|
93
|
+
|
|
94
|
+
### Changed
|
|
95
|
+
|
|
96
|
+
- `windows.click` now takes `button` (left/right), `clicks` (1/2), and
|
|
97
|
+
`x`/`y` coordinate clicks (double right-click = two `RightClick`
|
|
98
|
+
calls; no module-level `DoubleClick` exists in `uiautomation`).
|
|
99
|
+
- `windows.wait` now takes `wait_for` (`appear`/`disappear`) with a
|
|
100
|
+
symmetric poll loop; `PlatformError` mid-poll counts as still present.
|
|
101
|
+
- `smithy[windows]` extra now includes `pyperclip` (clipboard support).
|
|
102
|
+
|
|
103
|
+
## 0.2.0
|
|
104
|
+
|
|
105
|
+
First minor release: transactions, config, and hardening on top of the
|
|
106
|
+
0.1.x tool core.
|
|
107
|
+
|
|
108
|
+
### Added
|
|
109
|
+
|
|
110
|
+
- Transactional queue model (`core/queue.py`): `Queue` protocol,
|
|
111
|
+
`InMemoryQueue` / `SqliteQueue` with atomic FIFO claim, lease expiry,
|
|
112
|
+
`max_attempts` requeue, idempotent add, `run_id` ownership; `HttpQueue`
|
|
113
|
+
client for the orchestrator (stdlib only, retries on 502–504).
|
|
114
|
+
- REFramework-style runner (`core/transactions.py`): `run_transactions` /
|
|
115
|
+
`run_transactions_async`, `BusinessError` vs `InfrastructureError`
|
|
116
|
+
contract, `Cancelled` cooperative stop, background lease heartbeat
|
|
117
|
+
(capped at 30 min), `on_progress` hook, `TransactionReport`.
|
|
118
|
+
- TOML robot config (`core/config.py`): `load_config` with fail-fast
|
|
119
|
+
validation (`required` / `must_exist`), frozen attribute-style `Config`,
|
|
120
|
+
`SMITHY_*` env overlay (`__` nests, TOML-typed values).
|
|
121
|
+
- Schema validation (`core/schema.py`): `ToolRegistry.execute` validates
|
|
122
|
+
configs against `schema()` (hand-rolled subset, no new deps).
|
|
123
|
+
- Tool-level retries (`core/retry.py`): `RetryTool` wrapper
|
|
124
|
+
(`attempts` / `delay_ms` / `retry_on`, defaults to `ElementNotFound`).
|
|
125
|
+
- JSONL audit log (`core/logging.py`): `JsonlEventLogger` middleware with
|
|
126
|
+
`transaction_id`, duration, and error stamped per event.
|
|
127
|
+
- `windows_tools()` factory (`windows/tools/__init__.py`): default tool
|
|
128
|
+
set in one call, UIA imports stay lazy.
|
|
129
|
+
- `ProcessTool` allowlist is now configurable: constructor param,
|
|
130
|
+
`SMITHY_ALLOWED_COMMANDS` env override, `allowed_commands` introspection.
|
|
131
|
+
- `parse_control_type()` is public (`windows/selector.py`).
|
|
132
|
+
- Examples: `reframework_bot.py` (dispatcher + performer skeleton),
|
|
133
|
+
`config_demo.py` with good/broken TOMLs.
|
|
134
|
+
|
|
135
|
+
### Changed
|
|
136
|
+
|
|
137
|
+
- Error model consolidated to the single `ToolError` family; the unused
|
|
138
|
+
legacy `SmithError` / `InvalidParams` / `ContextError` were removed.
|
|
139
|
+
|
|
140
|
+
## 0.1.1
|
|
141
|
+
|
|
142
|
+
- Windows UI tools (process, click, wait, delay, screenshot, input_text,
|
|
143
|
+
keyboard, set_text, get_element), selector capture CLI, middleware
|
|
144
|
+
event bus, `@tool` decorator, `Smithy` facade.
|
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: smithy-engine
|
|
3
|
+
Version: 0.6.0
|
|
4
|
+
Summary: Free Python RPA engine — create automation bots with simple API
|
|
5
|
+
Author: as-kurosss
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Requires-Python: >=3.11
|
|
8
|
+
Provides-Extra: all
|
|
9
|
+
Requires-Dist: mss>=9; extra == 'all'
|
|
10
|
+
Requires-Dist: pillow>=10; extra == 'all'
|
|
11
|
+
Requires-Dist: pynput>=1.7; extra == 'all'
|
|
12
|
+
Requires-Dist: pyperclip>=1.8; extra == 'all'
|
|
13
|
+
Requires-Dist: uiautomation>=2.0.29; extra == 'all'
|
|
14
|
+
Provides-Extra: capture
|
|
15
|
+
Requires-Dist: pynput>=1.7; extra == 'capture'
|
|
16
|
+
Requires-Dist: pyperclip>=1.8; extra == 'capture'
|
|
17
|
+
Provides-Extra: dev
|
|
18
|
+
Requires-Dist: mypy>=1.18; extra == 'dev'
|
|
19
|
+
Requires-Dist: pytest-asyncio>=1.0; extra == 'dev'
|
|
20
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
21
|
+
Requires-Dist: ruff>=0.12; extra == 'dev'
|
|
22
|
+
Provides-Extra: windows
|
|
23
|
+
Requires-Dist: mss>=9; extra == 'windows'
|
|
24
|
+
Requires-Dist: pillow>=10; extra == 'windows'
|
|
25
|
+
Requires-Dist: pyperclip>=1.8; extra == 'windows'
|
|
26
|
+
Requires-Dist: uiautomation>=2.0.29; extra == 'windows'
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# Smithy
|
|
30
|
+
|
|
31
|
+
Free Python RPA engine — create automation bots with simple async API.
|
|
32
|
+
|
|
33
|
+
## Quick Start
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
import asyncio
|
|
37
|
+
from smithy import Smithy
|
|
38
|
+
from smithy.windows.tools import windows_tools
|
|
39
|
+
|
|
40
|
+
bot = Smithy(tools=windows_tools())
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
async def main() -> None:
|
|
44
|
+
app = await bot.process_run("notepad.exe")
|
|
45
|
+
await bot.wait(app, class_name="Notepad", name="*Notepad")
|
|
46
|
+
await bot.click(app, name="File")
|
|
47
|
+
await bot.delay(duration_ms=300)
|
|
48
|
+
await bot.click(app, name="Save As...")
|
|
49
|
+
await bot.input_text(app, text="hello world")
|
|
50
|
+
await bot.keyboard(keys="[CTRL]S")
|
|
51
|
+
await bot.screenshot("notepad.png")
|
|
52
|
+
await bot.process_stop(app)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
asyncio.run(main())
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Built-in Tools
|
|
59
|
+
|
|
60
|
+
- **ProcessTool** (`windows.process`) — launch and stop Windows processes by name
|
|
61
|
+
- **ClickTool** (`windows.click`) — click a UI element or coordinates; `button` (left/right), `clicks` (1/2)
|
|
62
|
+
- **WaitTool** (`windows.wait`) — poll until a UI element appears or disappears (`wait_for`, with timeout)
|
|
63
|
+
- **DelayTool** (`windows.delay`) — pause execution for a fixed duration
|
|
64
|
+
- **ScreenshotTool** (`windows.screenshot`) — capture the screen or a window to a file
|
|
65
|
+
- **InputTextTool** (`windows.input_text`) — type plain text into a UI element
|
|
66
|
+
- **KeyboardTool** (`windows.keyboard`) — send key combos and presses (e.g. `"[CTRL]S"`, `"[CTRL!]"`, `"[ENTER]"`)
|
|
67
|
+
- **SetTextTool** (`windows.set_text`) — replace a UI element's text programmatically (ValuePattern / WM_SETTEXT)
|
|
68
|
+
- **GetElementTool** (`windows.get_element`) — read a UI element's attributes as a dict
|
|
69
|
+
- **ScrollTool** (`windows.scroll`) — scroll the wheel over an element or point (`direction`, `wheel_clicks`)
|
|
70
|
+
- **HoverTool** (`windows.hover`) — move the mouse over an element (menus, tooltips)
|
|
71
|
+
- **ExistsTool** (`windows.exists`) — single-lookup boolean check (no waiting, no raising)
|
|
72
|
+
- **GetTextTool** (`windows.get_text`) — read an element's visible text (ValuePattern → Name)
|
|
73
|
+
- **WindowTool** (`windows.window`) — activate/minimize/maximize/restore/move/close a window by PID
|
|
74
|
+
- **SelectTool** (`windows.select`) — select an item in a dropdown, combobox, or list
|
|
75
|
+
- **DragTool** (`windows.drag`) — drag between two endpoints (coordinates or `from_*`/`to_*` selectors)
|
|
76
|
+
- **ClipboardTool** (`windows.clipboard`) — read/write clipboard text (needs `pyperclip`)
|
|
77
|
+
- **ListElementsTool** (`windows.list_elements`) — list direct children to discover automation IDs
|
|
78
|
+
- **HighlightTool** (`windows.highlight`) — flash a colored rectangle for debugging selectors
|
|
79
|
+
|
|
80
|
+
All UI tools accept optional `pid` (or a `ProcessHandle`) to scope element search to a specific window.
|
|
81
|
+
|
|
82
|
+
`ProcessTool` only starts executables from its allowlist — pass
|
|
83
|
+
`windows_tools(allowed_commands=["myapp.exe"])` or set
|
|
84
|
+
`SMITHY_ALLOWED_COMMANDS="myapp.exe,other.exe"` to override the demo list.
|
|
85
|
+
|
|
86
|
+
## Custom Tools
|
|
87
|
+
|
|
88
|
+
Create tools from simple async functions:
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
from smithy import Smithy, tool
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@tool("greet", description="Greet a person")
|
|
95
|
+
async def greet(config: dict) -> dict:
|
|
96
|
+
name = config.get("name", "World")
|
|
97
|
+
return {"message": f"Hello, {name}!"}
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
bot = Smithy(tools=[greet])
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
async def main() -> None:
|
|
104
|
+
result = await bot.call("greet", name="Alice")
|
|
105
|
+
print(result["message"]) # Hello, Alice!
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
asyncio.run(main())
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Transactions (REFramework-style)
|
|
112
|
+
|
|
113
|
+
The framework owns the Init → Get → Process → SetStatus → End loop over a
|
|
114
|
+
queue (local SQLite file or orchestrator via `HttpQueue`):
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
import asyncio
|
|
118
|
+
from smithy import InMemoryQueue, run_transactions_async
|
|
119
|
+
from smithy.core.errors import BusinessError
|
|
120
|
+
|
|
121
|
+
queue = InMemoryQueue()
|
|
122
|
+
queue.get_or_create_queue("invoices", max_attempts=3)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
async def process(item) -> dict:
|
|
126
|
+
if not item.payload.get("number"):
|
|
127
|
+
raise BusinessError("invoice has no number") # terminal, no retry
|
|
128
|
+
return {"posted": True}
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
async def main() -> None:
|
|
132
|
+
report = await run_transactions_async(queue, "invoices", process)
|
|
133
|
+
print(report.processed, report.succeeded, report.business_failed)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
asyncio.run(main())
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
`BusinessError` marks an item terminally failed; `InfrastructureError` (or
|
|
140
|
+
any unexpected exception) requeues it within the `max_attempts` budget;
|
|
141
|
+
`Cancelled` stops the loop cooperatively. Long items get a background
|
|
142
|
+
lease heartbeat (capped at 30 minutes). See
|
|
143
|
+
[`examples/reframework_bot.py`](examples/reframework_bot.py) for a full
|
|
144
|
+
dispatcher + performer skeleton.
|
|
145
|
+
|
|
146
|
+
## Robot Config (TOML)
|
|
147
|
+
|
|
148
|
+
One TOML per robot (replaces the two-column Excel sheet), validated up
|
|
149
|
+
front — the bot fails in Init, never mid-run:
|
|
150
|
+
|
|
151
|
+
```python
|
|
152
|
+
from smithy import load_config
|
|
153
|
+
|
|
154
|
+
CONFIG = load_config(
|
|
155
|
+
"reframework_bot.toml",
|
|
156
|
+
required=["robot.queue", "paths.workdir"],
|
|
157
|
+
must_exist=["paths.workdir"],
|
|
158
|
+
)
|
|
159
|
+
print(CONFIG.robot.queue) # attribute access, frozen after load
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Per-environment tweaks without editing TOML via `SMITHY_*` env vars:
|
|
163
|
+
`SMITHY_ROBOT__QUEUE=invoices-prod` overrides `robot.queue` (`__` nests,
|
|
164
|
+
values are TOML-typed). Secrets never live here — only references to
|
|
165
|
+
orchestrator assets. See [`examples/config_demo.py`](examples/config_demo.py).
|
|
166
|
+
|
|
167
|
+
## Error Handling
|
|
168
|
+
|
|
169
|
+
```python
|
|
170
|
+
from smithy.core.errors import InvalidInput, ElementNotFound, PlatformError
|
|
171
|
+
|
|
172
|
+
try:
|
|
173
|
+
await bot.click(app, name="Nonexistent")
|
|
174
|
+
except ElementNotFound:
|
|
175
|
+
print("Element not found")
|
|
176
|
+
except PlatformError as e:
|
|
177
|
+
print(f"Platform error: {e}")
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## Selector Ranking (Playwright-style)
|
|
181
|
+
|
|
182
|
+
Record mode (`record` below) ranks every captured element like
|
|
183
|
+
Playwright's codegen: candidates in priority order (automation ID →
|
|
184
|
+
name + type → class + type), stability scoring, and a live uniqueness
|
|
185
|
+
check. The winning selector ships with `high`/`medium`/`low` confidence
|
|
186
|
+
plus warnings — `low` means the element needs an anchor, not blind trust:
|
|
187
|
+
|
|
188
|
+
```python
|
|
189
|
+
from smithy.windows.selector_rank import rank_best_selector
|
|
190
|
+
from smithy.windows.tools.selector_capture.capture import capture_at_point
|
|
191
|
+
|
|
192
|
+
_, sel = capture_at_point(400, 300)
|
|
193
|
+
ranked = rank_best_selector(sel)
|
|
194
|
+
print(ranked.config) # e.g. {"automation_id": "btnOk"}
|
|
195
|
+
print(ranked.confidence, ranked.warnings)
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
`resolve_element(..., strict=True)` fails on ambiguous selectors (2+
|
|
199
|
+
matches) instead of taking the first — the desktop equivalent of strict
|
|
200
|
+
mode. Numeric control types from real captures (`"50000"`) are
|
|
201
|
+
translated to names automatically.
|
|
202
|
+
|
|
203
|
+
## Selector Capture
|
|
204
|
+
|
|
205
|
+
A dev utility for inspecting UI elements at screen coordinates and generating tool configs:
|
|
206
|
+
|
|
207
|
+
```bash
|
|
208
|
+
pip install smithy-engine[capture]
|
|
209
|
+
|
|
210
|
+
# Single capture mode — one flow node
|
|
211
|
+
python -m smithy.windows.tools.selector_capture single -o selectors.json
|
|
212
|
+
|
|
213
|
+
# Series mode — auto-record clicks and typing
|
|
214
|
+
python -m smithy.windows.tools.selector_capture series -o recording.json
|
|
215
|
+
|
|
216
|
+
# Interactive record mode
|
|
217
|
+
python -m smithy.windows.tools.selector_capture record -o flow.json
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
All three modes write the same shape — `{"tool": "selector-capture",
|
|
221
|
+
"nodes": [{"tool", "args", "full_path"}]}` (`single` is just a
|
|
222
|
+
one-node flow). `args` holds the ranked minimal selector (the
|
|
223
|
+
`best_selector` equivalent), `full_path` the full UIA path for debugging
|
|
224
|
+
and anchors. Note: series mode records click targets with full paths,
|
|
225
|
+
but keyboard input captures only the target element, not the typed text
|
|
226
|
+
itself — fill in `text` afterwards or use record mode.
|
|
227
|
+
|
|
228
|
+
## Codegen (Playwright-style code recording)
|
|
229
|
+
|
|
230
|
+
Any capture file renders as a replayable bot script — record once, get
|
|
231
|
+
runnable code:
|
|
232
|
+
|
|
233
|
+
```bash
|
|
234
|
+
python -m smithy.windows.tools.selector_capture emit -i flow.json -o bot.py
|
|
235
|
+
|
|
236
|
+
# ...or in one pass, straight from recording:
|
|
237
|
+
python -m smithy.windows.tools.selector_capture record -o flow.json --emit bot.py
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
The script uses `Smithy(tools=windows_tools())` with one `await bot.*`
|
|
241
|
+
call per node. No magic: the recorder never sees the launched process
|
|
242
|
+
(so there's a `TODO` showing `process_run` + PID scoping), uncaptured
|
|
243
|
+
`input_text` gets an explicit `text="TODO: fill in"` placeholder, and
|
|
244
|
+
fragile selectors ship with `WARNING` comments. Open `bot.py` in your
|
|
245
|
+
editor, fill in the TODOs, run.
|
|
246
|
+
|
|
247
|
+
## Visual Editor
|
|
248
|
+
|
|
249
|
+
The flow is built in [smithy-designer](https://github.com/as-kurosss/smithy-engine-designer) —
|
|
250
|
+
a separate visual editor (MIT): drag-and-drop canvas, step debugger with
|
|
251
|
+
breakpoints, XML-like selectors, typed variables.
|
|
252
|
+
|
|
253
|
+
```bash
|
|
254
|
+
pip install smithy-designer
|
|
255
|
+
smithy-designer flow.json
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
## Flow format (v2)
|
|
259
|
+
|
|
260
|
+
The flow file is a versioned JSON document — the contract between the
|
|
261
|
+
designer, the file on disk, and the execution engine. The schema lives in
|
|
262
|
+
[`schemas/flow-v2.schema.json`](schemas/flow-v2.schema.json).
|
|
263
|
+
|
|
264
|
+
Compatibility rules:
|
|
265
|
+
|
|
266
|
+
- **adding optional fields does not bump the version** — unknown keys are
|
|
267
|
+
ignored by older readers (`label`, `breakpoints` were added this way);
|
|
268
|
+
- **removing/renaming fields or changing semantics requires v3** and a
|
|
269
|
+
migration path; readers must reject unknown versions with an explicit error;
|
|
270
|
+
- the engine and the designer both validate `version` on load and never
|
|
271
|
+
silently overwrite a file of a different version.
|
|
272
|
+
|
|
273
|
+
Example:
|
|
274
|
+
|
|
275
|
+
```json
|
|
276
|
+
{
|
|
277
|
+
"version": 2,
|
|
278
|
+
"nodes": [
|
|
279
|
+
{ "id": "start", "kind": "start", "config": {}, "position": [120, 160] },
|
|
280
|
+
{ "id": "a1", "kind": "tool", "tool": "windows.click",
|
|
281
|
+
"config": { "name": "OK", "control_type": "Button" },
|
|
282
|
+
"save_as": "result", "position": [340, 160] }
|
|
283
|
+
],
|
|
284
|
+
"edges": [
|
|
285
|
+
{ "id": "e1", "source": "start", "source_handle": "out", "target": "a1" }
|
|
286
|
+
]
|
|
287
|
+
}
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
## Install
|
|
291
|
+
|
|
292
|
+
```bash
|
|
293
|
+
pip install smithy-engine # core (no deps)
|
|
294
|
+
pip install smithy-engine[windows] # Windows UIA tools
|
|
295
|
+
pip install smithy-engine[capture] # selector capture (pynput + pyperclip)
|
|
296
|
+
pip install smithy-engine[all] # everything
|
|
297
|
+
pip install -e ".[dev]" # development
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
## Development
|
|
301
|
+
|
|
302
|
+
```bash
|
|
303
|
+
# Using uv (recommended)
|
|
304
|
+
uv venv .venv
|
|
305
|
+
.venv\Scripts\activate
|
|
306
|
+
uv pip install -e ".[dev,windows,capture]"
|
|
307
|
+
|
|
308
|
+
# Or with pip
|
|
309
|
+
python -m venv .venv
|
|
310
|
+
.venv\Scripts\activate
|
|
311
|
+
pip install -e ".[dev,windows,capture]"
|
|
312
|
+
|
|
313
|
+
pytest # run tests
|
|
314
|
+
ruff check src/ tests/ # linter
|
|
315
|
+
mypy src/smithy --strict # type check
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
## Project Structure
|
|
319
|
+
|
|
320
|
+
```
|
|
321
|
+
src/smithy/
|
|
322
|
+
├── __init__.py — Public API: Smithy, ProcessHandle, Tool, errors
|
|
323
|
+
├── facade.py — Smithy facade (async tool dispatch)
|
|
324
|
+
├── core/
|
|
325
|
+
│ ├── tool.py — Tool protocol, AbstractTool, @tool decorator
|
|
326
|
+
│ ├── registry.py — ToolRegistry (name → tool dispatch, schema validation)
|
|
327
|
+
│ ├── schema.py — Hand-rolled JSON Schema subset validator
|
|
328
|
+
│ ├── retry.py — RetryTool (attempts / delay / retry_on)
|
|
329
|
+
│ ├── logging.py — JsonlEventLogger (JSONL audit log middleware)
|
|
330
|
+
│ ├── config.py — TOML robot config + SMITHY_* env overlay
|
|
331
|
+
│ ├── queue.py — Queue protocol, InMemoryQueue, SqliteQueue
|
|
332
|
+
│ ├── http_queue.py — HttpQueue client for the orchestrator
|
|
333
|
+
│ ├── transactions.py — REFramework-style runner + heartbeat
|
|
334
|
+
│ ├── events.py — EventBus, ToolEvent, Middleware
|
|
335
|
+
│ └── errors.py — Error hierarchy (ToolError, ElementNotFound, etc.)└── windows/
|
|
336
|
+
├── element.py — SafeUIElement (thread-safe COM wrapper)
|
|
337
|
+
├── selector.py — ElementSelector (UIA tree search + match counting)
|
|
338
|
+
├── selector_rank.py — Selector ranking (candidates, scoring, confidence)
|
|
339
|
+
└── tools/
|
|
340
|
+
├── process.py — ProcessTool
|
|
341
|
+
├── click.py — ClickTool (button/clicks/coordinates)
|
|
342
|
+
├── wait.py — WaitTool (appear/disappear)
|
|
343
|
+
├── delay.py — DelayTool
|
|
344
|
+
├── screenshot.py — ScreenshotTool
|
|
345
|
+
├── input_text.py — InputTextTool
|
|
346
|
+
├── keyboard.py — KeyboardTool
|
|
347
|
+
├── set_text.py — SetTextTool
|
|
348
|
+
├── get_element.py — GetElementTool
|
|
349
|
+
├── scroll.py — ScrollTool
|
|
350
|
+
├── hover.py — HoverTool
|
|
351
|
+
├── exists.py — ExistsTool
|
|
352
|
+
├── get_text.py — GetTextTool
|
|
353
|
+
├── window.py — WindowTool
|
|
354
|
+
├── select.py — SelectTool
|
|
355
|
+
├── drag.py — DragTool
|
|
356
|
+
├── clipboard.py — ClipboardTool
|
|
357
|
+
├── list_elements.py — ListElementsTool
|
|
358
|
+
├── highlight.py — HighlightTool
|
|
359
|
+
├── _resolve.py — Shared element/point resolution helpers
|
|
360
|
+
└── selector_capture/ — Dev tool for UI inspection + codegen
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
## Examples
|
|
364
|
+
|
|
365
|
+
- [`examples/basic_bot.py`](examples/basic_bot.py) — Launch Notepad and interact with its UI
|
|
366
|
+
- [`examples/custom_tool.py`](examples/custom_tool.py) — Create and use custom tools
|
|
367
|
+
- [`examples/reframework_bot.py`](examples/reframework_bot.py) — REFramework skeleton: dispatcher + performer over a queue
|
|
368
|
+
- [`examples/config_demo.py`](examples/config_demo.py) — Load and validate a TOML robot config
|
|
369
|
+
|
|
370
|
+
## License
|
|
371
|
+
|
|
372
|
+
MIT
|