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.
- hexdag/__init__.py +116 -0
- hexdag/__main__.py +30 -0
- hexdag/adapters/executors/__init__.py +5 -0
- hexdag/adapters/executors/local_executor.py +316 -0
- hexdag/builtin/__init__.py +6 -0
- hexdag/builtin/adapters/__init__.py +51 -0
- hexdag/builtin/adapters/anthropic/__init__.py +5 -0
- hexdag/builtin/adapters/anthropic/anthropic_adapter.py +151 -0
- hexdag/builtin/adapters/database/__init__.py +6 -0
- hexdag/builtin/adapters/database/csv/csv_adapter.py +249 -0
- hexdag/builtin/adapters/database/pgvector/__init__.py +5 -0
- hexdag/builtin/adapters/database/pgvector/pgvector_adapter.py +478 -0
- hexdag/builtin/adapters/database/sqlalchemy/sqlalchemy_adapter.py +252 -0
- hexdag/builtin/adapters/database/sqlite/__init__.py +5 -0
- hexdag/builtin/adapters/database/sqlite/sqlite_adapter.py +410 -0
- hexdag/builtin/adapters/local/README.md +59 -0
- hexdag/builtin/adapters/local/__init__.py +7 -0
- hexdag/builtin/adapters/local/local_observer_manager.py +696 -0
- hexdag/builtin/adapters/memory/__init__.py +47 -0
- hexdag/builtin/adapters/memory/file_memory_adapter.py +297 -0
- hexdag/builtin/adapters/memory/in_memory_memory.py +216 -0
- hexdag/builtin/adapters/memory/schemas.py +57 -0
- hexdag/builtin/adapters/memory/session_memory.py +178 -0
- hexdag/builtin/adapters/memory/sqlite_memory_adapter.py +215 -0
- hexdag/builtin/adapters/memory/state_memory.py +280 -0
- hexdag/builtin/adapters/mock/README.md +89 -0
- hexdag/builtin/adapters/mock/__init__.py +15 -0
- hexdag/builtin/adapters/mock/hexdag.toml +50 -0
- hexdag/builtin/adapters/mock/mock_database.py +225 -0
- hexdag/builtin/adapters/mock/mock_embedding.py +223 -0
- hexdag/builtin/adapters/mock/mock_llm.py +177 -0
- hexdag/builtin/adapters/mock/mock_tool_adapter.py +192 -0
- hexdag/builtin/adapters/mock/mock_tool_router.py +232 -0
- hexdag/builtin/adapters/openai/__init__.py +5 -0
- hexdag/builtin/adapters/openai/openai_adapter.py +634 -0
- hexdag/builtin/adapters/secret/__init__.py +7 -0
- hexdag/builtin/adapters/secret/local_secret_adapter.py +248 -0
- hexdag/builtin/adapters/unified_tool_router.py +280 -0
- hexdag/builtin/macros/__init__.py +17 -0
- hexdag/builtin/macros/conversation_agent.py +390 -0
- hexdag/builtin/macros/llm_macro.py +151 -0
- hexdag/builtin/macros/reasoning_agent.py +423 -0
- hexdag/builtin/macros/tool_macro.py +380 -0
- hexdag/builtin/nodes/__init__.py +38 -0
- hexdag/builtin/nodes/_discovery.py +123 -0
- hexdag/builtin/nodes/agent_node.py +696 -0
- hexdag/builtin/nodes/base_node_factory.py +242 -0
- hexdag/builtin/nodes/composite_node.py +926 -0
- hexdag/builtin/nodes/data_node.py +201 -0
- hexdag/builtin/nodes/expression_node.py +487 -0
- hexdag/builtin/nodes/function_node.py +454 -0
- hexdag/builtin/nodes/llm_node.py +491 -0
- hexdag/builtin/nodes/loop_node.py +920 -0
- hexdag/builtin/nodes/mapped_input.py +518 -0
- hexdag/builtin/nodes/port_call_node.py +269 -0
- hexdag/builtin/nodes/tool_call_node.py +195 -0
- hexdag/builtin/nodes/tool_utils.py +390 -0
- hexdag/builtin/prompts/__init__.py +68 -0
- hexdag/builtin/prompts/base.py +422 -0
- hexdag/builtin/prompts/chat_prompts.py +303 -0
- hexdag/builtin/prompts/error_correction_prompts.py +320 -0
- hexdag/builtin/prompts/tool_prompts.py +160 -0
- hexdag/builtin/tools/builtin_tools.py +84 -0
- hexdag/builtin/tools/database_tools.py +164 -0
- hexdag/cli/__init__.py +17 -0
- hexdag/cli/__main__.py +7 -0
- hexdag/cli/commands/__init__.py +27 -0
- hexdag/cli/commands/build_cmd.py +812 -0
- hexdag/cli/commands/create_cmd.py +208 -0
- hexdag/cli/commands/docs_cmd.py +293 -0
- hexdag/cli/commands/generate_types_cmd.py +252 -0
- hexdag/cli/commands/init_cmd.py +188 -0
- hexdag/cli/commands/pipeline_cmd.py +494 -0
- hexdag/cli/commands/plugin_dev_cmd.py +529 -0
- hexdag/cli/commands/plugins_cmd.py +441 -0
- hexdag/cli/commands/studio_cmd.py +101 -0
- hexdag/cli/commands/validate_cmd.py +221 -0
- hexdag/cli/main.py +84 -0
- hexdag/core/__init__.py +83 -0
- hexdag/core/config/__init__.py +20 -0
- hexdag/core/config/loader.py +479 -0
- hexdag/core/config/models.py +150 -0
- hexdag/core/configurable.py +294 -0
- hexdag/core/context/__init__.py +37 -0
- hexdag/core/context/execution_context.py +378 -0
- hexdag/core/docs/__init__.py +26 -0
- hexdag/core/docs/extractors.py +678 -0
- hexdag/core/docs/generators.py +890 -0
- hexdag/core/docs/models.py +120 -0
- hexdag/core/domain/__init__.py +10 -0
- hexdag/core/domain/dag.py +1225 -0
- hexdag/core/exceptions.py +234 -0
- hexdag/core/expression_parser.py +569 -0
- hexdag/core/logging.py +449 -0
- hexdag/core/models/__init__.py +17 -0
- hexdag/core/models/base.py +138 -0
- hexdag/core/orchestration/__init__.py +46 -0
- hexdag/core/orchestration/body_executor.py +481 -0
- hexdag/core/orchestration/components/__init__.py +97 -0
- hexdag/core/orchestration/components/adapter_lifecycle_manager.py +113 -0
- hexdag/core/orchestration/components/checkpoint_manager.py +134 -0
- hexdag/core/orchestration/components/execution_coordinator.py +360 -0
- hexdag/core/orchestration/components/health_check_manager.py +176 -0
- hexdag/core/orchestration/components/input_mapper.py +143 -0
- hexdag/core/orchestration/components/lifecycle_manager.py +583 -0
- hexdag/core/orchestration/components/node_executor.py +377 -0
- hexdag/core/orchestration/components/secret_manager.py +202 -0
- hexdag/core/orchestration/components/wave_executor.py +158 -0
- hexdag/core/orchestration/constants.py +17 -0
- hexdag/core/orchestration/events/README.md +312 -0
- hexdag/core/orchestration/events/__init__.py +104 -0
- hexdag/core/orchestration/events/batching.py +330 -0
- hexdag/core/orchestration/events/decorators.py +139 -0
- hexdag/core/orchestration/events/events.py +573 -0
- hexdag/core/orchestration/events/observers/__init__.py +30 -0
- hexdag/core/orchestration/events/observers/core_observers.py +690 -0
- hexdag/core/orchestration/events/observers/models.py +111 -0
- hexdag/core/orchestration/events/taxonomy.py +269 -0
- hexdag/core/orchestration/hook_context.py +237 -0
- hexdag/core/orchestration/hooks.py +437 -0
- hexdag/core/orchestration/models.py +418 -0
- hexdag/core/orchestration/orchestrator.py +910 -0
- hexdag/core/orchestration/orchestrator_factory.py +275 -0
- hexdag/core/orchestration/port_wrappers.py +327 -0
- hexdag/core/orchestration/prompt/__init__.py +32 -0
- hexdag/core/orchestration/prompt/template.py +332 -0
- hexdag/core/pipeline_builder/__init__.py +21 -0
- hexdag/core/pipeline_builder/component_instantiator.py +386 -0
- hexdag/core/pipeline_builder/include_tag.py +265 -0
- hexdag/core/pipeline_builder/pipeline_config.py +133 -0
- hexdag/core/pipeline_builder/py_tag.py +223 -0
- hexdag/core/pipeline_builder/tag_discovery.py +268 -0
- hexdag/core/pipeline_builder/yaml_builder.py +1196 -0
- hexdag/core/pipeline_builder/yaml_validator.py +569 -0
- hexdag/core/ports/__init__.py +65 -0
- hexdag/core/ports/api_call.py +133 -0
- hexdag/core/ports/database.py +489 -0
- hexdag/core/ports/embedding.py +215 -0
- hexdag/core/ports/executor.py +237 -0
- hexdag/core/ports/file_storage.py +117 -0
- hexdag/core/ports/healthcheck.py +87 -0
- hexdag/core/ports/llm.py +551 -0
- hexdag/core/ports/memory.py +70 -0
- hexdag/core/ports/observer_manager.py +130 -0
- hexdag/core/ports/secret.py +145 -0
- hexdag/core/ports/tool_router.py +94 -0
- hexdag/core/ports_builder.py +623 -0
- hexdag/core/protocols.py +273 -0
- hexdag/core/resolver.py +304 -0
- hexdag/core/schema/__init__.py +9 -0
- hexdag/core/schema/generator.py +742 -0
- hexdag/core/secrets.py +242 -0
- hexdag/core/types.py +413 -0
- hexdag/core/utils/async_warnings.py +206 -0
- hexdag/core/utils/schema_conversion.py +78 -0
- hexdag/core/utils/sql_validation.py +86 -0
- hexdag/core/validation/secure_json.py +148 -0
- hexdag/core/yaml_macro.py +517 -0
- hexdag/mcp_server.py +3120 -0
- hexdag/studio/__init__.py +10 -0
- hexdag/studio/build_ui.py +92 -0
- hexdag/studio/server/__init__.py +1 -0
- hexdag/studio/server/main.py +100 -0
- hexdag/studio/server/routes/__init__.py +9 -0
- hexdag/studio/server/routes/execute.py +208 -0
- hexdag/studio/server/routes/export.py +558 -0
- hexdag/studio/server/routes/files.py +207 -0
- hexdag/studio/server/routes/plugins.py +419 -0
- hexdag/studio/server/routes/validate.py +220 -0
- hexdag/studio/ui/index.html +13 -0
- hexdag/studio/ui/package-lock.json +2992 -0
- hexdag/studio/ui/package.json +31 -0
- hexdag/studio/ui/postcss.config.js +6 -0
- hexdag/studio/ui/public/hexdag.svg +5 -0
- hexdag/studio/ui/src/App.tsx +251 -0
- hexdag/studio/ui/src/components/Canvas.tsx +408 -0
- hexdag/studio/ui/src/components/ContextMenu.tsx +187 -0
- hexdag/studio/ui/src/components/FileBrowser.tsx +123 -0
- hexdag/studio/ui/src/components/Header.tsx +181 -0
- hexdag/studio/ui/src/components/HexdagNode.tsx +193 -0
- hexdag/studio/ui/src/components/NodeInspector.tsx +512 -0
- hexdag/studio/ui/src/components/NodePalette.tsx +262 -0
- hexdag/studio/ui/src/components/NodePortsSection.tsx +403 -0
- hexdag/studio/ui/src/components/PluginManager.tsx +347 -0
- hexdag/studio/ui/src/components/PortsEditor.tsx +481 -0
- hexdag/studio/ui/src/components/PythonEditor.tsx +195 -0
- hexdag/studio/ui/src/components/ValidationPanel.tsx +105 -0
- hexdag/studio/ui/src/components/YamlEditor.tsx +196 -0
- hexdag/studio/ui/src/components/index.ts +8 -0
- hexdag/studio/ui/src/index.css +92 -0
- hexdag/studio/ui/src/main.tsx +10 -0
- hexdag/studio/ui/src/types/index.ts +123 -0
- hexdag/studio/ui/src/vite-env.d.ts +1 -0
- hexdag/studio/ui/tailwind.config.js +29 -0
- hexdag/studio/ui/tsconfig.json +37 -0
- hexdag/studio/ui/tsconfig.node.json +13 -0
- hexdag/studio/ui/vite.config.ts +35 -0
- hexdag/visualization/__init__.py +69 -0
- hexdag/visualization/dag_visualizer.py +1020 -0
- hexdag-0.5.0.dev1.dist-info/METADATA +369 -0
- hexdag-0.5.0.dev1.dist-info/RECORD +261 -0
- hexdag-0.5.0.dev1.dist-info/WHEEL +4 -0
- hexdag-0.5.0.dev1.dist-info/entry_points.txt +4 -0
- hexdag-0.5.0.dev1.dist-info/licenses/LICENSE +190 -0
- hexdag_plugins/.gitignore +43 -0
- hexdag_plugins/README.md +73 -0
- hexdag_plugins/__init__.py +1 -0
- hexdag_plugins/azure/LICENSE +21 -0
- hexdag_plugins/azure/README.md +414 -0
- hexdag_plugins/azure/__init__.py +21 -0
- hexdag_plugins/azure/azure_blob_adapter.py +450 -0
- hexdag_plugins/azure/azure_cosmos_adapter.py +383 -0
- hexdag_plugins/azure/azure_keyvault_adapter.py +314 -0
- hexdag_plugins/azure/azure_openai_adapter.py +415 -0
- hexdag_plugins/azure/pyproject.toml +107 -0
- hexdag_plugins/azure/tests/__init__.py +1 -0
- hexdag_plugins/azure/tests/test_azure_blob_adapter.py +350 -0
- hexdag_plugins/azure/tests/test_azure_cosmos_adapter.py +323 -0
- hexdag_plugins/azure/tests/test_azure_keyvault_adapter.py +330 -0
- hexdag_plugins/azure/tests/test_azure_openai_adapter.py +329 -0
- hexdag_plugins/hexdag_etl/README.md +168 -0
- hexdag_plugins/hexdag_etl/__init__.py +53 -0
- hexdag_plugins/hexdag_etl/examples/01_simple_pandas_transform.py +270 -0
- hexdag_plugins/hexdag_etl/examples/02_simple_pandas_only.py +149 -0
- hexdag_plugins/hexdag_etl/examples/03_file_io_pipeline.py +109 -0
- hexdag_plugins/hexdag_etl/examples/test_pandas_transform.py +84 -0
- hexdag_plugins/hexdag_etl/hexdag.toml +25 -0
- hexdag_plugins/hexdag_etl/hexdag_etl/__init__.py +48 -0
- hexdag_plugins/hexdag_etl/hexdag_etl/nodes/__init__.py +13 -0
- hexdag_plugins/hexdag_etl/hexdag_etl/nodes/api_extract.py +230 -0
- hexdag_plugins/hexdag_etl/hexdag_etl/nodes/base_node_factory.py +181 -0
- hexdag_plugins/hexdag_etl/hexdag_etl/nodes/file_io.py +415 -0
- hexdag_plugins/hexdag_etl/hexdag_etl/nodes/outlook.py +492 -0
- hexdag_plugins/hexdag_etl/hexdag_etl/nodes/pandas_transform.py +563 -0
- hexdag_plugins/hexdag_etl/hexdag_etl/nodes/sql_extract_load.py +112 -0
- hexdag_plugins/hexdag_etl/pyproject.toml +82 -0
- hexdag_plugins/hexdag_etl/test_transform.py +54 -0
- hexdag_plugins/hexdag_etl/tests/test_plugin_integration.py +62 -0
- hexdag_plugins/mysql_adapter/LICENSE +21 -0
- hexdag_plugins/mysql_adapter/README.md +224 -0
- hexdag_plugins/mysql_adapter/__init__.py +6 -0
- hexdag_plugins/mysql_adapter/mysql_adapter.py +408 -0
- hexdag_plugins/mysql_adapter/pyproject.toml +93 -0
- hexdag_plugins/mysql_adapter/tests/test_mysql_adapter.py +259 -0
- hexdag_plugins/storage/README.md +184 -0
- hexdag_plugins/storage/__init__.py +19 -0
- hexdag_plugins/storage/file/__init__.py +5 -0
- hexdag_plugins/storage/file/local.py +325 -0
- hexdag_plugins/storage/ports/__init__.py +5 -0
- hexdag_plugins/storage/ports/vector_store.py +236 -0
- hexdag_plugins/storage/sql/__init__.py +7 -0
- hexdag_plugins/storage/sql/base.py +187 -0
- hexdag_plugins/storage/sql/mysql.py +27 -0
- hexdag_plugins/storage/sql/postgresql.py +27 -0
- hexdag_plugins/storage/tests/__init__.py +1 -0
- hexdag_plugins/storage/tests/test_local_file_storage.py +161 -0
- hexdag_plugins/storage/tests/test_sql_adapters.py +212 -0
- hexdag_plugins/storage/vector/__init__.py +7 -0
- hexdag_plugins/storage/vector/chromadb.py +223 -0
- hexdag_plugins/storage/vector/in_memory.py +285 -0
- 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
|