ai-parrot 0.17.2__cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.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 (535) hide show
  1. agentui/.prettierrc +15 -0
  2. agentui/QUICKSTART.md +272 -0
  3. agentui/README.md +59 -0
  4. agentui/env.example +16 -0
  5. agentui/jsconfig.json +14 -0
  6. agentui/package-lock.json +4242 -0
  7. agentui/package.json +34 -0
  8. agentui/scripts/postinstall/apply-patches.mjs +260 -0
  9. agentui/src/app.css +61 -0
  10. agentui/src/app.d.ts +13 -0
  11. agentui/src/app.html +12 -0
  12. agentui/src/components/LoadingSpinner.svelte +64 -0
  13. agentui/src/components/ThemeSwitcher.svelte +159 -0
  14. agentui/src/components/index.js +4 -0
  15. agentui/src/lib/api/bots.ts +60 -0
  16. agentui/src/lib/api/chat.ts +22 -0
  17. agentui/src/lib/api/http.ts +25 -0
  18. agentui/src/lib/components/BotCard.svelte +33 -0
  19. agentui/src/lib/components/ChatBubble.svelte +63 -0
  20. agentui/src/lib/components/Toast.svelte +21 -0
  21. agentui/src/lib/config.ts +20 -0
  22. agentui/src/lib/stores/auth.svelte.ts +73 -0
  23. agentui/src/lib/stores/theme.svelte.js +64 -0
  24. agentui/src/lib/stores/toast.svelte.ts +31 -0
  25. agentui/src/lib/utils/conversation.ts +39 -0
  26. agentui/src/routes/+layout.svelte +20 -0
  27. agentui/src/routes/+page.svelte +232 -0
  28. agentui/src/routes/login/+page.svelte +200 -0
  29. agentui/src/routes/talk/[agentId]/+page.svelte +297 -0
  30. agentui/src/routes/talk/[agentId]/+page.ts +7 -0
  31. agentui/static/README.md +1 -0
  32. agentui/svelte.config.js +11 -0
  33. agentui/tailwind.config.ts +53 -0
  34. agentui/tsconfig.json +3 -0
  35. agentui/vite.config.ts +10 -0
  36. ai_parrot-0.17.2.dist-info/METADATA +472 -0
  37. ai_parrot-0.17.2.dist-info/RECORD +535 -0
  38. ai_parrot-0.17.2.dist-info/WHEEL +6 -0
  39. ai_parrot-0.17.2.dist-info/entry_points.txt +2 -0
  40. ai_parrot-0.17.2.dist-info/licenses/LICENSE +21 -0
  41. ai_parrot-0.17.2.dist-info/top_level.txt +6 -0
  42. crew-builder/.prettierrc +15 -0
  43. crew-builder/QUICKSTART.md +259 -0
  44. crew-builder/README.md +113 -0
  45. crew-builder/env.example +17 -0
  46. crew-builder/jsconfig.json +14 -0
  47. crew-builder/package-lock.json +4182 -0
  48. crew-builder/package.json +37 -0
  49. crew-builder/scripts/postinstall/apply-patches.mjs +260 -0
  50. crew-builder/src/app.css +62 -0
  51. crew-builder/src/app.d.ts +13 -0
  52. crew-builder/src/app.html +12 -0
  53. crew-builder/src/components/LoadingSpinner.svelte +64 -0
  54. crew-builder/src/components/ThemeSwitcher.svelte +149 -0
  55. crew-builder/src/components/index.js +9 -0
  56. crew-builder/src/lib/api/bots.ts +60 -0
  57. crew-builder/src/lib/api/chat.ts +80 -0
  58. crew-builder/src/lib/api/client.ts +56 -0
  59. crew-builder/src/lib/api/crew/crew.ts +136 -0
  60. crew-builder/src/lib/api/index.ts +5 -0
  61. crew-builder/src/lib/api/o365/auth.ts +65 -0
  62. crew-builder/src/lib/auth/auth.ts +54 -0
  63. crew-builder/src/lib/components/AgentNode.svelte +43 -0
  64. crew-builder/src/lib/components/BotCard.svelte +33 -0
  65. crew-builder/src/lib/components/ChatBubble.svelte +67 -0
  66. crew-builder/src/lib/components/ConfigPanel.svelte +278 -0
  67. crew-builder/src/lib/components/JsonTreeNode.svelte +76 -0
  68. crew-builder/src/lib/components/JsonViewer.svelte +24 -0
  69. crew-builder/src/lib/components/MarkdownEditor.svelte +48 -0
  70. crew-builder/src/lib/components/ThemeToggle.svelte +36 -0
  71. crew-builder/src/lib/components/Toast.svelte +67 -0
  72. crew-builder/src/lib/components/Toolbar.svelte +157 -0
  73. crew-builder/src/lib/components/index.ts +10 -0
  74. crew-builder/src/lib/config.ts +8 -0
  75. crew-builder/src/lib/stores/auth.svelte.ts +228 -0
  76. crew-builder/src/lib/stores/crewStore.ts +369 -0
  77. crew-builder/src/lib/stores/theme.svelte.js +145 -0
  78. crew-builder/src/lib/stores/toast.svelte.ts +69 -0
  79. crew-builder/src/lib/utils/conversation.ts +39 -0
  80. crew-builder/src/lib/utils/markdown.ts +122 -0
  81. crew-builder/src/lib/utils/talkHistory.ts +47 -0
  82. crew-builder/src/routes/+layout.svelte +20 -0
  83. crew-builder/src/routes/+page.svelte +539 -0
  84. crew-builder/src/routes/agents/+page.svelte +247 -0
  85. crew-builder/src/routes/agents/[agentId]/+page.svelte +288 -0
  86. crew-builder/src/routes/agents/[agentId]/+page.ts +7 -0
  87. crew-builder/src/routes/builder/+page.svelte +204 -0
  88. crew-builder/src/routes/crew/ask/+page.svelte +1052 -0
  89. crew-builder/src/routes/crew/ask/+page.ts +1 -0
  90. crew-builder/src/routes/integrations/o365/+page.svelte +304 -0
  91. crew-builder/src/routes/login/+page.svelte +197 -0
  92. crew-builder/src/routes/talk/[agentId]/+page.svelte +487 -0
  93. crew-builder/src/routes/talk/[agentId]/+page.ts +7 -0
  94. crew-builder/static/README.md +1 -0
  95. crew-builder/svelte.config.js +11 -0
  96. crew-builder/tailwind.config.ts +53 -0
  97. crew-builder/tsconfig.json +3 -0
  98. crew-builder/vite.config.ts +10 -0
  99. mcp_servers/calculator_server.py +309 -0
  100. parrot/__init__.py +27 -0
  101. parrot/__pycache__/__init__.cpython-310.pyc +0 -0
  102. parrot/__pycache__/version.cpython-310.pyc +0 -0
  103. parrot/_version.py +34 -0
  104. parrot/a2a/__init__.py +48 -0
  105. parrot/a2a/client.py +658 -0
  106. parrot/a2a/discovery.py +89 -0
  107. parrot/a2a/mixin.py +257 -0
  108. parrot/a2a/models.py +376 -0
  109. parrot/a2a/server.py +770 -0
  110. parrot/agents/__init__.py +29 -0
  111. parrot/bots/__init__.py +12 -0
  112. parrot/bots/a2a_agent.py +19 -0
  113. parrot/bots/abstract.py +3139 -0
  114. parrot/bots/agent.py +1129 -0
  115. parrot/bots/basic.py +9 -0
  116. parrot/bots/chatbot.py +669 -0
  117. parrot/bots/data.py +1618 -0
  118. parrot/bots/database/__init__.py +5 -0
  119. parrot/bots/database/abstract.py +3071 -0
  120. parrot/bots/database/cache.py +286 -0
  121. parrot/bots/database/models.py +468 -0
  122. parrot/bots/database/prompts.py +154 -0
  123. parrot/bots/database/retries.py +98 -0
  124. parrot/bots/database/router.py +269 -0
  125. parrot/bots/database/sql.py +41 -0
  126. parrot/bots/db/__init__.py +6 -0
  127. parrot/bots/db/abstract.py +556 -0
  128. parrot/bots/db/bigquery.py +602 -0
  129. parrot/bots/db/cache.py +85 -0
  130. parrot/bots/db/documentdb.py +668 -0
  131. parrot/bots/db/elastic.py +1014 -0
  132. parrot/bots/db/influx.py +898 -0
  133. parrot/bots/db/mock.py +96 -0
  134. parrot/bots/db/multi.py +783 -0
  135. parrot/bots/db/prompts.py +185 -0
  136. parrot/bots/db/sql.py +1255 -0
  137. parrot/bots/db/tools.py +212 -0
  138. parrot/bots/document.py +680 -0
  139. parrot/bots/hrbot.py +15 -0
  140. parrot/bots/kb.py +170 -0
  141. parrot/bots/mcp.py +36 -0
  142. parrot/bots/orchestration/README.md +463 -0
  143. parrot/bots/orchestration/__init__.py +1 -0
  144. parrot/bots/orchestration/agent.py +155 -0
  145. parrot/bots/orchestration/crew.py +3330 -0
  146. parrot/bots/orchestration/fsm.py +1179 -0
  147. parrot/bots/orchestration/hr.py +434 -0
  148. parrot/bots/orchestration/storage/__init__.py +4 -0
  149. parrot/bots/orchestration/storage/memory.py +100 -0
  150. parrot/bots/orchestration/storage/mixin.py +119 -0
  151. parrot/bots/orchestration/verify.py +202 -0
  152. parrot/bots/product.py +204 -0
  153. parrot/bots/prompts/__init__.py +96 -0
  154. parrot/bots/prompts/agents.py +155 -0
  155. parrot/bots/prompts/data.py +216 -0
  156. parrot/bots/prompts/output_generation.py +8 -0
  157. parrot/bots/scraper/__init__.py +3 -0
  158. parrot/bots/scraper/models.py +122 -0
  159. parrot/bots/scraper/scraper.py +1173 -0
  160. parrot/bots/scraper/templates.py +115 -0
  161. parrot/bots/stores/__init__.py +5 -0
  162. parrot/bots/stores/local.py +172 -0
  163. parrot/bots/webdev.py +81 -0
  164. parrot/cli.py +17 -0
  165. parrot/clients/__init__.py +16 -0
  166. parrot/clients/base.py +1491 -0
  167. parrot/clients/claude.py +1191 -0
  168. parrot/clients/factory.py +129 -0
  169. parrot/clients/google.py +4567 -0
  170. parrot/clients/gpt.py +1975 -0
  171. parrot/clients/grok.py +432 -0
  172. parrot/clients/groq.py +986 -0
  173. parrot/clients/hf.py +582 -0
  174. parrot/clients/models.py +18 -0
  175. parrot/conf.py +395 -0
  176. parrot/embeddings/__init__.py +9 -0
  177. parrot/embeddings/base.py +157 -0
  178. parrot/embeddings/google.py +98 -0
  179. parrot/embeddings/huggingface.py +74 -0
  180. parrot/embeddings/openai.py +84 -0
  181. parrot/embeddings/processor.py +88 -0
  182. parrot/exceptions.c +13868 -0
  183. parrot/exceptions.cpython-310-x86_64-linux-gnu.so +0 -0
  184. parrot/exceptions.pxd +22 -0
  185. parrot/exceptions.pxi +15 -0
  186. parrot/exceptions.pyx +44 -0
  187. parrot/generators/__init__.py +29 -0
  188. parrot/generators/base.py +200 -0
  189. parrot/generators/html.py +293 -0
  190. parrot/generators/react.py +205 -0
  191. parrot/generators/streamlit.py +203 -0
  192. parrot/generators/template.py +105 -0
  193. parrot/handlers/__init__.py +4 -0
  194. parrot/handlers/agent.py +861 -0
  195. parrot/handlers/agents/__init__.py +1 -0
  196. parrot/handlers/agents/abstract.py +900 -0
  197. parrot/handlers/bots.py +338 -0
  198. parrot/handlers/chat.py +915 -0
  199. parrot/handlers/creation.sql +192 -0
  200. parrot/handlers/crew/ARCHITECTURE.md +362 -0
  201. parrot/handlers/crew/README_BOTMANAGER_PERSISTENCE.md +303 -0
  202. parrot/handlers/crew/README_REDIS_PERSISTENCE.md +366 -0
  203. parrot/handlers/crew/__init__.py +0 -0
  204. parrot/handlers/crew/handler.py +801 -0
  205. parrot/handlers/crew/models.py +229 -0
  206. parrot/handlers/crew/redis_persistence.py +523 -0
  207. parrot/handlers/jobs/__init__.py +10 -0
  208. parrot/handlers/jobs/job.py +384 -0
  209. parrot/handlers/jobs/mixin.py +627 -0
  210. parrot/handlers/jobs/models.py +115 -0
  211. parrot/handlers/jobs/worker.py +31 -0
  212. parrot/handlers/models.py +596 -0
  213. parrot/handlers/o365_auth.py +105 -0
  214. parrot/handlers/stream.py +337 -0
  215. parrot/interfaces/__init__.py +6 -0
  216. parrot/interfaces/aws.py +143 -0
  217. parrot/interfaces/credentials.py +113 -0
  218. parrot/interfaces/database.py +27 -0
  219. parrot/interfaces/google.py +1123 -0
  220. parrot/interfaces/hierarchy.py +1227 -0
  221. parrot/interfaces/http.py +651 -0
  222. parrot/interfaces/images/__init__.py +0 -0
  223. parrot/interfaces/images/plugins/__init__.py +24 -0
  224. parrot/interfaces/images/plugins/abstract.py +58 -0
  225. parrot/interfaces/images/plugins/analisys.py +148 -0
  226. parrot/interfaces/images/plugins/classify.py +150 -0
  227. parrot/interfaces/images/plugins/classifybase.py +182 -0
  228. parrot/interfaces/images/plugins/detect.py +150 -0
  229. parrot/interfaces/images/plugins/exif.py +1103 -0
  230. parrot/interfaces/images/plugins/hash.py +52 -0
  231. parrot/interfaces/images/plugins/vision.py +104 -0
  232. parrot/interfaces/images/plugins/yolo.py +66 -0
  233. parrot/interfaces/images/plugins/zerodetect.py +197 -0
  234. parrot/interfaces/o365.py +978 -0
  235. parrot/interfaces/onedrive.py +822 -0
  236. parrot/interfaces/sharepoint.py +1435 -0
  237. parrot/interfaces/soap.py +257 -0
  238. parrot/loaders/__init__.py +8 -0
  239. parrot/loaders/abstract.py +1131 -0
  240. parrot/loaders/audio.py +199 -0
  241. parrot/loaders/basepdf.py +53 -0
  242. parrot/loaders/basevideo.py +1568 -0
  243. parrot/loaders/csv.py +409 -0
  244. parrot/loaders/docx.py +116 -0
  245. parrot/loaders/epubloader.py +316 -0
  246. parrot/loaders/excel.py +199 -0
  247. parrot/loaders/factory.py +55 -0
  248. parrot/loaders/files/__init__.py +0 -0
  249. parrot/loaders/files/abstract.py +39 -0
  250. parrot/loaders/files/html.py +26 -0
  251. parrot/loaders/files/text.py +63 -0
  252. parrot/loaders/html.py +152 -0
  253. parrot/loaders/markdown.py +442 -0
  254. parrot/loaders/pdf.py +373 -0
  255. parrot/loaders/pdfmark.py +320 -0
  256. parrot/loaders/pdftables.py +506 -0
  257. parrot/loaders/ppt.py +476 -0
  258. parrot/loaders/qa.py +63 -0
  259. parrot/loaders/splitters/__init__.py +10 -0
  260. parrot/loaders/splitters/base.py +138 -0
  261. parrot/loaders/splitters/md.py +228 -0
  262. parrot/loaders/splitters/token.py +143 -0
  263. parrot/loaders/txt.py +26 -0
  264. parrot/loaders/video.py +89 -0
  265. parrot/loaders/videolocal.py +218 -0
  266. parrot/loaders/videounderstanding.py +377 -0
  267. parrot/loaders/vimeo.py +167 -0
  268. parrot/loaders/web.py +599 -0
  269. parrot/loaders/youtube.py +504 -0
  270. parrot/manager/__init__.py +5 -0
  271. parrot/manager/manager.py +1030 -0
  272. parrot/mcp/__init__.py +28 -0
  273. parrot/mcp/adapter.py +105 -0
  274. parrot/mcp/cli.py +174 -0
  275. parrot/mcp/client.py +119 -0
  276. parrot/mcp/config.py +75 -0
  277. parrot/mcp/integration.py +842 -0
  278. parrot/mcp/oauth.py +933 -0
  279. parrot/mcp/server.py +225 -0
  280. parrot/mcp/transports/__init__.py +3 -0
  281. parrot/mcp/transports/base.py +279 -0
  282. parrot/mcp/transports/grpc_session.py +163 -0
  283. parrot/mcp/transports/http.py +312 -0
  284. parrot/mcp/transports/mcp.proto +108 -0
  285. parrot/mcp/transports/quic.py +1082 -0
  286. parrot/mcp/transports/sse.py +330 -0
  287. parrot/mcp/transports/stdio.py +309 -0
  288. parrot/mcp/transports/unix.py +395 -0
  289. parrot/mcp/transports/websocket.py +547 -0
  290. parrot/memory/__init__.py +16 -0
  291. parrot/memory/abstract.py +209 -0
  292. parrot/memory/agent.py +32 -0
  293. parrot/memory/cache.py +175 -0
  294. parrot/memory/core.py +555 -0
  295. parrot/memory/file.py +153 -0
  296. parrot/memory/mem.py +131 -0
  297. parrot/memory/redis.py +613 -0
  298. parrot/models/__init__.py +46 -0
  299. parrot/models/basic.py +118 -0
  300. parrot/models/compliance.py +208 -0
  301. parrot/models/crew.py +395 -0
  302. parrot/models/detections.py +654 -0
  303. parrot/models/generation.py +85 -0
  304. parrot/models/google.py +223 -0
  305. parrot/models/groq.py +23 -0
  306. parrot/models/openai.py +30 -0
  307. parrot/models/outputs.py +285 -0
  308. parrot/models/responses.py +938 -0
  309. parrot/notifications/__init__.py +743 -0
  310. parrot/openapi/__init__.py +3 -0
  311. parrot/openapi/components.yaml +641 -0
  312. parrot/openapi/config.py +322 -0
  313. parrot/outputs/__init__.py +32 -0
  314. parrot/outputs/formats/__init__.py +108 -0
  315. parrot/outputs/formats/altair.py +359 -0
  316. parrot/outputs/formats/application.py +122 -0
  317. parrot/outputs/formats/base.py +351 -0
  318. parrot/outputs/formats/bokeh.py +356 -0
  319. parrot/outputs/formats/card.py +424 -0
  320. parrot/outputs/formats/chart.py +436 -0
  321. parrot/outputs/formats/d3.py +255 -0
  322. parrot/outputs/formats/echarts.py +310 -0
  323. parrot/outputs/formats/generators/__init__.py +0 -0
  324. parrot/outputs/formats/generators/abstract.py +61 -0
  325. parrot/outputs/formats/generators/panel.py +145 -0
  326. parrot/outputs/formats/generators/streamlit.py +86 -0
  327. parrot/outputs/formats/generators/terminal.py +63 -0
  328. parrot/outputs/formats/holoviews.py +310 -0
  329. parrot/outputs/formats/html.py +147 -0
  330. parrot/outputs/formats/jinja2.py +46 -0
  331. parrot/outputs/formats/json.py +87 -0
  332. parrot/outputs/formats/map.py +933 -0
  333. parrot/outputs/formats/markdown.py +172 -0
  334. parrot/outputs/formats/matplotlib.py +237 -0
  335. parrot/outputs/formats/mixins/__init__.py +0 -0
  336. parrot/outputs/formats/mixins/emaps.py +855 -0
  337. parrot/outputs/formats/plotly.py +341 -0
  338. parrot/outputs/formats/seaborn.py +310 -0
  339. parrot/outputs/formats/table.py +397 -0
  340. parrot/outputs/formats/template_report.py +138 -0
  341. parrot/outputs/formats/yaml.py +125 -0
  342. parrot/outputs/formatter.py +152 -0
  343. parrot/outputs/templates/__init__.py +95 -0
  344. parrot/pipelines/__init__.py +0 -0
  345. parrot/pipelines/abstract.py +210 -0
  346. parrot/pipelines/detector.py +124 -0
  347. parrot/pipelines/models.py +90 -0
  348. parrot/pipelines/planogram.py +3002 -0
  349. parrot/pipelines/table.sql +97 -0
  350. parrot/plugins/__init__.py +106 -0
  351. parrot/plugins/importer.py +80 -0
  352. parrot/py.typed +0 -0
  353. parrot/registry/__init__.py +18 -0
  354. parrot/registry/registry.py +594 -0
  355. parrot/scheduler/__init__.py +1189 -0
  356. parrot/scheduler/models.py +60 -0
  357. parrot/security/__init__.py +16 -0
  358. parrot/security/prompt_injection.py +268 -0
  359. parrot/security/security_events.sql +25 -0
  360. parrot/services/__init__.py +1 -0
  361. parrot/services/mcp/__init__.py +8 -0
  362. parrot/services/mcp/config.py +13 -0
  363. parrot/services/mcp/server.py +295 -0
  364. parrot/services/o365_remote_auth.py +235 -0
  365. parrot/stores/__init__.py +7 -0
  366. parrot/stores/abstract.py +352 -0
  367. parrot/stores/arango.py +1090 -0
  368. parrot/stores/bigquery.py +1377 -0
  369. parrot/stores/cache.py +106 -0
  370. parrot/stores/empty.py +10 -0
  371. parrot/stores/faiss_store.py +1157 -0
  372. parrot/stores/kb/__init__.py +9 -0
  373. parrot/stores/kb/abstract.py +68 -0
  374. parrot/stores/kb/cache.py +165 -0
  375. parrot/stores/kb/doc.py +325 -0
  376. parrot/stores/kb/hierarchy.py +346 -0
  377. parrot/stores/kb/local.py +457 -0
  378. parrot/stores/kb/prompt.py +28 -0
  379. parrot/stores/kb/redis.py +659 -0
  380. parrot/stores/kb/store.py +115 -0
  381. parrot/stores/kb/user.py +374 -0
  382. parrot/stores/models.py +59 -0
  383. parrot/stores/pgvector.py +3 -0
  384. parrot/stores/postgres.py +2853 -0
  385. parrot/stores/utils/__init__.py +0 -0
  386. parrot/stores/utils/chunking.py +197 -0
  387. parrot/telemetry/__init__.py +3 -0
  388. parrot/telemetry/mixin.py +111 -0
  389. parrot/template/__init__.py +3 -0
  390. parrot/template/engine.py +259 -0
  391. parrot/tools/__init__.py +23 -0
  392. parrot/tools/abstract.py +644 -0
  393. parrot/tools/agent.py +363 -0
  394. parrot/tools/arangodbsearch.py +537 -0
  395. parrot/tools/arxiv_tool.py +188 -0
  396. parrot/tools/calculator/__init__.py +3 -0
  397. parrot/tools/calculator/operations/__init__.py +38 -0
  398. parrot/tools/calculator/operations/calculus.py +80 -0
  399. parrot/tools/calculator/operations/statistics.py +76 -0
  400. parrot/tools/calculator/tool.py +150 -0
  401. parrot/tools/cloudwatch.py +988 -0
  402. parrot/tools/codeinterpreter/__init__.py +127 -0
  403. parrot/tools/codeinterpreter/executor.py +371 -0
  404. parrot/tools/codeinterpreter/internals.py +473 -0
  405. parrot/tools/codeinterpreter/models.py +643 -0
  406. parrot/tools/codeinterpreter/prompts.py +224 -0
  407. parrot/tools/codeinterpreter/tool.py +664 -0
  408. parrot/tools/company_info/__init__.py +6 -0
  409. parrot/tools/company_info/tool.py +1138 -0
  410. parrot/tools/correlationanalysis.py +437 -0
  411. parrot/tools/database/abstract.py +286 -0
  412. parrot/tools/database/bq.py +115 -0
  413. parrot/tools/database/cache.py +284 -0
  414. parrot/tools/database/models.py +95 -0
  415. parrot/tools/database/pg.py +343 -0
  416. parrot/tools/databasequery.py +1159 -0
  417. parrot/tools/db.py +1800 -0
  418. parrot/tools/ddgo.py +370 -0
  419. parrot/tools/decorators.py +271 -0
  420. parrot/tools/dftohtml.py +282 -0
  421. parrot/tools/document.py +549 -0
  422. parrot/tools/ecs.py +819 -0
  423. parrot/tools/edareport.py +368 -0
  424. parrot/tools/elasticsearch.py +1049 -0
  425. parrot/tools/employees.py +462 -0
  426. parrot/tools/epson/__init__.py +96 -0
  427. parrot/tools/excel.py +683 -0
  428. parrot/tools/file/__init__.py +13 -0
  429. parrot/tools/file/abstract.py +76 -0
  430. parrot/tools/file/gcs.py +378 -0
  431. parrot/tools/file/local.py +284 -0
  432. parrot/tools/file/s3.py +511 -0
  433. parrot/tools/file/tmp.py +309 -0
  434. parrot/tools/file/tool.py +501 -0
  435. parrot/tools/file_reader.py +129 -0
  436. parrot/tools/flowtask/__init__.py +19 -0
  437. parrot/tools/flowtask/tool.py +761 -0
  438. parrot/tools/gittoolkit.py +508 -0
  439. parrot/tools/google/__init__.py +18 -0
  440. parrot/tools/google/base.py +169 -0
  441. parrot/tools/google/tools.py +1251 -0
  442. parrot/tools/googlelocation.py +5 -0
  443. parrot/tools/googleroutes.py +5 -0
  444. parrot/tools/googlesearch.py +5 -0
  445. parrot/tools/googlesitesearch.py +5 -0
  446. parrot/tools/googlevoice.py +2 -0
  447. parrot/tools/gvoice.py +695 -0
  448. parrot/tools/ibisworld/README.md +225 -0
  449. parrot/tools/ibisworld/__init__.py +11 -0
  450. parrot/tools/ibisworld/tool.py +366 -0
  451. parrot/tools/jiratoolkit.py +1718 -0
  452. parrot/tools/manager.py +1098 -0
  453. parrot/tools/math.py +152 -0
  454. parrot/tools/metadata.py +476 -0
  455. parrot/tools/msteams.py +1621 -0
  456. parrot/tools/msword.py +635 -0
  457. parrot/tools/multidb.py +580 -0
  458. parrot/tools/multistoresearch.py +369 -0
  459. parrot/tools/networkninja.py +167 -0
  460. parrot/tools/nextstop/__init__.py +4 -0
  461. parrot/tools/nextstop/base.py +286 -0
  462. parrot/tools/nextstop/employee.py +733 -0
  463. parrot/tools/nextstop/store.py +462 -0
  464. parrot/tools/notification.py +435 -0
  465. parrot/tools/o365/__init__.py +42 -0
  466. parrot/tools/o365/base.py +295 -0
  467. parrot/tools/o365/bundle.py +522 -0
  468. parrot/tools/o365/events.py +554 -0
  469. parrot/tools/o365/mail.py +992 -0
  470. parrot/tools/o365/onedrive.py +497 -0
  471. parrot/tools/o365/sharepoint.py +641 -0
  472. parrot/tools/openapi_toolkit.py +904 -0
  473. parrot/tools/openweather.py +527 -0
  474. parrot/tools/pdfprint.py +1001 -0
  475. parrot/tools/powerbi.py +518 -0
  476. parrot/tools/powerpoint.py +1113 -0
  477. parrot/tools/pricestool.py +146 -0
  478. parrot/tools/products/__init__.py +246 -0
  479. parrot/tools/prophet_tool.py +171 -0
  480. parrot/tools/pythonpandas.py +630 -0
  481. parrot/tools/pythonrepl.py +910 -0
  482. parrot/tools/qsource.py +436 -0
  483. parrot/tools/querytoolkit.py +395 -0
  484. parrot/tools/quickeda.py +827 -0
  485. parrot/tools/resttool.py +553 -0
  486. parrot/tools/retail/__init__.py +0 -0
  487. parrot/tools/retail/bby.py +528 -0
  488. parrot/tools/sandboxtool.py +703 -0
  489. parrot/tools/sassie/__init__.py +352 -0
  490. parrot/tools/scraping/__init__.py +7 -0
  491. parrot/tools/scraping/docs/select.md +466 -0
  492. parrot/tools/scraping/documentation.md +1278 -0
  493. parrot/tools/scraping/driver.py +436 -0
  494. parrot/tools/scraping/models.py +576 -0
  495. parrot/tools/scraping/options.py +85 -0
  496. parrot/tools/scraping/orchestrator.py +517 -0
  497. parrot/tools/scraping/readme.md +740 -0
  498. parrot/tools/scraping/tool.py +3115 -0
  499. parrot/tools/seasonaldetection.py +642 -0
  500. parrot/tools/shell_tool/__init__.py +5 -0
  501. parrot/tools/shell_tool/actions.py +408 -0
  502. parrot/tools/shell_tool/engine.py +155 -0
  503. parrot/tools/shell_tool/models.py +322 -0
  504. parrot/tools/shell_tool/tool.py +442 -0
  505. parrot/tools/site_search.py +214 -0
  506. parrot/tools/textfile.py +418 -0
  507. parrot/tools/think.py +378 -0
  508. parrot/tools/toolkit.py +298 -0
  509. parrot/tools/webapp_tool.py +187 -0
  510. parrot/tools/whatif.py +1279 -0
  511. parrot/tools/workday/MULTI_WSDL_EXAMPLE.md +249 -0
  512. parrot/tools/workday/__init__.py +6 -0
  513. parrot/tools/workday/models.py +1389 -0
  514. parrot/tools/workday/tool.py +1293 -0
  515. parrot/tools/yfinance_tool.py +306 -0
  516. parrot/tools/zipcode.py +217 -0
  517. parrot/utils/__init__.py +2 -0
  518. parrot/utils/helpers.py +73 -0
  519. parrot/utils/parsers/__init__.py +5 -0
  520. parrot/utils/parsers/toml.c +12078 -0
  521. parrot/utils/parsers/toml.cpython-310-x86_64-linux-gnu.so +0 -0
  522. parrot/utils/parsers/toml.pyx +21 -0
  523. parrot/utils/toml.py +11 -0
  524. parrot/utils/types.cpp +20936 -0
  525. parrot/utils/types.cpython-310-x86_64-linux-gnu.so +0 -0
  526. parrot/utils/types.pyx +213 -0
  527. parrot/utils/uv.py +11 -0
  528. parrot/version.py +10 -0
  529. parrot/yaml-rs/Cargo.lock +350 -0
  530. parrot/yaml-rs/Cargo.toml +19 -0
  531. parrot/yaml-rs/pyproject.toml +19 -0
  532. parrot/yaml-rs/python/yaml_rs/__init__.py +81 -0
  533. parrot/yaml-rs/src/lib.rs +222 -0
  534. requirements/docker-compose.yml +24 -0
  535. requirements/requirements-dev.txt +21 -0
@@ -0,0 +1,1123 @@
1
+ """
2
+ Google Services Client for AI-Parrot.
3
+
4
+ Simplified async-only implementation using aiogoogle.
5
+ Provides unified interface for Google services with credential management
6
+ and environment variable replacement.
7
+ """
8
+ from __future__ import annotations
9
+ import contextlib
10
+ from pathlib import Path, PurePath
11
+ from typing import Union, List, Dict, Any, Optional, Callable
12
+ from abc import ABC
13
+ import asyncio
14
+ import os
15
+ import re
16
+ import json
17
+ import logging
18
+ from contextlib import suppress
19
+ from urllib.parse import urlparse
20
+ import webbrowser
21
+ from aiohttp import web
22
+ from redis import asyncio as aioredis
23
+ from selenium.webdriver.chrome.service import Service as ChromeService
24
+ from selenium.webdriver.chrome.options import Options as ChromeOptions
25
+ from webdriver_manager.core.driver_cache import DriverCacheManager
26
+ from webdriver_manager.chrome import ChromeDriverManager
27
+ from playwright.async_api import async_playwright
28
+ from aiogoogle import Aiogoogle
29
+ from aiogoogle.auth.creds import ServiceAccountCreds, UserCreds
30
+ from aiogoogle.auth.utils import create_secret
31
+ from navconfig import BASE_DIR, config
32
+ from ..exceptions import ConfigError # pylint: disable=E0611 # noqa
33
+ from ..conf import GOOGLE_CREDENTIALS_FILE, REDIS_HISTORY_URL
34
+
35
+
36
+ # ============================================================================
37
+ # Default Scopes for Google Services
38
+ # ============================================================================
39
+
40
+ DEFAULT_SCOPES = {
41
+ # Google Drive
42
+ 'drive': [
43
+ 'https://www.googleapis.com/auth/drive',
44
+ 'https://www.googleapis.com/auth/drive.file',
45
+ 'https://www.googleapis.com/auth/drive.metadata.readonly'
46
+ ],
47
+ # Google Sheets
48
+ 'sheets': [
49
+ 'https://www.googleapis.com/auth/spreadsheets',
50
+ 'https://www.googleapis.com/auth/spreadsheets.readonly'
51
+ ],
52
+ # Google Docs
53
+ 'docs': [
54
+ 'https://www.googleapis.com/auth/documents',
55
+ 'https://www.googleapis.com/auth/documents.readonly'
56
+ ],
57
+ # Google Calendar
58
+ 'calendar': [
59
+ 'https://www.googleapis.com/auth/calendar',
60
+ 'https://www.googleapis.com/auth/calendar.readonly',
61
+ 'https://www.googleapis.com/auth/calendar.events'
62
+ ],
63
+ # Google Cloud Storage
64
+ 'storage': [
65
+ 'https://www.googleapis.com/auth/devstorage.full_control',
66
+ 'https://www.googleapis.com/auth/devstorage.read_only',
67
+ 'https://www.googleapis.com/auth/devstorage.read_write'
68
+ ],
69
+ # Gmail
70
+ 'gmail': [
71
+ 'https://www.googleapis.com/auth/gmail.readonly',
72
+ 'https://www.googleapis.com/auth/gmail.modify',
73
+ 'https://www.googleapis.com/auth/gmail.compose'
74
+ ],
75
+ # Google Search
76
+ 'search': [
77
+ 'https://www.googleapis.com/auth/cse'
78
+ ]
79
+ }
80
+
81
+ # Combined scopes for full access
82
+ DEFAULT_SCOPES['all'] = list(set(
83
+ DEFAULT_SCOPES['drive'] +
84
+ DEFAULT_SCOPES['sheets'] +
85
+ DEFAULT_SCOPES['docs'] +
86
+ DEFAULT_SCOPES['calendar'] +
87
+ DEFAULT_SCOPES['storage']
88
+ ))
89
+
90
+
91
+ # ============================================================================
92
+ # Credentials Interface Mixin
93
+ # ============================================================================
94
+
95
+ class CredentialsInterface:
96
+ """
97
+ Mixin for processing credentials with environment variable replacement.
98
+
99
+ Handles ${VAR_NAME} patterns in credential dictionaries.
100
+ """
101
+
102
+ ENV_VAR_PATTERN = re.compile(r'\$\{([^}]+)\}')
103
+
104
+ def processing_credentials(self) -> None:
105
+ """
106
+ Process credentials dictionary and replace environment variables.
107
+
108
+ Replaces ${VAR_NAME} patterns with values from environment variables.
109
+ Works with both navconfig and os.environ.
110
+ """
111
+ if not hasattr(self, 'credentials_dict') or not self.credentials_dict: # pylint: disable=E0203 # noqa
112
+ return
113
+
114
+ self.credentials_dict = self._replace_env_vars(self.credentials_dict)
115
+
116
+ def _replace_env_vars(self, obj: Any) -> Any:
117
+ """
118
+ Recursively replace environment variables in strings.
119
+
120
+ Args:
121
+ obj: Object to process (dict, list, str, or other)
122
+
123
+ Returns:
124
+ Processed object with environment variables replaced
125
+ """
126
+ if isinstance(obj, dict):
127
+ return {k: self._replace_env_vars(v) for k, v in obj.items()}
128
+ elif isinstance(obj, list):
129
+ return [self._replace_env_vars(item) for item in obj]
130
+ elif isinstance(obj, str):
131
+ return self._replace_env_var_string(obj)
132
+ return obj
133
+
134
+ def _replace_env_var_string(self, value: str) -> str:
135
+ """
136
+ Replace environment variables in a string.
137
+
138
+ Args:
139
+ value: String potentially containing ${VAR} patterns
140
+
141
+ Returns:
142
+ String with variables replaced
143
+ """
144
+ def replacer(match):
145
+ var_name = match.group(1)
146
+ # Try navconfig first, then os.environ
147
+ if hasattr(config, 'get'):
148
+ env_value = config.get(var_name)
149
+ if env_value is not None:
150
+ return env_value
151
+ return os.environ.get(var_name, match.group(0))
152
+
153
+ return self.ENV_VAR_PATTERN.sub(replacer, value)
154
+
155
+
156
+ # ============================================================================
157
+ # Google Client
158
+ # ============================================================================
159
+
160
+ class GoogleClient(CredentialsInterface, ABC):
161
+ """
162
+ Google Services Client for AI-Parrot.
163
+
164
+ Async-only implementation using aiogoogle for:
165
+ - Google Drive (file management)
166
+ - Google Sheets (spreadsheets)
167
+ - Google Docs (documents)
168
+ - Google Calendar (events)
169
+ - Google Cloud Storage (buckets)
170
+ - Gmail (email)
171
+ - Google Custom Search
172
+
173
+ Features:
174
+ - Service account and user credentials support
175
+ - Environment variable replacement in credentials
176
+ - Full async/await support via aiogoogle
177
+ - OAuth2 interactive login support (framework ready)
178
+ - Credential caching
179
+
180
+ Authentication Methods:
181
+ 1. Service Account (recommended for server apps):
182
+ - Use JSON key file
183
+ - Use JSON string
184
+ - Use dictionary
185
+
186
+ 2. User Credentials (OAuth2):
187
+ - Interactive browser login (TODO: implement)
188
+ - Cached credentials
189
+
190
+ Example:
191
+ # Service account from file
192
+ client = GoogleClient(credentials="path/to/key.json")
193
+ await client.initialize()
194
+
195
+ # Service account from dict with env vars
196
+ client = GoogleClient(credentials={
197
+ "type": "service_account",
198
+ "project_id": "${GCP_PROJECT_ID}",
199
+ "private_key": "${GCP_PRIVATE_KEY}",
200
+ ...
201
+ })
202
+ await client.initialize()
203
+
204
+ # Context manager (recommended)
205
+ async with GoogleClient(credentials="key.json", scopes="drive") as client:
206
+ result = await client.execute_api_call(...)
207
+ """
208
+
209
+ def __init__(
210
+ self,
211
+ credentials: Optional[Union[str, dict, Path]] = None,
212
+ scopes: Optional[Union[List[str], str]] = None,
213
+ user_creds_cache_file: Optional[Union[str, Path]] = None,
214
+ **kwargs
215
+ ):
216
+ """
217
+ Initialize Google Client.
218
+
219
+ Args:
220
+ credentials: Credentials (file path, dict, "user" for OAuth)
221
+ scopes: Service scopes (e.g., ["drive", "sheets"] or "all")
222
+ **kwargs: Additional arguments
223
+ """
224
+ self.logger = logging.getLogger(
225
+ f'Parrot.Interfaces.{self.__class__.__name__}'
226
+ )
227
+
228
+ # Credential storage
229
+ self.credentials_file: Optional[PurePath] = None
230
+ self.credentials_str: Optional[str] = None
231
+ self.credentials_dict: Optional[dict] = None
232
+ self.auth_type: str = 'service_account' # or 'user'
233
+ self._oauth_client_config: Optional[Dict[str, Any]] = None
234
+ self._client_credentials_source: Optional[str] = None
235
+ self.redis_url: str = kwargs.get(
236
+ "redis_url", REDIS_HISTORY_URL or "redis://localhost:6379/0"
237
+ )
238
+ self.redis: Optional[aioredis.Redis] = None
239
+ try:
240
+ self.redis = aioredis.from_url(self.redis_url, encoding="utf-8", decode_responses=True)
241
+ except Exception as e:
242
+ self.logger.warning(
243
+ "Google: Redis unavailable (%s); falling back to file cache only.", e
244
+ )
245
+ self.user_creds_ttl: int = int(kwargs.get("user_creds_ttl", 75 * 24 * 3600)) # 75 days
246
+
247
+
248
+ # Process scopes
249
+ self.scopes: List[str] = self._process_scopes(scopes or 'all')
250
+
251
+ # Credentials
252
+ self._service_account_creds: Optional[ServiceAccountCreds] = None
253
+ self._user_creds: Optional[UserCreds] = None
254
+ self._user_creds_payload: Optional[Dict[str, Any]] = None
255
+
256
+ # Authentication state
257
+ self._authenticated = False
258
+
259
+ # User credential cache
260
+ if isinstance(user_creds_cache_file, (str, Path)):
261
+ self.user_creds_cache_file: Optional[Path] = Path(user_creds_cache_file).expanduser().resolve()
262
+ else:
263
+ # Default cache location inside env directory
264
+ self.user_creds_cache_file = BASE_DIR.joinpath('env', 'google', 'user_creds.json')
265
+
266
+ # Process credentials
267
+ self._load_credentials(credentials)
268
+ super().__init__()
269
+
270
+ async def __aenter__(self) -> GoogleClient:
271
+ await self.ensure_interactive_session()
272
+ await self.initialize()
273
+ return self
274
+
275
+ async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
276
+ await self.close()
277
+
278
+ def _process_scopes(self, scopes: Union[List[str], str]) -> List[str]:
279
+ """
280
+ Process scope specification into full scope URLs.
281
+
282
+ Args:
283
+ scopes: Scope names or URLs
284
+
285
+ Returns:
286
+ List of full scope URLs
287
+ """
288
+ if isinstance(scopes, str):
289
+ # Single scope name or "all"
290
+ if scopes in DEFAULT_SCOPES:
291
+ return DEFAULT_SCOPES[scopes].copy()
292
+ scopes = [scopes]
293
+
294
+ # Expand scope names to URLs
295
+ result = []
296
+ for scope in scopes:
297
+ if scope.startswith('https://'):
298
+ result.append(scope)
299
+ elif scope in DEFAULT_SCOPES:
300
+ result.extend(DEFAULT_SCOPES[scope])
301
+ else:
302
+ self.logger.warning(f"Unknown scope: {scope}")
303
+
304
+ return list(set(result)) # Remove duplicates
305
+
306
+ def _redis_cache_key(self, client_id: Optional[str], user_hint: Optional[str], scopes: list[str]) -> str:
307
+ # Use OAuth client_id + (optional) user email hint + stable scopes hash
308
+ sid = (client_id or "unknown").strip()
309
+ u = (user_hint or "").strip()
310
+ scopes_key = "|".join(sorted(scopes))
311
+ return f"google:oauth:{sid}:{u}:{hash(scopes_key)}"
312
+
313
+ async def _save_user_creds_to_redis(self, creds: dict, client_id: Optional[str], user_hint: Optional[str], scopes: list[str]) -> None:
314
+ if not self.redis:
315
+ return
316
+ key = self._redis_cache_key(client_id, user_hint, scopes)
317
+ try:
318
+ await self.redis.set(key, json.dumps(creds, default=str), ex=self.user_creds_ttl)
319
+ self.logger.info("Google: saved user credentials to Redis cache")
320
+ self._user_creds_payload = creds.copy()
321
+ except Exception as e:
322
+ self.logger.warning("Google: failed to save creds to Redis: %s", e)
323
+
324
+ async def _load_user_creds_from_redis(self, client_id: Optional[str], user_hint: Optional[str], scopes: list[str]) -> bool:
325
+ if not self.redis:
326
+ return False
327
+ key = self._redis_cache_key(client_id, user_hint, scopes)
328
+ try:
329
+ blob = await self.redis.get(key)
330
+ if not blob:
331
+ return False
332
+ data = json.loads(blob)
333
+ scopes2 = data.get("scopes", scopes)
334
+ if isinstance(scopes2, str):
335
+ scopes2 = [scopes2]
336
+ d = data.copy()
337
+ d.pop("scopes", None)
338
+ self._user_creds = UserCreds(scopes=scopes2, **d)
339
+ self._user_creds_payload = data
340
+ self._service_account_creds = None
341
+ self._authenticated = True
342
+ self._client_credentials_source = f"redis:{key}"
343
+ self.logger.info("Google: loaded user credentials from Redis cache")
344
+ return True
345
+ except Exception as e:
346
+ self.logger.warning("Google: failed to load creds from Redis: %s", e)
347
+ return False
348
+
349
+ def _load_credentials(
350
+ self,
351
+ credentials: Optional[Union[str, dict, Path]]
352
+ ) -> None:
353
+ """
354
+ Load and validate credentials.
355
+
356
+ Args:
357
+ credentials: Credentials specification
358
+ """
359
+ if credentials is None:
360
+ if not GOOGLE_CREDENTIALS_FILE.exists():
361
+ raise RuntimeError(
362
+ "Google: No credentials provided and GOOGLE_CREDENTIALS_FILE not found."
363
+ )
364
+ self.credentials_file = GOOGLE_CREDENTIALS_FILE
365
+ self._client_credentials_source = f"file:{self.credentials_file}"
366
+ try:
367
+ self.credentials_dict = json.loads(self.credentials_file.read_text())
368
+ self._set_auth_type_from_dict(self.credentials_dict)
369
+ except json.JSONDecodeError:
370
+ # Keep lazy loading for malformed files; will raise during initialize
371
+ self.logger.debug("Google: Could not parse default credentials file on load.")
372
+ return
373
+
374
+ if isinstance(credentials, str):
375
+ if credentials.lower() == "user":
376
+ # OAuth2 user credentials
377
+ self.auth_type = 'user'
378
+ self._client_credentials_source = 'user:prompt'
379
+ return
380
+ elif credentials.endswith(".json"):
381
+ # JSON file path
382
+ self.credentials_file = Path(credentials).resolve()
383
+ if not self.credentials_file.exists():
384
+ # Try BASE_DIR
385
+ self.credentials_file = BASE_DIR.joinpath(credentials).resolve()
386
+ if not self.credentials_file.exists():
387
+ raise ConfigError(
388
+ f"Google: Credentials file not found: {credentials}"
389
+ )
390
+ try:
391
+ self.credentials_dict = json.loads(self.credentials_file.read_text())
392
+ self._set_auth_type_from_dict(self.credentials_dict)
393
+ self._client_credentials_source = f"file:{self.credentials_file}"
394
+ except json.JSONDecodeError as exc:
395
+ raise ConfigError(
396
+ f"Google: Invalid JSON in credentials file: {self.credentials_file}"
397
+ ) from exc
398
+ else:
399
+ # JSON string
400
+ try:
401
+ self.credentials_dict = json.loads(credentials)
402
+ self._set_auth_type_from_dict(self.credentials_dict)
403
+ self._client_credentials_source = 'string:json'
404
+ except json.JSONDecodeError as e:
405
+ raise ConfigError(
406
+ "Google: Invalid JSON credentials string"
407
+ ) from e
408
+
409
+ elif isinstance(credentials, (Path, PurePath)):
410
+ self.credentials_file = Path(credentials).resolve()
411
+ if not self.credentials_file.exists():
412
+ raise ConfigError(
413
+ f"Google: Credentials file not found: {self.credentials_file}"
414
+ )
415
+ try:
416
+ self.credentials_dict = json.loads(self.credentials_file.read_text())
417
+ self._set_auth_type_from_dict(self.credentials_dict)
418
+ self._client_credentials_source = f"file:{self.credentials_file}"
419
+ except json.JSONDecodeError as exc:
420
+ raise ConfigError(
421
+ f"Google: Invalid JSON in credentials file: {self.credentials_file}"
422
+ ) from exc
423
+
424
+ elif isinstance(credentials, dict):
425
+ self.credentials_dict = credentials
426
+ self._set_auth_type_from_dict(self.credentials_dict)
427
+ self._client_credentials_source = 'dict:provided'
428
+
429
+ else:
430
+ raise ConfigError(
431
+ f"Google: Invalid credentials type: {type(credentials)}"
432
+ )
433
+
434
+ def _set_auth_type_from_dict(self, data: Optional[Dict[str, Any]]) -> None:
435
+ """Determine authentication type based on credentials dictionary."""
436
+ if not data:
437
+ return
438
+
439
+ if data.get('type') == 'service_account':
440
+ self.auth_type = 'service_account'
441
+ self._oauth_client_config = None
442
+ return
443
+
444
+ if (oauth_config := self._extract_oauth_client_config(data)):
445
+ self.auth_type = 'user'
446
+ self._oauth_client_config = oauth_config
447
+ else:
448
+ self._oauth_client_config = None
449
+
450
+ @staticmethod
451
+ def _extract_oauth_client_config(data: Optional[Dict[str, Any]]) -> Optional[Dict[str, Any]]:
452
+ """Extract OAuth client configuration from credentials dictionary."""
453
+ if not data or not isinstance(data, dict):
454
+ return None
455
+
456
+ if data.get('type') == 'service_account':
457
+ return None
458
+
459
+ for key in ('installed', 'web'):
460
+ value = data.get(key)
461
+ if isinstance(value, dict) and 'client_id' in value:
462
+ return value
463
+
464
+ if 'client_id' in data and ('auth_uri' in data or 'token_uri' in data):
465
+ return data
466
+
467
+ return None
468
+
469
+ def _get_oauth_client_config(self) -> Dict[str, Any]:
470
+ """Resolve OAuth client credentials for interactive login."""
471
+ if self._oauth_client_config:
472
+ return self._oauth_client_config
473
+
474
+ candidates: List[Dict[str, Any]] = []
475
+
476
+ if isinstance(self.credentials_dict, dict):
477
+ candidates.append(self.credentials_dict)
478
+
479
+ if self.credentials_file and Path(self.credentials_file).exists():
480
+ try:
481
+ candidates.append(json.loads(Path(self.credentials_file).read_text()))
482
+ except json.JSONDecodeError:
483
+ self.logger.debug(
484
+ "Google: Failed to parse credentials file %s for OAuth config.",
485
+ self.credentials_file
486
+ )
487
+
488
+ default_file = GOOGLE_CREDENTIALS_FILE
489
+ if default_file and default_file.exists():
490
+ if not self.credentials_file or Path(self.credentials_file).resolve() != default_file.resolve():
491
+ try:
492
+ candidates.append(json.loads(default_file.read_text()))
493
+ except json.JSONDecodeError:
494
+ self.logger.debug(
495
+ "Google: Failed to parse default credentials file %s for OAuth config.",
496
+ default_file
497
+ )
498
+
499
+ for candidate in candidates:
500
+ if (oauth_config := self._extract_oauth_client_config(candidate)):
501
+ self._oauth_client_config = oauth_config
502
+ return oauth_config
503
+
504
+ raise ConfigError(
505
+ "Google: OAuth client credentials not found. Provide OAuth client JSON for user authentication."
506
+ )
507
+
508
+ def _prepare_user_creds(
509
+ self,
510
+ creds: Dict[str, Any],
511
+ scopes: List[str]
512
+ ) -> Dict[str, Any]:
513
+ """Prepare user credential payload for storage and UserCreds construction."""
514
+ allowed_keys = [
515
+ 'access_token', 'refresh_token', 'expires_in', 'expires_at',
516
+ 'token_type', 'token_uri', 'token_info_uri', 'revoke_uri', 'id_token_jwt'
517
+ ]
518
+ sanitized = {key: creds.get(key) for key in allowed_keys if creds.get(key) is not None}
519
+
520
+ id_token = creds.get('id_token')
521
+ if isinstance(id_token, (dict, str)):
522
+ sanitized['id_token'] = id_token
523
+
524
+ sanitized['scopes'] = scopes
525
+ return sanitized
526
+
527
+ def _export_active_user_creds(self, scopes: List[str]) -> Dict[str, Any]:
528
+ """Return the sanitized credential payload for the active user session."""
529
+ if not self._user_creds:
530
+ raise RuntimeError("Google: No active user credentials to export")
531
+
532
+ payload: Dict[str, Any]
533
+ if self._user_creds_payload:
534
+ payload = self._user_creds_payload.copy()
535
+ else:
536
+ payload = dict(self._user_creds)
537
+ payload['scopes'] = scopes
538
+ return self._prepare_user_creds(payload, scopes)
539
+
540
+ def _save_user_creds_to_cache(self, creds: Dict[str, Any]) -> None:
541
+ """Persist user credentials to cache for subsequent sessions."""
542
+ if not self.user_creds_cache_file:
543
+ return
544
+
545
+ try:
546
+ self.user_creds_cache_file.parent.mkdir(parents=True, exist_ok=True)
547
+ self.user_creds_cache_file.write_text(json.dumps(creds, indent=2, default=str))
548
+ except Exception as cache_error: # pragma: no cover - defensive
549
+ self.logger.warning(
550
+ "Google: Failed to cache user credentials: %s",
551
+ cache_error
552
+ )
553
+
554
+ def _load_cached_user_creds(self) -> bool:
555
+ """Load cached user credentials if available."""
556
+ if not self.user_creds_cache_file or not self.user_creds_cache_file.exists():
557
+ return False
558
+
559
+ try:
560
+ cached = json.loads(self.user_creds_cache_file.read_text())
561
+ scopes = cached.get('scopes', self.scopes)
562
+ if isinstance(scopes, str):
563
+ scopes = [scopes]
564
+ creds_kwargs = cached.copy()
565
+ creds_kwargs.pop('scopes', None)
566
+ self._user_creds = UserCreds(scopes=scopes, **creds_kwargs)
567
+ self._user_creds_payload = cached
568
+ self._client_credentials_source = f"cache:{self.user_creds_cache_file}"
569
+ return True
570
+ except Exception as cache_error: # pragma: no cover - defensive
571
+ self.logger.warning(
572
+ "Google: Failed to load cached user credentials: %s",
573
+ cache_error
574
+ )
575
+ return False
576
+
577
+ def load_cached_user_credentials(self) -> bool:
578
+ """Public helper for loading cached user credentials."""
579
+ return self._load_cached_user_creds()
580
+
581
+ def set_credentials(self, credentials: Optional[Union[str, dict, Path]]) -> None:
582
+ """Public helper to update credentials after initialization."""
583
+ self._load_credentials(credentials)
584
+ self._authenticated = False
585
+
586
+ @property
587
+ def active_credentials(self) -> Optional[Union[ServiceAccountCreds, UserCreds]]:
588
+ """Return whichever credential set is currently active."""
589
+ return self._service_account_creds or self._user_creds
590
+
591
+ @property
592
+ def credentials_source(self) -> Optional[str]:
593
+ """Return the source the client used to obtain credentials."""
594
+ return self._client_credentials_source
595
+
596
+ @property
597
+ def is_authenticated(self) -> bool:
598
+ """Expose authentication status for callers."""
599
+ return self._authenticated
600
+
601
+ def using_service_account(self) -> bool:
602
+ """Return True if the client is configured for service-account credentials."""
603
+ return self.auth_type == 'service_account' and self._service_account_creds is not None
604
+
605
+ def using_user_credentials(self) -> bool:
606
+ """Return True if the client is configured for end-user OAuth credentials."""
607
+ return self.auth_type == 'user' and self._user_creds is not None
608
+
609
+ async def initialize(self) -> GoogleClient:
610
+ """
611
+ Initialize the client and authenticate.
612
+
613
+ Returns:
614
+ Self for method chaining
615
+ """
616
+ if self._authenticated:
617
+ return self
618
+
619
+ # Process environment variables in credentials
620
+ self.processing_credentials()
621
+ if self.auth_type != 'service_account':
622
+ # user creds: try Redis first
623
+ client_id = None
624
+ try:
625
+ oauth_cfg = self._get_oauth_client_config() # you already have this
626
+ client_id = oauth_cfg.get("client_id")
627
+ except Exception:
628
+ pass
629
+
630
+ # Optional user hint from config/env (email), else None
631
+ user_hint = (self.credentials_dict or {}).get("user_email") or os.environ.get("GOOGLE_USER_HINT")
632
+
633
+ if not self._user_creds:
634
+ loaded = await self._load_user_creds_from_redis(client_id, user_hint, self.scopes)
635
+ if not loaded:
636
+ # Fall back to file cache
637
+ if not self._load_cached_user_creds():
638
+ raise RuntimeError(
639
+ "Google: User credentials not available. Run interactive_login() first."
640
+ )
641
+
642
+ elif self.auth_type == 'service_account':
643
+ # Service account credentials
644
+ if self.credentials_dict:
645
+ creds_dict = self.credentials_dict
646
+ elif self.credentials_file:
647
+ creds_dict = json.loads(self.credentials_file.read_text())
648
+ else:
649
+ raise RuntimeError("Google: No credentials available")
650
+
651
+ self._service_account_creds = ServiceAccountCreds(
652
+ scopes=self.scopes,
653
+ **creds_dict
654
+ )
655
+ self._user_creds = None
656
+ self._user_creds_payload = None
657
+ if not self._client_credentials_source:
658
+ self._client_credentials_source = 'service_account:runtime'
659
+ else:
660
+ # User credentials require interactive login
661
+ self._service_account_creds = None
662
+ if not self._user_creds and not self._load_cached_user_creds():
663
+ raise RuntimeError(
664
+ "Google: User credentials not available. Run interactive_login() first."
665
+ )
666
+
667
+ self._authenticated = True
668
+ self.logger.info("Google Client initialized")
669
+ return self
670
+
671
+ async def execute_api_call(
672
+ self,
673
+ service_name: str,
674
+ api_name: str,
675
+ method_chain: str,
676
+ version: str = None,
677
+ **kwargs
678
+ ) -> Any:
679
+ """
680
+ Execute a Google API call.
681
+
682
+ Args:
683
+ service_name: Service name (drive, sheets, docs, calendar, storage, gmail)
684
+ api_name: API resource name (e.g., 'files', 'spreadsheets', 'events')
685
+ method_chain: Method to call (e.g., 'list', 'get', 'create')
686
+ version: API version (defaults based on service)
687
+ **kwargs: Method parameters
688
+
689
+ Returns:
690
+ API response
691
+
692
+ Example:
693
+ # List Drive files
694
+ files = await client.execute_api_call(
695
+ 'drive', 'files', 'list',
696
+ pageSize=10,
697
+ fields='files(id, name)'
698
+ )
699
+
700
+ # Get spreadsheet
701
+ sheet = await client.execute_api_call(
702
+ 'sheets', 'spreadsheets', 'get',
703
+ version='v4',
704
+ spreadsheetId='abc123'
705
+ )
706
+ """
707
+ if not self._authenticated:
708
+ await self.initialize()
709
+
710
+ # Default versions
711
+ version_map = {
712
+ 'drive': 'v3',
713
+ 'sheets': 'v4',
714
+ 'docs': 'v1',
715
+ 'calendar': 'v3',
716
+ 'storage': 'v1',
717
+ 'gmail': 'v1',
718
+ 'customsearch': 'v1'
719
+ }
720
+
721
+ if version is None:
722
+ version = version_map.get(service_name, 'v1')
723
+
724
+ async with Aiogoogle(
725
+ service_account_creds=self._service_account_creds,
726
+ user_creds=self._user_creds
727
+ ) as aiogoogle:
728
+ # Discover the API
729
+ api = await aiogoogle.discover(service_name, version)
730
+
731
+ # Navigate to the resource
732
+ resource = getattr(api, api_name)
733
+ # Get the method
734
+ method = getattr(resource, method_chain)
735
+ # Execute the request
736
+ if self._service_account_creds:
737
+ result = await aiogoogle.as_service_account(method(**kwargs))
738
+ else:
739
+ result = await aiogoogle.as_user(method(**kwargs))
740
+
741
+ return result
742
+
743
+ async def get_drive_client(self, version: str = 'v3') -> Dict[str, Any]:
744
+ """Get Google Drive client config."""
745
+ return {'service': 'drive', 'version': version}
746
+
747
+ async def get_sheets_client(self, version: str = 'v4') -> Dict[str, Any]:
748
+ """Get Google Sheets client config."""
749
+ return {'service': 'sheets', 'version': version}
750
+
751
+ async def get_docs_client(self, version: str = 'v1') -> Dict[str, Any]:
752
+ """Get Google Docs client config."""
753
+ return {'service': 'docs', 'version': version}
754
+
755
+ async def get_calendar_client(self, version: str = 'v3') -> Dict[str, Any]:
756
+ """Get Google Calendar client config."""
757
+ return {'service': 'calendar', 'version': version}
758
+
759
+ async def get_storage_client(self, version: str = 'v1') -> Dict[str, Any]:
760
+ """Get Google Cloud Storage client config."""
761
+ return {'service': 'storage', 'version': version}
762
+
763
+ async def get_gmail_client(self, version: str = 'v1') -> Dict[str, Any]:
764
+ """Get Gmail client config."""
765
+ return {'service': 'gmail', 'version': version}
766
+
767
+ async def search(
768
+ self,
769
+ query: str,
770
+ cse_id: Optional[str] = None,
771
+ **kwargs
772
+ ) -> Dict[str, Any]:
773
+ """
774
+ Perform a Google Custom Search.
775
+
776
+ Args:
777
+ query: Search query
778
+ cse_id: Custom Search Engine ID
779
+ **kwargs: Additional search parameters
780
+
781
+ Returns:
782
+ Search results
783
+ """
784
+ if not (cse_id := cse_id or os.environ.get('GOOGLE_SEARCH_ENGINE_ID')):
785
+ raise RuntimeError(
786
+ "Google Custom Search requires cse_id parameter or "
787
+ "GOOGLE_SEARCH_ENGINE_ID environment variable"
788
+ )
789
+
790
+ return await self.execute_api_call(
791
+ 'customsearch',
792
+ 'cse',
793
+ 'list',
794
+ q=query,
795
+ cx=cse_id,
796
+ **kwargs
797
+ )
798
+
799
+ async def interactive_login(
800
+ self,
801
+ scopes: Optional[Union[List[str], str]] = None,
802
+ port: int = 5050,
803
+ redirect_uri: Optional[str] = None,
804
+ open_browser: bool = True,
805
+ browser: str = "system",
806
+ login_callback: Optional[Callable[[str], Optional[bool]]] = None,
807
+ timeout: int = 300
808
+ ) -> Dict[str, Any]:
809
+ """
810
+ Perform interactive OAuth2 login for user credentials.
811
+
812
+ This opens a browser for the user to authenticate.
813
+
814
+ Args:
815
+ scopes: Scopes to request (defaults to self.scopes)
816
+ port: Local server port for OAuth redirect
817
+ redirect_uri: Custom redirect URI
818
+ open_browser: When True, launch a Playwright browser to complete login
819
+ login_callback: Optional callback invoked with the authorization URL
820
+ timeout: Seconds to wait for the authentication flow to complete
821
+ """
822
+
823
+ self.auth_type = 'user'
824
+ scopes_list = self._process_scopes(scopes or self.scopes)
825
+ self.processing_credentials()
826
+
827
+ try:
828
+ await self.ensure_interactive_session(scopes_list)
829
+ except Exception as cached_error: # pragma: no cover - defensive reuse path
830
+ self.logger.debug(
831
+ "Google: cached interactive session unavailable: %s",
832
+ cached_error
833
+ )
834
+ else:
835
+ if self._user_creds:
836
+ self.logger.info(
837
+ "Google: reusing cached credentials for interactive login request"
838
+ )
839
+ return self._export_active_user_creds(scopes_list)
840
+
841
+ oauth_client_config = self._get_oauth_client_config()
842
+ redirect_uri = redirect_uri or oauth_client_config.get('redirect_uri')
843
+ if not redirect_uri:
844
+ redirect_uris = oauth_client_config.get('redirect_uris', [])
845
+ if redirect_uris:
846
+ redirect_uri = redirect_uris[0]
847
+ if not redirect_uri:
848
+ redirect_uri = f"http://localhost:{port}/callback/aiogoogle"
849
+
850
+ parsed_redirect = urlparse(redirect_uri)
851
+ callback_host = parsed_redirect.hostname or 'localhost'
852
+ callback_port = parsed_redirect.port or port
853
+ callback_path = parsed_redirect.path or '/'
854
+ if not callback_path.startswith('/'):
855
+ callback_path = f'/{callback_path}'
856
+
857
+ client_creds = {
858
+ 'client_id': oauth_client_config['client_id'],
859
+ 'client_secret': oauth_client_config.get('client_secret'),
860
+ 'scopes': scopes_list,
861
+ 'redirect_uri': redirect_uri
862
+ }
863
+
864
+ aiogoogle_client = Aiogoogle(client_creds=client_creds)
865
+ if not aiogoogle_client.oauth2.is_ready(client_creds):
866
+ raise ConfigError("Google: OAuth client configuration is incomplete for interactive login")
867
+
868
+ state = create_secret()
869
+ authorization_url = aiogoogle_client.oauth2.authorization_url(
870
+ client_creds=client_creds,
871
+ state=state,
872
+ access_type="offline",
873
+ include_granted_scopes=True,
874
+ prompt="consent"
875
+ )
876
+
877
+ # Provide URL via callback or console
878
+ if login_callback:
879
+ try:
880
+ login_callback(authorization_url)
881
+ except Exception as callback_error: # pragma: no cover - defensive
882
+ self.logger.warning(
883
+ "Login callback raised an exception: %s",
884
+ callback_error
885
+ )
886
+ self.logger.info("Authorize Google access by visiting: %s", authorization_url)
887
+ print("\n" + "=" * 60)
888
+ print("Open the following URL in your browser to authenticate:")
889
+ print(authorization_url)
890
+ print("=" * 60 + "\n")
891
+
892
+ login_event = asyncio.Event()
893
+ result_container: Dict[str, Any] = {}
894
+ error_container: Dict[str, Any] = {}
895
+
896
+ routes = web.RouteTableDef()
897
+
898
+ @routes.get(callback_path)
899
+ async def oauth_callback(request): # type: ignore[unused-variable]
900
+ if request.query.get('error'):
901
+ error_container['error'] = request.query.get('error_description') or request.query.get('error')
902
+ login_event.set()
903
+ return web.json_response({'status': 'error', **error_container}, status=400)
904
+
905
+ if not request.query.get('code'):
906
+ login_event.set()
907
+ error_container['error'] = 'Missing authorization code'
908
+ return web.Response(text="Missing authorization code", status=400)
909
+
910
+ returned_state = request.query.get('state')
911
+ if returned_state != state:
912
+ login_event.set()
913
+ error_container['error'] = 'State mismatch during OAuth2 callback'
914
+ return web.Response(text="State mismatch", status=400)
915
+
916
+ try:
917
+ full_user_creds = await aiogoogle_client.oauth2.build_user_creds(
918
+ grant=request.query.get('code'),
919
+ client_creds=client_creds
920
+ )
921
+ result_container['creds'] = full_user_creds
922
+ login_event.set()
923
+ return web.Response(
924
+ text="Authentication complete. You may close this window.",
925
+ content_type='text/plain'
926
+ )
927
+ except Exception as auth_error: # pragma: no cover - defensive
928
+ error_container['error'] = str(auth_error)
929
+ login_event.set()
930
+ return web.Response(
931
+ text=f"Authentication failed: {auth_error}",
932
+ status=500
933
+ )
934
+
935
+ app = web.Application()
936
+ app.add_routes(routes)
937
+
938
+ runner = web.AppRunner(app)
939
+ await runner.setup()
940
+ site = web.TCPSite(runner, host=callback_host, port=callback_port)
941
+ await site.start()
942
+
943
+ playwright_task: Optional[asyncio.Task] = None
944
+ if open_browser:
945
+ if browser == "system":
946
+ webbrowser.open(authorization_url, new=1, autoraise=True)
947
+ elif browser == "playwright":
948
+ try:
949
+ async def launch_browser():
950
+ try:
951
+ async with async_playwright() as playwright:
952
+ browser = await playwright.chromium.launch(channel="chrome", headless=False)
953
+ page = await browser.new_page()
954
+ try:
955
+ await page.goto(authorization_url, wait_until="load")
956
+ await login_event.wait()
957
+ finally:
958
+ with suppress(Exception):
959
+ await page.close()
960
+ with suppress(Exception):
961
+ await browser.close()
962
+ except asyncio.CancelledError: # pragma: no cover - cancellation support
963
+ raise
964
+ except Exception as browser_error: # pragma: no cover - defensive
965
+ self.logger.warning(
966
+ "Playwright interactive session failed: %s",
967
+ browser_error
968
+ )
969
+
970
+ playwright_task = asyncio.create_task(launch_browser())
971
+ except ImportError:
972
+ self.logger.warning(
973
+ "Playwright is not installed; open the authorization URL manually."
974
+ )
975
+ elif browser == "selenium":
976
+ try:
977
+ from selenium import webdriver
978
+ from selenium.webdriver.chrome.options import Options
979
+
980
+ def launch_selenium_browser():
981
+ try:
982
+ options = Options()
983
+ options.add_argument("--disable-infobars")
984
+ options.add_argument("--disable-extensions")
985
+ driver = webdriver.Chrome(options=options)
986
+ driver.get(authorization_url)
987
+ # Wait until login_event is set
988
+ while not login_event.is_set():
989
+ asyncio.sleep(1)
990
+ except Exception as selenium_error: # pragma: no cover - defensive
991
+ self.logger.warning(
992
+ "Selenium interactive session failed: %s",
993
+ selenium_error
994
+ )
995
+ finally:
996
+ with suppress(Exception):
997
+ driver.quit()
998
+
999
+ loop = asyncio.get_event_loop()
1000
+ playwright_task = loop.run_in_executor(None, launch_selenium_browser)
1001
+ except ImportError:
1002
+ self.logger.warning(
1003
+ "Selenium is not installed; open the authorization URL manually."
1004
+ )
1005
+ else:
1006
+ self.logger.warning("Unknown browser=%s, falling back to system browser", browser)
1007
+ webbrowser.open(authorization_url, new=1, autoraise=True)
1008
+
1009
+ try:
1010
+ await asyncio.wait_for(login_event.wait(), timeout=timeout)
1011
+ except asyncio.TimeoutError as exc:
1012
+ raise RuntimeError(
1013
+ "Google interactive login timed out. Try again and ensure the browser completes authentication."
1014
+ ) from exc
1015
+ finally:
1016
+ if playwright_task:
1017
+ playwright_task.cancel()
1018
+ with suppress(Exception):
1019
+ await playwright_task
1020
+ await runner.cleanup()
1021
+
1022
+ if error_container.get('error'):
1023
+ raise RuntimeError(f"Google interactive login failed: {error_container['error']}")
1024
+
1025
+ if 'creds' not in result_container:
1026
+ raise RuntimeError("Google interactive login did not return credentials")
1027
+
1028
+ sanitized_creds = self._prepare_user_creds(result_container['creds'], scopes_list)
1029
+ creds_for_instance = sanitized_creds.copy()
1030
+ scopes_for_user = creds_for_instance.pop('scopes', scopes_list)
1031
+ self._user_creds = UserCreds(scopes=scopes_for_user, **creds_for_instance)
1032
+ self._service_account_creds = None
1033
+ self._authenticated = True
1034
+ self._client_credentials_source = 'user:interactive'
1035
+ self._user_creds_payload = sanitized_creds.copy()
1036
+
1037
+ self._save_user_creds_to_cache(sanitized_creds)
1038
+ client_id = oauth_client_config.get('client_id')
1039
+
1040
+ self.logger.info("Google interactive login completed successfully")
1041
+ user_hint = sanitized_creds.get("id_token", {}) if isinstance(sanitized_creds.get("id_token"), dict) else None
1042
+ if isinstance(user_hint, dict):
1043
+ # try extracting email if present
1044
+ user_hint = user_hint.get("email")
1045
+ await self._save_user_creds_to_redis(sanitized_creds, client_id, user_hint, scopes_list)
1046
+ return sanitized_creds
1047
+
1048
+ async def close(self) -> None:
1049
+ """Clean up resources."""
1050
+ self._authenticated = False
1051
+ self.logger.info("Google Client closed")
1052
+
1053
+ async def __aenter__(self) -> GoogleClient:
1054
+ """Async context manager entry."""
1055
+ await self.initialize()
1056
+ return self
1057
+
1058
+ async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
1059
+ """Async context manager exit."""
1060
+ await self.close()
1061
+
1062
+ def __repr__(self) -> str:
1063
+ return (
1064
+ f"GoogleClient("
1065
+ f"auth_type={self.auth_type}, "
1066
+ f"authenticated={self._authenticated})"
1067
+ )
1068
+
1069
+ async def ensure_interactive_session(self, scopes: Optional[Union[List[str], str]] = None) -> None:
1070
+ """Ensure we have usable user creds in memory; load from Redis/file cache if possible."""
1071
+ scopes_list = self._process_scopes(scopes or self.scopes)
1072
+ if self._user_creds:
1073
+ return
1074
+
1075
+ client_id = None
1076
+ with contextlib.suppress(Exception):
1077
+ oauth_cfg = self._get_oauth_client_config()
1078
+ client_id = oauth_cfg.get("client_id")
1079
+
1080
+ user_hint = (self.credentials_dict or {}).get("user_email") or os.environ.get("GOOGLE_USER_HINT")
1081
+
1082
+ loaded = await self._load_user_creds_from_redis(client_id, user_hint, scopes_list)
1083
+ if not loaded and not self._load_cached_user_creds():
1084
+ raise RuntimeError("Google: no cached session; call interactive_login()")
1085
+
1086
+ # Optionally probe a trivial endpoint to trigger refresh if needed:
1087
+ try:
1088
+ async with Aiogoogle(user_creds=self._user_creds) as ag:
1089
+ # a lightweight no-op call: get token info endpoint
1090
+ _ = await ag.oauth2.get_user_info() # if scopes include openid/profile; otherwise skip
1091
+ except Exception as e:
1092
+ # If this fails due to expired token & bad refresh, force re-login
1093
+ raise RuntimeError(
1094
+ "Google: cached session expired; run interactive_login() again"
1095
+ ) from e
1096
+
1097
+
1098
+
1099
+ # ============================================================================
1100
+ # Helper Functions
1101
+ # ============================================================================
1102
+
1103
+ def create_google_client(
1104
+ credentials: Optional[Union[str, dict, Path]] = None,
1105
+ scopes: Optional[Union[List[str], str]] = None,
1106
+ **kwargs
1107
+ ) -> GoogleClient:
1108
+ """
1109
+ Factory function to create a GoogleClient.
1110
+
1111
+ Args:
1112
+ credentials: Credentials specification
1113
+ scopes: Service scopes
1114
+ **kwargs: Additional GoogleClient arguments
1115
+
1116
+ Returns:
1117
+ GoogleClient instance
1118
+ """
1119
+ return GoogleClient(
1120
+ credentials=credentials,
1121
+ scopes=scopes,
1122
+ **kwargs
1123
+ )