Skip to content

Commit 959db26

Browse files
GregoryEssertelEribertoLopez
authored andcommitted
feature: add filtering of instructions on the exec command (#210)
## Summary: - Add a `--exclude/-e` and `--include/-i` flags on the exec command to filter instructions. The filters are being validated then forwarded to SCLE where the filtering is done: https://github.com/strateos/lab/pull/1527 - Minor: use email address to specify who sent the payload. Require: https://github.com/strateos/lab/pull/1526 to be merged in lab as well ### Usage: ``` cat autoprotocol.json | \ transcriptic exec -a lab.core.strateos.com/haven/wctest -w wc4-mcx1 -e 1-9 -i 7 -e op:provision ``` This command will first filter out instruction from index 1 to 9 (`-e 1-9`) and the provisions (`-e op:provision`). Then add back the instruction at index 7 (`-i 7`). The order of `-i/-e` don't matter, it is first removing all the `-e`, then adding back all the `-i`. ### Filters Possible filter: - single index: 123 - range: 123-456 - key:value in the instruction: `op:provision`, or `x_human!:false`. The ! means that if the key (x_human) is not in the instruction, then the filter matches. Otherwise, without the '!' a missing key means that there is no match. Filter example: - { "op": "seal", "x_human": false } and { "op": "seal" } match `x_human!:false` - { "op": "seal" } does not match`x_human:true`, nor `x_human:false` ### Test plan Add unit tests #### Manual testing ``` internal_protocols/clients/sd2/current_protocols/growth_curve [master*]$ transcriptic preview GrowthCurve | \ transcriptic exec -a lab.core.strateos.com/haven/wctest -w wc4-mcx1 -e x_human:true Sending request to http://13.52.223.111:9000/testRun Success. View http://lab.core.strateos.com/haven/wctest/dashboard to see the scheduling outcome. ``` or ``` internal_protocols/clients/sd2/current_protocols/growth_curve [master*]$ transcriptic preview GrowthCurve | \ transcriptic exec -a lab.core.strateos.com/haven/wctest -w wc4-mcx1 -e op:provision Sending request to http://13.52.223.111:9000/testRun Success. View http://lab.core.strateos.com/haven/wctest/dashboard to see the scheduling outcome. ```
1 parent 2450a0e commit 959db26

3 files changed

Lines changed: 123 additions & 17 deletions

File tree

test/commands/exec_test.py

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,11 @@ def queue_test_success_res(sessionId="testSessionId"):
1313
return {"success": True, "sessionId": sessionId}
1414

1515

16+
fake_valid_URL = "something.bar.foo"
17+
18+
1619
def app_config_res():
17-
return {"hostManifest": {"lab": {"workcell": {"url": "something.bar.foo"}}}}
20+
return {"hostManifest": {"lab": {"workcell": {"url": fake_valid_URL}}}}
1821

1922

2023
def mock_api_endpoint():
@@ -30,7 +33,15 @@ def ap_file(tmpdir_factory):
3033
"""Make a temp autoprotocol file"""
3134
path = tmpdir_factory.mktemp("foo").join("ap.json")
3235
with open(str(path), "w") as f:
33-
f.write("{}") # any valid json works
36+
payload = {
37+
"instructions": [
38+
{"op": "provision", "x_human": True},
39+
{"op": "uncover"},
40+
{"op": "spin"},
41+
{"op": "cover"},
42+
]
43+
}
44+
f.write(json.dumps(payload))
3445
return path
3546

3647

@@ -94,7 +105,7 @@ def mockpost(*args, **kwargs):
94105
result = cli_test_runner.invoke(
95106
cli, ["exec", str(ap_file), "-a", mock_api_endpoint()]
96107
)
97-
assert result.exit_code == 0
108+
assert result.exit_code != 0
98109
assert "Error: " in result.stderr
99110

100111

@@ -160,5 +171,30 @@ def test_too_many_workcell_definition_arguments(cli_test_runner, monkeypatch, ap
160171
cli,
161172
["exec", str(ap_file), "-a", mock_api_endpoint(), "-s", "anthing", "-w", "wc0"],
162173
)
163-
assert result.exit_code == 0
174+
assert result.exit_code != 0
164175
assert "Error: --workcell-id, --session-id are mutually exclusive." in result.stderr
176+
177+
178+
def test_invalid_filters(cli_test_runner, monkeypatch, ap_file):
179+
invalid_command = [
180+
"-e",
181+
"10",
182+
"-i",
183+
"10",
184+
"-e",
185+
"2-1",
186+
"-i",
187+
"3-6",
188+
"-e",
189+
"anything",
190+
]
191+
invalid_filters = set(filter(lambda v: not v.startswith("-"), invalid_command))
192+
result = cli_test_runner.invoke(
193+
cli,
194+
["exec", str(ap_file), "-a", mock_api_endpoint()] + invalid_command,
195+
)
196+
assert result.exit_code != 0
197+
assert "Error: invalid filters" in result.stderr
198+
assert "(number of instructions: 4)" in result.stderr
199+
for inv in invalid_filters:
200+
assert inv in result.stderr

transcriptic/cli.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -690,6 +690,20 @@ def format_cmd(manifest):
690690
help="If set, the time constraints will be considered only a suggestion.",
691691
is_flag=True,
692692
)
693+
@click.option(
694+
"--exclude",
695+
"-e",
696+
metavar="FILTER",
697+
help="Remove those instructions from the payload. e.g.: 0, 0-5, x_human, op:povision, x_human!:false",
698+
multiple=True,
699+
)
700+
@click.option(
701+
"--include",
702+
"-i",
703+
metavar="FILTER",
704+
help="Add those instructions to the payload after the --exclude has been applied.",
705+
multiple=True,
706+
)
693707
@click.option(
694708
"--partition-group-size",
695709
type=click.INT,
@@ -707,7 +721,9 @@ def format_cmd(manifest):
707721
default=None,
708722
help="The device id to use as a swap space when partitioning.",
709723
)
724+
@click.pass_context
710725
def execute(
726+
ctx,
711727
autoprotocol,
712728
api,
713729
no_redirect,
@@ -718,6 +734,8 @@ def execute(
718734
schedule_at,
719735
schedule_delay,
720736
time_constraints_are_suggestion,
737+
exclude,
738+
include,
721739
partition_group_size,
722740
partition_horizon,
723741
partitioning_swap_device_id,
@@ -734,7 +752,10 @@ def execute(
734752
schedule_at,
735753
schedule_delay,
736754
time_constraints_are_suggestion,
755+
exclude,
756+
include,
737757
partition_group_size,
738758
partition_horizon,
739759
partitioning_swap_device_id,
760+
ctx.obj.api.email,
740761
)

transcriptic/commands.py

Lines changed: 62 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1474,6 +1474,28 @@ def run_protocol(api, manifest, protocol, inputs, view=False, dye_test=False):
14741474
return
14751475

14761476

1477+
def validate_filter(filters, number_of_instructions):
1478+
invalid_filters = set()
1479+
1480+
for arg in filters:
1481+
if arg.isdigit():
1482+
idx = int(arg)
1483+
if 0 > idx or idx >= number_of_instructions:
1484+
invalid_filters.add(arg)
1485+
elif re.match(r"\d+-\d+", arg):
1486+
[s, e] = [int(v) for v in arg.split("-")]
1487+
if s > e or number_of_instructions <= e:
1488+
invalid_filters.add(arg)
1489+
elif ":" in arg:
1490+
tokens = arg.split(":")
1491+
if len(tokens) != 2:
1492+
invalid_filters.add(arg)
1493+
else:
1494+
invalid_filters.add(arg)
1495+
1496+
return invalid_filters
1497+
1498+
14771499
def execute(
14781500
autoprotocol,
14791501
api,
@@ -1485,9 +1507,12 @@ def execute(
14851507
schedule_at,
14861508
schedule_delay,
14871509
time_constraints_are_suggestion,
1510+
exclude,
1511+
include,
14881512
partition_group_size,
14891513
partition_horizon,
14901514
partitioning_swap_device_id,
1515+
email,
14911516
):
14921517
# Clean api end point
14931518
if api.startswith("http://"):
@@ -1508,7 +1533,7 @@ def execute(
15081533
"Error: '--schedule-delay' and '--schedule-at' are mutually exclusive.",
15091534
err=True,
15101535
)
1511-
return
1536+
sys.exit(1)
15121537

15131538
# Get the requested scheduling time
15141539
if schedule_delay is not None:
@@ -1518,12 +1543,27 @@ def execute(
15181543
payload["scheduleAt"] = schedule_at
15191544

15201545
# Get the autoprotocol
1521-
autoprotocol_str = autoprotocol.read()
15221546
try:
1523-
payload["autoprotocol"] = json.loads(autoprotocol_str)
1547+
autoprotocol_json = json.loads(autoprotocol.read())
1548+
payload["autoprotocol"] = autoprotocol_json
15241549
except json.decoder.JSONDecodeError as err:
15251550
click.echo(f"Error decoding autoprotocol json: {err}", err=True)
1526-
return
1551+
sys.exit(1)
1552+
1553+
# validate filters
1554+
num_instructions = len(autoprotocol_json["instructions"])
1555+
invalid_exclude = validate_filter(exclude, num_instructions)
1556+
invalid_include = validate_filter(include, num_instructions)
1557+
if len(invalid_exclude) + len(invalid_include) > 0:
1558+
click.echo(
1559+
f"Error: invalid filters: {','.join(invalid_exclude.union(invalid_include))} (number of instructions: {num_instructions})",
1560+
err=True,
1561+
)
1562+
sys.exit(1)
1563+
1564+
# update the payload
1565+
payload["exclude"] = exclude
1566+
payload["include"] = include
15271567

15281568
# device set resolution
15291569
in_use = []
@@ -1534,7 +1574,7 @@ def execute(
15341574
payload["deviceSet"] = device_json
15351575
except json.decoder.JSONDecodeError as err:
15361576
click.echo(f"Error decoding device set json: {err}", err=True)
1537-
return
1577+
sys.exit(1)
15381578
in_use.append("--device-set")
15391579

15401580
if workcell_id:
@@ -1549,7 +1589,7 @@ def execute(
15491589

15501590
if len(in_use) > 1:
15511591
click.echo(f"Error: {', '.join(in_use)} are mutually exclusive.", err=True)
1552-
return
1592+
sys.exit(1)
15531593

15541594
if len(in_use) == 0:
15551595
payload["workcellIdForDeviceSet"] = "wctest-mcx1"
@@ -1573,9 +1613,10 @@ def execute(
15731613
path_tokens = clean_api.split("/")
15741614
if len(path_tokens) != 3:
15751615
click.echo(
1576-
f"Invalid api target, expects base-url/facility/workcell.", err=True
1616+
f"Error: Invalid api target, expects base-url/facility/workcell.",
1617+
err=True,
15771618
)
1578-
return
1619+
sys.exit(1)
15791620

15801621
clean_api = f"http://{clean_api}"
15811622
path_base = f"http://{path_tokens[0]}"
@@ -1596,16 +1637,22 @@ def execute(
15961637
]["url"]
15971638
else:
15981639
click.echo(
1599-
f"Error when get frontend node address: {res_json}", err=True
1640+
f"Error when getting frontend node address: {res_json}", err=True
16001641
)
1601-
return
1642+
sys.exit(1)
16021643
except json.decoder.JSONDecodeError:
1603-
click.echo(f"Error when get frontend node address: {res.text}", err=True)
1604-
return
1644+
click.echo(
1645+
f"Error when getting frontend node address: {res.text}", err=True
1646+
)
1647+
sys.exit(1)
1648+
1649+
# add sentBy
1650+
if email is not None:
1651+
payload["sentBy"] = email.split("@")[0]
16051652

16061653
# POST to workcell
16071654
test_run_endpoint = f"http://{frontend_node_address}/testRun"
1608-
click.echo(f"Sending request to http://{frontend_node_address}")
1655+
click.echo(f"Sending request to {test_run_endpoint}")
16091656
res = requests.post(test_run_endpoint, json=payload)
16101657
try:
16111658
res_json = json.loads(res.text)
@@ -1620,8 +1667,10 @@ def execute(
16201667
f"Dashboard can be seen at: {clean_api}/dashboard",
16211668
err=True,
16221669
)
1670+
sys.exit(1)
16231671
except json.decoder.JSONDecodeError:
16241672
click.echo(f"Error: {res.text}", err=True)
1673+
sys.exit(1)
16251674

16261675

16271676
def parse_json(json_file):

0 commit comments

Comments
 (0)