edda-framework 0.2.0__py3-none-any.whl → 0.3.1__py3-none-any.whl

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 @@
1
+ """Edda integrations package."""
@@ -0,0 +1,40 @@
1
+ """
2
+ Edda MCP (Model Context Protocol) Integration.
3
+
4
+ Provides MCP server functionality for Edda durable workflows,
5
+ enabling long-running workflow tools via the MCP protocol.
6
+
7
+ Example:
8
+ ```python
9
+ from edda.integrations.mcp import EddaMCPServer
10
+ from edda import WorkflowContext, activity
11
+
12
+ server = EddaMCPServer(
13
+ name="Order Service",
14
+ db_url="postgresql://user:pass@localhost/orders",
15
+ )
16
+
17
+ @activity
18
+ async def reserve_inventory(ctx, items):
19
+ return {"reserved": True}
20
+
21
+ @server.durable_tool(description="Process order workflow")
22
+ async def process_order(ctx: WorkflowContext, order_id: str):
23
+ await reserve_inventory(ctx, [order_id], activity_id="reserve:1")
24
+ return {"status": "completed"}
25
+
26
+ # Deploy with uvicorn
27
+ if __name__ == "__main__":
28
+ import uvicorn
29
+ uvicorn.run(server.asgi_app(), host="0.0.0.0", port=8000)
30
+ ```
31
+
32
+ The server automatically generates three MCP tools for each @durable_tool:
33
+ - `tool_name`: Start the workflow, returns instance_id
34
+ - `tool_name_status`: Check workflow status
35
+ - `tool_name_result`: Get workflow result (if completed)
36
+ """
37
+
38
+ from edda.integrations.mcp.server import EddaMCPServer
39
+
40
+ __all__ = ["EddaMCPServer"]
@@ -0,0 +1,188 @@
1
+ """Decorators for MCP durable tools."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import inspect
6
+ from collections.abc import Callable
7
+ from typing import TYPE_CHECKING, Any, cast
8
+
9
+ from edda.workflow import workflow
10
+
11
+ if TYPE_CHECKING:
12
+ from edda.integrations.mcp.server import EddaMCPServer
13
+ from edda.workflow import Workflow
14
+
15
+
16
+ def create_durable_tool(
17
+ server: EddaMCPServer,
18
+ func: Callable[..., Any],
19
+ *,
20
+ description: str = "",
21
+ ) -> Workflow:
22
+ """
23
+ Create a durable workflow tool with auto-generated status/result tools.
24
+
25
+ This function:
26
+ 1. Wraps the function as an Edda @workflow
27
+ 2. Registers three MCP tools:
28
+ - {name}: Start workflow, return instance_id
29
+ - {name}_status: Check workflow status
30
+ - {name}_result: Get workflow result
31
+
32
+ Args:
33
+ server: EddaMCPServer instance
34
+ func: Async workflow function
35
+ description: Tool description
36
+
37
+ Returns:
38
+ Workflow instance
39
+ """
40
+ # 1. Create Edda workflow
41
+ workflow_instance = cast(Workflow, workflow(func, event_handler=False))
42
+ workflow_name = func.__name__
43
+
44
+ # Register in server's workflow registry
45
+ server._workflows[workflow_name] = workflow_instance
46
+
47
+ # 2. Generate main tool (start workflow)
48
+ tool_description = description or func.__doc__ or f"Start {workflow_name} workflow"
49
+
50
+ # Extract parameters from workflow function (excluding ctx)
51
+ sig = inspect.signature(func)
52
+ params = [
53
+ param
54
+ for name, param in sig.parameters.items()
55
+ if name != "ctx" # Exclude WorkflowContext parameter
56
+ ]
57
+
58
+ # Create the tool function
59
+ async def start_tool(**kwargs: Any) -> dict[str, Any]:
60
+ """
61
+ Start workflow and return instance_id.
62
+
63
+ This is the main entry point for the durable tool.
64
+ """
65
+ # Remove 'ctx' if provided by client (workflow will inject it)
66
+ kwargs.pop("ctx", None)
67
+
68
+ # Start Edda workflow
69
+ instance_id = await workflow_instance.start(**kwargs)
70
+
71
+ # Return MCP-compliant response
72
+ return {
73
+ "content": [
74
+ {
75
+ "type": "text",
76
+ "text": (
77
+ f"Workflow '{workflow_name}' started successfully.\n"
78
+ f"Instance ID: {instance_id}\n\n"
79
+ f"Use '{workflow_name}_status' tool with instance_id='{instance_id}' to check progress.\n"
80
+ f"Use '{workflow_name}_result' tool to get the final result once completed."
81
+ ),
82
+ }
83
+ ],
84
+ "isError": False,
85
+ }
86
+
87
+ # Override the function's signature for introspection (FastMCP uses this for schema generation)
88
+ start_tool.__signature__ = inspect.Signature(parameters=params) # type: ignore[attr-defined]
89
+
90
+ # Register with FastMCP (call as function, not decorator syntax)
91
+ server._mcp.tool(name=workflow_name, description=tool_description)(start_tool)
92
+
93
+ # 3. Generate status tool
94
+ status_tool_name = f"{workflow_name}_status"
95
+ status_tool_description = f"Check status of {workflow_name} workflow"
96
+
97
+ @server._mcp.tool(name=status_tool_name, description=status_tool_description) # type: ignore[misc]
98
+ async def status_tool(instance_id: str) -> dict[str, Any]:
99
+ """Check workflow status."""
100
+ try:
101
+ instance = await server._edda_app.storage.get_instance(instance_id)
102
+ if instance is None:
103
+ return {
104
+ "content": [
105
+ {
106
+ "type": "text",
107
+ "text": f"Workflow instance not found: {instance_id}",
108
+ }
109
+ ],
110
+ "isError": True,
111
+ }
112
+
113
+ status = instance["status"]
114
+ current_activity_id = instance.get("current_activity_id", "N/A")
115
+
116
+ status_text = (
117
+ f"Workflow Status: {status}\n"
118
+ f"Current Activity: {current_activity_id}\n"
119
+ f"Instance ID: {instance_id}"
120
+ )
121
+
122
+ return {
123
+ "content": [{"type": "text", "text": status_text}],
124
+ "isError": False,
125
+ }
126
+ except Exception as e:
127
+ return {
128
+ "content": [
129
+ {
130
+ "type": "text",
131
+ "text": f"Error checking status: {str(e)}",
132
+ }
133
+ ],
134
+ "isError": True,
135
+ }
136
+
137
+ # 4. Generate result tool
138
+ result_tool_name = f"{workflow_name}_result"
139
+ result_tool_description = f"Get result of {workflow_name} workflow (if completed)"
140
+
141
+ @server._mcp.tool(name=result_tool_name, description=result_tool_description) # type: ignore[misc]
142
+ async def result_tool(instance_id: str) -> dict[str, Any]:
143
+ """Get workflow result (if completed)."""
144
+ try:
145
+ instance = await server._edda_app.storage.get_instance(instance_id)
146
+ if instance is None:
147
+ return {
148
+ "content": [
149
+ {
150
+ "type": "text",
151
+ "text": f"Workflow instance not found: {instance_id}",
152
+ }
153
+ ],
154
+ "isError": True,
155
+ }
156
+
157
+ status = instance["status"]
158
+
159
+ if status != "completed":
160
+ return {
161
+ "content": [
162
+ {
163
+ "type": "text",
164
+ "text": f"Workflow not completed yet. Current status: {status}",
165
+ }
166
+ ],
167
+ "isError": True,
168
+ }
169
+
170
+ output_data = instance.get("output_data")
171
+ result_text = f"Workflow Result:\n{output_data}"
172
+
173
+ return {
174
+ "content": [{"type": "text", "text": result_text}],
175
+ "isError": False,
176
+ }
177
+ except Exception as e:
178
+ return {
179
+ "content": [
180
+ {
181
+ "type": "text",
182
+ "text": f"Error getting result: {str(e)}",
183
+ }
184
+ ],
185
+ "isError": True,
186
+ }
187
+
188
+ return workflow_instance
@@ -0,0 +1,261 @@
1
+ """MCP Server implementation for Edda workflows."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Callable
6
+ from typing import Any, cast
7
+
8
+ from edda.app import EddaApp
9
+ from edda.workflow import Workflow
10
+
11
+ try:
12
+ from mcp.server.fastmcp import FastMCP # type: ignore[import-not-found]
13
+ except ImportError as e:
14
+ raise ImportError(
15
+ "MCP Python SDK is required for MCP integration. "
16
+ "Install it with: pip install edda-framework[mcp]"
17
+ ) from e
18
+
19
+
20
+ class EddaMCPServer:
21
+ """
22
+ MCP (Model Context Protocol) server for Edda durable workflows.
23
+
24
+ Integrates EddaApp (CloudEvents + Workflows) with FastMCP to provide
25
+ long-running workflow tools via the MCP protocol.
26
+
27
+ Example:
28
+ ```python
29
+ from edda.integrations.mcp import EddaMCPServer
30
+ from edda import WorkflowContext, activity
31
+
32
+ server = EddaMCPServer(
33
+ name="Order Service",
34
+ db_url="postgresql://user:pass@localhost/orders",
35
+ )
36
+
37
+ @activity
38
+ async def reserve_inventory(ctx, items):
39
+ return {"reserved": True}
40
+
41
+ @server.durable_tool(description="Process order workflow")
42
+ async def process_order(ctx: WorkflowContext, order_id: str):
43
+ await reserve_inventory(ctx, [order_id], activity_id="reserve:1")
44
+ return {"status": "completed"}
45
+
46
+ # Deploy with uvicorn (HTTP transport)
47
+ if __name__ == "__main__":
48
+ import uvicorn
49
+ uvicorn.run(server.asgi_app(), host="0.0.0.0", port=8000)
50
+
51
+ # Or deploy with stdio (for MCP clients, e.g., Claude Desktop)
52
+ if __name__ == "__main__":
53
+ import asyncio
54
+
55
+ async def main():
56
+ await server.initialize()
57
+ await server.run_stdio()
58
+
59
+ asyncio.run(main())
60
+ ```
61
+
62
+ The server automatically generates three MCP tools for each @durable_tool:
63
+ - `tool_name`: Start the workflow, returns instance_id
64
+ - `tool_name_status`: Check workflow status
65
+ - `tool_name_result`: Get workflow result (if completed)
66
+ """
67
+
68
+ def __init__(
69
+ self,
70
+ name: str,
71
+ db_url: str,
72
+ *,
73
+ outbox_enabled: bool = False,
74
+ broker_url: str | None = None,
75
+ token_verifier: Callable[[str], bool] | None = None,
76
+ ):
77
+ """
78
+ Initialize MCP server.
79
+
80
+ Args:
81
+ name: Service name (shown in MCP client)
82
+ db_url: Database URL for workflow storage
83
+ outbox_enabled: Enable transactional outbox pattern
84
+ broker_url: Message broker URL (if outbox enabled)
85
+ token_verifier: Optional function to verify authentication tokens
86
+ """
87
+ self._name = name
88
+ self._edda_app = EddaApp(
89
+ service_name=name,
90
+ db_url=db_url,
91
+ outbox_enabled=outbox_enabled,
92
+ broker_url=broker_url or "",
93
+ )
94
+ self._mcp = FastMCP(name, json_response=True, stateless_http=True)
95
+ self._token_verifier = token_verifier
96
+
97
+ # Registry of durable tools (workflow_name -> Workflow instance)
98
+ self._workflows: dict[str, Workflow] = {}
99
+
100
+ def durable_tool(
101
+ self,
102
+ func: Callable[..., Any] | None = None,
103
+ *,
104
+ description: str = "",
105
+ ) -> Callable[..., Any]:
106
+ """
107
+ Decorator to define a durable workflow tool.
108
+
109
+ Automatically generates three MCP tools:
110
+ 1. Main tool: Starts the workflow, returns instance_id
111
+ 2. Status tool: Checks workflow status
112
+ 3. Result tool: Gets workflow result (if completed)
113
+
114
+ Args:
115
+ func: Workflow function (async)
116
+ description: Tool description for MCP clients
117
+
118
+ Returns:
119
+ Decorated workflow instance
120
+
121
+ Example:
122
+ ```python
123
+ @server.durable_tool(description="Long-running order processing")
124
+ async def process_order(ctx, order_id: str):
125
+ # Workflow logic
126
+ return {"status": "completed"}
127
+ ```
128
+ """
129
+ from edda.integrations.mcp.decorators import create_durable_tool
130
+
131
+ def decorator(f: Callable[..., Any]) -> Workflow:
132
+ return create_durable_tool(self, f, description=description)
133
+
134
+ if func is None:
135
+ return decorator
136
+ return decorator(func)
137
+
138
+ def asgi_app(self) -> Callable[..., Any]:
139
+ """
140
+ Create ASGI application with MCP + CloudEvents support.
141
+
142
+ This method uses the Issue #1367 workaround: instead of using Mount,
143
+ we get the MCP's Starlette app directly and add Edda endpoints to it.
144
+
145
+ Routing:
146
+ - POST / -> FastMCP (MCP tools via streamable HTTP)
147
+ - POST /cancel/{instance_id} -> Workflow cancellation
148
+ - Other POST -> CloudEvents
149
+
150
+ Returns:
151
+ ASGI callable (Starlette app)
152
+ """
153
+ from starlette.requests import Request # type: ignore[import-not-found]
154
+ from starlette.responses import Response # type: ignore[import-not-found]
155
+
156
+ # Get MCP's Starlette app (Issue #1367 workaround: use directly)
157
+ app = self._mcp.streamable_http_app()
158
+
159
+ # Add Edda endpoints to Starlette router BEFORE wrapping with middleware
160
+ # Note: MCP's streamable HTTP is already mounted at "/" by default
161
+ # We add additional routes for Edda's CloudEvents and cancellation
162
+
163
+ async def edda_cancel_handler(request: Request) -> Response:
164
+ """Handle workflow cancellation."""
165
+ instance_id = request.path_params["instance_id"]
166
+
167
+ # Create ASGI scope for EddaApp
168
+ scope = dict(request.scope)
169
+ scope["path"] = f"/cancel/{instance_id}"
170
+
171
+ # Capture response
172
+ response_data: dict[str, Any] = {"status": 200, "headers": [], "body": b""}
173
+
174
+ async def send(message: dict[str, Any]) -> None:
175
+ if message["type"] == "http.response.start":
176
+ response_data["status"] = message["status"]
177
+ response_data["headers"] = message.get("headers", [])
178
+ elif message["type"] == "http.response.body":
179
+ response_data["body"] += message.get("body", b"")
180
+
181
+ # Forward to EddaApp
182
+ await self._edda_app(scope, request.receive, send)
183
+
184
+ # Return response
185
+ return Response(
186
+ content=response_data["body"],
187
+ status_code=response_data["status"],
188
+ headers=cast(dict[str, str], dict(response_data["headers"])),
189
+ )
190
+
191
+ # Add cancel route
192
+ app.router.add_route("/cancel/{instance_id}", edda_cancel_handler, methods=["POST"])
193
+
194
+ # Add authentication middleware if token_verifier provided (AFTER adding routes)
195
+ if self._token_verifier is not None:
196
+ from starlette.middleware.base import ( # type: ignore[import-not-found]
197
+ BaseHTTPMiddleware,
198
+ )
199
+
200
+ class AuthMiddleware(BaseHTTPMiddleware): # type: ignore[misc]
201
+ def __init__(self, app: Any, token_verifier: Callable[[str], bool]):
202
+ super().__init__(app)
203
+ self.token_verifier = token_verifier
204
+
205
+ async def dispatch(
206
+ self, request: Request, call_next: Callable[..., Any]
207
+ ) -> Response:
208
+ auth_header = request.headers.get("authorization", "")
209
+ if auth_header.startswith("Bearer "):
210
+ token = auth_header[7:]
211
+ if not self.token_verifier(token):
212
+ return Response("Unauthorized", status_code=401)
213
+ return await call_next(request)
214
+
215
+ # Wrap app with auth middleware
216
+ app = AuthMiddleware(app, self._token_verifier)
217
+
218
+ return cast(Callable[..., Any], app)
219
+
220
+ async def initialize(self) -> None:
221
+ """
222
+ Initialize the EddaApp (setup replay engine, storage, etc.).
223
+
224
+ This method must be called before running the server in stdio mode.
225
+ For HTTP mode (asgi_app()), initialization happens automatically
226
+ when the ASGI app is deployed.
227
+
228
+ Example:
229
+ ```python
230
+ async def main():
231
+ await server.initialize()
232
+ await server.run_stdio()
233
+
234
+ if __name__ == "__main__":
235
+ import asyncio
236
+ asyncio.run(main())
237
+ ```
238
+ """
239
+ await self._edda_app.initialize()
240
+
241
+ async def run_stdio(self) -> None:
242
+ """
243
+ Run MCP server with stdio transport (for MCP clients, e.g., Claude Desktop).
244
+
245
+ This method uses stdin/stdout for JSON-RPC communication.
246
+ stderr can be used for diagnostic messages.
247
+
248
+ The server will block until terminated (Ctrl+C or SIGTERM).
249
+
250
+ Example:
251
+ ```python
252
+ async def main():
253
+ await server.initialize()
254
+ await server.run_stdio()
255
+
256
+ if __name__ == "__main__":
257
+ import asyncio
258
+ asyncio.run(main())
259
+ ```
260
+ """
261
+ await self._mcp.run_stdio_async()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: edda-framework
3
- Version: 0.2.0
3
+ Version: 0.3.1
4
4
  Summary: Lightweight Durable Execution Framework
5
5
  Project-URL: Homepage, https://github.com/i2y/edda
6
6
  Project-URL: Documentation, https://github.com/i2y/edda#readme
@@ -38,6 +38,8 @@ Requires-Dist: ruff>=0.14.2; extra == 'dev'
38
38
  Requires-Dist: testcontainers[mysql]>=4.0.0; extra == 'dev'
39
39
  Requires-Dist: testcontainers[postgres]>=4.0.0; extra == 'dev'
40
40
  Requires-Dist: tsuno>=0.1.3; extra == 'dev'
41
+ Provides-Extra: mcp
42
+ Requires-Dist: mcp>=1.22.0; extra == 'mcp'
41
43
  Provides-Extra: mysql
42
44
  Requires-Dist: aiomysql>=0.2.0; extra == 'mysql'
43
45
  Provides-Extra: postgresql
@@ -77,6 +79,7 @@ For detailed documentation, visit [https://i2y.github.io/edda/](https://i2y.gith
77
79
  - 📦 **Transactional Outbox**: Reliable event publishing with guaranteed delivery
78
80
  - ☁️ **CloudEvents Support**: Native support for CloudEvents protocol
79
81
  - ⏱️ **Event & Timer Waiting**: Free up worker resources while waiting for events or timers, resume on any available worker
82
+ - 🤖 **MCP Integration**: Expose durable workflows as AI tools via Model Context Protocol
80
83
  - 🌍 **ASGI/WSGI Support**: Deploy with your preferred server (uvicorn, gunicorn, uWSGI)
81
84
 
82
85
  ## Use Cases
@@ -686,6 +689,49 @@ async def payment_workflow(ctx: WorkflowContext, order_id: str) -> dict:
686
689
 
687
690
  **Performance note**: ASGI servers (uvicorn, hypercorn) are recommended for better performance with Edda's async architecture. WSGI support is provided for compatibility with existing infrastructure and users who prefer synchronous programming.
688
691
 
692
+ ## MCP Integration
693
+
694
+ Edda integrates with the [Model Context Protocol (MCP)](https://modelcontextprotocol.io/), allowing AI assistants like Claude to interact with your durable workflows as long-running tools.
695
+
696
+ ### Quick Example
697
+
698
+ ```python
699
+ from edda.integrations.mcp import EddaMCPServer
700
+ from edda import WorkflowContext, activity
701
+
702
+ # Create MCP server
703
+ server = EddaMCPServer(
704
+ name="Order Service",
705
+ db_url="postgresql://user:pass@localhost/orders",
706
+ )
707
+
708
+ @activity
709
+ async def process_payment(ctx: WorkflowContext, amount: float):
710
+ return {"status": "paid", "amount": amount}
711
+
712
+ @server.durable_tool(description="Process customer order")
713
+ async def process_order(ctx: WorkflowContext, order_id: str):
714
+ await process_payment(ctx, 99.99)
715
+ return {"status": "completed", "order_id": order_id}
716
+
717
+ # Deploy with uvicorn
718
+ if __name__ == "__main__":
719
+ import uvicorn
720
+ uvicorn.run(server.asgi_app(), host="0.0.0.0", port=8000)
721
+ ```
722
+
723
+ ### Auto-Generated Tools
724
+
725
+ Each `@durable_tool` automatically generates **three MCP tools**:
726
+
727
+ 1. **Main tool** (`process_order`): Starts the workflow, returns instance ID
728
+ 2. **Status tool** (`process_order_status`): Checks workflow progress
729
+ 3. **Result tool** (`process_order_result`): Gets final result when completed
730
+
731
+ This enables AI assistants to work with workflows that take minutes, hours, or even days to complete.
732
+
733
+ **For detailed documentation**, see [MCP Integration Guide](docs/integrations/mcp.md).
734
+
689
735
  ## Observability Hooks
690
736
 
691
737
  Extend Edda with custom observability without coupling to specific tools:
@@ -12,6 +12,10 @@ edda/replay.py,sha256=5RIRd0q2ZrH9iiiy35eOUii2cipYg9dlua56OAXvIk4,32499
12
12
  edda/retry.py,sha256=t4_E1skrhotA1XWHTLbKi-DOgCMasOUnhI9OT-O_eCE,6843
13
13
  edda/workflow.py,sha256=daSppYAzgXkjY_9-HS93Zi7_tPR6srmchxY5YfwgU-4,7239
14
14
  edda/wsgi.py,sha256=1pGE5fhHpcsYnDR8S3NEFKWUs5P0JK4roTAzX9BsIj0,2391
15
+ edda/integrations/__init__.py,sha256=F_CaTvlDEbldfOpPKq_U9ve1E573tS6XzqXnOtyHcXI,33
16
+ edda/integrations/mcp/__init__.py,sha256=YK-8m0DIdP-RSqewlIX7xnWU7TD3NioCiW2_aZSgnn8,1232
17
+ edda/integrations/mcp/decorators.py,sha256=MIHDrcP_OelxOXISkX6a561Gl3DcVNT9yRakd4O4COo,6333
18
+ edda/integrations/mcp/server.py,sha256=sSemdl7uR133YgZn34uUWzjJUqnPi13HauzXOrf8Kq8,9175
15
19
  edda/outbox/__init__.py,sha256=azXG1rtheJEjOyoWmMsBeR2jp8Bz02R3wDEd5tQnaWA,424
16
20
  edda/outbox/relayer.py,sha256=2tnN1aOQ8pKWfwEGIlYwYLLwyOKXBjZ4XZsIr1HjgK4,9454
17
21
  edda/outbox/transactional.py,sha256=LFfUjunqRlGibaINi-efGXFFivWGO7v3mhqrqyGW6Nw,3808
@@ -29,8 +33,8 @@ edda/viewer_ui/data_service.py,sha256=mXV6bL6REa_UKsk8xMGBIFbsbLpIxe91lX3wgn-FOj
29
33
  edda/visualizer/__init__.py,sha256=DOpDstNhR0VcXAs_eMKxaL30p_0u4PKZ4o2ndnYhiRo,343
30
34
  edda/visualizer/ast_analyzer.py,sha256=plmx7C9X_X35xLY80jxOL3ljg3afXxBePRZubqUIkxY,13663
31
35
  edda/visualizer/mermaid_generator.py,sha256=XWa2egoOTNDfJEjPcwoxwQmblUqXf7YInWFjFRI1QGo,12457
32
- edda_framework-0.2.0.dist-info/METADATA,sha256=9qiWE872ENCjRcSC4ezcz5y1v1g5TfpLRgIiQUJXgHc,27823
33
- edda_framework-0.2.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
34
- edda_framework-0.2.0.dist-info/entry_points.txt,sha256=dPH47s6UoJgUZxHoeSMqZsQkLaSE-SGLi-gh88k2WrU,48
35
- edda_framework-0.2.0.dist-info/licenses/LICENSE,sha256=udxb-V7_cYKTHqW7lNm48rxJ-Zpf0WAY_PyGDK9BPCo,1069
36
- edda_framework-0.2.0.dist-info/RECORD,,
36
+ edda_framework-0.3.1.dist-info/METADATA,sha256=uOiXJuq8Xy4fgfyblF3FwbJpo-cJrXVWgHhoIjdIi9s,29421
37
+ edda_framework-0.3.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
38
+ edda_framework-0.3.1.dist-info/entry_points.txt,sha256=dPH47s6UoJgUZxHoeSMqZsQkLaSE-SGLi-gh88k2WrU,48
39
+ edda_framework-0.3.1.dist-info/licenses/LICENSE,sha256=udxb-V7_cYKTHqW7lNm48rxJ-Zpf0WAY_PyGDK9BPCo,1069
40
+ edda_framework-0.3.1.dist-info/RECORD,,