diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9934a52..c8ac527 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -88,9 +88,9 @@ jobs: python -m pip install --upgrade pip pip install pytest pip install . - - name: Set up instance of Aerie ${{ matrix.aerie-version }} + - name: Set up instance of PlanDev ${{ matrix.aerie-version }} env: - DOCKER_TAG: v${{ matrix.aerie-version }} # Prefix 'v' used in Aerie Docker image tags + DOCKER_TAG: v${{ matrix.aerie-version }} # Prefix 'v' used in PlanDev Docker image tags run: | docker compose -f docker-compose-test.yml up -d docker images diff --git a/README.md b/README.md index cb08ff7..689b5c3 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,22 @@ # Aerie-CLI -Aerie-CLI provides a command-line interface and user-extendable Python API for interacting with an instance of Aerie. +Aerie-CLI provides a command-line interface and user-extendable Python API for interacting with an instance of PlanDev. -> Note: this project is an informal CLI and is _not_ maintained by the MPSA Aerie team. +> Note: this project is an informal CLI and is _not_ maintained by the MPSA PlanDev team. + +## Aerie -> PlanDev Rebrand + +PlanDev was **formerly known as Aerie and is now named PlanDev**. While we've updated most documentation and external references, some legacy mentions of the old product name may remain as we complete the transition. + +What to know: + +* The planning product, including modeling, simulation, scheduling and constraint-checking, is now named PlanDev +* The sequencing product, including the sequence editor, workspaces, and actions, is now named SeqDev +* All features and functionality remain the same +* Currently, repository names, package names and other internal code references will retain their existing names, and deployment/migration procedures have not changed +* In a future release, our repository and/or package names may change. If so, this will be communicated to users via release notes and normal communication channels + +For the latest documentation, visit: [PlanDev Documentation](https://nasa-ammos.github.io/plandev-docs/) ## Getting Started @@ -16,11 +30,11 @@ This short procedure will get you up and running with the basics of the CLI. python3 -m pip install git+https://github.com/NASA-AMMOS/aerie-cli.git@main ``` -3. Configure access to an Aerie host +3. Configure access to an PlanDev host 1. If you've been provided a Configuration JSON, reference that file - 2. If you don't have already have a Configuration JSON, copy the following to a JSON file for a local Aerie deployment (replacing the username with your own): + 2. If you don't have already have a Configuration JSON, copy the following to a JSON file for a local PlanDev deployment (replacing the username with your own): ```json [ @@ -39,7 +53,7 @@ This short procedure will get you up and running with the basics of the CLI. aerie-cli configurations load -i JSON_FILE ``` -4. Activate a configuration to start a session with an Aerie host: +4. Activate a configuration to start a session with a PlanDev host: ```sh ➜ aerie-cli activate @@ -47,7 +61,7 @@ This short procedure will get you up and running with the basics of the CLI. Select an option: 1 ``` -5. Try out a command to list the plans in Aerie: +5. Try out a command to list the plans in PlanDev: ```sh aerie-cli plans list @@ -69,7 +83,7 @@ This short procedure will get you up and running with the basics of the CLI. ### Setup -Aerie-CLI uses configurations to define different Aerie hosts. Define configurations by either loading JSON configurations or manually via the CLI. Configurations persist on a per-user basis and may be shared between installations. +Aerie-CLI uses configurations to define different PlanDev hosts. Define configurations by either loading JSON configurations or manually via the CLI. Configurations persist on a per-user basis and may be shared between installations. #### Defining Hosts with a Configuration File @@ -85,9 +99,9 @@ You can view the configurations you've loaded with the `configurations list` com ➜ aerie-cli configurations list Configuration file location: /Users//Library/Application Support/aerie_cli/config.json - Aerie Host Configurations + PlanDev Host Configurations ┏━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━┓ -┃ Host Name ┃ GraphQL API URL ┃ Aerie Gateway URL ┃ Username ┃ +┃ Host Name ┃ GraphQL API URL ┃ PlanDev Gateway URL ┃ Username ┃ ┡━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━┩ │ localhost │ http://localhost:8080/v1/graphql │ http://localhost:9000 │ │ └───────────┴──────────────────────────────────┴───────────────────────┴──────────┘ @@ -101,17 +115,17 @@ If you haven't been provided a JSON configuration for a host, you can create a c Each configuration is stored as JSON object list entry in the configuration file provided with the `configurations list` command. The full contents of a host configuration are: -| Field | Description | Required | -| :-------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | -| `name` | User-facing name of the host | Yes | -| `graphql_url` | URL of the Aerie instance's Hasura GraphQL API | Yes | -| `gateway_url` | URL of the Aerie instance's Gateway | Yes | -| `username` | Username for authentication with Aerie | No | -| `external_auth` | Specification for external authentication required to reach an Aerie instance. See [Configuring for External Authentication](#configuring-for-external-authentication) for details | No | +| Field | Description | Required | +| :-------------- |:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -------- | +| `name` | User-facing name of the host | Yes | +| `graphql_url` | URL of the PlanDev instance's Hasura GraphQL API | Yes | +| `gateway_url` | URL of the PlanDev instance's Gateway | Yes | +| `username` | Username for authentication with PlanDev | No | +| `external_auth` | Specification for external authentication required to reach a PlanDev instance. See [Configuring for External Authentication](#configuring-for-external-authentication) for details | No | ### Sessions and Roles -Aerie-CLI maintains a persistent "session" with an Aerie instance so multiple commands can run without needing to re-authenticate. To start a session, use the `activate` command: +Aerie-CLI maintains a persistent "session" with a PlanDev instance so multiple commands can run without needing to re-authenticate. To start a session, use the `activate` command: ```sh ➜ aerie-cli activate @@ -120,7 +134,7 @@ Aerie-CLI maintains a persistent "session" with an Aerie instance so multiple co Select an option: 1 ``` -Aerie uses "roles" to adjust what a client is permitted to do. To view the active configuration name and current role, use the `status` command: +PlanDev uses "roles" to adjust what a client is permitted to do. To view the active configuration name and current role, use the `status` command: ```sh ➜ aerie-cli status @@ -128,7 +142,7 @@ Active configuration: localhost Active role: viewer ``` -The default role is configured by Aerie. To change the selected role for the active Aerie-CLI session, use the `role` command: +The default role is configured by PlanDev. To change the selected role for the active Aerie-CLI session, use the `role` command: ```sh ➜ aerie-cli role @@ -184,7 +198,7 @@ Alternatively, arguments can be provided using flags: #### Configuring for External Authentication -Aerie-CLI configurations include a mechanism to authenticate against an external authentication service which may require additional credentials as cookies for accessing Aerie. Aerie-CLI will issue a post request with given JSON data to a provided authentication endpoint and persist any returned cookies in a browser-like manner for the remainder of the Aerie-CLI session. +Aerie-CLI configurations include a mechanism to authenticate against an external authentication service which may require additional credentials as cookies for accessing PlanDev. Aerie-CLI will issue a post request with given JSON data to a provided authentication endpoint and persist any returned cookies in a browser-like manner for the remainder of the Aerie-CLI session. An external authentication service is configured using the key `external_auth` in the JSON configuration file as follows: @@ -220,7 +234,7 @@ In this example, the user would be prompted to enter a value for "password" and, #### Using a Hasura Admin Secret -In some cases, an admin secret may be used to permit otherwise prohibited requests through Hasura (the software behind the Aerie API). When running a command, the user may add the `--hasura-admin-secret` flag after the `aerie-cli` command to use these elevated privileges for the following command. +In some cases, an admin secret may be used to permit otherwise prohibited requests through Hasura (the software behind the PlanDev API). When running a command, the user may add the `--hasura-admin-secret` flag after the `aerie-cli` command to use these elevated privileges for the following command. --- @@ -232,8 +246,8 @@ Instead of using the CLI for interactive use cases, the underlying classes and m The key constructs are: -- `aerie_cli.aerie_host.AerieHost`: An abstraction for an Aerie Host, including methods for authentication and issuing requests to the Aerie API. -- `aerie_cli.aerie_client.AerieClient`: A class containing common requests and reusable logic to interact with data in Aerie. +- `aerie_cli.aerie_host.AerieHost`: An abstraction for a PlanDev Host, including methods for authentication and issuing requests to the PlanDev API. +- `aerie_cli.aerie_client.AerieClient`: A class containing common requests and reusable logic to interact with data in PlanDev. The following example defines an `AerieHost` using the necessary URLs, authenticates with a command-line prompt for the user's password, and issues a simple request using one of the built-in requests. @@ -243,15 +257,15 @@ from aerie_cli.aerie_host import AerieHost from getpass import getpass -# These URLs define the Aerie host +# These URLs define the PlanDev host GRAPHQL_URL = "http://myhostname:8080/v1/graphql" GATEWAY_URL = "http://myhostname:9000" -# User credentials. The password may be omitted on Aerie instances with authentication disabled +# User credentials. The password may be omitted on PlanDev instances with authentication disabled USERNAME = "myusername" PASSWORD = getpass(prompt='Password: ') -# Define the Aerie host and provide user credentials +# Define the host and provide user credentials aerie_host = AerieHost(GRAPHQL_URL, GATEWAY_URL) aerie_host.authenticate(USERNAME, PASSWORD) @@ -267,7 +281,7 @@ Look through the available methods in the provided `AerieClient` class to find o ### Adding Methods -If you need to write a custom query, you can extend the `AerieClient` class and add your own method. Access the Aerie host using the `aerie_host` property. For example: +If you need to write a custom query, you can extend the `AerieClient` class and add your own method. Access the PlanDev host using the `aerie_host` property. For example: ```py @@ -302,7 +316,7 @@ print(plan_id) ### Using the Active CLI Session -If your application will be run by a user who may also be using the CLI, you may reduce the amount of code required to configure an Aerie host and instead just use the active session. Aerie-CLI provides a utility to retrieve an `AerieClient` instance from the active CLI session: +If your application will be run by a user who may also be using the CLI, you may reduce the amount of code required to configure an PlanDev host and instead just use the active session. Aerie-CLI provides a utility to retrieve an `AerieClient` instance from the active CLI session: ```py from aerie_cli.utils.sessions import get_active_session_client @@ -370,7 +384,7 @@ Aerie-CLI has unit tests and integration tests built with the [pytest](https://d #### Unit Tests -Unit tests can be run anytime and reference local test files. `test_aerie_client.py` is where unit tests are added to exercise particular methods of the `AerieClient` class using mocked Aerie API responses. +Unit tests can be run anytime and reference local test files. `test_aerie_client.py` is where unit tests are added to exercise particular methods of the `AerieClient` class using mocked PlanDev API responses. Run the unit tests using `pytest` from the `tests/unit_tests` directory: @@ -381,7 +395,7 @@ python3 -m pytest . #### Integration Tests -A separate suite of tests is designed to validate CLI functionality against a local instance of Aerie. See the [integration testing documentation](tests/integration_tests/README.md) for details. +A separate suite of tests is designed to validate CLI functionality against a local instance of PlanDev. See the [integration testing documentation](tests/integration_tests/README.md) for details. The integration tests are based on `Typer` testing documentation found [here](https://typer.tiangolo.com/tutorial/testing/). diff --git a/pyproject.toml b/pyproject.toml index e7bb31b..eb72840 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [tool.poetry] name = "aerie-cli" version = "2.11.0" -description = "A CLI application and Python API for interacting with Aerie." +description = "A CLI application and Python API for interacting with PlanDev." authors = [] license = "MIT" readme = "README.md" diff --git a/src/aerie_cli/aerie_client.py b/src/aerie_cli/aerie_client.py index 139a56a..c47c626 100644 --- a/src/aerie_cli/aerie_client.py +++ b/src/aerie_cli/aerie_client.py @@ -1067,6 +1067,42 @@ def get_simulation_dataset_ids_by_plan_id(self, plan_id: int) -> List[int]: get_simulation_dataset_query, plan_id=plan_id) return [d["id"] for d in data[0]["simulation_datasets"]] + def update_simulation_boundary(self, plan_id:int, sim_start: str, sim_end: str) -> int: + change_boundary_mutation = """ + mutation ChangeBounds($plan_id: Int!, $new_start: timestamptz!, $new_end: timestamptz!) { + update_simulation(where: { plan_id: { _eq: $plan_id}}, _set: { simulation_end_time: $new_end, simulation_start_time: $new_start} + ) { + affected_rows + returning{ + simulation_end_time + simulation_start_time + } + } + } + """ + data = self.aerie_host.post_to_graphql( + change_boundary_mutation, + plan_id=plan_id, + new_start=sim_start, + new_end=sim_end + ) + return str(data["affected_rows"]) + + def get_simulation_boundary(self, plan_id:int) -> int: + get_boundary_mutation = """ + query GetBounds($plan_id: Int!) { + simulation_by_pk(id: $plan_id) { + simulation_end_time + simulation_start_time + } + } + """ + data = self.aerie_host.post_to_graphql( + get_boundary_mutation, + plan_id=plan_id + ) + return str(f"start: {data['simulation_start_time']} end: {data['simulation_end_time']}") + def expand_simulation( self, simulation_dataset_id: int, expansion_set_id: int ) -> int: @@ -1479,7 +1515,6 @@ def create_dictionary(self, dictionary: str, persist: bool=True) -> int: ) return next(iter(resp.values()))["id"] - def list_dictionaries(self) -> Dict[DictionaryType, List[DictionaryMetadata]]: """List all command, parameter, and channel dictionaries @@ -1917,7 +1952,6 @@ def upload_constraint(self, constraint): resp = self.aerie_host.post_to_graphql(upload_constraint_query, constraint=constraint) return resp["constraint_id"] - def add_constraint_to_plan(self, constraint_id, plan_id): """ Add a constraint to a plan's constraint specification. diff --git a/src/aerie_cli/aerie_host.py b/src/aerie_cli/aerie_host.py index 149878c..991dfa8 100644 --- a/src/aerie_cli/aerie_host.py +++ b/src/aerie_cli/aerie_host.py @@ -12,7 +12,8 @@ "3.7.0", "3.7.1", "3.8.0", - "3.8.1" + "3.8.1", + "4.0.0" ] class AerieHostVersionError(RuntimeError): diff --git a/src/aerie_cli/commands/plans.py b/src/aerie_cli/commands/plans.py index 7b616b8..f77b854 100644 --- a/src/aerie_cli/commands/plans.py +++ b/src/aerie_cli/commands/plans.py @@ -198,7 +198,18 @@ def duplicate( duplicated_plan_id = client.create_activity_plan(plan.model_id, plan_to_duplicate) typer.echo(f"Duplicate activity plan created with ID: {duplicated_plan_id}") +@plans_app.command() +def update_bounds( + id: int = typer.Option(..., '-i', help="Plan ID", prompt=True), + simulation_start: str = typer.Option(..., '-st', help="Simulation Start Time", prompt=True), + simulation_end: str = typer.Option(..., '-et', help="Simulation End Time", prompt=True), +): + """ Change Simulation Boundaries""" + client = CommandContext.get_client() + client.update_simulation_boundary(id, simulation_start, simulation_end) + typer.echo(f"Simulation bounds changed to {simulation_start} : {simulation_end}") + @plans_app.command() def simulate( id: int = typer.Option(..., help="Plan ID", prompt=True), diff --git a/tests/integration_tests/files/models/banananation-4.0.0.jar b/tests/integration_tests/files/models/banananation-4.0.0.jar new file mode 100644 index 0000000..95f30dd Binary files /dev/null and b/tests/integration_tests/files/models/banananation-4.0.0.jar differ diff --git a/tests/integration_tests/test_plans.py b/tests/integration_tests/test_plans.py index a9c50b5..edb1012 100644 --- a/tests/integration_tests/test_plans.py +++ b/tests/integration_tests/test_plans.py @@ -16,6 +16,7 @@ PLAN_ARTIFACTS_PATH.mkdir() DOWNLOADED_FILE_NAME = "downloaded_file.test" +SIM_BOUNDS_FILE = "sim_bounds_run.test" # Model Variables model_id = -1 @@ -27,6 +28,7 @@ plan_id = -1 PLAN_ARGS_INIT = os.path.join(PLANS_PATH, "create_config.json") PLAN_ARGS_UPDATE = os.path.join(PLANS_PATH, "update_config.json") +PLAN_BOUNDS_UPDATE = {"start":"2023-08-04T00:00:00", "end": "2023-08-04T12:00:00+00:00"} @pytest.fixture(scope="module", autouse=True) def set_up_environment(request): @@ -299,9 +301,33 @@ def test_simulate_after_update_config(): f"{result.stderr}" assert f"Simulation completed" in result.stdout -####################### +def test_sim_bounds(): + result = RUNNER.invoke( + app, + ["plans", "update-bounds"], + input=str(plan_id) + "\n" + PLAN_BOUNDS_UPDATE["start"] + "\n" + PLAN_BOUNDS_UPDATE["end"] + "\n", + catch_exceptions=False, + ) + assert result.exit_code == 0,\ + f"{result.stdout}"\ + f"{result.stderr}" + assert f"Simulation bounds changed to {PLAN_BOUNDS_UPDATE['start']} : {PLAN_BOUNDS_UPDATE['end']}" in result.stdout + +def test_simulate_after_update_bounds(): + result = cli_plan_simulate() + assert result.exit_code == 0,\ + f"{result.stdout}"\ + f"{result.stderr}" + assert f"Simulation completed" in result.stdout + + result = client.get_simulation_boundary(plan_id) + # If the bound change was correct, the simulation now starts after the activity + assert f"{PLAN_BOUNDS_UPDATE['start']}" in result and f"{PLAN_BOUNDS_UPDATE['end']}" in result + + +###################### # DELETE PLANS -####################### +###################### def test_plan_delete(): result = RUNNER.invoke(