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
parrot/tools/file/s3.py
ADDED
|
@@ -0,0 +1,511 @@
|
|
|
1
|
+
from typing import BinaryIO, Optional, List, Union
|
|
2
|
+
import asyncio
|
|
3
|
+
import contextlib
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
import mimetypes
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from io import BytesIO, StringIO
|
|
8
|
+
import fnmatch
|
|
9
|
+
import aioboto3
|
|
10
|
+
from botocore.exceptions import ClientError
|
|
11
|
+
from .abstract import FileManagerInterface, FileMetadata
|
|
12
|
+
from ...conf import AWS_CREDENTIALS
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class S3FileManager(FileManagerInterface):
|
|
16
|
+
"""File manager for AWS S3 bucket operations"""
|
|
17
|
+
|
|
18
|
+
# Multipart upload configuration
|
|
19
|
+
MULTIPART_THRESHOLD = 100 * 1024 * 1024 # 100MB
|
|
20
|
+
MULTIPART_CHUNKSIZE = 10 * 1024 * 1024 # 10MB per part
|
|
21
|
+
MAX_CONCURRENCY = 10 # Concurrent part uploads
|
|
22
|
+
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
bucket_name: Optional[str] = None,
|
|
26
|
+
aws_id: str = 'default',
|
|
27
|
+
region_name: Optional[str] = None,
|
|
28
|
+
prefix: str = "",
|
|
29
|
+
multipart_threshold: Optional[int] = None,
|
|
30
|
+
multipart_chunksize: Optional[int] = None,
|
|
31
|
+
max_concurrency: Optional[int] = None,
|
|
32
|
+
**kwargs
|
|
33
|
+
):
|
|
34
|
+
"""
|
|
35
|
+
Initialize S3 file manager.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
bucket_name: S3 bucket name (if None, read from credentials)
|
|
39
|
+
aws_id: Identifier for credentials in AWS_CREDENTIALS dict
|
|
40
|
+
region_name: AWS region (if None, read from credentials)
|
|
41
|
+
prefix: Default prefix/folder for all operations
|
|
42
|
+
multipart_threshold: File size threshold for multipart upload (bytes)
|
|
43
|
+
multipart_chunksize: Size of each part in multipart upload (bytes)
|
|
44
|
+
max_concurrency: Max concurrent part uploads
|
|
45
|
+
**kwargs: Additional arguments, including 'credentials' override
|
|
46
|
+
"""
|
|
47
|
+
# Get credentials from config or kwargs
|
|
48
|
+
credentials = kwargs.get('credentials', AWS_CREDENTIALS.get(aws_id, 'default'))
|
|
49
|
+
|
|
50
|
+
if isinstance(credentials, str) and credentials == 'default':
|
|
51
|
+
credentials = AWS_CREDENTIALS.get('default', {})
|
|
52
|
+
|
|
53
|
+
# Extract bucket_name from credentials if not provided
|
|
54
|
+
self.bucket_name = bucket_name or credentials.get('bucket_name')
|
|
55
|
+
if not self.bucket_name:
|
|
56
|
+
raise ValueError("bucket_name must be provided or present in credentials")
|
|
57
|
+
|
|
58
|
+
# Build AWS config
|
|
59
|
+
self.aws_config = {
|
|
60
|
+
'aws_access_key_id': credentials.get('aws_access_key_id'),
|
|
61
|
+
'aws_secret_access_key': credentials.get('aws_secret_access_key'),
|
|
62
|
+
'region_name': region_name or credentials.get('region_name', 'us-east-1'),
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
# Add optional session token if present
|
|
66
|
+
if 'aws_session_token' in credentials:
|
|
67
|
+
self.aws_config['aws_session_token'] = credentials['aws_session_token']
|
|
68
|
+
|
|
69
|
+
# Remove None values
|
|
70
|
+
self.aws_config = {k: v for k, v in self.aws_config.items() if v is not None}
|
|
71
|
+
|
|
72
|
+
self.prefix = prefix.rstrip('/') + '/' if prefix else ''
|
|
73
|
+
self.session = aioboto3.Session(**self.aws_config)
|
|
74
|
+
|
|
75
|
+
# Multipart upload configuration
|
|
76
|
+
self.multipart_threshold = multipart_threshold or self.MULTIPART_THRESHOLD
|
|
77
|
+
self.multipart_chunksize = multipart_chunksize or self.MULTIPART_CHUNKSIZE
|
|
78
|
+
self.max_concurrency = max_concurrency or self.MAX_CONCURRENCY
|
|
79
|
+
|
|
80
|
+
def _resolve_path(self, path: str) -> str:
|
|
81
|
+
"""Resolve path with prefix"""
|
|
82
|
+
path = path.lstrip('/')
|
|
83
|
+
|
|
84
|
+
if self.prefix and not path.startswith(self.prefix):
|
|
85
|
+
path = self.prefix + path
|
|
86
|
+
|
|
87
|
+
return path
|
|
88
|
+
|
|
89
|
+
def _strip_prefix(self, path: str) -> str:
|
|
90
|
+
"""Remove prefix from path for display"""
|
|
91
|
+
if self.prefix and path.startswith(self.prefix):
|
|
92
|
+
return path[len(self.prefix):]
|
|
93
|
+
return path
|
|
94
|
+
|
|
95
|
+
async def _get_object_metadata(self, key: str) -> FileMetadata:
|
|
96
|
+
"""Get metadata for an S3 object"""
|
|
97
|
+
async with self.session.client('s3') as s3:
|
|
98
|
+
try:
|
|
99
|
+
response = await s3.head_object(Bucket=self.bucket_name, Key=key)
|
|
100
|
+
|
|
101
|
+
url = await s3.generate_presigned_url(
|
|
102
|
+
'get_object',
|
|
103
|
+
Params={'Bucket': self.bucket_name, 'Key': key},
|
|
104
|
+
ExpiresIn=3600
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
return FileMetadata(
|
|
108
|
+
name=Path(key).name,
|
|
109
|
+
path=self._strip_prefix(key),
|
|
110
|
+
size=response['ContentLength'],
|
|
111
|
+
content_type=response.get('ContentType'),
|
|
112
|
+
modified_at=response['LastModified'],
|
|
113
|
+
url=url
|
|
114
|
+
)
|
|
115
|
+
except ClientError as e:
|
|
116
|
+
if e.response['Error']['Code'] == '404':
|
|
117
|
+
raise FileNotFoundError(
|
|
118
|
+
f"File not found: {key}"
|
|
119
|
+
) from e
|
|
120
|
+
raise
|
|
121
|
+
|
|
122
|
+
async def _upload_part(
|
|
123
|
+
self,
|
|
124
|
+
s3_client,
|
|
125
|
+
upload_id: str,
|
|
126
|
+
key: str,
|
|
127
|
+
part_number: int,
|
|
128
|
+
data: bytes
|
|
129
|
+
) -> dict:
|
|
130
|
+
"""Upload a single part in multipart upload"""
|
|
131
|
+
response = await s3_client.upload_part(
|
|
132
|
+
Bucket=self.bucket_name,
|
|
133
|
+
Key=key,
|
|
134
|
+
UploadId=upload_id,
|
|
135
|
+
PartNumber=part_number,
|
|
136
|
+
Body=data
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
'PartNumber': part_number,
|
|
141
|
+
'ETag': response['ETag']
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async def _multipart_upload(
|
|
145
|
+
self,
|
|
146
|
+
source: Path | BinaryIO,
|
|
147
|
+
key: str,
|
|
148
|
+
content_type: Optional[str]
|
|
149
|
+
) -> FileMetadata:
|
|
150
|
+
"""Perform multipart upload for large files"""
|
|
151
|
+
async with self.session.client('s3') as s3:
|
|
152
|
+
# Initiate multipart upload
|
|
153
|
+
extra_args = {}
|
|
154
|
+
if content_type:
|
|
155
|
+
extra_args['ContentType'] = content_type
|
|
156
|
+
|
|
157
|
+
response = await s3.create_multipart_upload(
|
|
158
|
+
Bucket=self.bucket_name,
|
|
159
|
+
Key=key,
|
|
160
|
+
**extra_args
|
|
161
|
+
)
|
|
162
|
+
upload_id = response['UploadId']
|
|
163
|
+
|
|
164
|
+
parts = []
|
|
165
|
+
part_number = 1
|
|
166
|
+
|
|
167
|
+
try:
|
|
168
|
+
# Open file for reading
|
|
169
|
+
if isinstance(source, Path):
|
|
170
|
+
file_obj = open(source, 'rb')
|
|
171
|
+
should_close = True
|
|
172
|
+
else:
|
|
173
|
+
source.seek(0)
|
|
174
|
+
file_obj = source
|
|
175
|
+
should_close = False
|
|
176
|
+
|
|
177
|
+
# Create upload tasks with concurrency limit
|
|
178
|
+
semaphore = asyncio.Semaphore(self.max_concurrency)
|
|
179
|
+
upload_tasks = []
|
|
180
|
+
|
|
181
|
+
async def upload_with_semaphore(part_num: int, chunk: bytes):
|
|
182
|
+
async with semaphore:
|
|
183
|
+
return await self._upload_part(
|
|
184
|
+
s3, upload_id, key, part_num, chunk
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
# Read and upload chunks
|
|
188
|
+
while True:
|
|
189
|
+
chunk = file_obj.read(self.multipart_chunksize)
|
|
190
|
+
if not chunk:
|
|
191
|
+
break
|
|
192
|
+
|
|
193
|
+
task = asyncio.create_task(
|
|
194
|
+
upload_with_semaphore(part_number, chunk)
|
|
195
|
+
)
|
|
196
|
+
upload_tasks.append(task)
|
|
197
|
+
part_number += 1
|
|
198
|
+
|
|
199
|
+
# Wait for all uploads to complete
|
|
200
|
+
parts = await asyncio.gather(*upload_tasks)
|
|
201
|
+
|
|
202
|
+
# Close file if we opened it
|
|
203
|
+
if should_close:
|
|
204
|
+
file_obj.close()
|
|
205
|
+
|
|
206
|
+
# Complete multipart upload
|
|
207
|
+
await s3.complete_multipart_upload(
|
|
208
|
+
Bucket=self.bucket_name,
|
|
209
|
+
Key=key,
|
|
210
|
+
UploadId=upload_id,
|
|
211
|
+
MultipartUpload={'Parts': parts}
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
return await self._get_object_metadata(key)
|
|
215
|
+
|
|
216
|
+
except Exception as e:
|
|
217
|
+
# Abort multipart upload on error
|
|
218
|
+
with contextlib.suppress(Exception):
|
|
219
|
+
await s3.abort_multipart_upload(
|
|
220
|
+
Bucket=self.bucket_name,
|
|
221
|
+
Key=key,
|
|
222
|
+
UploadId=upload_id
|
|
223
|
+
)
|
|
224
|
+
raise IOError(f"Multipart upload failed: {e}")
|
|
225
|
+
|
|
226
|
+
async def upload_file(
|
|
227
|
+
self,
|
|
228
|
+
source: BinaryIO | Path,
|
|
229
|
+
destination: str
|
|
230
|
+
) -> FileMetadata:
|
|
231
|
+
"""
|
|
232
|
+
Upload file to S3 with automatic multipart upload for large files.
|
|
233
|
+
|
|
234
|
+
Files larger than multipart_threshold will use multipart upload
|
|
235
|
+
with concurrent part uploads for better performance.
|
|
236
|
+
"""
|
|
237
|
+
key = self._resolve_path(destination)
|
|
238
|
+
|
|
239
|
+
# Guess content type
|
|
240
|
+
content_type, _ = mimetypes.guess_type(destination)
|
|
241
|
+
extra_args = {}
|
|
242
|
+
if content_type:
|
|
243
|
+
extra_args['ContentType'] = content_type
|
|
244
|
+
|
|
245
|
+
# Determine file size
|
|
246
|
+
if isinstance(source, Path):
|
|
247
|
+
file_size = source.stat().st_size
|
|
248
|
+
else:
|
|
249
|
+
# For file objects, try to get size
|
|
250
|
+
current_pos = source.tell()
|
|
251
|
+
source.seek(0, 2) # Seek to end
|
|
252
|
+
file_size = source.tell()
|
|
253
|
+
source.seek(current_pos) # Reset position
|
|
254
|
+
|
|
255
|
+
# Use multipart upload for large files
|
|
256
|
+
if file_size >= self.multipart_threshold:
|
|
257
|
+
return await self._multipart_upload(source, key, content_type)
|
|
258
|
+
|
|
259
|
+
# Use standard upload for small files
|
|
260
|
+
async with self.session.client('s3') as s3:
|
|
261
|
+
try:
|
|
262
|
+
if isinstance(source, Path):
|
|
263
|
+
await s3.upload_file(
|
|
264
|
+
str(source),
|
|
265
|
+
self.bucket_name,
|
|
266
|
+
key,
|
|
267
|
+
ExtraArgs=extra_args
|
|
268
|
+
)
|
|
269
|
+
else:
|
|
270
|
+
source.seek(0)
|
|
271
|
+
await s3.upload_fileobj(
|
|
272
|
+
source,
|
|
273
|
+
self.bucket_name,
|
|
274
|
+
key,
|
|
275
|
+
ExtraArgs=extra_args
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
return await self._get_object_metadata(key)
|
|
279
|
+
|
|
280
|
+
except ClientError as e:
|
|
281
|
+
raise IOError(
|
|
282
|
+
f"Failed to upload file: {e}"
|
|
283
|
+
) from e
|
|
284
|
+
|
|
285
|
+
async def list_files(
|
|
286
|
+
self,
|
|
287
|
+
path: str = "",
|
|
288
|
+
pattern: str = "*"
|
|
289
|
+
) -> List[FileMetadata]:
|
|
290
|
+
"""List files matching pattern in S3 bucket/prefix"""
|
|
291
|
+
prefix = self._resolve_path(path)
|
|
292
|
+
|
|
293
|
+
async with self.session.client('s3') as s3:
|
|
294
|
+
try:
|
|
295
|
+
paginator = s3.get_paginator('list_objects_v2')
|
|
296
|
+
files = []
|
|
297
|
+
|
|
298
|
+
async for page in paginator.paginate(
|
|
299
|
+
Bucket=self.bucket_name,
|
|
300
|
+
Prefix=prefix
|
|
301
|
+
):
|
|
302
|
+
if 'Contents' not in page:
|
|
303
|
+
continue
|
|
304
|
+
|
|
305
|
+
for obj in page['Contents']:
|
|
306
|
+
key = obj['Key']
|
|
307
|
+
name = Path(key).name
|
|
308
|
+
|
|
309
|
+
if key.endswith('/'):
|
|
310
|
+
continue
|
|
311
|
+
|
|
312
|
+
if fnmatch.fnmatch(name, pattern):
|
|
313
|
+
url = await s3.generate_presigned_url(
|
|
314
|
+
'get_object',
|
|
315
|
+
Params={'Bucket': self.bucket_name, 'Key': key},
|
|
316
|
+
ExpiresIn=3600
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
files.append(FileMetadata(
|
|
320
|
+
name=name,
|
|
321
|
+
path=self._strip_prefix(key),
|
|
322
|
+
size=obj['Size'],
|
|
323
|
+
content_type=None,
|
|
324
|
+
modified_at=obj['LastModified'],
|
|
325
|
+
url=url
|
|
326
|
+
))
|
|
327
|
+
|
|
328
|
+
return sorted(files, key=lambda x: x.name)
|
|
329
|
+
|
|
330
|
+
except ClientError as e:
|
|
331
|
+
if e.response['Error']['Code'] == 'NoSuchBucket':
|
|
332
|
+
raise ValueError(
|
|
333
|
+
f"Bucket not found: {self.bucket_name}"
|
|
334
|
+
) from e
|
|
335
|
+
raise
|
|
336
|
+
|
|
337
|
+
async def get_file_url(self, path: str, expiry: int = 3600) -> str:
|
|
338
|
+
"""Get presigned URL for S3 object"""
|
|
339
|
+
key = self._resolve_path(path)
|
|
340
|
+
|
|
341
|
+
async with self.session.client('s3') as s3:
|
|
342
|
+
try:
|
|
343
|
+
return await s3.generate_presigned_url(
|
|
344
|
+
'get_object',
|
|
345
|
+
Params={'Bucket': self.bucket_name, 'Key': key},
|
|
346
|
+
ExpiresIn=expiry
|
|
347
|
+
)
|
|
348
|
+
except ClientError as e:
|
|
349
|
+
if e.response['Error']['Code'] == '404':
|
|
350
|
+
raise FileNotFoundError(
|
|
351
|
+
f"File not found: {path}"
|
|
352
|
+
) from e
|
|
353
|
+
raise
|
|
354
|
+
|
|
355
|
+
async def download_file(
|
|
356
|
+
self,
|
|
357
|
+
source: str,
|
|
358
|
+
destination: Path | BinaryIO
|
|
359
|
+
) -> Path:
|
|
360
|
+
"""Download file from S3"""
|
|
361
|
+
key = self._resolve_path(source)
|
|
362
|
+
|
|
363
|
+
async with self.session.client('s3') as s3:
|
|
364
|
+
try:
|
|
365
|
+
if isinstance(destination, Path):
|
|
366
|
+
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
367
|
+
await s3.download_file(
|
|
368
|
+
self.bucket_name,
|
|
369
|
+
key,
|
|
370
|
+
str(destination)
|
|
371
|
+
)
|
|
372
|
+
return destination
|
|
373
|
+
else:
|
|
374
|
+
response = await s3.get_object(
|
|
375
|
+
Bucket=self.bucket_name,
|
|
376
|
+
Key=key
|
|
377
|
+
)
|
|
378
|
+
async with response['Body'] as stream:
|
|
379
|
+
data = await stream.read()
|
|
380
|
+
destination.write(data)
|
|
381
|
+
return Path(key)
|
|
382
|
+
|
|
383
|
+
except ClientError as e:
|
|
384
|
+
if e.response['Error']['Code'] == 'NoSuchKey':
|
|
385
|
+
raise FileNotFoundError(
|
|
386
|
+
f"File not found: {source}"
|
|
387
|
+
) from e
|
|
388
|
+
raise
|
|
389
|
+
|
|
390
|
+
async def copy_file(self, source: str, destination: str) -> FileMetadata:
|
|
391
|
+
"""Copy file within S3 bucket"""
|
|
392
|
+
source_key = self._resolve_path(source)
|
|
393
|
+
dest_key = self._resolve_path(destination)
|
|
394
|
+
|
|
395
|
+
copy_source = {
|
|
396
|
+
'Bucket': self.bucket_name,
|
|
397
|
+
'Key': source_key
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
async with self.session.client('s3') as s3:
|
|
401
|
+
try:
|
|
402
|
+
await s3.copy_object(
|
|
403
|
+
CopySource=copy_source,
|
|
404
|
+
Bucket=self.bucket_name,
|
|
405
|
+
Key=dest_key
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
return await self._get_object_metadata(dest_key)
|
|
409
|
+
|
|
410
|
+
except ClientError as e:
|
|
411
|
+
if e.response['Error']['Code'] == 'NoSuchKey':
|
|
412
|
+
raise FileNotFoundError(
|
|
413
|
+
f"Source file not found: {source}"
|
|
414
|
+
) from e
|
|
415
|
+
raise
|
|
416
|
+
|
|
417
|
+
async def delete_file(self, path: str) -> bool:
|
|
418
|
+
"""Delete file from S3"""
|
|
419
|
+
key = self._resolve_path(path)
|
|
420
|
+
|
|
421
|
+
async with self.session.client('s3') as s3:
|
|
422
|
+
try:
|
|
423
|
+
await s3.delete_object(
|
|
424
|
+
Bucket=self.bucket_name,
|
|
425
|
+
Key=key
|
|
426
|
+
)
|
|
427
|
+
return True
|
|
428
|
+
except ClientError:
|
|
429
|
+
return False
|
|
430
|
+
|
|
431
|
+
async def exists(self, path: str) -> bool:
|
|
432
|
+
"""Check if file exists in S3"""
|
|
433
|
+
key = self._resolve_path(path)
|
|
434
|
+
|
|
435
|
+
async with self.session.client('s3') as s3:
|
|
436
|
+
try:
|
|
437
|
+
await s3.head_object(Bucket=self.bucket_name, Key=key)
|
|
438
|
+
return True
|
|
439
|
+
except ClientError:
|
|
440
|
+
return False
|
|
441
|
+
|
|
442
|
+
async def get_file_metadata(self, path: str) -> FileMetadata:
|
|
443
|
+
"""Get metadata of S3 object"""
|
|
444
|
+
key = self._resolve_path(path)
|
|
445
|
+
return await self._get_object_metadata(key)
|
|
446
|
+
|
|
447
|
+
async def create_file(self, path: str, content: bytes) -> bool:
|
|
448
|
+
"""Create file in S3 with content"""
|
|
449
|
+
key = self._resolve_path(path)
|
|
450
|
+
|
|
451
|
+
content_type, _ = mimetypes.guess_type(path)
|
|
452
|
+
extra_args = {}
|
|
453
|
+
if content_type:
|
|
454
|
+
extra_args['ContentType'] = content_type
|
|
455
|
+
|
|
456
|
+
async with self.session.client('s3') as s3:
|
|
457
|
+
try:
|
|
458
|
+
await s3.put_object(
|
|
459
|
+
Bucket=self.bucket_name,
|
|
460
|
+
Key=key,
|
|
461
|
+
Body=content,
|
|
462
|
+
**extra_args
|
|
463
|
+
)
|
|
464
|
+
return True
|
|
465
|
+
except ClientError as e:
|
|
466
|
+
raise IOError(
|
|
467
|
+
f"Failed to create file: {e}"
|
|
468
|
+
) from e
|
|
469
|
+
|
|
470
|
+
async def create_from_bytes(
|
|
471
|
+
self,
|
|
472
|
+
path: str,
|
|
473
|
+
source: Union[bytes, BytesIO, StringIO],
|
|
474
|
+
encoding: str = 'utf-8'
|
|
475
|
+
) -> FileMetadata:
|
|
476
|
+
"""Create S3 object from bytes, BytesIO, or StringIO"""
|
|
477
|
+
key = self._resolve_path(path)
|
|
478
|
+
|
|
479
|
+
content_type, _ = mimetypes.guess_type(path)
|
|
480
|
+
extra_args = {}
|
|
481
|
+
if content_type:
|
|
482
|
+
extra_args['ContentType'] = content_type
|
|
483
|
+
|
|
484
|
+
async with self.session.client('s3') as s3:
|
|
485
|
+
try:
|
|
486
|
+
if isinstance(source, bytes):
|
|
487
|
+
body = source
|
|
488
|
+
elif isinstance(source, BytesIO):
|
|
489
|
+
source.seek(0)
|
|
490
|
+
body = source.read()
|
|
491
|
+
elif isinstance(source, StringIO):
|
|
492
|
+
source.seek(0)
|
|
493
|
+
body = source.read().encode(encoding)
|
|
494
|
+
else:
|
|
495
|
+
raise TypeError(
|
|
496
|
+
f"source must be bytes, BytesIO, or StringIO, got {type(source)}"
|
|
497
|
+
)
|
|
498
|
+
|
|
499
|
+
await s3.put_object(
|
|
500
|
+
Bucket=self.bucket_name,
|
|
501
|
+
Key=key,
|
|
502
|
+
Body=body,
|
|
503
|
+
**extra_args
|
|
504
|
+
)
|
|
505
|
+
|
|
506
|
+
return await self._get_object_metadata(key)
|
|
507
|
+
|
|
508
|
+
except ClientError as e:
|
|
509
|
+
raise IOError(
|
|
510
|
+
f"Failed to create file: {e}"
|
|
511
|
+
) from e
|