parsagon 0.10.16__py3-none-any.whl → 0.10.20__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.
parsagon/executor.py CHANGED
@@ -1,3 +1,4 @@
1
+ from collections import defaultdict
1
2
  import copy
2
3
  import json
3
4
  import logging
@@ -9,6 +10,8 @@ from urllib.parse import urljoin
9
10
  import lxml.html
10
11
  from pyvirtualdisplay import Display
11
12
  import undetected_chromedriver as uc
13
+ from selenium import webdriver
14
+ from selenium.webdriver.chrome.service import Service as ChromeService
12
15
  from selenium.webdriver.chrome.options import Options
13
16
  from selenium.webdriver.common.action_chains import ActionChains
14
17
  from selenium.webdriver.common.by import By
@@ -59,15 +62,20 @@ class Executor:
59
62
  Executes code produced by GPT with the proper context. Records custom_function usage along the way.
60
63
  """
61
64
 
62
- def __init__(self, headless=False, infer=False):
65
+ def __init__(self, headless=False, infer=False, use_uc=False):
63
66
  self.headless = headless
64
67
  if self.headless:
65
68
  self.display = Display(visible=False, size=(1280, 1050)).start()
66
- chrome_options = uc.ChromeOptions()
67
- chrome_options.add_argument("--start-maximized")
68
- driver_exec_path = ChromeDriverManager().install()
69
- self.driver = uc.Chrome(driver_executable_path=driver_exec_path, options=chrome_options)
70
- self.max_elem_id = 0
69
+ driver_executable_path = ChromeDriverManager().install()
70
+ if use_uc:
71
+ chrome_options = uc.ChromeOptions()
72
+ chrome_options.add_argument("--start-maximized")
73
+ self.driver = uc.Chrome(driver_executable_path=driver_executable_path, options=chrome_options)
74
+ else:
75
+ chrome_options = webdriver.ChromeOptions()
76
+ chrome_options.add_argument("--start-maximized")
77
+ self.driver = webdriver.Chrome(service=ChromeService(driver_executable_path), options=chrome_options)
78
+ self.max_elem_ids = defaultdict(int)
71
79
  self.execution_context = {
72
80
  "custom_assert": self.custom_assert,
73
81
  "goto": self.goto,
@@ -128,13 +136,12 @@ class Executor:
128
136
  Adds node IDs to elements on the current page that don't already have IDs.
129
137
  """
130
138
  logger.debug(" Marking HTML...")
131
- self.max_elem_id = self.driver.execute_script(
132
- "let elemIdx = 0; for (const node of document.all) { elemIdx = Math.max(elemIdx, parseInt(node.getAttribute('data-psgn-id') ?? 0)); } return elemIdx"
133
- )
134
- self.max_elem_id = self.driver.execute_script(
135
- f"let elemIdx = {self.max_elem_id}; "
139
+ max_elem_id = self.max_elem_ids[self.driver.current_window_handle]
140
+ max_elem_id = self.driver.execute_script(
141
+ f"let elemIdx = {max_elem_id}; "
136
142
  + "for (const node of document.all) { if (node.hasAttribute('data-psgn-id')) { continue; } node.setAttribute('data-psgn-id', elemIdx); elemIdx++; } return elemIdx;"
137
143
  )
144
+ self.max_elem_ids[self.driver.current_window_handle] = max_elem_id
138
145
  self.driver.execute_script(
139
146
  "for (const image of document.images) { image.setAttribute('data-psgn-width', image.parentElement.offsetWidth ?? -1); image.setAttribute('data-psgn-height', image.parentElement.offsetHeight ?? -1); }"
140
147
  )
@@ -187,7 +194,8 @@ class Executor:
187
194
  elem.text = ""
188
195
 
189
196
  # Remove invisible elements
190
- for elem_id in range(self.max_elem_id):
197
+ max_elem_id = self.max_elem_ids[self.driver.current_window_handle]
198
+ for elem_id in range(max_elem_id):
191
199
  try:
192
200
  lxml_elem = root.xpath(f'//*[@data-psgn-id="{elem_id}"]')[0]
193
201
  selenium_elem = driver.find_elements(By.XPATH, f'//*[@data-psgn-id="{elem_id}"]')[0]
@@ -212,6 +220,10 @@ class Executor:
212
220
  while user_input != "N/A" and not selected_node_ids and not user_input.startswith("XPATH:") and not user_input.startswith("CSS:"):
213
221
  user_input = input('Please click an element or type "N/A": ')
214
222
  selected_node_ids = self.get_selected_node_ids()
223
+ self.highlights_cleanup()
224
+ if user_input == "N/A":
225
+ return None, None, None, None
226
+
215
227
  css_selector = None
216
228
  xpath_selector = None
217
229
  if user_input.startswith("CSS:"):
@@ -221,12 +233,8 @@ class Executor:
221
233
  xpath_selector = user_input[6:].strip()
222
234
  selected_node_ids = self.get_selected_node_ids(xpath_selector=xpath_selector)
223
235
 
224
- self.highlights_cleanup()
225
- if user_input == "N/A":
226
- return None, None, None, None
227
- else:
228
- elem_id = selected_node_ids[0]
229
- elem = self._id_to_elem(elem_id)
236
+ elem_id = selected_node_ids[0] if selected_node_ids else None
237
+ elem = self._id_to_elem(elem_id) if selected_node_ids else None
230
238
  return elem, elem_id, css_selector, xpath_selector
231
239
 
232
240
  def get_elem_by_description(self, description, elem_type):
@@ -552,7 +560,10 @@ class Executor:
552
560
  finally:
553
561
  self.driver.quit()
554
562
  for proc in psutil.process_iter():
555
- if proc.name() == "chromedriver":
556
- proc.kill()
563
+ try:
564
+ if proc.name() == "chromedriver":
565
+ proc.kill()
566
+ except psutil.NoSuchProcess:
567
+ continue
557
568
  if self.headless:
558
569
  self.display.stop()
parsagon/highlights.js CHANGED
@@ -128,14 +128,24 @@ function hasValidData(element, dataType) {
128
128
  return true;
129
129
  }
130
130
 
131
- function makeVisible(element) {
132
- const rects = element.getClientRects();
133
- if (rects.length) {
134
- const rect = rects[0];
135
- if (rect.width * rect.height === 0) {
136
- element.style.display = 'block';
131
+ function makeVisible(elements) {
132
+
133
+ let elemsToDisplayBlock = [];
134
+ for (let element of elements) {
135
+ const rects = element.getClientRects();
136
+ if (rects.length) {
137
+ const rect = rects[0];
138
+ if (rect.width * rect.height === 0) {
139
+ elemsToDisplayBlock.push(element);
140
+ }
137
141
  }
138
142
  }
143
+
144
+ requestAnimationFrame(() => {
145
+ for (let elem of elemsToDisplayBlock) {
146
+ elem.style.display = 'block';
147
+ }
148
+ })
139
149
  }
140
150
 
141
151
  const DUMMY_FRAGMENT = document.createDocumentFragment()
@@ -241,28 +251,45 @@ function removeTargetStoredCSS(element) {
241
251
  element.style.removeProperty('position');
242
252
  element.classList.remove(TARGET_STORED_CLASSNAME);
243
253
  if (element.classList.contains(AUTOCOMPLETE_CLASSNAME)) {
244
- addAutocompleteCSS(element);
254
+ addAutocompleteCSS([element]);
245
255
  } else if (element.classList.contains(MOUSE_VISITED_CLASSNAME)) {
246
256
  addMouseVisitedCSS(element);
247
257
  }
248
258
  };
249
- function addAutocompleteCSS(element) {
250
- if (!element.classList.contains(TARGET_STORED_CLASSNAME)) {
251
- element.style.setProperty(
252
- 'outline',
253
- '3px solid rgb(255, 177, 255)',
254
- 'important'
255
- );
256
- element.style.setProperty('outline-offset', '-3px', 'important');
257
- if (
258
- window.getComputedStyle(element)
259
- .position === 'static'
260
- ) {
261
- element.style.setProperty('position', 'relative', 'important');
259
+ function addAutocompleteCSS(elements) {
260
+ let setOutlineElements = [];
261
+ let makeStaticElements = [];
262
+ for (const element of elements) {
263
+ if (!element.classList.contains(TARGET_STORED_CLASSNAME)) {
264
+ setOutlineElements.push(element);
265
+ if (
266
+ window.getComputedStyle(element)
267
+ .position === 'static'
268
+ ) {
269
+ makeStaticElements.push(element);
270
+ }
262
271
  }
263
272
  }
264
- element.classList.add(AUTOCOMPLETE_CLASSNAME);
273
+
274
+ // Perform batch writes
275
+ requestAnimationFrame(() => {
276
+ for (const element of setOutlineElements) {
277
+ element.style.setProperty(
278
+ 'outline',
279
+ '3px solid rgb(255, 177, 255)',
280
+ 'important'
281
+ );
282
+ element.style.setProperty('outline-offset', '-3px', 'important');
283
+ }
284
+ for (const element of makeStaticElements) {
285
+ element.style.setProperty('position', 'relative', 'important');
286
+ }
287
+ for (const element of elements) {
288
+ element.classList.add(AUTOCOMPLETE_CLASSNAME);
289
+ }
290
+ });
265
291
  };
292
+
266
293
  function removeAutocompleteCSS(element) {
267
294
  element.style.removeProperty('outline');
268
295
  element.style.removeProperty('outline-offset');
@@ -300,13 +327,13 @@ function addAutocompletes() {
300
327
  document.querySelectorAll(
301
328
  cssSelector
302
329
  );
303
- for (const elem of similarElems) {
304
- if (elem.classList.contains(TARGET_STORED_CLASSNAME)) {
305
- continue;
306
- }
307
- makeVisible(elem);
308
- addAutocompleteCSS(elem);
309
- }
330
+
331
+ // Set to similarElems where elem.classList.contains(TARGET_STORED_CLASSNAME) === false
332
+ const elemsToMakeVisibleAndAddAutocompleteCSS = Array.from(similarElems).filter((elem) => {
333
+ return !elem.classList.contains(TARGET_STORED_CLASSNAME);
334
+ });
335
+ makeVisible(elemsToMakeVisibleAndAddAutocompleteCSS);
336
+ addAutocompleteCSS(elemsToMakeVisibleAndAddAutocompleteCSS);
310
337
  }
311
338
  };
312
339
 
@@ -496,7 +523,7 @@ function handleMouseMove(e) {
496
523
  return;
497
524
  }
498
525
 
499
- makeVisible(srcElement);
526
+ makeVisible([srcElement]);
500
527
 
501
528
  addMouseVisitedCSS(srcElement);
502
529
  window.prevDOM = srcElement;
parsagon/main.py CHANGED
@@ -325,8 +325,11 @@ def run(program_name, variables={}, headless=False, remote=False, verbose=False)
325
325
  if "display" in globals_locals:
326
326
  globals_locals["display"].stop()
327
327
  for proc in psutil.process_iter():
328
- if proc.name() == "chromedriver":
329
- proc.kill()
328
+ try:
329
+ if proc.name() == "chromedriver":
330
+ proc.kill()
331
+ except psutil.NoSuchProcess:
332
+ continue
330
333
  logger.info("Done.")
331
334
  return globals_locals["output"]
332
335
 
@@ -359,6 +362,8 @@ def batch_runs(batch_name, program_name, runs=[], headless=False, ignore_errors=
359
362
  pbar.set_description(f"An error occurred: {e} - Waiting 60s before retrying (Attempt {j+2}/3)")
360
363
  time.sleep(60)
361
364
  pbar.set_description(default_desc)
365
+ error = None
366
+ error_variables = None
362
367
  continue
363
368
  else:
364
369
  if ignore_errors:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: parsagon
3
- Version: 0.10.16
3
+ Version: 0.10.20
4
4
  Summary: Allows you to create browser automations with natural language
5
5
  Author-email: Sandy Suh <sandy@parsagon.io>
6
6
  Project-URL: Homepage, https://parsagon.io
@@ -17,8 +17,8 @@ Requires-Dist: tqdm ==4.66.1
17
17
  Requires-Dist: PyVirtualDisplay ==3.0
18
18
  Requires-Dist: selenium-wire ==5.1.0
19
19
  Requires-Dist: cssselect ==1.1.0
20
- Requires-Dist: undetected-chromedriver ==3.5.2
21
- Requires-Dist: webdriver-manager ==4.0.0
20
+ Requires-Dist: undetected-chromedriver ==3.5.3
21
+ Requires-Dist: webdriver-manager ==4.0.1
22
22
  Requires-Dist: jsonpath-ng ==1.5.3
23
23
  Requires-Dist: simplejson ==3.19.1
24
24
  Provides-Extra: dev
@@ -75,6 +75,9 @@ parsagon.create('Go to https://www.google.com/. Type "the meaning of life" into
75
75
  # Run a program
76
76
  parsagon.run("My program")
77
77
 
78
+ # Run a program multiple times
79
+ parsagon.batch_runs("My batch name", "My program", runs=[{"variable_name": "value1"}, {"variable_name": "value2"}, ...])
80
+
78
81
  # List your programs
79
82
  parsagon.detail()
80
83
 
@@ -2,9 +2,9 @@ parsagon/__init__.py,sha256=n4-wiFVVuyW_KOJeNiycggAg9BTa5bbBIVpD_DkdOO4,125
2
2
  parsagon/api.py,sha256=eJULOzTyWqA4Mio7tH9PszwTrZyxRBI0uO9t1h3R7rw,6634
3
3
  parsagon/custom_function.py,sha256=oEj28qItaHUnsvLIHD7kg5QL3J3aO6rW6xKKP-H-Drs,770
4
4
  parsagon/exceptions.py,sha256=NYpFaSLZplBTv9fov_1LKPzDPIqb7Ffe7IunnjntxvA,819
5
- parsagon/executor.py,sha256=BRfmdYrr-HQUwlXRDePpclkKqjR9pc48MgtJhPpEBuM,22209
6
- parsagon/highlights.js,sha256=bm8OcDMgSMUJw_b_pJ-qN0W2fS-JnGZR14LQrPnIf1U,15690
7
- parsagon/main.py,sha256=jcXbKY3h3ZSvQOX7SRDC_fl3VqDu4N_Zbi8jdn2hQN0,14561
5
+ parsagon/executor.py,sha256=e_e9p5eLvf7wYHk1BNJf0j_qt0H17BfivPb8CoOKMHE,22791
6
+ parsagon/highlights.js,sha256=2UDfUApblU9xtGgTLCq4X7rHRV0wcqDSSFZPmJS6fJg,16643
7
+ parsagon/main.py,sha256=yQbIzqJ7Ea6XZJ3Eolx2lTh7Di87qPJvDI0WRNeoX14,14736
8
8
  parsagon/settings.py,sha256=s5_MsDMFM5tB8U8tfHaFnKibCoEqPnAu8b_ueg07Ftw,2947
9
9
  parsagon/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
10
  parsagon/tests/api_mocks.py,sha256=M8xhiyPa1dI8Vx-odDk7ETopfFAfcjfAf-ApmSqgvfw,3127
@@ -13,8 +13,8 @@ parsagon/tests/conftest.py,sha256=KMlHohc0QT77HzumraIojzKeqroyxarnaT6naJDNvEc,42
13
13
  parsagon/tests/test_executor.py,sha256=n3cmh84r74siSeJqUeAIwjjnNzDVPEdxcvYAeJ4hNX8,645
14
14
  parsagon/tests/test_invalid_args.py,sha256=kOjMpbZvviR1CwvXReteZMxBvuhq_rOv5Tm1muBSzNk,676
15
15
  parsagon/tests/test_pipeline_operations.py,sha256=TpBKCuRA8LHYWx3PD_k9mYCSsA_9SZjrOX-rS4mE8XE,1089
16
- parsagon-0.10.16.dist-info/METADATA,sha256=RuxfEuUUmyQUOTPjmckDOTlpI9phRtVDc_lIxlPvidU,2257
17
- parsagon-0.10.16.dist-info/WHEEL,sha256=yQN5g4mg4AybRjkgi-9yy4iQEFibGQmlz78Pik5Or-A,92
18
- parsagon-0.10.16.dist-info/entry_points.txt,sha256=I1UlPUb4oY2k9idkI8kvdkEcrjKGRSOl5pMbA6uu6kw,48
19
- parsagon-0.10.16.dist-info/top_level.txt,sha256=ih5uYQzW4qjhRKppys-WiHLIbXVZ99YdqDcfAtlcQwk,9
20
- parsagon-0.10.16.dist-info/RECORD,,
16
+ parsagon-0.10.20.dist-info/METADATA,sha256=vA0gDtSRv8FeLScDZJHTNQjSgBw2MaAetLcrDJ9bIaw,2410
17
+ parsagon-0.10.20.dist-info/WHEEL,sha256=yQN5g4mg4AybRjkgi-9yy4iQEFibGQmlz78Pik5Or-A,92
18
+ parsagon-0.10.20.dist-info/entry_points.txt,sha256=I1UlPUb4oY2k9idkI8kvdkEcrjKGRSOl5pMbA6uu6kw,48
19
+ parsagon-0.10.20.dist-info/top_level.txt,sha256=ih5uYQzW4qjhRKppys-WiHLIbXVZ99YdqDcfAtlcQwk,9
20
+ parsagon-0.10.20.dist-info/RECORD,,