lfx-microsoft 0.1.0__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.
- lfx_microsoft/__init__.py +25 -0
- lfx_microsoft/base.py +153 -0
- lfx_microsoft/components/__init__.py +1 -0
- lfx_microsoft/components/microsoft/__init__.py +19 -0
- lfx_microsoft/components/microsoft/capabilities.v1.json +255 -0
- lfx_microsoft/components/microsoft/outlook_calendar_create.py +132 -0
- lfx_microsoft/components/microsoft/outlook_calendar_list.py +125 -0
- lfx_microsoft/components/microsoft/outlook_search.py +129 -0
- lfx_microsoft/components/microsoft/outlook_send.py +166 -0
- lfx_microsoft/components/microsoft/sharepoint_fetch.py +124 -0
- lfx_microsoft/components/microsoft/sharepoint_list.py +128 -0
- lfx_microsoft/components/microsoft/teams_channel_post.py +75 -0
- lfx_microsoft/components/microsoft/teams_chat_post.py +79 -0
- lfx_microsoft/extension.json +23 -0
- lfx_microsoft/graph.py +567 -0
- lfx_microsoft/manifest.py +56 -0
- lfx_microsoft-0.1.0.dist-info/METADATA +61 -0
- lfx_microsoft-0.1.0.dist-info/RECORD +20 -0
- lfx_microsoft-0.1.0.dist-info/WHEEL +4 -0
- lfx_microsoft-0.1.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Microsoft 365 and Teams components backed by delegated Microsoft Graph permissions."""
|
|
2
|
+
|
|
3
|
+
from lfx_microsoft.components.microsoft import (
|
|
4
|
+
OutlookCalendarCreateComponent,
|
|
5
|
+
OutlookCalendarListComponent,
|
|
6
|
+
OutlookSearchComponent,
|
|
7
|
+
OutlookSendComponent,
|
|
8
|
+
SharePointFetchComponent,
|
|
9
|
+
SharePointListComponent,
|
|
10
|
+
TeamsChannelPostComponent,
|
|
11
|
+
TeamsChatPostComponent,
|
|
12
|
+
)
|
|
13
|
+
from lfx_microsoft.graph import GraphClient
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"GraphClient",
|
|
17
|
+
"OutlookCalendarCreateComponent",
|
|
18
|
+
"OutlookCalendarListComponent",
|
|
19
|
+
"OutlookSearchComponent",
|
|
20
|
+
"OutlookSendComponent",
|
|
21
|
+
"SharePointFetchComponent",
|
|
22
|
+
"SharePointListComponent",
|
|
23
|
+
"TeamsChannelPostComponent",
|
|
24
|
+
"TeamsChatPostComponent",
|
|
25
|
+
]
|
lfx_microsoft/base.py
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"""Shared base for every Microsoft Graph action component.
|
|
2
|
+
|
|
3
|
+
Each component declares one manifest capability. The base turns that into a
|
|
4
|
+
credential lease, checks the active capability requirements,
|
|
5
|
+
and wraps the provider call in the integration telemetry boundary.
|
|
6
|
+
|
|
7
|
+
This module is also the bundle's import facade for the ``lfx`` field types.
|
|
8
|
+
``lfx.custom.custom_component.component`` must be imported before ``lfx.io``
|
|
9
|
+
or ``lfx.inputs`` -- importing the field types first leaves the component
|
|
10
|
+
module half-initialized (a cold ``import lfx.io`` followed by
|
|
11
|
+
``from lfx.custom.custom_component.component import Component`` raises
|
|
12
|
+
``ImportError`` on lfx 1.13). Re-exporting the field types here means every
|
|
13
|
+
component module has exactly one lfx-facing import and cannot get that order
|
|
14
|
+
wrong.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from contextlib import asynccontextmanager
|
|
20
|
+
from typing import TYPE_CHECKING, Any, ClassVar
|
|
21
|
+
|
|
22
|
+
from lfx.custom.custom_component.component import Component
|
|
23
|
+
from lfx.integrations.capabilities import ScopeSet
|
|
24
|
+
from lfx.integrations.errors import ScopeMissingError
|
|
25
|
+
from lfx.integrations.telemetry import integration_action
|
|
26
|
+
from lfx.io import (
|
|
27
|
+
BoolInput,
|
|
28
|
+
DataInput,
|
|
29
|
+
DropdownInput,
|
|
30
|
+
FileInput,
|
|
31
|
+
IntInput,
|
|
32
|
+
MessageTextInput,
|
|
33
|
+
MultilineInput,
|
|
34
|
+
Output,
|
|
35
|
+
)
|
|
36
|
+
from lfx.schema.data import Data
|
|
37
|
+
from lfx.schema.message import Message
|
|
38
|
+
|
|
39
|
+
from lfx_microsoft.graph import PROVIDER_ID, GraphClient
|
|
40
|
+
from lfx_microsoft.manifest import capability
|
|
41
|
+
|
|
42
|
+
if TYPE_CHECKING:
|
|
43
|
+
from collections.abc import AsyncIterator
|
|
44
|
+
|
|
45
|
+
from lfx.integrations.models import CredentialLease, ResolvedCredential
|
|
46
|
+
|
|
47
|
+
__all__ = [
|
|
48
|
+
"BoolInput",
|
|
49
|
+
"Data",
|
|
50
|
+
"DataInput",
|
|
51
|
+
"DropdownInput",
|
|
52
|
+
"FileInput",
|
|
53
|
+
"IntInput",
|
|
54
|
+
"Message",
|
|
55
|
+
"MessageTextInput",
|
|
56
|
+
"MicrosoftGraphComponent",
|
|
57
|
+
"MultilineInput",
|
|
58
|
+
"Output",
|
|
59
|
+
"as_dict_list",
|
|
60
|
+
"as_list",
|
|
61
|
+
"recipients",
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class MicrosoftGraphComponent(Component):
|
|
66
|
+
"""Base class for the delegated Microsoft Graph actions."""
|
|
67
|
+
|
|
68
|
+
icon = "Microsoft"
|
|
69
|
+
capability_id: ClassVar[str] = ""
|
|
70
|
+
|
|
71
|
+
# Test seam: an ``httpx.AsyncBaseTransport`` used instead of the network.
|
|
72
|
+
graph_transport: Any = None
|
|
73
|
+
graph_base_url: str | None = None
|
|
74
|
+
|
|
75
|
+
def lease(self) -> CredentialLease:
|
|
76
|
+
"""Return the credential lease for the declared connection field."""
|
|
77
|
+
return self.resolve_connection("connection")
|
|
78
|
+
|
|
79
|
+
def client(self, lease: CredentialLease) -> GraphClient:
|
|
80
|
+
"""Build a Graph client bound to ``lease``."""
|
|
81
|
+
kwargs: dict[str, Any] = {}
|
|
82
|
+
if self.graph_transport is not None:
|
|
83
|
+
kwargs["transport"] = self.graph_transport
|
|
84
|
+
if self.graph_base_url:
|
|
85
|
+
kwargs["base_url"] = self.graph_base_url
|
|
86
|
+
return GraphClient(lease, **kwargs)
|
|
87
|
+
|
|
88
|
+
def _preflight_scopes(self, credential: ResolvedCredential, inputs: dict[str, Any]) -> None:
|
|
89
|
+
"""Fail before the network call when a conditional scope is missing.
|
|
90
|
+
|
|
91
|
+
The connection resolver enforces static and conditional input scopes.
|
|
92
|
+
This check also keeps direct action callers aligned with the manifest.
|
|
93
|
+
"""
|
|
94
|
+
if not credential.scopes_verified:
|
|
95
|
+
return
|
|
96
|
+
missing = ScopeSet.covers(
|
|
97
|
+
capability(self.capability_id),
|
|
98
|
+
inputs,
|
|
99
|
+
credential.granted_scopes,
|
|
100
|
+
provider=PROVIDER_ID,
|
|
101
|
+
)
|
|
102
|
+
if missing:
|
|
103
|
+
raise ScopeMissingError(missing, provider=PROVIDER_ID)
|
|
104
|
+
|
|
105
|
+
@asynccontextmanager
|
|
106
|
+
async def action(self, lease: CredentialLease, inputs: dict[str, Any] | None = None) -> AsyncIterator[GraphClient]:
|
|
107
|
+
"""Resolve, pre-flight, measure and normalize one Graph action."""
|
|
108
|
+
credential = await lease.get_credential()
|
|
109
|
+
self._preflight_scopes(credential, inputs or {})
|
|
110
|
+
client = self.client(lease)
|
|
111
|
+
try:
|
|
112
|
+
async with integration_action(
|
|
113
|
+
self,
|
|
114
|
+
provider=PROVIDER_ID,
|
|
115
|
+
capability=self.capability_id,
|
|
116
|
+
owner_kind=credential.owner_kind,
|
|
117
|
+
):
|
|
118
|
+
yield client
|
|
119
|
+
finally:
|
|
120
|
+
await client.aclose()
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def as_list(value: Any) -> list[str]:
|
|
124
|
+
"""Normalize a possibly-scalar, possibly-comma-separated input into a list."""
|
|
125
|
+
if value is None:
|
|
126
|
+
return []
|
|
127
|
+
if isinstance(value, str):
|
|
128
|
+
items = [entry.strip() for entry in value.replace(";", ",").split(",")]
|
|
129
|
+
elif isinstance(value, (list, tuple, set)):
|
|
130
|
+
items = [str(entry).strip() for entry in value]
|
|
131
|
+
else:
|
|
132
|
+
items = [str(value).strip()]
|
|
133
|
+
return [item for item in items if item]
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def as_dict_list(value: Any) -> list[dict[str, Any]]:
|
|
137
|
+
"""Normalize a Data / dict / list input into a list of plain dictionaries."""
|
|
138
|
+
if value is None:
|
|
139
|
+
return []
|
|
140
|
+
items = value if isinstance(value, (list, tuple)) else [value]
|
|
141
|
+
normalized: list[dict[str, Any]] = []
|
|
142
|
+
for item in items:
|
|
143
|
+
data = getattr(item, "data", None)
|
|
144
|
+
if isinstance(data, dict):
|
|
145
|
+
normalized.append(dict(data))
|
|
146
|
+
elif isinstance(item, dict):
|
|
147
|
+
normalized.append(item)
|
|
148
|
+
return normalized
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def recipients(addresses: Any) -> list[dict[str, dict[str, str]]]:
|
|
152
|
+
"""Build a Graph ``emailAddress`` recipient list."""
|
|
153
|
+
return [{"emailAddress": {"address": address}} for address in as_list(addresses)]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Component packages shipped by lfx-microsoft."""
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from lfx_microsoft.components.microsoft.outlook_calendar_create import OutlookCalendarCreateComponent
|
|
2
|
+
from lfx_microsoft.components.microsoft.outlook_calendar_list import OutlookCalendarListComponent
|
|
3
|
+
from lfx_microsoft.components.microsoft.outlook_search import OutlookSearchComponent
|
|
4
|
+
from lfx_microsoft.components.microsoft.outlook_send import OutlookSendComponent
|
|
5
|
+
from lfx_microsoft.components.microsoft.sharepoint_fetch import SharePointFetchComponent
|
|
6
|
+
from lfx_microsoft.components.microsoft.sharepoint_list import SharePointListComponent
|
|
7
|
+
from lfx_microsoft.components.microsoft.teams_channel_post import TeamsChannelPostComponent
|
|
8
|
+
from lfx_microsoft.components.microsoft.teams_chat_post import TeamsChatPostComponent
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"OutlookCalendarCreateComponent",
|
|
12
|
+
"OutlookCalendarListComponent",
|
|
13
|
+
"OutlookSearchComponent",
|
|
14
|
+
"OutlookSendComponent",
|
|
15
|
+
"SharePointFetchComponent",
|
|
16
|
+
"SharePointListComponent",
|
|
17
|
+
"TeamsChannelPostComponent",
|
|
18
|
+
"TeamsChatPostComponent",
|
|
19
|
+
]
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema_version": 1,
|
|
3
|
+
"provider_id": "microsoft",
|
|
4
|
+
"display_name": "Microsoft 365",
|
|
5
|
+
"icon": "Microsoft",
|
|
6
|
+
"docs_url": "https://docs.langflow.org/bundles-microsoft",
|
|
7
|
+
"auth_profiles": [
|
|
8
|
+
{
|
|
9
|
+
"id": "user",
|
|
10
|
+
"kind": "oauth2_authorization_code",
|
|
11
|
+
"identity": "user_delegated",
|
|
12
|
+
"authorization_url": "https://login.microsoftonline.com/{tenant}/oauth2/v2.0/authorize",
|
|
13
|
+
"token_url": "https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token",
|
|
14
|
+
"supports_pkce": true,
|
|
15
|
+
"supports_refresh": true,
|
|
16
|
+
"scope_separator": " ",
|
|
17
|
+
"default_scopes": [
|
|
18
|
+
"offline_access"
|
|
19
|
+
],
|
|
20
|
+
"client_type_by_context": {
|
|
21
|
+
"hosted": "confidential",
|
|
22
|
+
"self_managed": "confidential",
|
|
23
|
+
"desktop": "public",
|
|
24
|
+
"headless": "external"
|
|
25
|
+
},
|
|
26
|
+
"owner_by_context": {
|
|
27
|
+
"hosted": "langflow",
|
|
28
|
+
"self_managed": "customer",
|
|
29
|
+
"desktop": "langflow",
|
|
30
|
+
"headless": "customer"
|
|
31
|
+
},
|
|
32
|
+
"tenant_param": "tenant"
|
|
33
|
+
}
|
|
34
|
+
],
|
|
35
|
+
"capabilities": [
|
|
36
|
+
{
|
|
37
|
+
"id": "microsoft.outlook.send",
|
|
38
|
+
"display_name": "Outlook: Send Mail",
|
|
39
|
+
"auth_profile_id": "user",
|
|
40
|
+
"identity": "user_delegated",
|
|
41
|
+
"required_scopes": [
|
|
42
|
+
"Mail.Send"
|
|
43
|
+
],
|
|
44
|
+
"conditional_scopes": [],
|
|
45
|
+
"policy_keys": [
|
|
46
|
+
"integrations.microsoft.outlook.send"
|
|
47
|
+
],
|
|
48
|
+
"substrate": "rest",
|
|
49
|
+
"maturity": "ga",
|
|
50
|
+
"deployment_contexts": [
|
|
51
|
+
"hosted",
|
|
52
|
+
"self_managed",
|
|
53
|
+
"desktop",
|
|
54
|
+
"headless"
|
|
55
|
+
],
|
|
56
|
+
"risk": "write",
|
|
57
|
+
"component_ref": "OutlookSendComponent"
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"id": "microsoft.outlook.search",
|
|
61
|
+
"display_name": "Outlook: Search Mail",
|
|
62
|
+
"auth_profile_id": "user",
|
|
63
|
+
"identity": "user_delegated",
|
|
64
|
+
"required_scopes": [
|
|
65
|
+
"Mail.Read"
|
|
66
|
+
],
|
|
67
|
+
"conditional_scopes": [],
|
|
68
|
+
"policy_keys": [
|
|
69
|
+
"integrations.microsoft.outlook.search"
|
|
70
|
+
],
|
|
71
|
+
"substrate": "rest",
|
|
72
|
+
"maturity": "ga",
|
|
73
|
+
"deployment_contexts": [
|
|
74
|
+
"hosted",
|
|
75
|
+
"self_managed",
|
|
76
|
+
"desktop",
|
|
77
|
+
"headless"
|
|
78
|
+
],
|
|
79
|
+
"risk": "read",
|
|
80
|
+
"component_ref": "OutlookSearchComponent"
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
"id": "microsoft.calendar.list",
|
|
84
|
+
"display_name": "Outlook Calendar: List Events",
|
|
85
|
+
"auth_profile_id": "user",
|
|
86
|
+
"identity": "user_delegated",
|
|
87
|
+
"required_scopes": [
|
|
88
|
+
"Calendars.Read"
|
|
89
|
+
],
|
|
90
|
+
"conditional_scopes": [],
|
|
91
|
+
"policy_keys": [
|
|
92
|
+
"integrations.microsoft.calendar.list"
|
|
93
|
+
],
|
|
94
|
+
"substrate": "rest",
|
|
95
|
+
"maturity": "ga",
|
|
96
|
+
"deployment_contexts": [
|
|
97
|
+
"hosted",
|
|
98
|
+
"self_managed",
|
|
99
|
+
"desktop",
|
|
100
|
+
"headless"
|
|
101
|
+
],
|
|
102
|
+
"risk": "read",
|
|
103
|
+
"component_ref": "OutlookCalendarListComponent"
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
"id": "microsoft.calendar.create",
|
|
107
|
+
"display_name": "Outlook Calendar: Create Event",
|
|
108
|
+
"auth_profile_id": "user",
|
|
109
|
+
"identity": "user_delegated",
|
|
110
|
+
"required_scopes": [
|
|
111
|
+
"Calendars.ReadWrite"
|
|
112
|
+
],
|
|
113
|
+
"conditional_scopes": [],
|
|
114
|
+
"policy_keys": [
|
|
115
|
+
"integrations.microsoft.calendar.create"
|
|
116
|
+
],
|
|
117
|
+
"substrate": "rest",
|
|
118
|
+
"maturity": "ga",
|
|
119
|
+
"deployment_contexts": [
|
|
120
|
+
"hosted",
|
|
121
|
+
"self_managed",
|
|
122
|
+
"desktop",
|
|
123
|
+
"headless"
|
|
124
|
+
],
|
|
125
|
+
"risk": "write",
|
|
126
|
+
"component_ref": "OutlookCalendarCreateComponent"
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
"id": "microsoft.teams.chat_post",
|
|
130
|
+
"display_name": "Teams: Post Chat Message",
|
|
131
|
+
"auth_profile_id": "user",
|
|
132
|
+
"identity": "user_delegated",
|
|
133
|
+
"required_scopes": [
|
|
134
|
+
"ChatMessage.Send"
|
|
135
|
+
],
|
|
136
|
+
"conditional_scopes": [],
|
|
137
|
+
"policy_keys": [
|
|
138
|
+
"integrations.microsoft.teams.chat_post"
|
|
139
|
+
],
|
|
140
|
+
"substrate": "rest",
|
|
141
|
+
"maturity": "ga",
|
|
142
|
+
"deployment_contexts": [
|
|
143
|
+
"hosted",
|
|
144
|
+
"self_managed",
|
|
145
|
+
"desktop",
|
|
146
|
+
"headless"
|
|
147
|
+
],
|
|
148
|
+
"risk": "write",
|
|
149
|
+
"component_ref": "TeamsChatPostComponent"
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
"id": "microsoft.teams.channel_post",
|
|
153
|
+
"display_name": "Teams: Post Channel Message",
|
|
154
|
+
"auth_profile_id": "user",
|
|
155
|
+
"identity": "user_delegated",
|
|
156
|
+
"required_scopes": [
|
|
157
|
+
"ChannelMessage.Send"
|
|
158
|
+
],
|
|
159
|
+
"conditional_scopes": [],
|
|
160
|
+
"policy_keys": [
|
|
161
|
+
"integrations.microsoft.teams.channel_post"
|
|
162
|
+
],
|
|
163
|
+
"substrate": "rest",
|
|
164
|
+
"maturity": "ga",
|
|
165
|
+
"deployment_contexts": [
|
|
166
|
+
"hosted",
|
|
167
|
+
"self_managed",
|
|
168
|
+
"desktop",
|
|
169
|
+
"headless"
|
|
170
|
+
],
|
|
171
|
+
"risk": "write",
|
|
172
|
+
"component_ref": "TeamsChannelPostComponent"
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
"id": "microsoft.files.list",
|
|
176
|
+
"display_name": "SharePoint/OneDrive: List Items",
|
|
177
|
+
"auth_profile_id": "user",
|
|
178
|
+
"identity": "user_delegated",
|
|
179
|
+
"required_scopes": [
|
|
180
|
+
"Files.Read"
|
|
181
|
+
],
|
|
182
|
+
"conditional_scopes": [
|
|
183
|
+
{
|
|
184
|
+
"scope": "Files.Read.All",
|
|
185
|
+
"role": "optional",
|
|
186
|
+
"condition": {
|
|
187
|
+
"kind": "input_truthy",
|
|
188
|
+
"input": "drive_id"
|
|
189
|
+
}
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
"scope": "Sites.Read.All",
|
|
193
|
+
"role": "optional",
|
|
194
|
+
"condition": {
|
|
195
|
+
"kind": "input_truthy",
|
|
196
|
+
"input": "site_id"
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
],
|
|
200
|
+
"policy_keys": [
|
|
201
|
+
"integrations.microsoft.files.list"
|
|
202
|
+
],
|
|
203
|
+
"substrate": "rest",
|
|
204
|
+
"maturity": "ga",
|
|
205
|
+
"deployment_contexts": [
|
|
206
|
+
"hosted",
|
|
207
|
+
"self_managed",
|
|
208
|
+
"desktop",
|
|
209
|
+
"headless"
|
|
210
|
+
],
|
|
211
|
+
"risk": "read",
|
|
212
|
+
"component_ref": "SharePointListComponent"
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
"id": "microsoft.files.fetch",
|
|
216
|
+
"display_name": "SharePoint/OneDrive: Fetch Item",
|
|
217
|
+
"auth_profile_id": "user",
|
|
218
|
+
"identity": "user_delegated",
|
|
219
|
+
"required_scopes": [
|
|
220
|
+
"Files.Read"
|
|
221
|
+
],
|
|
222
|
+
"conditional_scopes": [
|
|
223
|
+
{
|
|
224
|
+
"scope": "Files.Read.All",
|
|
225
|
+
"role": "optional",
|
|
226
|
+
"condition": {
|
|
227
|
+
"kind": "input_truthy",
|
|
228
|
+
"input": "drive_id"
|
|
229
|
+
}
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
"scope": "Sites.Read.All",
|
|
233
|
+
"role": "optional",
|
|
234
|
+
"condition": {
|
|
235
|
+
"kind": "input_truthy",
|
|
236
|
+
"input": "site_id"
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
],
|
|
240
|
+
"policy_keys": [
|
|
241
|
+
"integrations.microsoft.files.fetch"
|
|
242
|
+
],
|
|
243
|
+
"substrate": "rest",
|
|
244
|
+
"maturity": "ga",
|
|
245
|
+
"deployment_contexts": [
|
|
246
|
+
"hosted",
|
|
247
|
+
"self_managed",
|
|
248
|
+
"desktop",
|
|
249
|
+
"headless"
|
|
250
|
+
],
|
|
251
|
+
"risk": "read",
|
|
252
|
+
"component_ref": "SharePointFetchComponent"
|
|
253
|
+
}
|
|
254
|
+
]
|
|
255
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""Create a calendar event (POST /me/events)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from lfx_microsoft.base import (
|
|
8
|
+
BoolInput,
|
|
9
|
+
Data,
|
|
10
|
+
DataInput,
|
|
11
|
+
MessageTextInput,
|
|
12
|
+
MicrosoftGraphComponent,
|
|
13
|
+
MultilineInput,
|
|
14
|
+
Output,
|
|
15
|
+
as_list,
|
|
16
|
+
)
|
|
17
|
+
from lfx_microsoft.graph import prefer_header
|
|
18
|
+
from lfx_microsoft.manifest import connection_input
|
|
19
|
+
|
|
20
|
+
DEFAULT_TIME_ZONE = "UTC"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class OutlookCalendarCreateComponent(MicrosoftGraphComponent):
|
|
24
|
+
"""Create an Outlook calendar event as the connected user."""
|
|
25
|
+
|
|
26
|
+
display_name = "Outlook Calendar: Create Event"
|
|
27
|
+
description = "Create a calendar event on the connected user's calendar."
|
|
28
|
+
documentation = "https://learn.microsoft.com/en-us/graph/api/user-post-events"
|
|
29
|
+
name = "OutlookCalendarCreateEvent"
|
|
30
|
+
capability_id = "microsoft.calendar.create"
|
|
31
|
+
|
|
32
|
+
inputs = [
|
|
33
|
+
connection_input(capability_id),
|
|
34
|
+
MessageTextInput(name="subject", display_name="Subject", required=True),
|
|
35
|
+
MessageTextInput(
|
|
36
|
+
name="start_time",
|
|
37
|
+
display_name="Start",
|
|
38
|
+
info="ISO 8601 local start, for example 2026-09-01T09:00:00.",
|
|
39
|
+
required=True,
|
|
40
|
+
),
|
|
41
|
+
MessageTextInput(name="end_time", display_name="End", info="ISO 8601 local end.", required=True),
|
|
42
|
+
MessageTextInput(
|
|
43
|
+
name="time_zone",
|
|
44
|
+
display_name="Time Zone",
|
|
45
|
+
info="dateTimeTimeZone.timeZone for both ends. Defaults to UTC.",
|
|
46
|
+
value=DEFAULT_TIME_ZONE,
|
|
47
|
+
),
|
|
48
|
+
MultilineInput(name="body", display_name="Body", advanced=True),
|
|
49
|
+
MessageTextInput(name="location", display_name="Location", advanced=True),
|
|
50
|
+
MessageTextInput(
|
|
51
|
+
name="attendees",
|
|
52
|
+
display_name="Attendees",
|
|
53
|
+
info="Required attendee addresses.",
|
|
54
|
+
is_list=True,
|
|
55
|
+
advanced=True,
|
|
56
|
+
),
|
|
57
|
+
BoolInput(name="is_online_meeting", display_name="Online Meeting", value=False, advanced=True),
|
|
58
|
+
DataInput(
|
|
59
|
+
name="recurrence",
|
|
60
|
+
display_name="Recurrence",
|
|
61
|
+
info="A Graph patternedRecurrence object.",
|
|
62
|
+
advanced=True,
|
|
63
|
+
),
|
|
64
|
+
MessageTextInput(
|
|
65
|
+
name="calendar_id",
|
|
66
|
+
display_name="Calendar ID",
|
|
67
|
+
info="Defaults to the user's default calendar.",
|
|
68
|
+
advanced=True,
|
|
69
|
+
),
|
|
70
|
+
MessageTextInput(
|
|
71
|
+
name="transaction_id",
|
|
72
|
+
display_name="Transaction ID",
|
|
73
|
+
info="Client-supplied id that makes a retried create idempotent.",
|
|
74
|
+
advanced=True,
|
|
75
|
+
),
|
|
76
|
+
]
|
|
77
|
+
|
|
78
|
+
outputs = [Output(display_name="Event", name="event", method="create_event")]
|
|
79
|
+
|
|
80
|
+
def _path(self) -> str:
|
|
81
|
+
calendar = (self.calendar_id or "").strip()
|
|
82
|
+
if calendar:
|
|
83
|
+
return f"/me/calendars/{calendar}/events"
|
|
84
|
+
return "/me/events"
|
|
85
|
+
|
|
86
|
+
def _recurrence(self) -> dict[str, Any] | None:
|
|
87
|
+
value = self.recurrence
|
|
88
|
+
if value is None:
|
|
89
|
+
return None
|
|
90
|
+
if isinstance(value, Data):
|
|
91
|
+
return dict(value.data)
|
|
92
|
+
if isinstance(value, dict):
|
|
93
|
+
return value
|
|
94
|
+
return None
|
|
95
|
+
|
|
96
|
+
def _event(self) -> dict[str, Any]:
|
|
97
|
+
time_zone = (self.time_zone or "").strip() or DEFAULT_TIME_ZONE
|
|
98
|
+
event: dict[str, Any] = {
|
|
99
|
+
"subject": self.subject,
|
|
100
|
+
"start": {"dateTime": self.start_time, "timeZone": time_zone},
|
|
101
|
+
"end": {"dateTime": self.end_time, "timeZone": time_zone},
|
|
102
|
+
}
|
|
103
|
+
if body := (self.body or ""):
|
|
104
|
+
event["body"] = {"contentType": "text", "content": body}
|
|
105
|
+
if location := (self.location or "").strip():
|
|
106
|
+
event["location"] = {"displayName": location}
|
|
107
|
+
if attendees := as_list(self.attendees):
|
|
108
|
+
event["attendees"] = [{"emailAddress": {"address": address}, "type": "required"} for address in attendees]
|
|
109
|
+
if self.is_online_meeting:
|
|
110
|
+
event["isOnlineMeeting"] = True
|
|
111
|
+
if recurrence := self._recurrence():
|
|
112
|
+
event["recurrence"] = recurrence
|
|
113
|
+
if transaction_id := (self.transaction_id or "").strip():
|
|
114
|
+
event["transactionId"] = transaction_id
|
|
115
|
+
return event
|
|
116
|
+
|
|
117
|
+
async def create_event(self) -> Data:
|
|
118
|
+
"""Create the event and return the created Graph resource."""
|
|
119
|
+
payload = self._event()
|
|
120
|
+
headers = prefer_header((self.time_zone or "").strip() or DEFAULT_TIME_ZONE)
|
|
121
|
+
lease = self.lease()
|
|
122
|
+
async with self.action(lease) as client:
|
|
123
|
+
response = await client.request(
|
|
124
|
+
"POST",
|
|
125
|
+
self._path(),
|
|
126
|
+
json_body=payload,
|
|
127
|
+
headers=headers or None,
|
|
128
|
+
)
|
|
129
|
+
created = response.json() if response.content else {}
|
|
130
|
+
result = Data(data=created if isinstance(created, dict) else {"response": created})
|
|
131
|
+
self.status = result.data.get("id", "created")
|
|
132
|
+
return result
|