From 353ffbfa04973d85afc11cb4a7e733d0136d0109 Mon Sep 17 00:00:00 2001 From: Carter Mak Date: Sat, 27 Jan 2024 15:21:32 -0800 Subject: [PATCH] Implement expansion deploy command --- src/aerie_cli/commands/expansion.py | 106 +++++++++++++++++- src/aerie_cli/schemas/client.py | 25 +++++ .../files/expansion/BakeBananaBread_exp.ts | 6 + .../files/expansion/BiteBanana_exp.ts | 6 + .../expansion/expansion_deploy_config.json | 28 +++++ tests/integration_tests/test_expansion.py | 29 +++++ 6 files changed, 199 insertions(+), 1 deletion(-) create mode 100644 tests/integration_tests/files/expansion/BakeBananaBread_exp.ts create mode 100644 tests/integration_tests/files/expansion/BiteBanana_exp.ts create mode 100644 tests/integration_tests/files/expansion/expansion_deploy_config.json diff --git a/src/aerie_cli/commands/expansion.py b/src/aerie_cli/commands/expansion.py index 998ee619..8ad2303d 100644 --- a/src/aerie_cli/commands/expansion.py +++ b/src/aerie_cli/commands/expansion.py @@ -4,12 +4,14 @@ from pathlib import Path import fnmatch +import arrow + from rich.console import Console from rich.table import Table from aerie_cli.commands.command_context import CommandContext from aerie_cli.utils.prompts import select_from_list -from aerie_cli.schemas.client import ExpansionRun +from aerie_cli.schemas.client import ExpansionRun, ExpansionDeployConfiguration app = typer.Typer() sequences_app = typer.Typer() @@ -19,6 +21,108 @@ app.add_typer(runs_app, name='runs', help='Commands for expansion runs') app.add_typer(sets_app, name='sets', help='Commands for expansion sets') +# === Bulk Deploy Command === + +@app.command('deploy') +def bulk_deploy( + model_id: int = typer.Option( + ..., '--model-id', '-m', prompt='Mission Model ID', + help='Mission Model ID' + ), + command_dictionary_id: int = typer.Option( + ..., '--command-dict-id', '-d', prompt='Command Dictionary ID', + help='Command Dictionary ID' + ), + config_file: str = typer.Option( + ..., "--config-file", "-c", prompt="Configuration file", + help="Deploy configuration JSON file" + ), + rules_path: Path = typer.Option( + Path.cwd(), help="Path to folder containing expansion rule files" + ), + time_tag: bool = typer.Option(False, help="Append time tags to create unique expansion rule/set names") +): + """ + Bulk deploy command expansion rules and sets to an Aerie instance according to a JSON configuration file. + + The configuration file contains a list of rules and a list of sets: + + ``` + { + "rules": [...], + "sets": [...] + } + ``` + + Each rule must provide a unique rule name, the activity type name, and the name of the file with expansion logic: + + ``` + { + "name": "Expansion Rule Name", + "activity_type": "Activity Type Name", + "file_name": "my_file.ts" + } + ``` + + Each set must provide a unique set name and a list of rule names to add: + + ``` + { + "name": "Expansion Set Name", + "rules": ["Expansion Rule Name", ...] + } + ``` + """ + + client = CommandContext.get_client() + + with open(Path(config_file), "r") as fid: + configuration: ExpansionDeployConfiguration = ExpansionDeployConfiguration.from_dict(json.load(fid)) + + name_suffix = arrow.utcnow().format("_YYYY-MM-DDTHH-mm-ss") if time_tag else "" + + # Loop and upload all expansion rules + uploaded_rules = {} + for rule in configuration.rules: + try: + with open(rules_path.joinpath(rule.file_name), "r") as fid: + expansion_logic = fid.read() + + rule_id = client.create_expansion_rule( + expansion_logic=expansion_logic, + activity_name=rule.activity_type, + model_id=model_id, + command_dictionary_id=command_dictionary_id, + name=rule.name + name_suffix + ) + typer.echo(f"Created expansion rule {rule.name + name_suffix}: {rule_id}") + uploaded_rules[rule.name] = rule_id + except: + typer.echo(f"Failed to create expansion rule {rule.name}") + + for set in configuration.sets: + try: + rule_ids = [] + for rule_name in set.rules: + if rule_name in uploaded_rules.keys(): + rule_ids.append(uploaded_rules[rule_name]) + else: + typer.echo(f"No uploaded rule {rule_name} for set {set.name}") + + assert len(rule_ids) + + set_id = client.create_expansion_set( + command_dictionary_id=command_dictionary_id, + model_id=model_id, + expansion_ids=rule_ids, + name=set.name + name_suffix + ) + + typer.echo(f"Created expansion set {set.name + name_suffix}: {set_id}") + except: + typer.echo(f"Failed to create expansion set {set.name}") + + # === Commands for expansion runs === diff --git a/src/aerie_cli/schemas/client.py b/src/aerie_cli/schemas/client.py index 9b84bec8..c9497f1c 100644 --- a/src/aerie_cli/schemas/client.py +++ b/src/aerie_cli/schemas/client.py @@ -392,3 +392,28 @@ class ExpansionRule(ClientSerialize): class ResourceType(ClientSerialize): name: str schema: Dict + + +@define +class ExpansionDeployRule(ClientSerialize): + name: str + activity_type: str + file_name: str + + +@define +class ExpansionDeploySet(ClientSerialize): + name: str + rules: List[str] + + +@define +class ExpansionDeployConfiguration(ClientSerialize): + rules: List[ExpansionDeployRule] = field( + converter=converters.optional( + lambda x: [ExpansionDeployRule.from_dict(d) if isinstance(d, dict) else d for d in x]) + ) + sets: List[ExpansionDeploySet] = field( + converter=converters.optional( + lambda x: [ExpansionDeploySet.from_dict(d) if isinstance(d, dict) else d for d in x]) + ) diff --git a/tests/integration_tests/files/expansion/BakeBananaBread_exp.ts b/tests/integration_tests/files/expansion/BakeBananaBread_exp.ts new file mode 100644 index 00000000..5efc4564 --- /dev/null +++ b/tests/integration_tests/files/expansion/BakeBananaBread_exp.ts @@ -0,0 +1,6 @@ +export default function MyExpansion(props: { + activityInstance: ActivityType +}): ExpansionReturn { + const { activityInstance } = props + return [] +} \ No newline at end of file diff --git a/tests/integration_tests/files/expansion/BiteBanana_exp.ts b/tests/integration_tests/files/expansion/BiteBanana_exp.ts new file mode 100644 index 00000000..5efc4564 --- /dev/null +++ b/tests/integration_tests/files/expansion/BiteBanana_exp.ts @@ -0,0 +1,6 @@ +export default function MyExpansion(props: { + activityInstance: ActivityType +}): ExpansionReturn { + const { activityInstance } = props + return [] +} \ No newline at end of file diff --git a/tests/integration_tests/files/expansion/expansion_deploy_config.json b/tests/integration_tests/files/expansion/expansion_deploy_config.json new file mode 100644 index 00000000..78e4f6d0 --- /dev/null +++ b/tests/integration_tests/files/expansion/expansion_deploy_config.json @@ -0,0 +1,28 @@ +{ + "rules": [ + { + "name": "integration_test_BakeBananaBread", + "activity_type": "BakeBananaBread", + "file_name": "BakeBananaBread_exp.ts" + }, + { + "name": "integration_test_BiteBanana", + "activity_type": "BiteBanana", + "file_name": "BiteBanana_exp.ts" + }, + { + "name": "integration_test_bad", + "activity_type": "Fake", + "file_name": "this path no exist" + } + ], + "sets": [ + { + "name": "integration_test_set", + "rules": [ + "integration_test_BakeBananaBread", + "integration_test_BiteBanana" + ] + } + ] +} \ No newline at end of file diff --git a/tests/integration_tests/test_expansion.py b/tests/integration_tests/test_expansion.py index c004fdb8..0e882e37 100644 --- a/tests/integration_tests/test_expansion.py +++ b/tests/integration_tests/test_expansion.py @@ -37,6 +37,8 @@ # Expansion Variables expansion_set_id = -1 expansion_sequence_id = 1 +EXPANSION_FILES_PATH = os.path.join(FILES_PATH, "expansion") +EXPANSION_DEPLOY_CONFIG_PATH = os.path.join(EXPANSION_FILES_PATH, "expansion_deploy_config.json") @pytest.fixture(scope="module", autouse=True) def set_up_environment(request): @@ -127,6 +129,33 @@ def test_expansion_sequence_delete(): # Uses model, command dictionary, and activity types ####################### + +def test_expansion_deploy(): + result = runner.invoke( + app, + [ + "expansion", + "deploy", + "-m", + str(model_id), + "-d", + str(command_dictionary_id), + "-c", + EXPANSION_DEPLOY_CONFIG_PATH, + "--rules-path", + EXPANSION_FILES_PATH, + "--time-tag" + ], + catch_exceptions=False + ) + assert result.exit_code == 0, \ + f"{result.stdout}"\ + f"{result.stderr}" + assert "Created expansion rule integration_test_BakeBananaBread" in result.stdout + assert "Created expansion rule integration_test_BiteBanana" in result.stdout + assert "Failed to create expansion rule integration_test_bad" in result.stdout + assert "Created expansion set integration_test_set" in result.stdout + def test_expansion_set_create(): client.create_expansion_rule( expansion_logic="""