clerk-sdk 0.4.4__py3-none-any.whl → 0.4.6__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/__init__.py +3 -0
- clerk/client.py +72 -0
- clerk/gui_automation/client_actor/model.py +13 -16
- clerk/gui_automation/ui_actions/actions.py +24 -24
- clerk/gui_automation/ui_actions/base.py +2 -2
- clerk/utils/logger.py +4 -1
- clerk_sdk-0.4.6.dist-info/METADATA +254 -0
- {clerk_sdk-0.4.4.dist-info → clerk_sdk-0.4.6.dist-info}/RECORD +11 -11
- clerk_sdk-0.4.4.dist-info/METADATA +0 -128
- {clerk_sdk-0.4.4.dist-info → clerk_sdk-0.4.6.dist-info}/WHEEL +0 -0
- {clerk_sdk-0.4.4.dist-info → clerk_sdk-0.4.6.dist-info}/licenses/LICENSE +0 -0
- {clerk_sdk-0.4.4.dist-info → clerk_sdk-0.4.6.dist-info}/top_level.txt +0 -0
clerk/__init__.py
CHANGED
clerk/client.py
CHANGED
|
@@ -14,6 +14,78 @@ class Clerk(BaseClerk):
|
|
|
14
14
|
)
|
|
15
15
|
return Document(**res.data[0])
|
|
16
16
|
|
|
17
|
+
def cancel_document_run(self, document_id: str) -> Document:
|
|
18
|
+
endpoint = f"/document/{document_id}/cancel"
|
|
19
|
+
res = self.post_request(endpoint=endpoint)
|
|
20
|
+
return Document(**res.data[0])
|
|
21
|
+
|
|
22
|
+
def update_document_structured_data(
|
|
23
|
+
self, document_id: str, updated_structured_data: Dict[str, Any]
|
|
24
|
+
) -> Document:
|
|
25
|
+
endpoint = f"/document/{document_id}"
|
|
26
|
+
payload = dict(structured_data=updated_structured_data)
|
|
27
|
+
res = self.put_request(endpoint, json=payload)
|
|
28
|
+
|
|
29
|
+
return Document(**res.data[0])
|
|
30
|
+
|
|
31
|
+
def get_document(self, document_id: str) -> Document:
|
|
32
|
+
endpoint = f"/document/{document_id}"
|
|
33
|
+
res = self.get_request(endpoint=endpoint)
|
|
34
|
+
return Document(**res.data[0])
|
|
35
|
+
|
|
36
|
+
def get_documents(self, request: GetDocumentsRequest) -> List[Document]:
|
|
37
|
+
if not any(
|
|
38
|
+
[
|
|
39
|
+
request.organization_id,
|
|
40
|
+
request.project_id,
|
|
41
|
+
request.start_date,
|
|
42
|
+
request.end_date,
|
|
43
|
+
request.query,
|
|
44
|
+
request.include_statuses,
|
|
45
|
+
]
|
|
46
|
+
):
|
|
47
|
+
raise ValueError(
|
|
48
|
+
"At least one query parameter (organization_id, project_id, start_date, end_date, query, or include_statuses) must be provided."
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
endpoint = f"/documents"
|
|
52
|
+
params = request.model_dump(mode="json")
|
|
53
|
+
res = self.get_request(endpoint, params=params)
|
|
54
|
+
|
|
55
|
+
return [Document(**d) for d in res.data]
|
|
56
|
+
|
|
57
|
+
def get_files_document(self, document_id: str) -> List[ParsedFile]:
|
|
58
|
+
endpoint = f"/document/{document_id}/files"
|
|
59
|
+
res = self.get_request(endpoint=endpoint)
|
|
60
|
+
return [ParsedFile(**d) for d in res.data]
|
|
61
|
+
|
|
62
|
+
def add_files_to_document(
|
|
63
|
+
self,
|
|
64
|
+
document_id: str,
|
|
65
|
+
type: Literal["input", "output"],
|
|
66
|
+
files: List[UploadFile],
|
|
67
|
+
):
|
|
68
|
+
endpoint = f"/document/{document_id}/files/upload"
|
|
69
|
+
params = {"type": type}
|
|
70
|
+
files_data = [f.to_multipart_format() for f in files]
|
|
71
|
+
self.post_request(endpoint, params=params, files=files_data)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class ClerkDocument(BaseClerk):
|
|
75
|
+
endpoint: str = "/document"
|
|
76
|
+
|
|
77
|
+
def upload_document(self, request: UploadDocumentRequest) -> Document:
|
|
78
|
+
endpoint = "/document"
|
|
79
|
+
res = self.post_request(
|
|
80
|
+
endpoint=endpoint, data=request.data, files=request.files_
|
|
81
|
+
)
|
|
82
|
+
return Document(**res.data[0])
|
|
83
|
+
|
|
84
|
+
def cancel_document_run(self, document_id: str) -> Document:
|
|
85
|
+
endpoint = f"/document/{document_id}/cancel"
|
|
86
|
+
res = self.post_request(endpoint=endpoint)
|
|
87
|
+
return Document(**res.data[0])
|
|
88
|
+
|
|
17
89
|
def update_document_structured_data(
|
|
18
90
|
self, document_id: str, updated_structured_data: Dict[str, Any]
|
|
19
91
|
) -> Document:
|
|
@@ -1,22 +1,20 @@
|
|
|
1
|
-
from typing import Any, List, Literal, Optional,
|
|
1
|
+
from typing import Any, List, Literal, Optional, Union
|
|
2
2
|
from pydantic import BaseModel, Field
|
|
3
3
|
from enum import Enum
|
|
4
4
|
|
|
5
|
-
from clerk.gui_automation.ui_actions.base import ActionTypes
|
|
6
5
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
# ]
|
|
6
|
+
ActionTypes = Literal[
|
|
7
|
+
"left_click",
|
|
8
|
+
"right_click",
|
|
9
|
+
"middle_click",
|
|
10
|
+
"double_click",
|
|
11
|
+
"send_keys",
|
|
12
|
+
"press_keys",
|
|
13
|
+
"hot_keys",
|
|
14
|
+
"paste_text",
|
|
15
|
+
"get_text",
|
|
16
|
+
"scroll",
|
|
17
|
+
]
|
|
20
18
|
|
|
21
19
|
|
|
22
20
|
class ActionStates(Enum):
|
|
@@ -79,7 +77,6 @@ class WindowExecutePayload(BaseModel):
|
|
|
79
77
|
"close_window",
|
|
80
78
|
"activate_window",
|
|
81
79
|
]
|
|
82
|
-
|
|
83
80
|
window_name: str
|
|
84
81
|
timeout: int = Field(default=10)
|
|
85
82
|
|
|
@@ -45,7 +45,7 @@ class File(BaseModel):
|
|
|
45
45
|
|
|
46
46
|
@field_validator("content", mode="before")
|
|
47
47
|
@classmethod
|
|
48
|
-
def convert_to_bytes(cls, v
|
|
48
|
+
def convert_to_bytes(cls, v) -> bytes:
|
|
49
49
|
if isinstance(v, str):
|
|
50
50
|
from base64 import b64decode
|
|
51
51
|
|
|
@@ -75,7 +75,7 @@ class LeftClick(BaseAction):
|
|
|
75
75
|
LeftClick(target="Suche").above("Kalender").do()
|
|
76
76
|
"""
|
|
77
77
|
|
|
78
|
-
action_type = "left_click"
|
|
78
|
+
action_type: Literal["left_click"] = "left_click"
|
|
79
79
|
|
|
80
80
|
@backoff.on_exception(
|
|
81
81
|
backoff.expo,
|
|
@@ -100,7 +100,7 @@ class LeftClick(BaseAction):
|
|
|
100
100
|
|
|
101
101
|
@property
|
|
102
102
|
def actionable_string(self):
|
|
103
|
-
return f"LeftClick(action_type='{self.action_type}', target='{self.target}', anchor='{self.
|
|
103
|
+
return f"LeftClick(action_type='{self.action_type}', target='{self.target}', anchor='{self.anchor}', relation='{self.relation}').do()"
|
|
104
104
|
|
|
105
105
|
|
|
106
106
|
class RightClick(BaseAction):
|
|
@@ -118,7 +118,7 @@ class RightClick(BaseAction):
|
|
|
118
118
|
RightClick(target="Suche").above("Kalender").do()
|
|
119
119
|
"""
|
|
120
120
|
|
|
121
|
-
action_type = "right_click"
|
|
121
|
+
action_type: Literal["right_click"] = "right_click"
|
|
122
122
|
|
|
123
123
|
@backoff.on_exception(
|
|
124
124
|
backoff.expo,
|
|
@@ -143,7 +143,7 @@ class RightClick(BaseAction):
|
|
|
143
143
|
|
|
144
144
|
@property
|
|
145
145
|
def actionable_string(self):
|
|
146
|
-
return f"RightClick(action_type='{self.action_type}', target='{self.target}', anchor='{self.
|
|
146
|
+
return f"RightClick(action_type='{self.action_type}', target='{self.target}', anchor='{self.anchor}', relation='{self.relation}').do()"
|
|
147
147
|
|
|
148
148
|
|
|
149
149
|
class MiddleClickAction(BaseAction):
|
|
@@ -161,7 +161,7 @@ class MiddleClickAction(BaseAction):
|
|
|
161
161
|
MiddleClickAction(target="Suche").above("Kalender").do()
|
|
162
162
|
"""
|
|
163
163
|
|
|
164
|
-
action_type = "middle_click"
|
|
164
|
+
action_type: Literal["middle_click"] = "middle_click"
|
|
165
165
|
|
|
166
166
|
@backoff.on_exception(
|
|
167
167
|
backoff.expo,
|
|
@@ -186,7 +186,7 @@ class MiddleClickAction(BaseAction):
|
|
|
186
186
|
|
|
187
187
|
@property
|
|
188
188
|
def actionable_string(self):
|
|
189
|
-
return f"MiddleClickAction(action_type='{self.action_type}', target='{self.target}', anchor='{self.
|
|
189
|
+
return f"MiddleClickAction(action_type='{self.action_type}', target='{self.target}', anchor='{self.anchor}', relation='{self.relation}').do()"
|
|
190
190
|
|
|
191
191
|
|
|
192
192
|
class DoubleClick(BaseAction):
|
|
@@ -204,7 +204,7 @@ class DoubleClick(BaseAction):
|
|
|
204
204
|
DoubleClick(target="Suche").above("Kalender").do()
|
|
205
205
|
"""
|
|
206
206
|
|
|
207
|
-
action_type = "double_click"
|
|
207
|
+
action_type: Literal["double_click"] = "double_click"
|
|
208
208
|
|
|
209
209
|
@backoff.on_exception(
|
|
210
210
|
backoff.expo,
|
|
@@ -229,7 +229,7 @@ class DoubleClick(BaseAction):
|
|
|
229
229
|
|
|
230
230
|
@property
|
|
231
231
|
def actionable_string(self):
|
|
232
|
-
return f"DoubleClick(action_type='{self.action_type}', target='{self.target}', anchor='{self.
|
|
232
|
+
return f"DoubleClick(action_type='{self.action_type}', target='{self.target}', anchor='{self.anchor}', relation='{self.relation}').do()"
|
|
233
233
|
|
|
234
234
|
|
|
235
235
|
class Scroll(BaseAction):
|
|
@@ -249,7 +249,7 @@ class Scroll(BaseAction):
|
|
|
249
249
|
DoubleClick(target="Suche").above("Kalender").do()
|
|
250
250
|
"""
|
|
251
251
|
|
|
252
|
-
action_type = "scroll"
|
|
252
|
+
action_type: Literal["scroll"] = "scroll"
|
|
253
253
|
clicks: int
|
|
254
254
|
click_coords: List[int] = Field(default=[])
|
|
255
255
|
|
|
@@ -333,7 +333,7 @@ class SendKeys(BaseAction):
|
|
|
333
333
|
|
|
334
334
|
@property
|
|
335
335
|
def actionable_string(self):
|
|
336
|
-
return f"SendKeys(action_type='{self.action_type}', target='{self.target}', anchor='{self.
|
|
336
|
+
return f"SendKeys(action_type='{self.action_type}', target='{self.target}', anchor='{self.anchor}', relation='{self.relation}', keys='{self.keys}').do()"
|
|
337
337
|
|
|
338
338
|
|
|
339
339
|
class PressKeys(BaseAction):
|
|
@@ -353,7 +353,7 @@ class PressKeys(BaseAction):
|
|
|
353
353
|
PressKeys(keys='ctrl+shift+esc').do()
|
|
354
354
|
"""
|
|
355
355
|
|
|
356
|
-
action_type = "press_keys"
|
|
356
|
+
action_type: ActionTypes = "press_keys"
|
|
357
357
|
keys: str
|
|
358
358
|
|
|
359
359
|
def do(self):
|
|
@@ -366,7 +366,7 @@ class PressKeys(BaseAction):
|
|
|
366
366
|
|
|
367
367
|
@property
|
|
368
368
|
def actionable_string(self):
|
|
369
|
-
return f"PressKeys(action_type='{self.action_type}', target='{self.target}', anchor='{self.
|
|
369
|
+
return f"PressKeys(action_type='{self.action_type}', target='{self.target}', anchor='{self.anchor}', relation='{self.relation}', keys='{self.keys}').do()"
|
|
370
370
|
|
|
371
371
|
|
|
372
372
|
class WaitFor(BaseAction):
|
|
@@ -385,7 +385,7 @@ class WaitFor(BaseAction):
|
|
|
385
385
|
WaitFor("element").do(timeout=60)
|
|
386
386
|
"""
|
|
387
387
|
|
|
388
|
-
action_type = "wait_for"
|
|
388
|
+
action_type: Literal["wait_for"] = "wait_for"
|
|
389
389
|
retry_timeout: float = 0.5
|
|
390
390
|
is_awaited: bool = True
|
|
391
391
|
|
|
@@ -450,7 +450,7 @@ class OpenApplication(BaseAction):
|
|
|
450
450
|
OpenApplication(app_path="/path/to/application.exe", app_window_name="Application Window").do()
|
|
451
451
|
"""
|
|
452
452
|
|
|
453
|
-
action_type = "open_app"
|
|
453
|
+
action_type: Literal["open_app"] = "open_app"
|
|
454
454
|
app_path: str = Field(description="Absolute path of the application")
|
|
455
455
|
app_window_name: str = Field(
|
|
456
456
|
description="Name of the application window once open. Wildcard logic enabled."
|
|
@@ -479,7 +479,7 @@ class ForceCloseApplication(BaseAction):
|
|
|
479
479
|
Executes the action to force close the application by creating and performing an ApplicationExecutePayload with the specified process name.
|
|
480
480
|
"""
|
|
481
481
|
|
|
482
|
-
action_type = "force_close_app"
|
|
482
|
+
action_type: Literal["force_close_app"] = "force_close_app"
|
|
483
483
|
process_name: str = Field(
|
|
484
484
|
description="Process name from task manager. Example: process.exe"
|
|
485
485
|
)
|
|
@@ -509,7 +509,7 @@ class SaveFiles(BaseAction):
|
|
|
509
509
|
SaveFiles(save_location="/path/to/", files=["/path/to/file_1", "/path/to/file_2"]).do()
|
|
510
510
|
"""
|
|
511
511
|
|
|
512
|
-
action_type = "save_files"
|
|
512
|
+
action_type: ActionTypes = "save_files"
|
|
513
513
|
save_location: str
|
|
514
514
|
files: Union[List[str], List[FileDetails]]
|
|
515
515
|
|
|
@@ -580,7 +580,7 @@ class GetFile(BaseAction):
|
|
|
580
580
|
GetFile(file_location="/path/to/file_1").do()
|
|
581
581
|
"""
|
|
582
582
|
|
|
583
|
-
action_type = "get_file"
|
|
583
|
+
action_type: Literal["get_file"] = "get_file"
|
|
584
584
|
file_location: str
|
|
585
585
|
|
|
586
586
|
def do(self) -> File:
|
|
@@ -605,7 +605,7 @@ class MaximizeWindow(BaseAction):
|
|
|
605
605
|
MaximizeWindow(window_name="MyWindow").do()
|
|
606
606
|
"""
|
|
607
607
|
|
|
608
|
-
action_type = "maximize_window"
|
|
608
|
+
action_type: Literal["maximize_window"] = "maximize_window"
|
|
609
609
|
window_name: str
|
|
610
610
|
|
|
611
611
|
def do(self, timeout: int = 10):
|
|
@@ -630,7 +630,7 @@ class MinimizeWindow(BaseAction):
|
|
|
630
630
|
MinimizeWindow(window_name="MyWindow").do()
|
|
631
631
|
"""
|
|
632
632
|
|
|
633
|
-
action_type = "minimize_window"
|
|
633
|
+
action_type: Literal["minimize_window"] = "minimize_window"
|
|
634
634
|
window_name: str
|
|
635
635
|
|
|
636
636
|
def do(self, timeout: int = 10):
|
|
@@ -655,7 +655,7 @@ class CloseWindow(BaseAction):
|
|
|
655
655
|
CloseWindow(window_name="MyWindow").do()
|
|
656
656
|
"""
|
|
657
657
|
|
|
658
|
-
action_type = "close_window"
|
|
658
|
+
action_type: Literal["close_window"] = "close_window"
|
|
659
659
|
window_name: str
|
|
660
660
|
|
|
661
661
|
def do(self, timeout: int = 10):
|
|
@@ -680,7 +680,7 @@ class ActivateWindow(BaseAction):
|
|
|
680
680
|
ActivateWindow(window_name="MyWindow").do()
|
|
681
681
|
"""
|
|
682
682
|
|
|
683
|
-
action_type = "activate_window"
|
|
683
|
+
action_type: Literal["activate_window"] = "activate_window"
|
|
684
684
|
window_name: str
|
|
685
685
|
|
|
686
686
|
def do(self, timeout: int = 10):
|
|
@@ -709,7 +709,7 @@ class GetText(BaseAction):
|
|
|
709
709
|
|
|
710
710
|
"""
|
|
711
711
|
|
|
712
|
-
action_type = "get_text"
|
|
712
|
+
action_type: Literal["get_text"] = "get_text"
|
|
713
713
|
|
|
714
714
|
@backoff.on_exception(
|
|
715
715
|
backoff.expo,
|
|
@@ -752,7 +752,7 @@ class PasteText(BaseAction):
|
|
|
752
752
|
|
|
753
753
|
"""
|
|
754
754
|
|
|
755
|
-
action_type = "paste_text"
|
|
755
|
+
action_type: Literal["paste_text"] = "paste_text"
|
|
756
756
|
keys: Union[str, List[str]]
|
|
757
757
|
followed_by: Optional[str] = Field(default=None)
|
|
758
758
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from typing import Literal, Self,
|
|
1
|
+
from typing import Literal, Self, Union, List, Optional
|
|
2
2
|
from pydantic import BaseModel, Field, model_validator
|
|
3
3
|
from ..client_actor import get_screen
|
|
4
4
|
from ..exceptions.modality.exc import TargetModalityError
|
|
@@ -24,7 +24,7 @@ def to_full_img_path(img: Union[str, ImageB64]) -> str:
|
|
|
24
24
|
return os.path.join(TARGET_IMAGES_PATH, img)
|
|
25
25
|
|
|
26
26
|
|
|
27
|
-
ActionTypes
|
|
27
|
+
ActionTypes = Literal[
|
|
28
28
|
"left_click",
|
|
29
29
|
"right_click",
|
|
30
30
|
"middle_click",
|
clerk/utils/logger.py
CHANGED
|
@@ -6,7 +6,10 @@ import sys
|
|
|
6
6
|
if sys.platform == "win32":
|
|
7
7
|
base_path = os.path.join(os.getcwd(), "data", "artifacts")
|
|
8
8
|
else:
|
|
9
|
-
|
|
9
|
+
if os.getenv("__TEST"):
|
|
10
|
+
base_path = os.path.join(os.getcwd(), "data", "artifacts")
|
|
11
|
+
else:
|
|
12
|
+
base_path = "/app/data/artifacts"
|
|
10
13
|
|
|
11
14
|
os.makedirs(base_path, exist_ok=True)
|
|
12
15
|
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: clerk-sdk
|
|
3
|
+
Version: 0.4.6
|
|
4
|
+
Summary: Library for interacting with Clerk
|
|
5
|
+
Home-page: https://github.com/F-ONE-Group/clerk_pypi
|
|
6
|
+
Author: F-ONE Group
|
|
7
|
+
Author-email: admin@f-one.group
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Requires-Python: >=3.11
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Requires-Dist: pydantic<3.0.0,>=2.0.0
|
|
15
|
+
Requires-Dist: backoff<3.0.0,>=2.0.0
|
|
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"
|
|
26
|
+
Dynamic: author
|
|
27
|
+
Dynamic: author-email
|
|
28
|
+
Dynamic: classifier
|
|
29
|
+
Dynamic: description
|
|
30
|
+
Dynamic: description-content-type
|
|
31
|
+
Dynamic: home-page
|
|
32
|
+
Dynamic: license-file
|
|
33
|
+
Dynamic: provides-extra
|
|
34
|
+
Dynamic: requires-dist
|
|
35
|
+
Dynamic: requires-python
|
|
36
|
+
Dynamic: summary
|
|
37
|
+
|
|
38
|
+
# Clerk Python SDK
|
|
39
|
+
|
|
40
|
+
A production-ready Python client for the Clerk API. The SDK wraps Clerk's REST endpoints, rich document models, automation helpers, and structured task decorators so that your applications can create, update, and process Clerk documents with minimal boilerplate.
|
|
41
|
+
|
|
42
|
+
## Table of Contents
|
|
43
|
+
- [Overview](#overview)
|
|
44
|
+
- [Key Features](#key-features)
|
|
45
|
+
- [Requirements](#requirements)
|
|
46
|
+
- [Installation](#installation)
|
|
47
|
+
- [Configuration](#configuration)
|
|
48
|
+
- [Quick Start](#quick-start)
|
|
49
|
+
- [Instantiate a Client](#instantiate-a-client)
|
|
50
|
+
- [Fetch Documents](#fetch-documents)
|
|
51
|
+
- [Upload a Document](#upload-a-document)
|
|
52
|
+
- [Update Structured Data](#update-structured-data)
|
|
53
|
+
- [Work with Files](#work-with-files)
|
|
54
|
+
- [Automation Utilities](#automation-utilities)
|
|
55
|
+
- [Task Decorator](#task-decorator)
|
|
56
|
+
- [GUI Automation Toolkit](#gui-automation-toolkit)
|
|
57
|
+
- [Error Handling](#error-handling)
|
|
58
|
+
- [Development Workflow](#development-workflow)
|
|
59
|
+
- [Contributing](#contributing)
|
|
60
|
+
- [License](#license)
|
|
61
|
+
|
|
62
|
+
## Overview
|
|
63
|
+
The Clerk SDK centers around the `Clerk` client (`clerk.client.Clerk`), which extends a resilient `BaseClerk` transport layer with automatic retries and typed responses. Models under `clerk.models` provide Pydantic-powered validation for documents, files, and API payloads, ensuring type safety across network boundaries. Additional modules cover automated task execution via the `clerk.decorator` package and UI workflows under `clerk.gui_automation`.
|
|
64
|
+
|
|
65
|
+
## Key Features
|
|
66
|
+
- **Document lifecycle management** – Create, fetch, list, and update Clerk documents with first-class models.
|
|
67
|
+
- **File handling** – Upload binary files or parsed base64 payloads and attach them to documents.
|
|
68
|
+
- **Robust networking** – Automatic retries for transient HTTP issues, configurable base URLs, and bearer authentication out of the box.
|
|
69
|
+
- **Structured task execution** – Decorators for running Clerk tasks locally or inside worker environments with consistent pickle-based I/O.
|
|
70
|
+
- **GUI automations** – Utilities for orchestrating low-level UI actions, state machines, and operator interactions when human-in-the-loop steps are required.
|
|
71
|
+
|
|
72
|
+
## Requirements
|
|
73
|
+
- Python 3.10+
|
|
74
|
+
- Dependencies listed in [`requirements.txt`](requirements.txt), including `pydantic` and `backoff`.
|
|
75
|
+
|
|
76
|
+
## Installation
|
|
77
|
+
Install the SDK from PyPI:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
pip install clerk-sdk
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
For local development inside this repository, install the dependencies in editable mode:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
pip install -e .[dev]
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Configuration
|
|
90
|
+
The client reads configuration from keyword arguments or environment variables.
|
|
91
|
+
|
|
92
|
+
| Setting | Environment Variable | Description |
|
|
93
|
+
| --- | --- | --- |
|
|
94
|
+
| API key | `CLERK_API_KEY` | Required secret used for bearer authentication. |
|
|
95
|
+
| Base URL | `CLERK_BASE_URL` | Optional override of the default API host (`https://api.clerk-app.com`). |
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
export CLERK_API_KEY="sk_live_123"
|
|
99
|
+
export CLERK_BASE_URL="https://staging.clerk-app.com" # optional
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
You can also pass the API key directly when instantiating `Clerk`:
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
from clerk import Clerk
|
|
106
|
+
|
|
107
|
+
client = Clerk(api_key="sk_live_123")
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Quick Start
|
|
111
|
+
The following snippets demonstrate the core document operations supported by the SDK.
|
|
112
|
+
|
|
113
|
+
### Instantiate a Client
|
|
114
|
+
```python
|
|
115
|
+
from clerk import Clerk
|
|
116
|
+
|
|
117
|
+
client = Clerk(api_key="sk_live_123")
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Fetch Documents
|
|
121
|
+
Retrieve a single document by its identifier or list documents with query filters.
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
from clerk.models.document import GetDocumentsRequest
|
|
125
|
+
|
|
126
|
+
# Single document
|
|
127
|
+
invoice = client.get_document(document_id="doc_123")
|
|
128
|
+
print(invoice.title, invoice.status)
|
|
129
|
+
|
|
130
|
+
# Query multiple documents
|
|
131
|
+
request = GetDocumentsRequest(project_id="proj_456", limit=25)
|
|
132
|
+
documents = client.get_documents(request)
|
|
133
|
+
for doc in documents:
|
|
134
|
+
print(doc.id, doc.status)
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### Upload a Document
|
|
138
|
+
Use `UploadDocumentRequest` to send metadata and file attachments. Files can be supplied as paths or `ParsedFile` instances.
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
from clerk.models.document import UploadDocumentRequest
|
|
142
|
+
|
|
143
|
+
upload_request = UploadDocumentRequest(
|
|
144
|
+
project_id="proj_456",
|
|
145
|
+
message_subject="Invoice 2024-01",
|
|
146
|
+
files=["/path/to/invoice.pdf"],
|
|
147
|
+
input_structured_data={"customer_id": "cust_789"},
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
created = client.upload_document(upload_request)
|
|
151
|
+
print(f"Created document: {created.id}")
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Update Structured Data
|
|
155
|
+
Patch a document's structured payload without re-uploading files.
|
|
156
|
+
|
|
157
|
+
```python
|
|
158
|
+
updated = client.update_document_structured_data(
|
|
159
|
+
document_id="doc_123",
|
|
160
|
+
updated_structured_data={"status": "processed", "processed_by": "automation"},
|
|
161
|
+
)
|
|
162
|
+
print(updated.structured_data)
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### Work with Files
|
|
166
|
+
Retrieve parsed file metadata or attach additional files to existing documents.
|
|
167
|
+
|
|
168
|
+
```python
|
|
169
|
+
from clerk.models.file import UploadFile
|
|
170
|
+
|
|
171
|
+
# List associated files
|
|
172
|
+
files = client.get_files_document(document_id="doc_123")
|
|
173
|
+
for file in files:
|
|
174
|
+
print(file.name, file.mimetype)
|
|
175
|
+
|
|
176
|
+
# Append output files
|
|
177
|
+
client.add_files_to_document(
|
|
178
|
+
document_id="doc_123",
|
|
179
|
+
type="output",
|
|
180
|
+
files=[
|
|
181
|
+
UploadFile(name="summary.txt", mimetype="text/plain", content=b"Processed")
|
|
182
|
+
],
|
|
183
|
+
)
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
## Custom Code Utilities
|
|
187
|
+
### Task Decorator
|
|
188
|
+
The `@clerk_code` decorator standardizes how Clerk tasks load inputs and persist outputs when executed by the Clerk workflow. It automatically reads a pickled `ClerkCodePayload` from `/app/data/input/input.pkl`, executes your function, and writes the result (or an `ApplicationException`) to `/app/data/output/output.pkl`.
|
|
189
|
+
|
|
190
|
+
```python
|
|
191
|
+
from clerk.decorator import clerk_code
|
|
192
|
+
from clerk.decorator.models import ClerkCodePayload, Document
|
|
193
|
+
|
|
194
|
+
@clerk_code()
|
|
195
|
+
def handle_document(payload: ClerkCodePayload) -> ClerkCodePayload:
|
|
196
|
+
document: Document = payload.document
|
|
197
|
+
payload.structured_data = payload.structured_data or {}
|
|
198
|
+
payload.structured_data["status"] = f"Processed {document.id}"
|
|
199
|
+
return payload
|
|
200
|
+
|
|
201
|
+
if __name__ == "__main__":
|
|
202
|
+
handle_document() # Auto-loads from pickle files when payload is omitted
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
For unit testing, you can bypass the pickle integration by passing an explicit payload instance:
|
|
206
|
+
|
|
207
|
+
```python
|
|
208
|
+
from datetime import datetime
|
|
209
|
+
from clerk.decorator.models import ClerkCodePayload, Document
|
|
210
|
+
from clerk.models.document_statuses import DocumentStatuses
|
|
211
|
+
|
|
212
|
+
sample_payload = ClerkCodePayload(
|
|
213
|
+
document=Document(
|
|
214
|
+
id="doc_123",
|
|
215
|
+
project_id="proj_456",
|
|
216
|
+
title="Sample",
|
|
217
|
+
upload_date=datetime.utcnow(),
|
|
218
|
+
status=DocumentStatuses.draft,
|
|
219
|
+
created_at=datetime.utcnow(),
|
|
220
|
+
updated_at=datetime.utcnow(),
|
|
221
|
+
),
|
|
222
|
+
structured_data={},
|
|
223
|
+
)
|
|
224
|
+
result = handle_document(sample_payload)
|
|
225
|
+
assert "Processed" in result.structured_data["status"]
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
> **Note:** Refer to `tests/test_task_decorator.py` for additional usage examples covering error propagation and pickle round-trips.
|
|
229
|
+
|
|
230
|
+
### GUI Automation Toolkit
|
|
231
|
+
The `clerk.gui_automation` package contains models, actions, and state machines for orchestrating UI interactions. Highlights include:
|
|
232
|
+
|
|
233
|
+
- `BaseAction` and concrete actions for cursor movement, clicks, and keyboard input.
|
|
234
|
+
- `ActionModel` builders that translate payloads into executable UI sequences.
|
|
235
|
+
- State machine primitives (`ui_state_machine`) to coordinate multi-step automations.
|
|
236
|
+
- Helpers for safely reading files, validating anchors, and converting payload flags.
|
|
237
|
+
|
|
238
|
+
These utilities are designed to be composed with your own automation runners or integrated into Clerk tasks. Review the tests in `tests/test_gui_automation.py` for patterns on stubbing operator clients and verifying payload transformations.
|
|
239
|
+
|
|
240
|
+
## Error Handling
|
|
241
|
+
All network helpers raise `requests` exceptions for HTTP errors. When using the task decorator, runtime failures are wrapped in `ApplicationException` objects that capture the exception type, message, and traceback for easier debugging. Deserialize the returned payload or inspect the pickle output to handle errors gracefully.
|
|
242
|
+
|
|
243
|
+
## Development Workflow
|
|
244
|
+
1. **Clone the repository** and install dependencies with `pip install -e .[dev]`.
|
|
245
|
+
2. **Run the test suite** using `pytest`. The CI workflow executes these tests before packaging releases.
|
|
246
|
+
3. **Add type-safe models** or extend the client in `clerk/client.py` and `clerk/models` as needed.
|
|
247
|
+
4. **Contribute automations** under `clerk/gui_automation` by following the established action/state machine patterns.
|
|
248
|
+
5. **Commit and open a pull request** once tests pass and documentation is updated.
|
|
249
|
+
|
|
250
|
+
## Contributing
|
|
251
|
+
Contributions are welcome! Please open an issue to discuss substantial changes, follow the existing code style (Pydantic models, typed functions, and pytest fixtures), and ensure the test suite passes before submitting a pull request.
|
|
252
|
+
|
|
253
|
+
## License
|
|
254
|
+
This project is licensed under the MIT License. See [LICENSE](LICENSE) for details.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
clerk/__init__.py,sha256=
|
|
1
|
+
clerk/__init__.py,sha256=ubAUuMPlxiWOVmwL7aMXdRDmiXjmyqibfKLX5ZcLZR4,50
|
|
2
2
|
clerk/base.py,sha256=S1RKc2pBw2FPlVjefJzsNtyTDPB0UG46C2K_QVV1opA,4008
|
|
3
|
-
clerk/client.py,sha256=
|
|
3
|
+
clerk/client.py,sha256=RdOvC23WK9ZtIXDOYaoSFk9debh3UTmstBXjAswAH6E,4981
|
|
4
4
|
clerk/decorator/__init__.py,sha256=yGGcS17VsZ7cZ-hVGCm3I3vGDJMiJIAqmDGzriIi0DI,65
|
|
5
5
|
clerk/decorator/models.py,sha256=PiAugPvX1c6BQBWtfhyh5k9Uau2OJNDIx7T1TLz7ZFY,409
|
|
6
6
|
clerk/decorator/task_decorator.py,sha256=H8caRvNvvl-IRwyREP66gBGVM-SpQJ1W7oAFImO-6Jw,3769
|
|
@@ -15,7 +15,7 @@ clerk/gui_automation/action_model/utils.py,sha256=xzFxgN-bTK6HKGS7J-esQZ-ePj_yG7
|
|
|
15
15
|
clerk/gui_automation/client_actor/__init__.py,sha256=SVuL6-oo1Xc0oJkjMKrO6mJwpPGjrCLKhDV6r2Abtf8,66
|
|
16
16
|
clerk/gui_automation/client_actor/client_actor.py,sha256=RT5WnvrM37pLpoDd_WZg8sSjBuugqMW_eDLTEkL7kWc,5117
|
|
17
17
|
clerk/gui_automation/client_actor/exception.py,sha256=zdnImHZ88yf52Xq3aMHivEU3aJg-r2c-r8x8XZnI3ic,407
|
|
18
|
-
clerk/gui_automation/client_actor/model.py,sha256=
|
|
18
|
+
clerk/gui_automation/client_actor/model.py,sha256=wVpFCi1w2kh4kAV8oNx489vf_SLUQnqhc02rFD5NIJA,6335
|
|
19
19
|
clerk/gui_automation/decorators/__init__.py,sha256=OCgXStEumscgT-RyVy5OKS7ml1w9y-lEnjCVnxuRnQs,43
|
|
20
20
|
clerk/gui_automation/decorators/gui_automation.py,sha256=jqzN_iqIq2KvjUU-JoYnUSOf-mst31EKCk8zdMFzGQM,5357
|
|
21
21
|
clerk/gui_automation/exceptions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -24,8 +24,8 @@ clerk/gui_automation/exceptions/websocket.py,sha256=-MdwSwlf1hbnu55aDgk3L1znkTZ6
|
|
|
24
24
|
clerk/gui_automation/exceptions/modality/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
25
25
|
clerk/gui_automation/exceptions/modality/exc.py,sha256=P-dMuCTyVZYD3pbGpCf_1SYEgaETn13c51pmfbsXr5k,1436
|
|
26
26
|
clerk/gui_automation/ui_actions/__init__.py,sha256=-EDQ5375HXrvG3sfFY7zOPC405YcBL6xXRACm2p-YyI,23
|
|
27
|
-
clerk/gui_automation/ui_actions/actions.py,sha256=
|
|
28
|
-
clerk/gui_automation/ui_actions/base.py,sha256=
|
|
27
|
+
clerk/gui_automation/ui_actions/actions.py,sha256=hhxl5VMDNSXdqm2L0tZqs6IhJHVXtlSVSdwsiz2BbDI,27449
|
|
28
|
+
clerk/gui_automation/ui_actions/base.py,sha256=oaUI3vIOoDwP_HdLu2GIG46-aMv0_Zv-PljMgSFeNmk,7329
|
|
29
29
|
clerk/gui_automation/ui_actions/support.py,sha256=Ulb8DBfwnrBMaYoMLDgldEy9V--NDUSdhIYXpuODZoU,5772
|
|
30
30
|
clerk/gui_automation/ui_state_inspector/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
31
31
|
clerk/gui_automation/ui_state_inspector/gui_vision.py,sha256=Pk5nuFZnp_zNbqSOtndSmgqb6PLeADJfnC-eRIJMDZk,7736
|
|
@@ -44,10 +44,10 @@ clerk/models/remote_device.py,sha256=2jijS-9WWq7y6xIbAaDaPnzZo3-tjp2-dCQvNKP8YSU
|
|
|
44
44
|
clerk/models/response_model.py,sha256=R62daUN1YVOwgnrh_epvFRsQcOwT7R4u97l73egvm-c,232
|
|
45
45
|
clerk/models/ui_operator.py,sha256=mKTJUFZgv7PeEt5oys28HVZxHOJsofmRQOcRpqj0dbU,293
|
|
46
46
|
clerk/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
47
|
-
clerk/utils/logger.py,sha256=
|
|
47
|
+
clerk/utils/logger.py,sha256=NrMIlJfVmRjjRw_N_Jngkl0qqv7btXUbg5wxcRmFEH4,3800
|
|
48
48
|
clerk/utils/save_artifact.py,sha256=94aYkYNVGcSUaSWZmdjiY6Oc-3yCKb2XWCZ56IAXQqk,1158
|
|
49
|
-
clerk_sdk-0.4.
|
|
50
|
-
clerk_sdk-0.4.
|
|
51
|
-
clerk_sdk-0.4.
|
|
52
|
-
clerk_sdk-0.4.
|
|
53
|
-
clerk_sdk-0.4.
|
|
49
|
+
clerk_sdk-0.4.6.dist-info/licenses/LICENSE,sha256=GTVQl3vH6ht70wJXKC0yMT8CmXKHxv_YyO_utAgm7EA,1065
|
|
50
|
+
clerk_sdk-0.4.6.dist-info/METADATA,sha256=gn2Ih5DuBL3CSIvPbu5HPEmw5DunhRv8GcpqGkUEqDU,9936
|
|
51
|
+
clerk_sdk-0.4.6.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
52
|
+
clerk_sdk-0.4.6.dist-info/top_level.txt,sha256=99eQiU6d05_-f41tmSFanfI_SIJeAdh7u9m3LNSfcv4,6
|
|
53
|
+
clerk_sdk-0.4.6.dist-info/RECORD,,
|
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: clerk-sdk
|
|
3
|
-
Version: 0.4.4
|
|
4
|
-
Summary: Library for interacting with Clerk
|
|
5
|
-
Home-page: https://github.com/F-ONE-Group/clerk_pypi
|
|
6
|
-
Author: F-ONE Group
|
|
7
|
-
Author-email: admin@f-one.group
|
|
8
|
-
Classifier: Programming Language :: Python :: 3
|
|
9
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
-
Classifier: Operating System :: OS Independent
|
|
11
|
-
Requires-Python: >=3.10
|
|
12
|
-
Description-Content-Type: text/markdown
|
|
13
|
-
License-File: LICENSE
|
|
14
|
-
Requires-Dist: pydantic<3.0.0,>=2.0.0
|
|
15
|
-
Requires-Dist: backoff<3.0.0,>=2.0.0
|
|
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"
|
|
26
|
-
Dynamic: author
|
|
27
|
-
Dynamic: author-email
|
|
28
|
-
Dynamic: classifier
|
|
29
|
-
Dynamic: description
|
|
30
|
-
Dynamic: description-content-type
|
|
31
|
-
Dynamic: home-page
|
|
32
|
-
Dynamic: license-file
|
|
33
|
-
Dynamic: provides-extra
|
|
34
|
-
Dynamic: requires-dist
|
|
35
|
-
Dynamic: requires-python
|
|
36
|
-
Dynamic: summary
|
|
37
|
-
|
|
38
|
-
# CLERK
|
|
39
|
-
|
|
40
|
-
`clerk-sdk` is a Python library designed to simplify interactions with the Clerk API. It provides a robust and user-friendly interface for managing documents, handling API requests, and integrating structured data models into your workflows. `clerk-sdk` is ideal for developers looking to streamline their integration with Clerk.
|
|
41
|
-
|
|
42
|
-
## Features
|
|
43
|
-
|
|
44
|
-
- **Document Management**: Retrieve and manage documents and their associated files.
|
|
45
|
-
- **API Request Handling**: Simplified GET and POST requests with automatic retries and error handling.
|
|
46
|
-
- **Data Models**: Predefined Pydantic models for structured data validation and serialization.
|
|
47
|
-
- **Task Flow Integration**: Decorator for creating and managing task flows.
|
|
48
|
-
- **Extensibility**: Easily extend and customize the library to fit your specific use case.
|
|
49
|
-
|
|
50
|
-
## Installation
|
|
51
|
-
|
|
52
|
-
Install the library using pip:
|
|
53
|
-
|
|
54
|
-
```bash
|
|
55
|
-
pip install clerk-sdk
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
## Usage
|
|
59
|
-
|
|
60
|
-
### Initialize the Client
|
|
61
|
-
|
|
62
|
-
```python
|
|
63
|
-
from clerk import Clerk
|
|
64
|
-
|
|
65
|
-
clerk_client = Clerk(api_key="your_api_key")
|
|
66
|
-
```
|
|
67
|
-
|
|
68
|
-
### Retrieve a Document
|
|
69
|
-
|
|
70
|
-
```python
|
|
71
|
-
document = clerk_client.get_document(document_id="12345")
|
|
72
|
-
print(document.title)
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
### Retrieve Files Associated with a Document
|
|
76
|
-
|
|
77
|
-
```python
|
|
78
|
-
files = clerk_client.get_files_document(document_id="12345")
|
|
79
|
-
for file in files:
|
|
80
|
-
print(file.name)
|
|
81
|
-
```
|
|
82
|
-
|
|
83
|
-
### Use the Task Decorator
|
|
84
|
-
|
|
85
|
-
#### PROD
|
|
86
|
-
|
|
87
|
-
```python
|
|
88
|
-
from clerk.decorator import clerk_code
|
|
89
|
-
from clerk.decorator.models import ClerkCodePayload
|
|
90
|
-
|
|
91
|
-
@clerk_code()
|
|
92
|
-
def main(payload: ClerkCodePayload) -> ClerkCodePayload:
|
|
93
|
-
payload.structured_data["status"] = "ok"
|
|
94
|
-
return payload
|
|
95
|
-
|
|
96
|
-
main()
|
|
97
|
-
```
|
|
98
|
-
|
|
99
|
-
#### TEST
|
|
100
|
-
|
|
101
|
-
```python
|
|
102
|
-
from clerk.decorator.models import ClerkCodePayload, Document
|
|
103
|
-
|
|
104
|
-
def test_main():
|
|
105
|
-
test_payload = ClerkCodePayload(
|
|
106
|
-
document=Document(id="doc-123", message_subject="Hello"),
|
|
107
|
-
structured_data={}
|
|
108
|
-
)
|
|
109
|
-
|
|
110
|
-
result = main(test_payload) # ✅ Just pass it!
|
|
111
|
-
assert result.structured_data["status"] == "ok"
|
|
112
|
-
```
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
## Requirements
|
|
116
|
-
|
|
117
|
-
- Python 3.10+
|
|
118
|
-
- Dependencies listed in `requirements.txt`:
|
|
119
|
-
- `pydantic>2.0.0`
|
|
120
|
-
- `backoff>2.0.0`
|
|
121
|
-
|
|
122
|
-
## License
|
|
123
|
-
|
|
124
|
-
This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
|
|
125
|
-
|
|
126
|
-
## Contributing
|
|
127
|
-
|
|
128
|
-
Contributions are welcome! Please submit a pull request or open an issue to discuss your ideas.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|