functualize-lambda 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,101 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ *.egg-info/
7
+ *.egg
8
+ dist/
9
+ build/
10
+ *.whl
11
+
12
+ # Agents
13
+ .spec/archive/
14
+ .spec/features/
15
+ .spec/scrutiny-reports/
16
+ .spec/.agentic-coding
17
+ .spec/STATE.md
18
+ .spec/PROJECT.md
19
+ .spec/REQUIREMENTS.md
20
+ .spec/ROADMAP.md
21
+ .opencode/
22
+
23
+
24
+ # Virtual environments
25
+ .venv/
26
+ venv/
27
+ ENV/
28
+
29
+ # Testing
30
+ .coverage
31
+ .pytest_cache/
32
+ htmlcov/
33
+ .hypothesis/
34
+ snapshot_report.html
35
+ _*_result*.txt
36
+ _debug.txt
37
+ _tui_debug.txt
38
+ _tui_eval_debug.txt
39
+
40
+ # IDE
41
+ .idea/
42
+ *.swp
43
+ *.swo
44
+ *~
45
+ *.code-workspace
46
+
47
+ # Coding-agent tooling state (guards — these dirs are not part of the repo)
48
+ .kiro/
49
+ .moai/
50
+
51
+ # OS
52
+ .DS_Store
53
+ Thumbs.db
54
+
55
+ # Environment / secrets
56
+ .env
57
+ .env.*
58
+ !.env.example
59
+
60
+ # Agent scratch space (test output, temp scripts)
61
+ tmp/
62
+
63
+ # Local-only files (not for the repo)
64
+ *.local.md
65
+ *.local.*
66
+
67
+ # Personal notes
68
+ HUMAN_NOTE.md
69
+
70
+ # Distribution
71
+ dist/
72
+
73
+ # Documentation site build output
74
+ site/
75
+
76
+ # uv
77
+ .python-version
78
+ .functualize/cache.json
79
+ .functualize_cache.json
80
+ .todos/
81
+ .sidecar/
82
+ .sidecar-agent
83
+ .sidecar-task
84
+ .sidecar-pr
85
+ .sidecar-start.sh
86
+ .sidecar-base
87
+ .td-root
88
+ .functualize/
89
+ .import_linter_cache/
90
+ .mypy_cache/
91
+ .pytest_cache/
92
+ .ruff_cache/
93
+
94
+ # OmO / OpenCode agent run-continuation scratch state
95
+ .omo/
96
+ .mcp.json
97
+ .agentsroom/handoff-transcript-*.txt
98
+ .agentsroom/handoff-summary-*.md
99
+
100
+ # Internal pre-release audit reports (contain session IDs / local infra notes)
101
+ .release/
@@ -0,0 +1,66 @@
1
+ Metadata-Version: 2.4
2
+ Name: functualize-lambda
3
+ Version: 0.1.0
4
+ Summary: AWS Lambda adapter plugin for functualize - supports fat and thin Lambda deployment patterns
5
+ Author-email: Mohammad Hakim Adiprasetya <viltohmyst@gmail.com>
6
+ License-Expression: MIT
7
+ Classifier: Development Status :: 3 - Alpha
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.11
10
+ Classifier: Programming Language :: Python :: 3.12
11
+ Classifier: Programming Language :: Python :: 3.13
12
+ Classifier: Typing :: Typed
13
+ Requires-Python: >=3.11
14
+ Requires-Dist: functualize<1.0.0,>=0.1.0
15
+ Provides-Extra: dev
16
+ Requires-Dist: pytest-cov>=4.1.0; extra == 'dev'
17
+ Requires-Dist: pytest>=7.4.0; extra == 'dev'
18
+ Description-Content-Type: text/markdown
19
+
20
+ # functualize-lambda
21
+
22
+ > **Status: Published** — Independently installable from PyPI.
23
+
24
+ AWS Lambda adapter plugin for [functualize](https://github.com/raicing-ai/functualize).
25
+
26
+ Supports two deployment patterns:
27
+
28
+ 1. **Fat Lambda** — Single Lambda function with internal routing via `event["job"]`
29
+ 2. **Thin Lambda** — One Lambda per job via `make_handler(job_name)`
30
+
31
+ ## Installation
32
+
33
+ ```bash
34
+ pip install functualize-lambda
35
+ ```
36
+
37
+ ## Usage
38
+
39
+ ### Fat Lambda (internal routing)
40
+
41
+ ```python
42
+ from functualize.app import FunctualizeApp, JobSources
43
+ from functualize_lambda import LambdaAdapter
44
+
45
+ app = FunctualizeApp("my-app", job_sources=JobSources(functions=[deploy, rollback]))
46
+ adapter = LambdaAdapter()
47
+ adapter(app)
48
+
49
+ def handler(event, context):
50
+ return adapter.run(event, context)
51
+ ```
52
+
53
+ Events should have the shape: `{"job": "job_name", "kwargs": {"key": "value"}}`
54
+
55
+ ### Thin Lambda (per-job handler)
56
+
57
+ ```python
58
+ from functualize.app import FunctualizeApp, JobSources
59
+ from functualize_lambda import LambdaAdapter
60
+
61
+ app = FunctualizeApp("my-app", job_sources=JobSources(functions=[deploy]))
62
+ adapter = LambdaAdapter()
63
+ adapter(app)
64
+
65
+ handler = adapter.make_handler("deploy")
66
+ ```
@@ -0,0 +1,47 @@
1
+ # functualize-lambda
2
+
3
+ > **Status: Published** — Independently installable from PyPI.
4
+
5
+ AWS Lambda adapter plugin for [functualize](https://github.com/raicing-ai/functualize).
6
+
7
+ Supports two deployment patterns:
8
+
9
+ 1. **Fat Lambda** — Single Lambda function with internal routing via `event["job"]`
10
+ 2. **Thin Lambda** — One Lambda per job via `make_handler(job_name)`
11
+
12
+ ## Installation
13
+
14
+ ```bash
15
+ pip install functualize-lambda
16
+ ```
17
+
18
+ ## Usage
19
+
20
+ ### Fat Lambda (internal routing)
21
+
22
+ ```python
23
+ from functualize.app import FunctualizeApp, JobSources
24
+ from functualize_lambda import LambdaAdapter
25
+
26
+ app = FunctualizeApp("my-app", job_sources=JobSources(functions=[deploy, rollback]))
27
+ adapter = LambdaAdapter()
28
+ adapter(app)
29
+
30
+ def handler(event, context):
31
+ return adapter.run(event, context)
32
+ ```
33
+
34
+ Events should have the shape: `{"job": "job_name", "kwargs": {"key": "value"}}`
35
+
36
+ ### Thin Lambda (per-job handler)
37
+
38
+ ```python
39
+ from functualize.app import FunctualizeApp, JobSources
40
+ from functualize_lambda import LambdaAdapter
41
+
42
+ app = FunctualizeApp("my-app", job_sources=JobSources(functions=[deploy]))
43
+ adapter = LambdaAdapter()
44
+ adapter(app)
45
+
46
+ handler = adapter.make_handler("deploy")
47
+ ```
@@ -0,0 +1,18 @@
1
+ # functualize-lambda Examples
2
+
3
+ The AWS Lambda delivery adapter: run jobs serverless.
4
+
5
+ | Directory | Demonstrates |
6
+ |-----------|--------------|
7
+ | [`lambda_handler/`](lambda_handler/) | "Fat Lambda" (one function routing all jobs) and "thin Lambda" (one function per job) deployment patterns |
8
+
9
+ ```bash
10
+ cd plugins/functualize-lambda/examples/lambda_handler
11
+ uv sync
12
+ ```
13
+
14
+ Tests:
15
+
16
+ ```bash
17
+ uv run pytest plugins/functualize-lambda/examples/ -v
18
+ ```
@@ -0,0 +1,60 @@
1
+ # Lambda Handler — Project Example
2
+
3
+ A functualize project deployed as AWS Lambda functions using the `functualize-lambda` adapter. Demonstrates both "fat Lambda" (single function with routing) and "thin Lambda" (one function per job) patterns.
4
+
5
+ ## Setup
6
+
7
+ ```bash
8
+ cd plugins/functualize-lambda/examples/lambda_handler
9
+ uv sync
10
+ ```
11
+
12
+ ## Deployment Patterns
13
+
14
+ ### Fat Lambda (Internal Routing)
15
+
16
+ Single Lambda function that routes to different jobs based on the event payload:
17
+
18
+ ```python
19
+ # handler.py
20
+ from lambda_service.app import fat_handler
21
+
22
+ def handler(event, context):
23
+ return fat_handler(event, context)
24
+ ```
25
+
26
+ Event format: `{"job": "process_order", "kwargs": {"order_id": "ORD-123"}}`
27
+
28
+ ### Thin Lambda (Per-Job)
29
+
30
+ One Lambda function per job — simpler IAM, clearer monitoring:
31
+
32
+ ```python
33
+ # handlers/process_order.py
34
+ from lambda_service.app import process_order_handler
35
+
36
+ handler = process_order_handler
37
+ ```
38
+
39
+ ## Testing Locally
40
+
41
+ ```bash
42
+ # Test job functions directly
43
+ uv run pytest tests/ -v
44
+
45
+ # Simulate a Lambda invocation
46
+ uv run python -c "
47
+ from lambda_service.app import fat_handler
48
+ result = fat_handler({'job': 'process_order', 'kwargs': {'order_id': 'ORD-001', 'amount': 99.99}}, None)
49
+ print(result)
50
+ "
51
+ ```
52
+
53
+ ## What This Demonstrates
54
+
55
+ - `FunctualizeApp` with `JobSources(functions=[...])` for explicit registration
56
+ - `LambdaAdapter` for AWS Lambda delivery
57
+ - Fat Lambda pattern with event routing
58
+ - Thin Lambda pattern with `make_handler()`
59
+ - `twelve_factor()` config preset (env vars only — no files in Lambda)
60
+ - Same jobs testable locally without AWS
@@ -0,0 +1,20 @@
1
+ [project]
2
+ name = "lambda-service"
3
+ version = "0.1.0"
4
+ description = "Example: Functualize jobs deployed as AWS Lambda functions"
5
+ requires-python = ">=3.11"
6
+ dependencies = [
7
+ "functualize",
8
+ "functualize-lambda",
9
+ ]
10
+
11
+ [build-system]
12
+ requires = ["hatchling"]
13
+ build-backend = "hatchling.build"
14
+
15
+ [tool.hatch.build.targets.wheel]
16
+ packages = ["src/lambda_service"]
17
+
18
+ [tool.uv.sources]
19
+ functualize = { path = "../../../..", editable = true }
20
+ functualize-lambda = { path = "../..", editable = true }
@@ -0,0 +1 @@
1
+ """Lambda service example — functualize jobs deployed to AWS Lambda."""
@@ -0,0 +1,44 @@
1
+ """Lambda application wiring — configures FunctualizeApp with Lambda adapter.
2
+
3
+ Demonstrates two deployment patterns:
4
+ 1. Fat Lambda — single handler routing via event["job"]
5
+ 2. Thin Lambda — one handler per job via make_handler()
6
+ """
7
+
8
+ from functualize_lambda import LambdaAdapter
9
+
10
+ from functualize.app import FunctualizeApp, JobSources, twelve_factor
11
+ from lambda_service.jobs import process_order, send_notification
12
+
13
+ # Create the app with explicit function registration (no directory scanning).
14
+ # This minimizes cold start time in Lambda — no filesystem I/O at boot.
15
+ app = FunctualizeApp(
16
+ name="lambda-service",
17
+ job_sources=JobSources(functions=[process_order, send_notification]),
18
+ config_sources=twelve_factor(), # Env vars only — no config files in Lambda
19
+ )
20
+
21
+ # Create the Lambda adapter
22
+ adapter = LambdaAdapter()
23
+ adapter(app)
24
+
25
+
26
+ # ---------------------------------------------------------------------------
27
+ # Fat Lambda handler — routes based on event["job"]
28
+ # ---------------------------------------------------------------------------
29
+
30
+
31
+ def fat_handler(event: dict, context) -> dict:
32
+ """Single Lambda entry point with internal job routing.
33
+
34
+ Event format: {"job": "process_order", "kwargs": {"order_id": "ORD-123"}}
35
+ """
36
+ return adapter.run(event, context)
37
+
38
+
39
+ # ---------------------------------------------------------------------------
40
+ # Thin Lambda handlers — one per job
41
+ # ---------------------------------------------------------------------------
42
+
43
+ process_order_handler = adapter.make_handler("process_order")
44
+ send_notification_handler = adapter.make_handler("send_notification")
@@ -0,0 +1,82 @@
1
+ """Job definitions for the Lambda service.
2
+
3
+ These jobs are registered explicitly via JobSources(functions=[...])
4
+ rather than directory scanning — common in Lambda where cold start
5
+ time matters and we want minimal import overhead.
6
+ """
7
+
8
+ from pydantic import BaseModel, Field
9
+
10
+ from functualize.job.context import RunContext
11
+ from functualize.job.decorators import job
12
+
13
+ # ---------------------------------------------------------------------------
14
+ # Job 1: Process Order
15
+ # ---------------------------------------------------------------------------
16
+
17
+
18
+ class ProcessOrderConfig(BaseModel):
19
+ """Configuration for order processing."""
20
+
21
+ order_id: str = Field(description="Order identifier")
22
+ amount: float = Field(ge=0, description="Order amount in USD")
23
+ priority: bool = Field(default=False, description="Priority processing flag")
24
+
25
+
26
+ @job(
27
+ extra_description="Process an incoming order and validate payment",
28
+ category="orders",
29
+ tags=["order", "payment"],
30
+ visibility="external",
31
+ )
32
+ def process_order(config: ProcessOrderConfig, rc: RunContext) -> dict:
33
+ """Process an order — validate, charge, and confirm.
34
+
35
+ In production this would integrate with payment providers and
36
+ inventory systems. Here we simulate the happy path.
37
+ """
38
+ rc.log(f"Processing order {config.order_id} (${config.amount:.2f})")
39
+
40
+ if config.priority:
41
+ rc.log("Priority processing enabled")
42
+
43
+ return {
44
+ "order_id": config.order_id,
45
+ "status": "processed",
46
+ "amount": config.amount,
47
+ "priority": config.priority,
48
+ "confirmation_code": f"CONF-{config.order_id[-4:]}",
49
+ }
50
+
51
+
52
+ # ---------------------------------------------------------------------------
53
+ # Job 2: Send Notification
54
+ # ---------------------------------------------------------------------------
55
+
56
+
57
+ class NotificationConfig(BaseModel):
58
+ """Configuration for sending notifications."""
59
+
60
+ recipient: str = Field(description="Email or user ID")
61
+ message: str = Field(description="Notification message body")
62
+ channel: str = Field(
63
+ default="email", description="Delivery channel: email, sms, push"
64
+ )
65
+
66
+
67
+ @job(
68
+ extra_description="Send a notification to a user via the specified channel",
69
+ category="messaging",
70
+ tags=["notification", "messaging"],
71
+ visibility="external",
72
+ )
73
+ def send_notification(config: NotificationConfig, rc: RunContext) -> dict:
74
+ """Send a notification through the configured channel."""
75
+ rc.log(f"Sending {config.channel} to {config.recipient}")
76
+
77
+ return {
78
+ "recipient": config.recipient,
79
+ "channel": config.channel,
80
+ "status": "sent",
81
+ "message_preview": config.message[:50],
82
+ }
@@ -0,0 +1,84 @@
1
+ """Tests for Lambda service jobs — prove they work without AWS."""
2
+
3
+ import sys
4
+ from pathlib import Path
5
+ from unittest.mock import MagicMock
6
+
7
+ sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
8
+
9
+ from lambda_service.jobs import (
10
+ NotificationConfig,
11
+ ProcessOrderConfig,
12
+ process_order,
13
+ send_notification,
14
+ )
15
+
16
+
17
+ def _make_rc():
18
+ """Create a minimal mock RunContext for testing."""
19
+ rc = MagicMock()
20
+ rc.log = MagicMock()
21
+ return rc
22
+
23
+
24
+ class TestProcessOrder:
25
+ """Tests for the process_order job."""
26
+
27
+ def test_processes_order_successfully(self):
28
+ rc = _make_rc()
29
+ config = ProcessOrderConfig(order_id="ORD-001", amount=49.99)
30
+ result = process_order(config, rc)
31
+
32
+ assert result["order_id"] == "ORD-001"
33
+ assert result["status"] == "processed"
34
+ assert result["amount"] == 49.99
35
+ assert result["confirmation_code"] == "CONF--001"
36
+
37
+ def test_priority_processing(self):
38
+ rc = _make_rc()
39
+ config = ProcessOrderConfig(order_id="ORD-VIP", amount=199.99, priority=True)
40
+ result = process_order(config, rc)
41
+
42
+ assert result["priority"] is True
43
+ log_calls = [str(call) for call in rc.log.call_args_list]
44
+ assert any("Priority" in call for call in log_calls)
45
+
46
+ def test_zero_amount_order(self):
47
+ rc = _make_rc()
48
+ config = ProcessOrderConfig(order_id="ORD-FREE", amount=0.0)
49
+ result = process_order(config, rc)
50
+
51
+ assert result["amount"] == 0.0
52
+ assert result["status"] == "processed"
53
+
54
+
55
+ class TestSendNotification:
56
+ """Tests for the send_notification job."""
57
+
58
+ def test_sends_email_notification(self):
59
+ rc = _make_rc()
60
+ config = NotificationConfig(
61
+ recipient="user@example.com", message="Your order is ready!"
62
+ )
63
+ result = send_notification(config, rc)
64
+
65
+ assert result["recipient"] == "user@example.com"
66
+ assert result["channel"] == "email"
67
+ assert result["status"] == "sent"
68
+
69
+ def test_sends_sms_notification(self):
70
+ rc = _make_rc()
71
+ config = NotificationConfig(
72
+ recipient="+1234567890", message="Delivery arriving", channel="sms"
73
+ )
74
+ result = send_notification(config, rc)
75
+
76
+ assert result["channel"] == "sms"
77
+
78
+ def test_message_preview_truncated(self):
79
+ rc = _make_rc()
80
+ long_message = "A" * 100
81
+ config = NotificationConfig(recipient="user@test.com", message=long_message)
82
+ result = send_notification(config, rc)
83
+
84
+ assert len(result["message_preview"]) == 50
@@ -0,0 +1,40 @@
1
+ [project]
2
+ name = "functualize-lambda"
3
+ version = "0.1.0"
4
+ description = "AWS Lambda adapter plugin for functualize - supports fat and thin Lambda deployment patterns"
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ authors = [
8
+ { name = "Mohammad Hakim Adiprasetya", email = "viltohmyst@gmail.com" }
9
+ ]
10
+ requires-python = ">=3.11"
11
+ dependencies = [
12
+ "functualize>=0.1.0,<1.0.0",
13
+ ]
14
+ classifiers = [
15
+ "Development Status :: 3 - Alpha",
16
+ "Programming Language :: Python :: 3",
17
+ "Programming Language :: Python :: 3.11",
18
+ "Programming Language :: Python :: 3.12",
19
+ "Programming Language :: Python :: 3.13",
20
+ "Typing :: Typed",
21
+ ]
22
+
23
+ [project.entry-points."functualize.plugins"]
24
+ lambda = "functualize_lambda:LambdaAdapter"
25
+
26
+ [project.optional-dependencies]
27
+ dev = [
28
+ "pytest>=7.4.0",
29
+ "pytest-cov>=4.1.0",
30
+ ]
31
+
32
+ [build-system]
33
+ requires = ["hatchling"]
34
+ build-backend = "hatchling.build"
35
+
36
+ [tool.uv.sources]
37
+ functualize = { workspace = true }
38
+
39
+ [tool.hatch.build.targets.wheel]
40
+ packages = ["src/functualize_lambda"]
@@ -0,0 +1,171 @@
1
+ """Functualize Lambda Adapter Plugin — AWS Lambda delivery for FunctualizeApp.
2
+
3
+ Implements the AdapterPlugin Protocol with adapter_type="lambda".
4
+ Supports two deployment patterns:
5
+
6
+ 1. Fat Lambda (internal routing):
7
+ - Single Lambda function handling multiple jobs
8
+ - Event contains {"job": "job_name", "kwargs": {...}}
9
+ - Adapter routes to the correct job via app.execute()
10
+
11
+ 2. Thin Lambda (per-job handler):
12
+ - One Lambda function per job, routing handled by infrastructure
13
+ - Use make_handler(job_name) to create a bound handler
14
+
15
+ Both patterns work with the static wiring fast path for <5ms cold start
16
+ when using JobSources(functions=[...]) with fully-explicit config.
17
+
18
+ Usage (fat Lambda):
19
+ app = FunctualizeApp("my-app", job_sources=JobSources(functions=[deploy, rollback]))
20
+ adapter = LambdaAdapter()
21
+ adapter(app)
22
+
23
+ def handler(event, context):
24
+ return adapter.run(event, context)
25
+
26
+ Usage (thin Lambda):
27
+ app = FunctualizeApp("my-app", job_sources=JobSources(functions=[deploy]))
28
+ adapter = LambdaAdapter()
29
+ adapter(app)
30
+
31
+ handler = adapter.make_handler("deploy")
32
+ """
33
+
34
+ from __future__ import annotations
35
+
36
+ from collections.abc import Callable
37
+ from typing import TYPE_CHECKING, Any
38
+
39
+ if TYPE_CHECKING:
40
+ from functualize.app.core import FunctualizeApp
41
+
42
+
43
+ class LambdaAdapter:
44
+ """AWS Lambda delivery adapter.
45
+
46
+ Satisfies the AdapterPlugin Protocol. Supports fat-Lambda (internal
47
+ routing via event["job"]) and thin-Lambda (per-job handler via
48
+ make_handler()) deployment patterns.
49
+
50
+ PluginMetadata attributes:
51
+ name: "functualize-lambda"
52
+ version: "1.0.0"
53
+ description: "AWS Lambda adapter supporting fat and thin Lambda patterns"
54
+ """
55
+
56
+ name: str = "functualize-lambda"
57
+ version: str = "1.0.0"
58
+ description: str = "AWS Lambda adapter supporting fat and thin Lambda patterns"
59
+ adapter_type: str = "lambda"
60
+
61
+ def __init__(self) -> None:
62
+ self._app: FunctualizeApp | None = None
63
+
64
+ def __call__(self, app: FunctualizeApp) -> None:
65
+ """Setup phase — store app reference.
66
+
67
+ Args:
68
+ app: The FunctualizeApp kernel instance.
69
+ """
70
+ self._app = app
71
+
72
+ def run(self, *args: Any, **kwargs: Any) -> Any:
73
+ """Fat Lambda entrypoint — route event to the correct job.
74
+
75
+ Parses the event to determine which job to execute and with
76
+ what arguments, then delegates to the kernel's execute method.
77
+
78
+ Args:
79
+ *args: Expected to be (event, context) where event is a dict
80
+ containing "job" (required) and "kwargs" (optional).
81
+
82
+ Returns:
83
+ Dict with "statusCode" (200 or 500) and "body" (result or
84
+ error message).
85
+
86
+ Raises:
87
+ RuntimeError: If run() is called before __call__(app).
88
+ """
89
+ if self._app is None:
90
+ raise RuntimeError("LambdaAdapter.run() called before __call__(app)")
91
+
92
+ # Extract event and context from positional args
93
+ event: dict[str, Any] = args[0] if args else kwargs.get("event", {})
94
+ # context is available but not used by the adapter itself
95
+ # _context = args[1] if len(args) > 1 else kwargs.get("context")
96
+
97
+ return self._handle_event(event)
98
+
99
+ def make_handler(self, job_name: str) -> Callable[..., Any]:
100
+ """Create a thin-Lambda handler bound to a specific job.
101
+
102
+ Returns a callable with the standard Lambda signature
103
+ (event, context) that always executes the specified job.
104
+ Event kwargs can still be provided via event.get("kwargs", {}).
105
+
106
+ Args:
107
+ job_name: The name of the job this handler will execute.
108
+
109
+ Returns:
110
+ A callable(event, context) -> dict suitable as a Lambda handler.
111
+
112
+ Raises:
113
+ RuntimeError: If make_handler() is called before __call__(app).
114
+ """
115
+ if self._app is None:
116
+ raise RuntimeError(
117
+ "LambdaAdapter.make_handler() called before __call__(app)"
118
+ )
119
+
120
+ app = self._app
121
+
122
+ def handler(event: dict[str, Any], context: Any) -> dict[str, Any]:
123
+ """Thin Lambda handler for job '{job_name}'."""
124
+ job_kwargs = event.get("kwargs", {})
125
+ try:
126
+ result = app.execute(job_name, **job_kwargs)
127
+ return {"statusCode": 200, "body": result.return_value}
128
+ except Exception as exc:
129
+ return {"statusCode": 500, "body": str(exc)}
130
+
131
+ # Set a useful name for debugging/logging
132
+ handler.__name__ = f"lambda_handler_{job_name}"
133
+ handler.__qualname__ = (
134
+ f"LambdaAdapter.make_handler.<locals>.handler[{job_name}]"
135
+ )
136
+
137
+ return handler
138
+
139
+ def shutdown(self) -> None:
140
+ """No-op shutdown. Lambda functions are stateless."""
141
+ pass
142
+
143
+ def _handle_event(self, event: dict[str, Any]) -> dict[str, Any]:
144
+ """Internal: parse event and execute the job.
145
+
146
+ Args:
147
+ event: Lambda event dict with "job" and optional "kwargs".
148
+
149
+ Returns:
150
+ Dict with "statusCode" and "body".
151
+ """
152
+ assert self._app is not None
153
+
154
+ try:
155
+ job_name = event["job"]
156
+ except (KeyError, TypeError) as exc:
157
+ return {
158
+ "statusCode": 400,
159
+ "body": f"Missing required field 'job' in event: {exc}",
160
+ }
161
+
162
+ job_kwargs = event.get("kwargs", {})
163
+
164
+ try:
165
+ result = self._app.execute(job_name, **job_kwargs)
166
+ return {"statusCode": 200, "body": result.return_value}
167
+ except Exception as exc:
168
+ return {"statusCode": 500, "body": str(exc)}
169
+
170
+
171
+ __all__ = ["LambdaAdapter"]
File without changes
@@ -0,0 +1,44 @@
1
+ """Shared fixtures for functualize-lambda plugin tests."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Any
7
+
8
+ import pytest
9
+
10
+
11
+ @dataclass
12
+ class FakeJobResult:
13
+ status: str = "success"
14
+ return_value: Any = None
15
+ duration_ms: float = 10.0
16
+
17
+
18
+ class FakeApp:
19
+ """Minimal FunctualizeApp fake for Lambda adapter tests."""
20
+
21
+ def __init__(
22
+ self,
23
+ execute_results: dict[str, FakeJobResult] | None = None,
24
+ execute_error: Exception | None = None,
25
+ ):
26
+ self._execute_results = execute_results or {}
27
+ self._execute_error = execute_error
28
+
29
+ def execute(self, job_name: str, **kwargs: Any) -> FakeJobResult:
30
+ if self._execute_error:
31
+ raise self._execute_error
32
+ if job_name in self._execute_results:
33
+ return self._execute_results[job_name]
34
+ return FakeJobResult(return_value=f"executed {job_name}")
35
+
36
+
37
+ @pytest.fixture
38
+ def fake_app() -> FakeApp:
39
+ return FakeApp()
40
+
41
+
42
+ @pytest.fixture
43
+ def failing_app() -> FakeApp:
44
+ return FakeApp(execute_error=RuntimeError("deploy failed"))
@@ -0,0 +1,98 @@
1
+ """Unit tests for functualize-lambda plugin.
2
+
3
+ Tests the Lambda adapter's fat-lambda routing, thin-lambda handler creation,
4
+ and error handling.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import pytest
10
+ from functualize_lambda import LambdaAdapter
11
+
12
+
13
+ class TestFatLambda:
14
+ """Tests for fat-Lambda (internal routing via event['job'])."""
15
+
16
+ def test_routes_to_correct_job(self, fake_app):
17
+ adapter = LambdaAdapter()
18
+ adapter(fake_app)
19
+ result = adapter.run({"job": "deploy", "kwargs": {"env": "prod"}}, None)
20
+ assert result["statusCode"] == 200
21
+ assert result["body"] == "executed deploy"
22
+
23
+ def test_missing_job_field_returns_400(self, fake_app):
24
+ adapter = LambdaAdapter()
25
+ adapter(fake_app)
26
+ result = adapter.run({}, None)
27
+ assert result["statusCode"] == 400
28
+ assert "Missing required field" in result["body"]
29
+
30
+ def test_execution_error_returns_500(self, failing_app):
31
+ adapter = LambdaAdapter()
32
+ adapter(failing_app)
33
+ result = adapter.run({"job": "deploy"}, None)
34
+ assert result["statusCode"] == 500
35
+ assert "deploy failed" in result["body"]
36
+
37
+ def test_run_before_setup_raises(self):
38
+ adapter = LambdaAdapter()
39
+ with pytest.raises(RuntimeError, match="called before __call__"):
40
+ adapter.run({"job": "test"}, None)
41
+
42
+ def test_kwargs_default_to_empty(self, fake_app):
43
+ adapter = LambdaAdapter()
44
+ adapter(fake_app)
45
+ result = adapter.run({"job": "deploy"}, None)
46
+ assert result["statusCode"] == 200
47
+
48
+
49
+ class TestThinLambda:
50
+ """Tests for thin-Lambda (per-job handlers via make_handler)."""
51
+
52
+ def test_make_handler_creates_callable(self, fake_app):
53
+ adapter = LambdaAdapter()
54
+ adapter(fake_app)
55
+ handler = adapter.make_handler("deploy")
56
+ assert callable(handler)
57
+
58
+ def test_handler_executes_bound_job(self, fake_app):
59
+ adapter = LambdaAdapter()
60
+ adapter(fake_app)
61
+ handler = adapter.make_handler("deploy")
62
+ result = handler({"kwargs": {"env": "staging"}}, None)
63
+ assert result["statusCode"] == 200
64
+ assert result["body"] == "executed deploy"
65
+
66
+ def test_handler_error_returns_500(self, failing_app):
67
+ adapter = LambdaAdapter()
68
+ adapter(failing_app)
69
+ handler = adapter.make_handler("deploy")
70
+ result = handler({}, None)
71
+ assert result["statusCode"] == 500
72
+ assert "deploy failed" in result["body"]
73
+
74
+ def test_make_handler_before_setup_raises(self):
75
+ adapter = LambdaAdapter()
76
+ with pytest.raises(RuntimeError, match="called before __call__"):
77
+ adapter.make_handler("deploy")
78
+
79
+ def test_handler_has_descriptive_name(self, fake_app):
80
+ adapter = LambdaAdapter()
81
+ adapter(fake_app)
82
+ handler = adapter.make_handler("deploy")
83
+ assert "deploy" in handler.__name__
84
+
85
+
86
+ class TestAdapterMetadata:
87
+ """Tests for adapter metadata attributes."""
88
+
89
+ def test_has_required_attributes(self):
90
+ adapter = LambdaAdapter()
91
+ assert adapter.name == "functualize-lambda"
92
+ assert adapter.adapter_type == "lambda"
93
+ assert adapter.version
94
+
95
+ def test_shutdown_is_noop(self, fake_app):
96
+ adapter = LambdaAdapter()
97
+ adapter(fake_app)
98
+ adapter.shutdown() # Should not raise