hexdag 0.5.0.dev1__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.
Files changed (261) hide show
  1. hexdag/__init__.py +116 -0
  2. hexdag/__main__.py +30 -0
  3. hexdag/adapters/executors/__init__.py +5 -0
  4. hexdag/adapters/executors/local_executor.py +316 -0
  5. hexdag/builtin/__init__.py +6 -0
  6. hexdag/builtin/adapters/__init__.py +51 -0
  7. hexdag/builtin/adapters/anthropic/__init__.py +5 -0
  8. hexdag/builtin/adapters/anthropic/anthropic_adapter.py +151 -0
  9. hexdag/builtin/adapters/database/__init__.py +6 -0
  10. hexdag/builtin/adapters/database/csv/csv_adapter.py +249 -0
  11. hexdag/builtin/adapters/database/pgvector/__init__.py +5 -0
  12. hexdag/builtin/adapters/database/pgvector/pgvector_adapter.py +478 -0
  13. hexdag/builtin/adapters/database/sqlalchemy/sqlalchemy_adapter.py +252 -0
  14. hexdag/builtin/adapters/database/sqlite/__init__.py +5 -0
  15. hexdag/builtin/adapters/database/sqlite/sqlite_adapter.py +410 -0
  16. hexdag/builtin/adapters/local/README.md +59 -0
  17. hexdag/builtin/adapters/local/__init__.py +7 -0
  18. hexdag/builtin/adapters/local/local_observer_manager.py +696 -0
  19. hexdag/builtin/adapters/memory/__init__.py +47 -0
  20. hexdag/builtin/adapters/memory/file_memory_adapter.py +297 -0
  21. hexdag/builtin/adapters/memory/in_memory_memory.py +216 -0
  22. hexdag/builtin/adapters/memory/schemas.py +57 -0
  23. hexdag/builtin/adapters/memory/session_memory.py +178 -0
  24. hexdag/builtin/adapters/memory/sqlite_memory_adapter.py +215 -0
  25. hexdag/builtin/adapters/memory/state_memory.py +280 -0
  26. hexdag/builtin/adapters/mock/README.md +89 -0
  27. hexdag/builtin/adapters/mock/__init__.py +15 -0
  28. hexdag/builtin/adapters/mock/hexdag.toml +50 -0
  29. hexdag/builtin/adapters/mock/mock_database.py +225 -0
  30. hexdag/builtin/adapters/mock/mock_embedding.py +223 -0
  31. hexdag/builtin/adapters/mock/mock_llm.py +177 -0
  32. hexdag/builtin/adapters/mock/mock_tool_adapter.py +192 -0
  33. hexdag/builtin/adapters/mock/mock_tool_router.py +232 -0
  34. hexdag/builtin/adapters/openai/__init__.py +5 -0
  35. hexdag/builtin/adapters/openai/openai_adapter.py +634 -0
  36. hexdag/builtin/adapters/secret/__init__.py +7 -0
  37. hexdag/builtin/adapters/secret/local_secret_adapter.py +248 -0
  38. hexdag/builtin/adapters/unified_tool_router.py +280 -0
  39. hexdag/builtin/macros/__init__.py +17 -0
  40. hexdag/builtin/macros/conversation_agent.py +390 -0
  41. hexdag/builtin/macros/llm_macro.py +151 -0
  42. hexdag/builtin/macros/reasoning_agent.py +423 -0
  43. hexdag/builtin/macros/tool_macro.py +380 -0
  44. hexdag/builtin/nodes/__init__.py +38 -0
  45. hexdag/builtin/nodes/_discovery.py +123 -0
  46. hexdag/builtin/nodes/agent_node.py +696 -0
  47. hexdag/builtin/nodes/base_node_factory.py +242 -0
  48. hexdag/builtin/nodes/composite_node.py +926 -0
  49. hexdag/builtin/nodes/data_node.py +201 -0
  50. hexdag/builtin/nodes/expression_node.py +487 -0
  51. hexdag/builtin/nodes/function_node.py +454 -0
  52. hexdag/builtin/nodes/llm_node.py +491 -0
  53. hexdag/builtin/nodes/loop_node.py +920 -0
  54. hexdag/builtin/nodes/mapped_input.py +518 -0
  55. hexdag/builtin/nodes/port_call_node.py +269 -0
  56. hexdag/builtin/nodes/tool_call_node.py +195 -0
  57. hexdag/builtin/nodes/tool_utils.py +390 -0
  58. hexdag/builtin/prompts/__init__.py +68 -0
  59. hexdag/builtin/prompts/base.py +422 -0
  60. hexdag/builtin/prompts/chat_prompts.py +303 -0
  61. hexdag/builtin/prompts/error_correction_prompts.py +320 -0
  62. hexdag/builtin/prompts/tool_prompts.py +160 -0
  63. hexdag/builtin/tools/builtin_tools.py +84 -0
  64. hexdag/builtin/tools/database_tools.py +164 -0
  65. hexdag/cli/__init__.py +17 -0
  66. hexdag/cli/__main__.py +7 -0
  67. hexdag/cli/commands/__init__.py +27 -0
  68. hexdag/cli/commands/build_cmd.py +812 -0
  69. hexdag/cli/commands/create_cmd.py +208 -0
  70. hexdag/cli/commands/docs_cmd.py +293 -0
  71. hexdag/cli/commands/generate_types_cmd.py +252 -0
  72. hexdag/cli/commands/init_cmd.py +188 -0
  73. hexdag/cli/commands/pipeline_cmd.py +494 -0
  74. hexdag/cli/commands/plugin_dev_cmd.py +529 -0
  75. hexdag/cli/commands/plugins_cmd.py +441 -0
  76. hexdag/cli/commands/studio_cmd.py +101 -0
  77. hexdag/cli/commands/validate_cmd.py +221 -0
  78. hexdag/cli/main.py +84 -0
  79. hexdag/core/__init__.py +83 -0
  80. hexdag/core/config/__init__.py +20 -0
  81. hexdag/core/config/loader.py +479 -0
  82. hexdag/core/config/models.py +150 -0
  83. hexdag/core/configurable.py +294 -0
  84. hexdag/core/context/__init__.py +37 -0
  85. hexdag/core/context/execution_context.py +378 -0
  86. hexdag/core/docs/__init__.py +26 -0
  87. hexdag/core/docs/extractors.py +678 -0
  88. hexdag/core/docs/generators.py +890 -0
  89. hexdag/core/docs/models.py +120 -0
  90. hexdag/core/domain/__init__.py +10 -0
  91. hexdag/core/domain/dag.py +1225 -0
  92. hexdag/core/exceptions.py +234 -0
  93. hexdag/core/expression_parser.py +569 -0
  94. hexdag/core/logging.py +449 -0
  95. hexdag/core/models/__init__.py +17 -0
  96. hexdag/core/models/base.py +138 -0
  97. hexdag/core/orchestration/__init__.py +46 -0
  98. hexdag/core/orchestration/body_executor.py +481 -0
  99. hexdag/core/orchestration/components/__init__.py +97 -0
  100. hexdag/core/orchestration/components/adapter_lifecycle_manager.py +113 -0
  101. hexdag/core/orchestration/components/checkpoint_manager.py +134 -0
  102. hexdag/core/orchestration/components/execution_coordinator.py +360 -0
  103. hexdag/core/orchestration/components/health_check_manager.py +176 -0
  104. hexdag/core/orchestration/components/input_mapper.py +143 -0
  105. hexdag/core/orchestration/components/lifecycle_manager.py +583 -0
  106. hexdag/core/orchestration/components/node_executor.py +377 -0
  107. hexdag/core/orchestration/components/secret_manager.py +202 -0
  108. hexdag/core/orchestration/components/wave_executor.py +158 -0
  109. hexdag/core/orchestration/constants.py +17 -0
  110. hexdag/core/orchestration/events/README.md +312 -0
  111. hexdag/core/orchestration/events/__init__.py +104 -0
  112. hexdag/core/orchestration/events/batching.py +330 -0
  113. hexdag/core/orchestration/events/decorators.py +139 -0
  114. hexdag/core/orchestration/events/events.py +573 -0
  115. hexdag/core/orchestration/events/observers/__init__.py +30 -0
  116. hexdag/core/orchestration/events/observers/core_observers.py +690 -0
  117. hexdag/core/orchestration/events/observers/models.py +111 -0
  118. hexdag/core/orchestration/events/taxonomy.py +269 -0
  119. hexdag/core/orchestration/hook_context.py +237 -0
  120. hexdag/core/orchestration/hooks.py +437 -0
  121. hexdag/core/orchestration/models.py +418 -0
  122. hexdag/core/orchestration/orchestrator.py +910 -0
  123. hexdag/core/orchestration/orchestrator_factory.py +275 -0
  124. hexdag/core/orchestration/port_wrappers.py +327 -0
  125. hexdag/core/orchestration/prompt/__init__.py +32 -0
  126. hexdag/core/orchestration/prompt/template.py +332 -0
  127. hexdag/core/pipeline_builder/__init__.py +21 -0
  128. hexdag/core/pipeline_builder/component_instantiator.py +386 -0
  129. hexdag/core/pipeline_builder/include_tag.py +265 -0
  130. hexdag/core/pipeline_builder/pipeline_config.py +133 -0
  131. hexdag/core/pipeline_builder/py_tag.py +223 -0
  132. hexdag/core/pipeline_builder/tag_discovery.py +268 -0
  133. hexdag/core/pipeline_builder/yaml_builder.py +1196 -0
  134. hexdag/core/pipeline_builder/yaml_validator.py +569 -0
  135. hexdag/core/ports/__init__.py +65 -0
  136. hexdag/core/ports/api_call.py +133 -0
  137. hexdag/core/ports/database.py +489 -0
  138. hexdag/core/ports/embedding.py +215 -0
  139. hexdag/core/ports/executor.py +237 -0
  140. hexdag/core/ports/file_storage.py +117 -0
  141. hexdag/core/ports/healthcheck.py +87 -0
  142. hexdag/core/ports/llm.py +551 -0
  143. hexdag/core/ports/memory.py +70 -0
  144. hexdag/core/ports/observer_manager.py +130 -0
  145. hexdag/core/ports/secret.py +145 -0
  146. hexdag/core/ports/tool_router.py +94 -0
  147. hexdag/core/ports_builder.py +623 -0
  148. hexdag/core/protocols.py +273 -0
  149. hexdag/core/resolver.py +304 -0
  150. hexdag/core/schema/__init__.py +9 -0
  151. hexdag/core/schema/generator.py +742 -0
  152. hexdag/core/secrets.py +242 -0
  153. hexdag/core/types.py +413 -0
  154. hexdag/core/utils/async_warnings.py +206 -0
  155. hexdag/core/utils/schema_conversion.py +78 -0
  156. hexdag/core/utils/sql_validation.py +86 -0
  157. hexdag/core/validation/secure_json.py +148 -0
  158. hexdag/core/yaml_macro.py +517 -0
  159. hexdag/mcp_server.py +3120 -0
  160. hexdag/studio/__init__.py +10 -0
  161. hexdag/studio/build_ui.py +92 -0
  162. hexdag/studio/server/__init__.py +1 -0
  163. hexdag/studio/server/main.py +100 -0
  164. hexdag/studio/server/routes/__init__.py +9 -0
  165. hexdag/studio/server/routes/execute.py +208 -0
  166. hexdag/studio/server/routes/export.py +558 -0
  167. hexdag/studio/server/routes/files.py +207 -0
  168. hexdag/studio/server/routes/plugins.py +419 -0
  169. hexdag/studio/server/routes/validate.py +220 -0
  170. hexdag/studio/ui/index.html +13 -0
  171. hexdag/studio/ui/package-lock.json +2992 -0
  172. hexdag/studio/ui/package.json +31 -0
  173. hexdag/studio/ui/postcss.config.js +6 -0
  174. hexdag/studio/ui/public/hexdag.svg +5 -0
  175. hexdag/studio/ui/src/App.tsx +251 -0
  176. hexdag/studio/ui/src/components/Canvas.tsx +408 -0
  177. hexdag/studio/ui/src/components/ContextMenu.tsx +187 -0
  178. hexdag/studio/ui/src/components/FileBrowser.tsx +123 -0
  179. hexdag/studio/ui/src/components/Header.tsx +181 -0
  180. hexdag/studio/ui/src/components/HexdagNode.tsx +193 -0
  181. hexdag/studio/ui/src/components/NodeInspector.tsx +512 -0
  182. hexdag/studio/ui/src/components/NodePalette.tsx +262 -0
  183. hexdag/studio/ui/src/components/NodePortsSection.tsx +403 -0
  184. hexdag/studio/ui/src/components/PluginManager.tsx +347 -0
  185. hexdag/studio/ui/src/components/PortsEditor.tsx +481 -0
  186. hexdag/studio/ui/src/components/PythonEditor.tsx +195 -0
  187. hexdag/studio/ui/src/components/ValidationPanel.tsx +105 -0
  188. hexdag/studio/ui/src/components/YamlEditor.tsx +196 -0
  189. hexdag/studio/ui/src/components/index.ts +8 -0
  190. hexdag/studio/ui/src/index.css +92 -0
  191. hexdag/studio/ui/src/main.tsx +10 -0
  192. hexdag/studio/ui/src/types/index.ts +123 -0
  193. hexdag/studio/ui/src/vite-env.d.ts +1 -0
  194. hexdag/studio/ui/tailwind.config.js +29 -0
  195. hexdag/studio/ui/tsconfig.json +37 -0
  196. hexdag/studio/ui/tsconfig.node.json +13 -0
  197. hexdag/studio/ui/vite.config.ts +35 -0
  198. hexdag/visualization/__init__.py +69 -0
  199. hexdag/visualization/dag_visualizer.py +1020 -0
  200. hexdag-0.5.0.dev1.dist-info/METADATA +369 -0
  201. hexdag-0.5.0.dev1.dist-info/RECORD +261 -0
  202. hexdag-0.5.0.dev1.dist-info/WHEEL +4 -0
  203. hexdag-0.5.0.dev1.dist-info/entry_points.txt +4 -0
  204. hexdag-0.5.0.dev1.dist-info/licenses/LICENSE +190 -0
  205. hexdag_plugins/.gitignore +43 -0
  206. hexdag_plugins/README.md +73 -0
  207. hexdag_plugins/__init__.py +1 -0
  208. hexdag_plugins/azure/LICENSE +21 -0
  209. hexdag_plugins/azure/README.md +414 -0
  210. hexdag_plugins/azure/__init__.py +21 -0
  211. hexdag_plugins/azure/azure_blob_adapter.py +450 -0
  212. hexdag_plugins/azure/azure_cosmos_adapter.py +383 -0
  213. hexdag_plugins/azure/azure_keyvault_adapter.py +314 -0
  214. hexdag_plugins/azure/azure_openai_adapter.py +415 -0
  215. hexdag_plugins/azure/pyproject.toml +107 -0
  216. hexdag_plugins/azure/tests/__init__.py +1 -0
  217. hexdag_plugins/azure/tests/test_azure_blob_adapter.py +350 -0
  218. hexdag_plugins/azure/tests/test_azure_cosmos_adapter.py +323 -0
  219. hexdag_plugins/azure/tests/test_azure_keyvault_adapter.py +330 -0
  220. hexdag_plugins/azure/tests/test_azure_openai_adapter.py +329 -0
  221. hexdag_plugins/hexdag_etl/README.md +168 -0
  222. hexdag_plugins/hexdag_etl/__init__.py +53 -0
  223. hexdag_plugins/hexdag_etl/examples/01_simple_pandas_transform.py +270 -0
  224. hexdag_plugins/hexdag_etl/examples/02_simple_pandas_only.py +149 -0
  225. hexdag_plugins/hexdag_etl/examples/03_file_io_pipeline.py +109 -0
  226. hexdag_plugins/hexdag_etl/examples/test_pandas_transform.py +84 -0
  227. hexdag_plugins/hexdag_etl/hexdag.toml +25 -0
  228. hexdag_plugins/hexdag_etl/hexdag_etl/__init__.py +48 -0
  229. hexdag_plugins/hexdag_etl/hexdag_etl/nodes/__init__.py +13 -0
  230. hexdag_plugins/hexdag_etl/hexdag_etl/nodes/api_extract.py +230 -0
  231. hexdag_plugins/hexdag_etl/hexdag_etl/nodes/base_node_factory.py +181 -0
  232. hexdag_plugins/hexdag_etl/hexdag_etl/nodes/file_io.py +415 -0
  233. hexdag_plugins/hexdag_etl/hexdag_etl/nodes/outlook.py +492 -0
  234. hexdag_plugins/hexdag_etl/hexdag_etl/nodes/pandas_transform.py +563 -0
  235. hexdag_plugins/hexdag_etl/hexdag_etl/nodes/sql_extract_load.py +112 -0
  236. hexdag_plugins/hexdag_etl/pyproject.toml +82 -0
  237. hexdag_plugins/hexdag_etl/test_transform.py +54 -0
  238. hexdag_plugins/hexdag_etl/tests/test_plugin_integration.py +62 -0
  239. hexdag_plugins/mysql_adapter/LICENSE +21 -0
  240. hexdag_plugins/mysql_adapter/README.md +224 -0
  241. hexdag_plugins/mysql_adapter/__init__.py +6 -0
  242. hexdag_plugins/mysql_adapter/mysql_adapter.py +408 -0
  243. hexdag_plugins/mysql_adapter/pyproject.toml +93 -0
  244. hexdag_plugins/mysql_adapter/tests/test_mysql_adapter.py +259 -0
  245. hexdag_plugins/storage/README.md +184 -0
  246. hexdag_plugins/storage/__init__.py +19 -0
  247. hexdag_plugins/storage/file/__init__.py +5 -0
  248. hexdag_plugins/storage/file/local.py +325 -0
  249. hexdag_plugins/storage/ports/__init__.py +5 -0
  250. hexdag_plugins/storage/ports/vector_store.py +236 -0
  251. hexdag_plugins/storage/sql/__init__.py +7 -0
  252. hexdag_plugins/storage/sql/base.py +187 -0
  253. hexdag_plugins/storage/sql/mysql.py +27 -0
  254. hexdag_plugins/storage/sql/postgresql.py +27 -0
  255. hexdag_plugins/storage/tests/__init__.py +1 -0
  256. hexdag_plugins/storage/tests/test_local_file_storage.py +161 -0
  257. hexdag_plugins/storage/tests/test_sql_adapters.py +212 -0
  258. hexdag_plugins/storage/vector/__init__.py +7 -0
  259. hexdag_plugins/storage/vector/chromadb.py +223 -0
  260. hexdag_plugins/storage/vector/in_memory.py +285 -0
  261. hexdag_plugins/storage/vector/pgvector.py +502 -0
@@ -0,0 +1,492 @@
1
+ """Outlook nodes for reading and sending emails via Microsoft Graph API.
2
+
3
+ These nodes provide email integration for ETL pipelines using Microsoft 365/Outlook.
4
+ """
5
+
6
+ from typing import Any
7
+
8
+ from hexdag.builtin.nodes.base_node_factory import BaseNodeFactory
9
+ from hexdag.core.domain.dag import NodeSpec
10
+ from hexdag.core.registry import node
11
+ from hexdag.core.registry.models import NodeSubtype
12
+ from pydantic import BaseModel
13
+
14
+
15
+ class EmailMessage(BaseModel):
16
+ """Model representing an email message."""
17
+
18
+ id: str
19
+ subject: str
20
+ sender: str
21
+ recipients: list[str]
22
+ body: str
23
+ body_preview: str
24
+ received_at: str | None = None
25
+ has_attachments: bool = False
26
+ is_read: bool = False
27
+
28
+
29
+ class OutlookReaderOutput(BaseModel):
30
+ """Output model for OutlookReaderNode."""
31
+
32
+ messages: list[dict[str, Any]]
33
+ count: int
34
+ folder: str
35
+
36
+
37
+ class OutlookSenderOutput(BaseModel):
38
+ """Output model for OutlookSenderNode."""
39
+
40
+ message_id: str
41
+ subject: str
42
+ recipients: list[str]
43
+ success: bool
44
+
45
+
46
+ @node(name="outlook_reader_node", subtype=NodeSubtype.FUNCTION, namespace="etl")
47
+ class OutlookReaderNode(BaseNodeFactory):
48
+ """Node for reading emails from Microsoft Outlook via Graph API.
49
+
50
+ Requires Microsoft Graph API credentials configured via environment variables
51
+ or passed directly:
52
+ - MICROSOFT_CLIENT_ID
53
+ - MICROSOFT_CLIENT_SECRET
54
+ - MICROSOFT_TENANT_ID
55
+
56
+ Examples
57
+ --------
58
+ YAML pipeline::
59
+
60
+ - kind: etl:outlook_reader_node
61
+ metadata:
62
+ name: read_inbox
63
+ spec:
64
+ folder: inbox
65
+ max_messages: 50
66
+ filter: "isRead eq false"
67
+ dependencies: []
68
+
69
+ - kind: etl:outlook_reader_node
70
+ metadata:
71
+ name: read_sent
72
+ spec:
73
+ folder: sentitems
74
+ max_messages: 20
75
+ dependencies: []
76
+ """
77
+
78
+ def __call__(
79
+ self,
80
+ name: str,
81
+ folder: str = "inbox",
82
+ max_messages: int = 50,
83
+ filter: str | None = None,
84
+ select: list[str] | None = None,
85
+ include_body: bool = True,
86
+ deps: list[str] | None = None,
87
+ **kwargs: Any,
88
+ ) -> NodeSpec:
89
+ """Create an Outlook reader node specification.
90
+
91
+ Parameters
92
+ ----------
93
+ name : str
94
+ Node name
95
+ folder : str
96
+ Mail folder to read from: 'inbox', 'sentitems', 'drafts', 'deleteditems'
97
+ Or a custom folder path like 'inbox/subfolder'
98
+ max_messages : int
99
+ Maximum number of messages to retrieve (default: 50)
100
+ filter : str, optional
101
+ OData filter expression (e.g., "isRead eq false", "from/emailAddress/address eq 'someone@example.com'")
102
+ select : list[str], optional
103
+ Fields to retrieve. Default: subject, from, toRecipients, body, receivedDateTime, hasAttachments, isRead
104
+ include_body : bool
105
+ Whether to include full email body (default: True)
106
+ deps : list[str], optional
107
+ Dependency node names
108
+ **kwargs : Any
109
+ Additional node parameters
110
+
111
+ Returns
112
+ -------
113
+ NodeSpec
114
+ Node specification ready for execution
115
+ """
116
+ wrapped_fn = self._create_reader_function(name, folder, max_messages, filter, select, include_body)
117
+
118
+ input_schema = {"input_data": dict | None}
119
+ output_model = OutlookReaderOutput
120
+
121
+ input_model = self.create_pydantic_model(f"{name}Input", input_schema)
122
+
123
+ node_params = {
124
+ "folder": folder,
125
+ "max_messages": max_messages,
126
+ "filter": filter,
127
+ "select": select,
128
+ "include_body": include_body,
129
+ **kwargs,
130
+ }
131
+
132
+ return NodeSpec(
133
+ name=name,
134
+ fn=wrapped_fn,
135
+ in_model=input_model,
136
+ out_model=output_model,
137
+ deps=frozenset(deps or []),
138
+ params=node_params,
139
+ )
140
+
141
+ def _create_reader_function(
142
+ self,
143
+ name: str,
144
+ folder: str,
145
+ max_messages: int,
146
+ filter: str | None,
147
+ select: list[str] | None,
148
+ include_body: bool,
149
+ ) -> Any:
150
+ """Create the email reading function."""
151
+
152
+ async def read_emails(input_data: Any = None) -> dict[str, Any]:
153
+ """Read emails from Outlook via Microsoft Graph API."""
154
+ import os
155
+
156
+ # Get credentials from environment or input
157
+ client_id = os.environ.get("MICROSOFT_CLIENT_ID")
158
+ client_secret = os.environ.get("MICROSOFT_CLIENT_SECRET")
159
+ tenant_id = os.environ.get("MICROSOFT_TENANT_ID")
160
+
161
+ if not all([client_id, client_secret, tenant_id]):
162
+ raise ValueError(
163
+ "Microsoft Graph API credentials not configured. "
164
+ "Set MICROSOFT_CLIENT_ID, MICROSOFT_CLIENT_SECRET, and MICROSOFT_TENANT_ID environment variables."
165
+ )
166
+
167
+ # Import Azure Identity and Graph SDK
168
+ try:
169
+ from azure.identity import ClientSecretCredential
170
+ from msgraph import GraphServiceClient
171
+ except ImportError as e:
172
+ raise ImportError(
173
+ "Microsoft Graph SDK not installed. Install with: pip install azure-identity msgraph-sdk"
174
+ ) from e
175
+
176
+ # Create credential and client
177
+ credential = ClientSecretCredential(
178
+ tenant_id=tenant_id,
179
+ client_id=client_id,
180
+ client_secret=client_secret,
181
+ )
182
+ client = GraphServiceClient(credential)
183
+
184
+ # Build query parameters
185
+ default_select = [
186
+ "id",
187
+ "subject",
188
+ "from",
189
+ "toRecipients",
190
+ "receivedDateTime",
191
+ "hasAttachments",
192
+ "isRead",
193
+ "bodyPreview",
194
+ ]
195
+ if include_body:
196
+ default_select.append("body")
197
+
198
+ query_select = select or default_select
199
+
200
+ # Map folder names to Graph API paths
201
+ folder_map = {
202
+ "inbox": "inbox",
203
+ "sentitems": "sentItems",
204
+ "sent": "sentItems",
205
+ "drafts": "drafts",
206
+ "deleteditems": "deletedItems",
207
+ "deleted": "deletedItems",
208
+ "junk": "junkemail",
209
+ "archive": "archive",
210
+ }
211
+ graph_folder = folder_map.get(folder.lower(), folder)
212
+
213
+ # Build request
214
+ messages_request = client.me.mail_folders.by_mail_folder_id(graph_folder).messages
215
+
216
+ # Get messages
217
+ result = await messages_request.get(
218
+ request_configuration=lambda config: setattr(config.query_parameters, "top", max_messages)
219
+ or (setattr(config.query_parameters, "select", query_select) if query_select else None)
220
+ or (setattr(config.query_parameters, "filter", filter) if filter else None)
221
+ )
222
+
223
+ messages = []
224
+ if result and result.value:
225
+ for msg in result.value:
226
+ message_dict = {
227
+ "id": msg.id,
228
+ "subject": msg.subject or "",
229
+ "sender": msg.from_.email_address.address if msg.from_ and msg.from_.email_address else "",
230
+ "recipients": [r.email_address.address for r in (msg.to_recipients or []) if r.email_address],
231
+ "body": msg.body.content if msg.body else "",
232
+ "body_preview": msg.body_preview or "",
233
+ "received_at": msg.received_date_time.isoformat() if msg.received_date_time else None,
234
+ "has_attachments": msg.has_attachments or False,
235
+ "is_read": msg.is_read or False,
236
+ }
237
+ messages.append(message_dict)
238
+
239
+ return {
240
+ "messages": messages,
241
+ "count": len(messages),
242
+ "folder": folder,
243
+ }
244
+
245
+ read_emails.__name__ = f"outlook_reader_{name}"
246
+ read_emails.__doc__ = f"Read emails from Outlook folder: {folder}"
247
+
248
+ return read_emails
249
+
250
+
251
+ @node(name="outlook_sender_node", subtype=NodeSubtype.FUNCTION, namespace="etl")
252
+ class OutlookSenderNode(BaseNodeFactory):
253
+ """Node for sending emails via Microsoft Outlook Graph API.
254
+
255
+ Requires Microsoft Graph API credentials configured via environment variables:
256
+ - MICROSOFT_CLIENT_ID
257
+ - MICROSOFT_CLIENT_SECRET
258
+ - MICROSOFT_TENANT_ID
259
+
260
+ Examples
261
+ --------
262
+ YAML pipeline::
263
+
264
+ - kind: etl:outlook_sender_node
265
+ metadata:
266
+ name: send_report
267
+ spec:
268
+ to:
269
+ - recipient@example.com
270
+ subject: "Daily Report - {{date}}"
271
+ body_template: |
272
+ Hello,
273
+
274
+ Please find the daily report attached.
275
+
276
+ Summary:
277
+ - Total records: {{total}}
278
+ - Processed: {{processed}}
279
+
280
+ Best regards
281
+ dependencies:
282
+ - generate_report
283
+
284
+ - kind: etl:outlook_sender_node
285
+ metadata:
286
+ name: send_alert
287
+ spec:
288
+ to:
289
+ - alerts@example.com
290
+ cc:
291
+ - manager@example.com
292
+ subject: "Alert: {{alert_type}}"
293
+ body_template: "{{alert_message}}"
294
+ importance: high
295
+ dependencies:
296
+ - check_alerts
297
+ """
298
+
299
+ def __call__(
300
+ self,
301
+ name: str,
302
+ to: list[str] | None = None,
303
+ cc: list[str] | None = None,
304
+ bcc: list[str] | None = None,
305
+ subject: str = "",
306
+ body_template: str = "",
307
+ body_type: str = "text",
308
+ importance: str = "normal",
309
+ save_to_sent: bool = True,
310
+ deps: list[str] | None = None,
311
+ **kwargs: Any,
312
+ ) -> NodeSpec:
313
+ """Create an Outlook sender node specification.
314
+
315
+ Parameters
316
+ ----------
317
+ name : str
318
+ Node name
319
+ to : list[str], optional
320
+ List of recipient email addresses (can be templated from input)
321
+ cc : list[str], optional
322
+ List of CC recipients
323
+ bcc : list[str], optional
324
+ List of BCC recipients
325
+ subject : str
326
+ Email subject (supports Jinja2 templating)
327
+ body_template : str
328
+ Email body template (supports Jinja2 templating)
329
+ body_type : str
330
+ Body content type: 'text' or 'html' (default: 'text')
331
+ importance : str
332
+ Email importance: 'low', 'normal', 'high' (default: 'normal')
333
+ save_to_sent : bool
334
+ Save copy to Sent Items (default: True)
335
+ deps : list[str], optional
336
+ Dependency node names
337
+ **kwargs : Any
338
+ Additional node parameters
339
+
340
+ Returns
341
+ -------
342
+ NodeSpec
343
+ Node specification ready for execution
344
+ """
345
+ wrapped_fn = self._create_sender_function(
346
+ name, to, cc, bcc, subject, body_template, body_type, importance, save_to_sent
347
+ )
348
+
349
+ input_schema = {"input_data": dict | None}
350
+ output_model = OutlookSenderOutput
351
+
352
+ input_model = self.create_pydantic_model(f"{name}Input", input_schema)
353
+
354
+ node_params = {
355
+ "to": to,
356
+ "cc": cc,
357
+ "bcc": bcc,
358
+ "subject": subject,
359
+ "body_template": body_template,
360
+ "body_type": body_type,
361
+ "importance": importance,
362
+ "save_to_sent": save_to_sent,
363
+ **kwargs,
364
+ }
365
+
366
+ return NodeSpec(
367
+ name=name,
368
+ fn=wrapped_fn,
369
+ in_model=input_model,
370
+ out_model=output_model,
371
+ deps=frozenset(deps or []),
372
+ params=node_params,
373
+ )
374
+
375
+ def _create_sender_function(
376
+ self,
377
+ name: str,
378
+ to: list[str] | None,
379
+ cc: list[str] | None,
380
+ bcc: list[str] | None,
381
+ subject: str,
382
+ body_template: str,
383
+ body_type: str,
384
+ importance: str,
385
+ save_to_sent: bool,
386
+ ) -> Any:
387
+ """Create the email sending function."""
388
+
389
+ async def send_email(input_data: Any = None) -> dict[str, Any]:
390
+ """Send email via Microsoft Graph API."""
391
+ import os
392
+
393
+ from jinja2 import Template
394
+
395
+ # Get credentials from environment
396
+ client_id = os.environ.get("MICROSOFT_CLIENT_ID")
397
+ client_secret = os.environ.get("MICROSOFT_CLIENT_SECRET")
398
+ tenant_id = os.environ.get("MICROSOFT_TENANT_ID")
399
+
400
+ if not all([client_id, client_secret, tenant_id]):
401
+ raise ValueError(
402
+ "Microsoft Graph API credentials not configured. "
403
+ "Set MICROSOFT_CLIENT_ID, MICROSOFT_CLIENT_SECRET, and MICROSOFT_TENANT_ID environment variables."
404
+ )
405
+
406
+ try:
407
+ from azure.identity import ClientSecretCredential
408
+ from msgraph import GraphServiceClient
409
+ from msgraph.generated.models.body_type import BodyType
410
+ from msgraph.generated.models.email_address import EmailAddress
411
+ from msgraph.generated.models.importance import Importance
412
+ from msgraph.generated.models.item_body import ItemBody
413
+ from msgraph.generated.models.message import Message
414
+ from msgraph.generated.models.recipient import Recipient
415
+ from msgraph.generated.users.item.send_mail.send_mail_post_request_body import (
416
+ SendMailPostRequestBody,
417
+ )
418
+ except ImportError as e:
419
+ raise ImportError(
420
+ "Microsoft Graph SDK not installed. Install with: pip install azure-identity msgraph-sdk"
421
+ ) from e
422
+
423
+ # Prepare template context from input_data
424
+ context = {}
425
+ if isinstance(input_data, dict):
426
+ context = input_data
427
+
428
+ # Render templates
429
+ rendered_subject = Template(subject).render(**context)
430
+ rendered_body = Template(body_template).render(**context)
431
+
432
+ # Get recipients - from spec or from input_data
433
+ recipients_to = to or context.get("to", [])
434
+ recipients_cc = cc or context.get("cc", [])
435
+ recipients_bcc = bcc or context.get("bcc", [])
436
+
437
+ if not recipients_to:
438
+ raise ValueError("No recipients specified. Set 'to' in spec or provide in input_data.")
439
+
440
+ # Create credential and client
441
+ credential = ClientSecretCredential(
442
+ tenant_id=tenant_id,
443
+ client_id=client_id,
444
+ client_secret=client_secret,
445
+ )
446
+ client = GraphServiceClient(credential)
447
+
448
+ # Build message
449
+ def make_recipient(email: str) -> Recipient:
450
+ recipient = Recipient()
451
+ recipient.email_address = EmailAddress()
452
+ recipient.email_address.address = email
453
+ return recipient
454
+
455
+ message = Message()
456
+ message.subject = rendered_subject
457
+ message.body = ItemBody()
458
+ message.body.content_type = BodyType.Html if body_type.lower() == "html" else BodyType.Text
459
+ message.body.content = rendered_body
460
+ message.to_recipients = [make_recipient(r) for r in recipients_to]
461
+
462
+ if recipients_cc:
463
+ message.cc_recipients = [make_recipient(r) for r in recipients_cc]
464
+ if recipients_bcc:
465
+ message.bcc_recipients = [make_recipient(r) for r in recipients_bcc]
466
+
467
+ # Set importance
468
+ importance_map = {
469
+ "low": Importance.Low,
470
+ "normal": Importance.Normal,
471
+ "high": Importance.High,
472
+ }
473
+ message.importance = importance_map.get(importance.lower(), Importance.Normal)
474
+
475
+ # Send the message
476
+ request_body = SendMailPostRequestBody()
477
+ request_body.message = message
478
+ request_body.save_to_sent_items = save_to_sent
479
+
480
+ await client.me.send_mail.post(request_body)
481
+
482
+ return {
483
+ "message_id": message.id or "sent",
484
+ "subject": rendered_subject,
485
+ "recipients": recipients_to,
486
+ "success": True,
487
+ }
488
+
489
+ send_email.__name__ = f"outlook_sender_{name}"
490
+ send_email.__doc__ = f"Send email: {subject}"
491
+
492
+ return send_email