gslides-automator 0.4.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.
- gslides_automator/__init__.py +43 -0
- gslides_automator/__main__.py +7 -0
- gslides_automator/auth.py +103 -0
- gslides_automator/cli.py +96 -0
- gslides_automator/drive_layout.py +233 -0
- gslides_automator/generate_data.py +937 -0
- gslides_automator/generate_report.py +2761 -0
- gslides_automator-0.4.0.dist-info/METADATA +131 -0
- gslides_automator-0.4.0.dist-info/RECORD +13 -0
- gslides_automator-0.4.0.dist-info/WHEEL +5 -0
- gslides_automator-0.4.0.dist-info/entry_points.txt +2 -0
- gslides_automator-0.4.0.dist-info/licenses/LICENSE.txt +21 -0
- gslides_automator-0.4.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Package exports for gslides_automator.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from typing import List
|
|
8
|
+
|
|
9
|
+
from gslides_automator.auth import load_credentials
|
|
10
|
+
from gslides_automator.generate_data import generate_data as _gd
|
|
11
|
+
from gslides_automator.generate_report import generate_report as _gr
|
|
12
|
+
from gslides_automator.drive_layout import DriveLayout, resolve_layout
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def generate_data(
|
|
16
|
+
shared_drive_url: str,
|
|
17
|
+
service_account_credentials: str,
|
|
18
|
+
) -> dict:
|
|
19
|
+
"""Populate L1-Data from L0-Data using the shared-drive layout."""
|
|
20
|
+
creds = load_credentials(service_account_credentials)
|
|
21
|
+
layout: DriveLayout = resolve_layout(shared_drive_url, creds)
|
|
22
|
+
|
|
23
|
+
return _gd.generate_data(
|
|
24
|
+
creds=creds,
|
|
25
|
+
layout=layout,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def generate_report(
|
|
30
|
+
shared_drive_url: str,
|
|
31
|
+
service_account_credentials: str,
|
|
32
|
+
) -> dict:
|
|
33
|
+
"""Build Google Slides reports from L1-Data for entities marked with generate=Y."""
|
|
34
|
+
creds = load_credentials(service_account_credentials)
|
|
35
|
+
layout: DriveLayout = resolve_layout(shared_drive_url, creds)
|
|
36
|
+
|
|
37
|
+
return _gr.generate_report(
|
|
38
|
+
creds=creds,
|
|
39
|
+
input_folder_id=layout.l1_data_id,
|
|
40
|
+
template_id=layout.report_template_id,
|
|
41
|
+
output_folder_id=layout.l2_report_id,
|
|
42
|
+
layout=layout,
|
|
43
|
+
)
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Shared authentication module for Google API access.
|
|
4
|
+
Provides service account credential management for Google Sheets, Drive, and Slides APIs.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from google.oauth2 import service_account
|
|
8
|
+
import os
|
|
9
|
+
|
|
10
|
+
# Combined scopes required by all scripts
|
|
11
|
+
SCOPES = [
|
|
12
|
+
'https://www.googleapis.com/auth/spreadsheets', # Full access (covers readonly)
|
|
13
|
+
'https://www.googleapis.com/auth/drive.readonly', # For reading/copying templates
|
|
14
|
+
'https://www.googleapis.com/auth/drive.file', # For creating and moving files
|
|
15
|
+
'https://www.googleapis.com/auth/drive', # Full Drive access for modifying file permissions
|
|
16
|
+
'https://www.googleapis.com/auth/presentations' # For modifying slides
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
# Get the project root directory
|
|
20
|
+
PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
21
|
+
SERVICE_ACCOUNT_CREDENTIALS = os.path.join(PROJECT_ROOT, 'service-account-credentials.json')
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def get_oauth_credentials(service_account_credentials: str | None = None, scopes=None):
|
|
25
|
+
"""
|
|
26
|
+
Get service account credentials using a JSON key file. Defaults to service-account-credentials.json in repo root.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
service_account_credentials: Optional path to a service account key JSON.
|
|
30
|
+
scopes: Optional list of scopes to request.
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
google.oauth2.service_account.Credentials: Service account credentials object
|
|
34
|
+
|
|
35
|
+
Raises:
|
|
36
|
+
FileNotFoundError: If the credentials file is not found
|
|
37
|
+
"""
|
|
38
|
+
key_path = service_account_credentials or SERVICE_ACCOUNT_CREDENTIALS
|
|
39
|
+
if not os.path.exists(key_path):
|
|
40
|
+
raise FileNotFoundError(
|
|
41
|
+
f"Service account credentials file '{key_path}' not found. "
|
|
42
|
+
"Please download service account credentials from Google Cloud Console."
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
requested_scopes = scopes or SCOPES
|
|
46
|
+
|
|
47
|
+
creds = service_account.Credentials.from_service_account_file(
|
|
48
|
+
key_path,
|
|
49
|
+
scopes=requested_scopes
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
return creds
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def load_credentials(service_account_credentials: str):
|
|
56
|
+
"""
|
|
57
|
+
Load service-account credentials from a provided path.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
service_account_credentials: Path to the service account JSON key file.
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
google.oauth2.service_account.Credentials: Service account credentials object
|
|
64
|
+
|
|
65
|
+
Raises:
|
|
66
|
+
ValueError: If service_account_credentials is empty
|
|
67
|
+
FileNotFoundError: If the credentials file is not found
|
|
68
|
+
"""
|
|
69
|
+
if not service_account_credentials:
|
|
70
|
+
raise ValueError("service_account_credentials is required.")
|
|
71
|
+
if not os.path.exists(service_account_credentials):
|
|
72
|
+
raise FileNotFoundError(f"Credentials file not found: {service_account_credentials}")
|
|
73
|
+
return get_oauth_credentials(service_account_credentials=service_account_credentials, scopes=SCOPES)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def get_service_account_email():
|
|
77
|
+
"""
|
|
78
|
+
Get the service account email from the credentials file.
|
|
79
|
+
Useful for sharing files/folders with the service account.
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
str: Service account email address
|
|
83
|
+
|
|
84
|
+
Raises:
|
|
85
|
+
FileNotFoundError: If service-account-credentials.json is not found
|
|
86
|
+
KeyError: If email is not found in the credentials file
|
|
87
|
+
"""
|
|
88
|
+
import json
|
|
89
|
+
|
|
90
|
+
if not os.path.exists(SERVICE_ACCOUNT_CREDENTIALS):
|
|
91
|
+
raise FileNotFoundError(
|
|
92
|
+
f"Service account credentials file '{SERVICE_ACCOUNT_CREDENTIALS}' not found."
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
with open(SERVICE_ACCOUNT_CREDENTIALS, 'r') as f:
|
|
96
|
+
creds_data = json.load(f)
|
|
97
|
+
|
|
98
|
+
email = creds_data.get('client_email')
|
|
99
|
+
if not email:
|
|
100
|
+
raise KeyError("'client_email' not found in service account credentials file")
|
|
101
|
+
|
|
102
|
+
return email
|
|
103
|
+
|
gslides_automator/cli.py
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""Command-line interface for gslides_automator."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import sys
|
|
7
|
+
from typing import Callable
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _run_generate_data(args: argparse.Namespace) -> int:
|
|
11
|
+
"""Entrypoint for the `generate-data` subcommand."""
|
|
12
|
+
from .generate_data import generate_data, get_oauth_credentials, resolve_layout
|
|
13
|
+
|
|
14
|
+
creds = get_oauth_credentials(service_account_credentials=args.service_account_credentials)
|
|
15
|
+
layout = resolve_layout(args.shared_drive_url, creds)
|
|
16
|
+
generate_data(
|
|
17
|
+
creds=creds,
|
|
18
|
+
layout=layout,
|
|
19
|
+
)
|
|
20
|
+
return 0
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _run_generate_report(args: argparse.Namespace) -> int:
|
|
24
|
+
"""Entrypoint for the `generate-report` subcommand."""
|
|
25
|
+
from .generate_report import generate_report, get_oauth_credentials, resolve_layout
|
|
26
|
+
|
|
27
|
+
creds = get_oauth_credentials(service_account_credentials=args.service_account_credentials)
|
|
28
|
+
layout = resolve_layout(args.shared_drive_url, creds)
|
|
29
|
+
generate_report(
|
|
30
|
+
creds=creds,
|
|
31
|
+
layout=layout,
|
|
32
|
+
)
|
|
33
|
+
return 0
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
37
|
+
parser = argparse.ArgumentParser(
|
|
38
|
+
prog="gslides_automator",
|
|
39
|
+
description="Utilities for generating data and reports from Google Drive and Sheets.",
|
|
40
|
+
)
|
|
41
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
42
|
+
|
|
43
|
+
gen_parser = subparsers.add_parser(
|
|
44
|
+
"generate-data",
|
|
45
|
+
help="Generate L1 data from the shared drive layout.",
|
|
46
|
+
)
|
|
47
|
+
gen_parser.add_argument(
|
|
48
|
+
"--shared-drive-url",
|
|
49
|
+
required=True,
|
|
50
|
+
help="Shared Drive root URL or ID that contains L0/L1 data and templates.",
|
|
51
|
+
)
|
|
52
|
+
gen_parser.add_argument(
|
|
53
|
+
"--service-account-credentials",
|
|
54
|
+
default=None,
|
|
55
|
+
help="Path to the service account JSON key file. Defaults to service-account-credentials.json in the project root.",
|
|
56
|
+
)
|
|
57
|
+
gen_parser.set_defaults(func=_run_generate_data)
|
|
58
|
+
|
|
59
|
+
report_parser = subparsers.add_parser(
|
|
60
|
+
"generate-report",
|
|
61
|
+
help="Generate Google Slides reports from L1 data.",
|
|
62
|
+
)
|
|
63
|
+
report_parser.add_argument(
|
|
64
|
+
"--shared-drive-file",
|
|
65
|
+
required=True,
|
|
66
|
+
help="Shared Drive root URL or ID that contains L1/L2 data, templates, and entities.",
|
|
67
|
+
)
|
|
68
|
+
report_parser.add_argument(
|
|
69
|
+
"--credentials",
|
|
70
|
+
dest="credentials",
|
|
71
|
+
default=None,
|
|
72
|
+
help="Path to the service account JSON key file. Defaults to service-account-credentials.json in the project root.",
|
|
73
|
+
)
|
|
74
|
+
report_parser.set_defaults(func=_run_generate_report)
|
|
75
|
+
|
|
76
|
+
return parser
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def main(argv: list[str] | None = None) -> int:
|
|
80
|
+
parser = _build_parser()
|
|
81
|
+
args = parser.parse_args(argv)
|
|
82
|
+
|
|
83
|
+
handler: Callable[[argparse.Namespace], int] | None = getattr(args, "func", None)
|
|
84
|
+
if handler is None:
|
|
85
|
+
parser.print_help()
|
|
86
|
+
return 1
|
|
87
|
+
|
|
88
|
+
try:
|
|
89
|
+
return handler(args)
|
|
90
|
+
except Exception as exc: # pragma: no cover - CLI guardrail
|
|
91
|
+
print(f"Error: {exc}", file=sys.stderr)
|
|
92
|
+
return 1
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
if __name__ == "__main__": # pragma: no cover
|
|
96
|
+
sys.exit(main())
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
import csv
|
|
5
|
+
import io
|
|
6
|
+
import re
|
|
7
|
+
from typing import Dict, Iterable, List, Optional, Sequence, Set
|
|
8
|
+
|
|
9
|
+
from googleapiclient.discovery import build
|
|
10
|
+
from googleapiclient.http import MediaIoBaseDownload
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class DriveLayout:
|
|
15
|
+
root_id: str
|
|
16
|
+
l0_data_id: str
|
|
17
|
+
l1_data_id: str
|
|
18
|
+
l2_report_id: str
|
|
19
|
+
templates_id: str
|
|
20
|
+
data_template_id: str
|
|
21
|
+
report_template_id: str
|
|
22
|
+
entities_csv_id: str
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _extract_id_from_url(shared_drive_url: str) -> str:
|
|
26
|
+
"""
|
|
27
|
+
Extract a Drive folder/file ID from a shared Drive URL or raw ID.
|
|
28
|
+
"""
|
|
29
|
+
if re.fullmatch(r"[A-Za-z0-9_\-]+", shared_drive_url):
|
|
30
|
+
return shared_drive_url
|
|
31
|
+
|
|
32
|
+
patterns = [
|
|
33
|
+
r"/folders/([A-Za-z0-9_\-]+)",
|
|
34
|
+
r"[?&]id=([A-Za-z0-9_\-]+)",
|
|
35
|
+
]
|
|
36
|
+
for pattern in patterns:
|
|
37
|
+
match = re.search(pattern, shared_drive_url)
|
|
38
|
+
if match:
|
|
39
|
+
return match.group(1)
|
|
40
|
+
raise ValueError("Could not extract Drive folder ID from URL. Pass a folder link or ID.")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _find_child_by_name(
|
|
44
|
+
drive_service,
|
|
45
|
+
parent_id: str,
|
|
46
|
+
names: str | Sequence[str],
|
|
47
|
+
mime_type: str | None = None,
|
|
48
|
+
) -> str:
|
|
49
|
+
"""
|
|
50
|
+
Locate a child by exact name (supports multiple candidate names).
|
|
51
|
+
"""
|
|
52
|
+
candidates: Iterable[str] = [names] if isinstance(names, str) else names
|
|
53
|
+
mime_clause = f" and mimeType='{mime_type}'" if mime_type else ""
|
|
54
|
+
|
|
55
|
+
for name in candidates:
|
|
56
|
+
query = f"'{parent_id}' in parents and name='{name}' and trashed=false{mime_clause}"
|
|
57
|
+
result = drive_service.files().list(
|
|
58
|
+
q=query,
|
|
59
|
+
fields="files(id,name,mimeType)",
|
|
60
|
+
supportsAllDrives=True,
|
|
61
|
+
includeItemsFromAllDrives=True,
|
|
62
|
+
pageSize=5,
|
|
63
|
+
).execute()
|
|
64
|
+
files = result.get("files", [])
|
|
65
|
+
if files:
|
|
66
|
+
return files[0]["id"]
|
|
67
|
+
|
|
68
|
+
raise FileNotFoundError(
|
|
69
|
+
f"Could not find any of {list(candidates)} inside parent id {parent_id}"
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def resolve_layout(shared_drive_url: str, creds) -> DriveLayout:
|
|
74
|
+
"""
|
|
75
|
+
Discover the standard folder/file layout starting from the shared drive URL.
|
|
76
|
+
"""
|
|
77
|
+
drive_service = build("drive", "v3", credentials=creds)
|
|
78
|
+
root_id = _extract_id_from_url(shared_drive_url)
|
|
79
|
+
|
|
80
|
+
l0_id = _find_child_by_name(drive_service, root_id, "L0-Data", mime_type="application/vnd.google-apps.folder")
|
|
81
|
+
l1_id = _find_child_by_name(drive_service, root_id, "L1-Data", mime_type="application/vnd.google-apps.folder")
|
|
82
|
+
l2_id = _find_child_by_name(drive_service, root_id, "L2-Reports", mime_type="application/vnd.google-apps.folder")
|
|
83
|
+
templates_id = _find_child_by_name(drive_service, root_id, "Templates", mime_type="application/vnd.google-apps.folder")
|
|
84
|
+
|
|
85
|
+
data_template_id = _find_child_by_name(
|
|
86
|
+
drive_service,
|
|
87
|
+
templates_id,
|
|
88
|
+
names=("data-template.gsheet", "data-template"),
|
|
89
|
+
mime_type="application/vnd.google-apps.spreadsheet",
|
|
90
|
+
)
|
|
91
|
+
report_template_id = _find_child_by_name(
|
|
92
|
+
drive_service,
|
|
93
|
+
templates_id,
|
|
94
|
+
names=("report-template.gslide", "report-template"),
|
|
95
|
+
mime_type="application/vnd.google-apps.presentation",
|
|
96
|
+
)
|
|
97
|
+
entities_csv_id = _find_child_by_name(
|
|
98
|
+
drive_service,
|
|
99
|
+
root_id,
|
|
100
|
+
names=("entities.csv", "entities"),
|
|
101
|
+
mime_type="text/csv",
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
return DriveLayout(
|
|
105
|
+
root_id=root_id,
|
|
106
|
+
l0_data_id=l0_id,
|
|
107
|
+
l1_data_id=l1_id,
|
|
108
|
+
l2_report_id=l2_id,
|
|
109
|
+
templates_id=templates_id,
|
|
110
|
+
data_template_id=data_template_id,
|
|
111
|
+
report_template_id=report_template_id,
|
|
112
|
+
entities_csv_id=entities_csv_id,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def load_entities(entities_csv_id: str, creds) -> List[str]:
|
|
117
|
+
"""
|
|
118
|
+
Download entities.csv and return entity names (first column) where the adjacent
|
|
119
|
+
`generate` column (second column) is exactly `Y`.
|
|
120
|
+
"""
|
|
121
|
+
drive_service = build("drive", "v3", credentials=creds)
|
|
122
|
+
request = drive_service.files().get_media(fileId=entities_csv_id, supportsAllDrives=True)
|
|
123
|
+
buffer = io.BytesIO()
|
|
124
|
+
downloader = MediaIoBaseDownload(buffer, request)
|
|
125
|
+
done = False
|
|
126
|
+
while not done:
|
|
127
|
+
status, done = downloader.next_chunk()
|
|
128
|
+
buffer.seek(0)
|
|
129
|
+
content = buffer.read().decode("utf-8")
|
|
130
|
+
|
|
131
|
+
reader = csv.reader(io.StringIO(content))
|
|
132
|
+
entities: List[str] = []
|
|
133
|
+
for row in reader:
|
|
134
|
+
if not row:
|
|
135
|
+
continue
|
|
136
|
+
|
|
137
|
+
name = row[0].strip()
|
|
138
|
+
generate_flag = row[1].strip() if len(row) > 1 else ""
|
|
139
|
+
|
|
140
|
+
if not name:
|
|
141
|
+
continue
|
|
142
|
+
|
|
143
|
+
# Skip header row
|
|
144
|
+
if not entities and name.lower().startswith("entity"):
|
|
145
|
+
continue
|
|
146
|
+
|
|
147
|
+
# Only include rows explicitly marked for generation
|
|
148
|
+
if generate_flag == "Y":
|
|
149
|
+
entities.append(name)
|
|
150
|
+
return entities
|
|
151
|
+
|
|
152
|
+
def _parse_slides_value(slides_value: str) -> Optional[Set[int]]:
|
|
153
|
+
"""
|
|
154
|
+
Parse a slides column value into a set of slide numbers.
|
|
155
|
+
|
|
156
|
+
Accepts comma-separated numbers and ranges (e.g., "1,2-4,6").
|
|
157
|
+
Returns None when the value is blank or no valid numbers are found
|
|
158
|
+
to indicate that all slides should be processed.
|
|
159
|
+
"""
|
|
160
|
+
if not slides_value:
|
|
161
|
+
return None
|
|
162
|
+
|
|
163
|
+
slides: Set[int] = set()
|
|
164
|
+
for part in slides_value.split(","):
|
|
165
|
+
part = part.strip()
|
|
166
|
+
if not part:
|
|
167
|
+
continue
|
|
168
|
+
|
|
169
|
+
if "-" in part:
|
|
170
|
+
bounds = part.split("-", 1)
|
|
171
|
+
if len(bounds) != 2:
|
|
172
|
+
continue
|
|
173
|
+
try:
|
|
174
|
+
start = int(bounds[0].strip())
|
|
175
|
+
end = int(bounds[1].strip())
|
|
176
|
+
except ValueError:
|
|
177
|
+
continue
|
|
178
|
+
|
|
179
|
+
if start > end:
|
|
180
|
+
start, end = end, start
|
|
181
|
+
|
|
182
|
+
for num in range(start, end + 1):
|
|
183
|
+
if num > 0:
|
|
184
|
+
slides.add(num)
|
|
185
|
+
else:
|
|
186
|
+
try:
|
|
187
|
+
value = int(part)
|
|
188
|
+
except ValueError:
|
|
189
|
+
continue
|
|
190
|
+
if value > 0:
|
|
191
|
+
slides.add(value)
|
|
192
|
+
|
|
193
|
+
return slides or None
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def load_entities_with_slides(entities_csv_id: str, creds) -> Dict[str, Optional[Set[int]]]:
|
|
197
|
+
"""
|
|
198
|
+
Download entities.csv and return a mapping of entity name to requested slide
|
|
199
|
+
numbers for rows marked with generate=Y. A value of None means all slides.
|
|
200
|
+
"""
|
|
201
|
+
drive_service = build("drive", "v3", credentials=creds)
|
|
202
|
+
request = drive_service.files().get_media(fileId=entities_csv_id, supportsAllDrives=True)
|
|
203
|
+
buffer = io.BytesIO()
|
|
204
|
+
downloader = MediaIoBaseDownload(buffer, request)
|
|
205
|
+
done = False
|
|
206
|
+
while not done:
|
|
207
|
+
status, done = downloader.next_chunk()
|
|
208
|
+
buffer.seek(0)
|
|
209
|
+
content = buffer.read().decode("utf-8")
|
|
210
|
+
|
|
211
|
+
reader = csv.reader(io.StringIO(content))
|
|
212
|
+
entities: Dict[str, Optional[Set[int]]] = {}
|
|
213
|
+
for row in reader:
|
|
214
|
+
if not row:
|
|
215
|
+
continue
|
|
216
|
+
|
|
217
|
+
name = row[0].strip()
|
|
218
|
+
generate_flag = row[1].strip() if len(row) > 1 else ""
|
|
219
|
+
slides_value = row[2].strip() if len(row) > 2 else ""
|
|
220
|
+
|
|
221
|
+
if not name:
|
|
222
|
+
continue
|
|
223
|
+
|
|
224
|
+
# Skip header row
|
|
225
|
+
if not entities and name.lower().startswith("entity"):
|
|
226
|
+
continue
|
|
227
|
+
|
|
228
|
+
if generate_flag == "Y":
|
|
229
|
+
slides = _parse_slides_value(slides_value)
|
|
230
|
+
entities[name] = slides
|
|
231
|
+
|
|
232
|
+
return entities
|
|
233
|
+
|