diff --git a/block-kit/README.md b/block-kit/README.md index 9bdf134..a35a6c0 100644 --- a/block-kit/README.md +++ b/block-kit/README.md @@ -62,3 +62,15 @@ Read the [docs](https://docs.slack.dev/block-kit/) to learn concepts behind thes - **[User](https://docs.slack.dev/reference/block-kit/block-elements/user-element)**: Renders as a mention of a user. [Implementation](./src/elements/user.py). - **[Usergroup](https://docs.slack.dev/reference/block-kit/block-elements/usergroup-element)**: Renders as a mention of a user group. [Implementation](./src/elements/usergroup.py). - **[Workflow button](https://docs.slack.dev/reference/block-kit/block-elements/workflow-button-element)**: Allows users to run a link trigger with customizable inputs. [Implementation](./src/elements/workflow_button.py). + +### Composition objects + +- **[Confirmation dialog](https://docs.slack.dev/reference/block-kit/composition-objects/confirmation-dialog-object)**: Defines a dialog that adds a confirmation step to interactive elements. [Implementation](./src/compositions/confirmation_dialog.py). +- **[Conversation filter](https://docs.slack.dev/reference/block-kit/composition-objects/conversation-filter-object)**: Defines a filter for the list of options in a conversation selector menu. [Implementation](./src/compositions/conversation_filter.py). +- **[Dispatch action configuration](https://docs.slack.dev/reference/block-kit/composition-objects/dispatch-action-configuration-object)**: Defines when a plain-text input element will return a `block_actions` interaction payload. [Implementation](./src/compositions/dispatch_action_configuration.py). +- **[Option group](https://docs.slack.dev/reference/block-kit/composition-objects/option-group-object)**: Defines a way to group options in a menu. [Implementation](./src/compositions/option_group.py). +- **[Option](https://docs.slack.dev/reference/block-kit/composition-objects/option-object)**: Defines a single item in a number of item selection elements. [Implementation](./src/compositions/option.py). +- **[Slack file](https://docs.slack.dev/reference/block-kit/composition-objects/slack-file-object)**: Defines an object containing Slack file information to be used in an image block or image element. [Implementation](./src/compositions/slack_file.py). +- **[Text](https://docs.slack.dev/reference/block-kit/composition-objects/text-object)**: Defines an object containing some text. [Implementation](./src/compositions/text.py). +- **[Trigger](https://docs.slack.dev/reference/block-kit/composition-objects/trigger-object)**: Defines an object containing trigger information. [Implementation](./src/compositions/trigger.py). +- **[Workflow](https://docs.slack.dev/reference/block-kit/composition-objects/workflow-object)**: Defines an object containing workflow information. [Implementation](./src/compositions/workflow.py). diff --git a/block-kit/src/compositions/__init__.py b/block-kit/src/compositions/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/block-kit/src/compositions/confirmation_dialog.py b/block-kit/src/compositions/confirmation_dialog.py new file mode 100644 index 0000000..54a3b91 --- /dev/null +++ b/block-kit/src/compositions/confirmation_dialog.py @@ -0,0 +1,39 @@ +from slack_sdk.models.blocks import ActionsBlock +from slack_sdk.models.blocks.basic_components import ( + ConfirmObject, + MarkdownTextObject, + PlainTextObject, +) +from slack_sdk.models.blocks.block_elements import ButtonElement + + +def example01() -> ActionsBlock: + """ + Defines a dialog that adds a confirmation step to interactive elements. + https://docs.slack.dev/reference/block-kit/composition-objects/confirmation-dialog-object/ + + An actions block with a button carrying a confirmation dialog. + """ + block = ActionsBlock( + elements=[ + ButtonElement( + text=PlainTextObject(text="Approve", emoji=True), + confirm=ConfirmObject( + title=PlainTextObject(text="Are you sure?"), + text=MarkdownTextObject( + text="Would you not prefer a good game of _chess_?" + ), + confirm=PlainTextObject(text="Do it"), + deny=PlainTextObject(text="Stop, I changed my mind!"), + ), + style="primary", + value="click_me_123", + ), + ButtonElement( + text=PlainTextObject(text="Deny", emoji=True), + style="danger", + value="click_me_123", + ), + ], + ) + return block diff --git a/block-kit/src/compositions/conversation_filter.py b/block-kit/src/compositions/conversation_filter.py new file mode 100644 index 0000000..11c2174 --- /dev/null +++ b/block-kit/src/compositions/conversation_filter.py @@ -0,0 +1,40 @@ +from slack_sdk.models.blocks import InputBlock +from slack_sdk.models.blocks.basic_components import PlainTextObject +from slack_sdk.models.blocks.block_elements import ( + ConversationFilter, + ConversationSelectElement, +) +from slack_sdk.models.views import View + + +def example01() -> View: + """ + Defines a filter for the list of options in a conversation selector menu. + https://docs.slack.dev/reference/block-kit/composition-objects/conversation-filter-object/ + + A modal view with a conversations select input carrying a conversation filter. + """ + view = View( + type="modal", + title=PlainTextObject(text="My App", emoji=True), + submit=PlainTextObject(text="Submit", emoji=True), + close=PlainTextObject(text="Cancel", emoji=True), + blocks=[ + InputBlock( + element=ConversationSelectElement( + placeholder=PlainTextObject( + text="Select a conversation", emoji=True + ), + filter=ConversationFilter( + include=["public", "mpim"], + exclude_bot_users=True, + ), + ), + label=PlainTextObject( + text="Choose the conversation to publish your result to:", + emoji=True, + ), + ), + ], + ) + return view diff --git a/block-kit/src/compositions/dispatch_action_configuration.py b/block-kit/src/compositions/dispatch_action_configuration.py new file mode 100644 index 0000000..31664b3 --- /dev/null +++ b/block-kit/src/compositions/dispatch_action_configuration.py @@ -0,0 +1,26 @@ +from slack_sdk.models.blocks import InputBlock +from slack_sdk.models.blocks.basic_components import ( + DispatchActionConfig, + PlainTextObject, +) +from slack_sdk.models.blocks.block_elements import PlainTextInputElement + + +def example01() -> InputBlock: + """ + Defines when a plain-text input element will return a block_actions interaction payload. + https://docs.slack.dev/reference/block-kit/composition-objects/dispatch-action-configuration-object/ + + An input block with a multiline plain-text input carrying a dispatch action configuration. + """ + block = InputBlock( + dispatch_action=True, + element=PlainTextInputElement( + multiline=True, + dispatch_action_config=DispatchActionConfig( + trigger_actions_on=["on_character_entered"], + ), + ), + label=PlainTextObject(text="This is a multiline plain-text input", emoji=True), + ) + return block diff --git a/block-kit/src/compositions/option.py b/block-kit/src/compositions/option.py new file mode 100644 index 0000000..a98cd1c --- /dev/null +++ b/block-kit/src/compositions/option.py @@ -0,0 +1,56 @@ +from slack_sdk.models.blocks import Block, DividerBlock, SectionBlock +from slack_sdk.models.blocks.basic_components import ( + MarkdownTextObject, + Option, + PlainTextObject, +) +from slack_sdk.models.blocks.block_elements import StaticSelectElement + + +def example01() -> Option: + """ + Defines a single item in a number of item selection elements. + https://docs.slack.dev/reference/block-kit/composition-objects/option-object/ + + A single option object. + """ + option = Option( + text=PlainTextObject(text="Save it", emoji=True), + value="value-2", + ) + return option + + +def example02() -> list[Block]: + """ + A static select menu element with several option objects. + """ + blocks: list[Block] = [ + SectionBlock( + text=MarkdownTextObject(text=":mag: Search results for *Cata*"), + ), + DividerBlock(), + SectionBlock( + text=MarkdownTextObject( + text="**\nUse Case Catalogue for the following departments/roles..." + ), + accessory=StaticSelectElement( + placeholder=PlainTextObject(text="Manage", emoji=True), + options=[ + Option( + text=PlainTextObject(text="Edit it", emoji=True), + value="value-0", + ), + Option( + text=PlainTextObject(text="Read it", emoji=True), + value="value-1", + ), + Option( + text=PlainTextObject(text="Save it", emoji=True), + value="value-2", + ), + ], + ), + ), + ] + return blocks diff --git a/block-kit/src/compositions/option_group.py b/block-kit/src/compositions/option_group.py new file mode 100644 index 0000000..ad2b242 --- /dev/null +++ b/block-kit/src/compositions/option_group.py @@ -0,0 +1,60 @@ +from slack_sdk.models.blocks import Block, DividerBlock, SectionBlock +from slack_sdk.models.blocks.basic_components import ( + MarkdownTextObject, + Option, + OptionGroup, + PlainTextObject, +) +from slack_sdk.models.blocks.block_elements import StaticSelectElement + + +def example01() -> list[Block]: + """ + Defines a way to group options in a menu. + https://docs.slack.dev/reference/block-kit/composition-objects/option-group-object/ + + A static select menu containing the option group object. + """ + blocks: list[Block] = [ + SectionBlock( + text=MarkdownTextObject(text=":mag: Search results for *Cata*"), + ), + DividerBlock(), + SectionBlock( + text=MarkdownTextObject( + text="**\nUse Case Catalogue for the following departments/roles..." + ), + accessory=StaticSelectElement( + placeholder=PlainTextObject(text="Manage", emoji=True), + option_groups=[ + OptionGroup( + label=PlainTextObject(text="Group 1"), + options=[ + Option( + text=PlainTextObject(text="*this is plain_text text*"), + value="value-0", + ), + Option( + text=PlainTextObject(text="*this is plain_text text*"), + value="value-1", + ), + Option( + text=PlainTextObject(text="*this is plain_text text*"), + value="value-2", + ), + ], + ), + OptionGroup( + label=PlainTextObject(text="Group 2"), + options=[ + Option( + text=PlainTextObject(text="*this is plain_text text*"), + value="value-3", + ), + ], + ), + ], + ), + ), + ] + return blocks diff --git a/block-kit/src/compositions/slack_file.py b/block-kit/src/compositions/slack_file.py new file mode 100644 index 0000000..61f2cfa --- /dev/null +++ b/block-kit/src/compositions/slack_file.py @@ -0,0 +1,33 @@ +from slack_sdk.models.blocks import ImageBlock +from slack_sdk.models.blocks.basic_components import PlainTextObject, SlackFile + + +def example01() -> ImageBlock: + """ + Defines an object containing Slack file information to be used in an image block or image element. + https://docs.slack.dev/reference/block-kit/composition-objects/slack-file-object/ + + An image block using slack_file with a url. + """ + block = ImageBlock( + title=PlainTextObject(text="Please enjoy this photo of a kitten"), + block_id="image4", + slack_file=SlackFile( + url="https://files.slack.com/files-pri/T0123456-F0123456/xyz.png" + ), + alt_text="An incredibly cute kitten.", + ) + return block + + +def example02() -> ImageBlock: + """ + An image block using slack_file with an id. + """ + block = ImageBlock( + title=PlainTextObject(text="Please enjoy this photo of a kitten"), + block_id="image4", + slack_file=SlackFile(id="F0123456"), + alt_text="An incredibly cute kitten.", + ) + return block diff --git a/block-kit/src/compositions/text.py b/block-kit/src/compositions/text.py new file mode 100644 index 0000000..39dfb34 --- /dev/null +++ b/block-kit/src/compositions/text.py @@ -0,0 +1,17 @@ +from slack_sdk.models.blocks import SectionBlock +from slack_sdk.models.blocks.basic_components import MarkdownTextObject + + +def example01() -> SectionBlock: + """ + Defines an object containing some text. + https://docs.slack.dev/reference/block-kit/composition-objects/text-object/ + + A section block containing a text object. + """ + block = SectionBlock( + text=MarkdownTextObject( + text="A message *with some bold text* and _some italicized text_." + ) + ) + return block diff --git a/block-kit/src/compositions/trigger.py b/block-kit/src/compositions/trigger.py new file mode 100644 index 0000000..8c49351 --- /dev/null +++ b/block-kit/src/compositions/trigger.py @@ -0,0 +1,42 @@ +from slack_sdk.models.blocks import SectionBlock +from slack_sdk.models.blocks.basic_components import ( + MarkdownTextObject, + PlainTextObject, + Workflow, + WorkflowTrigger, +) +from slack_sdk.models.blocks.block_elements import WorkflowButtonElement + + +def example01() -> SectionBlock: + """ + Defines an object containing trigger information. + https://docs.slack.dev/reference/block-kit/composition-objects/trigger-object/ + + A section block with a workflow button whose trigger carries customizable input parameters. + """ + block = SectionBlock( + text=MarkdownTextObject( + text="A message *with some bold text* and _some italicized text_." + ), + accessory=WorkflowButtonElement( + text=PlainTextObject(text="Run Workflow"), + action_id="workflowbutton123", + workflow=Workflow( + trigger=WorkflowTrigger( + url="https://slack.com/shortcuts/Ft0123ABC456/xyz...zyx", + customizable_input_parameters=[ + { + "name": "input_parameter_a", + "value": "Value for input param A", + }, + { + "name": "input_parameter_b", + "value": "Value for input param B", + }, + ], + ), + ), + ), + ) + return block diff --git a/block-kit/src/compositions/workflow.py b/block-kit/src/compositions/workflow.py new file mode 100644 index 0000000..b761a0f --- /dev/null +++ b/block-kit/src/compositions/workflow.py @@ -0,0 +1,42 @@ +from slack_sdk.models.blocks import SectionBlock +from slack_sdk.models.blocks.basic_components import ( + MarkdownTextObject, + PlainTextObject, + Workflow, + WorkflowTrigger, +) +from slack_sdk.models.blocks.block_elements import WorkflowButtonElement + + +def example01() -> SectionBlock: + """ + Defines an object containing workflow information. + https://docs.slack.dev/reference/block-kit/composition-objects/workflow-object/ + + A section block with a workflow button whose workflow carries a trigger. + """ + block = SectionBlock( + text=MarkdownTextObject( + text="A message *with some bold text* and _some italicized text_." + ), + accessory=WorkflowButtonElement( + text=PlainTextObject(text="Run Workflow"), + action_id="workflowbutton123", + workflow=Workflow( + trigger=WorkflowTrigger( + url="https://slack.com/shortcuts/Ft0123ABC456/xyz...zyx", + customizable_input_parameters=[ + { + "name": "input_parameter_a", + "value": "Value for input param A", + }, + { + "name": "input_parameter_b", + "value": "Value for input param B", + }, + ], + ), + ), + ), + ) + return block diff --git a/block-kit/tests/compositions/__init__.py b/block-kit/tests/compositions/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/block-kit/tests/compositions/test_confirmation_dialog.py b/block-kit/tests/compositions/test_confirmation_dialog.py new file mode 100644 index 0000000..8424a95 --- /dev/null +++ b/block-kit/tests/compositions/test_confirmation_dialog.py @@ -0,0 +1,52 @@ +import json + +from src.compositions import confirmation_dialog + + +def test_example01(): + block = confirmation_dialog.example01() + actual = block.to_dict() + expected = { + "type": "actions", + "elements": [ + { + "type": "button", + "text": { + "type": "plain_text", + "emoji": True, + "text": "Approve", + }, + "confirm": { + "title": { + "type": "plain_text", + "text": "Are you sure?", + }, + "text": { + "type": "mrkdwn", + "text": "Would you not prefer a good game of _chess_?", + }, + "confirm": { + "type": "plain_text", + "text": "Do it", + }, + "deny": { + "type": "plain_text", + "text": "Stop, I changed my mind!", + }, + }, + "style": "primary", + "value": "click_me_123", + }, + { + "type": "button", + "text": { + "type": "plain_text", + "emoji": True, + "text": "Deny", + }, + "style": "danger", + "value": "click_me_123", + }, + ], + } + assert json.dumps(actual, sort_keys=True) == json.dumps(expected, sort_keys=True) diff --git a/block-kit/tests/compositions/test_conversation_filter.py b/block-kit/tests/compositions/test_conversation_filter.py new file mode 100644 index 0000000..25dffb2 --- /dev/null +++ b/block-kit/tests/compositions/test_conversation_filter.py @@ -0,0 +1,49 @@ +import json + +from src.compositions import conversation_filter + + +def test_example01(): + view = conversation_filter.example01() + actual = view.to_dict() + expected = { + "title": { + "type": "plain_text", + "text": "My App", + "emoji": True, + }, + "submit": { + "type": "plain_text", + "text": "Submit", + "emoji": True, + }, + "type": "modal", + "close": { + "type": "plain_text", + "text": "Cancel", + "emoji": True, + }, + "blocks": [ + { + "type": "input", + "element": { + "type": "conversations_select", + "placeholder": { + "type": "plain_text", + "text": "Select a conversation", + "emoji": True, + }, + "filter": { + "include": ["public", "mpim"], + "exclude_bot_users": True, + }, + }, + "label": { + "type": "plain_text", + "text": "Choose the conversation to publish your result to:", + "emoji": True, + }, + }, + ], + } + assert json.dumps(actual, sort_keys=True) == json.dumps(expected, sort_keys=True) diff --git a/block-kit/tests/compositions/test_dispatch_action_configuration.py b/block-kit/tests/compositions/test_dispatch_action_configuration.py new file mode 100644 index 0000000..0bb451c --- /dev/null +++ b/block-kit/tests/compositions/test_dispatch_action_configuration.py @@ -0,0 +1,25 @@ +import json + +from src.compositions import dispatch_action_configuration + + +def test_example01(): + block = dispatch_action_configuration.example01() + actual = block.to_dict() + expected = { + "type": "input", + "dispatch_action": True, + "element": { + "type": "plain_text_input", + "multiline": True, + "dispatch_action_config": { + "trigger_actions_on": ["on_character_entered"], + }, + }, + "label": { + "type": "plain_text", + "text": "This is a multiline plain-text input", + "emoji": True, + }, + } + assert json.dumps(actual, sort_keys=True) == json.dumps(expected, sort_keys=True) diff --git a/block-kit/tests/compositions/test_option.py b/block-kit/tests/compositions/test_option.py new file mode 100644 index 0000000..abdd816 --- /dev/null +++ b/block-kit/tests/compositions/test_option.py @@ -0,0 +1,76 @@ +import json + +from src.compositions import option + + +def test_example01(): + obj = option.example01() + actual = obj.to_dict() + expected = { + "text": { + "type": "plain_text", + "emoji": True, + "text": "Save it", + }, + "value": "value-2", + } + assert json.dumps(actual, sort_keys=True) == json.dumps(expected, sort_keys=True) + + +def test_example02(): + blocks = option.example02() + actual = [block.to_dict() for block in blocks] + expected = [ + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": ":mag: Search results for *Cata*", + }, + }, + { + "type": "divider", + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "**\nUse Case Catalogue for the following departments/roles...", + }, + "accessory": { + "type": "static_select", + "placeholder": { + "type": "plain_text", + "emoji": True, + "text": "Manage", + }, + "options": [ + { + "text": { + "type": "plain_text", + "emoji": True, + "text": "Edit it", + }, + "value": "value-0", + }, + { + "text": { + "type": "plain_text", + "emoji": True, + "text": "Read it", + }, + "value": "value-1", + }, + { + "text": { + "type": "plain_text", + "emoji": True, + "text": "Save it", + }, + "value": "value-2", + }, + ], + }, + }, + ] + assert json.dumps(actual, sort_keys=True) == json.dumps(expected, sort_keys=True) diff --git a/block-kit/tests/compositions/test_option_group.py b/block-kit/tests/compositions/test_option_group.py new file mode 100644 index 0000000..e764ca1 --- /dev/null +++ b/block-kit/tests/compositions/test_option_group.py @@ -0,0 +1,82 @@ +import json + +from src.compositions import option_group + + +def test_example01(): + blocks = option_group.example01() + actual = [block.to_dict() for block in blocks] + expected = [ + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": ":mag: Search results for *Cata*", + }, + }, + { + "type": "divider", + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "**\nUse Case Catalogue for the following departments/roles...", + }, + "accessory": { + "type": "static_select", + "placeholder": { + "type": "plain_text", + "emoji": True, + "text": "Manage", + }, + "option_groups": [ + { + "label": { + "type": "plain_text", + "text": "Group 1", + }, + "options": [ + { + "text": { + "type": "plain_text", + "text": "*this is plain_text text*", + }, + "value": "value-0", + }, + { + "text": { + "type": "plain_text", + "text": "*this is plain_text text*", + }, + "value": "value-1", + }, + { + "text": { + "type": "plain_text", + "text": "*this is plain_text text*", + }, + "value": "value-2", + }, + ], + }, + { + "label": { + "type": "plain_text", + "text": "Group 2", + }, + "options": [ + { + "text": { + "type": "plain_text", + "text": "*this is plain_text text*", + }, + "value": "value-3", + }, + ], + }, + ], + }, + }, + ] + assert json.dumps(actual, sort_keys=True) == json.dumps(expected, sort_keys=True) diff --git a/block-kit/tests/compositions/test_slack_file.py b/block-kit/tests/compositions/test_slack_file.py new file mode 100644 index 0000000..56bb541 --- /dev/null +++ b/block-kit/tests/compositions/test_slack_file.py @@ -0,0 +1,39 @@ +import json + +from src.compositions import slack_file + + +def test_example01(): + block = slack_file.example01() + actual = block.to_dict() + expected = { + "type": "image", + "title": { + "type": "plain_text", + "text": "Please enjoy this photo of a kitten", + }, + "block_id": "image4", + "slack_file": { + "url": "https://files.slack.com/files-pri/T0123456-F0123456/xyz.png", + }, + "alt_text": "An incredibly cute kitten.", + } + assert json.dumps(actual, sort_keys=True) == json.dumps(expected, sort_keys=True) + + +def test_example02(): + block = slack_file.example02() + actual = block.to_dict() + expected = { + "type": "image", + "title": { + "type": "plain_text", + "text": "Please enjoy this photo of a kitten", + }, + "block_id": "image4", + "slack_file": { + "id": "F0123456", + }, + "alt_text": "An incredibly cute kitten.", + } + assert json.dumps(actual, sort_keys=True) == json.dumps(expected, sort_keys=True) diff --git a/block-kit/tests/compositions/test_text.py b/block-kit/tests/compositions/test_text.py new file mode 100644 index 0000000..6eb9ade --- /dev/null +++ b/block-kit/tests/compositions/test_text.py @@ -0,0 +1,16 @@ +import json + +from src.compositions import text + + +def test_example01(): + block = text.example01() + actual = block.to_dict() + expected = { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "A message *with some bold text* and _some italicized text_.", + }, + } + assert json.dumps(actual, sort_keys=True) == json.dumps(expected, sort_keys=True) diff --git a/block-kit/tests/compositions/test_trigger.py b/block-kit/tests/compositions/test_trigger.py new file mode 100644 index 0000000..cb5fccb --- /dev/null +++ b/block-kit/tests/compositions/test_trigger.py @@ -0,0 +1,39 @@ +import json + +from src.compositions import trigger + + +def test_example01(): + block = trigger.example01() + actual = block.to_dict() + expected = { + "type": "section", + "text": { + "text": "A message *with some bold text* and _some italicized text_.", + "type": "mrkdwn", + }, + "accessory": { + "type": "workflow_button", + "text": { + "type": "plain_text", + "text": "Run Workflow", + }, + "action_id": "workflowbutton123", + "workflow": { + "trigger": { + "url": "https://slack.com/shortcuts/Ft0123ABC456/xyz...zyx", + "customizable_input_parameters": [ + { + "name": "input_parameter_a", + "value": "Value for input param A", + }, + { + "name": "input_parameter_b", + "value": "Value for input param B", + }, + ], + }, + }, + }, + } + assert json.dumps(actual, sort_keys=True) == json.dumps(expected, sort_keys=True) diff --git a/block-kit/tests/compositions/test_workflow.py b/block-kit/tests/compositions/test_workflow.py new file mode 100644 index 0000000..a6e9f21 --- /dev/null +++ b/block-kit/tests/compositions/test_workflow.py @@ -0,0 +1,39 @@ +import json + +from src.compositions import workflow + + +def test_example01(): + block = workflow.example01() + actual = block.to_dict() + expected = { + "type": "section", + "text": { + "text": "A message *with some bold text* and _some italicized text_.", + "type": "mrkdwn", + }, + "accessory": { + "type": "workflow_button", + "text": { + "type": "plain_text", + "text": "Run Workflow", + }, + "action_id": "workflowbutton123", + "workflow": { + "trigger": { + "url": "https://slack.com/shortcuts/Ft0123ABC456/xyz...zyx", + "customizable_input_parameters": [ + { + "name": "input_parameter_a", + "value": "Value for input param A", + }, + { + "name": "input_parameter_b", + "value": "Value for input param B", + }, + ], + }, + }, + }, + } + assert json.dumps(actual, sort_keys=True) == json.dumps(expected, sort_keys=True)