clerk-sdk 0.1.8__py3-none-any.whl → 0.2.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.
- clerk/base.py +94 -0
- clerk/client.py +3 -104
- clerk/decorator/models.py +1 -0
- clerk/decorator/task_decorator.py +4 -1
- clerk/gui_automation/__init__.py +0 -0
- clerk/gui_automation/action_model/__init__.py +0 -0
- clerk/gui_automation/action_model/model.py +126 -0
- clerk/gui_automation/action_model/utils.py +26 -0
- clerk/gui_automation/client.py +144 -0
- clerk/gui_automation/client_actor/__init__.py +4 -0
- clerk/gui_automation/client_actor/client_actor.py +178 -0
- clerk/gui_automation/client_actor/exception.py +22 -0
- clerk/gui_automation/client_actor/model.py +192 -0
- clerk/gui_automation/decorators/__init__.py +1 -0
- clerk/gui_automation/decorators/gui_automation.py +109 -0
- clerk/gui_automation/exceptions/__init__.py +0 -0
- clerk/gui_automation/exceptions/modality/__init__.py +0 -0
- clerk/gui_automation/exceptions/modality/exc.py +46 -0
- clerk/gui_automation/exceptions/websocket.py +6 -0
- clerk/gui_automation/ui_actions/__init__.py +1 -0
- clerk/gui_automation/ui_actions/actions.py +781 -0
- clerk/gui_automation/ui_actions/base.py +200 -0
- clerk/gui_automation/ui_actions/support.py +68 -0
- clerk/gui_automation/ui_state_inspector/__init__.py +0 -0
- clerk/gui_automation/ui_state_inspector/gui_vision.py +184 -0
- clerk/gui_automation/ui_state_inspector/models.py +184 -0
- clerk/gui_automation/ui_state_machine/__init__.py +11 -0
- clerk/gui_automation/ui_state_machine/ai_recovery.py +110 -0
- clerk/gui_automation/ui_state_machine/decorators.py +71 -0
- clerk/gui_automation/ui_state_machine/exceptions.py +42 -0
- clerk/gui_automation/ui_state_machine/models.py +40 -0
- clerk/gui_automation/ui_state_machine/state_machine.py +838 -0
- clerk/models/remote_device.py +7 -0
- clerk/utils/__init__.py +0 -0
- clerk/utils/logger.py +118 -0
- clerk/utils/save_artifact.py +35 -0
- {clerk_sdk-0.1.8.dist-info → clerk_sdk-0.2.0.dist-info}/METADATA +11 -1
- clerk_sdk-0.2.0.dist-info/RECORD +48 -0
- clerk_sdk-0.1.8.dist-info/RECORD +0 -15
- {clerk_sdk-0.1.8.dist-info → clerk_sdk-0.2.0.dist-info}/WHEEL +0 -0
- {clerk_sdk-0.1.8.dist-info → clerk_sdk-0.2.0.dist-info}/licenses/LICENSE +0 -0
- {clerk_sdk-0.1.8.dist-info → clerk_sdk-0.2.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,781 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import time
|
|
3
|
+
import backoff
|
|
4
|
+
from typing import Literal, List, Optional, Self, Union
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel, Field, model_validator, field_validator
|
|
7
|
+
from .base import BaseAction, ActionTypes
|
|
8
|
+
from .support import maybe_engage_operator_ui_action
|
|
9
|
+
from ..action_model.model import (
|
|
10
|
+
Coords,
|
|
11
|
+
Screenshot,
|
|
12
|
+
)
|
|
13
|
+
from ..client_actor import perform_action
|
|
14
|
+
from ..action_model.utils import get_coordinates
|
|
15
|
+
from ..client_actor.model import (
|
|
16
|
+
DeleteFilesExecutePayload,
|
|
17
|
+
ExecutePayload,
|
|
18
|
+
ApplicationExecutePayload,
|
|
19
|
+
FileDetails,
|
|
20
|
+
GetFileExecutePayload,
|
|
21
|
+
SaveFilesExecutePayload,
|
|
22
|
+
WindowExecutePayload,
|
|
23
|
+
)
|
|
24
|
+
from ..client_actor.exception import GetScreenError
|
|
25
|
+
from ..exceptions.modality.exc import ModalityNotKnownError
|
|
26
|
+
|
|
27
|
+
MAX_TIME = 5
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class File(BaseModel):
|
|
31
|
+
"""A class representing a file with filename, mimetype, and content.
|
|
32
|
+
|
|
33
|
+
Attributes:
|
|
34
|
+
filename (str): filename of the file
|
|
35
|
+
mimetype (Optional[str]): type of the file
|
|
36
|
+
content (bytes): file content
|
|
37
|
+
|
|
38
|
+
Methods:
|
|
39
|
+
save(path: str): Saves the file content to the specified path after creating any necessary directories.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
filename: str = Field(description="filename of the file")
|
|
43
|
+
mimetype: Optional[str] = Field(description="type of the file")
|
|
44
|
+
content: bytes = Field(description="file content")
|
|
45
|
+
|
|
46
|
+
@field_validator("content", mode="before")
|
|
47
|
+
@classmethod
|
|
48
|
+
def convert_to_bytes(cls, v) -> bytes:
|
|
49
|
+
if isinstance(v, str):
|
|
50
|
+
from base64 import b64decode
|
|
51
|
+
|
|
52
|
+
return b64decode(v)
|
|
53
|
+
return v
|
|
54
|
+
|
|
55
|
+
def save(self, path: str):
|
|
56
|
+
if not os.path.exists(path):
|
|
57
|
+
os.makedirs(path)
|
|
58
|
+
|
|
59
|
+
with open(os.path.join(path, self.filename), "wb") as f:
|
|
60
|
+
f.write(self.content)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class LeftClick(BaseAction):
|
|
64
|
+
"""
|
|
65
|
+
Class representing a left click action.
|
|
66
|
+
|
|
67
|
+
Attributes:
|
|
68
|
+
action_type (Literal["left_click"]): The type of action, which is always "left_click".
|
|
69
|
+
|
|
70
|
+
Methods:
|
|
71
|
+
do(): Performs the left click action by preparing the payload, getting the widget coordinates, and executing the action.
|
|
72
|
+
actionable_string(): Returns a string representation of the action that can be executed.
|
|
73
|
+
|
|
74
|
+
Example Usage:
|
|
75
|
+
LeftClick(target="Suche").above("Kalender").do()
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
action_type: Literal["left_click"] = "left_click"
|
|
79
|
+
|
|
80
|
+
@backoff.on_exception(
|
|
81
|
+
backoff.expo,
|
|
82
|
+
(RuntimeError, GetScreenError),
|
|
83
|
+
max_time=MAX_TIME,
|
|
84
|
+
on_giveup=maybe_engage_operator_ui_action,
|
|
85
|
+
raise_on_giveup=False, # Exception might be raised in the giveup handler instead
|
|
86
|
+
)
|
|
87
|
+
def do(self):
|
|
88
|
+
if not self.widget_bbox:
|
|
89
|
+
payload: Screenshot
|
|
90
|
+
payload = self._prepare_payload()
|
|
91
|
+
|
|
92
|
+
widget_bbox: Coords = get_coordinates(payload)
|
|
93
|
+
center_coords = self._get_center_coords(widget_bbox)
|
|
94
|
+
else:
|
|
95
|
+
center_coords = self._get_center_coords(self.widget_bbox)
|
|
96
|
+
execute_payload = ExecutePayload(
|
|
97
|
+
action_type=self.action_type, coordinates=center_coords
|
|
98
|
+
)
|
|
99
|
+
perform_action(execute_payload)
|
|
100
|
+
|
|
101
|
+
@property
|
|
102
|
+
def actionable_string(self):
|
|
103
|
+
return f"LeftClick(action_type='{self.action_type}', target='{self.target}', anchor='{self.anchor}', relation='{self.relation}').do()"
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class RightClick(BaseAction):
|
|
107
|
+
"""
|
|
108
|
+
Class representing a right click action.
|
|
109
|
+
|
|
110
|
+
Attributes:
|
|
111
|
+
action_type (Literal["right_click"]): The type of action, which is always "right_click".
|
|
112
|
+
|
|
113
|
+
Methods:
|
|
114
|
+
do(): Performs the right click action by preparing the payload, getting the widget coordinates, and executing the action.
|
|
115
|
+
actionable_string(): Returns a string representation of the action that can be executed.
|
|
116
|
+
|
|
117
|
+
Example Usage:
|
|
118
|
+
RightClick(target="Suche").above("Kalender").do()
|
|
119
|
+
"""
|
|
120
|
+
|
|
121
|
+
action_type: Literal["right_click"] = "right_click"
|
|
122
|
+
|
|
123
|
+
@backoff.on_exception(
|
|
124
|
+
backoff.expo,
|
|
125
|
+
(RuntimeError, GetScreenError),
|
|
126
|
+
max_time=MAX_TIME,
|
|
127
|
+
on_giveup=maybe_engage_operator_ui_action,
|
|
128
|
+
raise_on_giveup=False, # Exception might be raised in the giveup handler instead
|
|
129
|
+
)
|
|
130
|
+
def do(self):
|
|
131
|
+
if not self.widget_bbox:
|
|
132
|
+
payload: Screenshot
|
|
133
|
+
payload = self._prepare_payload()
|
|
134
|
+
|
|
135
|
+
widget_bbox: Coords = get_coordinates(payload)
|
|
136
|
+
center_coords = self._get_center_coords(widget_bbox)
|
|
137
|
+
else:
|
|
138
|
+
center_coords = self._get_center_coords(self.widget_bbox)
|
|
139
|
+
execute_payload = ExecutePayload(
|
|
140
|
+
action_type=self.action_type, coordinates=center_coords
|
|
141
|
+
)
|
|
142
|
+
perform_action(execute_payload)
|
|
143
|
+
|
|
144
|
+
@property
|
|
145
|
+
def actionable_string(self):
|
|
146
|
+
return f"RightClick(action_type='{self.action_type}', target='{self.target}', anchor='{self.anchor}', relation='{self.relation}').do()"
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class MiddleClickAction(BaseAction):
|
|
150
|
+
"""
|
|
151
|
+
Class representing a middle click action.
|
|
152
|
+
|
|
153
|
+
Attributes:
|
|
154
|
+
action_type (Literal["middle_click"]): The type of action, which is always "middle_click".
|
|
155
|
+
|
|
156
|
+
Methods:
|
|
157
|
+
do(): Performs the middle click action by preparing the payload, getting the widget coordinates, and executing the action.
|
|
158
|
+
actionable_string(): Returns a string representation of the action that can be executed.
|
|
159
|
+
|
|
160
|
+
Example Usage:
|
|
161
|
+
MiddleClickAction(target="Suche").above("Kalender").do()
|
|
162
|
+
"""
|
|
163
|
+
|
|
164
|
+
action_type: Literal["middle_click"] = "middle_click"
|
|
165
|
+
|
|
166
|
+
@backoff.on_exception(
|
|
167
|
+
backoff.expo,
|
|
168
|
+
(RuntimeError, GetScreenError),
|
|
169
|
+
max_time=MAX_TIME,
|
|
170
|
+
on_giveup=maybe_engage_operator_ui_action,
|
|
171
|
+
raise_on_giveup=False, # Exception might be raised in the giveup handler instead
|
|
172
|
+
)
|
|
173
|
+
def do(self):
|
|
174
|
+
if not self.widget_bbox:
|
|
175
|
+
payload: Screenshot
|
|
176
|
+
payload = self._prepare_payload()
|
|
177
|
+
|
|
178
|
+
widget_bbox: Coords = get_coordinates(payload)
|
|
179
|
+
center_coords = self._get_center_coords(widget_bbox)
|
|
180
|
+
else:
|
|
181
|
+
center_coords = self._get_center_coords(self.widget_bbox)
|
|
182
|
+
execute_payload = ExecutePayload(
|
|
183
|
+
action_type=self.action_type, coordinates=center_coords
|
|
184
|
+
)
|
|
185
|
+
perform_action(execute_payload)
|
|
186
|
+
|
|
187
|
+
@property
|
|
188
|
+
def actionable_string(self):
|
|
189
|
+
return f"MiddleClickAction(action_type='{self.action_type}', target='{self.target}', anchor='{self.anchor}', relation='{self.relation}').do()"
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
class DoubleClick(BaseAction):
|
|
193
|
+
"""
|
|
194
|
+
Class representing a double click action.
|
|
195
|
+
|
|
196
|
+
Attributes:
|
|
197
|
+
action_type (Literal["double_click"]): The type of action, which is always "double_click".
|
|
198
|
+
|
|
199
|
+
Methods:
|
|
200
|
+
do(): Performs the double click action by preparing the payload, getting the widget coordinates, and executing the action.
|
|
201
|
+
actionable_string(): Returns a string representation of the action that can be executed.
|
|
202
|
+
|
|
203
|
+
Example Usage:
|
|
204
|
+
DoubleClick(target="Suche").above("Kalender").do()
|
|
205
|
+
"""
|
|
206
|
+
|
|
207
|
+
action_type: Literal["double_click"] = "double_click"
|
|
208
|
+
|
|
209
|
+
@backoff.on_exception(
|
|
210
|
+
backoff.expo,
|
|
211
|
+
(RuntimeError, GetScreenError),
|
|
212
|
+
max_time=MAX_TIME,
|
|
213
|
+
on_giveup=maybe_engage_operator_ui_action,
|
|
214
|
+
raise_on_giveup=False, # Exception might be raised in the giveup handler instead
|
|
215
|
+
)
|
|
216
|
+
def do(self):
|
|
217
|
+
if not self.widget_bbox:
|
|
218
|
+
payload: Screenshot
|
|
219
|
+
payload = self._prepare_payload()
|
|
220
|
+
|
|
221
|
+
widget_bbox: Coords = get_coordinates(payload)
|
|
222
|
+
center_coords = self._get_center_coords(widget_bbox)
|
|
223
|
+
else:
|
|
224
|
+
center_coords = self._get_center_coords(self.widget_bbox)
|
|
225
|
+
execute_payload = ExecutePayload(
|
|
226
|
+
action_type=self.action_type, coordinates=center_coords
|
|
227
|
+
)
|
|
228
|
+
perform_action(execute_payload)
|
|
229
|
+
|
|
230
|
+
@property
|
|
231
|
+
def actionable_string(self):
|
|
232
|
+
return f"DoubleClick(action_type='{self.action_type}', target='{self.target}', anchor='{self.anchor}', relation='{self.relation}').do()"
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
class Scroll(BaseAction):
|
|
236
|
+
"""
|
|
237
|
+
Class representing a mouse scroll action.
|
|
238
|
+
|
|
239
|
+
Attributes:
|
|
240
|
+
action_type (Literal["scroll"]): The type of action, which is always "scroll".
|
|
241
|
+
clicks (int): indicates the amount of clicks to scroll. A positive integer scrolls up, a negative down
|
|
242
|
+
click_coords (Optional[List[int]]): Optional, if provided specifies coordinates of the click action which will be perfomed before scrolling.
|
|
243
|
+
y (Optional[int]): the y coordinate to be clicked.
|
|
244
|
+
Methods:
|
|
245
|
+
do(): Performs the double click action by preparing the payload, getting the widget coordinates, and executing the action.
|
|
246
|
+
actionable_string(): Returns a string representation of the action that can be executed.
|
|
247
|
+
|
|
248
|
+
Example Usage:
|
|
249
|
+
DoubleClick(target="Suche").above("Kalender").do()
|
|
250
|
+
"""
|
|
251
|
+
|
|
252
|
+
action_type: Literal["scroll"] = "scroll"
|
|
253
|
+
clicks: int
|
|
254
|
+
click_coords: List[int] = Field(default=[])
|
|
255
|
+
|
|
256
|
+
@backoff.on_exception(
|
|
257
|
+
backoff.expo,
|
|
258
|
+
(RuntimeError, GetScreenError),
|
|
259
|
+
max_time=MAX_TIME,
|
|
260
|
+
on_giveup=maybe_engage_operator_ui_action,
|
|
261
|
+
raise_on_giveup=False, # Exception might be raised in the giveup handler instead
|
|
262
|
+
)
|
|
263
|
+
def do(self):
|
|
264
|
+
execute_payload = ExecutePayload(
|
|
265
|
+
action_type=self.action_type,
|
|
266
|
+
coordinates=self.click_coords,
|
|
267
|
+
clicks=self.clicks,
|
|
268
|
+
)
|
|
269
|
+
perform_action(execute_payload)
|
|
270
|
+
|
|
271
|
+
@property
|
|
272
|
+
def actionable_string(self):
|
|
273
|
+
return f"Scroll(action_type='{self.action_type}', clicks={self.clicks}, click_coords={self.click_coords}).do()"
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
class SendKeys(BaseAction):
|
|
277
|
+
"""
|
|
278
|
+
Class representing a send keys action. Use for typing on the target machine.
|
|
279
|
+
|
|
280
|
+
Attributes:
|
|
281
|
+
action_type (Union[Literal["send_keys"], Literal["type"]]): The type of action, which can be "send_keys" or "type".
|
|
282
|
+
keys Union[str, List[str]]: The keys to be typed. If a list of strings is provided, it is mandatory to specify their key_separator (ie, tab, enter, etc..)
|
|
283
|
+
key_separator Optional[str]: The key to be pressed in order to separate the list of strings provided.
|
|
284
|
+
followed_by Optional[str]: The key that needs to be pressed after the keys are typed.
|
|
285
|
+
interval (float): The interval between each key press. Default is 0.05 seconds.
|
|
286
|
+
|
|
287
|
+
Methods:
|
|
288
|
+
do(): Performs the send keys action by preparing the payload, getting the widget coordinates, and executing the action.
|
|
289
|
+
actionable_string(): Returns a string representation of the action that can be executed.
|
|
290
|
+
|
|
291
|
+
Example Usage:
|
|
292
|
+
SendKeys(keys="Hello World").do()
|
|
293
|
+
"""
|
|
294
|
+
|
|
295
|
+
action_type: ActionTypes = "send_keys"
|
|
296
|
+
keys: Union[str, List[str]]
|
|
297
|
+
key_separator: Optional[str] = Field(default=None)
|
|
298
|
+
followed_by: Optional[str] = Field(default=None)
|
|
299
|
+
interval: float = 0.05
|
|
300
|
+
|
|
301
|
+
@model_validator(mode="after")
|
|
302
|
+
def validate_keys(self) -> Self:
|
|
303
|
+
if isinstance(self.keys, list) and not self.key_separator:
|
|
304
|
+
raise ValueError(
|
|
305
|
+
"The attribute 'key_seperator' must be provided if 'keys' is a list."
|
|
306
|
+
)
|
|
307
|
+
return self
|
|
308
|
+
|
|
309
|
+
def do(self):
|
|
310
|
+
payload: Screenshot
|
|
311
|
+
try:
|
|
312
|
+
if self.target:
|
|
313
|
+
payload = self._prepare_payload()
|
|
314
|
+
widget_bbox: Coords = get_coordinates(payload)
|
|
315
|
+
center_coords = self._get_center_coords(widget_bbox)
|
|
316
|
+
elif self.widget_bbox:
|
|
317
|
+
center_coords = self._get_center_coords(self.widget_bbox)
|
|
318
|
+
else:
|
|
319
|
+
center_coords: List[int] = []
|
|
320
|
+
|
|
321
|
+
except ModalityNotKnownError:
|
|
322
|
+
center_coords: List[int] = []
|
|
323
|
+
|
|
324
|
+
execute_payload = ExecutePayload(
|
|
325
|
+
action_type="send_keys",
|
|
326
|
+
coordinates=center_coords,
|
|
327
|
+
keys=self.keys,
|
|
328
|
+
interval=self.interval,
|
|
329
|
+
key_separator=self.key_separator,
|
|
330
|
+
followed_by=self.followed_by,
|
|
331
|
+
)
|
|
332
|
+
perform_action(execute_payload)
|
|
333
|
+
|
|
334
|
+
@property
|
|
335
|
+
def actionable_string(self):
|
|
336
|
+
return f"SendKeys(action_type='{self.action_type}', target='{self.target}', anchor='{self.anchor}', relation='{self.relation}', keys='{self.keys}').do()"
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
class PressKeys(BaseAction):
|
|
340
|
+
"""
|
|
341
|
+
Class representing a press keys action or keyboard shortcut action.
|
|
342
|
+
|
|
343
|
+
Attributes:
|
|
344
|
+
action_type (Union[Literal["press_keys"], Literal["keyboard_shortcut"]]): The type of action, which can be "press_keys" or "keyboard_shortcut".
|
|
345
|
+
keys (str): The keys to be pressed or the keyboard shortcut to be executed.
|
|
346
|
+
|
|
347
|
+
Methods:
|
|
348
|
+
do(): Performs the press keys action or keyboard shortcut action by preparing the payload and executing the action.
|
|
349
|
+
actionable_string(): Returns a string representation of the action that can be executed.
|
|
350
|
+
|
|
351
|
+
Example Usage:
|
|
352
|
+
PressKeys(keys='ctrl+c').do()
|
|
353
|
+
PressKeys(keys='ctrl+shift+esc').do()
|
|
354
|
+
"""
|
|
355
|
+
|
|
356
|
+
action_type: ActionTypes = "press_keys"
|
|
357
|
+
keys: str
|
|
358
|
+
|
|
359
|
+
def do(self):
|
|
360
|
+
# provide widget + screen to the action model via http request
|
|
361
|
+
execute_payload = ExecutePayload(
|
|
362
|
+
action_type="hot_keys",
|
|
363
|
+
keys=self.keys,
|
|
364
|
+
)
|
|
365
|
+
perform_action(execute_payload)
|
|
366
|
+
|
|
367
|
+
@property
|
|
368
|
+
def actionable_string(self):
|
|
369
|
+
return f"PressKeys(action_type='{self.action_type}', target='{self.target}', anchor='{self.anchor}', relation='{self.relation}', keys='{self.keys}').do()"
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
class WaitFor(BaseAction):
|
|
373
|
+
"""
|
|
374
|
+
Class representing a wait for action.
|
|
375
|
+
|
|
376
|
+
Attributes:
|
|
377
|
+
action_type (Literal["wait_for"]): The type of action, which is always "wait_for".
|
|
378
|
+
retry_timeout (float): The time interval between each retry in seconds. Default is 0.5 seconds.
|
|
379
|
+
is_awaited (bool): A flag to signal whether the target should appear immediately or is awaited.
|
|
380
|
+
|
|
381
|
+
Methods:
|
|
382
|
+
do(timeout: int = 30) -> bool: Waits for a UI target for a specified timeout and returns True if found, False otherwise.
|
|
383
|
+
|
|
384
|
+
Example Usage:
|
|
385
|
+
WaitFor("element").do(timeout=60)
|
|
386
|
+
"""
|
|
387
|
+
|
|
388
|
+
action_type: Literal["wait_for"] = "wait_for"
|
|
389
|
+
retry_timeout: float = 0.5
|
|
390
|
+
is_awaited: bool = True
|
|
391
|
+
|
|
392
|
+
def do(self, timeout: int = 30) -> Union[bool, Coords]:
|
|
393
|
+
"""
|
|
394
|
+
Attempts to find a UI target within a specified timeout, optimizing wait times between retries.
|
|
395
|
+
|
|
396
|
+
This method introduces an adaptive wait strategy based on the duration of previous attempts,
|
|
397
|
+
aiming to maximize the number of retries within the given timeout period. Unlike `slow_do`,
|
|
398
|
+
which statically waits for a fixed interval, `do` dynamically adjusts wait times to improve
|
|
399
|
+
the chances of finding the target within the timeout.
|
|
400
|
+
|
|
401
|
+
Parameters:
|
|
402
|
+
- timeout (int): The maximum time to wait in seconds. Default is 30.
|
|
403
|
+
|
|
404
|
+
Returns:
|
|
405
|
+
- bool: True if the UI target is found before the timeout, False otherwise.
|
|
406
|
+
"""
|
|
407
|
+
time_spent: float = 0.0
|
|
408
|
+
n_requests: int = 0
|
|
409
|
+
|
|
410
|
+
while True:
|
|
411
|
+
# Start tracking time
|
|
412
|
+
start = time.perf_counter()
|
|
413
|
+
|
|
414
|
+
# Given that after a screenshot is taken, an end-to-end request to the AM might take some meaningful time,
|
|
415
|
+
# additional sleeping time was removed
|
|
416
|
+
# We assume that the screen will have enough time to update,
|
|
417
|
+
# while the previous screen travels through the AM services
|
|
418
|
+
|
|
419
|
+
# take new screenshot
|
|
420
|
+
try:
|
|
421
|
+
return get_coordinates(self._prepare_payload())
|
|
422
|
+
except RuntimeError:
|
|
423
|
+
n_requests += 1
|
|
424
|
+
time_spent += round((time.perf_counter() - start), 2) # e.g. 2.45 s
|
|
425
|
+
average_request_time: float = round((time_spent / n_requests), 2)
|
|
426
|
+
|
|
427
|
+
# do we have time for one more request?
|
|
428
|
+
# if not, let's not wait for another retry and quit immediately
|
|
429
|
+
if time_spent > timeout - average_request_time:
|
|
430
|
+
return False
|
|
431
|
+
|
|
432
|
+
## check if this is the last call, notify the action model that the target is not awaited anymore
|
|
433
|
+
if time_spent > timeout - 2 * average_request_time:
|
|
434
|
+
self.is_awaited = False
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
class OpenApplication(BaseAction):
|
|
438
|
+
"""
|
|
439
|
+
Class representing an open application action.
|
|
440
|
+
|
|
441
|
+
Attributes:
|
|
442
|
+
action_type (Literal["open_app"]): The type of action, which is always "open_app".
|
|
443
|
+
app_path (str): The absolute path of the application.
|
|
444
|
+
app_window_name (str): The name of the application window once open. Wildcard logic enabled.
|
|
445
|
+
|
|
446
|
+
Methods:
|
|
447
|
+
do(timeout: int = 60): Opens the application by preparing the payload and executing the action.
|
|
448
|
+
|
|
449
|
+
Example Usage:
|
|
450
|
+
OpenApplication(app_path="/path/to/application.exe", app_window_name="Application Window").do()
|
|
451
|
+
"""
|
|
452
|
+
|
|
453
|
+
action_type: Literal["open_app"] = "open_app"
|
|
454
|
+
app_path: str = Field(description="Absolute path of the application")
|
|
455
|
+
app_window_name: str = Field(
|
|
456
|
+
description="Name of the application window once open. Wildcard logic enabled."
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
def do(self, timeout: int = 60):
|
|
460
|
+
payload = ApplicationExecutePayload(
|
|
461
|
+
action_type=self.action_type,
|
|
462
|
+
app_path=self.app_path,
|
|
463
|
+
app_window_name=self.app_window_name,
|
|
464
|
+
timeout=timeout,
|
|
465
|
+
)
|
|
466
|
+
perform_action(payload)
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
class ForceCloseApplication(BaseAction):
|
|
470
|
+
"""
|
|
471
|
+
ForceCloseApplication class represents an action to force close an application.
|
|
472
|
+
|
|
473
|
+
Attributes:
|
|
474
|
+
action_type (Literal["force_close_app"]): Type of the action, set to "force_close_app".
|
|
475
|
+
process_name (str): Process name from the task manager that identifies the application to be force closed.
|
|
476
|
+
|
|
477
|
+
Methods:
|
|
478
|
+
do():
|
|
479
|
+
Executes the action to force close the application by creating and performing an ApplicationExecutePayload with the specified process name.
|
|
480
|
+
"""
|
|
481
|
+
|
|
482
|
+
action_type: Literal["force_close_app"] = "force_close_app"
|
|
483
|
+
process_name: str = Field(
|
|
484
|
+
description="Process name from task manager. Example: process.exe"
|
|
485
|
+
)
|
|
486
|
+
|
|
487
|
+
def do(self):
|
|
488
|
+
payload = ApplicationExecutePayload(
|
|
489
|
+
action_type=self.action_type,
|
|
490
|
+
process_name=self.process_name,
|
|
491
|
+
)
|
|
492
|
+
perform_action(payload)
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
class SaveFiles(BaseAction):
|
|
496
|
+
"""
|
|
497
|
+
SaveFiles class represents an action for saving files.
|
|
498
|
+
|
|
499
|
+
Attributes:
|
|
500
|
+
action_type (Literal["save_files"]): The type of action, indicating that it is for saving files.
|
|
501
|
+
save_location (str): The location where the files will be saved on client machine.
|
|
502
|
+
files (List[str]): A list of absolute paths of the files to be saved.
|
|
503
|
+
|
|
504
|
+
Methods:
|
|
505
|
+
do():
|
|
506
|
+
Executes the save files action by creating a payload and performing the action using the perform_action function.
|
|
507
|
+
|
|
508
|
+
Example Usage:
|
|
509
|
+
SaveFiles(save_location="/path/to/", files=["/path/to/file_1", "/path/to/file_2"]).do()
|
|
510
|
+
"""
|
|
511
|
+
|
|
512
|
+
action_type: ActionTypes = "save_files"
|
|
513
|
+
save_location: str
|
|
514
|
+
files: Union[List[str], List[FileDetails]]
|
|
515
|
+
|
|
516
|
+
def get_files_details(self) -> List[FileDetails]:
|
|
517
|
+
import os
|
|
518
|
+
import base64
|
|
519
|
+
|
|
520
|
+
files_details: List[FileDetails] = []
|
|
521
|
+
for file in self.files:
|
|
522
|
+
if isinstance(file, str):
|
|
523
|
+
if not os.path.exists(file):
|
|
524
|
+
raise FileExistsError(file)
|
|
525
|
+
file_details: FileDetails = FileDetails(
|
|
526
|
+
filename=os.path.basename(file),
|
|
527
|
+
value=base64.standard_b64encode(open(file, "rb").read()).decode(),
|
|
528
|
+
)
|
|
529
|
+
files_details.append(file_details)
|
|
530
|
+
else:
|
|
531
|
+
files_details.append(file)
|
|
532
|
+
return files_details
|
|
533
|
+
|
|
534
|
+
def do(self):
|
|
535
|
+
payload = SaveFilesExecutePayload(
|
|
536
|
+
action_type=self.action_type,
|
|
537
|
+
save_location=self.save_location,
|
|
538
|
+
files=self.get_files_details(),
|
|
539
|
+
)
|
|
540
|
+
perform_action(payload)
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
class DeleteFiles(BaseAction):
|
|
544
|
+
"""
|
|
545
|
+
DeleteFiles class represents an action for deleting files.
|
|
546
|
+
|
|
547
|
+
Attributes:
|
|
548
|
+
action_type (Literal["delete_files"]): The type of action, indicating that it is for deleting files.
|
|
549
|
+
files_location (List[str]): A list of file locations representing the files to be deleted.
|
|
550
|
+
|
|
551
|
+
Methods:
|
|
552
|
+
do():
|
|
553
|
+
Executes the delete files action by creating a payload and performing the action using the 'perform_action' function.
|
|
554
|
+
Example Usage:
|
|
555
|
+
DeleteFiles(files_location=["/path/to/file_1", "/path/to/file_2"]).do()
|
|
556
|
+
"""
|
|
557
|
+
|
|
558
|
+
action_type: ActionTypes = "delete_files"
|
|
559
|
+
files_location: List[str]
|
|
560
|
+
|
|
561
|
+
def do(self):
|
|
562
|
+
payload = DeleteFilesExecutePayload(
|
|
563
|
+
action_type=self.action_type, files_location=self.files_location
|
|
564
|
+
)
|
|
565
|
+
perform_action(payload)
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
class GetFile(BaseAction):
|
|
569
|
+
"""
|
|
570
|
+
GetFile class represents an action for getting file from target machine.
|
|
571
|
+
|
|
572
|
+
Attributes:
|
|
573
|
+
action_type (Literal["get_file"]): The type of action, indicating that it is for getting a file
|
|
574
|
+
files_location (str): file location of the target file.
|
|
575
|
+
|
|
576
|
+
Methods:
|
|
577
|
+
do():
|
|
578
|
+
Executes the delete files action by creating a payload and performing the action using the 'perform_action' function.
|
|
579
|
+
Example Usage:
|
|
580
|
+
GetFile(file_location="/path/to/file_1").do()
|
|
581
|
+
"""
|
|
582
|
+
|
|
583
|
+
action_type: Literal["get_file"] = "get_file"
|
|
584
|
+
file_location: str
|
|
585
|
+
|
|
586
|
+
def do(self) -> File:
|
|
587
|
+
payload = GetFileExecutePayload(
|
|
588
|
+
action_type=self.action_type, file_location=self.file_location
|
|
589
|
+
)
|
|
590
|
+
return File(**perform_action(payload))
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
class MaximizeWindow(BaseAction):
|
|
594
|
+
"""
|
|
595
|
+
Class representing a maximize window action.
|
|
596
|
+
|
|
597
|
+
Attributes:
|
|
598
|
+
action_type (Literal["maximize_window"]): The type of action, which is always "maximize_window".
|
|
599
|
+
window_name (str): The name of the window to be maximized.
|
|
600
|
+
|
|
601
|
+
Methods:
|
|
602
|
+
do(timeout: int = 10): Maximizes the specified window by preparing the payload and executing the action.
|
|
603
|
+
|
|
604
|
+
Example Usage:
|
|
605
|
+
MaximizeWindow(window_name="MyWindow").do()
|
|
606
|
+
"""
|
|
607
|
+
|
|
608
|
+
action_type: Literal["maximize_window"] = "maximize_window"
|
|
609
|
+
window_name: str
|
|
610
|
+
|
|
611
|
+
def do(self, timeout: int = 10):
|
|
612
|
+
payload = WindowExecutePayload(
|
|
613
|
+
action_type=self.action_type, window_name=self.window_name, timeout=timeout
|
|
614
|
+
)
|
|
615
|
+
perform_action(payload)
|
|
616
|
+
|
|
617
|
+
|
|
618
|
+
class MinimizeWindow(BaseAction):
|
|
619
|
+
"""
|
|
620
|
+
Class representing a minimize window action.
|
|
621
|
+
|
|
622
|
+
Attributes:
|
|
623
|
+
action_type (Literal["minimize_window"]): The type of action, which is always "minimize_window".
|
|
624
|
+
window_name (str): The name of the window to be minimized.
|
|
625
|
+
|
|
626
|
+
Methods:
|
|
627
|
+
do(timeout: int = 10): Minimizes the specified window by preparing the payload and executing the action.
|
|
628
|
+
|
|
629
|
+
Example Usage:
|
|
630
|
+
MinimizeWindow(window_name="MyWindow").do()
|
|
631
|
+
"""
|
|
632
|
+
|
|
633
|
+
action_type: Literal["minimize_window"] = "minimize_window"
|
|
634
|
+
window_name: str
|
|
635
|
+
|
|
636
|
+
def do(self, timeout: int = 10):
|
|
637
|
+
payload = WindowExecutePayload(
|
|
638
|
+
action_type=self.action_type, window_name=self.window_name, timeout=timeout
|
|
639
|
+
)
|
|
640
|
+
perform_action(payload)
|
|
641
|
+
|
|
642
|
+
|
|
643
|
+
class CloseWindow(BaseAction):
|
|
644
|
+
"""
|
|
645
|
+
Class representing a close window action.
|
|
646
|
+
|
|
647
|
+
Attributes:
|
|
648
|
+
action_type (Literal["close_window"]): The type of action, which is always "close_window".
|
|
649
|
+
window_name (str): The name of the window to be closed.
|
|
650
|
+
|
|
651
|
+
Methods:
|
|
652
|
+
do(timeout: int = 10): Closes the specified window by preparing the payload and executing the action.
|
|
653
|
+
|
|
654
|
+
Example Usage:
|
|
655
|
+
CloseWindow(window_name="MyWindow").do()
|
|
656
|
+
"""
|
|
657
|
+
|
|
658
|
+
action_type: Literal["close_window"] = "close_window"
|
|
659
|
+
window_name: str
|
|
660
|
+
|
|
661
|
+
def do(self, timeout: int = 10):
|
|
662
|
+
payload = WindowExecutePayload(
|
|
663
|
+
action_type=self.action_type, window_name=self.window_name, timeout=timeout
|
|
664
|
+
)
|
|
665
|
+
perform_action(payload)
|
|
666
|
+
|
|
667
|
+
|
|
668
|
+
class ActivateWindow(BaseAction):
|
|
669
|
+
"""
|
|
670
|
+
Class representing an activate window action.
|
|
671
|
+
|
|
672
|
+
Attributes:
|
|
673
|
+
action_type (Literal["activate_window"]): The type of action, which is always "activate_window".
|
|
674
|
+
window_name (str): The name of the window to be activated.
|
|
675
|
+
|
|
676
|
+
Methods:
|
|
677
|
+
do(timeout: int = 10): Activates the specified window by preparing the payload and executing the action.
|
|
678
|
+
|
|
679
|
+
Example Usage:
|
|
680
|
+
ActivateWindow(window_name="MyWindow").do()
|
|
681
|
+
"""
|
|
682
|
+
|
|
683
|
+
action_type: Literal["activate_window"] = "activate_window"
|
|
684
|
+
window_name: str
|
|
685
|
+
|
|
686
|
+
def do(self, timeout: int = 10):
|
|
687
|
+
payload = WindowExecutePayload(
|
|
688
|
+
action_type=self.action_type, window_name=self.window_name, timeout=timeout
|
|
689
|
+
)
|
|
690
|
+
perform_action(payload)
|
|
691
|
+
|
|
692
|
+
|
|
693
|
+
class GetText(BaseAction):
|
|
694
|
+
"""
|
|
695
|
+
GetText class represents a UI action for retrieving text from a target/preselected textbox or input field.
|
|
696
|
+
|
|
697
|
+
Attributes:
|
|
698
|
+
action_type (Literal["get_text"]): Type of UI action to execute.
|
|
699
|
+
|
|
700
|
+
Methods:
|
|
701
|
+
do(): Executes the UI action and returns the retrieved text.
|
|
702
|
+
|
|
703
|
+
Example:
|
|
704
|
+
# retrieve text from a target input field
|
|
705
|
+
text = GetText(target="Suche").above("Kalender").do()
|
|
706
|
+
|
|
707
|
+
# retrieve text from a pre-selected
|
|
708
|
+
text = GetText().do()
|
|
709
|
+
|
|
710
|
+
"""
|
|
711
|
+
|
|
712
|
+
action_type: Literal["get_text"] = "get_text"
|
|
713
|
+
|
|
714
|
+
@backoff.on_exception(
|
|
715
|
+
backoff.expo,
|
|
716
|
+
(RuntimeError, GetScreenError),
|
|
717
|
+
max_time=MAX_TIME,
|
|
718
|
+
on_giveup=maybe_engage_operator_ui_action,
|
|
719
|
+
raise_on_giveup=False, # Exception might be raised in the giveup handler instead
|
|
720
|
+
)
|
|
721
|
+
def do(self) -> str:
|
|
722
|
+
if self.target:
|
|
723
|
+
payload: Screenshot
|
|
724
|
+
payload = self._prepare_payload()
|
|
725
|
+
|
|
726
|
+
widget_bbox: Coords = get_coordinates(payload)
|
|
727
|
+
center_coords = self._get_center_coords(widget_bbox)
|
|
728
|
+
else:
|
|
729
|
+
center_coords = []
|
|
730
|
+
execute_payload = ExecutePayload(
|
|
731
|
+
action_type=self.action_type, coordinates=center_coords
|
|
732
|
+
)
|
|
733
|
+
return perform_action(execute_payload)
|
|
734
|
+
|
|
735
|
+
|
|
736
|
+
class PasteText(BaseAction):
|
|
737
|
+
"""
|
|
738
|
+
PasteText class represents a UI action for paste text from a using ctrl+a,ctrl+v combination.
|
|
739
|
+
|
|
740
|
+
Attributes:
|
|
741
|
+
action_type (Literal["paste_text"]): Type of UI action to execute.
|
|
742
|
+
|
|
743
|
+
Methods:
|
|
744
|
+
do(): Executes the UI action and returns the retrieved text.
|
|
745
|
+
|
|
746
|
+
Example:
|
|
747
|
+
# paste text into a target input field
|
|
748
|
+
text = PasteText(target="Suche", keys="text to paste").above("Kalender").do()
|
|
749
|
+
|
|
750
|
+
# paste text into a pre-selected field
|
|
751
|
+
text = PasteText(keys="text to paste").do()
|
|
752
|
+
|
|
753
|
+
"""
|
|
754
|
+
|
|
755
|
+
action_type: Literal["paste_text"] = "paste_text"
|
|
756
|
+
keys: Union[str, List[str]]
|
|
757
|
+
followed_by: Optional[str] = Field(default=None)
|
|
758
|
+
|
|
759
|
+
@backoff.on_exception(
|
|
760
|
+
backoff.expo,
|
|
761
|
+
(RuntimeError, GetScreenError),
|
|
762
|
+
max_time=MAX_TIME,
|
|
763
|
+
on_giveup=maybe_engage_operator_ui_action,
|
|
764
|
+
raise_on_giveup=False, # Exception might be raised in the giveup handler instead
|
|
765
|
+
)
|
|
766
|
+
def do(self) -> str:
|
|
767
|
+
if self.target:
|
|
768
|
+
payload: Screenshot
|
|
769
|
+
payload = self._prepare_payload()
|
|
770
|
+
|
|
771
|
+
widget_bbox: Coords = get_coordinates(payload)
|
|
772
|
+
center_coords = self._get_center_coords(widget_bbox)
|
|
773
|
+
else:
|
|
774
|
+
center_coords = []
|
|
775
|
+
execute_payload = ExecutePayload(
|
|
776
|
+
action_type=self.action_type,
|
|
777
|
+
coordinates=center_coords,
|
|
778
|
+
followed_by=self.followed_by,
|
|
779
|
+
keys=self.keys,
|
|
780
|
+
)
|
|
781
|
+
return perform_action(execute_payload)
|