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.
@@ -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