simplex 1.2.4__py3-none-any.whl → 1.2.5__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.
Potentially problematic release.
This version of simplex might be problematic. Click here for more details.
- simplex/cli.py +31 -0
- simplex/deploy/__init__.py +3 -0
- simplex/deploy/push.py +208 -0
- simplex/simplex.py +213 -303
- {simplex-1.2.4.dist-info → simplex-1.2.5.dist-info}/METADATA +11 -3
- simplex-1.2.5.dist-info/RECORD +11 -0
- simplex/constants.py +0 -1
- simplex/utils.py +0 -12
- simplex-1.2.4.dist-info/RECORD +0 -10
- {simplex-1.2.4.dist-info → simplex-1.2.5.dist-info}/LICENSE +0 -0
- {simplex-1.2.4.dist-info → simplex-1.2.5.dist-info}/WHEEL +0 -0
- {simplex-1.2.4.dist-info → simplex-1.2.5.dist-info}/entry_points.txt +0 -0
- {simplex-1.2.4.dist-info → simplex-1.2.5.dist-info}/top_level.txt +0 -0
simplex/cli.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import click
|
|
2
|
+
from .deploy import push_directory, run_directory
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
@click.group()
|
|
6
|
+
def cli():
|
|
7
|
+
"""Simplex CLI tool"""
|
|
8
|
+
pass
|
|
9
|
+
|
|
10
|
+
@cli.command()
|
|
11
|
+
@click.argument('directory', type=click.Path(exists=True, file_okay=False, dir_okay=True))
|
|
12
|
+
def push(directory):
|
|
13
|
+
try:
|
|
14
|
+
push_directory(directory)
|
|
15
|
+
except Exception as e:
|
|
16
|
+
print(f"Error running job: {e}")
|
|
17
|
+
raise
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@cli.command()
|
|
22
|
+
@click.argument('directory', type=click.Path(exists=True, file_okay=False, dir_okay=True))
|
|
23
|
+
def run(directory):
|
|
24
|
+
try:
|
|
25
|
+
run_directory(directory)
|
|
26
|
+
except Exception as e:
|
|
27
|
+
print(f"Error running job: {e}")
|
|
28
|
+
raise
|
|
29
|
+
|
|
30
|
+
def main():
|
|
31
|
+
cli()
|
simplex/deploy/push.py
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import yaml
|
|
3
|
+
import tempfile
|
|
4
|
+
import zipfile
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from dotenv import load_dotenv
|
|
7
|
+
import requests
|
|
8
|
+
from typing import Optional
|
|
9
|
+
from datetime import datetime, timedelta
|
|
10
|
+
load_dotenv() # This loads the .env file
|
|
11
|
+
|
|
12
|
+
S3_URL = "https://u3mvtbirxf.us-east-1.awsapprunner.com"
|
|
13
|
+
JOB_URL = "https://simplex--job-scheduler-app-handler.modal.run"
|
|
14
|
+
|
|
15
|
+
def read_config(directory: str) -> dict:
|
|
16
|
+
"""Read and validate the simplex.yaml configuration file"""
|
|
17
|
+
config_path = Path(directory) / "simplex.yaml"
|
|
18
|
+
if not config_path.exists():
|
|
19
|
+
raise FileNotFoundError(f"No simplex.yaml found in {directory}")
|
|
20
|
+
|
|
21
|
+
with open(config_path) as f:
|
|
22
|
+
return yaml.safe_load(f)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def create_archive(directory: str, config: dict) -> str:
|
|
26
|
+
"""Create a temporary zip archive of the entire directory"""
|
|
27
|
+
with tempfile.NamedTemporaryFile(suffix='.zip', delete=False) as tmp:
|
|
28
|
+
with zipfile.ZipFile(tmp.name, 'w', zipfile.ZIP_DEFLATED) as zip_file:
|
|
29
|
+
base_path = Path(directory)
|
|
30
|
+
|
|
31
|
+
# Walk through all files and directories
|
|
32
|
+
for root, dirs, files in os.walk(directory):
|
|
33
|
+
for file in files:
|
|
34
|
+
file_path = Path(root) / file
|
|
35
|
+
# Get path relative to the base directory
|
|
36
|
+
relative_path = file_path.relative_to(base_path)
|
|
37
|
+
# Add file to zip with its relative path
|
|
38
|
+
zip_file.write(file_path, arcname=str(relative_path))
|
|
39
|
+
|
|
40
|
+
return tmp.name
|
|
41
|
+
|
|
42
|
+
def push_directory(directory: str):
|
|
43
|
+
"""Main function to handle parsing yaml and creating zip archive"""
|
|
44
|
+
config = read_config(directory)
|
|
45
|
+
archive_path = create_archive(directory, config)
|
|
46
|
+
|
|
47
|
+
project_name = config.get('name')
|
|
48
|
+
if not project_name:
|
|
49
|
+
raise ValueError("Project name not specified in simplex.yaml")
|
|
50
|
+
|
|
51
|
+
cron_schedule = config.get("cron_schedule", None)
|
|
52
|
+
if not cron_schedule:
|
|
53
|
+
raise ValueError("Cron schedule not specified in simplex.yaml")
|
|
54
|
+
|
|
55
|
+
try:
|
|
56
|
+
s3_uri = upload_files_to_s3(archive_path, project_name)
|
|
57
|
+
except Exception as e:
|
|
58
|
+
print(f"Error uploading project: {e}")
|
|
59
|
+
raise
|
|
60
|
+
finally:
|
|
61
|
+
os.unlink(archive_path)
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
create_job_scheduled(project_name, cron_schedule, s3_uri)
|
|
65
|
+
except Exception as e:
|
|
66
|
+
print(f"Error creating job: {e}")
|
|
67
|
+
raise
|
|
68
|
+
finally:
|
|
69
|
+
print("Successfully created job!")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def upload_files_to_s3(archive_path: str, project_name: str):
|
|
74
|
+
# Read API key from environment
|
|
75
|
+
api_key = os.getenv('SIMPLEX_API_KEY')
|
|
76
|
+
if not api_key:
|
|
77
|
+
raise ValueError("SIMPLEX_API_KEY environment variable not set")
|
|
78
|
+
|
|
79
|
+
url = S3_URL + "/upload_files_to_s3"
|
|
80
|
+
|
|
81
|
+
try:
|
|
82
|
+
# Prepare the multipart form data
|
|
83
|
+
files = {
|
|
84
|
+
'zip_file': ('archive.zip', open(archive_path, 'rb'), 'application/zip')
|
|
85
|
+
}
|
|
86
|
+
data = {
|
|
87
|
+
'project_name': project_name,
|
|
88
|
+
}
|
|
89
|
+
headers = {
|
|
90
|
+
'x-api-key': api_key
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
# Make the POST request
|
|
94
|
+
response = requests.post(
|
|
95
|
+
url,
|
|
96
|
+
files=files,
|
|
97
|
+
data=data,
|
|
98
|
+
headers=headers
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
# Check response
|
|
102
|
+
response.raise_for_status()
|
|
103
|
+
result = response.json()
|
|
104
|
+
print(f"Successfully uploaded project: {result['project_name']}")
|
|
105
|
+
return result['s3_uri']
|
|
106
|
+
except Exception as e:
|
|
107
|
+
print(f"Error uploading project: {e}")
|
|
108
|
+
raise
|
|
109
|
+
|
|
110
|
+
def create_job_scheduled(project_name: str, cron_schedule: str, s3_uri: str):
|
|
111
|
+
"""Create a new job via the API"""
|
|
112
|
+
# Read API key from environment
|
|
113
|
+
api_key = os.getenv('SIMPLEX_API_KEY')
|
|
114
|
+
if not api_key:
|
|
115
|
+
raise ValueError("SIMPLEX_API_KEY environment variable not set")
|
|
116
|
+
|
|
117
|
+
url = JOB_URL + "/api/v1/jobs/"
|
|
118
|
+
|
|
119
|
+
try:
|
|
120
|
+
headers = {
|
|
121
|
+
'Content-Type': 'application/json',
|
|
122
|
+
'X-API-Key': api_key
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
data = {
|
|
126
|
+
'name': project_name,
|
|
127
|
+
's3_uri': s3_uri,
|
|
128
|
+
'cron_schedule': cron_schedule
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
response = requests.post(
|
|
132
|
+
url,
|
|
133
|
+
json=data,
|
|
134
|
+
headers=headers
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
# Check response
|
|
138
|
+
response.raise_for_status()
|
|
139
|
+
result = response.json()
|
|
140
|
+
print(f"Successfully created job: {result['name']}")
|
|
141
|
+
return result
|
|
142
|
+
|
|
143
|
+
except Exception as e:
|
|
144
|
+
print(f"Error creating job: {e}")
|
|
145
|
+
raise
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def create_job_immediately(project_name: str, s3_uri: str):
|
|
149
|
+
"""Create a new job via the API"""
|
|
150
|
+
# Read API key from environment
|
|
151
|
+
api_key = os.getenv('SIMPLEX_API_KEY')
|
|
152
|
+
if not api_key:
|
|
153
|
+
raise ValueError("SIMPLEX_API_KEY environment variable not set")
|
|
154
|
+
|
|
155
|
+
url = JOB_URL + "/api/v1/jobs/"
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
try:
|
|
159
|
+
headers = {
|
|
160
|
+
'Content-Type': 'application/json',
|
|
161
|
+
'X-API-Key': api_key
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
data = {
|
|
165
|
+
'name': project_name,
|
|
166
|
+
's3_uri': s3_uri,
|
|
167
|
+
'schedule_time': (datetime.utcnow() + timedelta(minutes=2)).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
response = requests.post(
|
|
171
|
+
url,
|
|
172
|
+
json=data,
|
|
173
|
+
headers=headers
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
# Check response
|
|
177
|
+
response.raise_for_status()
|
|
178
|
+
result = response.json()
|
|
179
|
+
print(f"Successfully created job: {result['name']}")
|
|
180
|
+
return result
|
|
181
|
+
|
|
182
|
+
except Exception as e:
|
|
183
|
+
print(f"Error creating job: {e}")
|
|
184
|
+
raise
|
|
185
|
+
|
|
186
|
+
def run_directory(directory: str):
|
|
187
|
+
"""Run a directory immediately"""
|
|
188
|
+
config = read_config(directory)
|
|
189
|
+
archive_path = create_archive(directory, config)
|
|
190
|
+
|
|
191
|
+
project_name = config.get('name')
|
|
192
|
+
if not project_name:
|
|
193
|
+
raise ValueError("Project name not specified in simplex.yaml")
|
|
194
|
+
|
|
195
|
+
try:
|
|
196
|
+
s3_uri = upload_files_to_s3(archive_path, project_name)
|
|
197
|
+
except Exception as e:
|
|
198
|
+
print(f"Error uploading project: {e}")
|
|
199
|
+
raise
|
|
200
|
+
|
|
201
|
+
finally:
|
|
202
|
+
os.unlink(archive_path)
|
|
203
|
+
|
|
204
|
+
try:
|
|
205
|
+
create_job_immediately(project_name, s3_uri)
|
|
206
|
+
except Exception as e:
|
|
207
|
+
print(f"Error creating job: {e}")
|
|
208
|
+
raise
|
simplex/simplex.py
CHANGED
|
@@ -1,324 +1,234 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import
|
|
4
|
-
from typing import List
|
|
5
|
-
import io
|
|
1
|
+
import os
|
|
2
|
+
import requests
|
|
3
|
+
import atexit
|
|
6
4
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
BASE_URL = "https://u3mvtbirxf.us-east-1.awsapprunner.com"
|
|
5
|
+
BASE_URL = "https://api.simplex.sh"
|
|
10
6
|
|
|
11
7
|
class Simplex:
|
|
12
|
-
def __init__(self, api_key: str
|
|
13
|
-
"""
|
|
14
|
-
Initialize Simplex instance
|
|
15
|
-
|
|
16
|
-
Args:
|
|
17
|
-
api_key (str): API key for authentication
|
|
18
|
-
browser (optional): Browser instance (for Hyperbrowser integration)
|
|
19
|
-
"""
|
|
8
|
+
def __init__(self, api_key: str):
|
|
20
9
|
self.api_key = api_key
|
|
21
|
-
self.
|
|
10
|
+
self.session_id = None
|
|
11
|
+
atexit.register(self.cleanup_session)
|
|
12
|
+
|
|
13
|
+
def cleanup_session(self):
|
|
14
|
+
if self.session_id:
|
|
15
|
+
try:
|
|
16
|
+
self.close_session(self.session_id)
|
|
17
|
+
except:
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
def close_session(self):
|
|
21
|
+
response = requests.post(
|
|
22
|
+
f"{BASE_URL}/close",
|
|
23
|
+
headers={
|
|
24
|
+
'x-api-key': self.api_key
|
|
25
|
+
},
|
|
26
|
+
data={'session_id': self.session_id}
|
|
27
|
+
)
|
|
28
|
+
self.session_id = None
|
|
29
|
+
return response.json()
|
|
30
|
+
|
|
31
|
+
def create_session(self):
|
|
32
|
+
response = requests.post(
|
|
33
|
+
f"{BASE_URL}/create-session",
|
|
34
|
+
headers={
|
|
35
|
+
'x-api-key': self.api_key
|
|
36
|
+
}
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
response_json = response.json()
|
|
40
|
+
self.session_id = response_json['session_id']
|
|
41
|
+
livestream_url = response_json['livestream_url']
|
|
42
|
+
|
|
43
|
+
print(f"\nSession created successfully!")
|
|
44
|
+
print(f"Session ID: {self.session_id}")
|
|
45
|
+
print(f"Livestream URL: {livestream_url}\n")
|
|
46
|
+
|
|
47
|
+
return self.session_id, livestream_url
|
|
48
|
+
|
|
49
|
+
def goto(self, url: str, cdp_url: str = None):
|
|
50
|
+
if not cdp_url and not self.session_id:
|
|
51
|
+
raise ValueError(f"Must call create_session before calling action goto with url='{url}'")
|
|
22
52
|
|
|
23
|
-
if
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
53
|
+
if not url.startswith('http://') and not url.startswith('https://'):
|
|
54
|
+
url = 'https://' + url
|
|
55
|
+
|
|
56
|
+
data = {'url': url}
|
|
57
|
+
|
|
58
|
+
if cdp_url:
|
|
59
|
+
data['cdp_url'] = cdp_url
|
|
27
60
|
else:
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
self.driver.set_default_navigation_timeout(0)
|
|
31
|
-
self.driver.set_default_timeout(0)
|
|
32
|
-
|
|
33
|
-
def extract_bbox(self, element_description: str, state: Image.Image | None = None, annotate: bool = True) -> List[int]:
|
|
34
|
-
"""
|
|
35
|
-
Find an element in the screenshot using the element description
|
|
36
|
-
|
|
37
|
-
Args:
|
|
38
|
-
element_description (str): Description of the element to find
|
|
39
|
-
screenshot (PIL.Image.Image): Screenshot of the page
|
|
40
|
-
|
|
41
|
-
Returns:
|
|
42
|
-
bounding_box (tuple): [x1, y1, x2, y2] bounding box of the found element
|
|
43
|
-
"""
|
|
44
|
-
print(f"[SIMPLEX] Finding element \"{element_description}\"...")
|
|
45
|
-
if state is None:
|
|
46
|
-
state = self.take_stable_screenshot()
|
|
47
|
-
|
|
48
|
-
endpoint = f"{BASE_URL}/find-element"
|
|
49
|
-
|
|
50
|
-
# Convert PIL Image to bytes
|
|
51
|
-
img_byte_arr = io.BytesIO()
|
|
52
|
-
state.save(img_byte_arr, format='PNG')
|
|
53
|
-
img_byte_arr = img_byte_arr.getvalue()
|
|
54
|
-
|
|
55
|
-
# Prepare multipart form data
|
|
56
|
-
files = {
|
|
57
|
-
'image_data': ('screenshot.png', img_byte_arr, 'image/png'),
|
|
58
|
-
'element_description': (None, element_description),
|
|
59
|
-
'api_key': (None, self.api_key)
|
|
60
|
-
}
|
|
61
|
-
print("[SIMPLEX] Sending screenshot to server...")
|
|
62
|
-
# Make the request
|
|
61
|
+
data['session_id'] = self.session_id
|
|
62
|
+
|
|
63
63
|
response = requests.post(
|
|
64
|
-
|
|
65
|
-
|
|
64
|
+
f"{BASE_URL}/goto",
|
|
65
|
+
headers={
|
|
66
|
+
'x-api-key': self.api_key
|
|
67
|
+
},
|
|
68
|
+
data=data
|
|
66
69
|
)
|
|
70
|
+
print(response.json())
|
|
71
|
+
|
|
72
|
+
def click(self, element_description: str, cdp_url: str = None):
|
|
73
|
+
if not cdp_url and not self.session_id:
|
|
74
|
+
raise ValueError(f"Must call create_session before calling action click with element_description='{element_description}'")
|
|
67
75
|
|
|
76
|
+
data = {'element_description': element_description}
|
|
68
77
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
if response.status_code == 200:
|
|
72
|
-
res = response.json()
|
|
73
|
-
bbox = [int(res['x1']), int(res['y1']), int(res['x2']), int(res['y2'])]
|
|
74
|
-
|
|
75
|
-
# Add overlay directly to the page if driver exists
|
|
76
|
-
if hasattr(self, 'driver') and annotate:
|
|
77
|
-
# Create and inject overlay element
|
|
78
|
-
self.driver.evaluate("""
|
|
79
|
-
(bbox) => {
|
|
80
|
-
// Remove any existing overlay
|
|
81
|
-
const existingOverlay = document.getElementById('simplex-bbox-overlay');
|
|
82
|
-
if (existingOverlay) {
|
|
83
|
-
existingOverlay.remove();
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// Create new overlay
|
|
87
|
-
const overlay = document.createElement('div');
|
|
88
|
-
overlay.id = 'simplex-bbox-overlay';
|
|
89
|
-
overlay.style.position = 'fixed';
|
|
90
|
-
overlay.style.border = '2px dashed rgba(0, 255, 0, 1)';
|
|
91
|
-
overlay.style.background = 'rgba(74, 144, 226, 0.1)';
|
|
92
|
-
overlay.style.animation = 'marching-ants 0.5s linear infinite';
|
|
93
|
-
overlay.style.left = bbox[0] + 'px';
|
|
94
|
-
overlay.style.top = bbox[1] + 'px';
|
|
95
|
-
overlay.style.width = (bbox[2] - bbox[0]) + 'px';
|
|
96
|
-
overlay.style.height = (bbox[3] - bbox[1]) + 'px';
|
|
97
|
-
overlay.style.pointerEvents = 'none';
|
|
98
|
-
overlay.style.zIndex = '10000';
|
|
99
|
-
overlay.style.margin = '0';
|
|
100
|
-
overlay.style.padding = '0';
|
|
101
|
-
|
|
102
|
-
// Add marching ants animation keyframes
|
|
103
|
-
if (!document.querySelector('#marching-ants-keyframes')) {
|
|
104
|
-
const style = document.createElement('style');
|
|
105
|
-
style.id = 'marching-ants-keyframes';
|
|
106
|
-
style.textContent = `
|
|
107
|
-
@keyframes marching-ants {
|
|
108
|
-
0% { border-style: dashed; }
|
|
109
|
-
50% { border-style: solid; }
|
|
110
|
-
100% { border-style: dashed; }
|
|
111
|
-
}
|
|
112
|
-
`;
|
|
113
|
-
document.head.appendChild(style);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
document.body.appendChild(overlay);
|
|
117
|
-
|
|
118
|
-
// Remove overlay after 3 second
|
|
119
|
-
setTimeout(() => {
|
|
120
|
-
overlay.remove();
|
|
121
|
-
}, 2000);
|
|
122
|
-
}
|
|
123
|
-
""", bbox)
|
|
124
|
-
self.driver.wait_for_selector('#simplex-bbox-overlay')
|
|
125
|
-
return bbox
|
|
78
|
+
if cdp_url:
|
|
79
|
+
data['cdp_url'] = cdp_url
|
|
126
80
|
else:
|
|
127
|
-
|
|
81
|
+
data['session_id'] = self.session_id
|
|
128
82
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
83
|
+
response = requests.post(
|
|
84
|
+
f"{BASE_URL}/click",
|
|
85
|
+
headers={
|
|
86
|
+
'x-api-key': self.api_key
|
|
87
|
+
},
|
|
88
|
+
data=data
|
|
89
|
+
)
|
|
90
|
+
print(response.json())
|
|
132
91
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
92
|
+
def type(self, text: str, cdp_url: str = None):
|
|
93
|
+
if not cdp_url and not self.session_id:
|
|
94
|
+
raise ValueError(f"Must call create_session before calling action type with text='{text}'")
|
|
95
|
+
|
|
96
|
+
data = {'text': text}
|
|
97
|
+
|
|
98
|
+
if cdp_url:
|
|
99
|
+
data['cdp_url'] = cdp_url
|
|
100
|
+
else:
|
|
101
|
+
data['session_id'] = self.session_id
|
|
142
102
|
|
|
143
|
-
endpoint = f"{BASE_URL}/step_to_action"
|
|
144
|
-
|
|
145
|
-
# Convert PIL Image to bytes
|
|
146
|
-
img_byte_arr = io.BytesIO()
|
|
147
|
-
state.save(img_byte_arr, format='PNG')
|
|
148
|
-
img_byte_arr = img_byte_arr.getvalue()
|
|
149
|
-
|
|
150
|
-
# Prepare form data
|
|
151
|
-
files = {
|
|
152
|
-
'image_data': ('screenshot.png', img_byte_arr, 'image/png'),
|
|
153
|
-
'step': (None, step_description),
|
|
154
|
-
'api_key': (None, self.api_key)
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
# Make the request
|
|
158
103
|
response = requests.post(
|
|
159
|
-
|
|
160
|
-
|
|
104
|
+
f"{BASE_URL}/type",
|
|
105
|
+
headers={
|
|
106
|
+
'x-api-key': self.api_key
|
|
107
|
+
},
|
|
108
|
+
data=data
|
|
161
109
|
)
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
110
|
+
print(response.json())
|
|
111
|
+
|
|
112
|
+
def press_enter(self, cdp_url: str = None):
|
|
113
|
+
if not cdp_url and not self.session_id:
|
|
114
|
+
raise ValueError("Must call create_session before calling action press_enter")
|
|
115
|
+
|
|
116
|
+
data = {}
|
|
117
|
+
|
|
118
|
+
if cdp_url:
|
|
119
|
+
data['cdp_url'] = cdp_url
|
|
170
120
|
else:
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
return []
|
|
174
|
-
|
|
175
|
-
def goto(self, url: str, new_tab: bool = False) -> None:
|
|
176
|
-
"""
|
|
177
|
-
Navigate to a URL
|
|
178
|
-
|
|
179
|
-
Args:
|
|
180
|
-
url (str): URL to navigate to
|
|
181
|
-
new_tab (bool): Whether to open a new tab or use the current tab
|
|
182
|
-
"""
|
|
183
|
-
if new_tab:
|
|
184
|
-
self.driver = self.browser.new_page()
|
|
185
|
-
self.driver.wait_for_load_state()
|
|
186
|
-
print(f"[SIMPLEX] Navigating to URL {url}...")
|
|
187
|
-
self.driver.goto(url)
|
|
188
|
-
|
|
189
|
-
def click(self, element_description: str, annotate: bool = True) -> None:
|
|
190
|
-
"""
|
|
191
|
-
Click on an element
|
|
192
|
-
"""
|
|
193
|
-
self.execute_action(["CLICK", element_description], annotate=annotate)
|
|
194
|
-
|
|
195
|
-
def type(self, text: str) -> None:
|
|
196
|
-
"""
|
|
197
|
-
Type text into an element
|
|
198
|
-
"""
|
|
199
|
-
self.execute_action(["TYPE", text])
|
|
200
|
-
|
|
201
|
-
def press_enter(self, annotate: bool = True) -> None:
|
|
202
|
-
"""
|
|
203
|
-
Press enter
|
|
204
|
-
"""
|
|
205
|
-
self.execute_action(["ENTER", ""], annotate=annotate)
|
|
206
|
-
|
|
207
|
-
def scroll(self, scroll_amount: int, annotate: bool = True) -> None:
|
|
208
|
-
"""
|
|
209
|
-
Scroll the page
|
|
210
|
-
"""
|
|
211
|
-
self.execute_action(["SCROLL", scroll_amount], annotate=annotate)
|
|
212
|
-
|
|
213
|
-
def wait(self, wait_time: int, annotate: bool = True) -> None:
|
|
214
|
-
"""
|
|
215
|
-
Wait for a given amount of time
|
|
216
|
-
"""
|
|
217
|
-
self.execute_action(["WAIT", wait_time], annotate=annotate)
|
|
218
|
-
|
|
219
|
-
def execute_action(self, action: List[List[str]], state: Image.Image | None = None, annotate: bool = True) -> None:
|
|
220
|
-
"""
|
|
221
|
-
Execute an action with playwright driver
|
|
222
|
-
|
|
223
|
-
Args:
|
|
224
|
-
action (List[List[str]]): List of actions to perform
|
|
225
|
-
"""
|
|
226
|
-
action_type, description = action
|
|
227
|
-
try:
|
|
228
|
-
if action_type == "CLICK":
|
|
229
|
-
bbox = self.extract_bbox(description, state, annotate=annotate)
|
|
230
|
-
center_x, center_y = center_bbox(bbox)
|
|
231
|
-
self.driver.mouse.click(center_x, center_y)
|
|
232
|
-
print(f"[SIMPLEX] Clicked on element \"{description}\"")
|
|
233
|
-
elif action_type == "TYPE":
|
|
234
|
-
self.driver.keyboard.type(description)
|
|
235
|
-
print(f"[SIMPLEX] Typed \"{description}\"")
|
|
236
|
-
|
|
237
|
-
elif action_type == "ENTER":
|
|
238
|
-
self.driver.keyboard.press("Enter")
|
|
239
|
-
print(f"[SIMPLEX] Pressed enter")
|
|
240
|
-
|
|
241
|
-
elif action_type == "SCROLL":
|
|
242
|
-
self.driver.mouse.wheel(0, int(description))
|
|
243
|
-
print(f"[SIMPLEX] Scrolled {description} pixels")
|
|
244
|
-
|
|
245
|
-
elif action_type == "WAIT":
|
|
246
|
-
self.driver.wait_for_timeout(int(description))
|
|
247
|
-
print(f"[SIMPLEX] Waited {description} seconds")
|
|
248
|
-
|
|
249
|
-
except Exception as e:
|
|
250
|
-
print(f"[SIMPLEX] Error executing action: {e}")
|
|
251
|
-
return None
|
|
252
|
-
|
|
253
|
-
def do(self, step_description: str, annotate: bool = True) -> None:
|
|
254
|
-
"""
|
|
255
|
-
Execute a step description
|
|
256
|
-
"""
|
|
257
|
-
state = self.take_stable_screenshot()
|
|
258
|
-
actions = self.step_to_action(step_description, state)
|
|
259
|
-
for action in actions:
|
|
260
|
-
self.execute_action(action, annotate=annotate)
|
|
261
|
-
|
|
262
|
-
def extract_text(self, element_description: str, state: Image.Image | None = None) -> List[str] | None:
|
|
263
|
-
"""
|
|
264
|
-
Extract an element text from the page
|
|
265
|
-
"""
|
|
266
|
-
print(f"[SIMPLEX] Finding element \"{element_description}\"...")
|
|
267
|
-
if state is None:
|
|
268
|
-
state = self.take_stable_screenshot()
|
|
269
|
-
|
|
270
|
-
endpoint = f"{BASE_URL}/extract-text"
|
|
271
|
-
|
|
272
|
-
# Convert PIL Image to bytes
|
|
273
|
-
img_byte_arr = io.BytesIO()
|
|
274
|
-
state.save(img_byte_arr, format='PNG')
|
|
275
|
-
img_byte_arr = img_byte_arr.getvalue()
|
|
276
|
-
|
|
277
|
-
# Prepare multipart form data
|
|
278
|
-
files = {
|
|
279
|
-
'image_data': ('screenshot.png', img_byte_arr, 'image/png'),
|
|
280
|
-
'element_description': (None, element_description),
|
|
281
|
-
'api_key': (None, self.api_key)
|
|
282
|
-
}
|
|
283
|
-
print("[SIMPLEX] Sending screenshot to server...")
|
|
284
|
-
|
|
285
|
-
# Make the request
|
|
121
|
+
data['session_id'] = self.session_id
|
|
122
|
+
|
|
286
123
|
response = requests.post(
|
|
287
|
-
|
|
288
|
-
|
|
124
|
+
f"{BASE_URL}/press-enter",
|
|
125
|
+
headers={
|
|
126
|
+
'x-api-key': self.api_key
|
|
127
|
+
},
|
|
128
|
+
data=data
|
|
289
129
|
)
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
130
|
+
print(response.json())
|
|
131
|
+
|
|
132
|
+
def extract_bbox(self, element_description: str, cdp_url: str = None):
|
|
133
|
+
if not cdp_url and not self.session_id:
|
|
134
|
+
raise ValueError(f"Must call create_session before calling action extract_bbox with element_description='{element_description}'")
|
|
135
|
+
|
|
136
|
+
data = {'element_description': element_description}
|
|
137
|
+
|
|
138
|
+
if cdp_url:
|
|
139
|
+
data['cdp_url'] = cdp_url
|
|
296
140
|
else:
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
bbox = self.extract_bbox(element_description, state)
|
|
308
|
-
cropped_state = state.crop((bbox[0], bbox[1], bbox[2], bbox[3]))
|
|
309
|
-
return cropped_state
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
def take_stable_screenshot(self) -> Image.Image:
|
|
314
|
-
"""
|
|
315
|
-
Take a screenshot after ensuring the page is in a stable state.
|
|
141
|
+
data['session_id'] = self.session_id
|
|
142
|
+
|
|
143
|
+
response = requests.get(
|
|
144
|
+
f"{BASE_URL}/extract-bbox",
|
|
145
|
+
headers={
|
|
146
|
+
'x-api-key': self.api_key
|
|
147
|
+
},
|
|
148
|
+
params=data
|
|
149
|
+
)
|
|
316
150
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
151
|
+
return response.json()
|
|
152
|
+
|
|
153
|
+
def extract_text(self, element_description: str, cdp_url: str = None, use_vision: bool = False):
|
|
154
|
+
if not cdp_url and not self.session_id:
|
|
155
|
+
raise ValueError(f"Must call create_session before calling action extract_text with element_description='{element_description}'")
|
|
156
|
+
|
|
157
|
+
data = {'element_description': element_description}
|
|
158
|
+
|
|
159
|
+
if cdp_url:
|
|
160
|
+
data['cdp_url'] = cdp_url
|
|
161
|
+
else:
|
|
162
|
+
data['session_id'] = self.session_id
|
|
163
|
+
|
|
164
|
+
if use_vision:
|
|
165
|
+
data['use_vision'] = use_vision
|
|
166
|
+
|
|
167
|
+
response = requests.get(
|
|
168
|
+
f"{BASE_URL}/extract-text",
|
|
169
|
+
headers={
|
|
170
|
+
'x-api-key': self.api_key
|
|
171
|
+
},
|
|
172
|
+
params=data
|
|
173
|
+
)
|
|
174
|
+
return response.json()
|
|
175
|
+
|
|
176
|
+
def extract_image(self, element_description: str, cdp_url: str = None):
|
|
177
|
+
if not cdp_url and not self.session_id:
|
|
178
|
+
raise ValueError(f"Must call create_session before calling action extract_image with element_description='{element_description}'")
|
|
179
|
+
|
|
180
|
+
data = {'element_description': element_description}
|
|
181
|
+
|
|
182
|
+
if cdp_url:
|
|
183
|
+
data['cdp_url'] = cdp_url
|
|
184
|
+
else:
|
|
185
|
+
data['session_id'] = self.session_id
|
|
186
|
+
|
|
187
|
+
response = requests.get(
|
|
188
|
+
f"{BASE_URL}/extract-image",
|
|
189
|
+
headers={
|
|
190
|
+
'x-api-key': self.api_key
|
|
191
|
+
},
|
|
192
|
+
params=data
|
|
193
|
+
)
|
|
194
|
+
return response.json()
|
|
195
|
+
|
|
196
|
+
def scroll(self, pixels: int, cdp_url: str = None):
|
|
197
|
+
if not cdp_url and not self.session_id:
|
|
198
|
+
raise ValueError(f"Must call create_session before calling action scroll with pixels={pixels}")
|
|
199
|
+
|
|
200
|
+
data = {'pixels': pixels}
|
|
201
|
+
|
|
202
|
+
if cdp_url:
|
|
203
|
+
data['cdp_url'] = cdp_url
|
|
204
|
+
else:
|
|
205
|
+
data['session_id'] = self.session_id
|
|
206
|
+
|
|
207
|
+
response = requests.post(
|
|
208
|
+
f"{BASE_URL}/scroll",
|
|
209
|
+
headers={
|
|
210
|
+
'x-api-key': self.api_key
|
|
211
|
+
},
|
|
212
|
+
data=data
|
|
213
|
+
)
|
|
214
|
+
return response.json()
|
|
215
|
+
|
|
216
|
+
def wait(self, milliseconds: int, cdp_url: str = None):
|
|
217
|
+
if not cdp_url and not self.session_id:
|
|
218
|
+
raise ValueError(f"Must call create_session before calling action wait with milliseconds={milliseconds}")
|
|
219
|
+
|
|
220
|
+
data = {'milliseconds': milliseconds}
|
|
221
|
+
|
|
222
|
+
if cdp_url:
|
|
223
|
+
data['cdp_url'] = cdp_url
|
|
224
|
+
else:
|
|
225
|
+
data['session_id'] = self.session_id
|
|
226
|
+
|
|
227
|
+
response = requests.post(
|
|
228
|
+
f"{BASE_URL}/wait",
|
|
229
|
+
headers={
|
|
230
|
+
'x-api-key': self.api_key
|
|
231
|
+
},
|
|
232
|
+
data=data
|
|
233
|
+
)
|
|
234
|
+
return response.json()
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
Metadata-Version: 2.2
|
|
2
2
|
Name: simplex
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.5
|
|
4
4
|
Summary: Official Python SDK for Simplex API
|
|
5
|
-
Home-page: https://
|
|
5
|
+
Home-page: https://simplex.sh
|
|
6
6
|
Author: Simplex Labs, Inc.
|
|
7
7
|
Author-email: founders@simplex.sh
|
|
8
8
|
Classifier: Programming Language :: Python :: 3
|
|
@@ -10,7 +10,7 @@ Classifier: License :: OSI Approved :: MIT License
|
|
|
10
10
|
Classifier: Operating System :: OS Independent
|
|
11
11
|
Classifier: Development Status :: 4 - Beta
|
|
12
12
|
Classifier: Intended Audience :: Developers
|
|
13
|
-
Requires-Python: >=3.
|
|
13
|
+
Requires-Python: >=3.9
|
|
14
14
|
Description-Content-Type: text/markdown
|
|
15
15
|
License-File: LICENSE
|
|
16
16
|
Requires-Dist: openai>=1.0.0
|
|
@@ -21,6 +21,14 @@ Requires-Dist: rich>=13.0.0
|
|
|
21
21
|
Requires-Dist: prompt_toolkit>=3.0.0
|
|
22
22
|
Requires-Dist: playwright>=1.0.0
|
|
23
23
|
Requires-Dist: Pillow>=9.0.0
|
|
24
|
+
Requires-Dist: PyYAML>=6.0.1
|
|
25
|
+
Requires-Dist: boto3>=1.28.0
|
|
26
|
+
Requires-Dist: requests>=2.31.0
|
|
27
|
+
Requires-Dist: MainContentExtractor
|
|
28
|
+
Requires-Dist: langchain_core
|
|
29
|
+
Requires-Dist: langchain_community
|
|
30
|
+
Requires-Dist: langchain_openai
|
|
31
|
+
Requires-Dist: langchain_anthropic
|
|
24
32
|
Dynamic: author
|
|
25
33
|
Dynamic: author-email
|
|
26
34
|
Dynamic: classifier
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
simplex/__init__.py,sha256=1mbM4XUk0FNW161WOkM4ayC1s_QSsaBEls6PZ0iBScY,74
|
|
2
|
+
simplex/cli.py,sha256=PkHt3sBKYfu0Smil4aCMoZUD-JbPw8xsBewpFcBYDKM,675
|
|
3
|
+
simplex/simplex.py,sha256=BsAp0UMjgL2MKjdT8akEjDIBajaaf_VCWyvIiDn4i4g,6990
|
|
4
|
+
simplex/deploy/__init__.py,sha256=_JQ81F_Nu7hSAfMA691gzs6a4-8oZ-buJ9h3Au12BKw,96
|
|
5
|
+
simplex/deploy/push.py,sha256=hRAbtFZaECKnBljaOLQ5nzJ6hk7tZgc1c7QdgxKQFoY,6123
|
|
6
|
+
simplex-1.2.5.dist-info/LICENSE,sha256=Xh0SJjYZfNI71pCNMB40aKlBLLuOB0blx5xkTtufFNQ,1075
|
|
7
|
+
simplex-1.2.5.dist-info/METADATA,sha256=aTiiF2h8Tgs1g20UWYBuEF3jGSnvzjRYnEhaE2GCCRw,1349
|
|
8
|
+
simplex-1.2.5.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
|
|
9
|
+
simplex-1.2.5.dist-info/entry_points.txt,sha256=3veL2w3c5vxb3dm8I_M8Fs-370n1ZnvD8uu1nSsL7z8,45
|
|
10
|
+
simplex-1.2.5.dist-info/top_level.txt,sha256=cbMH1bYpN0A3gP-ecibPRHasHoqB-01T_2BUFS8p0CE,8
|
|
11
|
+
simplex-1.2.5.dist-info/RECORD,,
|
simplex/constants.py
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
BASE_URL = "https://u3mvtbirxf.us-east-1.awsapprunner.com"
|
simplex/utils.py
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
from typing import List
|
|
2
|
-
from PIL import Image
|
|
3
|
-
import io
|
|
4
|
-
def center_bbox(bbox: List[int]) -> List[int]:
|
|
5
|
-
"""
|
|
6
|
-
Calculate the center coordinates of a bounding box
|
|
7
|
-
"""
|
|
8
|
-
return [(bbox[0] + bbox[2]) // 2, (bbox[1] + bbox[3]) // 2]
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
def screenshot_to_image(screenshot: bytes) -> Image:
|
|
12
|
-
return Image.open(io.BytesIO(screenshot))
|
simplex-1.2.4.dist-info/RECORD
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
simplex/__init__.py,sha256=1mbM4XUk0FNW161WOkM4ayC1s_QSsaBEls6PZ0iBScY,74
|
|
2
|
-
simplex/constants.py,sha256=nIXF2oVNNNknXweXAlmE-KBM9QjJtYw9osXVYjvloN0,59
|
|
3
|
-
simplex/simplex.py,sha256=m5vQxhVCyeU-h-cOVmpaAMoTATPJKdecfp1Fv51uaL0,12137
|
|
4
|
-
simplex/utils.py,sha256=UrD4Ena3yk0POmxxyiqMszzPbTscTCJpMP4xZFDAuOc,339
|
|
5
|
-
simplex-1.2.4.dist-info/LICENSE,sha256=Xh0SJjYZfNI71pCNMB40aKlBLLuOB0blx5xkTtufFNQ,1075
|
|
6
|
-
simplex-1.2.4.dist-info/METADATA,sha256=YyDn-TaMU_3JQO9ZWFbRA214_Lak1zKmA8beIHl0zPI,1114
|
|
7
|
-
simplex-1.2.4.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
|
|
8
|
-
simplex-1.2.4.dist-info/entry_points.txt,sha256=3veL2w3c5vxb3dm8I_M8Fs-370n1ZnvD8uu1nSsL7z8,45
|
|
9
|
-
simplex-1.2.4.dist-info/top_level.txt,sha256=cbMH1bYpN0A3gP-ecibPRHasHoqB-01T_2BUFS8p0CE,8
|
|
10
|
-
simplex-1.2.4.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|