clerk-sdk 0.1.9__tar.gz → 0.2.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. clerk_sdk-0.2.1/MANIFEST.in +3 -0
  2. {clerk_sdk-0.1.9/clerk_sdk.egg-info → clerk_sdk-0.2.1}/PKG-INFO +11 -1
  3. clerk_sdk-0.1.9/clerk/client.py → clerk_sdk-0.2.1/clerk/base.py +7 -31
  4. clerk_sdk-0.2.1/clerk/client.py +17 -0
  5. {clerk_sdk-0.1.9 → clerk_sdk-0.2.1}/clerk/decorator/models.py +1 -0
  6. {clerk_sdk-0.1.9 → clerk_sdk-0.2.1}/clerk/decorator/task_decorator.py +1 -0
  7. clerk_sdk-0.2.1/clerk/gui_automation/action_model/__init__.py +0 -0
  8. clerk_sdk-0.2.1/clerk/gui_automation/action_model/model.py +126 -0
  9. clerk_sdk-0.2.1/clerk/gui_automation/action_model/utils.py +26 -0
  10. clerk_sdk-0.2.1/clerk/gui_automation/client.py +148 -0
  11. clerk_sdk-0.2.1/clerk/gui_automation/client_actor/__init__.py +4 -0
  12. clerk_sdk-0.2.1/clerk/gui_automation/client_actor/client_actor.py +176 -0
  13. clerk_sdk-0.2.1/clerk/gui_automation/client_actor/exception.py +22 -0
  14. clerk_sdk-0.2.1/clerk/gui_automation/client_actor/model.py +192 -0
  15. clerk_sdk-0.2.1/clerk/gui_automation/decorators/__init__.py +1 -0
  16. clerk_sdk-0.2.1/clerk/gui_automation/decorators/gui_automation.py +135 -0
  17. clerk_sdk-0.2.1/clerk/gui_automation/exceptions/__init__.py +0 -0
  18. clerk_sdk-0.2.1/clerk/gui_automation/exceptions/agent_manager.py +6 -0
  19. clerk_sdk-0.2.1/clerk/gui_automation/exceptions/modality/__init__.py +0 -0
  20. clerk_sdk-0.2.1/clerk/gui_automation/exceptions/modality/exc.py +46 -0
  21. clerk_sdk-0.2.1/clerk/gui_automation/exceptions/websocket.py +6 -0
  22. clerk_sdk-0.2.1/clerk/gui_automation/requirements.txt +2 -0
  23. clerk_sdk-0.2.1/clerk/gui_automation/ui_actions/__init__.py +1 -0
  24. clerk_sdk-0.2.1/clerk/gui_automation/ui_actions/actions.py +781 -0
  25. clerk_sdk-0.2.1/clerk/gui_automation/ui_actions/base.py +200 -0
  26. clerk_sdk-0.2.1/clerk/gui_automation/ui_actions/support.py +69 -0
  27. clerk_sdk-0.2.1/clerk/gui_automation/ui_state_inspector/__init__.py +0 -0
  28. clerk_sdk-0.2.1/clerk/gui_automation/ui_state_inspector/gui_vision.py +184 -0
  29. clerk_sdk-0.2.1/clerk/gui_automation/ui_state_inspector/models.py +184 -0
  30. clerk_sdk-0.2.1/clerk/gui_automation/ui_state_machine/__init__.py +11 -0
  31. clerk_sdk-0.2.1/clerk/gui_automation/ui_state_machine/ai_recovery.py +110 -0
  32. clerk_sdk-0.2.1/clerk/gui_automation/ui_state_machine/decorators.py +71 -0
  33. clerk_sdk-0.2.1/clerk/gui_automation/ui_state_machine/exceptions.py +42 -0
  34. clerk_sdk-0.2.1/clerk/gui_automation/ui_state_machine/models.py +40 -0
  35. clerk_sdk-0.2.1/clerk/gui_automation/ui_state_machine/state_machine.py +838 -0
  36. clerk_sdk-0.2.1/clerk/models/__init__.py +0 -0
  37. clerk_sdk-0.2.1/clerk/models/remote_device.py +7 -0
  38. clerk_sdk-0.2.1/clerk/utils/__init__.py +0 -0
  39. clerk_sdk-0.2.1/clerk/utils/logger.py +124 -0
  40. clerk_sdk-0.2.1/clerk/utils/save_artifact.py +37 -0
  41. {clerk_sdk-0.1.9 → clerk_sdk-0.2.1/clerk_sdk.egg-info}/PKG-INFO +11 -1
  42. clerk_sdk-0.2.1/clerk_sdk.egg-info/SOURCES.txt +56 -0
  43. clerk_sdk-0.2.1/clerk_sdk.egg-info/requires.txt +14 -0
  44. {clerk_sdk-0.1.9 → clerk_sdk-0.2.1}/requirements.txt +1 -1
  45. {clerk_sdk-0.1.9 → clerk_sdk-0.2.1}/setup.py +16 -5
  46. clerk_sdk-0.1.9/MANIFEST.in +0 -2
  47. clerk_sdk-0.1.9/clerk_sdk.egg-info/SOURCES.txt +0 -21
  48. clerk_sdk-0.1.9/clerk_sdk.egg-info/requires.txt +0 -3
  49. {clerk_sdk-0.1.9 → clerk_sdk-0.2.1}/LICENSE +0 -0
  50. {clerk_sdk-0.1.9 → clerk_sdk-0.2.1}/README.md +0 -0
  51. {clerk_sdk-0.1.9 → clerk_sdk-0.2.1}/clerk/__init__.py +0 -0
  52. {clerk_sdk-0.1.9 → clerk_sdk-0.2.1}/clerk/decorator/__init__.py +0 -0
  53. {clerk_sdk-0.1.9/clerk/models → clerk_sdk-0.2.1/clerk/gui_automation}/__init__.py +0 -0
  54. {clerk_sdk-0.1.9 → clerk_sdk-0.2.1}/clerk/models/document.py +0 -0
  55. {clerk_sdk-0.1.9 → clerk_sdk-0.2.1}/clerk/models/document_statuses.py +0 -0
  56. {clerk_sdk-0.1.9 → clerk_sdk-0.2.1}/clerk/models/file.py +0 -0
  57. {clerk_sdk-0.1.9 → clerk_sdk-0.2.1}/clerk/models/response_model.py +0 -0
  58. {clerk_sdk-0.1.9 → clerk_sdk-0.2.1}/clerk_sdk.egg-info/dependency_links.txt +0 -0
  59. {clerk_sdk-0.1.9 → clerk_sdk-0.2.1}/clerk_sdk.egg-info/top_level.txt +0 -0
  60. {clerk_sdk-0.1.9 → clerk_sdk-0.2.1}/pyproject.toml +0 -0
  61. {clerk_sdk-0.1.9 → clerk_sdk-0.2.1}/setup.cfg +0 -0
@@ -0,0 +1,3 @@
1
+ include requirements.txt
2
+ include clerk/gui_automation/requirements.txt
3
+ include README.md
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: clerk-sdk
3
- Version: 0.1.9
3
+ Version: 0.2.1
4
4
  Summary: Library for interacting with Clerk
5
5
  Home-page: https://github.com/F-ONE-Group/clerk_pypi
6
6
  Author: F-ONE Group
@@ -14,6 +14,15 @@ License-File: LICENSE
14
14
  Requires-Dist: pydantic<3.0.0,>=2.0.0
15
15
  Requires-Dist: backoff<3.0.0,>=2.0.0
16
16
  Requires-Dist: requests<3.0.0,>=2.32.3
17
+ Provides-Extra: all
18
+ Requires-Dist: pydantic<3.0.0,>=2.0.0; extra == "all"
19
+ Requires-Dist: backoff<3.0.0,>=2.0.0; extra == "all"
20
+ Requires-Dist: requests<3.0.0,>=2.32.3; extra == "all"
21
+ Requires-Dist: networkx<4.0.0,>=3.5.0; extra == "all"
22
+ Requires-Dist: websockets>=15.0.1; extra == "all"
23
+ Provides-Extra: gui-automation
24
+ Requires-Dist: networkx<4.0.0,>=3.5.0; extra == "gui-automation"
25
+ Requires-Dist: websockets>=15.0.1; extra == "gui-automation"
17
26
  Dynamic: author
18
27
  Dynamic: author-email
19
28
  Dynamic: classifier
@@ -21,6 +30,7 @@ Dynamic: description
21
30
  Dynamic: description-content-type
22
31
  Dynamic: home-page
23
32
  Dynamic: license-file
33
+ Dynamic: provides-extra
24
34
  Dynamic: requires-dist
25
35
  Dynamic: requires-python
26
36
  Dynamic: summary
@@ -1,34 +1,15 @@
1
1
  import os
2
2
 
3
- # import logging
4
3
  import requests
5
4
  import backoff
6
- from typing import Dict, List, Optional, Self
7
- from xml.dom.minidom import Document
5
+ from typing import Dict, Optional, Self
8
6
 
9
7
 
10
8
  from pydantic import BaseModel, model_validator, Field
11
9
 
12
- from .models.file import ParsedFile
13
10
  from .models.response_model import StandardResponse
14
11
 
15
12
 
16
- # logger = logging.getLogger(__name__)
17
- # logger.setLevel(logging.INFO)
18
-
19
- # if not logger.handlers:
20
- # handler = logging.StreamHandler()
21
- # formatter = logging.Formatter("[%(levelname)s] %(asctime)s - %(message)s")
22
- # handler.setFormatter(formatter)
23
- # logger.addHandler(handler)
24
-
25
-
26
- # def backoff_handler(details):
27
- # logger.warning(
28
- # f"Retrying {details['target'].__name__} after {details['tries']} tries..."
29
- # )
30
-
31
-
32
13
  def giveup_handler(e):
33
14
  return (
34
15
  isinstance(e, requests.exceptions.HTTPError)
@@ -37,12 +18,13 @@ def giveup_handler(e):
37
18
  )
38
19
 
39
20
 
40
- class Clerk(BaseModel):
21
+ class BaseClerk(BaseModel):
41
22
  api_key: Optional[str] = Field(default=None, min_length=1)
42
23
  headers: Dict[str, str] = Field(default_factory=dict)
43
24
  base_url: str = Field(
44
25
  default_factory=lambda: os.getenv("CLERK_BASE_URL", "https://api.clerk-app.com")
45
26
  )
27
+ root_endpoint: Optional[str] = None
46
28
 
47
29
  @model_validator(mode="after")
48
30
  def validate_api_key(self) -> Self:
@@ -73,6 +55,8 @@ class Clerk(BaseModel):
73
55
 
74
56
  merged_headers = {**self.headers, **headers}
75
57
  url = f"{self.base_url}{endpoint}"
58
+ if self.root_endpoint:
59
+ url = f"{self.base_url}{self.root_endpoint}{endpoint}"
76
60
 
77
61
  # logger.info(f"GET {url} | params={params}")
78
62
 
@@ -99,6 +83,8 @@ class Clerk(BaseModel):
99
83
 
100
84
  merged_headers = {**self.headers, **headers}
101
85
  url = f"{self.base_url}{endpoint}"
86
+ if self.root_endpoint:
87
+ url = f"{self.base_url}{self.root_endpoint}{endpoint}"
102
88
 
103
89
  # logger.info(f"POST {url} | body={json} | params={params}")
104
90
 
@@ -106,13 +92,3 @@ class Clerk(BaseModel):
106
92
  response.raise_for_status()
107
93
 
108
94
  return StandardResponse(**response.json())
109
-
110
- def get_document(self, document_id: str) -> Document:
111
- endpoint = f"/document/{document_id}"
112
- res = self.get_request(endpoint=endpoint)
113
- return Document(**res.data[0])
114
-
115
- def get_files_document(self, document_id: str) -> List[ParsedFile]:
116
- endpoint = f"/document/{document_id}/files"
117
- res = self.get_request(endpoint=endpoint)
118
- return [ParsedFile(**d) for d in res.data]
@@ -0,0 +1,17 @@
1
+ from typing import List
2
+ from xml.dom.minidom import Document
3
+
4
+ from clerk.base import BaseClerk
5
+ from .models.file import ParsedFile
6
+
7
+
8
+ class Clerk(BaseClerk):
9
+ def get_document(self, document_id: str) -> Document:
10
+ endpoint = f"/document/{document_id}"
11
+ res = self.get_request(endpoint=endpoint)
12
+ return Document(**res.data[0])
13
+
14
+ def get_files_document(self, document_id: str) -> List[ParsedFile]:
15
+ endpoint = f"/document/{document_id}/files"
16
+ res = self.get_request(endpoint=endpoint)
17
+ return [ParsedFile(**d) for d in res.data]
@@ -17,3 +17,4 @@ class Document(BaseModel):
17
17
  class ClerkCodePayload(BaseModel):
18
18
  document: Document
19
19
  structured_data: Dict
20
+ run_id: Optional[str] = None
@@ -1,3 +1,4 @@
1
+ import os
1
2
  import pickle
2
3
  from typing import Callable, Optional
3
4
  from functools import wraps
@@ -0,0 +1,126 @@
1
+ import base64
2
+ import os
3
+ from typing import List, Literal, Optional, Union
4
+ from pydantic import BaseModel, Field, validator
5
+
6
+ CoordsType = Union[List[float], List[int]]
7
+
8
+ PredictionsFormat = Union[
9
+ Literal["xyxy"], Literal["xyxyn"], Literal["xywh"], Literal["xywhn"]
10
+ ]
11
+
12
+ RelationsType = Union[
13
+ Literal["above"], Literal["below"], Literal["left"], Literal["right"], Literal[""]
14
+ ]
15
+
16
+
17
+ class ImageB64(BaseModel):
18
+ """
19
+ A class representing an image encoded in base64 format.
20
+
21
+ Attributes:
22
+ id (Optional[str]): The ID of the image. Defaults to None.
23
+ value (str): The base64 encoded value of the image.
24
+
25
+ Methods:
26
+ from_path(value: Union[str, "ImageB64"]) -> "ImageB64":
27
+ Creates an ImageB64 instance from a file path or an existing ImageB64 instance.
28
+
29
+ Args:
30
+ value (Union[str, "ImageB64"]): The file path or an existing ImageB64 instance.
31
+
32
+ Returns:
33
+ ImageB64: The created ImageB64 instance.
34
+
35
+ _to_b64(path: str) -> str:
36
+ Encodes the image file at the given path to base64 format.
37
+
38
+ Args:
39
+ path (str): The path to the image file.
40
+
41
+ Returns:
42
+ str: The base64 encoded image.
43
+ """
44
+
45
+ id: Optional[str] = None
46
+ value: str = ""
47
+
48
+ @classmethod
49
+ def from_path(cls, value: Union[str, "ImageB64"]) -> "ImageB64":
50
+ if isinstance(value, ImageB64):
51
+ return value
52
+ return ImageB64(
53
+ id=os.path.basename(value),
54
+ value=to_b64(value),
55
+ )
56
+
57
+
58
+ def to_b64(path: str) -> str:
59
+ with open(path, "rb") as f:
60
+ img_b64: str = base64.b64encode(f.read()).decode("utf-8")
61
+ return img_b64
62
+
63
+
64
+ class Anchor(BaseModel):
65
+ """
66
+ A class representing an anchor for a screenshot.
67
+
68
+ Attributes:
69
+ value (Union[str, ImageB64]): The value of the anchor, which can be a string or an ImageB64 instance.
70
+ relation (RelationsType): The relation of the anchor to the target, which can be one of the following: "above", "below", "left", "right", or an empty string.
71
+
72
+ """
73
+
74
+ value: Union[str, ImageB64] = ""
75
+ relation: RelationsType = ""
76
+
77
+
78
+ class Screenshot(BaseModel):
79
+ """
80
+ A class representing a screenshot.
81
+
82
+ Attributes:
83
+ screen_b64 (ImageB64): The base64 encoded value of the screenshot.
84
+ target (Union[str, ImageB64]): The target of the screenshot, which can be a string or an ImageB64 instance.
85
+ anchors (List[Anchor]): The list of anchors for the screenshot.
86
+ is_awaited (bool): A flag to signal whether the target should appear immediately or is awaited.
87
+ target_name (Optional[str]): A readable representation of a target which is set automatically when validating the target and is used in the AM for logging.
88
+
89
+
90
+ """
91
+
92
+ screen_b64: ImageB64
93
+ target: Union[str, ImageB64]
94
+ anchors: List[Anchor] = []
95
+ is_awaited: bool = False
96
+ target_name: Optional[str] = None
97
+
98
+
99
+ class Coords(BaseModel):
100
+ """
101
+ A class representing coordinates.
102
+
103
+ Attributes:
104
+ value (CoordsType): The value of the coordinates, which can be a list of floats or a list of integers.
105
+ score (int): The score associated with the coordinates, defaults to 0.
106
+
107
+ """
108
+
109
+ value: CoordsType
110
+ score: int = 0
111
+
112
+
113
+ class RouterOutput(BaseModel):
114
+ """
115
+ A class representing the output of a router.
116
+
117
+ Attributes:
118
+ Resources (List[Coords]): A list of coordinates representing the resources.
119
+ StatusMessage (Union[Literal["Success"], Literal["Failure"], None]): The status message of the router output.
120
+ ErrorMessage (str): The error message associated with the router output.
121
+
122
+ """
123
+
124
+ Resources: List[Coords] = []
125
+ StatusMessage: Union[Literal["Success"], Literal["Failure"], None] = None
126
+ ErrorMessage: str = ""
@@ -0,0 +1,26 @@
1
+ from .model import Coords, Screenshot
2
+ from ..decorators.gui_automation import clerk_client
3
+
4
+
5
+ def get_coordinates(payload: Screenshot) -> Coords:
6
+ """
7
+ Get coordinates from the action model API endpoint.
8
+
9
+ The method requires the following environmental variables to work:
10
+ - AM_URL: action model URL
11
+
12
+ Parameters:
13
+ payload (Screenshot): The payload containing the necessary data for the request.
14
+
15
+ Returns:
16
+ Coords: The coordinates obtained from the API response.
17
+
18
+ Raises:
19
+ RuntimeError: If the API response status code is not 200.
20
+
21
+ Example:
22
+ payload = Screenshot(screen_b64="base64_encoded_image", target="target_image")
23
+ coordinates = get_coordinates(payload)
24
+ """
25
+
26
+ return clerk_client.get_coordinates(payload.model_dump())
@@ -0,0 +1,148 @@
1
+ from typing import Dict, List, Optional
2
+
3
+ from pydantic import BaseModel
4
+ from clerk.base import BaseClerk
5
+ from clerk.gui_automation.action_model.model import Coords
6
+ from clerk.gui_automation.exceptions.agent_manager import NoClientsAvailable
7
+ from clerk.gui_automation.ui_state_inspector.models import (
8
+ ActionString,
9
+ BaseState,
10
+ States,
11
+ TargetWithAnchor,
12
+ )
13
+ from clerk.models.remote_device import RemoteDevice
14
+
15
+
16
+ class RPAClerk(BaseClerk):
17
+
18
+ root_endpoint: str = "/gui_automation"
19
+
20
+ def allocate_remote_device(self, group_name: str, run_id: str):
21
+ endpoint = "/remote_device/allocate"
22
+ res = self.post_request(
23
+ endpoint=endpoint, json={"group_name": group_name, "run_id": run_id}
24
+ )
25
+
26
+ if res.data[0] is None:
27
+ raise NoClientsAvailable()
28
+
29
+ return RemoteDevice(**res.data[0])
30
+
31
+ def deallocate_remote_device(
32
+ self,
33
+ remote_device: RemoteDevice,
34
+ run_id: str,
35
+ ):
36
+ endpoint = "/remote_device/deallocate"
37
+ self.post_request(
38
+ endpoint=endpoint,
39
+ json={"id": remote_device.id, "name": remote_device.name, "run_id": run_id},
40
+ )
41
+
42
+ def get_coordinates(self, payload: Dict) -> Coords:
43
+ endpoint = "/action_model/get_coordinates"
44
+ res = self.post_request(endpoint=endpoint, json=payload)
45
+ if res.data[0] is None:
46
+ raise RuntimeError("No coordinates found in the response.")
47
+ return Coords(**res.data[0])
48
+
49
+
50
+ class GUIVisionClerk(BaseClerk):
51
+ root_endpoint: str = "/gui_automation/vision"
52
+
53
+ def find_target(self, screen_b64: str, use_ocr: bool, target_prompt: str):
54
+ endpoint = "/find_target"
55
+ res = self.post_request(
56
+ endpoint=endpoint,
57
+ json={
58
+ "screen_b64": screen_b64,
59
+ "use_ocr": use_ocr,
60
+ "target_prompt": target_prompt,
61
+ },
62
+ )
63
+ return TargetWithAnchor(**res.data[0])
64
+
65
+ def verify_state(
66
+ self, screen_b64: str, use_ocr: bool, possible_states: States
67
+ ) -> BaseState:
68
+ endpoint = "/verify_state"
69
+ res = self.post_request(
70
+ endpoint=endpoint,
71
+ json={
72
+ "screen_b64": screen_b64,
73
+ "use_ocr": use_ocr,
74
+ "possible_states": possible_states,
75
+ },
76
+ )
77
+
78
+ return BaseState(**res.data[0])
79
+
80
+ def answer(
81
+ self, screen_b64: str, use_ocr: bool, question: str, output_model: BaseModel
82
+ ) -> Dict:
83
+ endpoint = "/answer"
84
+ res = self.post_request(
85
+ endpoint=endpoint,
86
+ json={
87
+ "screen_b64": screen_b64,
88
+ "use_ocr": use_ocr,
89
+ "question": question,
90
+ "output_model": output_model.model_json_schema(),
91
+ },
92
+ )
93
+
94
+ return output_model(**res.data[0])
95
+
96
+ def classify_state(
97
+ self, screen_b64: str, use_ocr: bool, possible_states: List[Dict[str, str]]
98
+ ) -> BaseState:
99
+ endpoint = "/classify_state"
100
+ res = self.post_request(
101
+ endpoint=endpoint,
102
+ json={
103
+ "screen_b64": screen_b64,
104
+ "use_ocr": use_ocr,
105
+ "possible_states": possible_states,
106
+ },
107
+ )
108
+
109
+ return BaseState(**res.data[0])
110
+
111
+ def write_action_string(
112
+ self, screen_b64: str, use_ocr: bool, action_prompt: str
113
+ ) -> ActionString:
114
+ endpoint = "/write_action-string"
115
+ res = self.post_request(
116
+ endpoint=endpoint,
117
+ json={
118
+ "screen_b64": screen_b64,
119
+ "use_ocr": use_ocr,
120
+ "action_prompt": action_prompt,
121
+ },
122
+ )
123
+
124
+ return ActionString(**res.data[0])
125
+
126
+
127
+ class CourseCorrectorClerk(BaseClerk):
128
+ root_endpoint: str = "/gui_automation/course_corrector"
129
+
130
+ def get_corrective_actions(
131
+ self,
132
+ screen_b64: str,
133
+ use_ocr: str,
134
+ goal: str,
135
+ custom_instructions: Optional[str] = None,
136
+ ) -> ActionString:
137
+ endpoint = "/get_corrective_actions"
138
+ res = self.post_request(
139
+ endpoint=endpoint,
140
+ json={
141
+ "screen_b64": screen_b64,
142
+ "use_ocr": use_ocr,
143
+ "goal": goal,
144
+ "custom_instructions": custom_instructions,
145
+ },
146
+ )
147
+
148
+ return ActionString(**res.data[0])
@@ -0,0 +1,4 @@
1
+ from .client_actor import (
2
+ get_screen,
3
+ perform_action,
4
+ )
@@ -0,0 +1,176 @@
1
+ import asyncio
2
+ import json
3
+ import os
4
+ from typing import Any, Dict, Union
5
+
6
+ import pydantic
7
+ import requests
8
+
9
+
10
+ from .model import (
11
+ ExecutePayload,
12
+ DeleteFilesExecutePayload,
13
+ ApplicationExecutePayload,
14
+ SaveFilesExecutePayload,
15
+ WindowExecutePayload,
16
+ GetFileExecutePayload,
17
+ )
18
+ import backoff
19
+
20
+ from .model import PerformActionResponse, ActionStates
21
+ from .exception import PerformActionException, GetScreenError
22
+
23
+
24
+ async def _perform_action_ws(payload: Dict) -> PerformActionResponse:
25
+ """Perform an action over a WebSocket connection.
26
+
27
+ Args:
28
+ payload (Dict): The payload request to be sent.
29
+
30
+ Returns:
31
+ PerformActionResponse: The response of performing the action.
32
+
33
+ Raises:
34
+ RuntimeError: If the ACK message is not received within the specified timeout.
35
+ """
36
+
37
+ from ..decorators.gui_automation import global_ws
38
+
39
+ # 1. Send the payload request
40
+ if global_ws:
41
+ await global_ws.send(json.dumps(payload))
42
+
43
+ # 2. wait for ack message
44
+ try:
45
+ ack = await asyncio.wait_for(global_ws.recv(), 90)
46
+ if ack == "OK":
47
+ action_info = await asyncio.wait_for(global_ws.recv(), 90)
48
+ return PerformActionResponse(**json.loads(action_info))
49
+ else:
50
+ raise RuntimeError("Received ACK != OK")
51
+ except asyncio.TimeoutError:
52
+ raise RuntimeError("The ack message did not arrive.")
53
+ else:
54
+ raise RuntimeError("The Websocket has not been initiated.")
55
+
56
+
57
+ async def _get_screen_async() -> str:
58
+ """
59
+ Asynchronously retrieves a screen using a WebSocket connection.
60
+
61
+ Returns:
62
+ str: The base64 encoded screen image.
63
+
64
+ Note:
65
+ This function sends a request to perform a screenshot action over a WebSocket connection
66
+ and returns the base64 encoded image of the screen captured.
67
+ """
68
+ payload = {
69
+ "proc_inst_id": os.getenv("PROC_ID"),
70
+ "client_name": os.getenv("REMOTE_DEVICE_NAME"),
71
+ "headless": True,
72
+ "action": {"action_type": "screenshot"},
73
+ }
74
+ try:
75
+ action_info = await _perform_action_ws(payload)
76
+ except Exception as e:
77
+ if str(e) in (
78
+ "The ack message did not arrive.",
79
+ "Received ACK != OK",
80
+ ):
81
+ raise GetScreenError("The ack message did not arrive.")
82
+ raise # else raise the error
83
+
84
+ if action_info.screen_b64 is not None:
85
+ return action_info.screen_b64
86
+ raise GetScreenError()
87
+
88
+
89
+ @backoff.on_exception(
90
+ backoff.expo,
91
+ (requests.RequestException, pydantic.ValidationError, GetScreenError),
92
+ max_time=120,
93
+ )
94
+ def get_screen() -> str:
95
+ """
96
+ Request the VDI screen and return the base64 representation of the screenshot.
97
+
98
+ Returns:
99
+ str: The base64 representation of the screenshot.
100
+
101
+ Raises:
102
+ RuntimeError: If the request to the VDI screen fails.
103
+ """
104
+
105
+ loop = asyncio.get_event_loop()
106
+ # asyncio.set_event_loop(loop)
107
+ task = loop.create_task(_get_screen_async())
108
+ res = loop.run_until_complete(task)
109
+ return res
110
+
111
+
112
+ async def _perform_action_async(
113
+ payload: Union[
114
+ ExecutePayload,
115
+ ApplicationExecutePayload,
116
+ WindowExecutePayload,
117
+ SaveFilesExecutePayload,
118
+ DeleteFilesExecutePayload,
119
+ GetFileExecutePayload,
120
+ ],
121
+ ) -> Any:
122
+ """
123
+ Perform an asynchronous action based on the provided payload.
124
+
125
+ Args:
126
+ payload (Union[ExecutePayload, ApplicationExecutePayload, WindowExecutePayload, SaveFilesExecutePayload, DeleteFilesExecutePayload, GetFileExecutePayload]): The payload containing information about the action to be performed.
127
+
128
+ Returns:
129
+ Any: The return value of the action.
130
+
131
+ Raises:
132
+ PerformActionException: If the action fails with an error message.
133
+ """
134
+ req_payload: Dict = {
135
+ "proc_inst_id": os.getenv("PROC_ID"),
136
+ "client_name": os.getenv("REMOTE_DEVICE_NAME"),
137
+ "headless": (
138
+ True if os.getenv("HEADLESS", "True").lower() == "true" else False
139
+ ),
140
+ "action": payload.model_dump(),
141
+ }
142
+ action_info = await _perform_action_ws(req_payload)
143
+
144
+ if action_info.state == ActionStates.failed:
145
+ raise PerformActionException(action_info.message)
146
+ return action_info.return_value
147
+
148
+
149
+ def perform_action(
150
+ payload: Union[
151
+ ExecutePayload,
152
+ ApplicationExecutePayload,
153
+ WindowExecutePayload,
154
+ SaveFilesExecutePayload,
155
+ DeleteFilesExecutePayload,
156
+ GetFileExecutePayload,
157
+ ],
158
+ ) -> Any:
159
+ """
160
+ Perform an action on the VDI client.
161
+
162
+ Args:
163
+ payload (Union[ExecutePayload, ApplicationExecutePayload, WindowExecutePayload]): The payload containing the details of the action to be performed.
164
+
165
+ Raises:
166
+ PerformActionException: If the action fails.
167
+ RuntimeError: If the request to perform the action fails.
168
+
169
+ Returns:
170
+ Any
171
+ """
172
+
173
+ loop = asyncio.get_event_loop()
174
+ task = loop.create_task(_perform_action_async(payload))
175
+ res = loop.run_until_complete(task)
176
+ return res
@@ -0,0 +1,22 @@
1
+ class PerformActionException(Exception):
2
+ """
3
+ A custom exception class for handling errors related to performing actions.
4
+ """
5
+
6
+ pass
7
+
8
+
9
+ class GetScreenError(Exception):
10
+ """
11
+ A custom exception class for handling errors related to getting the screen.
12
+ """
13
+
14
+ pass
15
+
16
+
17
+ class WebSocketConnectionFailed(Exception):
18
+ """
19
+ Connection to websocket was not successful
20
+ """
21
+
22
+ pass