pyautoscene 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,5 @@
1
+ from .references import ReferenceImage, ReferenceText
2
+ from .scene import Scene
3
+ from .session import Session
4
+
5
+ __all__ = ["Scene", "Session", "ReferenceImage", "ReferenceText"]
@@ -0,0 +1,38 @@
1
+ from abc import ABC, abstractmethod
2
+
3
+ import pyautogui as gui
4
+ from pyscreeze import Box
5
+
6
+
7
+ class ReferenceElement(ABC):
8
+ """Base class for reference elements used to identify scenes."""
9
+
10
+ @abstractmethod
11
+ def is_visible(self):
12
+ """Detect the presence of the reference element."""
13
+ raise NotImplementedError("Subclasses must implement this method")
14
+
15
+
16
+ class ReferenceImage(ReferenceElement):
17
+ """Reference element that identifies a scene by an image."""
18
+
19
+ def __init__(self, image_path: str):
20
+ self.image_path = image_path
21
+
22
+ def is_visible(self, region: Box | None = None):
23
+ """Method to detect the presence of the image in the current screen."""
24
+ try:
25
+ return gui.locateOnScreen(self.image_path, region=region)
26
+ except gui.ImageNotFoundException:
27
+ return None
28
+
29
+
30
+ class ReferenceText(ReferenceElement):
31
+ """Reference element that identifies a scene by text."""
32
+
33
+ def __init__(self, text: str):
34
+ self.text = text
35
+
36
+ def is_visible(self):
37
+ """Method to detect the presence of the text in the current screen."""
38
+ raise NotImplementedError("Text recognition is not implemented yet.")
pyautoscene/scene.py ADDED
@@ -0,0 +1,61 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Callable, TypedDict
4
+
5
+ from pyscreeze import Box
6
+ from statemachine import State
7
+
8
+ from pyautoscene.utils import is_valid_variable_name
9
+
10
+ from .references import ReferenceElement, ReferenceImage
11
+
12
+
13
+ class ActionInfo(TypedDict):
14
+ """Type definition for action information in a scene."""
15
+
16
+ action: Callable[..., None]
17
+ transitions_to: Scene | None
18
+
19
+
20
+ class Scene(State):
21
+ """A scene represents a state in the GUI automation state machine."""
22
+
23
+ def __init__(
24
+ self,
25
+ name: str,
26
+ elements: list[ReferenceElement] | None = None,
27
+ initial: bool = False,
28
+ ):
29
+ assert is_valid_variable_name(name), (
30
+ f"Invalid scene name: {name}, must be a valid Python identifier."
31
+ )
32
+ super().__init__(name, initial=initial)
33
+ self.elements = elements or []
34
+ self.actions: dict[str, ActionInfo] = {}
35
+
36
+ def action(self, transitions_to: Scene | None = None):
37
+ """Decorator to register an action for this scene."""
38
+
39
+ def decorator(func: Callable[..., None]) -> Callable[..., None]:
40
+ if func.__name__ not in self.actions:
41
+ action_name = func.__name__
42
+ self.actions[action_name] = {
43
+ "action": func,
44
+ "transitions_to": transitions_to,
45
+ }
46
+ return func
47
+
48
+ return decorator
49
+
50
+ def get_action(self, action_name: str) -> ActionInfo | None:
51
+ """Get an action by name."""
52
+ return self.actions.get(action_name)
53
+
54
+ def is_on_screen(self, region: Box | None = None) -> bool:
55
+ """Check if any reference element is currently on screen."""
56
+ # TODO: Refactor after text recognition is implemented
57
+ elements = (elem for elem in self.elements if isinstance(elem, ReferenceImage))
58
+ return all(elem.is_visible(region) for elem in elements)
59
+
60
+ def __repr__(self):
61
+ return f"Scene({self.name!r}, elements={len(self.elements)})"
pyautoscene/session.py ADDED
@@ -0,0 +1,140 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Callable
4
+
5
+ import networkx as nx
6
+ from pyscreeze import Box
7
+ from statemachine import State, StateMachine
8
+ from statemachine.factory import StateMachineMetaclass
9
+ from statemachine.states import States
10
+ from statemachine.transition_list import TransitionList
11
+
12
+ from .scene import Scene
13
+
14
+
15
+ class SceneRecognitionError(Exception):
16
+ pass
17
+
18
+
19
+ def build_dynamic_state_machine(
20
+ scenes: list[Scene],
21
+ ) -> tuple[StateMachine, dict[str, TransitionList], dict[str, Callable]]:
22
+ """Create a dynamic StateMachine class from scenes using StateMachineMetaclass."""
23
+
24
+ states = {scene.name: scene for scene in scenes}
25
+ transitions = {}
26
+ leaf_actions = {}
27
+ for scene in scenes:
28
+ for action_name, action_info in scene.actions.items():
29
+ target_scene = action_info["transitions_to"]
30
+ if target_scene is not None:
31
+ event_name = f"event_{action_name}"
32
+ new_transition = scene.to(target_scene, event=event_name)
33
+ new_transition.on(action_info["action"])
34
+ transitions[event_name] = new_transition
35
+ else:
36
+ leaf_actions[action_name] = action_info["action"]
37
+
38
+ SessionSM = StateMachineMetaclass(
39
+ "SessionSM",
40
+ (StateMachine,),
41
+ {"states": States(states), **transitions}, # type: ignore[call-arg]
42
+ )
43
+ session_sm: StateMachine = SessionSM() # type: ignore[no-redef]
44
+
45
+ return session_sm, transitions, leaf_actions
46
+
47
+
48
+ def get_current_scene(scenes: list[Scene], region: Box | None = None) -> Scene:
49
+ """Get the current scene from the list of scenes."""
50
+ current_scenes = [scene for scene in scenes if scene.is_on_screen(region)]
51
+ if len(current_scenes) == 1:
52
+ return current_scenes[0]
53
+ elif len(current_scenes) > 1:
54
+ raise SceneRecognitionError(
55
+ f"Multiple scenes are currently on screen.\n{' '.join(str(scene) for scene in current_scenes)}"
56
+ )
57
+ else:
58
+ raise SceneRecognitionError("No scene is currently on screen.")
59
+
60
+
61
+ class Session:
62
+ """A session manages the state machine for GUI automation scenes."""
63
+
64
+ def __init__(self, scenes: list[Scene]):
65
+ self._scenes_list = scenes
66
+ self._scenes_dict = {scene.name: scene for scene in scenes}
67
+
68
+ # Create dynamic StateMachine class and instantiate it
69
+ self._sm, self.transitions, self.leaf_actions = build_dynamic_state_machine(
70
+ scenes
71
+ )
72
+ self.graph: nx.MultiDiGraph = nx.nx_pydot.from_pydot(self._sm._graph())
73
+
74
+ @property
75
+ def current_scene(self) -> State:
76
+ """Get the current state."""
77
+ return self._sm.current_state
78
+
79
+ def expect(self, target_scene: Scene, **kwargs):
80
+ """Navigate to a specific scene."""
81
+ if target_scene.is_on_screen():
82
+ return
83
+
84
+ present_scene = get_current_scene(self._scenes_list)
85
+ all_paths = list(
86
+ nx.all_simple_paths(
87
+ self.graph,
88
+ source=present_scene.name,
89
+ target=target_scene.name,
90
+ )
91
+ )
92
+ if len(all_paths) == 0:
93
+ raise SceneRecognitionError(
94
+ f"No path found from {present_scene.name} to {target_scene.name}"
95
+ )
96
+ elif len(all_paths) > 1:
97
+ raise SceneRecognitionError(
98
+ f"Multiple paths found from {present_scene.name} to {target_scene.name}"
99
+ )
100
+
101
+ path = all_paths[0]
102
+ events: list[str] = [
103
+ self.graph.get_edge_data(path[i], path[i + 1])[0]["label"] # type: ignore
104
+ for i in range(len(path) - 1)
105
+ ]
106
+
107
+ for event in events:
108
+ self._sm.send(event, **kwargs)
109
+
110
+ def invoke(self, action_name: str, **kwargs):
111
+ """Invoke an action in the current scene."""
112
+ event_name = f"event_{action_name}"
113
+ transition = next(
114
+ (tr for tr_name, tr in self.transitions.items() if tr_name == event_name),
115
+ None,
116
+ )
117
+ if transition:
118
+ return self._sm.send(event_name, **kwargs)
119
+
120
+ leaf_action = next(
121
+ (
122
+ action
123
+ for name, action in self.leaf_actions.items()
124
+ if name == action_name
125
+ ),
126
+ None,
127
+ )
128
+ if leaf_action:
129
+ return leaf_action(**kwargs)
130
+
131
+ raise ValueError(
132
+ f"Action '{action_name}' not found in current scene '{self.current_scene.name}'"
133
+ )
134
+
135
+ def __repr__(self):
136
+ current = self.current_scene
137
+ current_name = current.name if current else "None"
138
+ return (
139
+ f"Session(scenes={list(self._scenes_dict.keys())}, current={current_name})"
140
+ )
pyautoscene/utils.py ADDED
@@ -0,0 +1,25 @@
1
+ from __future__ import annotations
2
+
3
+ import time
4
+ from keyword import iskeyword
5
+ from typing import Literal
6
+
7
+ import pyautogui as gui
8
+
9
+ LOCATE_AND_CLICK_DELAY = 0.2
10
+
11
+
12
+ def locate_and_click(
13
+ filename: str, clicks: int = 1, button: Literal["left", "right"] = "left"
14
+ ):
15
+ time.sleep(LOCATE_AND_CLICK_DELAY)
16
+ locate = gui.locateOnScreen(filename, grayscale=True)
17
+ assert locate is not None, f"Could not locate {filename} on screen."
18
+ locate_center = (locate.left + locate.width // 2), (locate.top + locate.height // 2)
19
+ gui.moveTo(*locate_center, 0.6, gui.easeInOutQuad) # type: ignore
20
+ gui.click(clicks=clicks, button=button)
21
+ time.sleep(LOCATE_AND_CLICK_DELAY)
22
+
23
+
24
+ def is_valid_variable_name(name):
25
+ return name.isidentifier() and not iskeyword(name)
@@ -0,0 +1,200 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyautoscene
3
+ Version: 0.1.0
4
+ Summary: Advance GUI automation
5
+ Author-email: pritam-dey3 <pritam.pritamdey.984@gmail.com>
6
+ Requires-Python: >=3.13
7
+ Requires-Dist: networkx>=3.5
8
+ Requires-Dist: onnxruntime>=1.22.0
9
+ Requires-Dist: pillow>=11.3.0
10
+ Requires-Dist: pyautogui>=0.9.54
11
+ Requires-Dist: python-statemachine[diagrams]
12
+ Requires-Dist: rapidocr>=3.2.0
13
+ Description-Content-Type: text/markdown
14
+
15
+ # PyAutoScene
16
+
17
+ **Advanced GUI Automation with Scene-Based State Management**
18
+
19
+ PyAutoScene is a Python library that provides a declarative approach to GUI automation by modeling application interfaces as scenes and transitions. It combines element detection with state machine patterns to create robust, maintainable automation scripts.
20
+
21
+ ## 🌟 Features
22
+
23
+ - **Scene-Based Architecture**: Model your application as a collection of scenes with defined elements and transitions
24
+ - **Visual Element Detection**: Supports both image-based element recognition. (Text recognition support coming soon!)
25
+ - **Automatic Navigation**: Intelligent pathfinding between scenes using graph algorithms
26
+ - **Action Decorators**: Clean, declarative syntax for defining scene actions and transitions
27
+
28
+ ## 🚀 Quick Start
29
+
30
+ ### Installation
31
+
32
+ ```bash
33
+ pip install pyautoscene
34
+ ```
35
+
36
+ ### Basic Example
37
+
38
+ Here's how to automate a simple login flow:
39
+
40
+ ```python
41
+ import pyautogui as gui
42
+ from pyautoscene import ReferenceImage, ReferenceText, Scene, Session
43
+ from pyautoscene.utils import locate_and_click
44
+
45
+ # Define scenes
46
+ login = Scene(
47
+ "Login",
48
+ elements=[
49
+ ReferenceText("Welcome to Login"),
50
+ ReferenceImage("references/login_button.png"),
51
+ ],
52
+ initial=True,
53
+ )
54
+
55
+ dashboard = Scene(
56
+ "Dashboard",
57
+ elements=[
58
+ ReferenceText("Dashboard"),
59
+ ReferenceImage("references/user_menu.png"),
60
+ ],
61
+ )
62
+
63
+ # Define actions with transitions
64
+ @login.action(transitions_to=dashboard)
65
+ def perform_login(username: str, password: str):
66
+ """Performs login and transitions to dashboard."""
67
+ locate_and_click("references/username_field.png")
68
+ gui.write(username, interval=0.1)
69
+ gui.press("tab")
70
+ gui.write(password, interval=0.1)
71
+ gui.press("enter")
72
+
73
+ # Create session and navigate
74
+ session = Session(scenes=[login, dashboard])
75
+ session.expect(dashboard, username="user", password="pass")
76
+ ```
77
+
78
+ ## 📖 Core Concepts
79
+
80
+ ### Scenes
81
+
82
+ A **Scene** represents a distinct state in your application's UI. Each scene contains:
83
+
84
+ - **Elements**: Visual markers that identify when the scene is active
85
+ - **Actions**: Functions that can be performed in this scene
86
+ - **Transitions**: Connections to other scenes
87
+
88
+ ```python
89
+ scene = Scene(
90
+ "SceneName",
91
+ elements=[
92
+ ReferenceImage("path/to/image.png"),
93
+ ReferenceText("Expected Text"),
94
+ ],
95
+ initial=False # Set to True for starting scene
96
+ )
97
+ ```
98
+
99
+ ### Reference Elements
100
+
101
+ PyAutoScene supports two types of reference elements:
102
+
103
+ #### ReferenceImage
104
+
105
+ Detects scenes using image matching:
106
+
107
+ ```python
108
+ ReferenceImage("path/to/reference/image.png")
109
+ ```
110
+
111
+ #### ReferenceText
112
+ (Coming soon)
113
+ Detects scenes using text recognition:
114
+
115
+ ```python
116
+ ReferenceText("Expected text on screen")
117
+ ```
118
+
119
+ ### Actions and Transitions
120
+
121
+ Actions are decorated functions that define what can be done in a scene:
122
+
123
+ ```python
124
+ @scene.action(transitions_to=target_scene) # Action that changes scenes
125
+ def action_with_transition():
126
+ # Perform GUI operations
127
+ pass
128
+
129
+ @scene.action() # Action that stays in current scene
130
+ def action_without_transition():
131
+ # Perform GUI operations
132
+ pass
133
+ ```
134
+
135
+ ### Session Management
136
+
137
+ The **Session** class manages the state machine and provides navigation:
138
+
139
+ ```python
140
+ session = Session(scenes=[scene1, scene2, scene3])
141
+
142
+ # Navigate to a specific scene (finds optimal path)
143
+ session.expect(target_scene, **action_params)
144
+
145
+ # Invoke an action in the current scene
146
+ session.invoke("action_name", **action_params)
147
+
148
+ # Get current scene
149
+ current = session.current_scene
150
+ ```
151
+
152
+ ### Automatic Scene Detection
153
+
154
+ PyAutoScene automatically detects which scene is currently active:
155
+
156
+ ```python
157
+ from pyautoscene.session import get_current_scene
158
+
159
+ current_scene = get_current_scene(scenes)
160
+ print(f"Currently on: {current_scene.name}")
161
+ ```
162
+
163
+ ### Path Finding
164
+
165
+ The library uses NetworkX to find optimal paths between scenes:
166
+
167
+ ```python
168
+ # This will automatically navigate: Login → Dashboard → Cart
169
+ session.expect(cart_scene, username="user", password="pass")
170
+ ```
171
+
172
+ ### Error Handling
173
+
174
+ ```python
175
+ from pyautoscene.session import SceneRecognitionError
176
+
177
+ try:
178
+ session.expect(target_scene)
179
+ except SceneRecognitionError as e:
180
+ print(f"Navigation failed: {e}")
181
+ ```
182
+
183
+ ## 🤝 Contributing
184
+
185
+ 1. Fork the repository
186
+ 2. Create a feature branch: `git checkout -b feature-name`
187
+ 3. Make your changes and add tests
188
+ 4. Run pre-commit hooks: `pre-commit run --all-files`
189
+ 5. Submit a pull request
190
+
191
+ ## 📝 License
192
+
193
+ This project is licensed under the Apache License 2.0 - see the [LICENSE](LICENSE) file for details.
194
+
195
+ ## 🔮 Roadmap
196
+
197
+ - [ ] Text recognition implementation
198
+ - [ ] Enhanced image matching algorithms
199
+ - [ ] Multiple session support
200
+
@@ -0,0 +1,9 @@
1
+ pyautoscene/__init__.py,sha256=4ppGaF-TV-r874c4Q679n6gkSflveKTFu_u-eCpG5yU,175
2
+ pyautoscene/references.py,sha256=wAKZIIrZX2gXnuHsXb-eliF6xk-taVG_ZOvWUj8QLV0,1224
3
+ pyautoscene/scene.py,sha256=Koc2hTQQvNOVRfoYvPwtEYNosb1aRuhpo-3KKOuoWfw,2063
4
+ pyautoscene/session.py,sha256=D-YKH8HJNjpqxqV9HdxZA-cbsbYf8_iMR59Tl3IRph4,4862
5
+ pyautoscene/utils.py,sha256=yc6jkaz-X_sUaOpRS9UG42UnFumhMN7648BunRp2PXM,794
6
+ pyautoscene-0.1.0.dist-info/METADATA,sha256=gHbgYywzt8pw1_uSU5YFiKK9EhRL6b73k7bH4uZnCnk,5080
7
+ pyautoscene-0.1.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
8
+ pyautoscene-0.1.0.dist-info/entry_points.txt,sha256=6aKjylfDivCRMrJasIIi7ICU4fZR-8HjcOHhRmHpYpQ,49
9
+ pyautoscene-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ pyautoscene = pyautoscene:main