Skip to content

Commit 37eddaf

Browse files
Add --payload option to zappa invoke command (#1412) (#1432)
Allow passing a JSON payload to invoked functions via `zappa invoke <env> <func> --payload '{"key": "value"}'`. The payload keys are merged into the Lambda event dict alongside the command key, so the invoked function receives them in `event`. Validates that the payload is valid JSON and a dict, raising a clear error otherwise. Co-authored-by: shane <shane.cousins@kiconiaworks.com>
1 parent 881bde6 commit 37eddaf

2 files changed

Lines changed: 109 additions & 1 deletion

File tree

tests/test_core.py

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2429,6 +2429,95 @@ def test_cli_colorize_invoke_command_bad_string(self):
24292429
colorized_string = zappa_cli.colorize_invoke_command(plain_string)
24302430
self.assertEqual(final_string, colorized_string)
24312431

2432+
@mock.patch("zappa.cli.ZappaCLI.format_lambda_response")
2433+
@mock.patch("zappa.core.Zappa.invoke_lambda_function")
2434+
def test_invoke_with_payload(self, mock_invoke, mock_format):
2435+
"""Test that --payload merges JSON into the invoke event."""
2436+
mock_invoke.return_value = {"StatusCode": 200}
2437+
mock_format.return_value = "ok"
2438+
2439+
zappa_cli = ZappaCLI()
2440+
zappa_cli.api_stage = "dev"
2441+
zappa_cli.lambda_name = "test-func"
2442+
zappa_cli.zappa = mock.MagicMock()
2443+
zappa_cli.zappa.invoke_lambda_function = mock_invoke
2444+
2445+
zappa_cli.invoke(
2446+
"my_app.my_function",
2447+
payload='{"key1": "value1", "key2": "value2"}',
2448+
)
2449+
2450+
call_args = mock_invoke.call_args
2451+
sent_payload = json.loads(call_args[0][1])
2452+
self.assertEqual(sent_payload["command"], "my_app.my_function")
2453+
self.assertEqual(sent_payload["key1"], "value1")
2454+
self.assertEqual(sent_payload["key2"], "value2")
2455+
2456+
@mock.patch("zappa.cli.ZappaCLI.format_lambda_response")
2457+
@mock.patch("zappa.core.Zappa.invoke_lambda_function")
2458+
def test_invoke_with_payload_raw_python(self, mock_invoke, mock_format):
2459+
"""Test that --payload works with --raw."""
2460+
mock_invoke.return_value = {"StatusCode": 200}
2461+
mock_format.return_value = "ok"
2462+
2463+
zappa_cli = ZappaCLI()
2464+
zappa_cli.api_stage = "dev"
2465+
zappa_cli.lambda_name = "test-func"
2466+
zappa_cli.zappa = mock.MagicMock()
2467+
zappa_cli.zappa.invoke_lambda_function = mock_invoke
2468+
2469+
zappa_cli.invoke(
2470+
"print('hello')",
2471+
raw_python=True,
2472+
payload='{"extra": "data"}',
2473+
)
2474+
2475+
call_args = mock_invoke.call_args
2476+
sent_payload = json.loads(call_args[0][1])
2477+
self.assertEqual(sent_payload["raw_command"], "print('hello')")
2478+
self.assertEqual(sent_payload["extra"], "data")
2479+
2480+
def test_invoke_with_invalid_payload_json(self):
2481+
"""Test that invalid JSON in --payload raises ClickException."""
2482+
zappa_cli = ZappaCLI()
2483+
zappa_cli.api_stage = "dev"
2484+
zappa_cli.lambda_name = "test-func"
2485+
zappa_cli.zappa = mock.MagicMock()
2486+
2487+
with self.assertRaises(ClickException) as cm:
2488+
zappa_cli.invoke("my_app.my_function", payload="not-valid-json")
2489+
self.assertIn("--payload must be valid JSON", str(cm.exception))
2490+
2491+
def test_invoke_with_non_dict_payload(self):
2492+
"""Test that a non-dict JSON payload raises ClickException."""
2493+
zappa_cli = ZappaCLI()
2494+
zappa_cli.api_stage = "dev"
2495+
zappa_cli.lambda_name = "test-func"
2496+
zappa_cli.zappa = mock.MagicMock()
2497+
2498+
with self.assertRaises(ClickException) as cm:
2499+
zappa_cli.invoke("my_app.my_function", payload='["a", "b"]')
2500+
self.assertIn("--payload must be a JSON object", str(cm.exception))
2501+
2502+
@mock.patch("zappa.cli.ZappaCLI.format_lambda_response")
2503+
@mock.patch("zappa.core.Zappa.invoke_lambda_function")
2504+
def test_invoke_without_payload(self, mock_invoke, mock_format):
2505+
"""Test that invoke without --payload works as before."""
2506+
mock_invoke.return_value = {"StatusCode": 200}
2507+
mock_format.return_value = "ok"
2508+
2509+
zappa_cli = ZappaCLI()
2510+
zappa_cli.api_stage = "dev"
2511+
zappa_cli.lambda_name = "test-func"
2512+
zappa_cli.zappa = mock.MagicMock()
2513+
zappa_cli.zappa.invoke_lambda_function = mock_invoke
2514+
2515+
zappa_cli.invoke("my_app.my_function")
2516+
2517+
call_args = mock_invoke.call_args
2518+
sent_payload = json.loads(call_args[0][1])
2519+
self.assertEqual(sent_payload, {"command": "my_app.my_function"})
2520+
24322521
@mock.patch("zappa.cli.ZappaCLI.colorize_invoke_command")
24332522
@mock.patch("zappa.cli.ZappaCLI.format_invoke_command")
24342523
def test_cli_format_lambda_response(self, mock_format, mock_colorize):

zappa/cli.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,10 @@ def handle(self, argv=None):
325325
"--qualifier",
326326
help="The qualifier (version or alias) of the lambda version to invoke. $LATEST if omitted.",
327327
)
328+
invoke_parser.add_argument(
329+
"--payload",
330+
help="A JSON string of key/value pairs to include in the event passed to the function.",
331+
)
328332
invoke_parser.add_argument("command_rest")
329333

330334
##
@@ -658,6 +662,7 @@ def dispatch_command(self, command, stage):
658662
no_color=self.vargs["no_color"],
659663
client_context=self.vargs["client_context"],
660664
qualifier=self.vargs["qualifier"],
665+
payload=self.vargs.get("payload"),
661666
)
662667
elif command == "manage": # pragma: no cover
663668
if not self.vargs.get("command_rest"):
@@ -1543,7 +1548,9 @@ def unschedule(self):
15431548
removed_arns = self.zappa.remove_async_sns_topic(self.lambda_name)
15441549
click.echo("SNS Topic removed: %s" % ", ".join(removed_arns))
15451550

1546-
def invoke(self, function_name, raw_python=False, command=None, no_color=False, client_context=None, qualifier=None):
1551+
def invoke(
1552+
self, function_name, raw_python=False, command=None, no_color=False, client_context=None, qualifier=None, payload=None
1553+
):
15471554
"""
15481555
Invoke a remote function.
15491556
"""
@@ -1557,6 +1564,18 @@ def invoke(self, function_name, raw_python=False, command=None, no_color=False,
15571564
command = {"raw_command": function_name}
15581565
else:
15591566
command = {key: function_name}
1567+
1568+
if payload:
1569+
import json as json_module
1570+
1571+
try:
1572+
payload_dict = json_module.loads(payload)
1573+
except (ValueError, TypeError) as e:
1574+
raise ClickException("--payload must be valid JSON: {}".format(e))
1575+
if not isinstance(payload_dict, dict):
1576+
raise ClickException("--payload must be a JSON object (dict), not {}.".format(type(payload_dict).__name__))
1577+
command.update(payload_dict)
1578+
15601579
client_context = base64.b64encode(client_context.encode("utf-8")).decode("utf-8") if client_context else None
15611580

15621581
# Can't use hjson

0 commit comments

Comments
 (0)