diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1aff968..1179c21 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,7 +2,7 @@ # See https://pre-commit.com/hooks.html for more hooks repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.0.1 + rev: v4.1.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer @@ -10,13 +10,13 @@ repos: - id: check-added-large-files - repo: https://github.com/psf/black - rev: 21.12b0 + rev: 22.3.0 hooks: - id: black-jupyter language_version: python3 - repo: https://github.com/pycqa/isort - rev: 5.8.0 + rev: 5.10.1 hooks: - id: isort name: isort (python) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 9a56a8b..2e315b8 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -10,6 +10,29 @@ Unreleased changes are documented in files in the `changelog.d`_ directory. .. scriv-insert-here +0.14.0 — 2022-03-25 +=================== + +Features +-------- + +- `[sc-13426] `_ + Support setting tags when using the ``flow run`` subcommand. +- Support batch updates of one or more Runs. +- Support updating tags and labels using the ``flow run-update`` subcommand. +- Support erasing the list of Run managers and Run monitors using the ``flow run-update`` subcommand. + This can be done by specifying an empty string for the value of the ``--run-manager`` and ``--run-monitor`` options. + +Bugfixes +-------- + +- `[sc-13664] `_ + Fix tabular ``run-list`` output. +- `[sc-14109] `_ + Mark the ``run-status`` subcommand's ``--flow-id`` option as a mandatory UUID. +- `[sc-14127] `_ + Prevent a validation error that occurs when an input schema is not provided to the ``flow deploy`` subcommand. + 0.13.1 — 2022-03-02 =================== diff --git a/docs/source/authoring_flows.rst b/docs/source/authoring_flows.rst index 051d072..b2978d5 100644 --- a/docs/source/authoring_flows.rst +++ b/docs/source/authoring_flows.rst @@ -516,8 +516,8 @@ Example Flows .. _example-flow-move: -"Move" Flow -^^^^^^^^^^^ +"Move (Globus Example)" +^^^^^^^^^^^^^^^^^^^^^^^ Flow ID: ``9123c20b-61e0-46e8-9469-92c999b6b8f2``. @@ -526,6 +526,8 @@ from a source to a destination and then deleting the directory from the source. The entire directory's contents, including files and subdirectories, will be moved to the destination and then removed from the source. +Note that this flow requires at least one of the collections to be managed under a Globus subscription. + View the `Move flow definition`_ in the Globus web app. (You may need to log in first.) @@ -533,45 +535,58 @@ View the `Move flow definition`_ in the Globus web app. :caption: Example Input { - "source_endpoint_id": "ddb59af0-6d04-11e5-ba46-22000b92c6ec", - "source_path": "/~/source-directory", - "destination_endpoint_id": "ddb59aef-6d04-11e5-ba46-22000b92c6ec", - "destination_path": "/~/destination-directory", - "transfer_label": "Transfer for Generic Move from Globus Tutorial Endpoint 2 to Globus Tutorial Endpoint 1", - "delete_label": "Delete after Transfer for Generic Move from Globus Tutorial Endpoint 2 to Globus Tutorial Endpoint 1" + "source": { + "id": "ddb59aef-6d04-11e5-ba46-22000b92c6ec", + "path": "/~/source-directory" + }, + "destination": { + "id": "ddb59af0-6d04-11e5-ba46-22000b92c6ec", + "path": "/~/destination-directory" + } + "transfer_label": "Transfer for Generic Move from Globus Tutorial Endpoint 1 to Globus Tutorial Endpoint 2", + "delete_label": "Delete after Transfer for Generic Move from Globus Tutorial Endpoint 1 to Globus Tutorial Endpoint 2" } -(Choose different ``source_path`` and ``destination_path`` as needed to run this -example flow.) +(Choose different ``source.path`` and ``destination.path`` as needed to run this example flow.) .. _example-flow-2-stage-transfer: -"2 Stage Transfer" Flow -^^^^^^^^^^^^^^^^^^^^^^^ +"2 Stage Transfer (Globus Example)" +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Flow ID: ``79a4653f-f8da-43b6-a581-5d3b345ad575``. Transfer from source to destination with an intermediate endpoint in-between. Remove from intermediate after completion. +Note that this flow requires at least one of the collections to be managed under a Globus subscription. + View the `2 Stage Transfer flow definition`_ in the Globus web app. .. code-block:: json :caption: Example Input { - "source_endpoint_id": "ddb59af0-6d04-11e5-ba46-22000b92c6ec", - "source_path": "/~/source-directory", - "intermediate_endpoint_id": "ddb59af0-6d04-11e5-ba46-22000b92c6ec", - "intermediate_path": "/~/intermediate-directory-which-will-be-removed", - "destination_endpoint_id": "ddb59aef-6d04-11e5-ba46-22000b92c6ec", - "destination_path": "/~/destination-directory", + "source": { + "id": "ddb59aef-6d04-11e5-ba46-22000b92c6ec", + "path": "/~/ep1-example-directory/" + }, + "intermediate": { + "id": "ddb59af0-6d04-11e5-ba46-22000b92c6ec", + "path": "/~/ep2-intermediate-directory/" + }, + "destination__": { + "id": "ddb59aef-6d04-11e5-ba46-22000b92c6ec", + "path": "/~/ep1-duplicate-example-directory/" + } + "transfer1_label": "This value will be used as a label for the Globus Transfer Task to copy data from the source collection to the intermediate collection", + "transfer2_label": "This value will be used as a label for the Globus Transfer Task to copy data from the intermediate collection to the destination collection" } .. _example-flow-transfer-set-permissions: -"Transfer Set Permissions" Flow -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +"Transfer Set Permissions (Globus Example)" +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Flow ID: ``cdcd6d1a-b1c3-4e0b-8d4c-f205c16bf80c``. diff --git a/docs/source/overview.rst b/docs/source/overview.rst index e89e1f3..4ccdce4 100644 --- a/docs/source/overview.rst +++ b/docs/source/overview.rst @@ -46,7 +46,7 @@ service. Access to ``Action Providers`` and their invocation is controlled via ``Globus Auth``. Some of these services may be *synchronous* meaning that an invocation will complete in the context of the HTTP request that triggered it. Other services support *asynchronous* activities, meaning that the invocation -will persist beyond the HTTP request that invoked it and the the caller must +will persist beyond the HTTP request that invoked it and the caller must monitor the ``Action`` for updates on when it is completed and its result. Globus operates a series of these ``Action Providers`` available for diff --git a/docs/source/using_the_cli.rst b/docs/source/using_the_cli.rst index bb330b6..4d24a35 100644 --- a/docs/source/using_the_cli.rst +++ b/docs/source/using_the_cli.rst @@ -29,7 +29,7 @@ useful for reusing tokens, debugging purposes, or for getting more familiar with the Globus Automate services' APIs. Each command also supports a ``--help`` option which provides concise -information on the the command and documents its expected inputs. +information on the command and documents its expected inputs. In almost all cases, the output from each command will be in JSON format. The CLI will format the output to try to improve readability, however, you may @@ -163,7 +163,7 @@ introspect operation to get a description of the ``Action Provider``: From this introspection response we can see that the *scope string* for -this ``Action Provider`` is the the value of the ``globus_auth_scope`` field, +this ``Action Provider`` is the value of the ``globus_auth_scope`` field, ``https://auth.globus.org/scopes/actions.globus.org/hello_world``. We can also see that the ``admin_contact`` is Globus. @@ -514,7 +514,7 @@ view the Flow and only they will be able to run an instance of the Flow. When deploying, it's possible to specify who should be able to see and run the Flow. Using the ``visible_to`` flag, you can indicate which Globus identities can view the deployed Flow, or set it to ``public``, which creates a Flow viewable by -anyone. Using the ``runnable_by`` flag, you can indicate which Globus ideneties +anyone. Using the ``runnable_by`` flag, you can indicate which Globus identities can run an instance of the deployed Flow, or set a value of ``all_authenticated_users`` which allows any authenticated user to run an instance of the Flow. diff --git a/examples/flows/end-to-end-test-yaml/definition-generic.yaml b/examples/flows/end-to-end-test-yaml/definition-generic.yaml new file mode 100644 index 0000000..8588fc2 --- /dev/null +++ b/examples/flows/end-to-end-test-yaml/definition-generic.yaml @@ -0,0 +1,240 @@ +Comment: >- + A Flow for invoking all action providers and hitting a url upon success. + + Actions tested: + - GlobusTransfer + - GlobusTransferDelete + - GlobusLs + - ExpressionEval + - GlobusHttp + + Actions to be tested: + - GlobusTransferCollectionInfo + - GlobusTransferMkdir + - GlobusTransferSetAcl + - Notification + - WebOption + - DataciteMint + - DataciteMintDCSchema + - SearchIngest + - SearchDelete +StartAt: Stage1RunGlobusTransfer +States: + + Stage1RunGlobusTransfer: + Comment: >- + Run the initial transfer operation from the source ep/source path to the + destination ep/destination path + Type: Action + ActionUrl: 'https://ENVIRONMENTNAME.actions.automate.globus.org/transfer/transfer' + WaitTime: 60 + ExceptionOnActionFailure: false + Parameters: + source_endpoint_id.$: $.source.id + destination_endpoint_id.$: $.destination.id + label.$: $.transfer_label + transfer_items: + - source_path.$: $.source.path + destination_path.$: $.destination.path + recursive: false + ResultPath: $.Stage1RunGlobusTransferResult + Next: Stage1CheckTransferStatus + + Stage1CheckTransferStatus: + Comment: >- + Examine the status of the Action. If it failed, branch to a state + that reports the error + Type: Choice + Choices: + - Variable: $.Stage1RunGlobusTransferResult.status + StringEquals: FAILED + Next: Stage1RunGlobusTransferFailed + Default: Stage2RunGlobusLsToConfirmTransfer + + Stage1RunGlobusTransferFailed: + Comment: Report the error and end the flow execution + Type: Pass + Parameters: + message: Transfer failed + details.$: $.Stage1RunGlobusTransferResult.details + ResultPath: $.FlowResult + End: true + + + Stage2RunGlobusLsToConfirmTransfer: + Comment: >- + Use GlobusLs to check that the transfered file was actually transferred. + Type: Action + ActionUrl: 'https://ENVIRONMENTNAME.actions.automate.globus.org/transfer/ls' + WaitTime: 30 + ExceptionOnActionFailure: false + Parameters: + endpoint_id.$: $.destination.id + path.$: $.destination.path + path_only: true + ResultPath: $.Stage2RunGlobusLsToConfirmTransferResult + Next: Stage2RunGlobusLsToConfirmTransferStatus + + Stage2RunGlobusLsToConfirmTransferStatus: + Comment: >- + Examine the action status. If failed, branch to a state that reports the error + Type: Choice + Choices: + - Variable: $.Stage2RunGlobusLsToConfirmTransferResult.status + StringEquals: FAILED + Next: Stage2RunGlobusLsToConfirmTransferResultFailed + Default: Stage2CheckFilesLsData + + Stage2RunGlobusLsToConfirmTransferResultFailed: + Comment: Report the error and end the flow execution + Type: Pass + Parameters: + message: Check of file transfer failed + details.$: $.Stage2RunGlobusLsToConfirmTransferResult.details + ResultPath: $.FlowResult + End: true + + Stage2CheckFilesLsData: + Comment: Check that there is a file at the destination + Type: Choice + Choices: + - Variable: "$.Stage2RunGlobusLsToConfirmTransferResult.details.DATA[0]" + IsPresent: false + Next: Stage2LsFailed + Default: Stage3RunGlobusDelete + + Stage2LsFailed: + Type: Pass + Parameters: + message: "Transfer file listing didn't show any files" + details.$: $.Stage2CheckFilesLsData.details + ResultPath: $.FlowResult + End: true + + + Stage3RunGlobusDelete: + Comment: >- + Use Transfer to delete the transferred file. + Type: Action + ActionUrl: 'https://ENVIRONMENTNAME.actions.automate.globus.org/transfer/delete' + WaitTime: 60 + ExceptionOnActionFailure: false + Parameters: + endpoint_id.$: $.destination.id + recursive: true + label.$: $.delete_label + items.=: '[`$.destination.path`]' + ResultPath: $.Stage3DeleteResult + Next: Stage3CheckRunGlobusDeleteStatus + + Stage3CheckRunGlobusDeleteStatus: + Comment: >- + Examine the status of the Action. If it failed, branch to a state + that reports the error + Type: Choice + Choices: + - Variable: $.Stage3DeleteResult.status + StringEquals: FAILED + Next: Stage3RunGlobusDeleteFailed + Default: Stage4RunGlobusLsToConfirmDeletion + + Stage3RunGlobusDeleteFailed: + Comment: Report the error and end the flow execution + Type: Pass + Parameters: + message: Deletion failed + details.$: $.Stage3DeleteResult.details + ResultPath: $.FlowResult + End: true + + + Stage4RunGlobusLsToConfirmDeletion: + Comment: >- + Use GlobusLs to check that the transfered file was actually transferred. + Type: Action + ActionUrl: 'https://ENVIRONMENTNAME.actions.automate.globus.org/transfer/ls' + WaitTime: 30 + ExceptionOnActionFailure: false + Parameters: + endpoint_id.$: $.destination.id + path.$: $.destination.path + path_only: true + ResultPath: $.Stage4RunGlobusLsToConfirmDeletionResult + Next: Stage4RunGlobusLsToConfirmDeletionStatus + + Stage4RunGlobusLsToConfirmDeletionStatus: + Comment: >- + Examine the action status. If failed, branch to a state that reports the error + Type: Choice + Choices: + - Variable: $.Stage4RunGlobusLsToConfirmDeletionResult.status + StringEquals: FAILED + Next: Stage4RunGlobusLsToConfirmDeletionResultFailed + Default: Stage4CheckFilesLsData + + Stage4RunGlobusLsToConfirmDeletionResultFailed: + Comment: Report the error and end the flow execution + Type: Pass + Parameters: + message: Check of file transferred failed + details.$: $.Stage4RunGlobusLsToConfirmDeletionResult.details + ResultPath: $.FlowResult + End: true + + Stage4CheckFilesLsData: + Comment: Check that there is no longer a file at the destination + Type: Choice + Choices: + - Variable: "$.Stage4RunGlobusLsToConfirmTransferResult.details.DATA[0]" + IsPresent: true + Next: Stage4LsFailed + Default: Stage5RunHttpToResetTimer + + Stage4LsFailed: + Type: Pass + Parameters: + message: "Transfer file listing still showed the file" + details.$: $.Stage4CheckFilesLsData.details + ResultPath: $.FlowResult + End: true + + + Stage5RunHttpToResetTimer: + Type: Action + ActionUrl: 'https://ENVIRONMENTNAME.actions.automate.globus.org/http' + WaitTime: 30 + ExceptionOnActionFailure: false + Parameters: + method: GET + url.$: $.timer_url + ResultPath: $.Stage5RunHttpToResetTimerResult + Next: Stage5RunHttpToResetTimerStatus + + Stage5RunHttpToResetTimerStatus: + Comment: >- + Examine the status of the Action. If it failed, branch to a state + that reports the error + Type: Choice + Choices: + - Variable: $.Stage5RunHttpToResetTimerResult.status + StringEquals: FAILED + Next: Stage5RunHttpToResetTimerFailed + Default: AllComplete + + Stage5RunHttpToResetTimerFailed: + Comment: Report the error and end the flow execution + Type: Pass + Parameters: + message: Deletion failed + details.$: $.Stage5RunHttpToResetTimerResult.details + ResultPath: $.FlowResult + End: true + + + AllComplete: + Comment: 'Normal completion, so report success and exit' + Type: Pass + Parameters: + message: Move operation complete + ResultPath: $.FlowResult + End: true diff --git a/examples/flows/end-to-end-test-yaml/input_schema.yaml b/examples/flows/end-to-end-test-yaml/input_schema.yaml new file mode 100644 index 0000000..21ace87 --- /dev/null +++ b/examples/flows/end-to-end-test-yaml/input_schema.yaml @@ -0,0 +1,55 @@ +additionalProperties: false +required: + - source + - destination + - timer_url +type: object +properties: + source: + title: Source + description: The collection which serves as the source of the Move operation + type: object + format: globus-collection + required: + - id + - path + additionalProperties: false + properties: + id: + title: Source Collection ID + type: string + format: uuid + path: + title: Source Collection Path + description: The path on the source collection for the data + type: string + destination: + title: Destination + description: The collection which serves as the destination for the Move operation + type: object + format: globus-collection + required: + - id + - path + additionalProperties: false + properties: + id: + title: Destination Collection ID + type: string + format: uuid + path: + title: Destination Collection Path + description: The path on the destination collection where the data will be stored + type: string + transfer_label: + default: Test all action providers + description: A label placed on the Transfer operation + type: string + delete_label: + default: Test file deletion + description: A label placed on the Delete operation + type: string + timer_url: + default: + description: The timercheck.io url that nagios is checking for freshness + type: string diff --git a/examples/flows/end-to-end-test-yaml/prod_input.yaml b/examples/flows/end-to-end-test-yaml/prod_input.yaml new file mode 100644 index 0000000..8e5784f --- /dev/null +++ b/examples/flows/end-to-end-test-yaml/prod_input.yaml @@ -0,0 +1,10 @@ +source: + id: ddb59aef-6d04-11e5-ba46-22000b92c6ec + path: /share/godata/file1.txt +destination: + id: ddb59af0-6d04-11e5-ba46-22000b92c6ec + path: /~/automated-testing/transfer-successful.txt +destination_path_parent_dir: /~/automated-testing/ +timer_url: https://timercheck.io/automate-80932743889269524169320866183160580494/300 +transfer_label: "end-to-end-test-transfer" +delete_label: "end-to-end-test-clean-up" diff --git a/examples/flows/end-to-end-test-yaml/sandbox_input.yaml b/examples/flows/end-to-end-test-yaml/sandbox_input.yaml new file mode 100644 index 0000000..c294c35 --- /dev/null +++ b/examples/flows/end-to-end-test-yaml/sandbox_input.yaml @@ -0,0 +1,10 @@ +source: + id: 25fbda1e-5ae6-11e5-b577-22000b47140e + path: /share/godata/file1.txt +destination: + id: b88da93e-4186-11e7-8e40-22000b508383 + path: /~/automated-testing/transfer-successful.txt +destination_path_parent_dir: /~/automated-testing/ +timer_url: https://timercheck.io/automate-80932743889269524169320866183160580494/120 +transfer_label: "transfer1" +delete_label: "delete1" diff --git a/examples/flows/end-to-end-test-yaml/test_input.yaml b/examples/flows/end-to-end-test-yaml/test_input.yaml new file mode 100644 index 0000000..c294c35 --- /dev/null +++ b/examples/flows/end-to-end-test-yaml/test_input.yaml @@ -0,0 +1,10 @@ +source: + id: 25fbda1e-5ae6-11e5-b577-22000b47140e + path: /share/godata/file1.txt +destination: + id: b88da93e-4186-11e7-8e40-22000b508383 + path: /~/automated-testing/transfer-successful.txt +destination_path_parent_dir: /~/automated-testing/ +timer_url: https://timercheck.io/automate-80932743889269524169320866183160580494/120 +transfer_label: "transfer1" +delete_label: "delete1" diff --git a/examples/flows/transfer-mv-yaml/definition.yaml b/examples/flows/transfer-mv-yaml/definition.yaml index 605fc46..d97475d 100644 --- a/examples/flows/transfer-mv-yaml/definition.yaml +++ b/examples/flows/transfer-mv-yaml/definition.yaml @@ -24,7 +24,7 @@ States: CheckTransfer: Comment: >- Examine the status of the Transfer Action. If it failed, branch to a state - the reports the error + that reports the error Type: Choice Choices: - Variable: $.TransferResult.status @@ -49,7 +49,7 @@ States: CheckDelete: Comment: >- Examine the status of the Delete Action. If it failed, branch to a state - the reports the error + that reports the error Type: Choice Choices: - Variable: $.DeleteResult.status diff --git a/examples/flows/transfer-mv/definition.json b/examples/flows/transfer-mv/definition.json index 21ed841..5be1cf0 100644 --- a/examples/flows/transfer-mv/definition.json +++ b/examples/flows/transfer-mv/definition.json @@ -24,7 +24,7 @@ "Next": "CheckTransfer" }, "CheckTransfer": { - "Comment": "Examine the status of the Transfer Action. If it failed, branch to a state the reports the error", + "Comment": "Examine the status of the Transfer Action. If it failed, branch to a state that reports the error", "Type": "Choice", "Choices": [ { @@ -51,7 +51,7 @@ "Next": "CheckDelete" }, "CheckDelete": { - "Comment": "Examine the status of the Delete Action. If it failed, branch to a state the reports the error", + "Comment": "Examine the status of the Delete Action. If it failed, branch to a state that reports the error", "Type": "Choice", "Choices": [ { diff --git a/globus_automate_client/action_client.py b/globus_automate_client/action_client.py index efe342b..c42f221 100644 --- a/globus_automate_client/action_client.py +++ b/globus_automate_client/action_client.py @@ -1,5 +1,5 @@ import uuid -from typing import Any, Dict, Iterable, Mapping, Optional, Type, TypeVar, Union +from typing import Any, Dict, Iterable, List, Mapping, Optional, Type, TypeVar, Union from urllib.parse import quote from globus_sdk import BaseClient, GlobusHTTPResponse @@ -52,6 +52,7 @@ def run( manage_by: Optional[Iterable[str]] = None, monitor_by: Optional[Iterable[str]] = None, label: Optional[str] = None, + tags: Optional[List[str]] = None, force_path: Optional[str] = None, **kwargs, ) -> GlobusHTTPResponse: @@ -74,6 +75,7 @@ def run( :param force_path: A URL to use for running this action, ignoring any previous configuration :param label: Set a label for the Action that is run. + :param tags: A list of tags to associate with the Run. :param run_monitors: May be used as an alias for ``monitor_by`` :param run_managers: May be used as an alias for ``manage_by`` """ @@ -89,6 +91,7 @@ def run( "monitor_by": merge_keywords(monitor_by, kwargs, "run_monitors"), "manage_by": merge_keywords(manage_by, kwargs, "run_managers"), "label": label, + "tags": tags, } # Remove None items from the temp_body data = {k: v for k, v in body.items() if v is not None} diff --git a/globus_automate_client/cli/callbacks.py b/globus_automate_client/cli/callbacks.py index 3803270..7450f9d 100644 --- a/globus_automate_client/cli/callbacks.py +++ b/globus_automate_client/cli/callbacks.py @@ -3,7 +3,7 @@ import pathlib import re from errno import ENAMETOOLONG -from typing import AbstractSet, List, Optional +from typing import AbstractSet, Callable, List, Optional, cast from urllib.parse import urlparse import typer @@ -77,32 +77,18 @@ def _base_principal_validator( def principal_validator(principals: List[str]) -> List[str]: - """ - A principal ID needs to be a valid UUID. - """ - return _base_principal_validator(principals) + """A principal ID needs to be a valid UUID.""" + return _base_principal_validator(cast(List[str], principals)) -def principal_or_all_authenticated_users_validator(principals: List[str]) -> List[str]: - """ - Certain fields expect values to be a valid Globus Auth UUID or one of a set - of special strings that are meaningful in the context of authentication. - This callback is a specialized form of the principal_validator where the - special value of 'all_authenticated_users' is accepted. - """ - return _base_principal_validator( - principals, special_vals={"all_authenticated_users"} - ) +def custom_principal_validator(special_values: AbstractSet[str]) -> Callable: + """A principal ID needs to be a valid UUID.""" -def principal_or_public_validator(principals: List[str]) -> List[str]: - """ - Certain fields expect values to be a valid Globus Auth UUID or one of a set - of special strings that are meaningful in the context of authentication. - This callback is a specialized form of the principal_validator where the - special value of 'public' is accepted. - """ - return _base_principal_validator(principals, special_vals={"public"}) + def wrapper(principals: List[str]) -> List[str]: + return _base_principal_validator(principals, special_vals=special_values) + + return wrapper def flows_endpoint_envvar_callback(default_value: str) -> str: diff --git a/globus_automate_client/cli/flows.py b/globus_automate_client/cli/flows.py index 7970a0a..a8e016a 100644 --- a/globus_automate_client/cli/flows.py +++ b/globus_automate_client/cli/flows.py @@ -1,4 +1,5 @@ import functools +import textwrap import uuid from typing import List, Optional @@ -6,10 +7,9 @@ from globus_automate_client.cli.auth import CLIENT_ID from globus_automate_client.cli.callbacks import ( + custom_principal_validator, flow_input_validator, input_validator, - principal_or_all_authenticated_users_validator, - principal_or_public_validator, principal_validator, url_validator_callback, ) @@ -36,7 +36,6 @@ FlowListDisplayFields, LogCompletionDetector, RequestRunner, - RunEnumerateDisplayFields, RunListDisplayFields, RunLogDisplayFields, ) @@ -58,6 +57,12 @@ ) +def dedent(text: str) -> str: + """Dedent help text, so it wraps neatly on the command line.""" + + return textwrap.dedent(text).strip() + + @app.callback() def flows(): """ @@ -110,18 +115,18 @@ def flow_deploy( + " The special value of 'public' may be used to " "indicate that any user can view this Flow. [repeatable]" ), - callback=principal_or_public_validator, + callback=custom_principal_validator({"public"}), hidden=False, ), # viewer and visible_to are aliases for the full flow_viewer viewer: List[str] = typer.Option( None, - callback=principal_or_public_validator, + callback=custom_principal_validator({"public"}), hidden=True, ), visible_to: List[str] = typer.Option( None, - callback=principal_or_public_validator, + callback=custom_principal_validator({"public"}), hidden=True, ), flow_starter: List[str] = typer.Option( @@ -133,14 +138,18 @@ def flow_deploy( "'all_authenticated_users' may be used to indicate that any authenticated user " "can invoke this flow. [repeatable]" ), - callback=principal_or_all_authenticated_users_validator, + callback=custom_principal_validator({"all_authenticated_users"}), ), # starter and runnable_by are aliases for the full flow_starter starter: List[str] = typer.Option( - None, callback=principal_or_all_authenticated_users_validator, hidden=True + None, + callback=custom_principal_validator({"all_authenticated_users"}), + hidden=True, ), runnable_by: List[str] = typer.Option( - None, callback=principal_or_all_authenticated_users_validator, hidden=True + None, + callback=custom_principal_validator({"all_authenticated_users"}), + hidden=True, ), flow_administrator: List[str] = typer.Option( None, @@ -186,6 +195,9 @@ def flow_deploy( fc = create_flows_client(CLIENT_ID, flows_endpoint) flow_dict = process_input(definition) input_schema_dict = process_input(input_schema) + if input_schema_dict is None: + # If no input schema is provided, default to a no-op schema. + input_schema_dict = {} method = functools.partial( fc.deploy_flow, @@ -260,13 +272,13 @@ def flow_update( + _principal_description + "The special value of 'public' may be used to " "indicate that any user can view this Flow. [repeatable]", - callback=principal_or_public_validator, + callback=custom_principal_validator({"public"}), ), viewer: List[str] = typer.Option( - None, callback=principal_or_public_validator, hidden=True + None, callback=custom_principal_validator({"public"}), hidden=True ), visible_to: List[str] = typer.Option( - None, callback=principal_or_public_validator, hidden=True + None, callback=custom_principal_validator({"public"}), hidden=True ), flow_starter: List[str] = typer.Option( None, @@ -275,13 +287,17 @@ def flow_update( + " The special value of " "'all_authenticated_users' may be used to indicate that any " "authenticated user can invoke this flow. [repeatable]", - callback=principal_or_all_authenticated_users_validator, + callback=custom_principal_validator({"all_authenticated_users"}), ), starter: List[str] = typer.Option( - None, callback=principal_or_all_authenticated_users_validator, hidden=True + None, + callback=custom_principal_validator({"all_authenticated_users"}), + hidden=True, ), runnable_by: List[str] = typer.Option( - None, callback=principal_or_all_authenticated_users_validator, hidden=True + None, + callback=custom_principal_validator({"all_authenticated_users"}), + hidden=True, ), flow_administrator: List[str] = typer.Option( None, @@ -603,6 +619,18 @@ def flow_run( "-l", help="Label to mark this run.", ), + tags: Optional[List[str]] = typer.Option( + None, + "--tag", + help=dedent( + """ + A tag to associate with this Run. + + This option can be used multiple times. + The full collection of tags will associated with the Run. + """ + ), + ), watch: bool = typer.Option( False, "--watch", @@ -647,6 +675,7 @@ def flow_run( dry_run=dry_run, monitor_by=monitor_by, manage_by=manage_by, + tags=tags, ) with live_content: result = RequestRunner( @@ -790,8 +819,8 @@ def flow_actions_list( @app.command("run-status") def flow_action_status( action_id: str = typer.Argument(...), - flow_id: str = typer.Option( - None, + flow_id: uuid.UUID = typer.Option( + ..., help="The ID for the Flow which triggered the Action.", ), flow_scope: str = typer.Option( @@ -1144,44 +1173,234 @@ def flow_action_enumerate( format=output_format, verbose=verbose, watch=watch, - fields=RunEnumerateDisplayFields, + fields=RunListDisplayFields, ).run_and_render() @app.command("action-update") @app.command("run-update") -def flow_action_update( - action_id: str = typer.Argument(...), - run_manager: Optional[List[str]] = typer.Option( +def update_run( + run_id: str = typer.Argument(...), + run_managers: Optional[List[str]] = typer.Option( None, + "--run-manager", help="A principal which may change the execution of the Run." + _principal_description + + " Specify an empty string once to erase all Run managers." + " [repeatable]", - callback=principal_validator, + callback=custom_principal_validator({""}), ), - run_monitor: Optional[List[str]] = typer.Option( + run_monitors: Optional[List[str]] = typer.Option( None, + "--run-monitor", help="A principal which may monitor the execution of the Run." + _principal_description + " [repeatable]", - callback=principal_validator, + callback=custom_principal_validator({""}), + ), + tags: Optional[List[str]] = typer.Option( + None, + "--tag", + help=( + "A tag to associate with the Run." + " If specified, the existing tags on the Run will be replaced" + " with the list of tags specified here." + " Specify an empty string once to erase all tags." + " [repeatable]" + ), + ), + label: Optional[str] = typer.Option( + None, + help="A label to associate with the Run.", ), flows_endpoint: str = flows_env_var_option, verbose: bool = verbosity_option, output_format: OutputFormat = output_format_option, ): """ - Update a Run on the Flows service + Update a Run on the Flows service. """ - run_manager = run_manager if run_manager else None - run_monitor = run_monitor if run_monitor else None + + # Special cases: + # * If the user specifies a single empty string, replace [""] with [] + # so all values currently set on the Run will be erased. + # * If the user specifies nothing, replace the default empty list with None + # to prevent erasure of the values currently set on the Run. + run_managers = [] if run_managers == [""] else (run_managers or None) + run_monitors = [] if run_monitors == [""] else (run_monitors or None) + tags = [] if tags and list(tags) == [""] else (tags or None) + fc = create_flows_client(CLIENT_ID, flows_endpoint, RUN_STATUS_SCOPE) RequestRunner( functools.partial( fc.flow_action_update, - action_id, - run_managers=run_manager, - run_monitors=run_monitor, + run_id, + label=label, + run_managers=run_managers, + run_monitors=run_monitors, + tags=tags, + ), + format=output_format, + verbose=verbose, + ).run_and_render() + + +@app.command("batch-run-update") +def update_runs( + run_ids: List[str] = typer.Argument(...), + # + # Run manager parameters + set_run_managers: Optional[List[str]] = typer.Option( + None, + "--set-run-manager", + help="Set a principal on affected Runs that can change the Run execution.", + callback=custom_principal_validator({""}), + ), + add_run_managers: Optional[List[str]] = typer.Option( + None, + "--add-run-manager", + help="Add a principal to affected Runs that can change the Run execution.", + callback=custom_principal_validator({""}), + ), + remove_run_managers: Optional[List[str]] = typer.Option( + None, + "--remove-run-manager", + help="Remove a principal from affected Runs that can change the Run execution.", + callback=custom_principal_validator({""}), + ), + # + # Run monitor parameters + set_run_monitors: Optional[List[str]] = typer.Option( + None, + "--set-run-monitor", + help="Set a principal on affected Runs that can monitor Run execution.", + callback=custom_principal_validator({""}), + ), + add_run_monitors: Optional[List[str]] = typer.Option( + None, + "--add-run-monitor", + help="Add a principal to affected Runs that can monitor Run execution.", + callback=custom_principal_validator({""}), + ), + remove_run_monitors: Optional[List[str]] = typer.Option( + None, + "--remove-run-monitor", + help="Remove a principal from affected Runs that can monitor Run execution.", + callback=custom_principal_validator({""}), + ), + # + # Tag parameters + set_tags: Optional[List[str]] = typer.Option( + None, + "--set-tag", + help="A tag to set on the specified Runs.", + ), + add_tags: Optional[List[str]] = typer.Option( + None, + "--add-tag", + help="A tag to add to the affected Runs.", + ), + remove_tags: Optional[List[str]] = typer.Option( + None, + "--remove-tag", + help="A tag to remove from the affected Runs.", + ), + status: Optional[str] = typer.Option( + None, + help=dedent( + """ + Set the status of the affected Runs. + + Currently, "cancel" is the only valid value. + """ + ), + ), + flows_endpoint: str = flows_env_var_option, + verbose: bool = verbosity_option, + output_format: OutputFormat = output_format_option, +): + """ + Update metadata and permissions on one or more Runs. + + \b + Modifying lists of values + ========================= + + Most options support set, add, and remove operations. + + The "add" option variants will add the specified value + to whatever is set on each affected Run. + For example, if one Run has a "star" tag and another has a "circle" tag, + `--add-tag square` will result in a Run with "star" and "square" tags, + and the other Run will have "circle" and "square" tags. + + The "remove" option variants will remove the specified value + from whatever is set on each affected Run. + There will not be an error if the value is not set on a Run. + For example, if one Run has a "star" tag and another has a "circle" tag, + `--remove-tag star` will result in a Run with no tags + while the other still has a "circle" tag. + + The "set" option variants will overwrite the metadata and permissions + currently set on all affected Runs. + For example, `--set-tag example` will standardize all affected Runs + so that they have just one tag: "example". + + To remove all values on all affected Runs, + use the "set" variant of an option with an empty string. + For example, to erase all Run monitors, use `--set-run-monitors ""`. + + All options with "set", "add", and "remove" variants can be used multiple times. + However, only one variation of an option can be specified at a time. + For example, `--set-tag` and `--add-tag` cannot be combined in the same command, + and `--set-run-manager` and `--add-run-manager` cannot be combined. + It is fine to combine `--add-tag` and `--remove-run-manager`. + + \b + Modifying roles + =============== + + Run managers and monitors must be specified in one of these forms: + + \b + * A user's Globus Auth username + * A user's identity UUID in the form urn:globus:auth:identity: + * A group's identity UUID in the form urn:globus:groups:id: + """ + + # Until typing.Literal is available on all supported Python versions, + # `status` must be checked in-code. + if status is not None and status != "cancel": + raise ValueError("'cancel' is the only valid --status value.") + + # Special cases: + # * If the user specifies a single empty string, replace [""] with [] + # so all values currently set on the Run will be erased. + # * If the user specifies nothing, replace the default empty list with None + # to prevent erasure of the values currently set on the Run. + set_run_managers = [] if set_run_managers == [""] else (set_run_managers or None) + set_run_monitors = [] if set_run_monitors == [""] else (set_run_monitors or None) + set_tags = [] if set_tags and list(set_tags) == [""] else (set_tags or None) + + fc = create_flows_client(CLIENT_ID, flows_endpoint, RUN_STATUS_SCOPE) + RequestRunner( + functools.partial( + fc.update_runs, + run_ids=run_ids, + # Run managers + add_run_managers=add_run_managers or None, + remove_run_managers=remove_run_managers or None, + set_run_managers=set_run_managers, + # Run monitors + add_run_monitors=add_run_monitors or None, + remove_run_monitors=remove_run_monitors or None, + set_run_monitors=set_run_monitors, + # Tags + add_tags=add_tags or None, + remove_tags=remove_tags or None, + set_tags=set_tags, + # Status + status=status, ), format=output_format, verbose=verbose, diff --git a/globus_automate_client/cli/rich_helpers.py b/globus_automate_client/cli/rich_helpers.py index f262f54..a8a35be 100644 --- a/globus_automate_client/cli/rich_helpers.py +++ b/globus_automate_client/cli/rich_helpers.py @@ -128,17 +128,8 @@ class RunListDisplayFields(DisplayFields): Field("created_by", "", humanize_auth_urn), Field("flow_id", ""), ] - path_to_data_list = "actions" - prehook = functools.partial(identity_to_user, "created_by") - - -class RunEnumerateDisplayFields(RunListDisplayFields): - """ - This object defines the fields and display style of a Run enumeration into a - table. - """ - path_to_data_list = "runs" + prehook = functools.partial(identity_to_user, "created_by") class FlowListDisplayFields(DisplayFields): diff --git a/globus_automate_client/flows_client.py b/globus_automate_client/flows_client.py index 6c2deae..952398b 100644 --- a/globus_automate_client/flows_client.py +++ b/globus_automate_client/flows_client.py @@ -7,8 +7,10 @@ Callable, Dict, Iterable, + List, Mapping, Optional, + Sequence, Set, Type, TypeVar, @@ -43,22 +45,13 @@ "staging": "https://staging.flows.automate.globus.org", } -MANAGE_FLOWS_SCOPE = ( - "https://auth.globus.org/scopes/eec9b274-0c81-4334-bdc2-54e90e689b9a/manage_flows" -) -VIEW_FLOWS_SCOPE = ( - "https://auth.globus.org/scopes/eec9b274-0c81-4334-bdc2-54e90e689b9a/view_flows" -) -RUN_FLOWS_SCOPE = ( - "https://auth.globus.org/scopes/eec9b274-0c81-4334-bdc2-54e90e689b9a/run" -) -RUN_STATUS_SCOPE = ( - "https://auth.globus.org/scopes/eec9b274-0c81-4334-bdc2-54e90e689b9a/run_status" -) -RUN_MANAGE_SCOPE = ( - "https://auth.globus.org/scopes/eec9b274-0c81-4334-bdc2-54e90e689b9a/run_manage" -) -NULL_SCOPE = "https://auth.globus.org/scopes/eec9b274-0c81-4334-bdc2-54e90e689b9a/null" +FLOWS_CLIENT_ID = "eec9b274-0c81-4334-bdc2-54e90e689b9a" +MANAGE_FLOWS_SCOPE = f"https://auth.globus.org/scopes/{FLOWS_CLIENT_ID}/manage_flows" +VIEW_FLOWS_SCOPE = f"https://auth.globus.org/scopes/{FLOWS_CLIENT_ID}/view_flows" +RUN_FLOWS_SCOPE = f"https://auth.globus.org/scopes/{FLOWS_CLIENT_ID}/run" +RUN_STATUS_SCOPE = f"https://auth.globus.org/scopes/{FLOWS_CLIENT_ID}/run_status" +RUN_MANAGE_SCOPE = f"https://auth.globus.org/scopes/{FLOWS_CLIENT_ID}/run_manage" +NULL_SCOPE = f"https://auth.globus.org/scopes/{FLOWS_CLIENT_ID}/null" ALL_FLOW_SCOPES = ( MANAGE_FLOWS_SCOPE, @@ -114,19 +107,21 @@ def _all_vals_for_keys( def validate_flow_definition(flow_definition: Mapping[str, Any]) -> None: - """Perform local, JSONSchema based validation of a Flow definition. This is validation - on the basic structure of your Flow definition.such as required fields / properties - for the various state types and the overall structure of the Flow. This schema based - validation *does not* do any validation of input values or parameters passed to - Actions as those Actions define their own schemas and the Flow may generate or - compute values to these Actions and thus static, schema based validation cannot - determine if the Action parameter values generated during execution are correct. + """Perform local, JSONSchema based validation of a Flow definition. + + This is validation on the basic structure of your Flow definition such as required + fields / properties for the various state types and the overall structure of the + Flow. This schema based validation *does not* do any validation of input values or + parameters passed to Actions as those Actions define their own schemas and the Flow + may generate or compute values to these Actions and thus static, schema based + validation cannot determine if the Action parameter values generated during + execution are correct. The input is the dictionary containing the flow definition. If the flow passes validation, no value is returned. If validation errors are found, - a FlowValidationError exception will be raised containing a string message describing - the error(s) encountered. + a FlowValidationError exception will be raised containing a string message + describing the error(s) encountered. """ schema_path = Path(__file__).parent / "flows_schema.json" with schema_path.open() as sf: @@ -273,7 +268,7 @@ def deploy_flow( :param flow_definition: A mapping corresponding to a Globus Flows definition. - :param title: A simple, human readable title for the deployed Flow + :param title: A simple, human-readable title for the deployed Flow :param subtitle: A longer, more verbose title for the deployed Flow @@ -367,7 +362,7 @@ def update_flow( :param flow_definition: A mapping corresponding to a Globus Flows definition - :param title: A simple, human readable title for the deployed Flow + :param title: A simple, human-readable title for the deployed Flow :param subtitle: A longer, more verbose title for the deployed Flow @@ -430,8 +425,7 @@ def get_flow(self, flow_id: str, **kwargs) -> GlobusHTTPResponse: """ Retrieve a deployed Flow's definition and metadata - :param flow_id: The UUID identifying the Flow for which to retrieve - details + :param flow_id: The UUID identifying the Flow for which to retrieve details """ return self.get(f"/flows/{quote(flow_id)}", **kwargs) @@ -449,14 +443,15 @@ def list_flows( """Display all deployed Flows for which you have the selected role(s) :param roles: - .. deprecated:: 0.12 - Use ``role`` instead + .. deprecated:: 0.12 + Use ``role`` instead See description for ``role`` parameter. Providing multiple roles behaves as if only a single ``role`` value is provided and displays the equivalent of the most permissive role. - :param role: A role value specifying the minimum role-level permission which will + :param role: + A role value specifying the minimum role-level permission which will be displayed based on the follow precedence of role values: - flow_viewer @@ -465,8 +460,8 @@ def list_flows( - flow_owner Thus, if, for example, ``flow_starter`` is specified, flows for which the - user has the ``flow_starter``, ``flow_administrator`` or ``flow_owner`` roles - will be returned. + user has the ``flow_starter``, ``flow_administrator`` or ``flow_owner`` + roles will be returned. :param marker: A pagination_token indicating the page of results to return and how many entries to return. This is created by the Flows @@ -562,6 +557,7 @@ def run_flow( run_monitors: Optional[Iterable[str]] = None, dry_run: bool = False, label: Optional[str] = None, + tags: Optional[List[str]] = None, **kwargs, ) -> GlobusHTTPResponse: """ @@ -569,7 +565,7 @@ def run_flow( :param flow_id: The UUID identifying the Flow to run - :param flow_scope: The scope associated with the Flow ``flow_id``. If + :param flow_scope: The scope associated with the Flow ``flow_id``. If not provided, the SDK will attempt to perform an introspection on the Flow to determine its scope automatically @@ -588,9 +584,11 @@ def run_flow( :param label: An optional label which can be used to identify this run + :param tags: Tags that will be associated with this Run. + :param kwargs: Any additional kwargs passed into this method are passed onto the Globus BaseClient. If there exists an "authorizer" keyword - argument, that gets used to run the Flow operation. Otherwise the + argument, that gets used to run the Flow operation. Otherwise, the authorizer_callback defined for the FlowsClient will be used. :param dry_run: Set to ``True`` to test what will happen if the Flow is run @@ -616,6 +614,7 @@ def run_flow( monitor_by=run_monitors, force_path=path, label=label, + tags=tags, **kwargs, ) else: @@ -624,6 +623,7 @@ def run_flow( manage_by=run_managers, monitor_by=run_monitors, label=label, + tags=tags, **kwargs, ) @@ -639,12 +639,11 @@ def flow_action_status( not provided, the SDK will attempt to perform an introspection on the Flow to determine its scope automatically - :param flow_action_id: The ID specifying the Action for which's status - we want to query + :param flow_action_id: The ID specifying which Action's status to query :param kwargs: Any additional kwargs passed into this method are passed onto the Globus BaseClient. If there exists an "authorizer" keyword - argument, that gets used to run the Flow operation. Otherwise the + argument, that gets used to run the Flow operation. Otherwise, the authorizer_callback defined for the FlowsClient will be used. """ authorizer = self._get_authorizer_for_flow(flow_id, flow_scope, kwargs) @@ -669,7 +668,7 @@ def flow_action_resume( :param kwargs: Any additional kwargs passed into this method are passed onto the Globus BaseClient. If there exists an "authorizer" keyword - argument, that gets used to run the Flow operation. Otherwise the + argument, that gets used to run the Flow operation. Otherwise, the authorizer_callback defined for the FlowsClient will be used. """ authorizer = self._get_authorizer_for_flow(flow_id, flow_scope, kwargs) @@ -693,7 +692,7 @@ def flow_action_release( :param kwargs: Any additional kwargs passed into this method are passed onto the Globus BaseClient. If there exists an "authorizer" keyword - argument, that gets used to run the Flow operation. Otherwise the + argument, that gets used to run the Flow operation. Otherwise, the authorizer_callback defined for the FlowsClient will be used. """ authorizer = self._get_authorizer_for_flow(flow_id, flow_scope, kwargs) @@ -705,7 +704,7 @@ def flow_action_cancel( self, flow_id: str, flow_scope: Optional[str], flow_action_id: str, **kwargs ) -> GlobusHTTPResponse: """ - Cancel the excution of an Action that was launched by a Flow + Cancel the execution of an Action that was launched by a Flow :param flow_id: The UUID identifying the Flow which launched the Action @@ -717,7 +716,7 @@ def flow_action_cancel( :param kwargs: Any additional kwargs passed into this method are passed onto the Globus BaseClient. If there exists an "authorizer" keyword - argument, that gets used to run the Flow operation. Otherwise the + argument, that gets used to run the Flow operation. Otherwise, the authorizer_callback defined for the FlowsClient will be used. """ authorizer = self._get_authorizer_for_flow(flow_id, flow_scope, kwargs) @@ -757,8 +756,8 @@ def enumerate_runs( if only a single ``role`` value is provided and displays the equivalent of the most permissive role. - :param role: A role value specifying the minimum role-level permission on the runs which will - be returned based on the follow precedence of role values: + :param role: A role value specifying the minimum role-level permission on the + runs which will be returned based on the follow precedence of role values: - run_monitor - run_manager @@ -855,18 +854,19 @@ def list_flow_runs( role: Optional[str] = None, **kwargs, ) -> GlobusHTTPResponse: - """List all Runs for a particular Flow. If no flow_id is provided, all runs for all - Flows will be returned. + """List all Runs for a particular Flow. + + If no flow_id is provided, all runs for all Flows will be returned. - :param flow_id: The UUID identifying the Flow which launched the Run. If not - provided, all runs will be returned regardless of which Flow was used to start - the Run (equivalent to ``enumerate_runs``). + :param flow_id: The UUID identifying the Flow which launched the Run. + If not provided, all runs will be returned regardless of which Flow was + used to start the Run (equivalent to ``enumerate_runs``). :param flow_scope: The scope associated with the Flow ``flow_id``. If not provided, the SDK will attempt to perform an introspection on the Flow to determine its scope automatically - :param statuses: The same as in ``enumerate_runs``. + :param statuses: The same as in ``enumerate_runs``. :param roles: .. deprecated:: 0.12 @@ -874,19 +874,19 @@ def list_flow_runs( The same as in ``enumerate_runs``. - :param marker: The same as in ``enumerate_runs``. + :param marker: The same as in ``enumerate_runs``. - :param per_page: The same as in ``enumerate_runs``. + :param per_page: The same as in ``enumerate_runs``. - :param filters: The same as in ``enumerate_runs``. + :param filters: The same as in ``enumerate_runs``. - :param orderings: The same as in ``enumerate_runs``. + :param orderings: The same as in ``enumerate_runs``. - :param role: The same as in ``enumerate_runs``. + :param role: The same as in ``enumerate_runs``. :param kwargs: Any additional kwargs passed into this method are passed onto the Globus BaseClient. If there exists an "authorizer" keyword - argument, that gets used to run the Flow operation. Otherwise the + argument, that gets used to run the Flow operation. Otherwise, the authorizer_callback defined for the FlowsClient will be used. """ @@ -937,13 +937,15 @@ def list_flow_runs( authorizer = self._get_authorizer_for_flow(flow_id, flow_scope, kwargs) with self.use_temporary_authorizer(authorizer): - return self.get(f"/flows/{flow_id}/actions", query_params=params, **kwargs) + return self.get(f"/flows/{flow_id}/runs", query_params=params, **kwargs) def flow_action_update( self, action_id: str, - run_managers: Optional[Iterable[str]] = None, - run_monitors: Optional[Iterable[str]] = None, + run_managers: Optional[Sequence[str]] = None, + run_monitors: Optional[Sequence[str]] = None, + tags: Optional[Sequence[str]] = None, + label: Optional[str] = None, **kwargs, ) -> GlobusHTTPResponse: """ @@ -961,19 +963,192 @@ def flow_action_update( :param kwargs: Any additional kwargs passed into this method are passed onto the Globus BaseClient. If there exists an "authorizer" keyword - argument, that gets used to run the Flow operation. Otherwise the + argument, that gets used to run the Flow operation. Otherwise, the authorizer_callback defined for the FlowsClient will be used. + + :param tags: + A list of tags to apply to the Run. + + :param label: + A label to apply to the Run. """ + payload = {} if run_managers is not None: payload["run_managers"] = run_managers if run_monitors is not None: payload["run_monitors"] = run_monitors + if tags is not None: + payload["tags"] = tags + if label is not None: + payload["label"] = label authorizer = self._get_authorizer_for_flow("", RUN_MANAGE_SCOPE, kwargs) with self.use_temporary_authorizer(authorizer): return self.put(f"/runs/{action_id}", data=payload, **kwargs) + def update_runs( + self, + # Filters + run_ids: Iterable[str], + # Tags + add_tags: Optional[Iterable[str]] = None, + remove_tags: Optional[Iterable[str]] = None, + set_tags: Optional[Iterable[str]] = None, + # Run managers + add_run_managers: Optional[Iterable[str]] = None, + remove_run_managers: Optional[Iterable[str]] = None, + set_run_managers: Optional[Iterable[str]] = None, + # Run monitors + add_run_monitors: Optional[Iterable[str]] = None, + remove_run_monitors: Optional[Iterable[str]] = None, + set_run_monitors: Optional[Iterable[str]] = None, + # Status + status: Optional[str] = None, + **kwargs, + ) -> GlobusHTTPResponse: + """ + Update a Flow Action. + + :param run_ids: + A list of Run ID's to query. + + :param set_tags: + A list of tags to set on the specified Run ID's. + + If the list is empty, all tags will be deleted from the specified Run ID's. + + .. note:: + + The ``set_tags``, ``add_tags``, and ``remove_tags`` arguments + are mutually exclusive. + + :param add_tags: + A list of tags to add to each of the specified Run ID's. + + .. note:: + + The ``set_tags``, ``add_tags``, and ``remove_tags`` arguments + are mutually exclusive. + + :param remove_tags: + A list of tags to remove from each of the specified Run ID's. + + .. note:: + + The ``set_tags``, ``add_tags``, and ``remove_tags`` arguments + are mutually exclusive. + + :param set_run_managers: + A list of Globus Auth URN's to set on the specified Run ID's. + + If the list is empty, all Run managers will be deleted + from the specified Run ID's. + + .. note:: + + The ``set_run_managers``, ``add_run_managers``, and + ``remove_run_managers`` arguments are mutually exclusive. + + :param add_run_managers: + A list of Globus Auth URN's to add to each of the specified Run ID's. + + .. note:: + + The ``set_run_managers``, ``add_run_managers``, and + ``remove_run_managers`` arguments are mutually exclusive. + + :param remove_run_managers: + A list of Globus Auth URN's to remove from each of the specified Run ID's. + + .. note:: + + The ``set_run_managers``, ``add_run_managers``, and + ``remove_run_managers`` arguments are mutually exclusive. + + :param set_run_monitors: + A list of Globus Auth URN's to set on the specified Run ID's. + + If the list is empty, all Run monitors will be deleted + from the specified Run ID's. + + .. note:: + + The ``set_run_monitors``, ``add_run_monitors``, and + ``remove_run_monitors`` arguments are mutually exclusive. + + :param add_run_monitors: + A list of Globus Auth URN's to add to each of the specified Run ID's. + + .. note:: + + The ``set_run_monitors``, ``add_run_monitors``, and + ``remove_run_monitors`` arguments are mutually exclusive. + + :param remove_run_monitors: + A list of Globus Auth URN's to remove from each of the specified Run ID's. + + .. note:: + + The ``set_run_monitors``, ``add_run_monitors``, and + ``remove_run_monitors`` arguments are mutually exclusive. + + :param status: + A status to set for all specified Run ID's. + + :param kwargs: + Any additional keyword arguments passed to this method + are passed to the Globus BaseClient. + + If an "authorizer" keyword argument is passed, + it will be used to authorize the Flow operation. + Otherwise, the authorizer_callback defined for the FlowsClient will be used. + + :raises ValueError: + If more than one mutually-exclusive argument is provided. + For example, if ``set_tags`` and ``add_tags`` are both specified, + or if ``add_run_managers`` and ``remove_run_managers`` are both specified. + """ + + multi_fields = ("tags", "run_managers", "run_monitors") + multi_ops = ("add", "remove", "set") + + # Enforce mutual exclusivity of arguments. + for field in multi_fields: + values = ( + locals()[f"set_{field}"], + locals()[f"add_{field}"], + locals()[f"remove_{field}"], + ) + if sum(1 for value in values if value is not None) > 1: + raise ValueError( + f"`set_{field}`, `add_{field}`, and `remove_{field}`" + " are mutually exclusive. Only one can be used." + ) + + # Populate the JSON document to submit. + data: dict = { + "filters": { + "run_ids": list(run_ids), + }, + "set": {}, + "add": {}, + "remove": {}, + } + for field in multi_fields: + if locals()[f"add_{field}"] is not None: + data["add"][field] = locals()[f"add_{field}"] + if locals()[f"remove_{field}"] is not None: + data["remove"][field] = locals()[f"remove_{field}"] + if locals()[f"set_{field}"] is not None: + data["set"][field] = locals()[f"set_{field}"] + if status is not None: + data["set"]["status"] = status + + authorizer = self._get_authorizer_for_flow("", RUN_MANAGE_SCOPE, kwargs) + with self.use_temporary_authorizer(authorizer): + return self.post("/batch/runs", data=data, **kwargs) + def flow_action_log( self, flow_id: str, @@ -989,14 +1164,13 @@ def flow_action_log( Retrieve an Action's execution log history for an Action that was launched by a specific Flow. - :param flow_id: The UUID identifying the Flow which launched the Action + :param flow_id: The UUID identifying the Flow which launched the Action :param flow_scope: The scope associated with the Flow ``flow_id``. If not provided, the SDK will attempt to perform an introspection on the Flow to determine its scope automatically - :param flow_action_id: The ID specifying the Action for which's history - to query + :param flow_action_id: The ID specifying which Action's history to query :param limit: An integer specifying the maximum number of records for the Action's execution history to return. @@ -1012,7 +1186,7 @@ def flow_action_log( :param kwargs: Any additional kwargs passed into this method are passed onto the Globus BaseClient. If there exists an "authorizer" keyword - argument, that gets used to run the Flow operation. Otherwise the + argument, that gets used to run the Flow operation. Otherwise, the authorizer_callback defined for the FlowsClient will be used. """ authorizer = self._get_authorizer_for_flow(flow_id, flow_scope, kwargs) diff --git a/pyproject.toml b/pyproject.toml index 362f89c..3e0d90e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "globus-automate-client" -version = "0.13.1" +version = "0.14.0" description = "Client for the Globus Flows service" authors = [ "Jake Lewis ", diff --git a/tests/test_flows_client.py b/tests/test_flows_client.py index f066ba1..ac573de 100644 --- a/tests/test_flows_client.py +++ b/tests/test_flows_client.py @@ -772,7 +772,7 @@ def test_list_flow_runs_role_precedence( ): """Verify the *role* and *roles* precedence rules.""" - mocked_responses.add("GET", "https://flows.api.globus.org/flows/-/actions") + mocked_responses.add("GET", "https://flows.api.globus.org/flows/-/runs") fc.list_flow_runs( "-", role=role, @@ -808,7 +808,7 @@ def test_list_flow_runs_pagination_parameters( ): """Verify *marker* and *per_page* precedence rules.""" - mocked_responses.add("GET", "https://flows.api.globus.org/flows/-/actions") + mocked_responses.add("GET", "https://flows.api.globus.org/flows/-/runs") fc.list_flow_runs( "-", marker=marker, @@ -828,7 +828,7 @@ def test_list_flow_runs_pagination_parameters( def test_list_flow_runs_filters(fc, mocked_responses): """Verify that filters are applied to the query parameters.""" - mocked_responses.add("GET", "https://flows.api.globus.org/flows/-/actions") + mocked_responses.add("GET", "https://flows.api.globus.org/flows/-/runs") fc.list_flow_runs( "-", role="role", @@ -844,7 +844,7 @@ def test_list_flow_runs_filters(fc, mocked_responses): def test_list_flow_runs_orderings(fc, mocked_responses): """Verify that orderings are serialized as expected.""" - mocked_responses.add("GET", "https://flows.api.globus.org/flows/-/actions") + mocked_responses.add("GET", "https://flows.api.globus.org/flows/-/runs") fc.list_flow_runs( "-", orderings={"shape": "asc", "color": "DESC", "bogus": "bad"}, @@ -862,7 +862,7 @@ def test_list_flow_runs_orderings(fc, mocked_responses): def test_list_flow_runs_statuses(fc, mocked_responses): """Verify that orderings are serialized as expected.""" - mocked_responses.add("GET", "https://flows.api.globus.org/flows/-/actions") + mocked_responses.add("GET", "https://flows.api.globus.org/flows/-/runs") fc.list_flow_runs( "-", statuses=("SUCCEEDED", "FAILED"),