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,978 @@
1
+ from typing import Any, Optional, List, Dict, Callable
2
+ import asyncio
3
+ import contextlib
4
+ from concurrent.futures import ThreadPoolExecutor
5
+ import time
6
+ from urllib.parse import urlparse
7
+ from redis import asyncio as aioredis
8
+ import msal
9
+ from msgraph import GraphServiceClient
10
+ from msal import PublicClientApplication, SerializableTokenCache
11
+ from msal.application import ClientApplication
12
+ # Microsoft Graph SDK imports
13
+ from azure.identity import (
14
+ ClientSecretCredential,
15
+ UsernamePasswordCredential,
16
+ OnBehalfOfCredential
17
+ )
18
+ from azure.core.credentials import (
19
+ AccessToken,
20
+ TokenCredential
21
+ )
22
+ from navconfig.logging import logging
23
+ from ..conf import (
24
+ SHAREPOINT_TENANT_NAME,
25
+ O365_CLIENT_ID,
26
+ O365_CLIENT_SECRET,
27
+ O365_TENANT_ID,
28
+ REDIS_HISTORY_URL
29
+ )
30
+ from .credentials import CredentialsInterface
31
+
32
+
33
+ logging.getLogger('msal').setLevel(logging.INFO)
34
+
35
+
36
+ # Tokens are typically 90 days for non-SPA apps. :contentReference[oaicite:0]{index=0}
37
+ TOKEN_CACHE_TTL_SECONDS = 75 * 24 * 3600 # 75 days
38
+
39
+
40
+ class MSALTokenCredential(TokenCredential):
41
+ """
42
+ Custom TokenCredential that uses MSAL tokens for azure-identity compatibility.
43
+ This allows us to use MSAL-acquired tokens with the Graph SDK.
44
+ """
45
+
46
+ def __init__(self, msal_app, scopes: List[str], username: str = None, password: str = None):
47
+ self.msal_app = msal_app
48
+ self.scopes = scopes
49
+ self.username = username
50
+ self.password = password
51
+ self._token_cache = None
52
+ self.logger = logging.getLogger(__name__)
53
+ super().__init__()
54
+
55
+ def get_token(self, *scopes, **kwargs) -> AccessToken:
56
+ """Get token using MSAL."""
57
+ try:
58
+ # Use provided scopes or default
59
+ token_scopes = list(scopes) if scopes else self.scopes
60
+
61
+ if self.username and self.password:
62
+ # Username/password flow
63
+ result = self.msal_app.acquire_token_by_username_password(
64
+ username=self.username,
65
+ password=self.password,
66
+ scopes=token_scopes
67
+ )
68
+ else:
69
+ # Client credentials flow
70
+ result = self.msal_app.acquire_token_for_client(scopes=token_scopes)
71
+
72
+ if "access_token" not in result:
73
+ error_msg = result.get('error_description', 'Unknown error')
74
+ raise RuntimeError(f"MSAL token acquisition failed: {error_msg}")
75
+
76
+ # Convert to AccessToken
77
+ return AccessToken(
78
+ token=result['access_token'],
79
+ expires_on=result.get('expires_in', 3600) + asyncio.get_event_loop().time()
80
+ )
81
+
82
+ except Exception as e:
83
+ self.logger.error(f"MSALTokenCredential failed: {e}")
84
+ raise
85
+
86
+ class MSALCacheTokenCredential(TokenCredential):
87
+ """TokenCredential that uses an MSAL client application with a serialized cache."""
88
+
89
+ def __init__(
90
+ self,
91
+ app: ClientApplication,
92
+ scopes: List[str],
93
+ account=None,
94
+ logger=None
95
+ ):
96
+ self.app = app
97
+ self.scopes = scopes
98
+ self.account = account
99
+ self.logger = logger or logging.getLogger(__name__)
100
+ super().__init__()
101
+
102
+ def get_token(self, *scopes, **kwargs) -> AccessToken:
103
+ wanted_scopes = list(scopes) if scopes else self.scopes
104
+ result = self.app.acquire_token_silent(wanted_scopes, account=self.account)
105
+ if not result or "access_token" not in result:
106
+ raise RuntimeError(
107
+ "No cached token available. Run interactive_login() first."
108
+ )
109
+ # MSAL returns expires_in (seconds). Convert to absolute epoch as azure-core expects.
110
+ return AccessToken(
111
+ result["access_token"], int(time.time()) + int(result.get("expires_in", 3600))
112
+ )
113
+
114
+
115
+ class O365Client(CredentialsInterface):
116
+ """
117
+ O365Client - Migrated to Microsoft Graph SDK
118
+
119
+ Overview
120
+
121
+ The O365Client class is an abstract base class for managing connections to Office 365 services
122
+ using the official Microsoft Graph SDK. It handles authentication, credential processing,
123
+ and provides methods for obtaining the Graph client. It uses Azure Identity for authentication
124
+ and Microsoft Graph SDK for context management.
125
+
126
+ Supported Authentication Methods:
127
+ - Username/Password (UsernamePasswordCredential)
128
+ - Client Credentials (ClientSecretCredential)
129
+ - On-Behalf-Of (OnBehalfOfCredential)
130
+
131
+ .. table:: Properties
132
+ :widths: auto
133
+
134
+ +------------------+----------+--------------------------------------------------------------------------------------------------+
135
+ | Name | Required | Description |
136
+ +------------------+----------+--------------------------------------------------------------------------------------------------+
137
+ | url | No | The base URL for the Office 365 service. |
138
+ +------------------+----------+--------------------------------------------------------------------------------------------------+
139
+ | tenant | Yes | The tenant ID for the Office 365 service. |
140
+ +------------------+----------+--------------------------------------------------------------------------------------------------+
141
+ | site | No | The site URL for the Office 365 service. |
142
+ +------------------+----------+--------------------------------------------------------------------------------------------------+
143
+ | credential | Yes | The Azure Identity credential object. |
144
+ +------------------+----------+--------------------------------------------------------------------------------------------------+
145
+ | graph_client | Yes | The Microsoft Graph SDK client object. |
146
+ +------------------+----------+--------------------------------------------------------------------------------------------------+
147
+ | credentials | Yes | A dictionary containing the credentials for authentication. |
148
+ +------------------+----------+--------------------------------------------------------------------------------------------------+
149
+
150
+ Return
151
+
152
+ The methods in this class manage the authentication and connection setup for Office 365 services,
153
+ providing an abstract base for subclasses to implement specific service interactions.
154
+
155
+ """ # noqa
156
+ _credentials: dict = {
157
+ "username": str,
158
+ "password": str,
159
+ "client_id": str,
160
+ "client_secret": str,
161
+ "tenant": str,
162
+ "site": str,
163
+ "tenant_id": str,
164
+ "assertion": str, # For OnBehalfOfCredential
165
+ }
166
+
167
+ def __init__(self, *args, **kwargs) -> None:
168
+ self.redis_url = kwargs.get('redis_url', REDIS_HISTORY_URL)
169
+ self.url: Optional[str] = None
170
+ self.tenant_id: Optional[str] = None
171
+ self.tenant: Optional[str] = None
172
+ self.site: Optional[str] = None
173
+ self.auth_mode: Optional[str] = None
174
+
175
+ # Azure Identity and Graph SDK objects
176
+ self._credential: Optional[TokenCredential] = None
177
+ self._graph_client: Optional[GraphServiceClient] = None
178
+ self._access_token: Optional[str] = None
179
+
180
+ # Legacy compatibility properties
181
+ self.auth_context: Any = None # For backwards compatibility
182
+ self.context: Any = None # For backwards compatibility
183
+
184
+ self.logger = logging.getLogger('Flowtask.O365Client')
185
+ self._executor = ThreadPoolExecutor()
186
+
187
+ # Default credentials from config
188
+ self._default_tenant_id = O365_TENANT_ID
189
+ self._default_client_id = O365_CLIENT_ID
190
+ self._default_client_secret = O365_CLIENT_SECRET
191
+ self._default_tenant_name = SHAREPOINT_TENANT_NAME
192
+
193
+ # Default scopes for Graph API
194
+ self._default_scopes = ["https://graph.microsoft.com/.default"]
195
+
196
+ super(O365Client, self).__init__(*args, **kwargs)
197
+ # Redis connection for token cache
198
+ self.redis = aioredis.from_url(
199
+ self.redis_url, encoding="utf-8", decode_responses=True
200
+ )
201
+
202
+ def get_context(self, url: str, *args):
203
+ """Return the Graph client for the given URL."""
204
+ return self.graph_client
205
+
206
+ def _start_(self, **kwargs):
207
+ """Initialize subclass-specific configuration."""
208
+ return True
209
+
210
+ async def run_in_executor(self, fn, *args, **kwargs):
211
+ """
212
+ Calling any blocking process in an executor.
213
+ """
214
+ return await asyncio.get_event_loop().run_in_executor(
215
+ self._executor, fn, *args, **kwargs
216
+ )
217
+
218
+ def processing_credentials(self):
219
+ """Process credentials using the inherited CredentialsInterface."""
220
+ super().processing_credentials()
221
+
222
+ # Extract tenant and site from credentials
223
+ try:
224
+ self.tenant = self.credentials.get('tenant', None) or self._default_tenant_name
225
+ self.site = self.credentials.get('site', None)
226
+ self.tenant_id = self.credentials.get('tenant_id', self._default_tenant_id)
227
+ except KeyError as e:
228
+ raise RuntimeError(
229
+ f"Office365: Missing Tenant or Site Configuration: {e}."
230
+ ) from e
231
+
232
+ def _effective_scopes(self, scopes: Optional[List[str]] = None) -> List[str]:
233
+ if self.credentials.get("username") or self.credentials.get("assertion"):
234
+ return ["User.Read", "Files.ReadWrite.All", "Sites.Read.All", "offline_access", "openid", "profile"]
235
+ return scopes or self._default_scopes
236
+
237
+ def _create_credential(self) -> TokenCredential:
238
+ """
239
+ Create appropriate Azure Identity credential based on available credentials.
240
+
241
+ Returns:
242
+ TokenCredential: The appropriate credential for authentication
243
+ """
244
+ # Extract credentials
245
+ username = self.credentials.get("username")
246
+ password = self.credentials.get("password")
247
+ client_id = self.credentials.get("client_id", self._default_client_id)
248
+ client_secret = self.credentials.get("client_secret", self._default_client_secret)
249
+ tenant_id = self.credentials.get('tenant_id', self._default_tenant_id)
250
+ assertion = self.credentials.get("assertion") # For OnBehalfOfCredential
251
+
252
+ if not tenant_id:
253
+ raise RuntimeError(
254
+ "Office365: Missing tenant_id in credentials"
255
+ )
256
+
257
+ # Priority order for authentication methods:
258
+
259
+ # 1. OnBehalfOfCredential (if assertion is provided)
260
+ if assertion and client_id and client_secret:
261
+ self.logger.info("Using OnBehalfOfCredential authentication")
262
+ return OnBehalfOfCredential(
263
+ tenant_id=tenant_id,
264
+ client_id=client_id,
265
+ client_secret=client_secret,
266
+ user_assertion=assertion
267
+ )
268
+
269
+ # 2. Username/Password authentication
270
+ if username and password and client_id and client_secret:
271
+ self.logger.info("Using UsernamePasswordCredential authentication")
272
+ # Create MSAL confidential client app
273
+ msal_app = msal.ConfidentialClientApplication(
274
+ authority=f'https://login.microsoftonline.com/{tenant_id}',
275
+ client_id=client_id,
276
+ client_credential=client_secret
277
+ )
278
+
279
+ # Return custom credential that uses MSAL
280
+ return MSALTokenCredential(
281
+ msal_app=msal_app,
282
+ scopes=self._default_scopes,
283
+ username=username,
284
+ password=password
285
+ )
286
+ # 3. Public client Username/Password (only if no client_secret)
287
+ if username and password and client_id:
288
+ self.logger.info("Using UsernamePasswordCredential (public client) authentication")
289
+ return UsernamePasswordCredential(
290
+ client_id=client_id,
291
+ username=username,
292
+ password=password,
293
+ tenant_id=tenant_id
294
+ )
295
+
296
+ # 4. Client Credentials (app-only)
297
+ if client_id and client_secret:
298
+ self.logger.info("Using ClientSecretCredential authentication")
299
+ return ClientSecretCredential(
300
+ tenant_id=tenant_id,
301
+ client_id=client_id,
302
+ client_secret=client_secret
303
+ )
304
+
305
+ # No valid credential combination found
306
+ raise RuntimeError(
307
+ "Office365: No valid credential combination found. "
308
+ "Provide either (username + password), (client_id + client_secret), "
309
+ "or (assertion + client_id + client_secret)"
310
+ )
311
+
312
+ def _create_graph_client(self, scopes: Optional[List[str]] = None) -> GraphServiceClient:
313
+ """
314
+ Create Microsoft Graph client with the appropriate credential.
315
+
316
+ Args:
317
+ scopes: List of scopes for the Graph client
318
+
319
+ Returns:
320
+ GraphServiceClient: Configured Graph client
321
+ """
322
+ if not self._credential:
323
+ self._credential = self._create_credential()
324
+
325
+ scopes = self._effective_scopes(scopes)
326
+
327
+ # Create Graph client
328
+ graph_client = GraphServiceClient(
329
+ credentials=self._credential,
330
+ scopes=scopes
331
+ )
332
+
333
+ self.logger.info(
334
+ "Microsoft Graph client created successfully"
335
+ )
336
+ return graph_client
337
+
338
+ @property
339
+ def graph_client(self) -> GraphServiceClient:
340
+ """
341
+ Get the Graph client, creating it if necessary.
342
+
343
+ Returns:
344
+ GraphServiceClient: The configured Graph client
345
+ """
346
+ if not self._graph_client:
347
+ self._graph_client = self._create_graph_client()
348
+ return self._graph_client
349
+
350
+ @property
351
+ def access_token(self) -> Optional[str]:
352
+ """
353
+ Get current access token for backwards compatibility.
354
+
355
+ Returns:
356
+ str: Current access token or None
357
+ """
358
+ return self._access_token
359
+
360
+ def set_auth_mode(self, auth_mode: Optional[str]) -> None:
361
+ """Persist the authentication mode used to acquire tokens."""
362
+ self.auth_mode = auth_mode
363
+
364
+ @property
365
+ def is_app_only(self) -> bool:
366
+ """Return True when running with application (client credentials) permissions."""
367
+ has_user_context = bool(
368
+ self.credentials.get("username") or self.credentials.get("assertion")
369
+ )
370
+ return (self.auth_mode or "") == "direct" and not has_user_context
371
+
372
+ def get_user_context(self, user_id: Optional[str] = None):
373
+ """Return the appropriate user request builder for Graph operations."""
374
+ # Determine effective user identifier
375
+ if effective_user := (
376
+ user_id
377
+ or self.credentials.get("user_id")
378
+ or self.credentials.get("user_principal_name")
379
+ or self.credentials.get("mailbox")
380
+ or self.credentials.get("username")
381
+ ):
382
+ return self.graph_client.users.by_user_id(effective_user)
383
+
384
+ if self.is_app_only:
385
+ raise ValueError(
386
+ "App-only authentication requires a target user_id (UPN or GUID) "
387
+ "either in the tool arguments or credentials."
388
+ )
389
+
390
+ return self.graph_client.me
391
+
392
+ # Async Context Methods:
393
+ async def __aenter__(self):
394
+ return self
395
+
396
+ async def __aexit__(self, exc_type, exc_value, traceback):
397
+ if self.redis:
398
+ with contextlib.suppress(Exception):
399
+ await self.redis.close()
400
+ await self.redis.connection_pool.disconnect()
401
+ await self.close()
402
+
403
+ def connection(self):
404
+ """
405
+ Establish connection to Office 365 services using Microsoft Graph SDK.
406
+
407
+ This method replaces the old office365-rest-python-client based authentication
408
+ with modern Azure Identity + Microsoft Graph SDK approach.
409
+ """
410
+ # Call the abstract _start_ method
411
+ self._start_()
412
+
413
+ # Process credentials using the inherited interface
414
+ self.processing_credentials()
415
+
416
+ try:
417
+ # Create credential based on available authentication methods
418
+ self._credential = self._create_credential()
419
+
420
+ # Create Graph client
421
+ self._graph_client = self._create_graph_client()
422
+
423
+ # Test the connection by making a simple Graph API call
424
+ # await self._test_connection()
425
+
426
+ self.logger.info("Office365: Authentication success using Microsoft Graph SDK")
427
+
428
+ except Exception as err:
429
+ self.logger.error(f"Office365: Authentication Error: {err}")
430
+ raise RuntimeError(f"Office365: Authentication Error: {err}") from err
431
+
432
+ return self
433
+
434
+ async def _test_connection(self):
435
+ """Test the connection by making a simple Graph API call."""
436
+ try:
437
+ # Make a simple call to test authentication
438
+ # This is synchronous for compatibility with the existing interface
439
+ async def test_me():
440
+ try:
441
+ me = await self.graph_client.me.get()
442
+ self.logger.info(
443
+ f"🔗 Connected as: {me.display_name} ({me.user_principal_name})"
444
+ )
445
+ return True
446
+ except Exception:
447
+ # If /me fails (app-only), try a different endpoint
448
+ try:
449
+ organization = await self.graph_client.organization.get()
450
+ if organization and organization.value:
451
+ org = organization.value[0]
452
+ self.logger.info(
453
+ f"🔗 Connected to organization: {org.display_name}"
454
+ )
455
+ else:
456
+ self.logger.info(
457
+ "🔗 Graph API connection successful (app-only)"
458
+ )
459
+ return True
460
+ except Exception as e:
461
+ self.logger.warning(
462
+ f"⚠️ Connection test failed: {e}"
463
+ )
464
+ return False
465
+
466
+ # Run the async test
467
+ if await test_me():
468
+ self.logger.debug("Graph API connection test passed")
469
+ else:
470
+ self.logger.warning("Graph API connection test inconclusive")
471
+
472
+ except Exception as e:
473
+ self.logger.warning(f"Could not test Graph API connection: {e}")
474
+
475
+ def user_auth(self, username: str, password: str, scopes: Optional[List[str]] = None) -> Dict[str, Any]:
476
+ """
477
+ Authenticate using username and password with Microsoft Graph SDK.
478
+
479
+ This method is maintained for backwards compatibility but now uses
480
+ Azure Identity UsernamePasswordCredential internally.
481
+
482
+ Args:
483
+ username: User's username/email
484
+ password: User's password
485
+ scopes: List of scopes to request
486
+
487
+ Returns:
488
+ dict: Token information (for compatibility)
489
+ """
490
+ tenant_id = self.credentials.get('tenant_id', self._default_tenant_id)
491
+ client_id = self.credentials.get("client_id", self._default_client_id)
492
+ client_secret = self.credentials.get("client_secret", self._default_client_secret)
493
+
494
+ if not scopes:
495
+ scopes = self._default_scopes
496
+
497
+ try:
498
+ # For confidential clients (apps with client_secret), we need to use
499
+ # ConfidentialClientApplication with username/password flow
500
+ if client_secret:
501
+ self.logger.info("Using MSAL ConfidentialClientApplication for username/password")
502
+ app = msal.ConfidentialClientApplication(
503
+ authority=f'https://login.microsoftonline.com/{tenant_id}',
504
+ client_id=client_id,
505
+ client_credential=client_secret
506
+ )
507
+ else:
508
+ # Use MSAL for direct token acquisition (for compatibility)
509
+ app = msal.PublicClientApplication(
510
+ authority=f'https://login.microsoftonline.com/{tenant_id}',
511
+ client_id=client_id,
512
+ client_credential=client_secret
513
+ )
514
+
515
+ result = app.acquire_token_by_username_password(
516
+ username,
517
+ password,
518
+ scopes=scopes
519
+ )
520
+
521
+ if "access_token" not in result:
522
+ error_message = result.get('error_description', 'Unknown error')
523
+ error_code = result.get('error', 'Unknown error code')
524
+ raise RuntimeError(
525
+ f"Failed to obtain access token: {error_code} - {error_message}"
526
+ )
527
+
528
+ # Store token
529
+ self._access_token = result['access_token']
530
+
531
+ if client_secret:
532
+ # Create new MSAL-based credential for ongoing Graph SDK use
533
+ msal_app = msal.ConfidentialClientApplication(
534
+ authority=f'https://login.microsoftonline.com/{tenant_id}',
535
+ client_id=client_id,
536
+ client_credential=client_secret
537
+ )
538
+ self._credential = MSALTokenCredential(
539
+ msal_app=msal_app,
540
+ scopes=scopes,
541
+ username=username,
542
+ password=password
543
+ )
544
+ else:
545
+ # For public clients, use UsernamePasswordCredential
546
+ self._credential = UsernamePasswordCredential(
547
+ client_id=client_id,
548
+ username=username,
549
+ password=password,
550
+ tenant_id=tenant_id
551
+ )
552
+
553
+ self.logger.info(
554
+ "✅ Username/password authentication successful"
555
+ )
556
+ return result
557
+
558
+ except Exception as e:
559
+ self.logger.error(f"❌ Username/password authentication failed: {e}")
560
+ raise
561
+
562
+ def acquire_token(self, scopes: Optional[List[str]] = None) -> Dict[str, Any]:
563
+ """
564
+ Acquire token using client credentials with Microsoft Graph SDK.
565
+
566
+ This method is maintained for backwards compatibility but now uses
567
+ Azure Identity ClientSecretCredential internally.
568
+
569
+ Args:
570
+ scopes: List of scopes to request
571
+
572
+ Returns:
573
+ dict: Token information (for compatibility)
574
+ """
575
+ client_id = self.credentials.get("client_id", self._default_client_id)
576
+ client_secret = self.credentials.get("client_secret", self._default_client_secret)
577
+ tenant_id = self.credentials.get('tenant_id', self._default_tenant_id)
578
+
579
+ if not scopes:
580
+ scopes = self._default_scopes
581
+
582
+ try:
583
+ # Use MSAL for direct token acquisition (for compatibility)
584
+ authority_url = f'https://login.microsoftonline.com/{tenant_id}'
585
+ app = msal.ConfidentialClientApplication(
586
+ authority=authority_url,
587
+ client_id=client_id,
588
+ client_credential=client_secret
589
+ )
590
+
591
+ result = app.acquire_token_for_client(scopes=scopes)
592
+
593
+ if "access_token" not in result:
594
+ error_message = result.get('error_description', 'Unknown error')
595
+ error_code = result.get('error', 'Unknown error code')
596
+ raise RuntimeError(
597
+ f"Failed to obtain access token: {error_code} - {error_message}"
598
+ )
599
+
600
+ # Store token for backwards compatibility
601
+ self._access_token = result['access_token']
602
+
603
+ # Also create the proper credential for Graph SDK
604
+ self._credential = ClientSecretCredential(
605
+ tenant_id=tenant_id,
606
+ client_id=client_id,
607
+ client_secret=client_secret
608
+ )
609
+
610
+ self.logger.info(
611
+ "✅ Client credentials authentication successful"
612
+ )
613
+ return result
614
+
615
+ except Exception as e:
616
+ self.logger.error(
617
+ f"❌ Client credentials authentication failed: {e}"
618
+ )
619
+ raise
620
+
621
+ def acquire_token_on_behalf_of(
622
+ self,
623
+ user_assertion: str,
624
+ scopes: Optional[List[str]] = None
625
+ ) -> Dict[str, Any]:
626
+ """
627
+ Acquire token using On-Behalf-Of flow with Microsoft Graph SDK.
628
+
629
+ This is a new method that supports the OnBehalfOfCredential flow.
630
+
631
+ Args:
632
+ user_assertion: The user assertion (JWT token)
633
+ scopes: List of scopes to request
634
+
635
+ Returns:
636
+ dict: Token information
637
+ """
638
+ client_id = self.credentials.get("client_id", self._default_client_id)
639
+ client_secret = self.credentials.get("client_secret", self._default_client_secret)
640
+ tenant_id = self.credentials.get('tenant_id', self._default_tenant_id)
641
+
642
+ if not scopes:
643
+ scopes = self._default_scopes
644
+
645
+ try:
646
+ # Use MSAL for On-Behalf-Of flow
647
+ authority_url = f'https://login.microsoftonline.com/{tenant_id}'
648
+ app = msal.ConfidentialClientApplication(
649
+ authority=authority_url,
650
+ client_id=client_id,
651
+ client_credential=client_secret
652
+ )
653
+
654
+ result = app.acquire_token_on_behalf_of(
655
+ user_assertion=user_assertion,
656
+ scopes=scopes
657
+ )
658
+
659
+ if "access_token" not in result:
660
+ error_message = result.get('error_description', 'Unknown error')
661
+ error_code = result.get('error', 'Unknown error code')
662
+ raise RuntimeError(
663
+ f"Failed to obtain OBO token: {error_code} - {error_message}"
664
+ )
665
+
666
+ # Store token for backwards compatibility
667
+ self._access_token = result['access_token']
668
+
669
+ # Also create the proper credential for Graph SDK
670
+ self._credential = OnBehalfOfCredential(
671
+ tenant_id=tenant_id,
672
+ client_id=client_id,
673
+ client_secret=client_secret,
674
+ user_assertion=user_assertion
675
+ )
676
+
677
+ self.logger.info("✅ On-Behalf-Of authentication successful")
678
+ return result
679
+
680
+ except Exception as e:
681
+ self.logger.error(f"❌ On-Behalf-Of authentication failed: {e}")
682
+ raise
683
+
684
+ # Utility methods for easier Graph API access
685
+
686
+ async def get_me(self):
687
+ """Get current user information."""
688
+ return await self.graph_client.me.get()
689
+
690
+ async def get_organization(self):
691
+ """Get organization information."""
692
+ return await self.graph_client.organization.get()
693
+
694
+ async def get_sites(self):
695
+ """Get SharePoint sites."""
696
+ return await self.graph_client.sites.get()
697
+
698
+ async def get_drives(self):
699
+ """Get OneDrive/SharePoint drives."""
700
+ return await self.graph_client.me.drives.get()
701
+
702
+ # Backwards compatibility properties
703
+
704
+ @property
705
+ def _graph_client_legacy(self) -> GraphServiceClient:
706
+ """Legacy property name for backwards compatibility."""
707
+ return self.graph_client
708
+
709
+ async def close(self):
710
+ """Clean up resources."""
711
+ if self._executor:
712
+ self._executor.shutdown(wait=False)
713
+ self._credential = None
714
+ self._graph_client = None
715
+ self._access_token = None
716
+
717
+ def _cache_key(self) -> str:
718
+ tenant = self.credentials.get("tenant_id", self._default_tenant_id) or ""
719
+ client_id = self.credentials.get("client_id", self._default_client_id) or ""
720
+ user_hint = self.credentials.get("username", "") # optional
721
+ return f"msal:cache:{tenant}:{client_id}:{user_hint}"
722
+
723
+ async def _load_token_cache(self, cache: SerializableTokenCache) -> None:
724
+ try:
725
+ if getattr(self, "redis", None):
726
+ blob = await self.redis.get(self._cache_key())
727
+ if blob:
728
+ cache.deserialize(blob)
729
+ self.logger.info("Loaded MSAL token cache from Redis")
730
+ except Exception as e:
731
+ self.logger.warning(
732
+ f"Could not load token cache: {e}"
733
+ )
734
+
735
+ async def _save_token_cache(self, cache: SerializableTokenCache) -> None:
736
+ try:
737
+ if getattr(self, "redis", None) and cache.has_state_changed:
738
+ blob = cache.serialize()
739
+ await self.redis.set(
740
+ self._cache_key(),
741
+ blob.encode("utf-8"),
742
+ ex=TOKEN_CACHE_TTL_SECONDS
743
+ )
744
+ self.logger.info("Saved MSAL token cache to Redis")
745
+ except Exception as e:
746
+ self.logger.warning(
747
+ f"Could not save token cache: {e}"
748
+ )
749
+
750
+ def _filter_reserved_scopes(self, scopes: List[str]) -> List[str]:
751
+ """
752
+ Filter out reserved scopes that MSAL adds automatically.
753
+
754
+ Args:
755
+ scopes: List of scopes
756
+
757
+ Returns:
758
+ Filtered list without reserved scopes
759
+ """
760
+ reserved_scopes = {'offline_access', 'profile', 'openid'}
761
+ return [s for s in scopes if s not in reserved_scopes]
762
+
763
+ async def interactive_login(
764
+ self,
765
+ scopes: Optional[List[str]] = None,
766
+ redirect_uri: str = "http://localhost",
767
+ open_browser: bool = True,
768
+ login_callback: Optional[Callable[[str], Optional[bool]]] = None,
769
+ device_flow_callback: Optional[Callable[[Dict[str, Any]], None]] = None,
770
+ ) -> Dict[str, Any]:
771
+ """
772
+ Perform interactive login supporting both public and confidential clients.
773
+
774
+ - If client_secret is provided: Uses device code flow (confidential client)
775
+ - If no client_secret: Uses interactive browser flow (public client)
776
+
777
+ Args:
778
+ scopes: Requested permission scopes.
779
+ redirect_uri: Redirect URI to use for interactive login.
780
+ open_browser: When False, suppress automatic browser launch.
781
+ login_callback: Optional callback invoked with the interactive login URL
782
+ when ``open_browser`` is False. Should return ``False`` to prevent the
783
+ default browser launch behaviour.
784
+ device_flow_callback: Optional callback invoked with the device flow
785
+ payload when running the device code flow.
786
+ """
787
+ scopes = self._filter_reserved_scopes(scopes or [
788
+ "User.Read", "Files.ReadWrite.All", "Sites.Read.All"
789
+ ])
790
+
791
+ tenant_id = self.credentials.get('tenant_id', self._default_tenant_id)
792
+ client_id = self.credentials.get("client_id", self._default_client_id)
793
+ client_secret = self.credentials.get("client_secret", self._default_client_secret)
794
+ authority = f"https://login.microsoftonline.com/{tenant_id}"
795
+
796
+ # Prepare cache, load from Redis if present
797
+ cache = SerializableTokenCache()
798
+ await self._load_token_cache(cache)
799
+
800
+ result: Optional[Dict[str, Any]] = None
801
+ active_app: Optional[ClientApplication] = None
802
+
803
+ confidential_app: Optional[msal.ConfidentialClientApplication] = None
804
+ if client_secret:
805
+ confidential_app = msal.ConfidentialClientApplication(
806
+ client_id=client_id,
807
+ client_credential=client_secret,
808
+ authority=authority,
809
+ token_cache=cache
810
+ )
811
+
812
+ if accounts := confidential_app.get_accounts():
813
+ result = confidential_app.acquire_token_silent(scopes, account=accounts[0])
814
+ if result and "access_token" in result:
815
+ active_app = confidential_app
816
+
817
+ public_app = PublicClientApplication(
818
+ client_id=client_id,
819
+ authority=authority,
820
+ token_cache=cache
821
+ )
822
+
823
+ if (not result) or ("access_token" not in result):
824
+ if accounts := public_app.get_accounts():
825
+ result = public_app.acquire_token_silent(scopes, account=accounts[0])
826
+ if result and "access_token" in result:
827
+ active_app = public_app
828
+
829
+ if (not result) or ("access_token" not in result):
830
+ if client_secret:
831
+ self.logger.info("Using device code flow (public client)")
832
+ flow = public_app.initiate_device_flow(scopes=scopes)
833
+
834
+ if "user_code" not in flow:
835
+ raise ValueError(
836
+ f"Failed to create device flow: {flow.get('error_description')}"
837
+ )
838
+
839
+ if device_flow_callback:
840
+ try:
841
+ device_flow_callback(flow)
842
+ except Exception as callback_error: # pragma: no cover - defensive
843
+ self.logger.warning(
844
+ "Device flow callback raised an exception: %s",
845
+ callback_error
846
+ )
847
+ else:
848
+ print("\n" + "=" * 60)
849
+ print(flow["message"])
850
+ print("=" * 60 + "\n")
851
+
852
+ loop = asyncio.get_running_loop()
853
+ result = await loop.run_in_executor(
854
+ None, lambda: public_app.acquire_token_by_device_flow(flow)
855
+ )
856
+ active_app = public_app
857
+ else:
858
+ self.logger.info("Starting interactive browser authentication (public client)")
859
+
860
+ interactive_kwargs: Dict[str, Any] = {"prompt": "select_account"}
861
+ if redirect_uri:
862
+ parsed_uri = urlparse(redirect_uri)
863
+ if parsed_uri.port:
864
+ interactive_kwargs["port"] = parsed_uri.port
865
+ interactive_kwargs["redirect_uri"] = redirect_uri
866
+
867
+ if not open_browser:
868
+ def _log_login_url(url: str) -> bool:
869
+ self.logger.info("Interactive authentication URL: %s", url)
870
+ if login_callback:
871
+ try:
872
+ callback_result = login_callback(url)
873
+ if callback_result is not None:
874
+ return bool(callback_result)
875
+ except Exception as callback_error: # pragma: no cover - defensive
876
+ self.logger.warning(
877
+ "Login callback raised an exception: %s",
878
+ callback_error
879
+ )
880
+ return False
881
+
882
+ interactive_kwargs["on_before_launching_ui"] = _log_login_url
883
+
884
+ loop = asyncio.get_running_loop()
885
+ result = await loop.run_in_executor(
886
+ None,
887
+ lambda: public_app.acquire_token_interactive(
888
+ scopes=scopes,
889
+ **interactive_kwargs
890
+ )
891
+ )
892
+ active_app = public_app
893
+
894
+ if "access_token" not in result:
895
+ error_desc = result.get('error_description', 'Unknown error')
896
+ error_code = result.get('error', 'Unknown error code')
897
+ raise RuntimeError(
898
+ f"Interactive login failed: {error_code} - {error_desc}"
899
+ )
900
+
901
+ self.token = result['access_token']
902
+
903
+ # Persist cache to Redis so future runs can refresh silently
904
+ await self._save_token_cache(cache)
905
+
906
+ # Build a TokenCredential backed by MSAL cache for GraphServiceClient
907
+ account = None
908
+ if active_app:
909
+ accounts = active_app.get_accounts()
910
+ account = accounts[0] if accounts else None
911
+
912
+ self._credential = MSALCacheTokenCredential(
913
+ app=active_app or public_app,
914
+ scopes=scopes,
915
+ account=account,
916
+ logger=self.logger
917
+ )
918
+ self._graph_client = self._create_graph_client(scopes=scopes)
919
+
920
+ self.logger.info("✅ Interactive login complete; tokens will refresh silently from cache")
921
+ return result
922
+
923
+ async def ensure_interactive_session(self, scopes: Optional[List[str]] = None):
924
+ """
925
+ Ensure an interactive session (with cached refresh tokens) exists.
926
+ Supports both public and confidential clients.
927
+ """
928
+ scopes = self._filter_reserved_scopes(scopes or [
929
+ "User.Read", "Files.ReadWrite.All", "Sites.Read.All"
930
+ ])
931
+
932
+ tenant_id = self.credentials.get('tenant_id', self._default_tenant_id)
933
+ client_id = self.credentials.get("client_id", self._default_client_id)
934
+ client_secret = self.credentials.get("client_secret", self._default_client_secret)
935
+ authority = f"https://login.microsoftonline.com/{tenant_id}"
936
+
937
+ cache = SerializableTokenCache()
938
+ await self._load_token_cache(cache)
939
+
940
+ # Choose app type based on whether we have a secret
941
+ if client_secret:
942
+ app = msal.ConfidentialClientApplication(
943
+ client_id=client_id,
944
+ client_credential=client_secret,
945
+ authority=authority,
946
+ token_cache=cache
947
+ )
948
+ else:
949
+ app = PublicClientApplication(
950
+ client_id=client_id,
951
+ authority=authority,
952
+ token_cache=cache
953
+ )
954
+
955
+ accounts = app.get_accounts()
956
+ if not accounts:
957
+ raise RuntimeError(
958
+ "No cached session; call interactive_login() first"
959
+ )
960
+
961
+ # Try silent refresh
962
+ result = app.acquire_token_silent(scopes, account=accounts[0])
963
+ if not result or "access_token" not in result:
964
+ raise RuntimeError(
965
+ "Cached session expired; call interactive_login() again"
966
+ )
967
+
968
+ # Build credential and graph client from cache
969
+ self._credential = MSALCacheTokenCredential(
970
+ app=app,
971
+ scopes=scopes,
972
+ account=accounts[0],
973
+ logger=self.logger
974
+ )
975
+ self._graph_client = self._create_graph_client(scopes=scopes)
976
+ self.logger.debug(
977
+ "🔒 Using cached MSAL session (silent refresh enabled)"
978
+ )