parsagon 0.10.21__py3-none-any.whl → 0.10.22__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/api.py CHANGED
@@ -192,11 +192,19 @@ def get_pipeline_code(pipeline_name, variables, headless):
192
192
  )
193
193
 
194
194
 
195
- def create_pipeline_run(pipeline_id, variables):
195
+ def create_pipeline_run(pipeline_id, variables, is_local):
196
196
  return _api_call(
197
197
  httpx.post,
198
198
  f"/pipelines/{pipeline_id}/runs/",
199
- json={"variables": variables},
199
+ json={"variables": variables, "is_local": is_local},
200
+ )
201
+
202
+
203
+ def update_pipeline_run(run_id, data):
204
+ return _api_call(
205
+ httpx.patch,
206
+ f"/pipelines/runs/{run_id}/",
207
+ json=data,
200
208
  )
201
209
 
202
210
 
parsagon/exceptions.py CHANGED
@@ -24,3 +24,8 @@ class ProgramNotFoundException(ParsagonException):
24
24
 
25
25
  def to_string(self, verbose):
26
26
  return f"A program with name {self.program} does not exist."
27
+
28
+
29
+ class RunFailedException(ParsagonException):
30
+ """Raised when a run fails."""
31
+ pass
parsagon/main.py CHANGED
@@ -1,9 +1,11 @@
1
1
  import argparse
2
+ import datetime
2
3
  import json
3
4
  import logging
4
5
  import logging.config
5
6
  import psutil
6
7
  import time
8
+ import traceback
7
9
 
8
10
  from halo import Halo
9
11
  from tqdm import tqdm
@@ -15,14 +17,14 @@ from parsagon.api import (
15
17
  create_custom_function,
16
18
  add_examples_to_custom_function,
17
19
  create_pipeline_run,
20
+ update_pipeline_run,
18
21
  get_pipeline,
19
22
  get_pipelines,
20
23
  get_pipeline_code,
21
24
  get_run,
22
25
  poll_data,
23
- APIException,
24
26
  )
25
- from parsagon.exceptions import ParsagonException
27
+ from parsagon.exceptions import ParsagonException, APIException, RunFailedException
26
28
  from parsagon.executor import Executor, custom_functions_to_descriptions
27
29
  from parsagon.secrets import extract_secrets
28
30
  from parsagon.settings import get_api_key, get_settings, clear_settings, save_setting, get_logging_config
@@ -140,6 +142,11 @@ def get_args():
140
142
  action="store_true",
141
143
  help="run the program in the cloud",
142
144
  )
145
+ parser_run.add_argument(
146
+ "--output_log",
147
+ action="store_true",
148
+ help="output log data from the run",
149
+ )
143
150
  parser_run.set_defaults(func=run)
144
151
 
145
152
  # Delete
@@ -186,6 +193,8 @@ def main():
186
193
 
187
194
 
188
195
  def create(task=None, program_name=None, headless=False, infer=False, verbose=False):
196
+ configure_logging(verbose)
197
+
189
198
  if task:
190
199
  logger.info("Launched with task description:\n%s", task)
191
200
  else:
@@ -246,6 +255,8 @@ def create(task=None, program_name=None, headless=False, infer=False, verbose=Fa
246
255
 
247
256
 
248
257
  def update(program_name, variables={}, headless=False, infer=False, replace=False, verbose=False):
258
+ configure_logging(verbose)
259
+
249
260
  pipeline = get_pipeline(program_name)
250
261
  abridged_program = pipeline["abridged_sketch"]
251
262
  # Make the program runnable
@@ -277,8 +288,7 @@ def update(program_name, variables={}, headless=False, infer=False, replace=Fals
277
288
  add_examples_to_custom_function(pipeline_id, call_id, custom_function, replace)
278
289
  logger.info(f"Saved.")
279
290
  except Exception as e:
280
- print(e)
281
- logger.info(f"An error occurred while saving the program. The program was not updated.")
291
+ logger.error(f"An error occurred while saving the program. The program was not updated.")
282
292
 
283
293
 
284
294
  def detail(program_name=None, verbose=False):
@@ -292,96 +302,165 @@ def detail(program_name=None, verbose=False):
292
302
  )
293
303
 
294
304
 
295
- def run(program_name, variables={}, headless=False, remote=False, verbose=False):
305
+ def run(program_name, variables={}, headless=False, remote=False, output_log=False, verbose=False):
296
306
  """
297
307
  Executes pipeline code
298
308
  """
309
+ configure_logging(verbose)
310
+
299
311
  if headless and remote:
300
312
  raise ParsagonException("Cannot run a program remotely in headless mode")
301
313
 
314
+ logger.info("Preparing to run program %s", program_name)
315
+ pipeline_id = get_pipeline(program_name)["id"]
316
+
302
317
  if remote:
303
- pipeline_id = get_pipeline(program_name)["id"]
304
- result = create_pipeline_run(pipeline_id, variables)
318
+ result = create_pipeline_run(pipeline_id, variables, False)
305
319
  with Halo(text="Program running remotely...", spinner="dots"):
306
320
  while True:
307
321
  run = get_run(result["id"])
308
322
  status = run["status"]
323
+
324
+ if output_log and status in ("FINISHED", "ERROR"):
325
+ return {k: v for k, v in run.items() if k in ("output", "status", "log", "warnings", "error")}
326
+
309
327
  if status == "FINISHED":
328
+ if verbose:
329
+ logger.info(run["log"])
330
+ for warning in run["warnings"]:
331
+ logger.warning(warning)
310
332
  logger.info("Program finished running.")
311
333
  return run["output"]
312
334
  elif status == "ERROR":
313
335
  raise ParsagonException(f"Program failed to run: {run['error']}")
314
336
  elif status == "CANCELED":
315
337
  raise ParsagonException("Program execution was canceled")
338
+
316
339
  time.sleep(5)
317
340
 
318
- logger.info("Preparing to run program %s", program_name)
341
+ run = create_pipeline_run(pipeline_id, variables, True)
319
342
  code = get_pipeline_code(program_name, variables, headless)["code"]
343
+ start_time = datetime.datetime.now(datetime.timezone.utc).isoformat()
344
+ run_data = {"start_time": start_time}
320
345
 
321
346
  logger.info("Running program...")
322
347
  globals_locals = {"PARSAGON_API_KEY": get_api_key()}
323
348
  try:
324
349
  exec(code, globals_locals, globals_locals)
350
+ run_data["status"] = "FINISHED"
351
+ except:
352
+ run_data["status"] = "ERROR"
353
+ run_data["error"] = str(traceback.format_exc())
354
+ if not output_log:
355
+ raise
325
356
  finally:
357
+ end_time = datetime.datetime.now(datetime.timezone.utc).isoformat()
358
+ run_data["end_time"] = end_time
326
359
  if "driver" in globals_locals:
327
360
  globals_locals["driver"].quit()
328
361
  if "display" in globals_locals:
329
362
  globals_locals["display"].stop()
363
+ if "parsagon_log" in globals_locals:
364
+ run_data["log"] = "\n".join(globals_locals["parsagon_log"])
365
+ logger.info(run_data["log"])
366
+ if "parsagon_warnings" in globals_locals:
367
+ run_data["warnings"] = globals_locals["parsagon_warnings"]
330
368
  for proc in psutil.process_iter():
331
369
  try:
332
370
  if proc.name() == "chromedriver":
333
371
  proc.kill()
334
372
  except psutil.NoSuchProcess:
335
373
  continue
374
+ run = update_pipeline_run(run["id"], run_data)
336
375
  logger.info("Done.")
376
+ if output_log:
377
+ if "error" not in run_data:
378
+ run["output"] = globals_locals["output"]
379
+ return {k: v for k, v in run.items() if k in ("output", "status", "log", "warnings", "error")}
337
380
  return globals_locals["output"]
338
381
 
339
382
 
340
- def batch_runs(batch_name, program_name, runs=[], headless=False, ignore_errors=False, error_value=None):
383
+ def batch_runs(batch_name, program_name, runs=[], headless=False, ignore_errors=False, error_value=None, rerun_warnings=False, rerun_warning_types=[], rerun_errors=False, verbose=False):
384
+ configure_logging(verbose)
385
+
341
386
  save_file = f"{batch_name}.json"
342
387
  try:
343
388
  with open(save_file) as f:
344
- results = json.load(f)
389
+ outputs = json.load(f)
345
390
  except FileNotFoundError:
346
- results = []
347
- num_initial_results = len(results)
391
+ outputs = []
392
+ metadata_file = f"{batch_name}_metadata.json"
393
+ try:
394
+ with open(metadata_file) as f:
395
+ metadata = json.load(f)
396
+ except FileNotFoundError:
397
+ metadata = []
398
+
399
+ num_initial_results = len(outputs)
348
400
  pbar = tqdm(runs)
349
401
  default_desc = f'Running program "{program_name}"'
350
402
  pbar.set_description(default_desc)
351
403
  error = None
352
- error_variables = None
404
+ variables = None
353
405
  try:
354
406
  for i, variables in enumerate(pbar):
355
407
  if i < num_initial_results:
356
- continue
408
+ if rerun_errors and metadata[i]["status"] == "ERROR":
409
+ pass
410
+ elif rerun_warnings and metadata[i]["warnings"]:
411
+ if not rerun_warning_types or any(warning["type"] in rerun_warning_types for warning in metadata[i]["warnings"]):
412
+ pass
413
+ else:
414
+ continue
415
+ else:
416
+ continue
357
417
  for j in range(3):
358
- try:
359
- results.append(run(program_name, variables, headless))
418
+ result = run(program_name, variables, headless, output_log=True)
419
+ if result["status"] != "ERROR":
420
+ output = result.pop("output")
421
+ if i < num_initial_results:
422
+ outputs[i] = output
423
+ metadata[i] = result
424
+ else:
425
+ outputs.append(output)
426
+ metadata.append(result)
360
427
  break
361
- except Exception as e:
362
- error = e
363
- error_variables = variables
428
+ else:
429
+ error = result["error"].strip().split("\n")[-1]
364
430
  if j < 2:
365
- pbar.set_description(f"An error occurred: {e} - Waiting 60s before retrying (Attempt {j+2}/3)")
431
+ pbar.set_description(f"An error occurred: {error} - Waiting 60s before retrying (Attempt {j+2}/3)")
366
432
  time.sleep(60)
367
433
  pbar.set_description(default_desc)
368
434
  error = None
369
- error_variables = None
370
435
  continue
371
436
  else:
372
437
  if ignore_errors:
373
438
  error = None
374
- error_variables = None
375
- results.append(error_value)
439
+ if i < num_initial_results:
440
+ outputs[i] = error_value
441
+ else:
442
+ outputs.append(error_value)
376
443
  break
377
444
  else:
378
- raise
445
+ raise RunFailedException
446
+ except RunFailedException:
447
+ logger.error(f"Unresolvable error occurred on run with variables {variables}: {error} - Data has been saved to {save_file}. Rerun your command to resume.")
379
448
  except Exception as e:
380
- logger.error(f"Unresolvable error occurred on run with variables {error_variables}: {error} - Data has been saved to {save_file}. Rerun your command to resume.")
449
+ error = str(e)
450
+ logger.error(f"Unresolvable error occurred while looping over runs: {error} - Data has been saved to {save_file}. Rerun your command to resume.")
381
451
  finally:
382
452
  with open(save_file, "w") as f:
383
- json.dump(results, f)
384
- return None if error else results
453
+ json.dump(outputs, f)
454
+ with open(metadata_file, "w") as f:
455
+ json.dump(metadata, f)
456
+ num_warnings = 0
457
+ num_runs_with_warnings = 0
458
+ for m in metadata:
459
+ if m["warnings"]:
460
+ num_warnings += len(m["warnings"])
461
+ num_runs_with_warnings += 1
462
+ logger.info(f"\nSummary: {len(outputs)} runs made; {num_warnings} warnings encountered across {num_runs_with_warnings} runs. See {metadata_file} for logs.\n")
463
+ return None if error else outputs
385
464
 
386
465
 
387
466
  def delete(program_name, verbose=False, confirm_with_user=False):
parsagon/secrets.py CHANGED
@@ -1,14 +1,15 @@
1
+ import ast
1
2
  import re
2
3
 
3
4
 
4
5
  def extract_secrets(task):
5
6
  secrets = {}
6
- matches = list(re.finditer(r'\{\s*(?P<var>[A-Za-z_]+)\s*:\s*"(?P<value>([^"]|\\")*)"\}', task))
7
+ matches = list(re.finditer(r'\{\s*(?P<var>[A-Za-z_]+)\s*:\s*(?P<value>"([^"]|\\")*")\}', task))
7
8
  for match in matches:
8
9
  var_name = match.group("var")
9
10
  if not var_name.startswith("SECRET"):
10
11
  continue
11
12
  new_match = re.sub(r'\{([A-Za-z_]+):\s*"([^"]|\\")*"\}', '{\\1: "******"}', match.group(0))
12
13
  task = task.replace(match.group(0), new_match)
13
- secrets[match.group(1)] = match.group(2)
14
+ secrets[match.group(1)] = ast.literal_eval(match.group(2))
14
15
  return task, secrets
@@ -9,7 +9,7 @@ def test_non_secrets_are_not_extracted():
9
9
  """
10
10
  task = 'Go to https://example.com. Type {username: "myusername"} in the username field'
11
11
  task, secrets = extract_secrets(task)
12
- assert len(secrets) == 0
12
+ assert secrets == {}
13
13
  assert task == 'Go to https://example.com. Type {username: "myusername"} in the username field'
14
14
 
15
15
 
@@ -19,7 +19,7 @@ def test_secret_is_extracted():
19
19
  """
20
20
  task = 'Go to https://example.com. Type {SECRET_PASSWORD: "mypassword"} in the password field'
21
21
  task, secrets = extract_secrets(task)
22
- assert len(secrets) == 1
22
+ assert secrets == {"SECRET_PASSWORD": "mypassword"}
23
23
  assert task == 'Go to https://example.com. Type {SECRET_PASSWORD: "******"} in the password field'
24
24
 
25
25
 
@@ -29,7 +29,7 @@ def test_secret_with_quotes_is_extracted():
29
29
  """
30
30
  task = 'Go to https://example.com. Type {SECRET_PASSWORD: "mypassword\\"?!1"} in the password field'
31
31
  task, secrets = extract_secrets(task)
32
- assert len(secrets) == 1
32
+ assert secrets == {"SECRET_PASSWORD": 'mypassword"?!1'}
33
33
  assert task == 'Go to https://example.com. Type {SECRET_PASSWORD: "******"} in the password field'
34
34
 
35
35
 
@@ -39,7 +39,7 @@ def test_multiple_secrets_are_extracted():
39
39
  """
40
40
  task = 'Go to https://example.com. Type {SECRET_PASSWORD: "mypassword"} in the password field. Type {SECRET_ADDRESS: "myaddress"} in the address field'
41
41
  task, secrets = extract_secrets(task)
42
- assert len(secrets) == 2
42
+ assert secrets == {"SECRET_PASSWORD": "mypassword", "SECRET_ADDRESS": "myaddress"}
43
43
  assert task == 'Go to https://example.com. Type {SECRET_PASSWORD: "******"} in the password field. Type {SECRET_ADDRESS: "******"} in the address field'
44
44
 
45
45
 
@@ -49,5 +49,5 @@ def test_secrets_mixed_with_non_secrets_are_extracted():
49
49
  """
50
50
  task = 'Go to https://example.com. Type {USERNAME: "myusername"} in the username field. Type {SECRET_PASSWORD: "mypassword"} in the password field. Type {SECRET_ADDRESS: "myaddress"} in the address field'
51
51
  task, secrets = extract_secrets(task)
52
- assert len(secrets) == 2
52
+ assert secrets == {"SECRET_PASSWORD": "mypassword", "SECRET_ADDRESS": "myaddress"}
53
53
  assert task == 'Go to https://example.com. Type {USERNAME: "myusername"} in the username field. Type {SECRET_PASSWORD: "******"} in the password field. Type {SECRET_ADDRESS: "******"} in the address field'
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: parsagon
3
- Version: 0.10.21
3
+ Version: 0.10.22
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
@@ -1,11 +1,11 @@
1
1
  parsagon/__init__.py,sha256=n4-wiFVVuyW_KOJeNiycggAg9BTa5bbBIVpD_DkdOO4,125
2
- parsagon/api.py,sha256=11Wxi635Q2FpP5y1nN_YYoVIInkTnjO3mmSVKgLbCNk,6663
2
+ parsagon/api.py,sha256=nDTDe0LdDTn1hSXbgqd8j1qxe_3xWm3wZXhrTsmbwOE,6842
3
3
  parsagon/custom_function.py,sha256=oEj28qItaHUnsvLIHD7kg5QL3J3aO6rW6xKKP-H-Drs,770
4
- parsagon/exceptions.py,sha256=NYpFaSLZplBTv9fov_1LKPzDPIqb7Ffe7IunnjntxvA,819
4
+ parsagon/exceptions.py,sha256=tG1vnpmUN1GdJ1GSpe1MaWH3zWmFLZCwtOfEGu8qPP0,910
5
5
  parsagon/executor.py,sha256=e_e9p5eLvf7wYHk1BNJf0j_qt0H17BfivPb8CoOKMHE,22791
6
6
  parsagon/highlights.js,sha256=2UDfUApblU9xtGgTLCq4X7rHRV0wcqDSSFZPmJS6fJg,16643
7
- parsagon/main.py,sha256=M4d9jYbN3XlfOM3RStloaabG-KLyBXefnaXdgvREq3k,14911
8
- parsagon/secrets.py,sha256=PAiTsVHZ-gPefUQU01GMigdVaS8bHnzQLSUkYB4IweA,516
7
+ parsagon/main.py,sha256=mHmeXPUskTXyxJvuDnmOKF_MXkaOXB2oYYu5VOAE8s4,18344
8
+ parsagon/secrets.py,sha256=72dr-6q1q2ATBkE75fT18tcvwDM-4nymTb9NDVwjHTE,545
9
9
  parsagon/settings.py,sha256=s5_MsDMFM5tB8U8tfHaFnKibCoEqPnAu8b_ueg07Ftw,2947
10
10
  parsagon/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
11
  parsagon/tests/api_mocks.py,sha256=M8xhiyPa1dI8Vx-odDk7ETopfFAfcjfAf-ApmSqgvfw,3127
@@ -14,9 +14,9 @@ parsagon/tests/conftest.py,sha256=KMlHohc0QT77HzumraIojzKeqroyxarnaT6naJDNvEc,42
14
14
  parsagon/tests/test_executor.py,sha256=n3cmh84r74siSeJqUeAIwjjnNzDVPEdxcvYAeJ4hNX8,645
15
15
  parsagon/tests/test_invalid_args.py,sha256=kOjMpbZvviR1CwvXReteZMxBvuhq_rOv5Tm1muBSzNk,676
16
16
  parsagon/tests/test_pipeline_operations.py,sha256=TpBKCuRA8LHYWx3PD_k9mYCSsA_9SZjrOX-rS4mE8XE,1089
17
- parsagon/tests/test_secrets.py,sha256=-R87SHG5lBpKEbkprC0YpvGrayauNgqQoUr9QAuL_-o,2492
18
- parsagon-0.10.21.dist-info/METADATA,sha256=zWxIJZ2XPydOtzYPu7VDrvQPYORdEEljdHWGfsb6dFE,2410
19
- parsagon-0.10.21.dist-info/WHEEL,sha256=yQN5g4mg4AybRjkgi-9yy4iQEFibGQmlz78Pik5Or-A,92
20
- parsagon-0.10.21.dist-info/entry_points.txt,sha256=I1UlPUb4oY2k9idkI8kvdkEcrjKGRSOl5pMbA6uu6kw,48
21
- parsagon-0.10.21.dist-info/top_level.txt,sha256=ih5uYQzW4qjhRKppys-WiHLIbXVZ99YdqDcfAtlcQwk,9
22
- parsagon-0.10.21.dist-info/RECORD,,
17
+ parsagon/tests/test_secrets.py,sha256=Ctsscl2tmMTZcFAy5dnyqUlgTov2UharZgLpbRCLdEg,2662
18
+ parsagon-0.10.22.dist-info/METADATA,sha256=WMKXNXXNse8ftQwxVvwnV9LTuur6NmF2KCOJr9C8yZI,2410
19
+ parsagon-0.10.22.dist-info/WHEEL,sha256=Xo9-1PvkuimrydujYJAjF7pCkriuXBpUPEjma1nZyJ0,92
20
+ parsagon-0.10.22.dist-info/entry_points.txt,sha256=I1UlPUb4oY2k9idkI8kvdkEcrjKGRSOl5pMbA6uu6kw,48
21
+ parsagon-0.10.22.dist-info/top_level.txt,sha256=ih5uYQzW4qjhRKppys-WiHLIbXVZ99YdqDcfAtlcQwk,9
22
+ parsagon-0.10.22.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: bdist_wheel (0.41.2)
2
+ Generator: bdist_wheel (0.41.3)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5