parsagon 0.10.21__tar.gz → 0.10.22__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {parsagon-0.10.21 → parsagon-0.10.22}/PKG-INFO +1 -1
- {parsagon-0.10.21 → parsagon-0.10.22}/pyproject.toml +1 -1
- {parsagon-0.10.21 → parsagon-0.10.22}/src/parsagon/api.py +10 -2
- {parsagon-0.10.21 → parsagon-0.10.22}/src/parsagon/exceptions.py +5 -0
- {parsagon-0.10.21 → parsagon-0.10.22}/src/parsagon/main.py +106 -27
- {parsagon-0.10.21 → parsagon-0.10.22}/src/parsagon/secrets.py +3 -2
- {parsagon-0.10.21 → parsagon-0.10.22}/src/parsagon/tests/test_secrets.py +5 -5
- {parsagon-0.10.21 → parsagon-0.10.22}/src/parsagon.egg-info/PKG-INFO +1 -1
- {parsagon-0.10.21 → parsagon-0.10.22}/README.md +0 -0
- {parsagon-0.10.21 → parsagon-0.10.22}/setup.cfg +0 -0
- {parsagon-0.10.21 → parsagon-0.10.22}/src/__init__.py +0 -0
- {parsagon-0.10.21 → parsagon-0.10.22}/src/parsagon/__init__.py +0 -0
- {parsagon-0.10.21 → parsagon-0.10.22}/src/parsagon/custom_function.py +0 -0
- {parsagon-0.10.21 → parsagon-0.10.22}/src/parsagon/executor.py +0 -0
- {parsagon-0.10.21 → parsagon-0.10.22}/src/parsagon/highlights.js +0 -0
- {parsagon-0.10.21 → parsagon-0.10.22}/src/parsagon/settings.py +0 -0
- {parsagon-0.10.21 → parsagon-0.10.22}/src/parsagon/tests/__init__.py +0 -0
- {parsagon-0.10.21 → parsagon-0.10.22}/src/parsagon/tests/api_mocks.py +0 -0
- {parsagon-0.10.21 → parsagon-0.10.22}/src/parsagon/tests/cli_mocks.py +0 -0
- {parsagon-0.10.21 → parsagon-0.10.22}/src/parsagon/tests/conftest.py +0 -0
- {parsagon-0.10.21 → parsagon-0.10.22}/src/parsagon/tests/test_executor.py +0 -0
- {parsagon-0.10.21 → parsagon-0.10.22}/src/parsagon/tests/test_invalid_args.py +0 -0
- {parsagon-0.10.21 → parsagon-0.10.22}/src/parsagon/tests/test_pipeline_operations.py +0 -0
- {parsagon-0.10.21 → parsagon-0.10.22}/src/parsagon.egg-info/SOURCES.txt +0 -0
- {parsagon-0.10.21 → parsagon-0.10.22}/src/parsagon.egg-info/dependency_links.txt +0 -0
- {parsagon-0.10.21 → parsagon-0.10.22}/src/parsagon.egg-info/entry_points.txt +0 -0
- {parsagon-0.10.21 → parsagon-0.10.22}/src/parsagon.egg-info/requires.txt +0 -0
- {parsagon-0.10.21 → parsagon-0.10.22}/src/parsagon.egg-info/top_level.txt +0 -0
@@ -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
|
|
@@ -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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
389
|
+
outputs = json.load(f)
|
345
390
|
except FileNotFoundError:
|
346
|
-
|
347
|
-
|
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
|
-
|
404
|
+
variables = None
|
353
405
|
try:
|
354
406
|
for i, variables in enumerate(pbar):
|
355
407
|
if i < num_initial_results:
|
356
|
-
|
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
|
-
|
359
|
-
|
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
|
-
|
362
|
-
error =
|
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: {
|
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
|
-
|
375
|
-
|
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
|
-
|
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(
|
384
|
-
|
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):
|
@@ -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*
|
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
|
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
|
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
|
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
|
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
|
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'
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|