oto-cli 1.0.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.
Files changed (217) hide show
  1. oto/__init__.py +3 -0
  2. oto/cli.py +74 -0
  3. oto/commands/__init__.py +0 -0
  4. oto/commands/anthropic.py +64 -0
  5. oto/commands/audio.py +91 -0
  6. oto/commands/browser.py +257 -0
  7. oto/commands/company.py +23 -0
  8. oto/commands/enrichment.py +152 -0
  9. oto/commands/folk.py +238 -0
  10. oto/commands/google.py +355 -0
  11. oto/commands/notion.py +58 -0
  12. oto/commands/pennylane.py +272 -0
  13. oto/commands/search.py +36 -0
  14. oto/commands/sirene.py +201 -0
  15. oto/commands/skills.py +91 -0
  16. oto/commands/whatsapp.py +47 -0
  17. oto/config.py +167 -0
  18. oto/tools/.cache/notion/07a366cf2d7a0f77e7a1736805e94786e6053bb581b6b7179591b71d34843ce8.json +98 -0
  19. oto/tools/.cache/notion/0b21878dbd4bf5a67ff52104361a07ee575b03a075b7b81cbc4a0d5d4045f759.json +142 -0
  20. oto/tools/.cache/notion/0bf47e753ec29da03776bca010122393b4fc188af7f28dd11b456a74e5cae8f4.json +98 -0
  21. oto/tools/.cache/notion/1b2c924688fdcef8b7e3748f92ed4fdb232cadb387a7e9e157c438d32374ff97.json +4356 -0
  22. oto/tools/.cache/notion/2a66512043d3531dc900b8244fc13972c0abfe7760c10fbb7fc21f309238ad0b.json +132 -0
  23. oto/tools/.cache/notion/2b59aed15d0c747ec2cae70a1eb3f554b3e170875110b82f8e4614b761c84dcf.json +142 -0
  24. oto/tools/.cache/notion/2f7d1284021d30e842cc9a2b506e87b4a32c61e8e9abb616aec37828c441231d.json +220 -0
  25. oto/tools/.cache/notion/33eb548bce70fb7b0e8eb09211ba6c03ad22b6e7a289606b05c5d690f724d8e4.json +1659 -0
  26. oto/tools/.cache/notion/364d3a5e790f3d8b5137aae0a5a9ee151cd8c9f7dacb4fd669220f968405ea36.json +88 -0
  27. oto/tools/.cache/notion/3e7e836a6bf18f951d4887fefeacb146473491ad2cbaa1793c79af08b7ef45b6.json +132 -0
  28. oto/tools/.cache/notion/3f95b2d8bdf3d47e8f6c167d6d9a0889f27b64e8c78f0afdad538fecae070858.json +98 -0
  29. oto/tools/.cache/notion/46c38d10065fb68ca912692df53b6e54eb1f6f9cdff342de563020917e27ccf5.json +98 -0
  30. oto/tools/.cache/notion/51fa4d44044d751ca2319d87d607bd58a01004a24a8065808d65c273b35d3a10.json +210 -0
  31. oto/tools/.cache/notion/59888e9c297cda6836afb560fb4048b100f98eb3abead8b175fe92fbf858210b.json +186 -0
  32. oto/tools/.cache/notion/60df8d17a4e08fdbafb0ea398ff68d45cb79da7c01bd129e98264be1bff7ed1d.json +176 -0
  33. oto/tools/.cache/notion/62a44af3f86b5841d905789565a0208f80c1a304bcfc8889ccd55f61f29508b2.json +132 -0
  34. oto/tools/.cache/notion/69ec094fc12a40d1591abbd4f21295af672389963f900ea421a6d75b6f21f03b.json +176 -0
  35. oto/tools/.cache/notion/703fc49f07e73447e176fa69f31c7ff7bbb7c8c95325af4ca1a8664385bfeb0b.json +98 -0
  36. oto/tools/.cache/notion/7e247cf818c2a930de8a5a8599716900cb5af90570a9f701699dd8897fe43c8d.json +88 -0
  37. oto/tools/.cache/notion/86741d020ca89789bda8156315ead5e980068c19754baf6e611ffc618557dc06.json +115 -0
  38. oto/tools/.cache/notion/8cc6e5e1d955310412df09c9179ec67d42767d1b557262e28bc2afea05ab7c24.json +210 -0
  39. oto/tools/.cache/notion/989d88c22dd009e5683987e70671d1e20fd6a5a3e9ee7670db9ea602fe8fbded.json +4605 -0
  40. oto/tools/.cache/notion/999d01d80c8ef0c856572028e31e701cad8c5704b8886388b756d32ef50cbfaf.json +132 -0
  41. oto/tools/.cache/notion/9f10e5fa6404211b156b2f5c22a0644e5eb0c49774d2b4f8d768211f0c6aea22.json +3380 -0
  42. oto/tools/.cache/notion/9f950bdfc2aeb9ae5e1b185e827636f83fa44a9d7de0d1462d87926508b26907.json +142 -0
  43. oto/tools/.cache/notion/a4719c76c9ff4c4b056e87689e2f487770c35d82d70acb098ead7712e0d3be8d.json +142 -0
  44. oto/tools/.cache/notion/a808160a7352fa1a3029ef1e2929dfa99b5021b0d22ce1cfb9efe3c68f7e7af1.json +98 -0
  45. oto/tools/.cache/notion/a9c29806556409fbfcd7622b876244caed085c63c70380dc6caacd162e6d05a9.json +132 -0
  46. oto/tools/.cache/notion/b3142c5509452a15382cade5cb81324a26d1946d128a1b4342a143872f9354fe.json +149 -0
  47. oto/tools/.cache/notion/b516a0820a66ae04229bfcb7f0463be0867f31a8dc1b875e57aba02b970a7a31.json +98 -0
  48. oto/tools/.cache/notion/c175f8c35c81ebd2fb9e4bf4d2f1cc29f98b45006f59354c9f33ee780609d896.json +247 -0
  49. oto/tools/.cache/notion/c950254390b901d475937c868e27e74960720fe65e2d8506bd39e30c634e619d.json +98 -0
  50. oto/tools/.cache/notion/cac99fb54fa178e1a7f72e5212e1e99eff5fe72a1fa52f65fcee432bcf528b53.json +166 -0
  51. oto/tools/.cache/notion/e38237abecf19ce5b9a92ed90723d2642a11b0f1ef189b1a1fc10b7ebcd3d4a4.json +88 -0
  52. oto/tools/.cache/notion/e58e933def9b7000d791c83f6660adbecac59ab61e54f8d855a52d80200eb929.json +292 -0
  53. oto/tools/.cache/notion/f8088377a1049850b0ec1acabf09da8504fb1f194479cc43ce4c31db3895afcd.json +176 -0
  54. oto/tools/.cache/notion/fc2ccb6edbe2020010e036a69e7ece3ab3f007c9a4fed39066d6792d332e65af.json +142 -0
  55. oto/tools/.cache/notion/ff117c74176f8fc26aba7f06649c7794dc4a0c51194033f453b16c8af8ac4201.json +36 -0
  56. oto/tools/__init__.py +1 -0
  57. oto/tools/anthropic/__init__.py +5 -0
  58. oto/tools/anthropic/client.py +354 -0
  59. oto/tools/anthropic_batch/__init__.py +5 -0
  60. oto/tools/anthropic_batch/client.py +352 -0
  61. oto/tools/apollo/__init__.py +5 -0
  62. oto/tools/apollo/client.py +191 -0
  63. oto/tools/attio/__init__.py +5 -0
  64. oto/tools/attio/client.py +242 -0
  65. oto/tools/audio/__init__.py +5 -0
  66. oto/tools/audio/client.py +116 -0
  67. oto/tools/browser/__init__.py +26 -0
  68. oto/tools/browser/crunchbase.py +423 -0
  69. oto/tools/browser/g2.py +236 -0
  70. oto/tools/browser/indeed.py +282 -0
  71. oto/tools/browser/linkedin.py +821 -0
  72. oto/tools/browser/pappers.py +344 -0
  73. oto/tools/clearbit/__init__.py +5 -0
  74. oto/tools/clearbit/client.py +126 -0
  75. oto/tools/collective/__init__.py +5 -0
  76. oto/tools/collective/client.py +333 -0
  77. oto/tools/common/__init__.py +5 -0
  78. oto/tools/common/rate_limiter.py +457 -0
  79. oto/tools/figma/__init__.py +5 -0
  80. oto/tools/figma/client.py +254 -0
  81. oto/tools/folk/__init__.py +5 -0
  82. oto/tools/folk/client.py +203 -0
  83. oto/tools/gemini/__init__.py +5 -0
  84. oto/tools/gemini/client.py +264 -0
  85. oto/tools/google/__init__.py +1 -0
  86. oto/tools/google/calendar/__init__.py +0 -0
  87. oto/tools/google/calendar/lib/__init__.py +0 -0
  88. oto/tools/google/calendar/lib/calendar_client.py +155 -0
  89. oto/tools/google/credentials.py +177 -0
  90. oto/tools/google/docs/get_section.py +53 -0
  91. oto/tools/google/docs/insert_section.py +56 -0
  92. oto/tools/google/docs/insert_text.py +99 -0
  93. oto/tools/google/docs/lib/__init__.py +0 -0
  94. oto/tools/google/docs/lib/docs_client.py +341 -0
  95. oto/tools/google/docs/list_headings.py +44 -0
  96. oto/tools/google/docs/move_section.py +37 -0
  97. oto/tools/google/docs/replace_section.py +47 -0
  98. oto/tools/google/docs/sync/__init__.py +1 -0
  99. oto/tools/google/docs/sync/gdoc_sync.py +427 -0
  100. oto/tools/google/drive/README.md +308 -0
  101. oto/tools/google/drive/check_quota.py +31 -0
  102. oto/tools/google/drive/copy_file.py +76 -0
  103. oto/tools/google/drive/create_folder.py +58 -0
  104. oto/tools/google/drive/download_file.py +60 -0
  105. oto/tools/google/drive/export_doc.py +64 -0
  106. oto/tools/google/drive/export_slides.py +95 -0
  107. oto/tools/google/drive/extract_slides_content.py +113 -0
  108. oto/tools/google/drive/lib/drive_client.py +454 -0
  109. oto/tools/google/drive/list_files.py +97 -0
  110. oto/tools/google/drive/list_shared_drives.py +32 -0
  111. oto/tools/google/drive/move_file.py +57 -0
  112. oto/tools/google/drive/requirements.txt +4 -0
  113. oto/tools/google/drive/tool.yaml +210 -0
  114. oto/tools/google/drive/upload_file.py +90 -0
  115. oto/tools/google/gmail/__init__.py +1 -0
  116. oto/tools/google/gmail/get_message.py +33 -0
  117. oto/tools/google/gmail/lib/__init__.py +1 -0
  118. oto/tools/google/gmail/lib/gmail_client.py +385 -0
  119. oto/tools/google/gmail/list_messages.py +37 -0
  120. oto/tools/google/gmail/requirements.txt +3 -0
  121. oto/tools/google/gmail/search.py +34 -0
  122. oto/tools/google/gmail/send.py +39 -0
  123. oto/tools/google/gmail/tool.yaml +107 -0
  124. oto/tools/google/keep/__init__.py +0 -0
  125. oto/tools/google/keep/create_note.py +42 -0
  126. oto/tools/google/keep/get_master_token.py +101 -0
  127. oto/tools/google/keep/get_note.py +33 -0
  128. oto/tools/google/keep/lib/__init__.py +0 -0
  129. oto/tools/google/keep/lib/keep_client.py +273 -0
  130. oto/tools/google/keep/list_notes.py +42 -0
  131. oto/tools/google/keep/search.py +34 -0
  132. oto/tools/google/sheets/create_sheet_direct.py +157 -0
  133. oto/tools/google/sheets/create_sheet_from_csv.py +167 -0
  134. oto/tools/google/sheets/create_sheet_in_folder.py +165 -0
  135. oto/tools/google/sheets/create_sheet_via_drive.py +157 -0
  136. oto/tools/google/sheets/tool.yaml +74 -0
  137. oto/tools/google/slides/.folders +19 -0
  138. oto/tools/google/slides/321-LAYOUTS.md +203 -0
  139. oto/tools/google/slides/README.md +210 -0
  140. oto/tools/google/slides/USAGE-EDIT.md +626 -0
  141. oto/tools/google/slides/USAGE.md +158 -0
  142. oto/tools/google/slides/create-demo-presentation.py +348 -0
  143. oto/tools/google/slides/generate_slides.py +437 -0
  144. oto/tools/google/slides/lib/__init__.py +1 -0
  145. oto/tools/google/slides/lib/content_filler.py +579 -0
  146. oto/tools/google/slides/lib/layout_mappings.py +182 -0
  147. oto/tools/google/slides/lib/slides_client.py +1239 -0
  148. oto/tools/google/slides/requirements.txt +5 -0
  149. oto/tools/google/slides/test-copy-and-edit.py +187 -0
  150. oto/tools/google/slides/test-copy-slide.py +148 -0
  151. oto/tools/google/slides/test-edit.py +183 -0
  152. oto/tools/google/slides/tool.yaml +37 -0
  153. oto/tools/groq/__init__.py +5 -0
  154. oto/tools/groq/client.py +165 -0
  155. oto/tools/hithorizons/__init__.py +5 -0
  156. oto/tools/hithorizons/client.py +168 -0
  157. oto/tools/hunter/__init__.py +5 -0
  158. oto/tools/hunter/client.py +104 -0
  159. oto/tools/kaspr/__init__.py +5 -0
  160. oto/tools/kaspr/client.py +80 -0
  161. oto/tools/lemlist/__init__.py +5 -0
  162. oto/tools/lemlist/client.py +486 -0
  163. oto/tools/mistral/__init__.py +5 -0
  164. oto/tools/mistral/client.py +149 -0
  165. oto/tools/naf/__init__.py +5 -0
  166. oto/tools/naf/suggester.py +140 -0
  167. oto/tools/notion/README.md +310 -0
  168. oto/tools/notion/append_blocks.py +204 -0
  169. oto/tools/notion/append_blocks_chunked.py +296 -0
  170. oto/tools/notion/count_database_entries.py +47 -0
  171. oto/tools/notion/create_database_from_csv.py +269 -0
  172. oto/tools/notion/create_page.py +65 -0
  173. oto/tools/notion/get_database.py +67 -0
  174. oto/tools/notion/get_page.py +78 -0
  175. oto/tools/notion/known-pages.md +75 -0
  176. oto/tools/notion/lib/markdown_converter.py +287 -0
  177. oto/tools/notion/lib/notion_client.py +313 -0
  178. oto/tools/notion/list_teamspaces.py +244 -0
  179. oto/tools/notion/notion +122 -0
  180. oto/tools/notion/query_database.py +77 -0
  181. oto/tools/notion/requirements.txt +1 -0
  182. oto/tools/notion/search.py +93 -0
  183. oto/tools/notion/tool.yaml +235 -0
  184. oto/tools/notion/update_page.py +65 -0
  185. oto/tools/pennylane/__init__.py +5 -0
  186. oto/tools/pennylane/client.py +386 -0
  187. oto/tools/phantombuster/__init__.py +5 -0
  188. oto/tools/phantombuster/client.py +177 -0
  189. oto/tools/resend/__init__.py +5 -0
  190. oto/tools/resend/client.py +149 -0
  191. oto/tools/serpapi/__init__.py +5 -0
  192. oto/tools/serpapi/client.py +128 -0
  193. oto/tools/serper/__init__.py +5 -0
  194. oto/tools/serper/client.py +210 -0
  195. oto/tools/sirene/__init__.py +34 -0
  196. oto/tools/sirene/client.py +474 -0
  197. oto/tools/sirene/data/naf_codes.txt +732 -0
  198. oto/tools/sirene/entreprises.py +175 -0
  199. oto/tools/sirene/stock.py +377 -0
  200. oto/tools/slack/__init__.py +5 -0
  201. oto/tools/slack/client.py +202 -0
  202. oto/tools/unsplash/__init__.py +5 -0
  203. oto/tools/unsplash/client.py +197 -0
  204. oto/tools/whatsapp/__init__.py +3 -0
  205. oto/tools/whatsapp/client.py +68 -0
  206. oto/tools/whatsapp/node/package-lock.json +1564 -0
  207. oto/tools/whatsapp/node/package.json +11 -0
  208. oto/tools/whatsapp/node/whatsapp.mjs +357 -0
  209. oto/tools/wttj/__init__.py +5 -0
  210. oto/tools/wttj/client.py +251 -0
  211. oto/tools/zerobounce/__init__.py +5 -0
  212. oto/tools/zerobounce/client.py +93 -0
  213. oto_cli-1.0.0.dist-info/METADATA +171 -0
  214. oto_cli-1.0.0.dist-info/RECORD +217 -0
  215. oto_cli-1.0.0.dist-info/WHEEL +4 -0
  216. oto_cli-1.0.0.dist-info/entry_points.txt +2 -0
  217. oto_cli-1.0.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,437 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Generate Google Slides presentation from .slides.yaml file
4
+
5
+ Usage:
6
+ python3 generate_slides.py --input presentation.slides.yaml [--share]
7
+ """
8
+ import json
9
+ import sys
10
+ import time
11
+ import yaml
12
+ import typer
13
+ from typing_extensions import Annotated
14
+ from typing import Optional
15
+ from pathlib import Path
16
+
17
+ # Add parent directory to path for imports
18
+ sys.path.insert(0, str(Path(__file__).parent))
19
+ from lib.slides_client import SlidesClient
20
+
21
+ app = typer.Typer(help="Generate Google Slides from YAML")
22
+
23
+
24
+ def load_credentials_path():
25
+ """Load Google service account credentials path from local .keys directory."""
26
+ credentials_path = Path(__file__).parent / '.keys' / 'gdrive-key.json'
27
+
28
+ if not credentials_path.exists():
29
+ raise FileNotFoundError(f"Credentials file not found at {credentials_path}")
30
+
31
+ return str(credentials_path)
32
+
33
+
34
+ def load_default_folder():
35
+ """Load default folder ID from .folders file."""
36
+ folders_path = Path(__file__).parent / '.folders'
37
+
38
+ if not folders_path.exists():
39
+ return None
40
+
41
+ with open(folders_path, 'r') as f:
42
+ for line in f:
43
+ line = line.strip()
44
+ if line.startswith('DEFAULT_FOLDER='):
45
+ return line.split('=', 1)[1].strip()
46
+
47
+ return None
48
+
49
+
50
+ def load_default_template():
51
+ """Load default template ID from .folders file."""
52
+ folders_path = Path(__file__).parent / '.folders'
53
+
54
+ if not folders_path.exists():
55
+ return None
56
+
57
+ with open(folders_path, 'r') as f:
58
+ for line in f:
59
+ line = line.strip()
60
+ if line.startswith('DEFAULT_TEMPLATE='):
61
+ return line.split('=', 1)[1].strip()
62
+
63
+ return None
64
+
65
+
66
+ def load_slides_yaml(file_path):
67
+ """Load and parse slides YAML file"""
68
+ with open(file_path, 'r', encoding='utf-8') as f:
69
+ return yaml.safe_load(f)
70
+
71
+
72
+ def extract_images_from_markdown(content):
73
+ """
74
+ Extract image references from markdown content
75
+
76
+ Returns:
77
+ list: List of dicts with 'alt' (alt text) and 'url' (image URL/path)
78
+ """
79
+ if not content:
80
+ return []
81
+
82
+ import re
83
+ # Match markdown images: ![alt text](url)
84
+ pattern = r'!\[([^\]]*)\]\(([^\)]+)\)'
85
+ matches = re.findall(pattern, content)
86
+
87
+ return [{'alt': alt, 'url': url} for alt, url in matches]
88
+
89
+
90
+ def get_google_layout_for_yaml_layout(yaml_layout):
91
+ """
92
+ Map YAML layout to Google Slides layout
93
+
94
+ Uses the complete 321 template layout mappings
95
+ """
96
+ from lib.layout_mappings import get_api_name
97
+ return get_api_name(yaml_layout)
98
+
99
+
100
+
101
+
102
+ def generate_presentation(yaml_path, share=False, folder_id=None, template_id=None, output_format='url'):
103
+ """
104
+ Generate Google Slides presentation from YAML
105
+
106
+ Args:
107
+ yaml_path: Path to .slides.yaml file
108
+ share: Make presentation publicly accessible
109
+ folder_id: Optional Google Drive folder ID
110
+ template_id: Optional presentation ID to use as template (for theme)
111
+ output_format: 'url' or 'json'
112
+
113
+ Returns:
114
+ dict: Presentation info (id, url, title)
115
+ """
116
+ # Load YAML
117
+ data = load_slides_yaml(yaml_path)
118
+
119
+ # Initialize client with credentials from .env.keys
120
+ credentials_path = load_credentials_path()
121
+ client = SlidesClient(credentials_path)
122
+
123
+ # Use folder_id or default from .folders
124
+ if folder_id is None:
125
+ folder_id = load_default_folder()
126
+
127
+ # Get template_id from YAML, command line, or default (in order of priority)
128
+ if template_id is None:
129
+ template_id = data.get('template_id')
130
+ if template_id is None:
131
+ template_id = load_default_template()
132
+
133
+ # Create a parent folder for this deck (deck + images)
134
+ title = data.get('title', 'Untitled Presentation')
135
+ deck_folder_name = title
136
+ print(f"Creating deck folder: {deck_folder_name}")
137
+ deck_folder_id = client.create_folder(deck_folder_name, parent_folder_id=folder_id)
138
+
139
+ # Create presentation in the deck folder
140
+ presentation = client.create_presentation(title, folder_id=deck_folder_id, template_id=template_id)
141
+ presentation_id = presentation['presentationId']
142
+
143
+ # Remove all slides from template (if template was used)
144
+ if template_id:
145
+ pres = client.get_presentation(presentation_id)
146
+ existing_slides = pres.get('slides', [])
147
+
148
+ if existing_slides:
149
+ # Delete all slides in one batch request
150
+ delete_requests = [
151
+ {'deleteObject': {'objectId': slide['objectId']}}
152
+ for slide in existing_slides
153
+ ]
154
+
155
+ client.slides_service.presentations().batchUpdate(
156
+ presentationId=presentation_id,
157
+ body={'requests': delete_requests}
158
+ ).execute()
159
+ else:
160
+ # Remove default slide for new presentations
161
+ pres = client.get_presentation(presentation_id)
162
+ if pres.get('slides'):
163
+ first_slide_id = pres['slides'][0]['objectId']
164
+ client.slides_service.presentations().batchUpdate(
165
+ presentationId=presentation_id,
166
+ body={'requests': [{'deleteObject': {'objectId': first_slide_id}}]}
167
+ ).execute()
168
+
169
+ # Get base path for resolving relative image paths
170
+ yaml_base_path = Path(yaml_path).parent
171
+
172
+ # Create an "images" subfolder in the deck folder if we have any images
173
+ images_folder_id = None
174
+ has_images = any(
175
+ extract_images_from_markdown(slide.get('content', '')) or
176
+ extract_images_from_markdown('\n\n'.join([col.get('content', '') for col in slide.get('columns', [])]))
177
+ for slide in data.get('slides', [])
178
+ )
179
+
180
+ if has_images:
181
+ print(f"Creating images subfolder in {deck_folder_name}/")
182
+ images_folder_id = client.create_folder("images", parent_folder_id=deck_folder_id)
183
+
184
+ # Create content filler
185
+ from lib.content_filler import ContentFiller
186
+ filler = ContentFiller(client, presentation_id)
187
+
188
+ # Add slides
189
+ slides = data.get('slides', [])
190
+ for slide_data in slides:
191
+ # Map YAML layout to Google Slides layout
192
+ yaml_layout = slide_data.get('layout', 'default')
193
+ google_layout = get_google_layout_for_yaml_layout(yaml_layout)
194
+
195
+ # Create slide with native Google layout
196
+ slide_id = client.add_slide(presentation_id, layout=google_layout)
197
+
198
+ # Fill content using smart strategy-based filler
199
+ filler.fill_slide(slide_id, yaml_layout, slide_data)
200
+
201
+ # Handle single image field (for layouts with image placeholders)
202
+ single_image = slide_data.get('image')
203
+ if single_image:
204
+ # Get image placeholders in this slide
205
+ image_placeholders = client.get_image_placeholders_in_slide(presentation_id, slide_id)
206
+
207
+ if image_placeholders:
208
+ image_url = single_image
209
+
210
+ # Resolve local paths
211
+ if not image_url.startswith(('http://', 'https://')):
212
+ if image_url.startswith('/api/media/'):
213
+ import re
214
+ match = re.search(r'/api/media/runs/([^/]+)/(.+)', image_url)
215
+ if match:
216
+ run_id, file_path = match.groups()
217
+ project_root = yaml_base_path
218
+ while project_root.name != '' and not (project_root / 'agents').exists():
219
+ project_root = project_root.parent
220
+ local_path = project_root / 'agents' / run_id / file_path
221
+ else:
222
+ local_path = yaml_base_path / image_url.lstrip('/')
223
+ else:
224
+ local_path = yaml_base_path / image_url
225
+
226
+ # Upload to Drive
227
+ if local_path.exists():
228
+ print(f"Uploading image for placeholder: {local_path}")
229
+ upload_folder = images_folder_id if images_folder_id else folder_id
230
+ image_url = client.upload_image_to_drive(str(local_path), upload_folder)
231
+ else:
232
+ print(f"Warning: Image not found: {local_path}")
233
+ image_url = None
234
+
235
+ # Replace first image placeholder
236
+ if image_url:
237
+ try:
238
+ client.replace_image_placeholder(
239
+ presentation_id,
240
+ image_placeholders[0], # Use first placeholder
241
+ image_url,
242
+ replace_method='CENTER_INSIDE'
243
+ )
244
+ print(f"Replaced image placeholder with: {single_image}")
245
+ except Exception as e:
246
+ print(f"Error replacing image placeholder: {e}")
247
+ else:
248
+ print(f"Warning: 'image' field specified but layout has no image placeholder")
249
+
250
+ # Extract images for insertion
251
+ columns = slide_data.get('columns', [])
252
+
253
+ # Get slide dimensions (standard 16:9 - 10" x 5.625")
254
+ # 1 inch = 914400 EMU
255
+ slide_width = 10 * 914400
256
+ slide_height = 5.625 * 914400
257
+
258
+ # Handle column layouts: extract images per column
259
+ if columns and yaml_layout in ['2-columns', '3-columns']:
260
+ # Extract images per column
261
+ images_by_column = []
262
+ for col in columns:
263
+ col_images = extract_images_from_markdown(col.get('content', ''))
264
+ images_by_column.append(col_images)
265
+
266
+ # Position images at the top of each column
267
+ num_columns = len(columns)
268
+ column_width = slide_width / num_columns
269
+ margin = 0.5 * 914400 # 0.5 inch margin
270
+
271
+ # Smaller images for column layouts
272
+ img_width = column_width - (2 * margin) # Fit within column
273
+ img_height = img_width * 0.75 # Maintain 4:3 ratio
274
+
275
+ # Position: top of each column
276
+ y_top = 1.5 * 914400 # 1.5 inches from top (below title)
277
+
278
+ for col_idx, col_images in enumerate(images_by_column):
279
+ if not col_images:
280
+ continue
281
+
282
+ # X position for this column
283
+ x = (col_idx * column_width) + margin
284
+
285
+ # Insert first image of column (if multiple, only use first)
286
+ for img_data in col_images[:1]: # Only first image per column
287
+ image_url = img_data['url']
288
+
289
+ # Resolve local paths
290
+ if not image_url.startswith(('http://', 'https://')):
291
+ # Handle relative paths and /api/media paths
292
+ if image_url.startswith('/api/media/'):
293
+ # Convert /api/media/runs/xxx to local path
294
+ import re
295
+ match = re.search(r'/api/media/runs/([^/]+)/(.+)', image_url)
296
+ if match:
297
+ run_id, file_path = match.groups()
298
+ # Find the project root (where agents/ folder is)
299
+ project_root = yaml_base_path
300
+ while project_root.name != '' and not (project_root / 'agents').exists():
301
+ project_root = project_root.parent
302
+ local_path = project_root / 'agents' / run_id / file_path
303
+ else:
304
+ local_path = yaml_base_path / image_url.lstrip('/')
305
+ else:
306
+ local_path = yaml_base_path / image_url
307
+
308
+ # Upload to Drive and get public URL
309
+ if local_path.exists():
310
+ print(f"Uploading image: {local_path} (column {col_idx + 1})")
311
+ upload_folder = images_folder_id if images_folder_id else folder_id
312
+ image_url = client.upload_image_to_drive(str(local_path), upload_folder)
313
+ else:
314
+ print(f"Warning: Image not found: {local_path}")
315
+ continue
316
+
317
+ # Insert image at top of column
318
+ try:
319
+ client.insert_image(presentation_id, slide_id, image_url,
320
+ int(x), int(y_top), int(img_width), int(img_height))
321
+ print(f"Inserted image in column {col_idx + 1}: {img_data['alt'] or 'Image'}")
322
+ except Exception as e:
323
+ print(f"Error inserting image: {e}")
324
+
325
+ else:
326
+ # Standard layouts: extract all images and center them
327
+ all_content = slide_data.get('content', '')
328
+ images = extract_images_from_markdown(all_content)
329
+
330
+ if images:
331
+ # Default image dimensions for standard layouts
332
+ img_width = 2.5 * 914400 # 2.5 inches (smaller than before)
333
+ img_height = img_width * 0.75 # Maintain 4:3 ratio
334
+ spacing = 0.5 * 914400
335
+
336
+ total_images = len(images)
337
+
338
+ for idx, img_data in enumerate(images):
339
+ image_url = img_data['url']
340
+
341
+ # Resolve local paths
342
+ if not image_url.startswith(('http://', 'https://')):
343
+ if image_url.startswith('/api/media/'):
344
+ import re
345
+ match = re.search(r'/api/media/runs/([^/]+)/(.+)', image_url)
346
+ if match:
347
+ run_id, file_path = match.groups()
348
+ project_root = yaml_base_path
349
+ while project_root.name != '' and not (project_root / 'agents').exists():
350
+ project_root = project_root.parent
351
+ local_path = project_root / 'agents' / run_id / file_path
352
+ else:
353
+ local_path = yaml_base_path / image_url.lstrip('/')
354
+ else:
355
+ local_path = yaml_base_path / image_url
356
+
357
+ if local_path.exists():
358
+ print(f"Uploading image: {local_path}")
359
+ upload_folder = images_folder_id if images_folder_id else folder_id
360
+ image_url = client.upload_image_to_drive(str(local_path), upload_folder)
361
+ else:
362
+ print(f"Warning: Image not found: {local_path}")
363
+ continue
364
+
365
+ # Calculate position (distribute images horizontally)
366
+ if total_images == 1:
367
+ # Center single image
368
+ x = (slide_width - img_width) / 2
369
+ y = (slide_height - img_height) / 2 + (1 * 914400)
370
+ else:
371
+ # Arrange multiple images horizontally
372
+ total_width = (img_width * total_images) + (spacing * (total_images - 1))
373
+ start_x = (slide_width - total_width) / 2
374
+ x = start_x + (idx * (img_width + spacing))
375
+ y = (slide_height - img_height) / 2 + (1 * 914400)
376
+
377
+ # Insert image
378
+ try:
379
+ client.insert_image(presentation_id, slide_id, image_url,
380
+ int(x), int(y), int(img_width), int(img_height))
381
+ print(f"Inserted image: {img_data['alt'] or 'Image'}")
382
+ except Exception as e:
383
+ print(f"Error inserting image: {e}")
384
+
385
+ # Throttle API requests to avoid rate limiting (60 requests/minute limit)
386
+ # Sleep 2 seconds between slides to stay under limit
387
+ time.sleep(2)
388
+
389
+ # Share if requested
390
+ if share:
391
+ client.share_presentation(presentation_id)
392
+
393
+ # Get URL
394
+ url = client.get_presentation_url(presentation_id)
395
+
396
+ result = {
397
+ 'id': presentation_id,
398
+ 'title': title,
399
+ 'url': url,
400
+ 'slides_count': len(slides)
401
+ }
402
+
403
+ if output_format == 'json':
404
+ return json.dumps(result, indent=2)
405
+ else:
406
+ return url
407
+
408
+
409
+ @app.command()
410
+ def main(
411
+ input: Annotated[str, typer.Option(help="Path to .slides.yaml file")],
412
+ share: Annotated[bool, typer.Option(help="Make presentation public")] = False,
413
+ folder_id: Annotated[Optional[str], typer.Option(help="Google Drive folder ID")] = None,
414
+ template_id: Annotated[Optional[str], typer.Option(help="Presentation ID to use as template (for theme)")] = None,
415
+ output: Annotated[str, typer.Option(help="Output format")] = 'url',
416
+ ):
417
+ """Generate Google Slides from YAML."""
418
+ if output not in ['url', 'json']:
419
+ print(f"Error: Invalid output format '{output}'. Choose from: url, json", file=sys.stderr)
420
+ raise typer.Exit(1)
421
+
422
+ try:
423
+ result = generate_presentation(
424
+ input,
425
+ share=share,
426
+ folder_id=folder_id,
427
+ template_id=template_id,
428
+ output_format=output
429
+ )
430
+ print(result)
431
+ except Exception as e:
432
+ print(f"Error: {e}", file=sys.stderr)
433
+ raise typer.Exit(1)
434
+
435
+
436
+ if __name__ == '__main__':
437
+ app()
@@ -0,0 +1 @@
1
+ # Google Slides tool library