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.
- agentui/.prettierrc +15 -0
- agentui/QUICKSTART.md +272 -0
- agentui/README.md +59 -0
- agentui/env.example +16 -0
- agentui/jsconfig.json +14 -0
- agentui/package-lock.json +4242 -0
- agentui/package.json +34 -0
- agentui/scripts/postinstall/apply-patches.mjs +260 -0
- agentui/src/app.css +61 -0
- agentui/src/app.d.ts +13 -0
- agentui/src/app.html +12 -0
- agentui/src/components/LoadingSpinner.svelte +64 -0
- agentui/src/components/ThemeSwitcher.svelte +159 -0
- agentui/src/components/index.js +4 -0
- agentui/src/lib/api/bots.ts +60 -0
- agentui/src/lib/api/chat.ts +22 -0
- agentui/src/lib/api/http.ts +25 -0
- agentui/src/lib/components/BotCard.svelte +33 -0
- agentui/src/lib/components/ChatBubble.svelte +63 -0
- agentui/src/lib/components/Toast.svelte +21 -0
- agentui/src/lib/config.ts +20 -0
- agentui/src/lib/stores/auth.svelte.ts +73 -0
- agentui/src/lib/stores/theme.svelte.js +64 -0
- agentui/src/lib/stores/toast.svelte.ts +31 -0
- agentui/src/lib/utils/conversation.ts +39 -0
- agentui/src/routes/+layout.svelte +20 -0
- agentui/src/routes/+page.svelte +232 -0
- agentui/src/routes/login/+page.svelte +200 -0
- agentui/src/routes/talk/[agentId]/+page.svelte +297 -0
- agentui/src/routes/talk/[agentId]/+page.ts +7 -0
- agentui/static/README.md +1 -0
- agentui/svelte.config.js +11 -0
- agentui/tailwind.config.ts +53 -0
- agentui/tsconfig.json +3 -0
- agentui/vite.config.ts +10 -0
- ai_parrot-0.17.2.dist-info/METADATA +472 -0
- ai_parrot-0.17.2.dist-info/RECORD +535 -0
- ai_parrot-0.17.2.dist-info/WHEEL +6 -0
- ai_parrot-0.17.2.dist-info/entry_points.txt +2 -0
- ai_parrot-0.17.2.dist-info/licenses/LICENSE +21 -0
- ai_parrot-0.17.2.dist-info/top_level.txt +6 -0
- crew-builder/.prettierrc +15 -0
- crew-builder/QUICKSTART.md +259 -0
- crew-builder/README.md +113 -0
- crew-builder/env.example +17 -0
- crew-builder/jsconfig.json +14 -0
- crew-builder/package-lock.json +4182 -0
- crew-builder/package.json +37 -0
- crew-builder/scripts/postinstall/apply-patches.mjs +260 -0
- crew-builder/src/app.css +62 -0
- crew-builder/src/app.d.ts +13 -0
- crew-builder/src/app.html +12 -0
- crew-builder/src/components/LoadingSpinner.svelte +64 -0
- crew-builder/src/components/ThemeSwitcher.svelte +149 -0
- crew-builder/src/components/index.js +9 -0
- crew-builder/src/lib/api/bots.ts +60 -0
- crew-builder/src/lib/api/chat.ts +80 -0
- crew-builder/src/lib/api/client.ts +56 -0
- crew-builder/src/lib/api/crew/crew.ts +136 -0
- crew-builder/src/lib/api/index.ts +5 -0
- crew-builder/src/lib/api/o365/auth.ts +65 -0
- crew-builder/src/lib/auth/auth.ts +54 -0
- crew-builder/src/lib/components/AgentNode.svelte +43 -0
- crew-builder/src/lib/components/BotCard.svelte +33 -0
- crew-builder/src/lib/components/ChatBubble.svelte +67 -0
- crew-builder/src/lib/components/ConfigPanel.svelte +278 -0
- crew-builder/src/lib/components/JsonTreeNode.svelte +76 -0
- crew-builder/src/lib/components/JsonViewer.svelte +24 -0
- crew-builder/src/lib/components/MarkdownEditor.svelte +48 -0
- crew-builder/src/lib/components/ThemeToggle.svelte +36 -0
- crew-builder/src/lib/components/Toast.svelte +67 -0
- crew-builder/src/lib/components/Toolbar.svelte +157 -0
- crew-builder/src/lib/components/index.ts +10 -0
- crew-builder/src/lib/config.ts +8 -0
- crew-builder/src/lib/stores/auth.svelte.ts +228 -0
- crew-builder/src/lib/stores/crewStore.ts +369 -0
- crew-builder/src/lib/stores/theme.svelte.js +145 -0
- crew-builder/src/lib/stores/toast.svelte.ts +69 -0
- crew-builder/src/lib/utils/conversation.ts +39 -0
- crew-builder/src/lib/utils/markdown.ts +122 -0
- crew-builder/src/lib/utils/talkHistory.ts +47 -0
- crew-builder/src/routes/+layout.svelte +20 -0
- crew-builder/src/routes/+page.svelte +539 -0
- crew-builder/src/routes/agents/+page.svelte +247 -0
- crew-builder/src/routes/agents/[agentId]/+page.svelte +288 -0
- crew-builder/src/routes/agents/[agentId]/+page.ts +7 -0
- crew-builder/src/routes/builder/+page.svelte +204 -0
- crew-builder/src/routes/crew/ask/+page.svelte +1052 -0
- crew-builder/src/routes/crew/ask/+page.ts +1 -0
- crew-builder/src/routes/integrations/o365/+page.svelte +304 -0
- crew-builder/src/routes/login/+page.svelte +197 -0
- crew-builder/src/routes/talk/[agentId]/+page.svelte +487 -0
- crew-builder/src/routes/talk/[agentId]/+page.ts +7 -0
- crew-builder/static/README.md +1 -0
- crew-builder/svelte.config.js +11 -0
- crew-builder/tailwind.config.ts +53 -0
- crew-builder/tsconfig.json +3 -0
- crew-builder/vite.config.ts +10 -0
- mcp_servers/calculator_server.py +309 -0
- parrot/__init__.py +27 -0
- parrot/__pycache__/__init__.cpython-310.pyc +0 -0
- parrot/__pycache__/version.cpython-310.pyc +0 -0
- parrot/_version.py +34 -0
- parrot/a2a/__init__.py +48 -0
- parrot/a2a/client.py +658 -0
- parrot/a2a/discovery.py +89 -0
- parrot/a2a/mixin.py +257 -0
- parrot/a2a/models.py +376 -0
- parrot/a2a/server.py +770 -0
- parrot/agents/__init__.py +29 -0
- parrot/bots/__init__.py +12 -0
- parrot/bots/a2a_agent.py +19 -0
- parrot/bots/abstract.py +3139 -0
- parrot/bots/agent.py +1129 -0
- parrot/bots/basic.py +9 -0
- parrot/bots/chatbot.py +669 -0
- parrot/bots/data.py +1618 -0
- parrot/bots/database/__init__.py +5 -0
- parrot/bots/database/abstract.py +3071 -0
- parrot/bots/database/cache.py +286 -0
- parrot/bots/database/models.py +468 -0
- parrot/bots/database/prompts.py +154 -0
- parrot/bots/database/retries.py +98 -0
- parrot/bots/database/router.py +269 -0
- parrot/bots/database/sql.py +41 -0
- parrot/bots/db/__init__.py +6 -0
- parrot/bots/db/abstract.py +556 -0
- parrot/bots/db/bigquery.py +602 -0
- parrot/bots/db/cache.py +85 -0
- parrot/bots/db/documentdb.py +668 -0
- parrot/bots/db/elastic.py +1014 -0
- parrot/bots/db/influx.py +898 -0
- parrot/bots/db/mock.py +96 -0
- parrot/bots/db/multi.py +783 -0
- parrot/bots/db/prompts.py +185 -0
- parrot/bots/db/sql.py +1255 -0
- parrot/bots/db/tools.py +212 -0
- parrot/bots/document.py +680 -0
- parrot/bots/hrbot.py +15 -0
- parrot/bots/kb.py +170 -0
- parrot/bots/mcp.py +36 -0
- parrot/bots/orchestration/README.md +463 -0
- parrot/bots/orchestration/__init__.py +1 -0
- parrot/bots/orchestration/agent.py +155 -0
- parrot/bots/orchestration/crew.py +3330 -0
- parrot/bots/orchestration/fsm.py +1179 -0
- parrot/bots/orchestration/hr.py +434 -0
- parrot/bots/orchestration/storage/__init__.py +4 -0
- parrot/bots/orchestration/storage/memory.py +100 -0
- parrot/bots/orchestration/storage/mixin.py +119 -0
- parrot/bots/orchestration/verify.py +202 -0
- parrot/bots/product.py +204 -0
- parrot/bots/prompts/__init__.py +96 -0
- parrot/bots/prompts/agents.py +155 -0
- parrot/bots/prompts/data.py +216 -0
- parrot/bots/prompts/output_generation.py +8 -0
- parrot/bots/scraper/__init__.py +3 -0
- parrot/bots/scraper/models.py +122 -0
- parrot/bots/scraper/scraper.py +1173 -0
- parrot/bots/scraper/templates.py +115 -0
- parrot/bots/stores/__init__.py +5 -0
- parrot/bots/stores/local.py +172 -0
- parrot/bots/webdev.py +81 -0
- parrot/cli.py +17 -0
- parrot/clients/__init__.py +16 -0
- parrot/clients/base.py +1491 -0
- parrot/clients/claude.py +1191 -0
- parrot/clients/factory.py +129 -0
- parrot/clients/google.py +4567 -0
- parrot/clients/gpt.py +1975 -0
- parrot/clients/grok.py +432 -0
- parrot/clients/groq.py +986 -0
- parrot/clients/hf.py +582 -0
- parrot/clients/models.py +18 -0
- parrot/conf.py +395 -0
- parrot/embeddings/__init__.py +9 -0
- parrot/embeddings/base.py +157 -0
- parrot/embeddings/google.py +98 -0
- parrot/embeddings/huggingface.py +74 -0
- parrot/embeddings/openai.py +84 -0
- parrot/embeddings/processor.py +88 -0
- parrot/exceptions.c +13868 -0
- parrot/exceptions.cpython-310-x86_64-linux-gnu.so +0 -0
- parrot/exceptions.pxd +22 -0
- parrot/exceptions.pxi +15 -0
- parrot/exceptions.pyx +44 -0
- parrot/generators/__init__.py +29 -0
- parrot/generators/base.py +200 -0
- parrot/generators/html.py +293 -0
- parrot/generators/react.py +205 -0
- parrot/generators/streamlit.py +203 -0
- parrot/generators/template.py +105 -0
- parrot/handlers/__init__.py +4 -0
- parrot/handlers/agent.py +861 -0
- parrot/handlers/agents/__init__.py +1 -0
- parrot/handlers/agents/abstract.py +900 -0
- parrot/handlers/bots.py +338 -0
- parrot/handlers/chat.py +915 -0
- parrot/handlers/creation.sql +192 -0
- parrot/handlers/crew/ARCHITECTURE.md +362 -0
- parrot/handlers/crew/README_BOTMANAGER_PERSISTENCE.md +303 -0
- parrot/handlers/crew/README_REDIS_PERSISTENCE.md +366 -0
- parrot/handlers/crew/__init__.py +0 -0
- parrot/handlers/crew/handler.py +801 -0
- parrot/handlers/crew/models.py +229 -0
- parrot/handlers/crew/redis_persistence.py +523 -0
- parrot/handlers/jobs/__init__.py +10 -0
- parrot/handlers/jobs/job.py +384 -0
- parrot/handlers/jobs/mixin.py +627 -0
- parrot/handlers/jobs/models.py +115 -0
- parrot/handlers/jobs/worker.py +31 -0
- parrot/handlers/models.py +596 -0
- parrot/handlers/o365_auth.py +105 -0
- parrot/handlers/stream.py +337 -0
- parrot/interfaces/__init__.py +6 -0
- parrot/interfaces/aws.py +143 -0
- parrot/interfaces/credentials.py +113 -0
- parrot/interfaces/database.py +27 -0
- parrot/interfaces/google.py +1123 -0
- parrot/interfaces/hierarchy.py +1227 -0
- parrot/interfaces/http.py +651 -0
- parrot/interfaces/images/__init__.py +0 -0
- parrot/interfaces/images/plugins/__init__.py +24 -0
- parrot/interfaces/images/plugins/abstract.py +58 -0
- parrot/interfaces/images/plugins/analisys.py +148 -0
- parrot/interfaces/images/plugins/classify.py +150 -0
- parrot/interfaces/images/plugins/classifybase.py +182 -0
- parrot/interfaces/images/plugins/detect.py +150 -0
- parrot/interfaces/images/plugins/exif.py +1103 -0
- parrot/interfaces/images/plugins/hash.py +52 -0
- parrot/interfaces/images/plugins/vision.py +104 -0
- parrot/interfaces/images/plugins/yolo.py +66 -0
- parrot/interfaces/images/plugins/zerodetect.py +197 -0
- parrot/interfaces/o365.py +978 -0
- parrot/interfaces/onedrive.py +822 -0
- parrot/interfaces/sharepoint.py +1435 -0
- parrot/interfaces/soap.py +257 -0
- parrot/loaders/__init__.py +8 -0
- parrot/loaders/abstract.py +1131 -0
- parrot/loaders/audio.py +199 -0
- parrot/loaders/basepdf.py +53 -0
- parrot/loaders/basevideo.py +1568 -0
- parrot/loaders/csv.py +409 -0
- parrot/loaders/docx.py +116 -0
- parrot/loaders/epubloader.py +316 -0
- parrot/loaders/excel.py +199 -0
- parrot/loaders/factory.py +55 -0
- parrot/loaders/files/__init__.py +0 -0
- parrot/loaders/files/abstract.py +39 -0
- parrot/loaders/files/html.py +26 -0
- parrot/loaders/files/text.py +63 -0
- parrot/loaders/html.py +152 -0
- parrot/loaders/markdown.py +442 -0
- parrot/loaders/pdf.py +373 -0
- parrot/loaders/pdfmark.py +320 -0
- parrot/loaders/pdftables.py +506 -0
- parrot/loaders/ppt.py +476 -0
- parrot/loaders/qa.py +63 -0
- parrot/loaders/splitters/__init__.py +10 -0
- parrot/loaders/splitters/base.py +138 -0
- parrot/loaders/splitters/md.py +228 -0
- parrot/loaders/splitters/token.py +143 -0
- parrot/loaders/txt.py +26 -0
- parrot/loaders/video.py +89 -0
- parrot/loaders/videolocal.py +218 -0
- parrot/loaders/videounderstanding.py +377 -0
- parrot/loaders/vimeo.py +167 -0
- parrot/loaders/web.py +599 -0
- parrot/loaders/youtube.py +504 -0
- parrot/manager/__init__.py +5 -0
- parrot/manager/manager.py +1030 -0
- parrot/mcp/__init__.py +28 -0
- parrot/mcp/adapter.py +105 -0
- parrot/mcp/cli.py +174 -0
- parrot/mcp/client.py +119 -0
- parrot/mcp/config.py +75 -0
- parrot/mcp/integration.py +842 -0
- parrot/mcp/oauth.py +933 -0
- parrot/mcp/server.py +225 -0
- parrot/mcp/transports/__init__.py +3 -0
- parrot/mcp/transports/base.py +279 -0
- parrot/mcp/transports/grpc_session.py +163 -0
- parrot/mcp/transports/http.py +312 -0
- parrot/mcp/transports/mcp.proto +108 -0
- parrot/mcp/transports/quic.py +1082 -0
- parrot/mcp/transports/sse.py +330 -0
- parrot/mcp/transports/stdio.py +309 -0
- parrot/mcp/transports/unix.py +395 -0
- parrot/mcp/transports/websocket.py +547 -0
- parrot/memory/__init__.py +16 -0
- parrot/memory/abstract.py +209 -0
- parrot/memory/agent.py +32 -0
- parrot/memory/cache.py +175 -0
- parrot/memory/core.py +555 -0
- parrot/memory/file.py +153 -0
- parrot/memory/mem.py +131 -0
- parrot/memory/redis.py +613 -0
- parrot/models/__init__.py +46 -0
- parrot/models/basic.py +118 -0
- parrot/models/compliance.py +208 -0
- parrot/models/crew.py +395 -0
- parrot/models/detections.py +654 -0
- parrot/models/generation.py +85 -0
- parrot/models/google.py +223 -0
- parrot/models/groq.py +23 -0
- parrot/models/openai.py +30 -0
- parrot/models/outputs.py +285 -0
- parrot/models/responses.py +938 -0
- parrot/notifications/__init__.py +743 -0
- parrot/openapi/__init__.py +3 -0
- parrot/openapi/components.yaml +641 -0
- parrot/openapi/config.py +322 -0
- parrot/outputs/__init__.py +32 -0
- parrot/outputs/formats/__init__.py +108 -0
- parrot/outputs/formats/altair.py +359 -0
- parrot/outputs/formats/application.py +122 -0
- parrot/outputs/formats/base.py +351 -0
- parrot/outputs/formats/bokeh.py +356 -0
- parrot/outputs/formats/card.py +424 -0
- parrot/outputs/formats/chart.py +436 -0
- parrot/outputs/formats/d3.py +255 -0
- parrot/outputs/formats/echarts.py +310 -0
- parrot/outputs/formats/generators/__init__.py +0 -0
- parrot/outputs/formats/generators/abstract.py +61 -0
- parrot/outputs/formats/generators/panel.py +145 -0
- parrot/outputs/formats/generators/streamlit.py +86 -0
- parrot/outputs/formats/generators/terminal.py +63 -0
- parrot/outputs/formats/holoviews.py +310 -0
- parrot/outputs/formats/html.py +147 -0
- parrot/outputs/formats/jinja2.py +46 -0
- parrot/outputs/formats/json.py +87 -0
- parrot/outputs/formats/map.py +933 -0
- parrot/outputs/formats/markdown.py +172 -0
- parrot/outputs/formats/matplotlib.py +237 -0
- parrot/outputs/formats/mixins/__init__.py +0 -0
- parrot/outputs/formats/mixins/emaps.py +855 -0
- parrot/outputs/formats/plotly.py +341 -0
- parrot/outputs/formats/seaborn.py +310 -0
- parrot/outputs/formats/table.py +397 -0
- parrot/outputs/formats/template_report.py +138 -0
- parrot/outputs/formats/yaml.py +125 -0
- parrot/outputs/formatter.py +152 -0
- parrot/outputs/templates/__init__.py +95 -0
- parrot/pipelines/__init__.py +0 -0
- parrot/pipelines/abstract.py +210 -0
- parrot/pipelines/detector.py +124 -0
- parrot/pipelines/models.py +90 -0
- parrot/pipelines/planogram.py +3002 -0
- parrot/pipelines/table.sql +97 -0
- parrot/plugins/__init__.py +106 -0
- parrot/plugins/importer.py +80 -0
- parrot/py.typed +0 -0
- parrot/registry/__init__.py +18 -0
- parrot/registry/registry.py +594 -0
- parrot/scheduler/__init__.py +1189 -0
- parrot/scheduler/models.py +60 -0
- parrot/security/__init__.py +16 -0
- parrot/security/prompt_injection.py +268 -0
- parrot/security/security_events.sql +25 -0
- parrot/services/__init__.py +1 -0
- parrot/services/mcp/__init__.py +8 -0
- parrot/services/mcp/config.py +13 -0
- parrot/services/mcp/server.py +295 -0
- parrot/services/o365_remote_auth.py +235 -0
- parrot/stores/__init__.py +7 -0
- parrot/stores/abstract.py +352 -0
- parrot/stores/arango.py +1090 -0
- parrot/stores/bigquery.py +1377 -0
- parrot/stores/cache.py +106 -0
- parrot/stores/empty.py +10 -0
- parrot/stores/faiss_store.py +1157 -0
- parrot/stores/kb/__init__.py +9 -0
- parrot/stores/kb/abstract.py +68 -0
- parrot/stores/kb/cache.py +165 -0
- parrot/stores/kb/doc.py +325 -0
- parrot/stores/kb/hierarchy.py +346 -0
- parrot/stores/kb/local.py +457 -0
- parrot/stores/kb/prompt.py +28 -0
- parrot/stores/kb/redis.py +659 -0
- parrot/stores/kb/store.py +115 -0
- parrot/stores/kb/user.py +374 -0
- parrot/stores/models.py +59 -0
- parrot/stores/pgvector.py +3 -0
- parrot/stores/postgres.py +2853 -0
- parrot/stores/utils/__init__.py +0 -0
- parrot/stores/utils/chunking.py +197 -0
- parrot/telemetry/__init__.py +3 -0
- parrot/telemetry/mixin.py +111 -0
- parrot/template/__init__.py +3 -0
- parrot/template/engine.py +259 -0
- parrot/tools/__init__.py +23 -0
- parrot/tools/abstract.py +644 -0
- parrot/tools/agent.py +363 -0
- parrot/tools/arangodbsearch.py +537 -0
- parrot/tools/arxiv_tool.py +188 -0
- parrot/tools/calculator/__init__.py +3 -0
- parrot/tools/calculator/operations/__init__.py +38 -0
- parrot/tools/calculator/operations/calculus.py +80 -0
- parrot/tools/calculator/operations/statistics.py +76 -0
- parrot/tools/calculator/tool.py +150 -0
- parrot/tools/cloudwatch.py +988 -0
- parrot/tools/codeinterpreter/__init__.py +127 -0
- parrot/tools/codeinterpreter/executor.py +371 -0
- parrot/tools/codeinterpreter/internals.py +473 -0
- parrot/tools/codeinterpreter/models.py +643 -0
- parrot/tools/codeinterpreter/prompts.py +224 -0
- parrot/tools/codeinterpreter/tool.py +664 -0
- parrot/tools/company_info/__init__.py +6 -0
- parrot/tools/company_info/tool.py +1138 -0
- parrot/tools/correlationanalysis.py +437 -0
- parrot/tools/database/abstract.py +286 -0
- parrot/tools/database/bq.py +115 -0
- parrot/tools/database/cache.py +284 -0
- parrot/tools/database/models.py +95 -0
- parrot/tools/database/pg.py +343 -0
- parrot/tools/databasequery.py +1159 -0
- parrot/tools/db.py +1800 -0
- parrot/tools/ddgo.py +370 -0
- parrot/tools/decorators.py +271 -0
- parrot/tools/dftohtml.py +282 -0
- parrot/tools/document.py +549 -0
- parrot/tools/ecs.py +819 -0
- parrot/tools/edareport.py +368 -0
- parrot/tools/elasticsearch.py +1049 -0
- parrot/tools/employees.py +462 -0
- parrot/tools/epson/__init__.py +96 -0
- parrot/tools/excel.py +683 -0
- parrot/tools/file/__init__.py +13 -0
- parrot/tools/file/abstract.py +76 -0
- parrot/tools/file/gcs.py +378 -0
- parrot/tools/file/local.py +284 -0
- parrot/tools/file/s3.py +511 -0
- parrot/tools/file/tmp.py +309 -0
- parrot/tools/file/tool.py +501 -0
- parrot/tools/file_reader.py +129 -0
- parrot/tools/flowtask/__init__.py +19 -0
- parrot/tools/flowtask/tool.py +761 -0
- parrot/tools/gittoolkit.py +508 -0
- parrot/tools/google/__init__.py +18 -0
- parrot/tools/google/base.py +169 -0
- parrot/tools/google/tools.py +1251 -0
- parrot/tools/googlelocation.py +5 -0
- parrot/tools/googleroutes.py +5 -0
- parrot/tools/googlesearch.py +5 -0
- parrot/tools/googlesitesearch.py +5 -0
- parrot/tools/googlevoice.py +2 -0
- parrot/tools/gvoice.py +695 -0
- parrot/tools/ibisworld/README.md +225 -0
- parrot/tools/ibisworld/__init__.py +11 -0
- parrot/tools/ibisworld/tool.py +366 -0
- parrot/tools/jiratoolkit.py +1718 -0
- parrot/tools/manager.py +1098 -0
- parrot/tools/math.py +152 -0
- parrot/tools/metadata.py +476 -0
- parrot/tools/msteams.py +1621 -0
- parrot/tools/msword.py +635 -0
- parrot/tools/multidb.py +580 -0
- parrot/tools/multistoresearch.py +369 -0
- parrot/tools/networkninja.py +167 -0
- parrot/tools/nextstop/__init__.py +4 -0
- parrot/tools/nextstop/base.py +286 -0
- parrot/tools/nextstop/employee.py +733 -0
- parrot/tools/nextstop/store.py +462 -0
- parrot/tools/notification.py +435 -0
- parrot/tools/o365/__init__.py +42 -0
- parrot/tools/o365/base.py +295 -0
- parrot/tools/o365/bundle.py +522 -0
- parrot/tools/o365/events.py +554 -0
- parrot/tools/o365/mail.py +992 -0
- parrot/tools/o365/onedrive.py +497 -0
- parrot/tools/o365/sharepoint.py +641 -0
- parrot/tools/openapi_toolkit.py +904 -0
- parrot/tools/openweather.py +527 -0
- parrot/tools/pdfprint.py +1001 -0
- parrot/tools/powerbi.py +518 -0
- parrot/tools/powerpoint.py +1113 -0
- parrot/tools/pricestool.py +146 -0
- parrot/tools/products/__init__.py +246 -0
- parrot/tools/prophet_tool.py +171 -0
- parrot/tools/pythonpandas.py +630 -0
- parrot/tools/pythonrepl.py +910 -0
- parrot/tools/qsource.py +436 -0
- parrot/tools/querytoolkit.py +395 -0
- parrot/tools/quickeda.py +827 -0
- parrot/tools/resttool.py +553 -0
- parrot/tools/retail/__init__.py +0 -0
- parrot/tools/retail/bby.py +528 -0
- parrot/tools/sandboxtool.py +703 -0
- parrot/tools/sassie/__init__.py +352 -0
- parrot/tools/scraping/__init__.py +7 -0
- parrot/tools/scraping/docs/select.md +466 -0
- parrot/tools/scraping/documentation.md +1278 -0
- parrot/tools/scraping/driver.py +436 -0
- parrot/tools/scraping/models.py +576 -0
- parrot/tools/scraping/options.py +85 -0
- parrot/tools/scraping/orchestrator.py +517 -0
- parrot/tools/scraping/readme.md +740 -0
- parrot/tools/scraping/tool.py +3115 -0
- parrot/tools/seasonaldetection.py +642 -0
- parrot/tools/shell_tool/__init__.py +5 -0
- parrot/tools/shell_tool/actions.py +408 -0
- parrot/tools/shell_tool/engine.py +155 -0
- parrot/tools/shell_tool/models.py +322 -0
- parrot/tools/shell_tool/tool.py +442 -0
- parrot/tools/site_search.py +214 -0
- parrot/tools/textfile.py +418 -0
- parrot/tools/think.py +378 -0
- parrot/tools/toolkit.py +298 -0
- parrot/tools/webapp_tool.py +187 -0
- parrot/tools/whatif.py +1279 -0
- parrot/tools/workday/MULTI_WSDL_EXAMPLE.md +249 -0
- parrot/tools/workday/__init__.py +6 -0
- parrot/tools/workday/models.py +1389 -0
- parrot/tools/workday/tool.py +1293 -0
- parrot/tools/yfinance_tool.py +306 -0
- parrot/tools/zipcode.py +217 -0
- parrot/utils/__init__.py +2 -0
- parrot/utils/helpers.py +73 -0
- parrot/utils/parsers/__init__.py +5 -0
- parrot/utils/parsers/toml.c +12078 -0
- parrot/utils/parsers/toml.cpython-310-x86_64-linux-gnu.so +0 -0
- parrot/utils/parsers/toml.pyx +21 -0
- parrot/utils/toml.py +11 -0
- parrot/utils/types.cpp +20936 -0
- parrot/utils/types.cpython-310-x86_64-linux-gnu.so +0 -0
- parrot/utils/types.pyx +213 -0
- parrot/utils/uv.py +11 -0
- parrot/version.py +10 -0
- parrot/yaml-rs/Cargo.lock +350 -0
- parrot/yaml-rs/Cargo.toml +19 -0
- parrot/yaml-rs/pyproject.toml +19 -0
- parrot/yaml-rs/python/yaml_rs/__init__.py +81 -0
- parrot/yaml-rs/src/lib.rs +222 -0
- requirements/docker-compose.yml +24 -0
- 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
|
+
)
|