pyvisionauto 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.
@@ -0,0 +1,8 @@
1
+ Copyright (c) 2026 PyVisionAuto contributors. All rights reserved.
2
+
3
+ This software and its source code are proprietary and confidential.
4
+ Redistribution, modification, or use of the source code in any form is
5
+ prohibited without explicit written permission from the copyright holder.
6
+
7
+ The compiled/distributed package may be used in accordance with the terms
8
+ provided at: https://pypi.org/project/pyvisionauto/
@@ -0,0 +1,64 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyvisionauto
3
+ Version: 0.1.0
4
+ Summary: PyVisionAuto: Linux end-to-end automation toolkit with visual image matching, mouse/keyboard control, and screen recording
5
+ Author: PyVisionAuto contributors
6
+ License-Expression: LicenseRef-Proprietary
7
+ Project-URL: Homepage, https://pypi.org/project/pyvisionauto/
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.10
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Operating System :: POSIX :: Linux
15
+ Classifier: Environment :: X11 Applications
16
+ Classifier: Topic :: Software Development :: Testing
17
+ Requires-Python: >=3.10
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ Requires-Dist: opencv-python>=4.8.0
21
+ Requires-Dist: mss>=9.0.0
22
+ Requires-Dist: numpy>=1.24.0
23
+ Requires-Dist: pyautogui>=0.9.54
24
+ Requires-Dist: pillow>=10.0.0
25
+ Provides-Extra: test
26
+ Requires-Dist: pytest>=8.0; extra == "test"
27
+ Dynamic: license-file
28
+
29
+ # PyVisionAuto
30
+
31
+ PyVisionAuto (`pyvisionauto`) is a Linux end-to-end automation testing toolkit.
32
+ It is centered on visual image matching and also includes screen recording, mouse automation, and keyboard automation capabilities.
33
+
34
+ ## Scope
35
+
36
+ - Linux only
37
+ - X11 session only
38
+ - Real physical display required
39
+
40
+ ## Install
41
+
42
+ ```bash
43
+ pip install pyvisionauto
44
+ ```
45
+
46
+ ## System dependencies
47
+
48
+ - python3-tk (for border overlay highlight)
49
+ - xdotool (preferred for window activation)
50
+ - wmctrl (fallback for window activation)
51
+ - ffmpeg (optional, only for recording APIs)
52
+
53
+ ## Quick start
54
+
55
+ ```python
56
+ from pyvisionauto import Screen
57
+
58
+ screen = Screen()
59
+ screen.wait("login_button.png", timeout=10).highlight().click()
60
+ ```
61
+
62
+ ## Notes
63
+
64
+ Wayland-first and headless-only environments are not supported in v0.1.
@@ -0,0 +1,36 @@
1
+ # PyVisionAuto
2
+
3
+ PyVisionAuto (`pyvisionauto`) is a Linux end-to-end automation testing toolkit.
4
+ It is centered on visual image matching and also includes screen recording, mouse automation, and keyboard automation capabilities.
5
+
6
+ ## Scope
7
+
8
+ - Linux only
9
+ - X11 session only
10
+ - Real physical display required
11
+
12
+ ## Install
13
+
14
+ ```bash
15
+ pip install pyvisionauto
16
+ ```
17
+
18
+ ## System dependencies
19
+
20
+ - python3-tk (for border overlay highlight)
21
+ - xdotool (preferred for window activation)
22
+ - wmctrl (fallback for window activation)
23
+ - ffmpeg (optional, only for recording APIs)
24
+
25
+ ## Quick start
26
+
27
+ ```python
28
+ from pyvisionauto import Screen
29
+
30
+ screen = Screen()
31
+ screen.wait("login_button.png", timeout=10).highlight().click()
32
+ ```
33
+
34
+ ## Notes
35
+
36
+ Wayland-first and headless-only environments are not supported in v0.1.
@@ -0,0 +1,49 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "pyvisionauto"
7
+ version = "0.1.0"
8
+ description = "PyVisionAuto: Linux end-to-end automation toolkit with visual image matching, mouse/keyboard control, and screen recording"
9
+ readme = "README.md"
10
+ license = "LicenseRef-Proprietary"
11
+ requires-python = ">=3.10"
12
+ authors = [
13
+ { name = "PyVisionAuto contributors" }
14
+ ]
15
+ dependencies = [
16
+ "opencv-python>=4.8.0",
17
+ "mss>=9.0.0",
18
+ "numpy>=1.24.0",
19
+ "pyautogui>=0.9.54",
20
+ "pillow>=10.0.0"
21
+ ]
22
+ classifiers = [
23
+ "Development Status :: 3 - Alpha",
24
+ "Intended Audience :: Developers",
25
+ "Programming Language :: Python :: 3",
26
+ "Programming Language :: Python :: 3.10",
27
+ "Programming Language :: Python :: 3.11",
28
+ "Programming Language :: Python :: 3.12",
29
+ "Operating System :: POSIX :: Linux",
30
+ "Environment :: X11 Applications",
31
+ "Topic :: Software Development :: Testing"
32
+ ]
33
+
34
+ [project.urls]
35
+ Homepage = "https://pypi.org/project/pyvisionauto/"
36
+
37
+ [project.optional-dependencies]
38
+ test = [
39
+ "pytest>=8.0"
40
+ ]
41
+
42
+ [tool.setuptools]
43
+ package-dir = {"" = "src"}
44
+
45
+ [tool.setuptools.package-data]
46
+ pyvisionauto = ["py.typed"]
47
+
48
+ [tool.setuptools.packages.find]
49
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,36 @@
1
+ from .config import DEFAULT_CONFIDENCE, DEFAULT_POLL_INTERVAL
2
+ from .envcheck import EnvCheck
3
+ from .errors import (
4
+ EnvironmentNotSupportedError,
5
+ OverlayError,
6
+ PyVisionAutoError,
7
+ RecorderError,
8
+ TemplateNotFoundError,
9
+ VanishTimeoutError,
10
+ WaitTimeoutError,
11
+ )
12
+ from .highlighter import Highlighter
13
+ from .input import Input
14
+ from .models import EnvironmentReport, Match, TimingProfile
15
+ from .recorder import Recorder
16
+ from .screen import Screen
17
+
18
+ __all__ = [
19
+ "DEFAULT_CONFIDENCE",
20
+ "DEFAULT_POLL_INTERVAL",
21
+ "EnvCheck",
22
+ "EnvironmentNotSupportedError",
23
+ "EnvironmentReport",
24
+ "Highlighter",
25
+ "Input",
26
+ "Match",
27
+ "OverlayError",
28
+ "PyVisionAutoError",
29
+ "Recorder",
30
+ "RecorderError",
31
+ "Screen",
32
+ "TemplateNotFoundError",
33
+ "TimingProfile",
34
+ "VanishTimeoutError",
35
+ "WaitTimeoutError",
36
+ ]
@@ -0,0 +1,23 @@
1
+ from .models import TimingProfile
2
+
3
+ DEFAULT_CONFIDENCE = 0.88
4
+ DEFAULT_POLL_INTERVAL = 0.5
5
+
6
+ HIGHLIGHT_ENABLED = True
7
+ HIGHLIGHT_COLOR = "#ff0000"
8
+ HIGHLIGHT_THICKNESS = 3
9
+ HIGHLIGHT_DURATION_MS = 700
10
+
11
+ DEFAULT_TIMING = TimingProfile(
12
+ typing_delay_min=0.04,
13
+ typing_delay_max=0.12,
14
+ special_char_extra_delay=0.03,
15
+ key_press_delay_min=0.01,
16
+ key_press_delay_max=0.04,
17
+ key_post_delay_min=0.03,
18
+ key_post_delay_max=0.08,
19
+ hotkey_gap_min=0.02,
20
+ hotkey_gap_max=0.06,
21
+ human_like_default=True,
22
+ deterministic_mode=False,
23
+ )
@@ -0,0 +1,87 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import platform
5
+ import shutil
6
+ import subprocess
7
+
8
+ from .errors import EnvironmentNotSupportedError
9
+ from .models import EnvironmentReport
10
+
11
+
12
+ class EnvCheck:
13
+ """Validate runtime prerequisites for PyVisionAuto on Linux desktops."""
14
+
15
+ def check(self, strict: bool = True) -> EnvironmentReport:
16
+ """Run platform and dependency checks.
17
+
18
+ Args:
19
+ strict: If ``True``, raise when environment is not supported.
20
+
21
+ Returns:
22
+ Structured environment report.
23
+
24
+ Raises:
25
+ EnvironmentNotSupportedError: If ``strict=True`` and checks fail.
26
+ """
27
+ is_linux = platform.system().lower() == "linux"
28
+ has_display = bool(os.environ.get("DISPLAY"))
29
+
30
+ x11_ok = False
31
+ if has_display:
32
+ try:
33
+ out = subprocess.run(
34
+ ["sh", "-lc", "echo ${XDG_SESSION_TYPE:-unknown}"],
35
+ capture_output=True,
36
+ text=True,
37
+ check=False,
38
+ )
39
+ x11_ok = out.stdout.strip().lower() in {"x11", "unknown"}
40
+ except Exception:
41
+ x11_ok = False
42
+
43
+ tk_ok = True
44
+ try:
45
+ import tkinter # noqa: F401
46
+ except Exception:
47
+ tk_ok = False
48
+
49
+ xdotool_ok = shutil.which("xdotool") is not None
50
+ wmctrl_ok = shutil.which("wmctrl") is not None
51
+ ibus_ok = shutil.which("ibus") is not None
52
+ ffmpeg_ok = shutil.which("ffmpeg") is not None
53
+
54
+ messages: list[str] = []
55
+ if not is_linux:
56
+ messages.append("Only Linux is supported in v0.1")
57
+ if not has_display:
58
+ messages.append("DISPLAY is missing")
59
+ if not x11_ok:
60
+ messages.append("X11 session not detected")
61
+ if not tk_ok:
62
+ messages.append("tkinter is unavailable (install python3-tk)")
63
+ if not (xdotool_ok or wmctrl_ok):
64
+ messages.append("Neither xdotool nor wmctrl is installed")
65
+
66
+ supported = is_linux and has_display and x11_ok and tk_ok and (xdotool_ok or wmctrl_ok)
67
+ report = EnvironmentReport(
68
+ is_supported=supported,
69
+ platform_ok=is_linux,
70
+ display_ok=has_display,
71
+ x11_ok=x11_ok,
72
+ tk_ok=tk_ok,
73
+ xdotool_ok=xdotool_ok,
74
+ wmctrl_ok=wmctrl_ok,
75
+ ibus_ok=ibus_ok,
76
+ ffmpeg_ok=ffmpeg_ok,
77
+ messages=messages,
78
+ )
79
+
80
+ if strict and not report.is_supported:
81
+ raise EnvironmentNotSupportedError("; ".join(report.messages) or "Unsupported environment")
82
+ return report
83
+
84
+
85
+ def check_env(strict: bool = True) -> EnvironmentReport:
86
+ """Convenience wrapper for :meth:`EnvCheck.check`."""
87
+ return EnvCheck().check(strict=strict)
@@ -0,0 +1,26 @@
1
+ class PyVisionAutoError(Exception):
2
+ """Base exception for PyVisionAuto."""
3
+
4
+
5
+ class TemplateNotFoundError(PyVisionAutoError):
6
+ """Raised when an image template file path does not exist."""
7
+
8
+
9
+ class WaitTimeoutError(PyVisionAutoError):
10
+ """Raised when waiting for a match times out."""
11
+
12
+
13
+ class VanishTimeoutError(PyVisionAutoError):
14
+ """Raised when waiting for an image to vanish times out."""
15
+
16
+
17
+ class EnvironmentNotSupportedError(PyVisionAutoError):
18
+ """Raised when runtime prerequisites are not met."""
19
+
20
+
21
+ class OverlayError(PyVisionAutoError):
22
+ """Raised when highlight overlay cannot be rendered."""
23
+
24
+
25
+ class RecorderError(PyVisionAutoError):
26
+ """Raised when recording actions fail."""
@@ -0,0 +1,74 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ import subprocess
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ from .config import HIGHLIGHT_COLOR, HIGHLIGHT_DURATION_MS, HIGHLIGHT_THICKNESS
9
+ from .errors import OverlayError
10
+ from .models import Match
11
+
12
+ LOGGER = logging.getLogger(__name__)
13
+
14
+
15
+ class Highlighter:
16
+ """Overlay helper used to draw temporary highlight borders for matches."""
17
+
18
+ def __init__(self) -> None:
19
+ """Initialize overlay script path."""
20
+ self._overlay_script = Path(__file__).with_name("overlay.py")
21
+
22
+ def show(
23
+ self,
24
+ match: Match,
25
+ duration_ms: int | None = None,
26
+ color: str | None = None,
27
+ thickness: int | None = None,
28
+ ) -> None:
29
+ """Spawn overlay process to highlight one match rectangle.
30
+
31
+ Args:
32
+ match: Match rectangle to visualize.
33
+ duration_ms: Overlay lifetime in milliseconds. ``None`` uses default.
34
+ color: Border color. ``None`` uses default.
35
+ thickness: Border width in pixels. ``None`` uses default.
36
+
37
+ Raises:
38
+ OverlayError: If the overlay process cannot be started.
39
+ """
40
+ resolved_duration = duration_ms if duration_ms is not None else HIGHLIGHT_DURATION_MS
41
+ resolved_color = color if color is not None else HIGHLIGHT_COLOR
42
+ resolved_thickness = thickness if thickness is not None else HIGHLIGHT_THICKNESS
43
+
44
+ try:
45
+ subprocess.Popen(
46
+ [
47
+ sys.executable,
48
+ str(self._overlay_script),
49
+ str(match.x),
50
+ str(match.y),
51
+ str(match.w),
52
+ str(match.h),
53
+ str(resolved_duration),
54
+ resolved_color,
55
+ str(resolved_thickness),
56
+ ],
57
+ stdout=subprocess.DEVNULL,
58
+ stderr=subprocess.DEVNULL,
59
+ )
60
+ except Exception as exc: # pragma: no cover
61
+ raise OverlayError(str(exc)) from exc
62
+
63
+ def safe_show(
64
+ self,
65
+ match: Match,
66
+ duration_ms: int | None = None,
67
+ color: str | None = None,
68
+ thickness: int | None = None,
69
+ ) -> None:
70
+ """Best-effort highlight wrapper that never interrupts user actions."""
71
+ try:
72
+ self.show(match, duration_ms=duration_ms, color=color, thickness=thickness)
73
+ except OverlayError as exc:
74
+ LOGGER.warning("Highlight failed but action continues: %s", exc)
@@ -0,0 +1,103 @@
1
+ from __future__ import annotations
2
+
3
+ import importlib
4
+ import random
5
+ import time
6
+
7
+ from .config import DEFAULT_TIMING
8
+ from .models import TimingProfile
9
+
10
+
11
+ class Input:
12
+ """Keyboard text/input helper with optional human-like timing behavior."""
13
+
14
+ def __init__(self, timing: TimingProfile | None = None, deterministic: bool | None = None) -> None:
15
+ """Create input helper.
16
+
17
+ Args:
18
+ timing: Timing profile. ``None`` uses ``DEFAULT_TIMING``.
19
+ deterministic: Override deterministic mode. ``None`` follows timing profile.
20
+ """
21
+ self.timing = timing or DEFAULT_TIMING
22
+ self.deterministic = self.timing.deterministic_mode if deterministic is None else deterministic
23
+
24
+ def _get_pyautogui(self):
25
+ """Lazily import pyautogui to reduce module import constraints."""
26
+ return importlib.import_module("pyautogui")
27
+
28
+ def _pause(self, low: float, high: float) -> None:
29
+ """Sleep for a delay interval, deterministic or random based on settings."""
30
+ if self.deterministic:
31
+ time.sleep((low + high) / 2.0)
32
+ return
33
+ time.sleep(random.uniform(low, high))
34
+
35
+ def type_text(
36
+ self,
37
+ text: str,
38
+ human_like: bool | None = None,
39
+ delay_min: float | None = None,
40
+ delay_max: float | None = None,
41
+ ) -> None:
42
+ """Type text into the active UI control.
43
+
44
+ Args:
45
+ text: Text content to type.
46
+ human_like: Whether to use per-character delays. ``None`` uses timing default.
47
+ delay_min: Minimum delay between characters in seconds.
48
+ delay_max: Maximum delay between characters in seconds.
49
+ """
50
+ pyautogui = self._get_pyautogui()
51
+ use_human = self.timing.human_like_default if human_like is None else human_like
52
+ if not use_human:
53
+ pyautogui.write(text)
54
+ return
55
+
56
+ low = self.timing.typing_delay_min if delay_min is None else delay_min
57
+ high = self.timing.typing_delay_max if delay_max is None else delay_max
58
+
59
+ for ch in text:
60
+ pyautogui.write(ch)
61
+ self._pause(low, high)
62
+ if ch in "/-_.":
63
+ time.sleep(self.timing.special_char_extra_delay)
64
+
65
+ def press(self, key: str, human_like: bool | None = None) -> None:
66
+ """Press one key.
67
+
68
+ Args:
69
+ key: Key name understood by pyautogui (for example ``"enter"``).
70
+ human_like: Whether to apply pre/post action delays.
71
+ """
72
+ pyautogui = self._get_pyautogui()
73
+ use_human = self.timing.human_like_default if human_like is None else human_like
74
+ if use_human:
75
+ self._pause(self.timing.key_press_delay_min, self.timing.key_press_delay_max)
76
+ pyautogui.press(key)
77
+ if use_human:
78
+ self._pause(self.timing.key_post_delay_min, self.timing.key_post_delay_max)
79
+
80
+ def hotkey(self, *keys: str, human_like: bool | None = None) -> None:
81
+ """Send a key combination.
82
+
83
+ Args:
84
+ *keys: Ordered key sequence, for example ``("ctrl", "v")``.
85
+ human_like: Whether to apply delay between key down/up events.
86
+ """
87
+ pyautogui = self._get_pyautogui()
88
+ use_human = self.timing.human_like_default if human_like is None else human_like
89
+ if not use_human:
90
+ pyautogui.hotkey(*keys)
91
+ return
92
+
93
+ for key in keys:
94
+ pyautogui.keyDown(key)
95
+ self._pause(self.timing.hotkey_gap_min, self.timing.hotkey_gap_max)
96
+ for key in reversed(keys):
97
+ pyautogui.keyUp(key)
98
+ self._pause(self.timing.hotkey_gap_min, self.timing.hotkey_gap_max)
99
+
100
+ def clear_text(self) -> None:
101
+ """Clear active input field using Ctrl+A followed by Backspace."""
102
+ self.hotkey("ctrl", "a")
103
+ self.press("backspace")
@@ -0,0 +1,77 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+
5
+
6
+ @dataclass(frozen=True)
7
+ class Match:
8
+ """Rectangle and confidence returned by template matching.
9
+
10
+ Attributes:
11
+ x: Absolute screen X of the matched top-left corner.
12
+ y: Absolute screen Y of the matched top-left corner.
13
+ w: Matched width in pixels.
14
+ h: Matched height in pixels.
15
+ score: Normalized confidence score in [0.0, 1.0].
16
+ """
17
+
18
+ x: int
19
+ y: int
20
+ w: int
21
+ h: int
22
+ score: float
23
+
24
+ @property
25
+ def center(self) -> tuple[int, int]:
26
+ """Return center point ``(x + w // 2, y + h // 2)``."""
27
+ return (self.x + self.w // 2, self.y + self.h // 2)
28
+
29
+
30
+ @dataclass(frozen=True)
31
+ class TimingProfile:
32
+ """Timing configuration used by :class:`pyvisionauto.input.Input`."""
33
+
34
+ typing_delay_min: float
35
+ typing_delay_max: float
36
+ special_char_extra_delay: float
37
+ key_press_delay_min: float
38
+ key_press_delay_max: float
39
+ key_post_delay_min: float
40
+ key_post_delay_max: float
41
+ hotkey_gap_min: float
42
+ hotkey_gap_max: float
43
+ human_like_default: bool = True
44
+ deterministic_mode: bool = False
45
+
46
+
47
+ @dataclass
48
+ class EnvironmentReport:
49
+ """Result container for environment compatibility checks."""
50
+
51
+ is_supported: bool
52
+ platform_ok: bool
53
+ display_ok: bool
54
+ x11_ok: bool
55
+ tk_ok: bool
56
+ xdotool_ok: bool
57
+ wmctrl_ok: bool
58
+ ibus_ok: bool
59
+ ffmpeg_ok: bool
60
+ messages: list[str] = field(default_factory=list)
61
+
62
+
63
+ @dataclass(frozen=True)
64
+ class Region:
65
+ """Search/capture rectangle in absolute screen coordinates.
66
+
67
+ Attributes:
68
+ x: Absolute screen X of the region top-left corner.
69
+ y: Absolute screen Y of the region top-left corner.
70
+ w: Region width in pixels.
71
+ h: Region height in pixels.
72
+ """
73
+
74
+ x: int
75
+ y: int
76
+ w: int
77
+ h: int
@@ -0,0 +1,51 @@
1
+ from __future__ import annotations
2
+
3
+ import sys
4
+ import tkinter as tk
5
+
6
+
7
+ def main() -> int:
8
+ if len(sys.argv) != 8:
9
+ return 2
10
+
11
+ x = int(sys.argv[1])
12
+ y = int(sys.argv[2])
13
+ w = int(sys.argv[3])
14
+ h = int(sys.argv[4])
15
+ duration_ms = int(sys.argv[5])
16
+ color = sys.argv[6]
17
+ thickness = int(sys.argv[7])
18
+
19
+ # Keep center visually transparent by drawing border only on a canvas.
20
+ root = tk.Tk()
21
+ root.overrideredirect(True)
22
+ root.attributes("-topmost", True)
23
+ root.geometry(f"{w}x{h}+{x}+{y}")
24
+
25
+ bg = "black"
26
+ root.configure(bg=bg)
27
+ try:
28
+ root.wm_attributes("-transparentcolor", bg)
29
+ except tk.TclError:
30
+ # Some X11 window managers do not support transparentcolor.
31
+ root.attributes("-alpha", 0.35)
32
+
33
+ canvas = tk.Canvas(root, width=w, height=h, highlightthickness=0, bg=bg)
34
+ canvas.pack(fill="both", expand=True)
35
+ inset = max(1, thickness // 2)
36
+ canvas.create_rectangle(
37
+ inset,
38
+ inset,
39
+ max(inset + 1, w - inset),
40
+ max(inset + 1, h - inset),
41
+ outline=color,
42
+ width=thickness,
43
+ )
44
+
45
+ root.after(duration_ms, root.destroy)
46
+ root.mainloop()
47
+ return 0
48
+
49
+
50
+ if __name__ == "__main__":
51
+ raise SystemExit(main())
File without changes