pyautoscene 0.2.0__py3-none-any.whl → 0.2.2__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.
pyautoscene/scene.py CHANGED
@@ -1,61 +1,61 @@
1
- from __future__ import annotations
2
-
3
- from typing import Callable, TypedDict
4
-
5
- from statemachine import State
6
-
7
- from pyautoscene.utils import is_valid_variable_name
8
-
9
- from .references import ReferenceElement
10
- from .screen import Region
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: Region | 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 self.elements)
59
-
60
- def __repr__(self):
61
- return f"Scene({self.name!r}, elements={len(self.elements)})"
1
+ from __future__ import annotations
2
+
3
+ from typing import Callable, TypedDict
4
+
5
+ from statemachine import State
6
+
7
+ from pyautoscene.utils import is_valid_variable_name
8
+
9
+ from .references import ReferenceElement
10
+ from .region import Region
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: Region | 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.locate(region) for elem in self.elements)
59
+
60
+ def __repr__(self):
61
+ return f"Scene({self.name!r}, elements={len(self.elements)})"
pyautoscene/session.py CHANGED
@@ -1,140 +1,163 @@
1
- from __future__ import annotations
2
-
3
- from typing import Callable
4
-
5
- import networkx as nx
6
- from statemachine import State, StateMachine
7
- from statemachine.factory import StateMachineMetaclass
8
- from statemachine.states import States
9
- from statemachine.transition_list import TransitionList
10
-
11
- from .scene import Scene
12
- from .screen import Region
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: Region | 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
- )
1
+ from __future__ import annotations
2
+
3
+ import time
4
+ from typing import Callable
5
+
6
+ import networkx as nx
7
+ from PIL import Image
8
+ from statemachine import State, StateMachine
9
+ from statemachine.factory import StateMachineMetaclass
10
+ from statemachine.states import States
11
+ from statemachine.transition_list import TransitionList
12
+
13
+ from .references import ImageElement, ReferenceElement
14
+ from .region import Region
15
+ from .scene import Scene
16
+
17
+
18
+ class SceneRecognitionError(Exception):
19
+ pass
20
+
21
+
22
+ def build_dynamic_state_machine(
23
+ scenes: list[Scene],
24
+ ) -> tuple[StateMachine, dict[str, TransitionList], dict[str, Callable]]:
25
+ """Create a dynamic StateMachine class from scenes using StateMachineMetaclass."""
26
+
27
+ states = {scene.name: scene for scene in scenes}
28
+ transitions = {}
29
+ leaf_actions = {}
30
+ for scene in scenes:
31
+ for action_name, action_info in scene.actions.items():
32
+ target_scene = action_info["transitions_to"]
33
+ if target_scene is not None:
34
+ event_name = f"event_{action_name}"
35
+ new_transition = scene.to(target_scene, event=event_name)
36
+ new_transition.on(action_info["action"])
37
+ transitions[event_name] = new_transition
38
+ else:
39
+ leaf_actions[action_name] = action_info["action"]
40
+
41
+ SessionSM = StateMachineMetaclass(
42
+ "SessionSM",
43
+ (StateMachine,),
44
+ {"states": States(states), **transitions}, # type: ignore[call-arg]
45
+ )
46
+ session_sm: StateMachine = SessionSM() # type: ignore[no-redef]
47
+
48
+ return session_sm, transitions, leaf_actions
49
+
50
+
51
+ def get_current_scene(scenes: list[Scene], region: Region | None = None) -> Scene:
52
+ """Get the current scene from the list of scenes."""
53
+ current_scenes = [scene for scene in scenes if scene.is_on_screen(region)]
54
+ if len(current_scenes) == 1:
55
+ return current_scenes[0]
56
+ elif len(current_scenes) > 1:
57
+ raise SceneRecognitionError(
58
+ f"Multiple scenes are currently on screen.\n{' '.join(str(scene) for scene in current_scenes)}"
59
+ )
60
+ else:
61
+ raise SceneRecognitionError("No scene is currently on screen.")
62
+
63
+
64
+ class Session:
65
+ """A session manages the state machine for GUI automation scenes."""
66
+
67
+ def __init__(
68
+ self,
69
+ scenes: list[Scene],
70
+ image_locator: Callable[[Image.Image, Image.Image], list[Region]] | None = None,
71
+ ):
72
+ self._scenes_list = scenes
73
+ self._scenes_dict = {scene.name: scene for scene in scenes}
74
+ self.image_locator = image_locator
75
+ for scene in self._scenes_list:
76
+ for elem in scene.elements:
77
+ if isinstance(elem, ImageElement):
78
+ elem.locator = image_locator
79
+
80
+ # Create dynamic StateMachine class and instantiate it
81
+ self._sm, self.transitions, self.leaf_actions = build_dynamic_state_machine(
82
+ scenes
83
+ )
84
+ self.graph: nx.MultiDiGraph = nx.nx_pydot.from_pydot(self._sm._graph())
85
+
86
+ @property
87
+ def current_scene(self) -> State:
88
+ """Get the current state."""
89
+ return self._sm.current_state
90
+
91
+ def expect(self, target_scene: Scene, **kwargs):
92
+ """Navigate to a specific scene."""
93
+ if target_scene.is_on_screen():
94
+ return
95
+
96
+ present_scene = get_current_scene(self._scenes_list)
97
+ all_paths = list(
98
+ nx.all_simple_paths(
99
+ self.graph, source=present_scene.name, target=target_scene.name
100
+ )
101
+ )
102
+ if len(all_paths) == 0:
103
+ raise SceneRecognitionError(
104
+ f"No path found from {present_scene.name} to {target_scene.name}"
105
+ )
106
+ elif len(all_paths) > 1:
107
+ raise SceneRecognitionError(
108
+ f"Multiple paths found from {present_scene.name} to {target_scene.name}"
109
+ )
110
+
111
+ path = all_paths[0]
112
+ events: list[str] = [
113
+ self.graph.get_edge_data(path[i], path[i + 1])[0]["label"] # type: ignore
114
+ for i in range(len(path) - 1)
115
+ ]
116
+
117
+ for event in events:
118
+ self._sm.send(event, **kwargs)
119
+
120
+ def invoke(self, action_name: str, **kwargs):
121
+ """Invoke an action in the current scene."""
122
+ event_name = f"event_{action_name}"
123
+ transition = next(
124
+ (tr for tr_name, tr in self.transitions.items() if tr_name == event_name),
125
+ None,
126
+ )
127
+ if transition:
128
+ return self._sm.send(event_name, **kwargs)
129
+
130
+ leaf_action = next(
131
+ (
132
+ action
133
+ for name, action in self.leaf_actions.items()
134
+ if name == action_name
135
+ ),
136
+ None,
137
+ )
138
+ if leaf_action:
139
+ return leaf_action(**kwargs)
140
+
141
+ raise ValueError(
142
+ f"Action '{action_name}' not found in current scene '{self.current_scene.name}'"
143
+ )
144
+
145
+ def wait_until(self, target: Scene | ReferenceElement, interval: float = 1):
146
+ """Wait until the target scene or reference element is on screen."""
147
+ found = False
148
+ while not found:
149
+ if isinstance(target, Scene):
150
+ found = target.is_on_screen()
151
+ elif isinstance(target, ReferenceElement):
152
+ found = target.locate() is not None
153
+ else:
154
+ raise TypeError("Target must be a Scene or ReferenceElement.")
155
+ if not found:
156
+ time.sleep(interval)
157
+
158
+ def __repr__(self):
159
+ current = self.current_scene
160
+ current_name = current.name if current else "None"
161
+ return (
162
+ f"Session(scenes={list(self._scenes_dict.keys())}, current={current_name})"
163
+ )
pyautoscene/utils.py CHANGED
@@ -1,25 +1,148 @@
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)
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ import os
5
+ import time
6
+ from keyword import iskeyword
7
+ from typing import Callable, overload
8
+
9
+ import numpy as np
10
+ import pyautogui as gui
11
+ from PIL import Image
12
+
13
+ from ._types import MouseButton, TowardsDirection
14
+ from .constants import LOCATE_AND_CLICK_DELAY, POINTER_SPEED
15
+ from .region import Region, RegionSpec
16
+
17
+ logging.basicConfig(level=logging.INFO)
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ def locate_and_click(
22
+ reference: Image.Image | str,
23
+ clicks: int = 1,
24
+ button: MouseButton = "left",
25
+ region: RegionSpec | None = None,
26
+ confidence: float = 0.999,
27
+ grayscale: bool = True,
28
+ limit: int = 1,
29
+ offset: tuple[int, int] = (0, 0),
30
+ towards: TowardsDirection | None = None,
31
+ ):
32
+ time.sleep(LOCATE_AND_CLICK_DELAY)
33
+ found_region = locate_on_screen(
34
+ reference,
35
+ region=region,
36
+ confidence=confidence,
37
+ grayscale=grayscale,
38
+ limit=limit,
39
+ )
40
+ assert found_region is not None, f"Could not locate {reference} on screen."
41
+ move_and_click(
42
+ found_region, clicks=clicks, button=button, offset=offset, towards=towards
43
+ )
44
+ time.sleep(LOCATE_AND_CLICK_DELAY)
45
+
46
+
47
+ def move_and_click(
48
+ target_region: RegionSpec,
49
+ clicks: int = 1,
50
+ button: MouseButton = "left",
51
+ offset: tuple[int, int] = (0, 0),
52
+ towards: TowardsDirection | None = None,
53
+ ):
54
+ """Move to the center or edge of the region and click.
55
+
56
+ The offset is always added to the calculated target point.
57
+ For example, for 'bottom', offset=(0, 5) means 5 pixels below the bottom edge.
58
+ """
59
+ _target_region = Region.from_spec(target_region)
60
+ base_points = {
61
+ "top": (_target_region.center[0], _target_region.top),
62
+ "left": (_target_region.left, _target_region.center[1]),
63
+ "bottom": (
64
+ _target_region.center[0],
65
+ _target_region.top + _target_region.height - 1,
66
+ ),
67
+ "right": (
68
+ _target_region.left + _target_region.width - 1,
69
+ _target_region.center[1],
70
+ ),
71
+ None: _target_region.center,
72
+ }
73
+ if towards not in base_points:
74
+ raise ValueError(f"Invalid direction: {towards}")
75
+ base = base_points[towards]
76
+ target = (base[0] + offset[0], base[1] + offset[1])
77
+
78
+ current = gui.position()
79
+ duration = np.linalg.norm(np.array(target) - np.array(current)) / POINTER_SPEED
80
+ gui.moveTo(*target, float(duration), gui.easeInOutQuad) # type: ignore
81
+ gui.click(clicks=clicks, button=button)
82
+
83
+
84
+ def is_valid_variable_name(name):
85
+ return name.isidentifier() and not iskeyword(name)
86
+
87
+
88
+ @overload
89
+ def locate_on_screen(
90
+ reference: Image.Image | str,
91
+ region: RegionSpec | None = None,
92
+ confidence: float = 0.999,
93
+ grayscale: bool = True,
94
+ limit: int = 1,
95
+ locator: Callable[[Image.Image, Image.Image], list[Region]] | None = None,
96
+ ) -> Region | None: ...
97
+ @overload
98
+ def locate_on_screen(
99
+ reference: Image.Image | str,
100
+ region: RegionSpec | None = None,
101
+ confidence: float = 0.999,
102
+ grayscale: bool = True,
103
+ limit: int = 1,
104
+ locator: Callable[[Image.Image, Image.Image], list[Region]] | None = None,
105
+ ) -> list[Region] | None: ...
106
+ def locate_on_screen(
107
+ reference: Image.Image | str,
108
+ region: RegionSpec | None = None,
109
+ confidence: float = 0.999,
110
+ grayscale: bool = True,
111
+ limit: int = 1,
112
+ locator: Callable[[Image.Image, Image.Image], list[Region]] | None = None,
113
+ ):
114
+ """Locate a region on the screen."""
115
+ if isinstance(reference, str):
116
+ if not os.path.exists(reference):
117
+ raise FileNotFoundError(f"Image file {reference} does not exist.")
118
+ reference = Image.open(reference)
119
+ if locator is None:
120
+ try:
121
+ location = gui.locateOnScreen(
122
+ reference,
123
+ region=Region.from_spec(region).to_box() if region else None,
124
+ grayscale=grayscale,
125
+ confidence=confidence,
126
+ limit=limit,
127
+ )
128
+ if location:
129
+ return Region.from_box(location)
130
+ except gui.ImageNotFoundException:
131
+ return None
132
+ except FileNotFoundError:
133
+ return None
134
+ else:
135
+ screenshot = gui.screenshot(
136
+ region=Region.from_spec(region).to_box() if region else None
137
+ )
138
+ logger.info(
139
+ f"Searching in region: {Region.from_spec(region).to_box() if region else None}.\nGiven region: {region}"
140
+ )
141
+ detections = locator(reference, screenshot)
142
+ logger.info("total detections: %d", len(detections))
143
+ if len(detections) == 0:
144
+ return None
145
+ elif limit > 1:
146
+ return detections[:limit]
147
+ else:
148
+ return detections[0]
@@ -1,14 +1,18 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyautoscene
3
- Version: 0.2.0
3
+ Version: 0.2.2
4
4
  Summary: Advance GUI automation
5
5
  Author-email: pritam-dey3 <pritam.pritamdey.984@gmail.com>
6
6
  License-File: LICENSE
7
7
  Requires-Python: >=3.13
8
8
  Requires-Dist: networkx>=3.5
9
+ Requires-Dist: opencv-python-headless>=4.12.0.88
9
10
  Requires-Dist: pillow>=11.3.0
10
11
  Requires-Dist: pyautogui>=0.9.54
11
12
  Requires-Dist: python-statemachine[diagrams]
13
+ Provides-Extra: ocr
14
+ Requires-Dist: onnxruntime>=1.22.0; extra == 'ocr'
15
+ Requires-Dist: rapidocr>=3.2.0; extra == 'ocr'
12
16
  Description-Content-Type: text/markdown
13
17
 
14
18
  # PyAutoScene
@@ -0,0 +1,15 @@
1
+ pyautoscene/__init__.py,sha256=9w2x28VgAHsTZHeLIynHs0qcFfmdUM-FjURLwUahNO0,230
2
+ pyautoscene/_types.py,sha256=t6hRAUe9OLUVCpzEVRjYHgb5l5iu4gMGinB_ecNOkR4,180
3
+ pyautoscene/constants.py,sha256=ZkBdvW9gBw002hLqelgiyS-ZsTbu7juyEWEg4Fx16NE,219
4
+ pyautoscene/ocr.py,sha256=YbPNYXGIvM_5RSB4_Va6D1t0eWIo47R-0utrBCZLqAk,2322
5
+ pyautoscene/ocr_config.yaml,sha256=EGaKX1a-LyWk0gtI2wUo04LraFqGJ3aZnQ5NCPbgDYI,2231
6
+ pyautoscene/references.py,sha256=8-VgwAHUt9Te0np2O5TvnIzW5LhNY1Dzm222VdlxgK8,3338
7
+ pyautoscene/region.py,sha256=X2QQy6gIqbbFPWVTNiqbYaQ12nXv5MU-ptP20kA4u48,2123
8
+ pyautoscene/scene.py,sha256=VWtx5l9DMFu3TC-EAxhabREQDFCvUPs7-4Jxq35JDrI,1993
9
+ pyautoscene/session.py,sha256=lxv71Q4UW5xr3-UTYVSOV8gqbpDI5HXMopO5kUSZhM0,5678
10
+ pyautoscene/utils.py,sha256=ZaxlGlt3xjayCKUfWGr_C73QwqEsacOsnp0SYC6ao7w,4734
11
+ pyautoscene-0.2.2.dist-info/METADATA,sha256=N4woAGtRHFc2_DNHG3b_g7jXA3HYT6DB1cu369vVtNs,6068
12
+ pyautoscene-0.2.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
13
+ pyautoscene-0.2.2.dist-info/entry_points.txt,sha256=6aKjylfDivCRMrJasIIi7ICU4fZR-8HjcOHhRmHpYpQ,49
14
+ pyautoscene-0.2.2.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
15
+ pyautoscene-0.2.2.dist-info/RECORD,,