PyClickerRPA 0.2.0__tar.gz → 0.3.1__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 (66) hide show
  1. {pyclickerrpa-0.2.0 → pyclickerrpa-0.3.1}/PKG-INFO +7 -4
  2. {pyclickerrpa-0.2.0 → pyclickerrpa-0.3.1}/PyClickerRPA.egg-info/PKG-INFO +7 -4
  3. {pyclickerrpa-0.2.0 → pyclickerrpa-0.3.1}/PyClickerRPA.egg-info/SOURCES.txt +2 -0
  4. {pyclickerrpa-0.2.0 → pyclickerrpa-0.3.1}/README.md +6 -3
  5. {pyclickerrpa-0.2.0 → pyclickerrpa-0.3.1}/autoclicker/capture.py +26 -10
  6. pyclickerrpa-0.3.1/autoclicker/expr.py +94 -0
  7. {pyclickerrpa-0.2.0 → pyclickerrpa-0.3.1}/autoclicker/gui/flow_connection.py +8 -1
  8. pyclickerrpa-0.3.1/autoclicker/gui/flow_editor.py +524 -0
  9. {pyclickerrpa-0.2.0 → pyclickerrpa-0.3.1}/autoclicker/gui/flow_node.py +55 -8
  10. {pyclickerrpa-0.2.0 → pyclickerrpa-0.3.1}/autoclicker/gui/main_window.py +268 -37
  11. {pyclickerrpa-0.2.0 → pyclickerrpa-0.3.1}/autoclicker/gui/qt_runner.py +16 -1
  12. {pyclickerrpa-0.2.0 → pyclickerrpa-0.3.1}/autoclicker/gui/task_widget.py +80 -15
  13. {pyclickerrpa-0.2.0 → pyclickerrpa-0.3.1}/autoclicker/models.py +7 -1
  14. pyclickerrpa-0.3.1/autoclicker/ocr.py +107 -0
  15. {pyclickerrpa-0.2.0 → pyclickerrpa-0.3.1}/autoclicker/runner.py +107 -5
  16. {pyclickerrpa-0.2.0 → pyclickerrpa-0.3.1}/autoclicker/windows.py +24 -0
  17. {pyclickerrpa-0.2.0 → pyclickerrpa-0.3.1}/pyproject.toml +1 -1
  18. pyclickerrpa-0.3.1/tests/test_flow_branch.py +146 -0
  19. {pyclickerrpa-0.2.0 → pyclickerrpa-0.3.1}/tests/test_runner.py +37 -0
  20. pyclickerrpa-0.2.0/autoclicker/gui/flow_editor.py +0 -340
  21. pyclickerrpa-0.2.0/autoclicker/ocr.py +0 -63
  22. {pyclickerrpa-0.2.0 → pyclickerrpa-0.3.1}/LICENSE +0 -0
  23. {pyclickerrpa-0.2.0 → pyclickerrpa-0.3.1}/PyClickerRPA.egg-info/dependency_links.txt +0 -0
  24. {pyclickerrpa-0.2.0 → pyclickerrpa-0.3.1}/PyClickerRPA.egg-info/entry_points.txt +0 -0
  25. {pyclickerrpa-0.2.0 → pyclickerrpa-0.3.1}/PyClickerRPA.egg-info/requires.txt +0 -0
  26. {pyclickerrpa-0.2.0 → pyclickerrpa-0.3.1}/PyClickerRPA.egg-info/top_level.txt +0 -0
  27. {pyclickerrpa-0.2.0 → pyclickerrpa-0.3.1}/autoclicker/__init__.py +0 -0
  28. {pyclickerrpa-0.2.0 → pyclickerrpa-0.3.1}/autoclicker/adb.py +0 -0
  29. {pyclickerrpa-0.2.0 → pyclickerrpa-0.3.1}/autoclicker/app.py +0 -0
  30. {pyclickerrpa-0.2.0 → pyclickerrpa-0.3.1}/autoclicker/clicker.py +0 -0
  31. {pyclickerrpa-0.2.0 → pyclickerrpa-0.3.1}/autoclicker/dpi.py +0 -0
  32. {pyclickerrpa-0.2.0 → pyclickerrpa-0.3.1}/autoclicker/gui/__init__.py +0 -0
  33. {pyclickerrpa-0.2.0 → pyclickerrpa-0.3.1}/autoclicker/gui/hotkeys.py +0 -0
  34. {pyclickerrpa-0.2.0 → pyclickerrpa-0.3.1}/autoclicker/gui/monitor.py +0 -0
  35. {pyclickerrpa-0.2.0 → pyclickerrpa-0.3.1}/autoclicker/gui/preview_label.py +0 -0
  36. {pyclickerrpa-0.2.0 → pyclickerrpa-0.3.1}/autoclicker/gui/recorder.py +0 -0
  37. {pyclickerrpa-0.2.0 → pyclickerrpa-0.3.1}/autoclicker/gui/scheduler_widget.py +0 -0
  38. {pyclickerrpa-0.2.0 → pyclickerrpa-0.3.1}/autoclicker/gui/snippet_library.py +0 -0
  39. {pyclickerrpa-0.2.0 → pyclickerrpa-0.3.1}/autoclicker/gui/testkit_widget.py +0 -0
  40. {pyclickerrpa-0.2.0 → pyclickerrpa-0.3.1}/autoclicker/matcher.py +0 -0
  41. {pyclickerrpa-0.2.0 → pyclickerrpa-0.3.1}/autoclicker/run.py +0 -0
  42. {pyclickerrpa-0.2.0 → pyclickerrpa-0.3.1}/autoclicker/testkit/__init__.py +0 -0
  43. {pyclickerrpa-0.2.0 → pyclickerrpa-0.3.1}/autoclicker/testkit/__main__.py +0 -0
  44. {pyclickerrpa-0.2.0 → pyclickerrpa-0.3.1}/autoclicker/testkit/assertions.py +0 -0
  45. {pyclickerrpa-0.2.0 → pyclickerrpa-0.3.1}/autoclicker/testkit/case.py +0 -0
  46. {pyclickerrpa-0.2.0 → pyclickerrpa-0.3.1}/autoclicker/testkit/loader.py +0 -0
  47. {pyclickerrpa-0.2.0 → pyclickerrpa-0.3.1}/autoclicker/testkit/pytest_plugin.py +0 -0
  48. {pyclickerrpa-0.2.0 → pyclickerrpa-0.3.1}/autoclicker/testkit/report.py +0 -0
  49. {pyclickerrpa-0.2.0 → pyclickerrpa-0.3.1}/autoclicker/testkit/runner.py +0 -0
  50. {pyclickerrpa-0.2.0 → pyclickerrpa-0.3.1}/autoclicker/testkit/visual.py +0 -0
  51. {pyclickerrpa-0.2.0 → pyclickerrpa-0.3.1}/autoclicker/web/__init__.py +0 -0
  52. {pyclickerrpa-0.2.0 → pyclickerrpa-0.3.1}/autoclicker/web/__main__.py +0 -0
  53. {pyclickerrpa-0.2.0 → pyclickerrpa-0.3.1}/autoclicker/web/run_manager.py +0 -0
  54. {pyclickerrpa-0.2.0 → pyclickerrpa-0.3.1}/autoclicker/web/scheduler.py +0 -0
  55. {pyclickerrpa-0.2.0 → pyclickerrpa-0.3.1}/autoclicker/web/serialize.py +0 -0
  56. {pyclickerrpa-0.2.0 → pyclickerrpa-0.3.1}/autoclicker/web/server.py +0 -0
  57. {pyclickerrpa-0.2.0 → pyclickerrpa-0.3.1}/autoclicker/web/static/app.js +0 -0
  58. {pyclickerrpa-0.2.0 → pyclickerrpa-0.3.1}/autoclicker/web/static/index.html +0 -0
  59. {pyclickerrpa-0.2.0 → pyclickerrpa-0.3.1}/autoclicker/web/static/style.css +0 -0
  60. {pyclickerrpa-0.2.0 → pyclickerrpa-0.3.1}/setup.cfg +0 -0
  61. {pyclickerrpa-0.2.0 → pyclickerrpa-0.3.1}/tests/test_adb.py +0 -0
  62. {pyclickerrpa-0.2.0 → pyclickerrpa-0.3.1}/tests/test_matcher.py +0 -0
  63. {pyclickerrpa-0.2.0 → pyclickerrpa-0.3.1}/tests/test_models.py +0 -0
  64. {pyclickerrpa-0.2.0 → pyclickerrpa-0.3.1}/tests/test_testkit.py +0 -0
  65. {pyclickerrpa-0.2.0 → pyclickerrpa-0.3.1}/tests/test_testkit_gui.py +0 -0
  66. {pyclickerrpa-0.2.0 → pyclickerrpa-0.3.1}/tests/test_web.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyClickerRPA
3
- Version: 0.2.0
3
+ Version: 0.3.1
4
4
  Summary: Visual UI automation & testing tool: Windows + Android(ADB) automation, robust/self-healing image matching, OCR, node flow editor, recorder, a visual test framework (assertions, visual regression, HTML/JUnit reports) and a web console.
5
5
  Author: Equinox
6
6
  License-Expression: MIT
@@ -84,12 +84,15 @@ autoclicker-web --suites-dir examples # web console at http://127.0.0.1
84
84
  - **~30+ step types**: click image/text/position, click-all, relative/offset
85
85
  clicks, wait / random / wait-stable, drag / long-press / pinch / scroll,
86
86
  key / type / hotkey, loop / goto / while / do-until, if image/text/color/
87
- count/var, set-var, read-text→variable, HTTP request, run-Python-script,
88
- call sub-task, groups — with retry / repeat / until policies.
87
+ count/var with **true/false branching** (if-else), set-var, read-text→variable,
88
+ activate-window, HTTP request, run-Python-script, call sub-task, groups, and a
89
+ **stop** step to end a run — with retry / repeat / until policies.
89
90
 
90
91
  ## Authoring (desktop GUI)
91
92
 
92
- Drag-and-drop **node flow graph** and an interoperable **step list** with a full
93
+ Drag-and-drop **node flow graph** (condition nodes have **true/false branch
94
+ ports** for visual if-else; a terminal **stop** node ends the flow) and an
95
+ interoperable **step list** with a full
93
96
  per-step dialog; an action **recorder** (global hooks), snippet library, task
94
97
  packages (`.zip` with templates), on-preview coordinate/color picking, template
95
98
  crop, single-step test run; global hotkeys (F6 run/stop, ESC stop), scheduler,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyClickerRPA
3
- Version: 0.2.0
3
+ Version: 0.3.1
4
4
  Summary: Visual UI automation & testing tool: Windows + Android(ADB) automation, robust/self-healing image matching, OCR, node flow editor, recorder, a visual test framework (assertions, visual regression, HTML/JUnit reports) and a web console.
5
5
  Author: Equinox
6
6
  License-Expression: MIT
@@ -84,12 +84,15 @@ autoclicker-web --suites-dir examples # web console at http://127.0.0.1
84
84
  - **~30+ step types**: click image/text/position, click-all, relative/offset
85
85
  clicks, wait / random / wait-stable, drag / long-press / pinch / scroll,
86
86
  key / type / hotkey, loop / goto / while / do-until, if image/text/color/
87
- count/var, set-var, read-text→variable, HTTP request, run-Python-script,
88
- call sub-task, groups — with retry / repeat / until policies.
87
+ count/var with **true/false branching** (if-else), set-var, read-text→variable,
88
+ activate-window, HTTP request, run-Python-script, call sub-task, groups, and a
89
+ **stop** step to end a run — with retry / repeat / until policies.
89
90
 
90
91
  ## Authoring (desktop GUI)
91
92
 
92
- Drag-and-drop **node flow graph** and an interoperable **step list** with a full
93
+ Drag-and-drop **node flow graph** (condition nodes have **true/false branch
94
+ ports** for visual if-else; a terminal **stop** node ends the flow) and an
95
+ interoperable **step list** with a full
93
96
  per-step dialog; an action **recorder** (global hooks), snippet library, task
94
97
  packages (`.zip` with templates), on-preview coordinate/color picking, template
95
98
  crop, single-step test run; global hotkeys (F6 run/stop, ESC stop), scheduler,
@@ -13,6 +13,7 @@ autoclicker/app.py
13
13
  autoclicker/capture.py
14
14
  autoclicker/clicker.py
15
15
  autoclicker/dpi.py
16
+ autoclicker/expr.py
16
17
  autoclicker/matcher.py
17
18
  autoclicker/models.py
18
19
  autoclicker/ocr.py
@@ -52,6 +53,7 @@ autoclicker/web/static/app.js
52
53
  autoclicker/web/static/index.html
53
54
  autoclicker/web/static/style.css
54
55
  tests/test_adb.py
56
+ tests/test_flow_branch.py
55
57
  tests/test_matcher.py
56
58
  tests/test_models.py
57
59
  tests/test_runner.py
@@ -46,12 +46,15 @@ autoclicker-web --suites-dir examples # web console at http://127.0.0.1
46
46
  - **~30+ step types**: click image/text/position, click-all, relative/offset
47
47
  clicks, wait / random / wait-stable, drag / long-press / pinch / scroll,
48
48
  key / type / hotkey, loop / goto / while / do-until, if image/text/color/
49
- count/var, set-var, read-text→variable, HTTP request, run-Python-script,
50
- call sub-task, groups — with retry / repeat / until policies.
49
+ count/var with **true/false branching** (if-else), set-var, read-text→variable,
50
+ activate-window, HTTP request, run-Python-script, call sub-task, groups, and a
51
+ **stop** step to end a run — with retry / repeat / until policies.
51
52
 
52
53
  ## Authoring (desktop GUI)
53
54
 
54
- Drag-and-drop **node flow graph** and an interoperable **step list** with a full
55
+ Drag-and-drop **node flow graph** (condition nodes have **true/false branch
56
+ ports** for visual if-else; a terminal **stop** node ends the flow) and an
57
+ interoperable **step list** with a full
55
58
  per-step dialog; an action **recorder** (global hooks), snippet library, task
56
59
  packages (`.zip` with templates), on-preview coordinate/color picking, template
57
60
  crop, single-step test run; global hotkeys (F6 run/stop, ESC stop), scheduler,
@@ -30,35 +30,51 @@ def _bitmap_to_bgr(bmp, width: int, height: int) -> np.ndarray:
30
30
 
31
31
 
32
32
  def capture_window(hwnd) -> np.ndarray:
33
- """Capture the client area of *hwnd* as a BGR ndarray."""
33
+ """Capture the **client area** of *hwnd* as a BGR ndarray.
34
+
35
+ PrintWindow renders the whole window (title bar + borders + client) from the
36
+ window's top-left, so we render the full window and then crop to the client
37
+ rect. The result's (0, 0) is the client top-left — matching the Clicker's
38
+ ``ClientToScreen`` mapping, so match coords click the right spot (no offset
39
+ by the non-client area / title bar).
40
+ """
34
41
  if not _HAS_WIN32:
35
42
  raise RuntimeError("win32 modules unavailable")
43
+ import ctypes
36
44
 
37
- left, top, right, bottom = win32gui.GetClientRect(hwnd)
38
- width, height = right - left, bottom - top
39
- if width <= 0 or height <= 0:
45
+ _, _, cw, ch = win32gui.GetClientRect(hwnd) # client size
46
+ if cw <= 0 or ch <= 0:
47
+ return np.empty((0, 0, 3), dtype=np.uint8)
48
+
49
+ wl, wt, wr, wb = win32gui.GetWindowRect(hwnd) # whole-window screen rect
50
+ ww, wh = wr - wl, wb - wt
51
+ csx, csy = win32gui.ClientToScreen(hwnd, (0, 0)) # client top-left in screen
52
+ ox, oy = csx - wl, csy - wt # client offset within window
53
+ if ww <= 0 or wh <= 0:
40
54
  return np.empty((0, 0, 3), dtype=np.uint8)
41
55
 
42
56
  hwnd_dc = win32gui.GetWindowDC(hwnd)
43
57
  mfc_dc = win32ui.CreateDCFromHandle(hwnd_dc)
44
58
  save_dc = mfc_dc.CreateCompatibleDC()
45
59
  bmp = win32ui.CreateBitmap()
46
- bmp.CreateCompatibleBitmap(mfc_dc, width, height)
60
+ bmp.CreateCompatibleBitmap(mfc_dc, ww, wh) # full-window sized bitmap
47
61
  save_dc.SelectObject(bmp)
48
62
 
49
63
  try:
50
- import ctypes
51
64
  ok = ctypes.windll.user32.PrintWindow(hwnd, save_dc.GetSafeHdc(), PW_RENDERFULLCONTENT)
52
65
  if not ok:
53
- # Fallback: BitBlt the window DC
54
- save_dc.BitBlt((0, 0), (width, height), mfc_dc, (0, 0), win32con.SRCCOPY)
55
- img = _bitmap_to_bgr(bmp, width, height)
66
+ save_dc.BitBlt((0, 0), (ww, wh), mfc_dc, (0, 0), win32con.SRCCOPY)
67
+ full = _bitmap_to_bgr(bmp, ww, wh)
56
68
  finally:
57
69
  win32gui.DeleteObject(bmp.GetHandle())
58
70
  save_dc.DeleteDC()
59
71
  mfc_dc.DeleteDC()
60
72
  win32gui.ReleaseDC(hwnd, hwnd_dc)
61
- return img
73
+
74
+ ox = max(0, min(ox, max(0, ww - 1)))
75
+ oy = max(0, min(oy, max(0, wh - 1)))
76
+ client = full[oy:oy + ch, ox:ox + cw]
77
+ return np.ascontiguousarray(client)
62
78
 
63
79
 
64
80
  def capture_screen() -> np.ndarray:
@@ -0,0 +1,94 @@
1
+ """Safe arithmetic/string expression evaluator for the SetVar step.
2
+
3
+ `SetVar` values starting with ``=`` are evaluated here (e.g. ``=count + 1``,
4
+ ``=max(a, b) * 2``, ``=upper(name)``). Variable names resolve against the
5
+ runner's variable dict — numeric-looking values become numbers, everything
6
+ else stays a string; unknown names default to 0.
7
+
8
+ Security: this never calls the builtin ``eval``. It walks a whitelisted AST
9
+ and rejects attribute access, subscripting, comprehensions, and any call to a
10
+ function outside a fixed safe set — so ``__import__(...)`` / ``().__class__``
11
+ style escapes raise ``ValueError`` instead of executing.
12
+ """
13
+ from __future__ import annotations
14
+
15
+ import ast
16
+ import operator
17
+
18
+ _BIN_OPS = {
19
+ ast.Add: operator.add, ast.Sub: operator.sub, ast.Mult: operator.mul,
20
+ ast.Div: operator.truediv, ast.FloorDiv: operator.floordiv,
21
+ ast.Mod: operator.mod, ast.Pow: operator.pow,
22
+ }
23
+ _UNARY_OPS = {ast.UAdd: operator.pos, ast.USub: operator.neg, ast.Not: operator.not_}
24
+ _CMP_OPS = {
25
+ ast.Eq: operator.eq, ast.NotEq: operator.ne, ast.Lt: operator.lt,
26
+ ast.LtE: operator.le, ast.Gt: operator.gt, ast.GtE: operator.ge,
27
+ }
28
+
29
+ _FUNCS = {
30
+ "int": int, "float": float, "str": str, "round": round, "abs": abs,
31
+ "min": min, "max": max, "len": len,
32
+ "upper": lambda s: str(s).upper(), "lower": lambda s: str(s).lower(),
33
+ }
34
+
35
+
36
+ def _coerce(value: str):
37
+ """A variable value string → number when it looks numeric, else the string."""
38
+ try:
39
+ f = float(value)
40
+ return int(f) if f.is_integer() else f
41
+ except (TypeError, ValueError):
42
+ return value
43
+
44
+
45
+ def _eval(node, variables):
46
+ if isinstance(node, ast.Expression):
47
+ return _eval(node.body, variables)
48
+ if isinstance(node, ast.Constant): # numbers, strings, True/False/None
49
+ return node.value
50
+ if isinstance(node, ast.Name):
51
+ return _coerce(variables.get(node.id, "0"))
52
+ if isinstance(node, ast.BinOp) and type(node.op) in _BIN_OPS:
53
+ return _BIN_OPS[type(node.op)](_eval(node.left, variables), _eval(node.right, variables))
54
+ if isinstance(node, ast.UnaryOp) and type(node.op) in _UNARY_OPS:
55
+ return _UNARY_OPS[type(node.op)](_eval(node.operand, variables))
56
+ if isinstance(node, ast.BoolOp):
57
+ vals = [_eval(v, variables) for v in node.values]
58
+ result = vals[0]
59
+ for v in vals[1:]:
60
+ result = (result and v) if isinstance(node.op, ast.And) else (result or v)
61
+ return result
62
+ if isinstance(node, ast.Compare):
63
+ left = _eval(node.left, variables)
64
+ for op, comparator in zip(node.ops, node.comparators):
65
+ if type(op) not in _CMP_OPS:
66
+ raise ValueError(f"不支持的比较运算符: {type(op).__name__}")
67
+ right = _eval(comparator, variables)
68
+ if not _CMP_OPS[type(op)](left, right):
69
+ return False
70
+ left = right
71
+ return True
72
+ if isinstance(node, ast.Call):
73
+ if not isinstance(node.func, ast.Name) or node.func.id not in _FUNCS:
74
+ raise ValueError("只允许调用: " + ", ".join(sorted(_FUNCS)))
75
+ if node.keywords:
76
+ raise ValueError("函数不支持关键字参数")
77
+ args = [_eval(a, variables) for a in node.args]
78
+ return _FUNCS[node.func.id](*args)
79
+ raise ValueError(f"表达式包含不允许的语法: {type(node).__name__}")
80
+
81
+
82
+ def safe_eval(expr: str, variables: dict) -> str:
83
+ """Evaluate `expr` against `variables`, returning a string result.
84
+
85
+ Integer-valued floats render without a trailing ``.0`` so counters stay clean.
86
+ Raises ``ValueError`` (or SyntaxError) on anything outside the whitelist.
87
+ """
88
+ tree = ast.parse(expr, mode="eval")
89
+ result = _eval(tree, variables)
90
+ if isinstance(result, float) and result.is_integer():
91
+ return str(int(result))
92
+ if isinstance(result, bool):
93
+ return "1" if result else "0"
94
+ return str(result)
@@ -65,7 +65,14 @@ class FlowConnection(QGraphicsPathItem):
65
65
  dy = end.y() - start.y()
66
66
  ctrl = min(dy * 0.5, 80.0)
67
67
  path.cubicTo(start.x(), start.y() + ctrl, end.x(), end.y() - ctrl, end.x(), end.y())
68
- self.setPen(QPen(QColor(200, 200, 200), 2))
68
+ branch = getattr(self.from_port, "branch", None)
69
+ if branch == "true":
70
+ col = QColor(76, 175, 80) # 真:绿
71
+ elif branch == "false":
72
+ col = QColor(229, 57, 53) # 假:红
73
+ else:
74
+ col = QColor(200, 200, 200)
75
+ self.setPen(QPen(col, 2))
69
76
 
70
77
  self.setPath(path)
71
78
  mid = path.pointAtPercent(0.5)