clerk-sdk 0.4.14__tar.gz → 0.4.16__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 (70) hide show
  1. {clerk_sdk-0.4.14/clerk_sdk.egg-info → clerk_sdk-0.4.16}/PKG-INFO +1 -1
  2. {clerk_sdk-0.4.14 → clerk_sdk-0.4.16}/clerk/__init__.py +1 -1
  3. {clerk_sdk-0.4.14 → clerk_sdk-0.4.16}/clerk/base.py +1 -1
  4. {clerk_sdk-0.4.14 → clerk_sdk-0.4.16}/clerk/gui_automation/client.py +5 -0
  5. {clerk_sdk-0.4.14 → clerk_sdk-0.4.16}/clerk/gui_automation/client_actor/client_actor.py +18 -2
  6. clerk_sdk-0.4.16/clerk/gui_automation/decorators/gui_automation.py +113 -0
  7. {clerk_sdk-0.4.14 → clerk_sdk-0.4.16/clerk_sdk.egg-info}/PKG-INFO +1 -1
  8. {clerk_sdk-0.4.14 → clerk_sdk-0.4.16}/setup.py +1 -1
  9. clerk_sdk-0.4.14/clerk/gui_automation/decorators/gui_automation.py +0 -150
  10. {clerk_sdk-0.4.14 → clerk_sdk-0.4.16}/LICENSE +0 -0
  11. {clerk_sdk-0.4.14 → clerk_sdk-0.4.16}/MANIFEST.in +0 -0
  12. {clerk_sdk-0.4.14 → clerk_sdk-0.4.16}/README.md +0 -0
  13. {clerk_sdk-0.4.14 → clerk_sdk-0.4.16}/clerk/client.py +0 -0
  14. {clerk_sdk-0.4.14 → clerk_sdk-0.4.16}/clerk/decorator/__init__.py +0 -0
  15. {clerk_sdk-0.4.14 → clerk_sdk-0.4.16}/clerk/decorator/models.py +0 -0
  16. {clerk_sdk-0.4.14 → clerk_sdk-0.4.16}/clerk/decorator/task_decorator.py +0 -0
  17. {clerk_sdk-0.4.14 → clerk_sdk-0.4.16}/clerk/exceptions/__init__.py +0 -0
  18. {clerk_sdk-0.4.14 → clerk_sdk-0.4.16}/clerk/exceptions/exceptions.py +0 -0
  19. {clerk_sdk-0.4.14 → clerk_sdk-0.4.16}/clerk/exceptions/remote_device.py +0 -0
  20. {clerk_sdk-0.4.14 → clerk_sdk-0.4.16}/clerk/gui_automation/__init__.py +0 -0
  21. {clerk_sdk-0.4.14 → clerk_sdk-0.4.16}/clerk/gui_automation/action_model/__init__.py +0 -0
  22. {clerk_sdk-0.4.14 → clerk_sdk-0.4.16}/clerk/gui_automation/action_model/model.py +0 -0
  23. {clerk_sdk-0.4.14 → clerk_sdk-0.4.16}/clerk/gui_automation/action_model/utils.py +0 -0
  24. {clerk_sdk-0.4.14 → clerk_sdk-0.4.16}/clerk/gui_automation/client_actor/__init__.py +0 -0
  25. {clerk_sdk-0.4.14 → clerk_sdk-0.4.16}/clerk/gui_automation/client_actor/exception.py +0 -0
  26. {clerk_sdk-0.4.14 → clerk_sdk-0.4.16}/clerk/gui_automation/client_actor/model.py +0 -0
  27. {clerk_sdk-0.4.14 → clerk_sdk-0.4.16}/clerk/gui_automation/decorators/__init__.py +0 -0
  28. {clerk_sdk-0.4.14 → clerk_sdk-0.4.16}/clerk/gui_automation/exceptions/__init__.py +0 -0
  29. {clerk_sdk-0.4.14 → clerk_sdk-0.4.16}/clerk/gui_automation/exceptions/agent_manager.py +0 -0
  30. {clerk_sdk-0.4.14 → clerk_sdk-0.4.16}/clerk/gui_automation/exceptions/modality/__init__.py +0 -0
  31. {clerk_sdk-0.4.14 → clerk_sdk-0.4.16}/clerk/gui_automation/exceptions/modality/exc.py +0 -0
  32. {clerk_sdk-0.4.14 → clerk_sdk-0.4.16}/clerk/gui_automation/exceptions/websocket.py +0 -0
  33. {clerk_sdk-0.4.14 → clerk_sdk-0.4.16}/clerk/gui_automation/requirements.txt +0 -0
  34. {clerk_sdk-0.4.14 → clerk_sdk-0.4.16}/clerk/gui_automation/ui_actions/__init__.py +0 -0
  35. {clerk_sdk-0.4.14 → clerk_sdk-0.4.16}/clerk/gui_automation/ui_actions/actions.py +0 -0
  36. {clerk_sdk-0.4.14 → clerk_sdk-0.4.16}/clerk/gui_automation/ui_actions/base.py +0 -0
  37. {clerk_sdk-0.4.14 → clerk_sdk-0.4.16}/clerk/gui_automation/ui_actions/support.py +0 -0
  38. {clerk_sdk-0.4.14 → clerk_sdk-0.4.16}/clerk/gui_automation/ui_state_inspector/__init__.py +0 -0
  39. {clerk_sdk-0.4.14 → clerk_sdk-0.4.16}/clerk/gui_automation/ui_state_inspector/gui_vision.py +0 -0
  40. {clerk_sdk-0.4.14 → clerk_sdk-0.4.16}/clerk/gui_automation/ui_state_inspector/models.py +0 -0
  41. {clerk_sdk-0.4.14 → clerk_sdk-0.4.16}/clerk/gui_automation/ui_state_machine/__init__.py +0 -0
  42. {clerk_sdk-0.4.14 → clerk_sdk-0.4.16}/clerk/gui_automation/ui_state_machine/ai_recovery.py +0 -0
  43. {clerk_sdk-0.4.14 → clerk_sdk-0.4.16}/clerk/gui_automation/ui_state_machine/decorators.py +0 -0
  44. {clerk_sdk-0.4.14 → clerk_sdk-0.4.16}/clerk/gui_automation/ui_state_machine/exceptions.py +0 -0
  45. {clerk_sdk-0.4.14 → clerk_sdk-0.4.16}/clerk/gui_automation/ui_state_machine/state_machine.py +0 -0
  46. {clerk_sdk-0.4.14 → clerk_sdk-0.4.16}/clerk/models/__init__.py +0 -0
  47. {clerk_sdk-0.4.14 → clerk_sdk-0.4.16}/clerk/models/document.py +0 -0
  48. {clerk_sdk-0.4.14 → clerk_sdk-0.4.16}/clerk/models/document_statuses.py +0 -0
  49. {clerk_sdk-0.4.14 → clerk_sdk-0.4.16}/clerk/models/file.py +0 -0
  50. {clerk_sdk-0.4.14 → clerk_sdk-0.4.16}/clerk/models/remote_device.py +0 -0
  51. {clerk_sdk-0.4.14 → clerk_sdk-0.4.16}/clerk/models/response_model.py +0 -0
  52. {clerk_sdk-0.4.14 → clerk_sdk-0.4.16}/clerk/models/ui_operator.py +0 -0
  53. {clerk_sdk-0.4.14 → clerk_sdk-0.4.16}/clerk/utils/__init__.py +0 -0
  54. {clerk_sdk-0.4.14 → clerk_sdk-0.4.16}/clerk/utils/logger.py +0 -0
  55. {clerk_sdk-0.4.14 → clerk_sdk-0.4.16}/clerk/utils/save_artifact.py +0 -0
  56. {clerk_sdk-0.4.14 → clerk_sdk-0.4.16}/clerk_sdk.egg-info/SOURCES.txt +0 -0
  57. {clerk_sdk-0.4.14 → clerk_sdk-0.4.16}/clerk_sdk.egg-info/dependency_links.txt +0 -0
  58. {clerk_sdk-0.4.14 → clerk_sdk-0.4.16}/clerk_sdk.egg-info/requires.txt +0 -0
  59. {clerk_sdk-0.4.14 → clerk_sdk-0.4.16}/clerk_sdk.egg-info/top_level.txt +0 -0
  60. {clerk_sdk-0.4.14 → clerk_sdk-0.4.16}/pyproject.toml +0 -0
  61. {clerk_sdk-0.4.14 → clerk_sdk-0.4.16}/requirements.txt +0 -0
  62. {clerk_sdk-0.4.14 → clerk_sdk-0.4.16}/setup.cfg +0 -0
  63. {clerk_sdk-0.4.14 → clerk_sdk-0.4.16}/tests/test_base.py +0 -0
  64. {clerk_sdk-0.4.14 → clerk_sdk-0.4.16}/tests/test_client.py +0 -0
  65. {clerk_sdk-0.4.14 → clerk_sdk-0.4.16}/tests/test_document_models.py +0 -0
  66. {clerk_sdk-0.4.14 → clerk_sdk-0.4.16}/tests/test_exceptions.py +0 -0
  67. {clerk_sdk-0.4.14 → clerk_sdk-0.4.16}/tests/test_file_models.py +0 -0
  68. {clerk_sdk-0.4.14 → clerk_sdk-0.4.16}/tests/test_gui_automation.py +0 -0
  69. {clerk_sdk-0.4.14 → clerk_sdk-0.4.16}/tests/test_task_decorator.py +0 -0
  70. {clerk_sdk-0.4.14 → clerk_sdk-0.4.16}/tests/test_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: clerk-sdk
3
- Version: 0.4.14
3
+ Version: 0.4.16
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
@@ -1,4 +1,4 @@
1
1
  from .client import Clerk
2
2
 
3
3
 
4
- __version__ = "0.4.14"
4
+ __version__ = "0.4.16"
@@ -50,7 +50,7 @@ class BaseClerk(BaseModel):
50
50
  headers: Dict[str, str] = {},
51
51
  json: Dict[str, Any] = {},
52
52
  params: Dict[str, Any] = {},
53
- ) -> StandardResponse[Dict[str, Any]]:
53
+ ) -> StandardResponse[Any]:
54
54
 
55
55
  merged_headers = {**self.headers, **headers}
56
56
  url = f"{self.base_url}{endpoint}"
@@ -40,6 +40,11 @@ class RPAClerk(BaseClerk):
40
40
  json={"id": remote_device.id, "name": remote_device.name, "run_id": run_id},
41
41
  )
42
42
 
43
+ def get_wss_token(self) -> str:
44
+ endpoint = "/wss_token"
45
+ res = self.get_request(endpoint=endpoint)
46
+ return res.data[0]
47
+
43
48
  def get_coordinates(self, payload: Dict[str, Any]) -> Coords:
44
49
  endpoint = "/action_model/get_coordinates"
45
50
  try:
@@ -1,12 +1,14 @@
1
1
  import asyncio
2
2
  import json
3
3
  import os
4
+ import backoff
4
5
  from typing import Any, Dict, Union
5
6
 
7
+
6
8
  import pydantic
7
9
  import requests
8
10
 
9
-
11
+ from websockets import WebSocketException
10
12
  from .model import (
11
13
  ExecutePayload,
12
14
  DeleteFilesExecutePayload,
@@ -15,12 +17,24 @@ from .model import (
15
17
  WindowExecutePayload,
16
18
  GetFileExecutePayload,
17
19
  )
18
- import backoff
19
20
 
20
21
  from .model import PerformActionResponse, ActionStates
21
22
  from .exception import PerformActionException, GetScreenError
22
23
 
23
24
 
25
+ async def before_retry(details: Any):
26
+ from ..decorators.gui_automation import reconnect_ws
27
+
28
+ await reconnect_ws()
29
+
30
+
31
+ @backoff.on_exception(
32
+ backoff.constant,
33
+ WebSocketException,
34
+ interval=1,
35
+ max_tries=5,
36
+ on_backoff=before_retry,
37
+ )
24
38
  async def _perform_action_ws(payload: Dict[str, Any]) -> PerformActionResponse:
25
39
  """Perform an action over a WebSocket connection.
26
40
 
@@ -48,6 +62,8 @@ async def _perform_action_ws(payload: Dict[str, Any]) -> PerformActionResponse:
48
62
  return PerformActionResponse(**json.loads(action_info))
49
63
  else:
50
64
  raise RuntimeError("Received ACK != OK")
65
+ except WebSocketException as e:
66
+ raise e
51
67
  except asyncio.TimeoutError:
52
68
  raise RuntimeError("The ack message did not arrive.")
53
69
  else:
@@ -0,0 +1,113 @@
1
+ import asyncio
2
+ import functools
3
+ import os
4
+ import time
5
+ from typing import Any, Callable, Dict, Sequence, Union
6
+
7
+ from websockets.asyncio.client import connect, ClientConnection
8
+ from websockets.protocol import State
9
+
10
+ from clerk.gui_automation.client import RPAClerk
11
+ from clerk.gui_automation.exceptions.agent_manager import (
12
+ ClientAvailabilityTimeout,
13
+ NoClientsAvailable,
14
+ )
15
+ from clerk.models.remote_device import RemoteDevice
16
+ from clerk.decorator.models import ClerkCodePayload
17
+ from clerk.utils import logger
18
+ from ..exceptions.websocket import WebSocketConnectionFailed
19
+
20
+
21
+ # Global handle to the live connection (if any)
22
+ global_ws: Union[ClientConnection, None] = None
23
+
24
+ clerk_client = RPAClerk()
25
+ wss_uri = "wss://agent-manager.f-one.group/action"
26
+
27
+
28
+ async def connect_to_ws(uri: str) -> ClientConnection:
29
+ # Same knobs as before, just via the new connect()
30
+ return await connect(uri, max_size=2**23, ping_timeout=3600)
31
+
32
+
33
+ async def close_ws_connection(ws_conn: ClientConnection):
34
+ await ws_conn.close()
35
+
36
+
37
+ async def reconnect_ws():
38
+ global global_ws
39
+
40
+ remote_device_name = os.getenv("REMOTE_DEVICE_NAME")
41
+ if not remote_device_name:
42
+ raise RuntimeError(
43
+ "REMOTE_DEVICE_NAME environmental variable is required for reconnecting WebSocket."
44
+ )
45
+ wss_token = clerk_client.get_wss_token()
46
+ uri = f"{wss_uri}/{remote_device_name}/publisher?token={wss_token}"
47
+ global_ws = await connect_to_ws(uri)
48
+
49
+
50
+ def gui_automation():
51
+ """
52
+ Decorator that:
53
+ • Allocates a remote device,
54
+ • Opens a WebSocket to the agent manager,
55
+ • Passes control to the wrapped function,
56
+ • Cleans everything up afterwards.
57
+ """
58
+ remote_device_name: str | None = os.getenv("REMOTE_DEVICE_NAME")
59
+
60
+ if not remote_device_name:
61
+ raise ValueError("REMOTE_DEVICE_NAME environmental variable is required.")
62
+
63
+ wss_token = clerk_client.get_wss_token()
64
+
65
+ def decorator(func: Callable):
66
+ @functools.wraps(func)
67
+ def wrapper(
68
+ payload: ClerkCodePayload, *args: Sequence[Any], **kwargs: Dict[str, Any]
69
+ ):
70
+ global global_ws
71
+
72
+ os.environ["_document_id"] = payload.document.id
73
+ os.environ["_run_id"] = payload.run_id or ""
74
+
75
+ # Create a dedicated loop for the WebSocket work
76
+ event_loop = asyncio.new_event_loop()
77
+ asyncio.set_event_loop(event_loop)
78
+
79
+ try:
80
+ task = event_loop.create_task(
81
+ connect_to_ws(
82
+ f"{wss_uri}/{remote_device_name}/publisher"
83
+ f"?token={wss_token}"
84
+ )
85
+ )
86
+ global_ws = event_loop.run_until_complete(task)
87
+
88
+ if global_ws and global_ws.state is State.OPEN:
89
+ logger.debug("WebSocket connection established.")
90
+ func_ret = func(payload, *args, **kwargs)
91
+ else:
92
+ global_ws = None
93
+ raise WebSocketConnectionFailed()
94
+
95
+ except Exception:
96
+ raise
97
+ finally:
98
+ os.environ.pop("_run_id", None)
99
+ os.environ.pop("_document_id", None)
100
+
101
+ if global_ws and global_ws.state is State.OPEN:
102
+ close_task = event_loop.create_task(close_ws_connection(global_ws))
103
+ event_loop.run_until_complete(close_task)
104
+ logger.debug("WebSocket connection closed.")
105
+
106
+ event_loop.run_until_complete(event_loop.shutdown_asyncgens())
107
+ event_loop.close()
108
+
109
+ return func_ret
110
+
111
+ return wrapper
112
+
113
+ return decorator
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: clerk-sdk
3
- Version: 0.4.14
3
+ Version: 0.4.16
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
@@ -13,7 +13,7 @@ gui_requirements = get_requirements("./clerk/gui_automation")
13
13
 
14
14
  setup(
15
15
  name="clerk-sdk",
16
- version="0.4.14",
16
+ version="0.4.16",
17
17
  description="Library for interacting with Clerk",
18
18
  long_description=open("README.md").read(),
19
19
  long_description_content_type="text/markdown",
@@ -1,150 +0,0 @@
1
- import asyncio
2
- import functools
3
- import os
4
- import time
5
- from typing import Callable, Union
6
-
7
- from websockets.asyncio.client import connect, ClientConnection
8
- from websockets.protocol import State
9
-
10
- from clerk.gui_automation.client import RPAClerk
11
- from clerk.gui_automation.exceptions.agent_manager import (
12
- ClientAvailabilityTimeout,
13
- NoClientsAvailable,
14
- )
15
- from clerk.models.remote_device import RemoteDevice
16
- from clerk.decorator.models import ClerkCodePayload
17
- from clerk.utils import logger
18
- from ..exceptions.websocket import WebSocketConnectionFailed
19
-
20
-
21
- # Global handle to the live connection (if any)
22
- global_ws: Union[ClientConnection, None] = None
23
-
24
- clerk_client = RPAClerk()
25
- wss_uri = "wss://agent-manager.f-one.group/action"
26
-
27
- REMOTE_DEVICE_ALLOCATION_TIMEOUT = int(
28
- os.getenv("REMOTE_DEVICE_ALLOCATION_TIMEOUT", 60)
29
- )
30
- REMOTE_DEVICE_ALLOCATION_MAX_TRIES = int(
31
- os.getenv("REMOTE_DEVICE_ALLOCATION_MAX_TRIES", 60)
32
- )
33
-
34
-
35
- def _allocate_remote_device(
36
- clerk_client: RPAClerk, group_name: str, run_id: str
37
- ) -> RemoteDevice:
38
- remote_device = None
39
- retries = 0
40
-
41
- while True:
42
- try:
43
- remote_device = clerk_client.allocate_remote_device(
44
- group_name=group_name, run_id=run_id
45
- )
46
- os.environ["REMOTE_DEVICE_ID"] = remote_device.id
47
- os.environ["REMOTE_DEVICE_NAME"] = remote_device.name
48
- logger.debug(f"Remote device allocated: {remote_device.name}")
49
- return remote_device
50
-
51
- except NoClientsAvailable:
52
- logger.warning(
53
- f"No clients are available for {group_name} group. Initiating a {REMOTE_DEVICE_ALLOCATION_TIMEOUT} seconds wait. Retry count: {retries}"
54
- )
55
- if retries >= REMOTE_DEVICE_ALLOCATION_MAX_TRIES:
56
- raise ClientAvailabilityTimeout(
57
- f"No clients available for {group_name} group after {REMOTE_DEVICE_ALLOCATION_TIMEOUT * REMOTE_DEVICE_ALLOCATION_MAX_TRIES} seconds"
58
- )
59
- time.sleep(REMOTE_DEVICE_ALLOCATION_TIMEOUT)
60
- retries += 1
61
-
62
-
63
- def _deallocate_target(
64
- clerk_client: RPAClerk, remote_device: RemoteDevice, run_id: str
65
- ):
66
- clerk_client.deallocate_remote_device(remote_device=remote_device, run_id=run_id)
67
- logger.debug("Remote device deallocated")
68
- os.environ.pop("REMOTE_DEVICE_ID", None)
69
- os.environ.pop("REMOTE_DEVICE_NAME", None)
70
-
71
-
72
- def gui_automation(
73
- reserve_client: bool = False,
74
- ):
75
- """
76
- Decorator that:
77
- • Allocates a remote device,
78
- • Opens a WebSocket to the agent manager,
79
- • Passes control to the wrapped function,
80
- • Cleans everything up afterwards.
81
- """
82
- group_name: str | None = os.getenv("REMOTE_DEVICE_GROUP")
83
- if not group_name:
84
- raise ValueError("REMOTE_DEVICE_GROUP environmental variable is required.")
85
-
86
- async def connect_to_ws(uri: str) -> ClientConnection:
87
- # Same knobs as before, just via the new connect()
88
- return await connect(uri, max_size=2**23, ping_timeout=3600)
89
-
90
- async def close_ws_connection(ws_conn: ClientConnection):
91
- await ws_conn.close()
92
-
93
- def decorator(func: Callable):
94
- @functools.wraps(func)
95
- def wrapper(payload: ClerkCodePayload, *args, **kwargs):
96
- global global_ws
97
- force_deallocate = False
98
- os.environ["_document_id"] = payload.document.id
99
- os.environ["_run_id"] = payload.run_id
100
-
101
- remote_device = _allocate_remote_device(
102
- clerk_client, group_name, payload.run_id
103
- )
104
-
105
- # Create a dedicated loop for the WebSocket work
106
- event_loop = asyncio.new_event_loop()
107
- asyncio.set_event_loop(event_loop)
108
-
109
- try:
110
- task = event_loop.create_task(
111
- connect_to_ws(
112
- f"{wss_uri}/{remote_device.name}/publisher"
113
- f"?token={remote_device.wss_token}"
114
- )
115
- )
116
- global_ws = event_loop.run_until_complete(task)
117
-
118
- if global_ws and global_ws.state is State.OPEN:
119
- logger.debug("WebSocket connection established.")
120
- func_ret = func(payload, *args, **kwargs)
121
- else:
122
- global_ws = None
123
- raise WebSocketConnectionFailed()
124
-
125
- except Exception as e:
126
- force_deallocate = True
127
- raise
128
- finally:
129
- os.environ.pop("_run_id", None)
130
- os.environ.pop("_document_id", None)
131
- if not reserve_client or force_deallocate:
132
- _deallocate_target(clerk_client, remote_device, payload.run_id)
133
- else:
134
- logger.warning(
135
- f"The client stayed reserved for the this run id: {payload.run_id}"
136
- )
137
-
138
- if global_ws and global_ws.state is State.OPEN:
139
- close_task = event_loop.create_task(close_ws_connection(global_ws))
140
- event_loop.run_until_complete(close_task)
141
- logger.debug("WebSocket connection closed.")
142
-
143
- event_loop.run_until_complete(event_loop.shutdown_asyncgens())
144
- event_loop.close()
145
-
146
- return func_ret
147
-
148
- return wrapper
149
-
150
- return decorator
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes