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,822 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from typing import List, Optional, Union, Dict, Any
|
|
3
|
+
import contextlib
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from urllib.parse import quote, unquote
|
|
6
|
+
import asyncio
|
|
7
|
+
import aiofiles
|
|
8
|
+
from tqdm import tqdm
|
|
9
|
+
from io import BytesIO
|
|
10
|
+
import httpx
|
|
11
|
+
import aiohttp
|
|
12
|
+
import pandas as pd
|
|
13
|
+
# Microsoft Graph SDK
|
|
14
|
+
from msgraph.generated.models.drive_item import DriveItem
|
|
15
|
+
from msgraph.generated.models.folder import Folder
|
|
16
|
+
from msgraph.generated.models.file import File
|
|
17
|
+
from msgraph.generated.models.upload_session import UploadSession
|
|
18
|
+
from msgraph.generated.drives.item.items.item.create_upload_session.create_upload_session_post_request_body import (
|
|
19
|
+
CreateUploadSessionPostRequestBody
|
|
20
|
+
)
|
|
21
|
+
from msgraph.generated.models.drive_item_uploadable_properties import DriveItemUploadableProperties
|
|
22
|
+
from .o365 import O365Client
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class OneDriveClient(O365Client):
|
|
26
|
+
"""
|
|
27
|
+
OneDrive Client - Migrated to Microsoft Graph SDK
|
|
28
|
+
|
|
29
|
+
Uses Microsoft Graph SDK for all OneDrive operations.
|
|
30
|
+
|
|
31
|
+
Interface for Managing connections to OneDrive resources.
|
|
32
|
+
|
|
33
|
+
Methods:
|
|
34
|
+
file_list: Lists files in a specified OneDrive folder.
|
|
35
|
+
file_search: Searches for files matching a query.
|
|
36
|
+
file_download: Downloads a single file by its item ID.
|
|
37
|
+
download_files: Downloads multiple files provided as a list of dictionaries containing file info.
|
|
38
|
+
folder_download: Downloads a folder and its contents recursively.
|
|
39
|
+
file_delete: Deletes a file or folder by its item ID.
|
|
40
|
+
upload_files: Uploads multiple files to a specified OneDrive folder.
|
|
41
|
+
upload_file: Uploads a single file to OneDrive.
|
|
42
|
+
upload_folder: Uploads a local folder and its contents to OneDrive recursively.
|
|
43
|
+
download_excel_file: Downloads Excel files and optionally converts to pandas DataFrame.
|
|
44
|
+
upload_dataframe_as_excel: Uploads pandas DataFrame as Excel file to OneDrive.
|
|
45
|
+
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
def __init__(self, *args, **kwargs):
|
|
49
|
+
super().__init__(*args, **kwargs)
|
|
50
|
+
# OneDrive-specific properties
|
|
51
|
+
self.directory: Optional[str] = None
|
|
52
|
+
self.filename: Optional[str] = None
|
|
53
|
+
self._srcfiles: List = []
|
|
54
|
+
self._destination: List = []
|
|
55
|
+
|
|
56
|
+
# Upload settings
|
|
57
|
+
self.small_file_threshold = 4 * 1024 * 1024 # 4 MB
|
|
58
|
+
self.chunk_size = 10 * 1024 * 1024 # 10 MB
|
|
59
|
+
|
|
60
|
+
# Cached OneDrive objects
|
|
61
|
+
self._drive_id: Optional[str] = None
|
|
62
|
+
self._drive_info: Optional[DriveItem] = None
|
|
63
|
+
|
|
64
|
+
def connection(self):
|
|
65
|
+
"""
|
|
66
|
+
Establish OneDrive connection using the migrated O365Client.
|
|
67
|
+
|
|
68
|
+
This replaces the old office365-rest-python-client authentication
|
|
69
|
+
with Microsoft Graph SDK authentication.
|
|
70
|
+
"""
|
|
71
|
+
# Use the parent O365Client connection method
|
|
72
|
+
super().connection()
|
|
73
|
+
|
|
74
|
+
self.logger.info("OneDrive connection established successfully")
|
|
75
|
+
return self
|
|
76
|
+
|
|
77
|
+
async def verify_onedrive_access(self):
|
|
78
|
+
"""Verify OneDrive access and cache drive info."""
|
|
79
|
+
try:
|
|
80
|
+
# Resolve and cache drive info
|
|
81
|
+
self._drive_info = await self._resolve_drive()
|
|
82
|
+
self.logger.info(f"OneDrive accessible: {self._drive_info.name or 'Personal OneDrive'}")
|
|
83
|
+
|
|
84
|
+
except Exception as e:
|
|
85
|
+
self.logger.error(f"OneDrive access verification failed: {e}")
|
|
86
|
+
raise RuntimeError(f"OneDrive access verification failed: {e}") from e
|
|
87
|
+
|
|
88
|
+
async def _resolve_drive(self) -> DriveItem:
|
|
89
|
+
"""Resolve OneDrive using Graph API."""
|
|
90
|
+
if self._drive_info:
|
|
91
|
+
return self._drive_info
|
|
92
|
+
|
|
93
|
+
try:
|
|
94
|
+
# Get user's personal OneDrive
|
|
95
|
+
drive = await self.graph_client.me.drive.get()
|
|
96
|
+
|
|
97
|
+
if drive and drive.id:
|
|
98
|
+
self._drive_id = drive.id
|
|
99
|
+
self._drive_info = drive
|
|
100
|
+
self.logger.info(f"OneDrive resolved: {drive.name or drive.id}")
|
|
101
|
+
return drive
|
|
102
|
+
else:
|
|
103
|
+
raise RuntimeError("Could not resolve OneDrive")
|
|
104
|
+
|
|
105
|
+
except Exception as e:
|
|
106
|
+
raise RuntimeError(f"Failed to resolve OneDrive: {e}") from e
|
|
107
|
+
|
|
108
|
+
async def _ensure_folder(self, folder_path: str, create: bool = True) -> DriveItem:
|
|
109
|
+
"""Ensure folder exists in OneDrive using Graph API."""
|
|
110
|
+
drive_info = await self._resolve_drive()
|
|
111
|
+
drive_id = drive_info.id
|
|
112
|
+
|
|
113
|
+
folder_path = (folder_path or "").strip("/")
|
|
114
|
+
if not folder_path:
|
|
115
|
+
# Return root folder
|
|
116
|
+
root = await self.graph_client.drives.by_drive_id(drive_id).root.get()
|
|
117
|
+
return root
|
|
118
|
+
|
|
119
|
+
# Try to resolve existing folder
|
|
120
|
+
try:
|
|
121
|
+
folder_item = await self.graph_client.drives.by_drive_id(drive_id)\
|
|
122
|
+
.items.by_drive_item_id(f"root:/{folder_path}:").get()
|
|
123
|
+
if folder_item:
|
|
124
|
+
return folder_item
|
|
125
|
+
except Exception:
|
|
126
|
+
if not create:
|
|
127
|
+
raise
|
|
128
|
+
|
|
129
|
+
# Create folder recursively
|
|
130
|
+
root = await self.graph_client.drives.by_drive_id(drive_id).root.get()
|
|
131
|
+
parent_id = root.id
|
|
132
|
+
|
|
133
|
+
for segment in [s for s in folder_path.split("/") if s]:
|
|
134
|
+
# Check if segment already exists
|
|
135
|
+
children = await self.graph_client.drives.by_drive_id(drive_id)\
|
|
136
|
+
.items.by_drive_item_id(parent_id).children.get()
|
|
137
|
+
|
|
138
|
+
existing_folder = None
|
|
139
|
+
if children and children.value:
|
|
140
|
+
for child in children.value:
|
|
141
|
+
if child.name == segment and child.folder:
|
|
142
|
+
existing_folder = child
|
|
143
|
+
break
|
|
144
|
+
|
|
145
|
+
if existing_folder:
|
|
146
|
+
parent_id = existing_folder.id
|
|
147
|
+
continue
|
|
148
|
+
|
|
149
|
+
# Create new folder
|
|
150
|
+
new_folder = DriveItem()
|
|
151
|
+
new_folder.name = segment
|
|
152
|
+
new_folder.folder = Folder()
|
|
153
|
+
new_folder.additional_data = {
|
|
154
|
+
"@microsoft.graph.conflictBehavior": "replace"
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
created = await self.graph_client.drives.by_drive_id(drive_id)\
|
|
158
|
+
.items.by_drive_item_id(parent_id).children.post(new_folder)
|
|
159
|
+
parent_id = created.id
|
|
160
|
+
self.logger.info(f"Created folder: {segment}")
|
|
161
|
+
|
|
162
|
+
# Return the final folder
|
|
163
|
+
final_folder = await self.graph_client.drives.by_drive_id(drive_id)\
|
|
164
|
+
.items.by_drive_item_id(parent_id).get()
|
|
165
|
+
return final_folder
|
|
166
|
+
|
|
167
|
+
async def file_list(self, folder_path: str = None) -> List[dict]:
|
|
168
|
+
"""
|
|
169
|
+
List files in a given OneDrive folder using Microsoft Graph API.
|
|
170
|
+
"""
|
|
171
|
+
try:
|
|
172
|
+
drive_info = await self._resolve_drive()
|
|
173
|
+
drive_id = drive_info.id
|
|
174
|
+
|
|
175
|
+
if folder_path:
|
|
176
|
+
# Get specific folder
|
|
177
|
+
folder_path = folder_path.strip("/")
|
|
178
|
+
try:
|
|
179
|
+
folder_item = await self.graph_client.drives.by_drive_id(drive_id)\
|
|
180
|
+
.items.by_drive_item_id(f"root:/{folder_path}:").get()
|
|
181
|
+
except Exception as e:
|
|
182
|
+
raise RuntimeError(f"Folder '{folder_path}' not found: {e}") from e
|
|
183
|
+
else:
|
|
184
|
+
# Get root folder
|
|
185
|
+
folder_item = await self.graph_client.drives.by_drive_id(drive_id).root.get()
|
|
186
|
+
|
|
187
|
+
# Get children
|
|
188
|
+
children = await self.graph_client.drives.by_drive_id(drive_id)\
|
|
189
|
+
.items.by_drive_item_id(folder_item.id).children.get()
|
|
190
|
+
|
|
191
|
+
file_list = []
|
|
192
|
+
if children and children.value:
|
|
193
|
+
for item in children.value:
|
|
194
|
+
file_info = {
|
|
195
|
+
"name": item.name,
|
|
196
|
+
"id": item.id,
|
|
197
|
+
"webUrl": item.web_url,
|
|
198
|
+
"path": self._get_item_path_from_item(item),
|
|
199
|
+
"isFolder": item.folder is not None,
|
|
200
|
+
"size": item.size or 0,
|
|
201
|
+
"modified": item.last_modified_date_time.isoformat() if item.last_modified_date_time else None
|
|
202
|
+
}
|
|
203
|
+
file_list.append(file_info)
|
|
204
|
+
|
|
205
|
+
return file_list
|
|
206
|
+
|
|
207
|
+
except Exception as err:
|
|
208
|
+
self.logger.error(f"Error listing files: {err}")
|
|
209
|
+
raise RuntimeError(f"Error listing files: {err}") from err
|
|
210
|
+
|
|
211
|
+
async def file_search(self, search_query: str) -> List[dict]:
|
|
212
|
+
"""
|
|
213
|
+
Search for files in OneDrive matching the search query using Microsoft Graph API.
|
|
214
|
+
"""
|
|
215
|
+
try:
|
|
216
|
+
drive_info = await self._resolve_drive()
|
|
217
|
+
drive_id = drive_info.id
|
|
218
|
+
|
|
219
|
+
# Use Graph API search
|
|
220
|
+
search_results = await self.graph_client.drives.by_drive_id(drive_id)\
|
|
221
|
+
.search_with_q(search_query).get()
|
|
222
|
+
|
|
223
|
+
results = []
|
|
224
|
+
if search_results and search_results.value:
|
|
225
|
+
for item in search_results.value:
|
|
226
|
+
if item.file: # Only include files, not folders
|
|
227
|
+
file_info = {
|
|
228
|
+
"name": item.name,
|
|
229
|
+
"id": item.id,
|
|
230
|
+
"webUrl": item.web_url,
|
|
231
|
+
"path": self._get_item_path_from_item(item),
|
|
232
|
+
"isFolder": False,
|
|
233
|
+
"size": item.size or 0,
|
|
234
|
+
"modified": item.last_modified_date_time.isoformat() if item.last_modified_date_time else None # noqa
|
|
235
|
+
}
|
|
236
|
+
results.append(file_info)
|
|
237
|
+
|
|
238
|
+
return results
|
|
239
|
+
|
|
240
|
+
except Exception as err:
|
|
241
|
+
self.logger.error(f"Error searching files: {err}")
|
|
242
|
+
raise RuntimeError(f"Error searching files: {err}") from err
|
|
243
|
+
|
|
244
|
+
async def file_download(self, item_id: str, destination: Path) -> str:
|
|
245
|
+
"""
|
|
246
|
+
Download a file from OneDrive by item ID using Microsoft Graph API.
|
|
247
|
+
"""
|
|
248
|
+
try:
|
|
249
|
+
drive_info = await self._resolve_drive()
|
|
250
|
+
drive_id = drive_info.id
|
|
251
|
+
|
|
252
|
+
# Get item info
|
|
253
|
+
item = await self.graph_client.drives.by_drive_id(drive_id)\
|
|
254
|
+
.items.by_drive_item_id(item_id).get()
|
|
255
|
+
|
|
256
|
+
if not item.file:
|
|
257
|
+
raise RuntimeError(f"Item {item_id} is not a file")
|
|
258
|
+
|
|
259
|
+
self.logger.info(f"Downloading {item.name} to {destination}")
|
|
260
|
+
|
|
261
|
+
# Get download URL if available
|
|
262
|
+
download_url = ""
|
|
263
|
+
try:
|
|
264
|
+
add = getattr(item, "additional_data", {}) or {}
|
|
265
|
+
download_url = add.get("@microsoft.graph.downloadUrl", "") or ""
|
|
266
|
+
except Exception:
|
|
267
|
+
download_url = ""
|
|
268
|
+
|
|
269
|
+
# Ensure destination directory exists
|
|
270
|
+
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
271
|
+
|
|
272
|
+
if download_url:
|
|
273
|
+
# Stream via downloadUrl
|
|
274
|
+
async with httpx.AsyncClient(follow_redirects=True, timeout=None) as client:
|
|
275
|
+
async with client.stream("GET", download_url) as resp:
|
|
276
|
+
resp.raise_for_status()
|
|
277
|
+
async with aiofiles.open(destination, "wb") as f:
|
|
278
|
+
async for chunk in resp.aiter_bytes(1 << 20): # 1 MiB
|
|
279
|
+
await f.write(chunk)
|
|
280
|
+
else:
|
|
281
|
+
# Fallback: GET /content via Graph
|
|
282
|
+
content = await self.graph_client.drives.by_drive_id(drive_id)\
|
|
283
|
+
.items.by_drive_item_id(item_id).content.get()
|
|
284
|
+
async with aiofiles.open(destination, "wb") as f:
|
|
285
|
+
await f.write(content)
|
|
286
|
+
|
|
287
|
+
self.logger.info(f"Downloaded {item.name} successfully")
|
|
288
|
+
return str(destination)
|
|
289
|
+
|
|
290
|
+
except Exception as err:
|
|
291
|
+
self.logger.error(f"Error downloading file {item_id}: {err}")
|
|
292
|
+
raise RuntimeError(f"Error downloading file {item_id}: {err}") from err
|
|
293
|
+
|
|
294
|
+
async def download_files(self, items: List[dict], destination_folder: Path) -> List[str]:
|
|
295
|
+
"""
|
|
296
|
+
Download multiple files from OneDrive using Microsoft Graph API.
|
|
297
|
+
"""
|
|
298
|
+
downloaded_files = []
|
|
299
|
+
destination_folder = Path(destination_folder)
|
|
300
|
+
destination_folder.mkdir(parents=True, exist_ok=True)
|
|
301
|
+
|
|
302
|
+
for item in items:
|
|
303
|
+
try:
|
|
304
|
+
item_id = item.get("id")
|
|
305
|
+
file_name = item.get("name")
|
|
306
|
+
if not item_id or not file_name:
|
|
307
|
+
self.logger.warning(f"Skipping invalid item: {item}")
|
|
308
|
+
continue
|
|
309
|
+
|
|
310
|
+
destination = destination_folder / file_name
|
|
311
|
+
downloaded_path = await self.file_download(item_id, destination)
|
|
312
|
+
downloaded_files.append(downloaded_path)
|
|
313
|
+
|
|
314
|
+
except Exception as e:
|
|
315
|
+
self.logger.error(f"Failed to download {item.get('name', 'unknown')}: {e}")
|
|
316
|
+
continue
|
|
317
|
+
|
|
318
|
+
return downloaded_files
|
|
319
|
+
|
|
320
|
+
async def folder_download(self, folder_id: str, destination_folder: Path) -> bool:
|
|
321
|
+
"""
|
|
322
|
+
Download a folder and its contents from OneDrive using Microsoft Graph API.
|
|
323
|
+
"""
|
|
324
|
+
try:
|
|
325
|
+
drive_info = await self._resolve_drive()
|
|
326
|
+
drive_id = drive_info.id
|
|
327
|
+
|
|
328
|
+
# Get folder info
|
|
329
|
+
folder_item = await self.graph_client.drives.by_drive_id(drive_id)\
|
|
330
|
+
.items.by_drive_item_id(folder_id).get()
|
|
331
|
+
|
|
332
|
+
if not folder_item.folder:
|
|
333
|
+
raise RuntimeError(f"Item {folder_id} is not a folder")
|
|
334
|
+
|
|
335
|
+
await self._download_folder_recursive(drive_id, folder_item, destination_folder)
|
|
336
|
+
return True
|
|
337
|
+
|
|
338
|
+
except Exception as err:
|
|
339
|
+
self.logger.error(f"Error downloading folder {folder_id}: {err}")
|
|
340
|
+
raise RuntimeError(f"Error downloading folder {folder_id}: {err}") from err
|
|
341
|
+
|
|
342
|
+
async def _download_folder_recursive(self, drive_id: str, folder_item: DriveItem, local_path: Path):
|
|
343
|
+
"""
|
|
344
|
+
Recursively download a folder's contents using Microsoft Graph API.
|
|
345
|
+
"""
|
|
346
|
+
if not local_path.exists():
|
|
347
|
+
local_path.mkdir(parents=True, exist_ok=True)
|
|
348
|
+
|
|
349
|
+
# Get children
|
|
350
|
+
children = await self.graph_client.drives.by_drive_id(drive_id)\
|
|
351
|
+
.items.by_drive_item_id(folder_item.id).children.get()
|
|
352
|
+
|
|
353
|
+
if children and children.value:
|
|
354
|
+
for item in children.value:
|
|
355
|
+
item_path = local_path / item.name
|
|
356
|
+
|
|
357
|
+
if item.folder:
|
|
358
|
+
# Recursively download subfolder
|
|
359
|
+
await self._download_folder_recursive(drive_id, item, item_path)
|
|
360
|
+
else:
|
|
361
|
+
# Download file
|
|
362
|
+
await self.file_download(item.id, item_path)
|
|
363
|
+
|
|
364
|
+
async def file_delete(self, item_id: str) -> bool:
|
|
365
|
+
"""
|
|
366
|
+
Delete a file or folder in OneDrive by item ID using Microsoft Graph API.
|
|
367
|
+
"""
|
|
368
|
+
try:
|
|
369
|
+
drive_info = await self._resolve_drive()
|
|
370
|
+
drive_id = drive_info.id
|
|
371
|
+
|
|
372
|
+
# Get item info for logging
|
|
373
|
+
try:
|
|
374
|
+
item = await self.graph_client.drives.by_drive_id(drive_id)\
|
|
375
|
+
.items.by_drive_item_id(item_id).get()
|
|
376
|
+
item_name = item.name
|
|
377
|
+
item_type = "folder" if item.folder else "file"
|
|
378
|
+
except Exception:
|
|
379
|
+
item_name = f"item {item_id}"
|
|
380
|
+
item_type = "item"
|
|
381
|
+
|
|
382
|
+
# Delete the item
|
|
383
|
+
await self.graph_client.drives.by_drive_id(drive_id)\
|
|
384
|
+
.items.by_drive_item_id(item_id).delete()
|
|
385
|
+
|
|
386
|
+
self.logger.info(f"Deleted {item_type}: {item_name}")
|
|
387
|
+
return True
|
|
388
|
+
|
|
389
|
+
except Exception as err:
|
|
390
|
+
self.logger.error(f"Error deleting item {item_id}: {err}")
|
|
391
|
+
raise RuntimeError(f"Error deleting item {item_id}: {err}") from err
|
|
392
|
+
|
|
393
|
+
async def upload_file(self, file_path: Path, destination_folder: str = None) -> dict:
|
|
394
|
+
"""
|
|
395
|
+
Upload a single file to OneDrive using Microsoft Graph API.
|
|
396
|
+
"""
|
|
397
|
+
try:
|
|
398
|
+
file_path = Path(file_path)
|
|
399
|
+
if not file_path.exists():
|
|
400
|
+
raise RuntimeError(f"File not found: {file_path}")
|
|
401
|
+
|
|
402
|
+
drive_info = await self._resolve_drive()
|
|
403
|
+
drive_id = drive_info.id
|
|
404
|
+
|
|
405
|
+
# Ensure destination folder exists
|
|
406
|
+
if destination_folder:
|
|
407
|
+
folder_info = await self._ensure_folder(destination_folder, create=True)
|
|
408
|
+
parent_id = folder_info.id
|
|
409
|
+
else:
|
|
410
|
+
root = await self.graph_client.drives.by_drive_id(drive_id).root.get()
|
|
411
|
+
parent_id = root.id
|
|
412
|
+
|
|
413
|
+
file_size = file_path.stat().st_size
|
|
414
|
+
target_name = file_path.name
|
|
415
|
+
|
|
416
|
+
self.logger.info(f"Uploading {target_name} ({file_size:,} bytes)")
|
|
417
|
+
|
|
418
|
+
if file_size <= self.small_file_threshold:
|
|
419
|
+
# Small file upload
|
|
420
|
+
result = await self._upload_small_file(drive_id, parent_id, file_path, target_name)
|
|
421
|
+
else:
|
|
422
|
+
# Large file upload
|
|
423
|
+
upload_session = await self._create_upload_session(drive_id, parent_id, target_name)
|
|
424
|
+
result = await self._upload_large_file(upload_session, file_path)
|
|
425
|
+
|
|
426
|
+
self.logger.info(f"Uploaded successfully: {result.name}")
|
|
427
|
+
|
|
428
|
+
return {
|
|
429
|
+
"name": result.name,
|
|
430
|
+
"id": result.id,
|
|
431
|
+
"webUrl": result.web_url,
|
|
432
|
+
"size": getattr(result, 'size', file_size)
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
except Exception as err:
|
|
436
|
+
self.logger.error(f"Error uploading file {file_path}: {err}")
|
|
437
|
+
raise RuntimeError(f"Error uploading file {file_path}: {err}") from err
|
|
438
|
+
|
|
439
|
+
async def upload_files(self, files: List[Path], destination_folder: str = None) -> List[dict]:
|
|
440
|
+
"""
|
|
441
|
+
Upload multiple files to OneDrive using Microsoft Graph API.
|
|
442
|
+
"""
|
|
443
|
+
uploaded_files = []
|
|
444
|
+
|
|
445
|
+
for file_path in files:
|
|
446
|
+
try:
|
|
447
|
+
uploaded_item = await self.upload_file(file_path, destination_folder)
|
|
448
|
+
uploaded_files.append(uploaded_item)
|
|
449
|
+
except Exception as e:
|
|
450
|
+
self.logger.error(f"Failed to upload {file_path}: {e}")
|
|
451
|
+
continue
|
|
452
|
+
|
|
453
|
+
return uploaded_files
|
|
454
|
+
|
|
455
|
+
async def upload_folder(self, local_folder: Path, destination_folder: str = None) -> List[dict]:
|
|
456
|
+
"""
|
|
457
|
+
Upload a local folder and its contents to OneDrive using Microsoft Graph API.
|
|
458
|
+
"""
|
|
459
|
+
try:
|
|
460
|
+
local_path = Path(local_folder)
|
|
461
|
+
if not local_path.exists() or not local_path.is_dir():
|
|
462
|
+
raise FileNotFoundError(
|
|
463
|
+
f"Local folder does not exist or is not a directory: {local_folder}"
|
|
464
|
+
)
|
|
465
|
+
|
|
466
|
+
uploaded_items = []
|
|
467
|
+
|
|
468
|
+
# Get all files in the folder recursively
|
|
469
|
+
for root, dirs, files in os.walk(local_path):
|
|
470
|
+
relative_path = Path(root).relative_to(local_path)
|
|
471
|
+
|
|
472
|
+
# Calculate OneDrive destination path
|
|
473
|
+
if destination_folder:
|
|
474
|
+
onedrive_path = f"{destination_folder}/{relative_path}".replace("\\", "/").strip("/")
|
|
475
|
+
else:
|
|
476
|
+
onedrive_path = str(relative_path).replace("\\", "/")
|
|
477
|
+
|
|
478
|
+
# Ensure the directory exists in OneDrive
|
|
479
|
+
if onedrive_path and onedrive_path != ".":
|
|
480
|
+
await self._ensure_folder(onedrive_path, create=True)
|
|
481
|
+
|
|
482
|
+
# Upload files in this directory
|
|
483
|
+
for file_name in files:
|
|
484
|
+
file_path = Path(root) / file_name
|
|
485
|
+
try:
|
|
486
|
+
uploaded_item = await self.upload_file(
|
|
487
|
+
file_path,
|
|
488
|
+
onedrive_path if onedrive_path != "." else None
|
|
489
|
+
)
|
|
490
|
+
uploaded_items.append(uploaded_item)
|
|
491
|
+
except Exception as e:
|
|
492
|
+
self.logger.error(f"Failed to upload {file_path}: {e}")
|
|
493
|
+
continue
|
|
494
|
+
|
|
495
|
+
return uploaded_items
|
|
496
|
+
|
|
497
|
+
except Exception as err:
|
|
498
|
+
self.logger.error(f"Error uploading folder {local_folder}: {err}")
|
|
499
|
+
raise RuntimeError(f"Error uploading folder {local_folder}: {err}") from err
|
|
500
|
+
|
|
501
|
+
async def download_excel_file(self, item_id: str, destination: Path = None, as_pandas: bool = False):
|
|
502
|
+
"""
|
|
503
|
+
Download an Excel file from OneDrive by item ID using Microsoft Graph API.
|
|
504
|
+
If `as_pandas` is True, return as a pandas DataFrame.
|
|
505
|
+
If `as_pandas` is False, save to the destination path.
|
|
506
|
+
"""
|
|
507
|
+
try:
|
|
508
|
+
drive_info = await self._resolve_drive()
|
|
509
|
+
drive_id = drive_info.id
|
|
510
|
+
|
|
511
|
+
# Get item info
|
|
512
|
+
item = await self.graph_client.drives.by_drive_id(drive_id)\
|
|
513
|
+
.items.by_drive_item_id(item_id).get()
|
|
514
|
+
|
|
515
|
+
if not item.file:
|
|
516
|
+
raise RuntimeError(f"Item {item_id} is not a file")
|
|
517
|
+
|
|
518
|
+
# Get file content
|
|
519
|
+
content = await self.graph_client.drives.by_drive_id(drive_id)\
|
|
520
|
+
.items.by_drive_item_id(item_id).content.get()
|
|
521
|
+
|
|
522
|
+
if as_pandas:
|
|
523
|
+
bytes_buffer = BytesIO(content)
|
|
524
|
+
return pd.read_excel(bytes_buffer)
|
|
525
|
+
else:
|
|
526
|
+
if not destination:
|
|
527
|
+
raise ValueError("Destination path must be provided when `as_pandas` is False.")
|
|
528
|
+
|
|
529
|
+
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
530
|
+
async with aiofiles.open(destination, "wb") as f:
|
|
531
|
+
await f.write(content)
|
|
532
|
+
return str(destination)
|
|
533
|
+
|
|
534
|
+
except Exception as err:
|
|
535
|
+
self.logger.error(f"Error downloading Excel file {item_id}: {err}")
|
|
536
|
+
raise RuntimeError(f"Error downloading Excel file {item_id}: {err}") from err
|
|
537
|
+
|
|
538
|
+
async def upload_dataframe_as_excel(self, df: pd.DataFrame, file_name: str, destination_folder: str = None) -> dict:
|
|
539
|
+
"""
|
|
540
|
+
Upload a pandas DataFrame as an Excel file to OneDrive using Microsoft Graph API.
|
|
541
|
+
"""
|
|
542
|
+
try:
|
|
543
|
+
# Convert DataFrame to Excel bytes
|
|
544
|
+
output = BytesIO()
|
|
545
|
+
df.to_excel(output, index=False)
|
|
546
|
+
excel_content = output.getvalue()
|
|
547
|
+
|
|
548
|
+
drive_info = await self._resolve_drive()
|
|
549
|
+
drive_id = drive_info.id
|
|
550
|
+
|
|
551
|
+
# Ensure destination folder exists
|
|
552
|
+
if destination_folder:
|
|
553
|
+
folder_info = await self._ensure_folder(destination_folder, create=True)
|
|
554
|
+
parent_id = folder_info.id
|
|
555
|
+
else:
|
|
556
|
+
root = await self.graph_client.drives.by_drive_id(drive_id).root.get()
|
|
557
|
+
parent_id = root.id
|
|
558
|
+
|
|
559
|
+
self.logger.info(f"Uploading DataFrame as Excel: {file_name}")
|
|
560
|
+
|
|
561
|
+
# Upload the Excel content
|
|
562
|
+
if len(excel_content) <= self.small_file_threshold:
|
|
563
|
+
# Small file upload
|
|
564
|
+
encoded_name = quote(file_name)
|
|
565
|
+
request_path = f"{parent_id}:/{encoded_name}:"
|
|
566
|
+
|
|
567
|
+
uploaded_item = await self.graph_client.drives.by_drive_id(drive_id)\
|
|
568
|
+
.items.by_drive_item_id(request_path).content.put(excel_content)
|
|
569
|
+
else:
|
|
570
|
+
# Large file upload (create upload session)
|
|
571
|
+
upload_session = await self._create_upload_session(drive_id, parent_id, file_name)
|
|
572
|
+
uploaded_item = await self._upload_large_file_content(upload_session, excel_content, file_name)
|
|
573
|
+
|
|
574
|
+
self.logger.info(f"Uploaded DataFrame as Excel successfully: {uploaded_item.name}")
|
|
575
|
+
|
|
576
|
+
return {
|
|
577
|
+
"name": uploaded_item.name,
|
|
578
|
+
"id": uploaded_item.id,
|
|
579
|
+
"webUrl": uploaded_item.web_url
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
except Exception as err:
|
|
583
|
+
self.logger.error(f"Error uploading DataFrame as Excel file {file_name}: {err}")
|
|
584
|
+
raise RuntimeError(f"Error uploading DataFrame as Excel file {file_name}: {err}") from err
|
|
585
|
+
|
|
586
|
+
# Helper methods (similar to SharepointClient)
|
|
587
|
+
|
|
588
|
+
async def _upload_small_file(self, drive_id: str, parent_id: str, local_path: Path, target_name: str) -> DriveItem:
|
|
589
|
+
"""Upload small file using Graph API."""
|
|
590
|
+
try:
|
|
591
|
+
async with aiofiles.open(local_path, "rb") as f:
|
|
592
|
+
content = await f.read()
|
|
593
|
+
|
|
594
|
+
# URL encode the target name to handle special characters
|
|
595
|
+
encoded_name = quote(target_name)
|
|
596
|
+
request_path = f"{parent_id}:/{encoded_name}:"
|
|
597
|
+
|
|
598
|
+
return await self.graph_client.drives.by_drive_id(drive_id)\
|
|
599
|
+
.items.by_drive_item_id(request_path).content.put(content)
|
|
600
|
+
|
|
601
|
+
except Exception as e:
|
|
602
|
+
raise RuntimeError(f"Small file upload failed for {target_name}: {e}") from e
|
|
603
|
+
|
|
604
|
+
async def _create_upload_session(self, drive_id: str, parent_id: str, target_name: str) -> UploadSession:
|
|
605
|
+
"""Create upload session for large files using Graph API."""
|
|
606
|
+
try:
|
|
607
|
+
body = CreateUploadSessionPostRequestBody()
|
|
608
|
+
body.item = DriveItemUploadableProperties()
|
|
609
|
+
body.item.additional_data = {"@microsoft.graph.conflictBehavior": "replace"}
|
|
610
|
+
|
|
611
|
+
# URL encode the target name to handle special characters
|
|
612
|
+
encoded_name = quote(target_name)
|
|
613
|
+
|
|
614
|
+
return await self.graph_client.drives.by_drive_id(drive_id)\
|
|
615
|
+
.items.by_drive_item_id(f"{parent_id}:/{encoded_name}:/")\
|
|
616
|
+
.create_upload_session.post(body)
|
|
617
|
+
|
|
618
|
+
except Exception as e:
|
|
619
|
+
raise RuntimeError(f"Upload session creation failed for {target_name}: {e}") from e
|
|
620
|
+
|
|
621
|
+
async def _upload_large_file(self, upload_session: UploadSession, local_path: Union[str, Path]) -> DriveItem:
|
|
622
|
+
"""Upload large file using resumable upload session."""
|
|
623
|
+
file_size = os.path.getsize(local_path)
|
|
624
|
+
uploaded = 0
|
|
625
|
+
|
|
626
|
+
async with aiohttp.ClientSession() as session:
|
|
627
|
+
async with aiofiles.open(local_path, "rb") as f:
|
|
628
|
+
with tqdm(total=file_size, unit='B', unit_scale=True, desc=f'Uploading {Path(local_path).name}') as pbar: # noqa
|
|
629
|
+
while uploaded < file_size:
|
|
630
|
+
chunk = await f.read(self.chunk_size)
|
|
631
|
+
if not chunk:
|
|
632
|
+
break
|
|
633
|
+
|
|
634
|
+
start = uploaded
|
|
635
|
+
end = uploaded + len(chunk) - 1
|
|
636
|
+
|
|
637
|
+
headers = {
|
|
638
|
+
"Content-Length": str(len(chunk)),
|
|
639
|
+
"Content-Range": f"bytes {start}-{end}/{file_size}"
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
async with session.put(
|
|
643
|
+
upload_session.upload_url,
|
|
644
|
+
headers=headers,
|
|
645
|
+
data=chunk
|
|
646
|
+
) as response:
|
|
647
|
+
if response.status in (200, 201):
|
|
648
|
+
# Upload complete
|
|
649
|
+
pbar.update(file_size - uploaded)
|
|
650
|
+
result_data = await response.json()
|
|
651
|
+
|
|
652
|
+
# Convert to DriveItem (simplified)
|
|
653
|
+
drive_item = DriveItem()
|
|
654
|
+
drive_item.name = result_data.get('name')
|
|
655
|
+
drive_item.size = result_data.get('size')
|
|
656
|
+
drive_item.web_url = result_data.get('webUrl')
|
|
657
|
+
drive_item.additional_data = result_data
|
|
658
|
+
|
|
659
|
+
return drive_item
|
|
660
|
+
|
|
661
|
+
elif response.status == 202:
|
|
662
|
+
# Continue uploading
|
|
663
|
+
uploaded = end + 1
|
|
664
|
+
pbar.update(len(chunk))
|
|
665
|
+
|
|
666
|
+
# Check for retry-after header
|
|
667
|
+
if (retry_after := response.headers.get('Retry-After')):
|
|
668
|
+
await asyncio.sleep(int(retry_after))
|
|
669
|
+
continue
|
|
670
|
+
|
|
671
|
+
else:
|
|
672
|
+
error_text = await response.text()
|
|
673
|
+
raise RuntimeError(f"Chunk upload failed: {response.status} {error_text}")
|
|
674
|
+
|
|
675
|
+
raise RuntimeError("Upload session completed without final item response")
|
|
676
|
+
|
|
677
|
+
async def _upload_large_file_content(
|
|
678
|
+
self,
|
|
679
|
+
upload_session: UploadSession,
|
|
680
|
+
content: bytes,
|
|
681
|
+
file_name: str
|
|
682
|
+
) -> DriveItem:
|
|
683
|
+
"""Upload large content using resumable upload session."""
|
|
684
|
+
file_size = len(content)
|
|
685
|
+
uploaded = 0
|
|
686
|
+
|
|
687
|
+
async with aiohttp.ClientSession() as session:
|
|
688
|
+
with tqdm(total=file_size, unit='B', unit_scale=True, desc=f'Uploading {file_name}') as pbar:
|
|
689
|
+
while uploaded < file_size:
|
|
690
|
+
chunk = content[uploaded:uploaded + self.chunk_size]
|
|
691
|
+
if not chunk:
|
|
692
|
+
break
|
|
693
|
+
|
|
694
|
+
start = uploaded
|
|
695
|
+
end = uploaded + len(chunk) - 1
|
|
696
|
+
|
|
697
|
+
headers = {
|
|
698
|
+
"Content-Length": str(len(chunk)),
|
|
699
|
+
"Content-Range": f"bytes {start}-{end}/{file_size}"
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
async with session.put(
|
|
703
|
+
upload_session.upload_url,
|
|
704
|
+
headers=headers,
|
|
705
|
+
data=chunk
|
|
706
|
+
) as response:
|
|
707
|
+
if response.status in (200, 201):
|
|
708
|
+
# Upload complete
|
|
709
|
+
pbar.update(file_size - uploaded)
|
|
710
|
+
result_data = await response.json()
|
|
711
|
+
|
|
712
|
+
# Convert to DriveItem (simplified)
|
|
713
|
+
drive_item = DriveItem()
|
|
714
|
+
drive_item.name = result_data.get('name')
|
|
715
|
+
drive_item.size = result_data.get('size')
|
|
716
|
+
drive_item.web_url = result_data.get('webUrl')
|
|
717
|
+
drive_item.additional_data = result_data
|
|
718
|
+
|
|
719
|
+
return drive_item
|
|
720
|
+
|
|
721
|
+
elif response.status == 202:
|
|
722
|
+
# Continue uploading
|
|
723
|
+
uploaded = end + 1
|
|
724
|
+
pbar.update(len(chunk))
|
|
725
|
+
|
|
726
|
+
# Check for retry-after header
|
|
727
|
+
if (retry_after := response.headers.get('Retry-After')):
|
|
728
|
+
await asyncio.sleep(int(retry_after))
|
|
729
|
+
continue
|
|
730
|
+
|
|
731
|
+
else:
|
|
732
|
+
error_text = await response.text()
|
|
733
|
+
raise RuntimeError(f"Chunk upload failed: {response.status} {error_text}")
|
|
734
|
+
|
|
735
|
+
raise RuntimeError("Upload session completed without final item response")
|
|
736
|
+
|
|
737
|
+
def _get_item_path_from_item(self, item) -> str:
|
|
738
|
+
"""
|
|
739
|
+
Extract the full path from a DriveItem object.
|
|
740
|
+
"""
|
|
741
|
+
try:
|
|
742
|
+
# Try to get path from parent_reference
|
|
743
|
+
if hasattr(item, 'parent_reference') and item.parent_reference and item.parent_reference.path:
|
|
744
|
+
parent_path = item.parent_reference.path or ""
|
|
745
|
+
|
|
746
|
+
# Clean up the parent path
|
|
747
|
+
if parent_path.startswith("/drive/root:"):
|
|
748
|
+
parent_path = parent_path[12:]
|
|
749
|
+
elif parent_path.startswith("/drives/") and "/root:" in parent_path:
|
|
750
|
+
parent_path = parent_path.split("/root:")[-1]
|
|
751
|
+
|
|
752
|
+
# Build the full path
|
|
753
|
+
if parent_path:
|
|
754
|
+
full_path = f"{parent_path}/{item.name}".replace("//", "/").lstrip("/")
|
|
755
|
+
else:
|
|
756
|
+
full_path = item.name or ""
|
|
757
|
+
|
|
758
|
+
return full_path
|
|
759
|
+
|
|
760
|
+
# Fallback: try to get path from web_url if available
|
|
761
|
+
if hasattr(item, 'web_url') and item.web_url:
|
|
762
|
+
try:
|
|
763
|
+
# Extract path from OneDrive web URL
|
|
764
|
+
web_url = item.web_url
|
|
765
|
+
if "/personal/" in web_url and "/_layouts/15/onedrive.aspx" in web_url:
|
|
766
|
+
# Personal OneDrive URL format
|
|
767
|
+
return item.name or ""
|
|
768
|
+
else:
|
|
769
|
+
return unquote(item.name or "")
|
|
770
|
+
except Exception:
|
|
771
|
+
pass
|
|
772
|
+
|
|
773
|
+
# Final fallback: just return the filename
|
|
774
|
+
return item.name or ""
|
|
775
|
+
|
|
776
|
+
except Exception as e:
|
|
777
|
+
self.logger.debug(f"Error extracting path from item: {e}")
|
|
778
|
+
return item.name or ""
|
|
779
|
+
|
|
780
|
+
async def test_permissions(self) -> Dict[str, Any]:
|
|
781
|
+
"""
|
|
782
|
+
Test OneDrive permissions using Microsoft Graph API.
|
|
783
|
+
"""
|
|
784
|
+
results = {
|
|
785
|
+
"drive_access": False,
|
|
786
|
+
"folder_access": False,
|
|
787
|
+
"upload_access": False,
|
|
788
|
+
"errors": []
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
try:
|
|
792
|
+
# Test 1: Drive access
|
|
793
|
+
drive_info = await self._resolve_drive()
|
|
794
|
+
results["drive_access"] = True
|
|
795
|
+
self.logger.info(f"Drive access: {drive_info.name or 'Personal OneDrive'}")
|
|
796
|
+
|
|
797
|
+
# Test 2: Folder access (list root)
|
|
798
|
+
file_list = await self.file_list()
|
|
799
|
+
results["folder_access"] = True
|
|
800
|
+
self.logger.info(f"Folder access: Listed {len(file_list)} items")
|
|
801
|
+
|
|
802
|
+
# Test 3: Folder creation (upload capability test)
|
|
803
|
+
test_folder = await self._ensure_folder("test-folder-permissions", create=True)
|
|
804
|
+
results["upload_access"] = True
|
|
805
|
+
self.logger.info("Upload permissions confirmed")
|
|
806
|
+
|
|
807
|
+
# Clean up test folder
|
|
808
|
+
with contextlib.suppress(Exception):
|
|
809
|
+
await self.file_delete(test_folder.id)
|
|
810
|
+
self.logger.info("Test folder cleaned up")
|
|
811
|
+
|
|
812
|
+
except Exception as e:
|
|
813
|
+
results["errors"].append(str(e))
|
|
814
|
+
self.logger.error(f"Permission test failed: {e}")
|
|
815
|
+
|
|
816
|
+
return results
|
|
817
|
+
|
|
818
|
+
async def close(self):
|
|
819
|
+
"""Clean up resources."""
|
|
820
|
+
await super().close()
|
|
821
|
+
self._drive_info = None
|
|
822
|
+
self._drive_id = None
|