agentgraph-connector-google 0.5.0__tar.gz
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.
- agentgraph_connector_google-0.5.0/PKG-INFO +9 -0
- agentgraph_connector_google-0.5.0/agentgraph_connector_google/__init__.py +11 -0
- agentgraph_connector_google-0.5.0/agentgraph_connector_google/auth.py +195 -0
- agentgraph_connector_google-0.5.0/agentgraph_connector_google/gdocs.py +253 -0
- agentgraph_connector_google-0.5.0/agentgraph_connector_google/gdrive.py +554 -0
- agentgraph_connector_google-0.5.0/agentgraph_connector_google/gmail.py +741 -0
- agentgraph_connector_google-0.5.0/agentgraph_connector_google/gsheets.py +383 -0
- agentgraph_connector_google-0.5.0/agentgraph_connector_google/provider.py +147 -0
- agentgraph_connector_google-0.5.0/agentgraph_connector_google.egg-info/PKG-INFO +9 -0
- agentgraph_connector_google-0.5.0/agentgraph_connector_google.egg-info/SOURCES.txt +14 -0
- agentgraph_connector_google-0.5.0/agentgraph_connector_google.egg-info/dependency_links.txt +1 -0
- agentgraph_connector_google-0.5.0/agentgraph_connector_google.egg-info/entry_points.txt +5 -0
- agentgraph_connector_google-0.5.0/agentgraph_connector_google.egg-info/requires.txt +4 -0
- agentgraph_connector_google-0.5.0/agentgraph_connector_google.egg-info/top_level.txt +1 -0
- agentgraph_connector_google-0.5.0/pyproject.toml +23 -0
- agentgraph_connector_google-0.5.0/setup.cfg +4 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: agentgraph-connector-google
|
|
3
|
+
Version: 0.5.0
|
|
4
|
+
Summary: Google Docs, Sheets, Gmail, and Drive connectors for AgentGraph
|
|
5
|
+
Requires-Python: >=3.12
|
|
6
|
+
Requires-Dist: agentgraph-server<0.6,>=0.5.0
|
|
7
|
+
Requires-Dist: google-api-python-client>=2.193.0
|
|
8
|
+
Requires-Dist: google-auth>=2.49.1
|
|
9
|
+
Requires-Dist: google-auth-oauthlib>=1.3.0
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from agentgraph_connector_google.gdocs import GoogleDocsConnector
|
|
2
|
+
from agentgraph_connector_google.gdrive import DriveChangesConnector
|
|
3
|
+
from agentgraph_connector_google.gmail import GmailConnector
|
|
4
|
+
from agentgraph_connector_google.gsheets import GoogleSheetsConnector
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"GoogleDocsConnector",
|
|
8
|
+
"GoogleSheetsConnector",
|
|
9
|
+
"GmailConnector",
|
|
10
|
+
"DriveChangesConnector",
|
|
11
|
+
]
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"""Google OAuth2 flow for all Google connectors (Docs, Sheets, Drive, Gmail)."""
|
|
2
|
+
|
|
3
|
+
# pyright: reportUnknownMemberType=false, reportUnknownVariableType=false
|
|
4
|
+
# pyright: reportUnknownArgumentType=false
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import argparse
|
|
9
|
+
import socket
|
|
10
|
+
import webbrowser
|
|
11
|
+
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
12
|
+
from typing import NoReturn
|
|
13
|
+
from urllib.parse import parse_qs, urlparse
|
|
14
|
+
|
|
15
|
+
import typer
|
|
16
|
+
|
|
17
|
+
from agentgraph.auth.credentials import (
|
|
18
|
+
load_platform_accounts,
|
|
19
|
+
save_platform,
|
|
20
|
+
upsert_platform_account,
|
|
21
|
+
)
|
|
22
|
+
from agentgraph_connector_google.provider import (
|
|
23
|
+
GOOGLE_OAUTH_CLIENT_ID,
|
|
24
|
+
GOOGLE_OAUTH_CLIENT_SECRET,
|
|
25
|
+
GOOGLE_SCOPES,
|
|
26
|
+
GOOGLE_TOKEN_URI,
|
|
27
|
+
GoogleCredentials,
|
|
28
|
+
verify_google_auth,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class _AuthArgumentParser(argparse.ArgumentParser):
|
|
33
|
+
def error(self, message: str) -> NoReturn:
|
|
34
|
+
raise ValueError(message)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _validate_args(args: list[str]) -> None:
|
|
38
|
+
parser = _AuthArgumentParser(add_help=False, prog="agentgraph auth google")
|
|
39
|
+
parser.parse_args(args)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _find_free_port() -> int:
|
|
43
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
44
|
+
s.bind(("", 0))
|
|
45
|
+
return s.getsockname()[1]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def run_oauth_flow(
|
|
49
|
+
account_id: str | None = None,
|
|
50
|
+
add: bool = False,
|
|
51
|
+
args: list[str] | None = None,
|
|
52
|
+
) -> None:
|
|
53
|
+
"""Interactive OAuth2 browser flow. Stores credentials on completion."""
|
|
54
|
+
_validate_args(args or [])
|
|
55
|
+
|
|
56
|
+
existing_accounts = load_platform_accounts("google")
|
|
57
|
+
existing_data = None
|
|
58
|
+
if account_id is not None:
|
|
59
|
+
existing_data = next(
|
|
60
|
+
(item for item in existing_accounts if item.get("account_id") == account_id), None
|
|
61
|
+
)
|
|
62
|
+
elif existing_accounts and not add:
|
|
63
|
+
existing_data = existing_accounts[0]
|
|
64
|
+
existing = GoogleCredentials(**existing_data) if existing_data else None
|
|
65
|
+
|
|
66
|
+
if existing:
|
|
67
|
+
# Probe the saved refresh token before forcing the user back through
|
|
68
|
+
# the browser. If it still works, offer to skip.
|
|
69
|
+
raw_account_id = existing_data.get("account_id") if existing_data else None
|
|
70
|
+
verify_account_id = str(raw_account_id) if raw_account_id is not None else None
|
|
71
|
+
status, detail = (
|
|
72
|
+
verify_google_auth(verify_account_id)
|
|
73
|
+
if verify_account_id is not None
|
|
74
|
+
else verify_google_auth()
|
|
75
|
+
)
|
|
76
|
+
if status == "ok":
|
|
77
|
+
typer.echo(f"\nGoogle is already authenticated as {detail}.")
|
|
78
|
+
if not typer.confirm(
|
|
79
|
+
"Re-authenticate anyway (e.g. to switch accounts or grant new scopes)?",
|
|
80
|
+
default=False,
|
|
81
|
+
):
|
|
82
|
+
typer.echo("Keeping existing credentials.")
|
|
83
|
+
return
|
|
84
|
+
else:
|
|
85
|
+
typer.echo(f"\nGoogle credentials need re-authentication: {detail or status}.")
|
|
86
|
+
typer.echo("Re-opening browser consent using AgentGraph's packaged OAuth client.")
|
|
87
|
+
|
|
88
|
+
typer.echo(
|
|
89
|
+
f"Re-authenticating as {existing.user_email or 'existing account'} with updated scopes."
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
client_id = GOOGLE_OAUTH_CLIENT_ID
|
|
93
|
+
|
|
94
|
+
port = _find_free_port()
|
|
95
|
+
redirect_uri = f"http://localhost:{port}"
|
|
96
|
+
|
|
97
|
+
from google_auth_oauthlib.flow import Flow # type: ignore[import-untyped]
|
|
98
|
+
|
|
99
|
+
client_config = {
|
|
100
|
+
"installed": {
|
|
101
|
+
"client_id": client_id,
|
|
102
|
+
"client_secret": GOOGLE_OAUTH_CLIENT_SECRET,
|
|
103
|
+
"redirect_uris": [redirect_uri],
|
|
104
|
+
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
|
|
105
|
+
"token_uri": GOOGLE_TOKEN_URI,
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
flow: Flow = Flow.from_client_config(
|
|
110
|
+
client_config,
|
|
111
|
+
scopes=GOOGLE_SCOPES,
|
|
112
|
+
redirect_uri=redirect_uri,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
auth_url, _ = flow.authorization_url(
|
|
116
|
+
access_type="offline",
|
|
117
|
+
prompt="consent",
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
typer.echo("\nOpening browser for Google authorization...")
|
|
121
|
+
typer.echo(f"If the browser doesn't open, visit:\n {auth_url}\n")
|
|
122
|
+
webbrowser.open(auth_url)
|
|
123
|
+
|
|
124
|
+
auth_code = _wait_for_callback(port)
|
|
125
|
+
|
|
126
|
+
flow.fetch_token(code=auth_code)
|
|
127
|
+
token = flow.credentials
|
|
128
|
+
|
|
129
|
+
user_email: str | None = None
|
|
130
|
+
display_name: str | None = None
|
|
131
|
+
try:
|
|
132
|
+
import requests # type: ignore[import-untyped]
|
|
133
|
+
|
|
134
|
+
resp = requests.get(
|
|
135
|
+
"https://www.googleapis.com/oauth2/v2/userinfo",
|
|
136
|
+
headers={"Authorization": f"Bearer {token.token}"},
|
|
137
|
+
timeout=10,
|
|
138
|
+
)
|
|
139
|
+
if resp.ok:
|
|
140
|
+
info = resp.json()
|
|
141
|
+
user_email = info.get("email")
|
|
142
|
+
display_name = info.get("name")
|
|
143
|
+
except Exception:
|
|
144
|
+
pass
|
|
145
|
+
|
|
146
|
+
creds = GoogleCredentials(
|
|
147
|
+
client_id=client_id,
|
|
148
|
+
access_token=token.token or "",
|
|
149
|
+
refresh_token=token.refresh_token or "",
|
|
150
|
+
token_expiry=token.expiry,
|
|
151
|
+
user_email=user_email,
|
|
152
|
+
display_name=display_name,
|
|
153
|
+
)
|
|
154
|
+
resolved_account_id = account_id or (
|
|
155
|
+
user_email.lower() if user_email else f"google:{len(existing_accounts) + 1}"
|
|
156
|
+
)
|
|
157
|
+
if not add and len(existing_accounts) <= 1 and account_id is None:
|
|
158
|
+
save_platform(
|
|
159
|
+
"google", {**creds.model_dump(mode="json"), "account_id": resolved_account_id}
|
|
160
|
+
)
|
|
161
|
+
else:
|
|
162
|
+
upsert_platform_account("google", resolved_account_id, creds, make_default=True)
|
|
163
|
+
from agentgraph.config import CREDENTIALS_FILE
|
|
164
|
+
|
|
165
|
+
msg = f"Google credentials saved to {CREDENTIALS_FILE}"
|
|
166
|
+
if user_email:
|
|
167
|
+
msg += f" (authenticated as {user_email})"
|
|
168
|
+
typer.echo(msg)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _wait_for_callback(port: int) -> str:
|
|
172
|
+
"""Spin up a one-shot HTTP server to capture the OAuth redirect code."""
|
|
173
|
+
auth_code: list[str] = []
|
|
174
|
+
|
|
175
|
+
class _Handler(BaseHTTPRequestHandler):
|
|
176
|
+
def do_GET(self) -> None:
|
|
177
|
+
params = parse_qs(urlparse(self.path).query)
|
|
178
|
+
code = params.get("code", [""])[0]
|
|
179
|
+
auth_code.append(code)
|
|
180
|
+
self.send_response(200)
|
|
181
|
+
self.end_headers()
|
|
182
|
+
self.wfile.write(
|
|
183
|
+
b"<html><body><h2>Authorization complete. You can close this tab.</h2></body></html>"
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
def log_message(self, format: str, *args: object) -> None:
|
|
187
|
+
pass
|
|
188
|
+
|
|
189
|
+
server = HTTPServer(("localhost", port), _Handler)
|
|
190
|
+
server.handle_request()
|
|
191
|
+
server.server_close()
|
|
192
|
+
|
|
193
|
+
if not auth_code or not auth_code[0]:
|
|
194
|
+
raise typer.Exit(code=1)
|
|
195
|
+
return auth_code[0]
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
"""Google Docs connector."""
|
|
2
|
+
|
|
3
|
+
# pyright: reportUnknownMemberType=false, reportUnknownVariableType=false
|
|
4
|
+
# pyright: reportUnknownArgumentType=false, reportAttributeAccessIssue=false
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
import re
|
|
10
|
+
from datetime import UTC, datetime
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from googleapiclient.discovery import build # type: ignore[import-untyped]
|
|
14
|
+
from googleapiclient.errors import HttpError # type: ignore[import-untyped]
|
|
15
|
+
|
|
16
|
+
from agentgraph.connectors.base import (
|
|
17
|
+
BaseConnector,
|
|
18
|
+
ConnectorAccount,
|
|
19
|
+
EdgeRecord,
|
|
20
|
+
EntityBatch,
|
|
21
|
+
EntityRecord,
|
|
22
|
+
FetchPolicy,
|
|
23
|
+
PersonRecord,
|
|
24
|
+
ResourceType,
|
|
25
|
+
SourceReference,
|
|
26
|
+
)
|
|
27
|
+
from agentgraph.graph.upsert import upsert_batch
|
|
28
|
+
from agentgraph_connector_google.provider import (
|
|
29
|
+
get_credentials as google_credentials,
|
|
30
|
+
)
|
|
31
|
+
from agentgraph_connector_google.provider import (
|
|
32
|
+
get_user_email,
|
|
33
|
+
list_google_accounts,
|
|
34
|
+
verify_google_auth,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
logger = logging.getLogger(__name__)
|
|
38
|
+
|
|
39
|
+
_GDOCS_URL_RE = re.compile(r"https://docs\.google\.com/document/d/(?P<doc_id>[a-zA-Z0-9_-]+)")
|
|
40
|
+
|
|
41
|
+
# Staleness: re-fetch if doc hasn't been synced in the last 15 minutes
|
|
42
|
+
_STALE_AFTER = 15 * 60
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _build_drive_service(account_id: str | None = None) -> Any:
|
|
46
|
+
return build("drive", "v3", credentials=google_credentials(account_id))
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _build_drive_service_for(account_id: str | None) -> Any:
|
|
50
|
+
return _build_drive_service(account_id) if account_id is not None else _build_drive_service()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _web_url(doc_id: str) -> str:
|
|
54
|
+
return f"https://docs.google.com/document/d/{doc_id}/view"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _download_url(doc_id: str) -> str:
|
|
58
|
+
return f"https://www.googleapis.com/drive/v3/files/{doc_id}/export?mimeType=text/html"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _metadata(
|
|
62
|
+
doc_id: str, account_id: str | None = None
|
|
63
|
+
) -> dict[str, str | int | float | bool | None]:
|
|
64
|
+
meta: dict[str, str | int | float | bool | None] = {
|
|
65
|
+
"web_url": _web_url(doc_id),
|
|
66
|
+
"download_url": _download_url(doc_id),
|
|
67
|
+
"content_type": "text/html",
|
|
68
|
+
"mime_type": "text/html",
|
|
69
|
+
}
|
|
70
|
+
if account_id:
|
|
71
|
+
meta["account_id"] = account_id
|
|
72
|
+
return meta
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _export_as_html(drive_service: Any, doc_id: str) -> str:
|
|
76
|
+
"""Export a Google Doc as HTML via Drive."""
|
|
77
|
+
html: bytes = (
|
|
78
|
+
drive_service.files()
|
|
79
|
+
.export(
|
|
80
|
+
fileId=doc_id,
|
|
81
|
+
mimeType="text/html",
|
|
82
|
+
)
|
|
83
|
+
.execute()
|
|
84
|
+
)
|
|
85
|
+
return html.decode("utf-8", errors="replace")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class GoogleDocsConnector(BaseConnector):
|
|
89
|
+
source = "gdocs"
|
|
90
|
+
fetch_policy = FetchPolicy(stale_after_seconds=_STALE_AFTER)
|
|
91
|
+
url_patterns = ["https://docs.google.com/document/*"]
|
|
92
|
+
auth_label = "google"
|
|
93
|
+
auth_description = (
|
|
94
|
+
"Google Docs: Document entities with full HTML body content and owner authorship."
|
|
95
|
+
)
|
|
96
|
+
onboard_prompt = "Set up Google?"
|
|
97
|
+
|
|
98
|
+
@classmethod
|
|
99
|
+
def run_auth_flow(
|
|
100
|
+
cls,
|
|
101
|
+
account_id: str | None = None,
|
|
102
|
+
add: bool = False,
|
|
103
|
+
args: list[str] | None = None,
|
|
104
|
+
) -> None:
|
|
105
|
+
from agentgraph_connector_google.auth import run_oauth_flow
|
|
106
|
+
|
|
107
|
+
run_oauth_flow(account_id=account_id, add=add, args=args)
|
|
108
|
+
|
|
109
|
+
@classmethod
|
|
110
|
+
def get_authenticated_user(cls) -> str | None:
|
|
111
|
+
return get_user_email()
|
|
112
|
+
|
|
113
|
+
@classmethod
|
|
114
|
+
def list_accounts(cls) -> list[ConnectorAccount]:
|
|
115
|
+
return [
|
|
116
|
+
ConnectorAccount(
|
|
117
|
+
account_id=str(account["account_id"]),
|
|
118
|
+
label=str(account["label"]),
|
|
119
|
+
auth_group=cls.auth_label or cls.source,
|
|
120
|
+
source=cls.source,
|
|
121
|
+
user_id=account.get("email"),
|
|
122
|
+
email=account.get("email"),
|
|
123
|
+
)
|
|
124
|
+
for account in list_google_accounts()
|
|
125
|
+
]
|
|
126
|
+
|
|
127
|
+
@classmethod
|
|
128
|
+
async def verify_auth(cls, account_id: str | None = None) -> tuple[str, str | None]:
|
|
129
|
+
import asyncio
|
|
130
|
+
|
|
131
|
+
return await asyncio.to_thread(verify_google_auth, account_id)
|
|
132
|
+
|
|
133
|
+
@classmethod
|
|
134
|
+
def current_user_ids(cls) -> list[str]:
|
|
135
|
+
return [str(account["email"]) for account in list_google_accounts() if account.get("email")]
|
|
136
|
+
|
|
137
|
+
def can_handle(self, url: str) -> bool:
|
|
138
|
+
return self.resolve_url(url) is not None
|
|
139
|
+
|
|
140
|
+
def resolve_url(self, url: str) -> SourceReference | None:
|
|
141
|
+
match = _GDOCS_URL_RE.match(url)
|
|
142
|
+
if match is None:
|
|
143
|
+
return None
|
|
144
|
+
return SourceReference(
|
|
145
|
+
source=self.source,
|
|
146
|
+
resource_type="document",
|
|
147
|
+
resource_id=match.group("doc_id"),
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
def entity_url(self, platform_entity_id: str) -> str | None:
|
|
151
|
+
return f"https://docs.google.com/document/d/{platform_entity_id}"
|
|
152
|
+
|
|
153
|
+
async def fetch(
|
|
154
|
+
self,
|
|
155
|
+
resource_type: ResourceType,
|
|
156
|
+
resource_id: str,
|
|
157
|
+
meta: dict[str, str] | None = None,
|
|
158
|
+
account_id: str | None = None,
|
|
159
|
+
) -> EntityBatch:
|
|
160
|
+
last_sync = await self.last_synced_at(resource_id)
|
|
161
|
+
decision = self.fetch_policy.decide(last_sync)
|
|
162
|
+
|
|
163
|
+
if decision == FetchPolicy.FRESH:
|
|
164
|
+
logger.debug("gdocs/%s is fresh — updating last_accessed only", resource_id)
|
|
165
|
+
await _touch_last_accessed(resource_id)
|
|
166
|
+
return EntityBatch()
|
|
167
|
+
|
|
168
|
+
logger.info("Fetching Google Doc %s (policy=%s)", resource_id, decision)
|
|
169
|
+
selected_account_id = account_id or ((meta or {}).get("account_id") if meta else None)
|
|
170
|
+
batch = await _fetch_doc(resource_id, account_id=selected_account_id)
|
|
171
|
+
await upsert_batch(batch)
|
|
172
|
+
return batch
|
|
173
|
+
|
|
174
|
+
async def download(
|
|
175
|
+
self,
|
|
176
|
+
resource_type: ResourceType,
|
|
177
|
+
resource_id: str,
|
|
178
|
+
output_path: str | None = None,
|
|
179
|
+
) -> dict[str, Any]:
|
|
180
|
+
from agentgraph_connector_google.gdrive import download_drive_file
|
|
181
|
+
|
|
182
|
+
return await download_drive_file(resource_id, output_path)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
async def _touch_last_accessed(doc_id: str) -> None:
|
|
186
|
+
from agentgraph.core.context import get_backend
|
|
187
|
+
|
|
188
|
+
await get_backend().touch_last_accessed("gdocs", doc_id)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
async def _fetch_doc(doc_id: str, account_id: str | None = None) -> EntityBatch:
|
|
192
|
+
import asyncio
|
|
193
|
+
|
|
194
|
+
loop = asyncio.get_event_loop()
|
|
195
|
+
drive_service = await loop.run_in_executor(None, _build_drive_service_for, account_id)
|
|
196
|
+
|
|
197
|
+
try:
|
|
198
|
+
file_meta: dict[str, Any] = await loop.run_in_executor(
|
|
199
|
+
None,
|
|
200
|
+
lambda: (
|
|
201
|
+
drive_service.files()
|
|
202
|
+
.get(
|
|
203
|
+
fileId=doc_id,
|
|
204
|
+
fields="name,owners",
|
|
205
|
+
)
|
|
206
|
+
.execute()
|
|
207
|
+
),
|
|
208
|
+
)
|
|
209
|
+
except HttpError as exc:
|
|
210
|
+
if exc.resp.status == 404:
|
|
211
|
+
raise ValueError(f"Google Doc not found or not accessible: {doc_id}") from exc
|
|
212
|
+
raise
|
|
213
|
+
|
|
214
|
+
title: str = file_meta.get("name", "")
|
|
215
|
+
content = await loop.run_in_executor(None, _export_as_html, drive_service, doc_id)
|
|
216
|
+
|
|
217
|
+
persons: list[PersonRecord] = []
|
|
218
|
+
edges: list[EdgeRecord] = []
|
|
219
|
+
|
|
220
|
+
for owner in file_meta.get("owners", []):
|
|
221
|
+
email: str = owner.get("emailAddress", "")
|
|
222
|
+
name: str = owner.get("displayName", "")
|
|
223
|
+
if email:
|
|
224
|
+
persons.append(
|
|
225
|
+
PersonRecord(
|
|
226
|
+
platform="gdocs",
|
|
227
|
+
platform_user_id=email,
|
|
228
|
+
canonical_email=email,
|
|
229
|
+
display_name=name or None,
|
|
230
|
+
)
|
|
231
|
+
)
|
|
232
|
+
edges.append(
|
|
233
|
+
EdgeRecord(
|
|
234
|
+
edge_type="authored",
|
|
235
|
+
source_platform_user_id=email,
|
|
236
|
+
target_platform_entity_id=doc_id,
|
|
237
|
+
platform="gdocs",
|
|
238
|
+
)
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
entity = EntityRecord(
|
|
242
|
+
entity_type="Document",
|
|
243
|
+
platform="gdocs",
|
|
244
|
+
platform_entity_id=doc_id,
|
|
245
|
+
title=title,
|
|
246
|
+
content=content,
|
|
247
|
+
metadata=_metadata(doc_id, account_id),
|
|
248
|
+
updated_at=datetime.now(UTC),
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
batch = EntityBatch(entities=[entity], persons=persons, edges=edges)
|
|
252
|
+
batch.add_stubs_from(entity)
|
|
253
|
+
return batch
|