From e16606672afab6c257010ce4a0ff1614740aa096 Mon Sep 17 00:00:00 2001 From: Greyson Lalonde Date: Fri, 12 Sep 2025 21:58:02 -0400 Subject: [PATCH] Squashed 'packages/tools/' content from commit 78317b9c git-subtree-dir: packages/tools git-subtree-split: 78317b9c127f18bd040c1d77e3c0840cdc9a5b38 --- .github/workflows/generate-tool-specs.yml | 84 + .github/workflows/tests.yml | 42 + .gitignore | 15 + .pre-commit-config.yaml | 21 + BUILDING_TOOLS.md | 335 + LICENSE | 21 + README.md | 229 + assets/crew_only_logo.png | Bin 0 -> 14642 bytes assets/crewai_logo.png | Bin 0 -> 14642 bytes crewai_tools/__init__.py | 98 + crewai_tools/adapters/embedchain_adapter.py | 34 + crewai_tools/adapters/enterprise_adapter.py | 426 + crewai_tools/adapters/lancedb_adapter.py | 56 + crewai_tools/adapters/mcp_adapter.py | 163 + .../adapters/pdf_embedchain_adapter.py | 41 + crewai_tools/adapters/rag_adapter.py | 41 + crewai_tools/adapters/tool_collection.py | 74 + crewai_tools/adapters/zapier_adapter.py | 122 + crewai_tools/aws/__init__.py | 16 + crewai_tools/aws/bedrock/__init__.py | 11 + crewai_tools/aws/bedrock/agents/README.md | 181 + crewai_tools/aws/bedrock/agents/__init__.py | 3 + .../aws/bedrock/agents/invoke_agent_tool.py | 176 + crewai_tools/aws/bedrock/browser/README.md | 158 + crewai_tools/aws/bedrock/browser/__init__.py | 3 + .../browser/browser_session_manager.py | 260 + .../aws/bedrock/browser/browser_toolkit.py | 587 + crewai_tools/aws/bedrock/browser/utils.py | 43 + .../aws/bedrock/code_interpreter/README.md | 217 + .../aws/bedrock/code_interpreter/__init__.py | 3 + .../code_interpreter_toolkit.py | 543 + crewai_tools/aws/bedrock/exceptions.py | 17 + .../aws/bedrock/knowledge_base/README.md | 159 + .../aws/bedrock/knowledge_base/__init__.py | 3 + .../bedrock/knowledge_base/retriever_tool.py | 248 + crewai_tools/aws/s3/README.md | 52 + crewai_tools/aws/s3/__init__.py | 2 + crewai_tools/aws/s3/reader_tool.py | 47 + crewai_tools/aws/s3/writer_tool.py | 43 + crewai_tools/printer.py | 131 + crewai_tools/rag/__init__.py | 8 + crewai_tools/rag/base_loader.py | 37 + crewai_tools/rag/chunkers/__init__.py | 15 + crewai_tools/rag/chunkers/base_chunker.py | 167 + crewai_tools/rag/chunkers/default_chunker.py | 6 + .../rag/chunkers/structured_chunker.py | 49 + crewai_tools/rag/chunkers/text_chunker.py | 59 + crewai_tools/rag/chunkers/web_chunker.py | 20 + crewai_tools/rag/core.py | 232 + crewai_tools/rag/data_types.py | 137 + crewai_tools/rag/loaders/__init__.py | 20 + crewai_tools/rag/loaders/csv_loader.py | 72 + crewai_tools/rag/loaders/directory_loader.py | 142 + crewai_tools/rag/loaders/docx_loader.py | 72 + crewai_tools/rag/loaders/json_loader.py | 69 + crewai_tools/rag/loaders/mdx_loader.py | 59 + crewai_tools/rag/loaders/text_loader.py | 28 + crewai_tools/rag/loaders/webpage_loader.py | 47 + crewai_tools/rag/loaders/xml_loader.py | 61 + crewai_tools/rag/misc.py | 4 + crewai_tools/rag/source_content.py | 46 + crewai_tools/tools/__init__.py | 127 + crewai_tools/tools/ai_mind_tool/README.md | 79 + crewai_tools/tools/ai_mind_tool/__init__.py | 0 .../tools/ai_mind_tool/ai_mind_tool.py | 91 + .../tools/apify_actors_tool/README.md | 96 + .../apify_actors_tool/apify_actors_tool.py | 96 + .../tools/arxiv_paper_tool/Examples.md | 80 + crewai_tools/tools/arxiv_paper_tool/README.md | 142 + .../arxiv_paper_tool/arxiv_paper_tool.py | 152 + .../arxiv_paper_tool/arxiv_paper_tool_test.py | 113 + .../tools/brave_search_tool/README.md | 30 + .../tools/brave_search_tool/__init__.py | 0 .../brave_search_tool/brave_search_tool.py | 121 + crewai_tools/tools/brightdata_tool/README.md | 79 + .../tools/brightdata_tool/__init__.py | 9 + .../brightdata_tool/brightdata_dataset.py | 570 + .../tools/brightdata_tool/brightdata_serp.py | 207 + .../brightdata_tool/brightdata_unlocker.py | 122 + .../tools/browserbase_load_tool/README.md | 38 + .../browserbase_load_tool.py | 67 + .../tools/code_docs_search_tool/README.md | 56 + .../code_docs_search_tool.py | 56 + .../tools/code_interpreter_tool/Dockerfile | 6 + .../tools/code_interpreter_tool/README.md | 53 + .../code_interpreter_tool.py | 373 + crewai_tools/tools/composio_tool/README.md | 72 + .../tools/composio_tool/composio_tool.py | 124 + .../contextualai_create_agent_tool/README.md | 58 + .../contextual_create_agent_tool.py | 71 + .../tools/contextualai_parse_tool/README.md | 68 + .../contextual_parse_tool.py | 92 + .../tools/contextualai_query_tool/README.md | 54 + .../contextual_query_tool.py | 99 + .../tools/contextualai_rerank_tool/README.md | 72 + .../contextual_rerank_tool.py | 68 + crewai_tools/tools/couchbase_tool/README.md | 62 + .../tools/couchbase_tool/couchbase_tool.py | 241 + .../crewai_enterprise_tools.py | 88 + .../tools/crewai_platform_tools/__init__.py | 16 + .../crewai_platform_action_tool.py | 233 + .../crewai_platform_tool_builder.py | 135 + .../crewai_platform_tools.py | 28 + .../tools/crewai_platform_tools/misc.py | 13 + crewai_tools/tools/csv_search_tool/README.md | 59 + .../tools/csv_search_tool/csv_search_tool.py | 56 + crewai_tools/tools/dalle_tool/README.MD | 41 + crewai_tools/tools/dalle_tool/dalle_tool.py | 52 + .../tools/databricks_query_tool/README.md | 66 + .../tools/databricks_query_tool/__init__.py | 0 .../databricks_query_tool.py | 670 ++ .../tools/directory_read_tool/README.md | 40 + .../directory_read_tool.py | 47 + .../tools/directory_search_tool/README.md | 55 + .../directory_search_tool.py | 59 + crewai_tools/tools/docx_search_tool/README.md | 57 + .../docx_search_tool/docx_search_tool.py | 62 + crewai_tools/tools/exa_tools/README.md | 30 + .../tools/exa_tools/exa_search_tool.py | 108 + crewai_tools/tools/file_read_tool/README.md | 40 + .../tools/file_read_tool/file_read_tool.py | 97 + crewai_tools/tools/file_writer_tool/README.md | 35 + .../file_writer_tool/file_writer_tool.py | 62 + .../tests/test_file_writer_tool.py | 138 + .../tools/files_compressor_tool/README.md | 119 + .../files_compressor_tool.py | 117 + .../files_compressor_tool_test2.py | 93 + .../firecrawl_crawl_website_tool/README.md | 60 + .../firecrawl_crawl_website_tool.py | 115 + .../firecrawl_scrape_website_tool/README.md | 46 + .../firecrawl_scrape_website_tool.py | 103 + .../tools/firecrawl_search_tool/README.md | 44 + .../firecrawl_search_tool.py | 119 + .../generate_crewai_automation_tool/README.md | 50 + .../generate_crewai_automation_tool.py | 70 + .../tools/github_search_tool/README.md | 67 + .../github_search_tool/github_search_tool.py | 88 + .../tools/hyperbrowser_load_tool/README.md | 42 + .../hyperbrowser_load_tool.py | 107 + .../invoke_crewai_automation_tool/README.md | 159 + .../invoke_crewai_automation_tool.py | 176 + .../tools/jina_scrape_website_tool/README.md | 38 + .../jina_scrape_website_tool.py | 52 + crewai_tools/tools/json_search_tool/README.md | 55 + .../json_search_tool/json_search_tool.py | 47 + crewai_tools/tools/linkup/README.md | 98 + crewai_tools/tools/linkup/assets/icon.png | Bin 0 -> 32966 bytes .../tools/linkup/linkup_search_tool.py | 78 + crewai_tools/tools/llamaindex_tool/README.md | 53 + .../tools/llamaindex_tool/llamaindex_tool.py | 82 + crewai_tools/tools/mdx_search_tool/README.md | 57 + .../tools/mdx_search_tool/mdx_search_tool.py | 56 + .../mongodb_vector_search_tool/README.md | 87 + .../mongodb_vector_search_tool/__init__.py | 11 + .../tools/mongodb_vector_search_tool/utils.py | 120 + .../vector_search.py | 327 + crewai_tools/tools/multion_tool/README.md | 53 + crewai_tools/tools/multion_tool/example.py | 29 + .../tools/multion_tool/multion_tool.py | 80 + .../tools/mysql_search_tool/README.md | 56 + .../mysql_search_tool/mysql_search_tool.py | 51 + crewai_tools/tools/nl2sql/README.md | 73 + crewai_tools/tools/nl2sql/images/image-2.png | Bin 0 -> 84676 bytes crewai_tools/tools/nl2sql/images/image-3.png | Bin 0 -> 83521 bytes crewai_tools/tools/nl2sql/images/image-4.png | Bin 0 -> 84400 bytes crewai_tools/tools/nl2sql/images/image-5.png | Bin 0 -> 66131 bytes crewai_tools/tools/nl2sql/images/image-7.png | Bin 0 -> 24641 bytes crewai_tools/tools/nl2sql/images/image-9.png | Bin 0 -> 56650 bytes crewai_tools/tools/nl2sql/nl2sql_tool.py | 91 + crewai_tools/tools/ocr_tool/README.md | 42 + crewai_tools/tools/ocr_tool/ocr_tool.py | 126 + .../README.md | 55 + .../oxylabs_amazon_product_scraper_tool.py | 155 + .../README.md | 54 + .../oxylabs_amazon_search_scraper_tool.py | 157 + .../README.md | 50 + .../oxylabs_google_search_scraper_tool.py | 160 + .../oxylabs_universal_scraper_tool/README.md | 69 + .../oxylabs_universal_scraper_tool.py | 150 + crewai_tools/tools/parallel_tools/README.md | 153 + crewai_tools/tools/parallel_tools/__init__.py | 7 + .../parallel_tools/parallel_search_tool.py | 119 + .../tools/patronus_eval_tool/__init__.py | 3 + .../tools/patronus_eval_tool/example.py | 55 + .../patronus_eval_tool/patronus_eval_tool.py | 144 + .../patronus_local_evaluator_tool.py | 120 + .../patronus_predefined_criteria_eval_tool.py | 104 + crewai_tools/tools/pdf_search_tool/README.md | 57 + .../tools/pdf_search_tool/pdf_search_tool.py | 55 + .../pdf_text_writing_tool.py | 90 + crewai_tools/tools/pg_search_tool/README.md | 56 + .../tools/pg_search_tool/pg_search_tool.py | 51 + .../tools/qdrant_vector_search_tool/README.md | 49 + .../qdrant_search_tool.py | 183 + crewai_tools/tools/rag/README.md | 61 + crewai_tools/tools/rag/__init__.py | 0 crewai_tools/tools/rag/rag_tool.py | 70 + .../scrape_element_from_website.py | 81 + .../tools/scrape_website_tool/README.md | 24 + .../scrape_website_tool.py | 79 + .../tools/scrapegraph_scrape_tool/README.md | 84 + .../scrapegraph_scrape_tool.py | 183 + .../scrapfly_scrape_website_tool/README.md | 57 + .../scrapfly_scrape_website_tool.py | 76 + .../tools/selenium_scraping_tool/README.md | 44 + .../selenium_scraping_tool.py | 186 + crewai_tools/tools/serpapi_tool/README.md | 32 + .../tools/serpapi_tool/serpapi_base_tool.py | 54 + .../serpapi_google_search_tool.py | 60 + .../serpapi_google_shopping_tool.py | 60 + crewai_tools/tools/serper_dev_tool/README.md | 52 + .../tools/serper_dev_tool/serper_dev_tool.py | 247 + .../serper_scrape_website_tool.py | 80 + crewai_tools/tools/serply_api_tool/README.md | 117 + .../serply_api_tool/serply_job_search_tool.py | 89 + .../serply_news_search_tool.py | 89 + .../serply_scholar_search_tool.py | 93 + .../serply_api_tool/serply_web_search_tool.py | 104 + .../serply_webpage_to_markdown_tool.py | 54 + .../tools/singlestore_search_tool/README.md | 299 + .../tools/singlestore_search_tool/__init__.py | 6 + .../singlestore_search_tool.py | 429 + .../tools/snowflake_search_tool/README.md | 155 + .../tools/snowflake_search_tool/__init__.py | 11 + .../snowflake_search_tool.py | 268 + crewai_tools/tools/spider_tool/README.md | 87 + crewai_tools/tools/spider_tool/spider_tool.py | 214 + .../tools/stagehand_tool/.env.example | 5 + crewai_tools/tools/stagehand_tool/README.md | 273 + crewai_tools/tools/stagehand_tool/__init__.py | 3 + crewai_tools/tools/stagehand_tool/example.py | 116 + .../tools/stagehand_tool/stagehand_tool.py | 732 ++ .../tools/tavily_extractor_tool/README.md | 99 + .../tavily_extractor_tool.py | 170 + .../tools/tavily_search_tool/README.md | 115 + .../tavily_search_tool/tavily_search_tool.py | 249 + crewai_tools/tools/txt_search_tool/README.md | 59 + .../tools/txt_search_tool/txt_search_tool.py | 45 + crewai_tools/tools/vision_tool/README.md | 30 + crewai_tools/tools/vision_tool/vision_tool.py | 130 + crewai_tools/tools/weaviate_tool/README.md | 80 + .../tools/weaviate_tool/vector_search.py | 124 + crewai_tools/tools/website_search/README.md | 57 + .../website_search/website_search_tool.py | 58 + crewai_tools/tools/xml_search_tool/README.md | 57 + .../tools/xml_search_tool/xml_search_tool.py | 45 + .../youtube_channel_search_tool/README.md | 57 + .../youtube_channel_search_tool.py | 61 + .../tools/youtube_video_search_tool/README.md | 60 + .../youtube_video_search_tool.py | 58 + .../tools/zapier_action_tool/README.md | 91 + .../zapier_action_tool/zapier_action_tool.py | 33 + generate_tool_specs.py | 158 + pyproject.toml | 166 + tests/__init__.py | 0 tests/adapters/mcp_adapter_test.py | 230 + tests/base_tool_test.py | 104 + tests/conftest.py | 21 + tests/file_read_tool_test.py | 165 + tests/it/tools/__init__.py | 0 tests/it/tools/conftest.py | 21 + tests/rag/__init__.py | 0 tests/rag/test_csv_loader.py | 130 + tests/rag/test_directory_loader.py | 149 + tests/rag/test_docx_loader.py | 135 + tests/rag/test_json_loader.py | 180 + tests/rag/test_mdx_loader.py | 176 + tests/rag/test_text_loaders.py | 160 + tests/rag/test_webpage_loader.py | 137 + tests/rag/test_xml_loader.py | 137 + tests/test_generate_tool_specs.py | 193 + tests/test_optional_dependencies.py | 41 + tests/tools/__init__.py | 0 tests/tools/brave_search_tool_test.py | 50 + tests/tools/brightdata_serp_tool_test.py | 54 + .../tools/brightdata_webunlocker_tool_test.py | 64 + .../test_csv_search_tool.yaml | 251 + .../test_directory_search_tool.yaml | 544 + .../test_json_search_tool.yaml | 300 + .../test_mdx_search_tool.yaml | 255 + .../test_txt_search_tool.yaml | 251 + tests/tools/couchbase_tool_test.py | 365 + tests/tools/crewai_enterprise_tools_test.py | 355 + .../test_crewai_platform_action_tool.py | 165 + .../test_crewai_platform_tool_builder.py | 223 + .../test_crewai_platform_tools.py | 95 + tests/tools/exa_search_tool_test.py | 32 + .../generate_crewai_automation_tool_test.py | 187 + tests/tools/parallel_search_tool_test.py | 47 + tests/tools/rag/rag_tool_test.py | 43 + tests/tools/selenium_scraping_tool_test.py | 129 + tests/tools/serper_dev_tool_test.py | 151 + tests/tools/singlestore_search_tool_test.py | 336 + tests/tools/snowflake_search_tool_test.py | 103 + tests/tools/stagehand_tool_test.py | 262 + tests/tools/test_code_interpreter_tool.py | 175 + tests/tools/test_import_without_warnings.py | 10 + .../tools/test_mongodb_vector_search_tool.py | 75 + tests/tools/test_oxylabs_tools.py | 163 + tests/tools/test_search_tools.py | 309 + tests/tools/tool_collection_test.py | 231 + tool.specs.json | 9711 +++++++++++++++++ uv.lock | 7800 +++++++++++++ 303 files changed, 49010 insertions(+) create mode 100644 .github/workflows/generate-tool-specs.yml create mode 100644 .github/workflows/tests.yml create mode 100644 .gitignore create mode 100644 .pre-commit-config.yaml create mode 100644 BUILDING_TOOLS.md create mode 100644 LICENSE create mode 100644 README.md create mode 100644 assets/crew_only_logo.png create mode 100644 assets/crewai_logo.png create mode 100644 crewai_tools/__init__.py create mode 100644 crewai_tools/adapters/embedchain_adapter.py create mode 100644 crewai_tools/adapters/enterprise_adapter.py create mode 100644 crewai_tools/adapters/lancedb_adapter.py create mode 100644 crewai_tools/adapters/mcp_adapter.py create mode 100644 crewai_tools/adapters/pdf_embedchain_adapter.py create mode 100644 crewai_tools/adapters/rag_adapter.py create mode 100644 crewai_tools/adapters/tool_collection.py create mode 100644 crewai_tools/adapters/zapier_adapter.py create mode 100644 crewai_tools/aws/__init__.py create mode 100644 crewai_tools/aws/bedrock/__init__.py create mode 100644 crewai_tools/aws/bedrock/agents/README.md create mode 100644 crewai_tools/aws/bedrock/agents/__init__.py create mode 100644 crewai_tools/aws/bedrock/agents/invoke_agent_tool.py create mode 100644 crewai_tools/aws/bedrock/browser/README.md create mode 100644 crewai_tools/aws/bedrock/browser/__init__.py create mode 100644 crewai_tools/aws/bedrock/browser/browser_session_manager.py create mode 100644 crewai_tools/aws/bedrock/browser/browser_toolkit.py create mode 100644 crewai_tools/aws/bedrock/browser/utils.py create mode 100644 crewai_tools/aws/bedrock/code_interpreter/README.md create mode 100644 crewai_tools/aws/bedrock/code_interpreter/__init__.py create mode 100644 crewai_tools/aws/bedrock/code_interpreter/code_interpreter_toolkit.py create mode 100644 crewai_tools/aws/bedrock/exceptions.py create mode 100644 crewai_tools/aws/bedrock/knowledge_base/README.md create mode 100644 crewai_tools/aws/bedrock/knowledge_base/__init__.py create mode 100644 crewai_tools/aws/bedrock/knowledge_base/retriever_tool.py create mode 100644 crewai_tools/aws/s3/README.md create mode 100644 crewai_tools/aws/s3/__init__.py create mode 100644 crewai_tools/aws/s3/reader_tool.py create mode 100644 crewai_tools/aws/s3/writer_tool.py create mode 100644 crewai_tools/printer.py create mode 100644 crewai_tools/rag/__init__.py create mode 100644 crewai_tools/rag/base_loader.py create mode 100644 crewai_tools/rag/chunkers/__init__.py create mode 100644 crewai_tools/rag/chunkers/base_chunker.py create mode 100644 crewai_tools/rag/chunkers/default_chunker.py create mode 100644 crewai_tools/rag/chunkers/structured_chunker.py create mode 100644 crewai_tools/rag/chunkers/text_chunker.py create mode 100644 crewai_tools/rag/chunkers/web_chunker.py create mode 100644 crewai_tools/rag/core.py create mode 100644 crewai_tools/rag/data_types.py create mode 100644 crewai_tools/rag/loaders/__init__.py create mode 100644 crewai_tools/rag/loaders/csv_loader.py create mode 100644 crewai_tools/rag/loaders/directory_loader.py create mode 100644 crewai_tools/rag/loaders/docx_loader.py create mode 100644 crewai_tools/rag/loaders/json_loader.py create mode 100644 crewai_tools/rag/loaders/mdx_loader.py create mode 100644 crewai_tools/rag/loaders/text_loader.py create mode 100644 crewai_tools/rag/loaders/webpage_loader.py create mode 100644 crewai_tools/rag/loaders/xml_loader.py create mode 100644 crewai_tools/rag/misc.py create mode 100644 crewai_tools/rag/source_content.py create mode 100644 crewai_tools/tools/__init__.py create mode 100644 crewai_tools/tools/ai_mind_tool/README.md create mode 100644 crewai_tools/tools/ai_mind_tool/__init__.py create mode 100644 crewai_tools/tools/ai_mind_tool/ai_mind_tool.py create mode 100644 crewai_tools/tools/apify_actors_tool/README.md create mode 100644 crewai_tools/tools/apify_actors_tool/apify_actors_tool.py create mode 100644 crewai_tools/tools/arxiv_paper_tool/Examples.md create mode 100644 crewai_tools/tools/arxiv_paper_tool/README.md create mode 100644 crewai_tools/tools/arxiv_paper_tool/arxiv_paper_tool.py create mode 100644 crewai_tools/tools/arxiv_paper_tool/arxiv_paper_tool_test.py create mode 100644 crewai_tools/tools/brave_search_tool/README.md create mode 100644 crewai_tools/tools/brave_search_tool/__init__.py create mode 100644 crewai_tools/tools/brave_search_tool/brave_search_tool.py create mode 100644 crewai_tools/tools/brightdata_tool/README.md create mode 100644 crewai_tools/tools/brightdata_tool/__init__.py create mode 100644 crewai_tools/tools/brightdata_tool/brightdata_dataset.py create mode 100644 crewai_tools/tools/brightdata_tool/brightdata_serp.py create mode 100644 crewai_tools/tools/brightdata_tool/brightdata_unlocker.py create mode 100644 crewai_tools/tools/browserbase_load_tool/README.md create mode 100644 crewai_tools/tools/browserbase_load_tool/browserbase_load_tool.py create mode 100644 crewai_tools/tools/code_docs_search_tool/README.md create mode 100644 crewai_tools/tools/code_docs_search_tool/code_docs_search_tool.py create mode 100644 crewai_tools/tools/code_interpreter_tool/Dockerfile create mode 100644 crewai_tools/tools/code_interpreter_tool/README.md create mode 100644 crewai_tools/tools/code_interpreter_tool/code_interpreter_tool.py create mode 100644 crewai_tools/tools/composio_tool/README.md create mode 100644 crewai_tools/tools/composio_tool/composio_tool.py create mode 100644 crewai_tools/tools/contextualai_create_agent_tool/README.md create mode 100644 crewai_tools/tools/contextualai_create_agent_tool/contextual_create_agent_tool.py create mode 100644 crewai_tools/tools/contextualai_parse_tool/README.md create mode 100644 crewai_tools/tools/contextualai_parse_tool/contextual_parse_tool.py create mode 100644 crewai_tools/tools/contextualai_query_tool/README.md create mode 100644 crewai_tools/tools/contextualai_query_tool/contextual_query_tool.py create mode 100644 crewai_tools/tools/contextualai_rerank_tool/README.md create mode 100644 crewai_tools/tools/contextualai_rerank_tool/contextual_rerank_tool.py create mode 100644 crewai_tools/tools/couchbase_tool/README.md create mode 100644 crewai_tools/tools/couchbase_tool/couchbase_tool.py create mode 100644 crewai_tools/tools/crewai_enterprise_tools/crewai_enterprise_tools.py create mode 100644 crewai_tools/tools/crewai_platform_tools/__init__.py create mode 100644 crewai_tools/tools/crewai_platform_tools/crewai_platform_action_tool.py create mode 100644 crewai_tools/tools/crewai_platform_tools/crewai_platform_tool_builder.py create mode 100644 crewai_tools/tools/crewai_platform_tools/crewai_platform_tools.py create mode 100644 crewai_tools/tools/crewai_platform_tools/misc.py create mode 100644 crewai_tools/tools/csv_search_tool/README.md create mode 100644 crewai_tools/tools/csv_search_tool/csv_search_tool.py create mode 100644 crewai_tools/tools/dalle_tool/README.MD create mode 100644 crewai_tools/tools/dalle_tool/dalle_tool.py create mode 100644 crewai_tools/tools/databricks_query_tool/README.md create mode 100644 crewai_tools/tools/databricks_query_tool/__init__.py create mode 100644 crewai_tools/tools/databricks_query_tool/databricks_query_tool.py create mode 100644 crewai_tools/tools/directory_read_tool/README.md create mode 100644 crewai_tools/tools/directory_read_tool/directory_read_tool.py create mode 100644 crewai_tools/tools/directory_search_tool/README.md create mode 100644 crewai_tools/tools/directory_search_tool/directory_search_tool.py create mode 100644 crewai_tools/tools/docx_search_tool/README.md create mode 100644 crewai_tools/tools/docx_search_tool/docx_search_tool.py create mode 100644 crewai_tools/tools/exa_tools/README.md create mode 100644 crewai_tools/tools/exa_tools/exa_search_tool.py create mode 100644 crewai_tools/tools/file_read_tool/README.md create mode 100644 crewai_tools/tools/file_read_tool/file_read_tool.py create mode 100644 crewai_tools/tools/file_writer_tool/README.md create mode 100644 crewai_tools/tools/file_writer_tool/file_writer_tool.py create mode 100644 crewai_tools/tools/file_writer_tool/tests/test_file_writer_tool.py create mode 100644 crewai_tools/tools/files_compressor_tool/README.md create mode 100644 crewai_tools/tools/files_compressor_tool/files_compressor_tool.py create mode 100644 crewai_tools/tools/files_compressor_tool/files_compressor_tool_test2.py create mode 100644 crewai_tools/tools/firecrawl_crawl_website_tool/README.md create mode 100644 crewai_tools/tools/firecrawl_crawl_website_tool/firecrawl_crawl_website_tool.py create mode 100644 crewai_tools/tools/firecrawl_scrape_website_tool/README.md create mode 100644 crewai_tools/tools/firecrawl_scrape_website_tool/firecrawl_scrape_website_tool.py create mode 100644 crewai_tools/tools/firecrawl_search_tool/README.md create mode 100644 crewai_tools/tools/firecrawl_search_tool/firecrawl_search_tool.py create mode 100644 crewai_tools/tools/generate_crewai_automation_tool/README.md create mode 100644 crewai_tools/tools/generate_crewai_automation_tool/generate_crewai_automation_tool.py create mode 100644 crewai_tools/tools/github_search_tool/README.md create mode 100644 crewai_tools/tools/github_search_tool/github_search_tool.py create mode 100644 crewai_tools/tools/hyperbrowser_load_tool/README.md create mode 100644 crewai_tools/tools/hyperbrowser_load_tool/hyperbrowser_load_tool.py create mode 100644 crewai_tools/tools/invoke_crewai_automation_tool/README.md create mode 100644 crewai_tools/tools/invoke_crewai_automation_tool/invoke_crewai_automation_tool.py create mode 100644 crewai_tools/tools/jina_scrape_website_tool/README.md create mode 100644 crewai_tools/tools/jina_scrape_website_tool/jina_scrape_website_tool.py create mode 100644 crewai_tools/tools/json_search_tool/README.md create mode 100644 crewai_tools/tools/json_search_tool/json_search_tool.py create mode 100644 crewai_tools/tools/linkup/README.md create mode 100644 crewai_tools/tools/linkup/assets/icon.png create mode 100644 crewai_tools/tools/linkup/linkup_search_tool.py create mode 100644 crewai_tools/tools/llamaindex_tool/README.md create mode 100644 crewai_tools/tools/llamaindex_tool/llamaindex_tool.py create mode 100644 crewai_tools/tools/mdx_search_tool/README.md create mode 100644 crewai_tools/tools/mdx_search_tool/mdx_search_tool.py create mode 100644 crewai_tools/tools/mongodb_vector_search_tool/README.md create mode 100644 crewai_tools/tools/mongodb_vector_search_tool/__init__.py create mode 100644 crewai_tools/tools/mongodb_vector_search_tool/utils.py create mode 100644 crewai_tools/tools/mongodb_vector_search_tool/vector_search.py create mode 100644 crewai_tools/tools/multion_tool/README.md create mode 100644 crewai_tools/tools/multion_tool/example.py create mode 100644 crewai_tools/tools/multion_tool/multion_tool.py create mode 100644 crewai_tools/tools/mysql_search_tool/README.md create mode 100644 crewai_tools/tools/mysql_search_tool/mysql_search_tool.py create mode 100644 crewai_tools/tools/nl2sql/README.md create mode 100644 crewai_tools/tools/nl2sql/images/image-2.png create mode 100644 crewai_tools/tools/nl2sql/images/image-3.png create mode 100644 crewai_tools/tools/nl2sql/images/image-4.png create mode 100644 crewai_tools/tools/nl2sql/images/image-5.png create mode 100644 crewai_tools/tools/nl2sql/images/image-7.png create mode 100644 crewai_tools/tools/nl2sql/images/image-9.png create mode 100644 crewai_tools/tools/nl2sql/nl2sql_tool.py create mode 100644 crewai_tools/tools/ocr_tool/README.md create mode 100644 crewai_tools/tools/ocr_tool/ocr_tool.py create mode 100644 crewai_tools/tools/oxylabs_amazon_product_scraper_tool/README.md create mode 100644 crewai_tools/tools/oxylabs_amazon_product_scraper_tool/oxylabs_amazon_product_scraper_tool.py create mode 100644 crewai_tools/tools/oxylabs_amazon_search_scraper_tool/README.md create mode 100644 crewai_tools/tools/oxylabs_amazon_search_scraper_tool/oxylabs_amazon_search_scraper_tool.py create mode 100644 crewai_tools/tools/oxylabs_google_search_scraper_tool/README.md create mode 100644 crewai_tools/tools/oxylabs_google_search_scraper_tool/oxylabs_google_search_scraper_tool.py create mode 100644 crewai_tools/tools/oxylabs_universal_scraper_tool/README.md create mode 100644 crewai_tools/tools/oxylabs_universal_scraper_tool/oxylabs_universal_scraper_tool.py create mode 100644 crewai_tools/tools/parallel_tools/README.md create mode 100644 crewai_tools/tools/parallel_tools/__init__.py create mode 100644 crewai_tools/tools/parallel_tools/parallel_search_tool.py create mode 100644 crewai_tools/tools/patronus_eval_tool/__init__.py create mode 100644 crewai_tools/tools/patronus_eval_tool/example.py create mode 100644 crewai_tools/tools/patronus_eval_tool/patronus_eval_tool.py create mode 100644 crewai_tools/tools/patronus_eval_tool/patronus_local_evaluator_tool.py create mode 100644 crewai_tools/tools/patronus_eval_tool/patronus_predefined_criteria_eval_tool.py create mode 100644 crewai_tools/tools/pdf_search_tool/README.md create mode 100644 crewai_tools/tools/pdf_search_tool/pdf_search_tool.py create mode 100644 crewai_tools/tools/pdf_text_writing_tool/pdf_text_writing_tool.py create mode 100644 crewai_tools/tools/pg_search_tool/README.md create mode 100644 crewai_tools/tools/pg_search_tool/pg_search_tool.py create mode 100644 crewai_tools/tools/qdrant_vector_search_tool/README.md create mode 100644 crewai_tools/tools/qdrant_vector_search_tool/qdrant_search_tool.py create mode 100644 crewai_tools/tools/rag/README.md create mode 100644 crewai_tools/tools/rag/__init__.py create mode 100644 crewai_tools/tools/rag/rag_tool.py create mode 100644 crewai_tools/tools/scrape_element_from_website/scrape_element_from_website.py create mode 100644 crewai_tools/tools/scrape_website_tool/README.md create mode 100644 crewai_tools/tools/scrape_website_tool/scrape_website_tool.py create mode 100644 crewai_tools/tools/scrapegraph_scrape_tool/README.md create mode 100644 crewai_tools/tools/scrapegraph_scrape_tool/scrapegraph_scrape_tool.py create mode 100644 crewai_tools/tools/scrapfly_scrape_website_tool/README.md create mode 100644 crewai_tools/tools/scrapfly_scrape_website_tool/scrapfly_scrape_website_tool.py create mode 100644 crewai_tools/tools/selenium_scraping_tool/README.md create mode 100644 crewai_tools/tools/selenium_scraping_tool/selenium_scraping_tool.py create mode 100644 crewai_tools/tools/serpapi_tool/README.md create mode 100644 crewai_tools/tools/serpapi_tool/serpapi_base_tool.py create mode 100644 crewai_tools/tools/serpapi_tool/serpapi_google_search_tool.py create mode 100644 crewai_tools/tools/serpapi_tool/serpapi_google_shopping_tool.py create mode 100644 crewai_tools/tools/serper_dev_tool/README.md create mode 100644 crewai_tools/tools/serper_dev_tool/serper_dev_tool.py create mode 100644 crewai_tools/tools/serper_scrape_website_tool/serper_scrape_website_tool.py create mode 100644 crewai_tools/tools/serply_api_tool/README.md create mode 100644 crewai_tools/tools/serply_api_tool/serply_job_search_tool.py create mode 100644 crewai_tools/tools/serply_api_tool/serply_news_search_tool.py create mode 100644 crewai_tools/tools/serply_api_tool/serply_scholar_search_tool.py create mode 100644 crewai_tools/tools/serply_api_tool/serply_web_search_tool.py create mode 100644 crewai_tools/tools/serply_api_tool/serply_webpage_to_markdown_tool.py create mode 100644 crewai_tools/tools/singlestore_search_tool/README.md create mode 100644 crewai_tools/tools/singlestore_search_tool/__init__.py create mode 100644 crewai_tools/tools/singlestore_search_tool/singlestore_search_tool.py create mode 100644 crewai_tools/tools/snowflake_search_tool/README.md create mode 100644 crewai_tools/tools/snowflake_search_tool/__init__.py create mode 100644 crewai_tools/tools/snowflake_search_tool/snowflake_search_tool.py create mode 100644 crewai_tools/tools/spider_tool/README.md create mode 100644 crewai_tools/tools/spider_tool/spider_tool.py create mode 100644 crewai_tools/tools/stagehand_tool/.env.example create mode 100644 crewai_tools/tools/stagehand_tool/README.md create mode 100644 crewai_tools/tools/stagehand_tool/__init__.py create mode 100644 crewai_tools/tools/stagehand_tool/example.py create mode 100644 crewai_tools/tools/stagehand_tool/stagehand_tool.py create mode 100644 crewai_tools/tools/tavily_extractor_tool/README.md create mode 100644 crewai_tools/tools/tavily_extractor_tool/tavily_extractor_tool.py create mode 100644 crewai_tools/tools/tavily_search_tool/README.md create mode 100644 crewai_tools/tools/tavily_search_tool/tavily_search_tool.py create mode 100644 crewai_tools/tools/txt_search_tool/README.md create mode 100644 crewai_tools/tools/txt_search_tool/txt_search_tool.py create mode 100644 crewai_tools/tools/vision_tool/README.md create mode 100644 crewai_tools/tools/vision_tool/vision_tool.py create mode 100644 crewai_tools/tools/weaviate_tool/README.md create mode 100644 crewai_tools/tools/weaviate_tool/vector_search.py create mode 100644 crewai_tools/tools/website_search/README.md create mode 100644 crewai_tools/tools/website_search/website_search_tool.py create mode 100644 crewai_tools/tools/xml_search_tool/README.md create mode 100644 crewai_tools/tools/xml_search_tool/xml_search_tool.py create mode 100644 crewai_tools/tools/youtube_channel_search_tool/README.md create mode 100644 crewai_tools/tools/youtube_channel_search_tool/youtube_channel_search_tool.py create mode 100644 crewai_tools/tools/youtube_video_search_tool/README.md create mode 100644 crewai_tools/tools/youtube_video_search_tool/youtube_video_search_tool.py create mode 100644 crewai_tools/tools/zapier_action_tool/README.md create mode 100644 crewai_tools/tools/zapier_action_tool/zapier_action_tool.py create mode 100644 generate_tool_specs.py create mode 100644 pyproject.toml create mode 100644 tests/__init__.py create mode 100644 tests/adapters/mcp_adapter_test.py create mode 100644 tests/base_tool_test.py create mode 100644 tests/conftest.py create mode 100644 tests/file_read_tool_test.py create mode 100644 tests/it/tools/__init__.py create mode 100644 tests/it/tools/conftest.py create mode 100644 tests/rag/__init__.py create mode 100644 tests/rag/test_csv_loader.py create mode 100644 tests/rag/test_directory_loader.py create mode 100644 tests/rag/test_docx_loader.py create mode 100644 tests/rag/test_json_loader.py create mode 100644 tests/rag/test_mdx_loader.py create mode 100644 tests/rag/test_text_loaders.py create mode 100644 tests/rag/test_webpage_loader.py create mode 100644 tests/rag/test_xml_loader.py create mode 100644 tests/test_generate_tool_specs.py create mode 100644 tests/test_optional_dependencies.py create mode 100644 tests/tools/__init__.py create mode 100644 tests/tools/brave_search_tool_test.py create mode 100644 tests/tools/brightdata_serp_tool_test.py create mode 100644 tests/tools/brightdata_webunlocker_tool_test.py create mode 100644 tests/tools/cassettes/test_search_tools/test_csv_search_tool.yaml create mode 100644 tests/tools/cassettes/test_search_tools/test_directory_search_tool.yaml create mode 100644 tests/tools/cassettes/test_search_tools/test_json_search_tool.yaml create mode 100644 tests/tools/cassettes/test_search_tools/test_mdx_search_tool.yaml create mode 100644 tests/tools/cassettes/test_search_tools/test_txt_search_tool.yaml create mode 100644 tests/tools/couchbase_tool_test.py create mode 100644 tests/tools/crewai_enterprise_tools_test.py create mode 100644 tests/tools/crewai_platform_tools/test_crewai_platform_action_tool.py create mode 100644 tests/tools/crewai_platform_tools/test_crewai_platform_tool_builder.py create mode 100644 tests/tools/crewai_platform_tools/test_crewai_platform_tools.py create mode 100644 tests/tools/exa_search_tool_test.py create mode 100644 tests/tools/generate_crewai_automation_tool_test.py create mode 100644 tests/tools/parallel_search_tool_test.py create mode 100644 tests/tools/rag/rag_tool_test.py create mode 100644 tests/tools/selenium_scraping_tool_test.py create mode 100644 tests/tools/serper_dev_tool_test.py create mode 100644 tests/tools/singlestore_search_tool_test.py create mode 100644 tests/tools/snowflake_search_tool_test.py create mode 100644 tests/tools/stagehand_tool_test.py create mode 100644 tests/tools/test_code_interpreter_tool.py create mode 100644 tests/tools/test_import_without_warnings.py create mode 100644 tests/tools/test_mongodb_vector_search_tool.py create mode 100644 tests/tools/test_oxylabs_tools.py create mode 100644 tests/tools/test_search_tools.py create mode 100644 tests/tools/tool_collection_test.py create mode 100644 tool.specs.json create mode 100644 uv.lock diff --git a/.github/workflows/generate-tool-specs.yml b/.github/workflows/generate-tool-specs.yml new file mode 100644 index 000000000..ce7b2f5c7 --- /dev/null +++ b/.github/workflows/generate-tool-specs.yml @@ -0,0 +1,84 @@ +name: Generate Tool Specifications + +on: + push: + branches: + - main + +permissions: + contents: write + pull-requests: write + +jobs: + generate-specs: + if: github.event.head_commit.author.name != 'crewai-tools-spec-generator[bot]' + runs-on: ubuntu-latest + outputs: + specs_changed: ${{ steps.check_changes.outputs.specs_changed }} + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v3 + with: + enable-cache: true + + - name: Set up Python + run: uv python install 3.12.8 + + - name: Install the project + run: uv sync --dev --all-extras + + - name: Generate tool specifications + run: uv run python generate_tool_specs.py + + - name: Configure Git and add upstream + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git remote add upstream https://github.com/crewAIInc/crewAI-tools.git + git fetch upstream + + - name: Check for changes in tool specifications + id: check_changes + run: | + git add tool.specs.json + if git diff --quiet --staged; then + echo "No changes detected in tool.specs.json" + echo "specs_changed=false" >> $GITHUB_OUTPUT + else + echo "Changes detected in tool.specs.json" + echo "specs_changed=true" >> $GITHUB_OUTPUT + fi + + - name: Generate GitHub App token + if: steps.check_changes.outputs.specs_changed == 'true' + id: app-token + uses: tibdex/github-app-token@v2 + with: + app_id: ${{ secrets.CREWAI_RELEASE_TOOL_APP_ID }} + private_key: ${{ secrets.CREWAI_RELEASE_TOOL_PRIVATE_KEY }} + + - name: Create Pull Request + if: steps.check_changes.outputs.specs_changed == 'true' + uses: peter-evans/create-pull-request@v7 + with: + token: ${{ steps.app-token.outputs.token }} + title: "Automatically update tool specifications" + base: main + branch: update-tool-specs + commit-message: "Automatically update tool specifications" + committer: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> + author: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> + delete-branch: true + + notify-api: + if: github.event.head_commit.author.name == 'crewai-tools-spec-generator[bot]' + runs-on: ubuntu-latest + steps: + - name: Notify API about tool specification update + run: | + curl -X POST https://app.crewai.com/crewai_plus/api/v1/internal_tool_releases \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer ${{ secrets.CREWAI_RELEASE_TOOL_API_KEY }}" diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 000000000..141518f3b --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,42 @@ +name: Run Tests + +on: [pull_request] + +permissions: + contents: write + +env: + OPENAI_API_KEY: fake-openai-key + BRAVE_API_KEY: fake-brave-key + SNOWFLAKE_USER: fake-snowflake-user + SNOWFLAKE_PASSWORD: fake-snowflake-password + SNOWFLAKE_ACCOUNT: fake-snowflake-account + SNOWFLAKE_WAREHOUSE: fake-snowflake-warehouse + SNOWFLAKE_DATABASE: fake-snowflake-database + SNOWFLAKE_SCHEMA: fake-snowflake-schema + EMBEDCHAIN_DB_URI: sqlite:///test.db + +jobs: + tests: + runs-on: ubuntu-latest + timeout-minutes: 15 + strategy: + matrix: + python-version: ['3.10', '3.11', '3.12', '3.13'] + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v3 + with: + enable-cache: true + + - name: Set up Python ${{ matrix.python-version }} + run: uv python install ${{ matrix.python-version }} + + - name: Install the project + run: uv sync --dev --all-extras + + - name: Run tests + run: uv run pytest tests -vv diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..615dc0849 --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +.DS_Store +.pytest_cache +__pycache__ +dist/ +.env +.idea +test.py +docs_crew/ +chroma.sqlite3 +venv +.venv/ + +# chroma db +db +crewai-rag-tool.lock diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 000000000..4eae2b11a --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,21 @@ +repos: + + - repo: https://github.com/psf/black-pre-commit-mirror + rev: 23.12.1 + hooks: + - id: black + language_version: python3.11 + files: \.(py)$ + + - repo: https://github.com/pycqa/isort + rev: 5.13.2 + hooks: + - id: isort + name: isort (python) + args: ["--profile", "black", "--filter-files"] + + - repo: https://github.com/PyCQA/autoflake + rev: v2.2.1 + hooks: + - id: autoflake + args: ['--in-place', '--remove-all-unused-imports', '--remove-unused-variables', '--ignore-init-module-imports'] diff --git a/BUILDING_TOOLS.md b/BUILDING_TOOLS.md new file mode 100644 index 000000000..2994b918e --- /dev/null +++ b/BUILDING_TOOLS.md @@ -0,0 +1,335 @@ +## Building CrewAI Tools + +This guide shows you how to build high‑quality CrewAI tools that match the patterns in this repository and are ready to be merged. It focuses on: architecture, conventions, environment variables, dependencies, testing, documentation, and a complete example. + +### Who this is for +- Contributors creating new tools under `crewai_tools/tools/*` +- Maintainers reviewing PRs for consistency and DX + +--- + +## Quick‑start checklist +1. Create a new folder under `crewai_tools/tools//` with a `README.md` and a `.py`. +2. Implement a class that ends with `Tool` and subclasses `BaseTool` (or `RagTool` when appropriate). +3. Define a Pydantic `args_schema` with explicit field descriptions and validation. +4. Declare `env_vars` and `package_dependencies` in the class when needed. +5. Lazily initialize clients in `__init__` or `_run` and handle missing credentials with clear errors. +6. Implement `_run(...) -> str | dict` and, if needed, `_arun(...)`. +7. Add tests under `tests/tools/` (unit, no real network calls; mock or record safely). +8. Add a concise tool `README.md` with usage and required env vars. +9. If you add optional dependencies, register them in `pyproject.toml` under `[project.optional-dependencies]` and reference that extra in your tool docs. +10. Run `uv run pytest` and `pre-commit run -a` locally; ensure green. + +--- + +## Tool anatomy and conventions + +### BaseTool pattern +All tools follow this structure: + +```python +from typing import Any, List, Optional, Type + +import os +from pydantic import BaseModel, Field +from crewai.tools import BaseTool, EnvVar + + +class MyToolInput(BaseModel): + """Input schema for MyTool.""" + query: str = Field(..., description="Your input description here") + limit: int = Field(5, ge=1, le=50, description="Max items to return") + + +class MyTool(BaseTool): + name: str = "My Tool" + description: str = "Explain succinctly what this tool does and when to use it." + args_schema: Type[BaseModel] = MyToolInput + + # Only include when applicable + env_vars: List[EnvVar] = [ + EnvVar(name="MY_API_KEY", description="API key for My service", required=True), + ] + package_dependencies: List[str] = ["my-sdk"] + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + # Lazy import to keep base install light + try: + import my_sdk # noqa: F401 + except Exception as exc: + raise ImportError( + "Missing optional dependency 'my-sdk'. Install with: \n" + " uv add crewai-tools --extra my-sdk\n" + "or\n" + " pip install my-sdk\n" + ) from exc + + if "MY_API_KEY" not in os.environ: + raise ValueError("Environment variable MY_API_KEY is required for MyTool") + + def _run(self, query: str, limit: int = 5, **_: Any) -> str: + """Synchronous execution. Return a concise string or JSON string.""" + # Implement your logic here; do not print. Return the content. + # Handle errors gracefully, return clear messages. + return f"Processed {query} with limit={limit}" + + async def _arun(self, *args: Any, **kwargs: Any) -> str: + """Optional async counterpart if your client supports it.""" + # Prefer delegating to _run when the client is thread-safe + return self._run(*args, **kwargs) +``` + +Key points: +- Class name must end with `Tool` to be auto‑discovered by our tooling. +- Use `args_schema` for inputs; always include `description` and validation. +- Validate env vars early and fail with actionable errors. +- Keep outputs deterministic and compact; favor `str` (possibly JSON‑encoded) or small dicts converted to strings. +- Avoid printing; return the final string. + +### Error handling +- Wrap network and I/O with try/except and return a helpful message. See `BraveSearchTool` and others for patterns. +- Validate required inputs and environment configuration with clear messages. +- Keep exceptions user‑friendly; do not leak stack traces. + +### Rate limiting and retries +- If the upstream API enforces request pacing, implement minimal rate limiting (see `BraveSearchTool`). +- Consider idempotency and backoff for transient errors where appropriate. + +### Async support +- Implement `_arun` only if your library has a true async client or your sync calls are thread‑safe. +- Otherwise, delegate `_arun` to `_run` as in multiple existing tools. + +### Returning values +- Return a string (or JSON string) that’s ready to display in an agent transcript. +- If returning structured data, keep it small and human‑readable. Use stable keys and ordering. + +--- + +## RAG tools and adapters + +If your tool is a knowledge source, consider extending `RagTool` and/or creating an adapter. + +- `RagTool` exposes `add(...)` and a `query(question: str) -> str` contract through an `Adapter`. +- See `crewai_tools/tools/rag/rag_tool.py` and adapters like `embedchain_adapter.py` and `lancedb_adapter.py`. + +Minimal adapter example: + +```python +from typing import Any +from pydantic import BaseModel +from crewai_tools.tools.rag.rag_tool import Adapter, RagTool + + +class MemoryAdapter(Adapter): + store: list[str] = [] + + def add(self, text: str, **_: Any) -> None: + self.store.append(text) + + def query(self, question: str) -> str: + # naive demo: return all text containing any word from the question + tokens = set(question.lower().split()) + hits = [t for t in self.store if tokens & set(t.lower().split())] + return "\n".join(hits) if hits else "No relevant content found." + + +class MemoryRagTool(RagTool): + name: str = "In‑memory RAG" + description: str = "Toy RAG that stores text in memory and returns matches." + adapter: Adapter = MemoryAdapter() +``` + +When using external vector DBs (MongoDB, Qdrant, Weaviate), study the existing tools to follow indexing, embedding, and query configuration patterns closely. + +--- + +## Toolkits (multiple related tools) + +Some integrations expose a toolkit (a group of tools) rather than a single class. See Bedrock `browser_toolkit.py` and `code_interpreter_toolkit.py`. + +Guidelines: +- Provide small, focused `BaseTool` classes for each operation (e.g., `navigate`, `click`, `extract_text`). +- Offer a helper `create__toolkit(...) -> Tuple[ToolkitClass, List[BaseTool]]` to create tools and manage resources. +- If you open external resources (browsers, interpreters), support cleanup methods and optionally context manager usage. + +--- + +## Environment variables and dependencies + +### env_vars +- Declare as `env_vars: List[EnvVar]` with `name`, `description`, `required`, and optional `default`. +- Validate presence in `__init__` or on first `_run` call. + +### Dependencies +- List runtime packages in `package_dependencies` on the class. +- If they are genuinely optional, add an extra under `[project.optional-dependencies]` in `pyproject.toml` (e.g., `tavily-python`, `serpapi`, `scrapfly-sdk`). +- Use lazy imports to avoid hard deps for users who don’t need the tool. + +--- + +## Testing + +Place tests under `tests/tools/` and follow these rules: +- Do not hit real external services in CI. Use mocks, fakes, or recorded fixtures where allowed. +- Validate input validation, env var handling, error messages, and happy path output formatting. +- Keep tests fast and deterministic. + +Example skeleton (`tests/tools/my_tool_test.py`): + +```python +import os +import pytest +from crewai_tools.tools.my_tool.my_tool import MyTool + + +def test_requires_env_var(monkeypatch): + monkeypatch.delenv("MY_API_KEY", raising=False) + with pytest.raises(ValueError): + MyTool() + + +def test_happy_path(monkeypatch): + monkeypatch.setenv("MY_API_KEY", "test") + tool = MyTool() + result = tool.run(query="hello", limit=2) + assert "hello" in result +``` + +Run locally: + +```bash +uv run pytest +pre-commit run -a +``` + +--- + +## Documentation + +Each tool must include a `README.md` in its folder with: +- What it does and when to use it +- Required env vars and optional extras (with install snippet) +- Minimal usage example + +Update the root `README.md` only if the tool introduces a new category or notable capability. + +--- + +## Discovery and specs + +Our internal tooling discovers classes whose names end with `Tool`. Keep your class exported from the module path under `crewai_tools/tools/...` to be picked up by scripts like `generate_tool_specs.py`. + +--- + +## Full example: “Weather Search Tool” + +This example demonstrates: `args_schema`, `env_vars`, `package_dependencies`, lazy imports, validation, and robust error handling. + +```python +# file: crewai_tools/tools/weather_tool/weather_tool.py +from typing import Any, List, Optional, Type +import os +import requests +from pydantic import BaseModel, Field +from crewai.tools import BaseTool, EnvVar + + +class WeatherToolInput(BaseModel): + """Input schema for WeatherTool.""" + city: str = Field(..., description="City name, e.g., 'Berlin'") + country: Optional[str] = Field(None, description="ISO country code, e.g., 'DE'") + units: str = Field( + default="metric", + description="Units system: 'metric' or 'imperial'", + pattern=r"^(metric|imperial)$", + ) + + +class WeatherTool(BaseTool): + name: str = "Weather Search" + description: str = ( + "Look up current weather for a city using a public weather API." + ) + args_schema: Type[BaseModel] = WeatherToolInput + + env_vars: List[EnvVar] = [ + EnvVar( + name="WEATHER_API_KEY", + description="API key for the weather service", + required=True, + ), + ] + package_dependencies: List[str] = ["requests"] + + base_url: str = "https://api.openweathermap.org/data/2.5/weather" + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + if "WEATHER_API_KEY" not in os.environ: + raise ValueError("WEATHER_API_KEY is required for WeatherTool") + + def _run(self, city: str, country: Optional[str] = None, units: str = "metric") -> str: + try: + q = f"{city},{country}" if country else city + params = { + "q": q, + "units": units, + "appid": os.environ["WEATHER_API_KEY"], + } + resp = requests.get(self.base_url, params=params, timeout=10) + resp.raise_for_status() + data = resp.json() + + main = data.get("weather", [{}])[0].get("main", "Unknown") + desc = data.get("weather", [{}])[0].get("description", "") + temp = data.get("main", {}).get("temp") + feels = data.get("main", {}).get("feels_like") + city_name = data.get("name", city) + + return ( + f"Weather in {city_name}: {main} ({desc}). " + f"Temperature: {temp}°, feels like {feels}°." + ) + except requests.Timeout: + return "Weather service timed out. Please try again later." + except requests.HTTPError as e: + return f"Weather service error: {e.response.status_code} {e.response.text[:120]}" + except Exception as e: + return f"Unexpected error fetching weather: {e}" +``` + +Folder layout: + +``` +crewai_tools/tools/weather_tool/ + ├─ weather_tool.py + └─ README.md +``` + +And `README.md` should document env vars and usage. + +--- + +## PR checklist +- [ ] Tool lives under `crewai_tools/tools//` +- [ ] Class ends with `Tool` and subclasses `BaseTool` (or `RagTool`) +- [ ] Precise `args_schema` with descriptions and validation +- [ ] `env_vars` declared (if any) and validated +- [ ] `package_dependencies` and optional extras added in `pyproject.toml` (if any) +- [ ] Clear error handling; no prints +- [ ] Unit tests added (`tests/tools/`), fast and deterministic +- [ ] Tool `README.md` with usage and env vars +- [ ] `pre-commit` and `pytest` pass locally + +--- + +## Tips for great DX +- Keep responses short and useful—agents quote your tool output directly. +- Validate early; fail fast with actionable guidance. +- Prefer lazy imports; minimize default install surface. +- Mirror patterns from similar tools in this repo for a consistent developer experience. + +Happy building! + + diff --git a/LICENSE b/LICENSE new file mode 100644 index 000000000..48255e7b4 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 João Moura + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 000000000..3ee271370 --- /dev/null +++ b/README.md @@ -0,0 +1,229 @@ +
+ +![Logo of crewAI, two people rowing on a boat](./assets/crewai_logo.png) + +
+ +# CrewAI Tools + +Empower your CrewAI agents with powerful, customizable tools to elevate their capabilities and tackle sophisticated, real-world tasks. + +CrewAI Tools provide the essential functionality to extend your agents, helping you rapidly enhance your automations with reliable, ready-to-use tools or custom-built solutions tailored precisely to your needs. + +--- + +## Quick Links + +[Homepage](https://www.crewai.com/) | [Documentation](https://docs.crewai.com/) | [Examples](https://github.com/crewAIInc/crewAI-examples) | [Community](https://community.crewai.com/) + +--- + +## Available Tools + +CrewAI provides an extensive collection of powerful tools ready to enhance your agents: + +- **File Management**: `FileReadTool`, `FileWriteTool` +- **Web Scraping**: `ScrapeWebsiteTool`, `SeleniumScrapingTool` +- **Database Integrations**: `PGSearchTool`, `MySQLSearchTool` +- **Vector Database Integrations**: `MongoDBVectorSearchTool`, `QdrantVectorSearchTool`, `WeaviateVectorSearchTool` +- **API Integrations**: `SerperApiTool`, `EXASearchTool` +- **AI-powered Tools**: `DallETool`, `VisionTool`, `StagehandTool` + +And many more robust tools to simplify your agent integrations. + +--- + +## Creating Custom Tools + +CrewAI offers two straightforward approaches to creating custom tools: + +### Subclassing `BaseTool` + +Define your tool by subclassing: + +```python +from crewai.tools import BaseTool + +class MyCustomTool(BaseTool): + name: str = "Tool Name" + description: str = "Detailed description here." + + def _run(self, *args, **kwargs): + # Your tool logic here +``` + +### Using the `tool` Decorator + +Quickly create lightweight tools using decorators: + +```python +from crewai import tool + +@tool("Tool Name") +def my_custom_function(input): + # Tool logic here + return output +``` + +--- + +## CrewAI Tools and MCP + +CrewAI Tools supports the Model Context Protocol (MCP). It gives you access to thousands of tools from the hundreds of MCP servers out there built by the community. + +Before you start using MCP with CrewAI tools, you need to install the `mcp` extra dependencies: + +```bash +pip install crewai-tools[mcp] +# or +uv add crewai-tools --extra mcp +``` + +To quickly get started with MCP in CrewAI you have 2 options: + +### Option 1: Fully managed connection + +In this scenario we use a contextmanager (`with` statement) to start and stop the the connection with the MCP server. +This is done in the background and you only get to interact with the CrewAI tools corresponding to the MCP server's tools. + +For an STDIO based MCP server: + +```python +from mcp import StdioServerParameters +from crewai_tools import MCPServerAdapter + +serverparams = StdioServerParameters( + command="uvx", + args=["--quiet", "pubmedmcp@0.1.3"], + env={"UV_PYTHON": "3.12", **os.environ}, +) + +with MCPServerAdapter(serverparams) as tools: + # tools is now a list of CrewAI Tools matching 1:1 with the MCP server's tools + agent = Agent(..., tools=tools) + task = Task(...) + crew = Crew(..., agents=[agent], tasks=[task]) + crew.kickoff(...) +``` +For an SSE based MCP server: + +```python +serverparams = {"url": "http://localhost:8000/sse"} +with MCPServerAdapter(serverparams) as tools: + # tools is now a list of CrewAI Tools matching 1:1 with the MCP server's tools + agent = Agent(..., tools=tools) + task = Task(...) + crew = Crew(..., agents=[agent], tasks=[task]) + crew.kickoff(...) +``` + +### Option 2: More control over the MCP connection + +If you need more control over the MCP connection, you can instanciate the MCPServerAdapter into an `mcp_server_adapter` object which can be used to manage the connection with the MCP server and access the available tools. + +**important**: in this case you need to call `mcp_server_adapter.stop()` to make sure the connection is correctly stopped. We recommend that you use a `try ... finally` block run to make sure the `.stop()` is called even in case of errors. + +Here is the same example for an STDIO MCP Server: + +```python +from mcp import StdioServerParameters +from crewai_tools import MCPServerAdapter + +serverparams = StdioServerParameters( + command="uvx", + args=["--quiet", "pubmedmcp@0.1.3"], + env={"UV_PYTHON": "3.12", **os.environ}, +) + +try: + mcp_server_adapter = MCPServerAdapter(serverparams) + tools = mcp_server_adapter.tools + # tools is now a list of CrewAI Tools matching 1:1 with the MCP server's tools + agent = Agent(..., tools=tools) + task = Task(...) + crew = Crew(..., agents=[agent], tasks=[task]) + crew.kickoff(...) + +# ** important ** don't forget to stop the connection +finally: + mcp_server_adapter.stop() +``` + +And finally the same thing but for an SSE MCP Server: + +```python +from mcp import StdioServerParameters +from crewai_tools import MCPServerAdapter + +serverparams = {"url": "http://localhost:8000/sse"} + +try: + mcp_server_adapter = MCPServerAdapter(serverparams) + tools = mcp_server_adapter.tools + # tools is now a list of CrewAI Tools matching 1:1 with the MCP server's tools + agent = Agent(..., tools=tools) + task = Task(...) + crew = Crew(..., agents=[agent], tasks=[task]) + crew.kickoff(...) + +# ** important ** don't forget to stop the connection +finally: + mcp_server_adapter.stop() +``` + +### Considerations & Limitations + +#### Staying Safe with MCP + +Always make sure that you trust the MCP Server before using it. Using an STDIO server will execute code on your machine. Using SSE is still not a silver bullet with many injection possible into your application from a malicious MCP server. + +#### Limitations + +* At this time we only support tools from MCP Server not other type of primitives like prompts, resources... +* We only return the first text output returned by the MCP Server tool using `.content[0].text` + +--- + +## Why Use CrewAI Tools? + +- **Simplicity & Flexibility**: Easy-to-use yet powerful enough for complex workflows. +- **Rapid Integration**: Seamlessly incorporate external services, APIs, and databases. +- **Enterprise Ready**: Built for stability, performance, and consistent results. + +--- + +## Contribution Guidelines + +We welcome contributions from the community! + +1. Fork and clone the repository. +2. Create a new branch (`git checkout -b feature/my-feature`). +3. Commit your changes (`git commit -m 'Add my feature'`). +4. Push your branch (`git push origin feature/my-feature`). +5. Open a pull request. + +--- + +## Developer Quickstart + +```shell +pip install crewai[tools] +``` + +### Development Setup + +- Install dependencies: `uv sync` +- Run tests: `uv run pytest` +- Run static type checking: `uv run pyright` +- Set up pre-commit hooks: `pre-commit install` + +--- + +## Support and Community + +Join our rapidly growing community and receive real-time support: + +- [Discourse](https://community.crewai.com/) +- [Open an Issue](https://github.com/crewAIInc/crewAI/issues) + +Build smarter, faster, and more powerful AI solutions—powered by CrewAI Tools. diff --git a/assets/crew_only_logo.png b/assets/crew_only_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..bc44909d0566b2c0466b7be688cad684b7801c8d GIT binary patch literal 14642 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 BZl?eM literal 0 HcmV?d00001 diff --git a/assets/crewai_logo.png b/assets/crewai_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..bc44909d0566b2c0466b7be688cad684b7801c8d GIT binary patch literal 14642 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 BZl?eM literal 0 HcmV?d00001 diff --git a/crewai_tools/__init__.py b/crewai_tools/__init__.py new file mode 100644 index 000000000..85fe5ed6e --- /dev/null +++ b/crewai_tools/__init__.py @@ -0,0 +1,98 @@ +from .adapters.enterprise_adapter import EnterpriseActionTool +from .adapters.mcp_adapter import MCPServerAdapter +from .adapters.zapier_adapter import ZapierActionTool +from .aws import ( + BedrockInvokeAgentTool, + BedrockKBRetrieverTool, + S3ReaderTool, + S3WriterTool, +) +from .tools import ( + AIMindTool, + ApifyActorsTool, + ArxivPaperTool, + BraveSearchTool, + BrightDataDatasetTool, + BrightDataSearchTool, + BrightDataWebUnlockerTool, + BrowserbaseLoadTool, + CodeDocsSearchTool, + CodeInterpreterTool, + ComposioTool, + ContextualAICreateAgentTool, + ContextualAIParseTool, + ContextualAIQueryTool, + ContextualAIRerankTool, + CouchbaseFTSVectorSearchTool, + CrewaiEnterpriseTools, + CrewaiPlatformTools, + CSVSearchTool, + DallETool, + DatabricksQueryTool, + DirectoryReadTool, + DirectorySearchTool, + DOCXSearchTool, + EXASearchTool, + FileCompressorTool, + FileReadTool, + FileWriterTool, + FirecrawlCrawlWebsiteTool, + FirecrawlScrapeWebsiteTool, + FirecrawlSearchTool, + GenerateCrewaiAutomationTool, + GithubSearchTool, + HyperbrowserLoadTool, + InvokeCrewAIAutomationTool, + JSONSearchTool, + LinkupSearchTool, + LlamaIndexTool, + MDXSearchTool, + MongoDBVectorSearchConfig, + MongoDBVectorSearchTool, + MultiOnTool, + MySQLSearchTool, + NL2SQLTool, + OCRTool, + OxylabsAmazonProductScraperTool, + OxylabsAmazonSearchScraperTool, + OxylabsGoogleSearchScraperTool, + OxylabsUniversalScraperTool, + PatronusEvalTool, + PatronusLocalEvaluatorTool, + PatronusPredefinedCriteriaEvalTool, + PDFSearchTool, + PGSearchTool, + QdrantVectorSearchTool, + RagTool, + ScrapeElementFromWebsiteTool, + ScrapegraphScrapeTool, + ScrapegraphScrapeToolSchema, + ScrapeWebsiteTool, + ScrapflyScrapeWebsiteTool, + SeleniumScrapingTool, + SerpApiGoogleSearchTool, + SerpApiGoogleShoppingTool, + SerperDevTool, + SerperScrapeWebsiteTool, + SerplyJobSearchTool, + SerplyNewsSearchTool, + SerplyScholarSearchTool, + SerplyWebpageToMarkdownTool, + SerplyWebSearchTool, + SingleStoreSearchTool, + SnowflakeConfig, + SnowflakeSearchTool, + SpiderTool, + StagehandTool, + TavilyExtractorTool, + TavilySearchTool, + TXTSearchTool, + VisionTool, + WeaviateVectorSearchTool, + WebsiteSearchTool, + XMLSearchTool, + YoutubeChannelSearchTool, + YoutubeVideoSearchTool, + ZapierActionTools, + ParallelSearchTool, +) diff --git a/crewai_tools/adapters/embedchain_adapter.py b/crewai_tools/adapters/embedchain_adapter.py new file mode 100644 index 000000000..1e7b83c0b --- /dev/null +++ b/crewai_tools/adapters/embedchain_adapter.py @@ -0,0 +1,34 @@ +from typing import Any + +from crewai_tools.tools.rag.rag_tool import Adapter + +try: + from embedchain import App + EMBEDCHAIN_AVAILABLE = True +except ImportError: + EMBEDCHAIN_AVAILABLE = False + + +class EmbedchainAdapter(Adapter): + embedchain_app: Any # Will be App when embedchain is available + summarize: bool = False + + def __init__(self, **data): + if not EMBEDCHAIN_AVAILABLE: + raise ImportError("embedchain is not installed. Please install it with `pip install crewai-tools[embedchain]`") + super().__init__(**data) + + def query(self, question: str) -> str: + result, sources = self.embedchain_app.query( + question, citations=True, dry_run=(not self.summarize) + ) + if self.summarize: + return result + return "\n\n".join([source[0] for source in sources]) + + def add( + self, + *args: Any, + **kwargs: Any, + ) -> None: + self.embedchain_app.add(*args, **kwargs) diff --git a/crewai_tools/adapters/enterprise_adapter.py b/crewai_tools/adapters/enterprise_adapter.py new file mode 100644 index 000000000..c4bfa35eb --- /dev/null +++ b/crewai_tools/adapters/enterprise_adapter.py @@ -0,0 +1,426 @@ +import os +import json +import requests +import warnings +from typing import List, Any, Dict, Literal, Optional, Union, get_origin, Type, cast +from pydantic import Field, create_model +from crewai.tools import BaseTool +import re + + +def get_enterprise_api_base_url() -> str: + """Get the enterprise API base URL from environment or use default.""" + base_url = os.getenv("CREWAI_PLUS_URL", "https://app.crewai.com") + return f"{base_url}/crewai_plus/api/v1/integrations" + +ENTERPRISE_API_BASE_URL = get_enterprise_api_base_url() + + +class EnterpriseActionTool(BaseTool): + """A tool that executes a specific enterprise action.""" + + enterprise_action_token: str = Field( + default="", description="The enterprise action token" + ) + action_name: str = Field(default="", description="The name of the action") + action_schema: Dict[str, Any] = Field( + default={}, description="The schema of the action" + ) + enterprise_api_base_url: str = Field( + default=ENTERPRISE_API_BASE_URL, description="The base API URL" + ) + + def __init__( + self, + name: str, + description: str, + enterprise_action_token: str, + action_name: str, + action_schema: Dict[str, Any], + enterprise_api_base_url: Optional[str] = None, + ): + self._model_registry = {} + self._base_name = self._sanitize_name(name) + + schema_props, required = self._extract_schema_info(action_schema) + + # Define field definitions for the model + field_definitions = {} + for param_name, param_details in schema_props.items(): + param_desc = param_details.get("description", "") + is_required = param_name in required + + try: + field_type = self._process_schema_type( + param_details, self._sanitize_name(param_name).title() + ) + except Exception as e: + print(f"Warning: Could not process schema for {param_name}: {e}") + field_type = str + + # Create field definition based on requirement + field_definitions[param_name] = self._create_field_definition( + field_type, is_required, param_desc + ) + + # Create the model + if field_definitions: + try: + args_schema = create_model( + f"{self._base_name}Schema", **field_definitions + ) + except Exception as e: + print(f"Warning: Could not create main schema model: {e}") + args_schema = create_model( + f"{self._base_name}Schema", + input_text=(str, Field(description="Input for the action")), + ) + else: + # Fallback for empty schema + args_schema = create_model( + f"{self._base_name}Schema", + input_text=(str, Field(description="Input for the action")), + ) + + super().__init__(name=name, description=description, args_schema=args_schema) + self.enterprise_action_token = enterprise_action_token + self.action_name = action_name + self.action_schema = action_schema + self.enterprise_api_base_url = enterprise_api_base_url or get_enterprise_api_base_url() + + def _sanitize_name(self, name: str) -> str: + """Sanitize names to create proper Python class names.""" + sanitized = re.sub(r"[^a-zA-Z0-9_]", "", name) + parts = sanitized.split("_") + return "".join(word.capitalize() for word in parts if word) + + def _extract_schema_info( + self, action_schema: Dict[str, Any] + ) -> tuple[Dict[str, Any], List[str]]: + """Extract schema properties and required fields from action schema.""" + schema_props = ( + action_schema.get("function", {}) + .get("parameters", {}) + .get("properties", {}) + ) + required = ( + action_schema.get("function", {}).get("parameters", {}).get("required", []) + ) + return schema_props, required + + def _process_schema_type(self, schema: Dict[str, Any], type_name: str) -> Type[Any]: + """Process a JSON schema and return appropriate Python type.""" + if "anyOf" in schema: + any_of_types = schema["anyOf"] + is_nullable = any(t.get("type") == "null" for t in any_of_types) + non_null_types = [t for t in any_of_types if t.get("type") != "null"] + + if non_null_types: + base_type = self._process_schema_type(non_null_types[0], type_name) + return Optional[base_type] if is_nullable else base_type + return cast(Type[Any], Optional[str]) + + if "oneOf" in schema: + return self._process_schema_type(schema["oneOf"][0], type_name) + + if "allOf" in schema: + return self._process_schema_type(schema["allOf"][0], type_name) + + json_type = schema.get("type", "string") + + if "enum" in schema: + enum_values = schema["enum"] + if not enum_values: + return self._map_json_type_to_python(json_type) + return Literal[tuple(enum_values)] # type: ignore[return-value] + + if json_type == "array": + items_schema = schema.get("items", {"type": "string"}) + item_type = self._process_schema_type(items_schema, f"{type_name}Item") + return List[item_type] + + if json_type == "object": + return self._create_nested_model(schema, type_name) + + return self._map_json_type_to_python(json_type) + + def _create_nested_model(self, schema: Dict[str, Any], model_name: str) -> Type[Any]: + """Create a nested Pydantic model for complex objects.""" + full_model_name = f"{self._base_name}{model_name}" + + if full_model_name in self._model_registry: + return self._model_registry[full_model_name] + + properties = schema.get("properties", {}) + required_fields = schema.get("required", []) + + if not properties: + return dict + + field_definitions = {} + for prop_name, prop_schema in properties.items(): + prop_desc = prop_schema.get("description", "") + is_required = prop_name in required_fields + + try: + prop_type = self._process_schema_type( + prop_schema, f"{model_name}{self._sanitize_name(prop_name).title()}" + ) + except Exception as e: + print(f"Warning: Could not process schema for {prop_name}: {e}") + prop_type = str + + field_definitions[prop_name] = self._create_field_definition( + prop_type, is_required, prop_desc + ) + + try: + nested_model = create_model(full_model_name, **field_definitions) + self._model_registry[full_model_name] = nested_model + return nested_model + except Exception as e: + print(f"Warning: Could not create nested model {full_model_name}: {e}") + return dict + + def _create_field_definition( + self, field_type: Type[Any], is_required: bool, description: str + ) -> tuple: + """Create Pydantic field definition based on type and requirement.""" + if is_required: + return (field_type, Field(description=description)) + else: + if get_origin(field_type) is Union: + return (field_type, Field(default=None, description=description)) + else: + return ( + Optional[field_type], + Field(default=None, description=description), + ) + + def _map_json_type_to_python(self, json_type: str) -> Type[Any]: + """Map basic JSON schema types to Python types.""" + type_mapping = { + "string": str, + "integer": int, + "number": float, + "boolean": bool, + "array": list, + "object": dict, + "null": type(None), + } + return type_mapping.get(json_type, str) + + def _get_required_nullable_fields(self) -> List[str]: + """Get a list of required nullable fields from the action schema.""" + schema_props, required = self._extract_schema_info(self.action_schema) + + required_nullable_fields = [] + for param_name in required: + param_details = schema_props.get(param_name, {}) + if self._is_nullable_type(param_details): + required_nullable_fields.append(param_name) + + return required_nullable_fields + + def _is_nullable_type(self, schema: Dict[str, Any]) -> bool: + """Check if a schema represents a nullable type.""" + if "anyOf" in schema: + return any(t.get("type") == "null" for t in schema["anyOf"]) + return schema.get("type") == "null" + + def _run(self, **kwargs) -> str: + """Execute the specific enterprise action with validated parameters.""" + try: + cleaned_kwargs = {} + for key, value in kwargs.items(): + if value is not None: + cleaned_kwargs[key] = value + + required_nullable_fields = self._get_required_nullable_fields() + + for field_name in required_nullable_fields: + if field_name not in cleaned_kwargs: + cleaned_kwargs[field_name] = None + + + api_url = f"{self.enterprise_api_base_url}/actions/{self.action_name}/execute" + headers = { + "Authorization": f"Bearer {self.enterprise_action_token}", + "Content-Type": "application/json", + } + payload = cleaned_kwargs + + response = requests.post( + url=api_url, headers=headers, json=payload, timeout=60 + ) + + data = response.json() + if not response.ok: + error_message = data.get("error", {}).get("message", json.dumps(data)) + return f"API request failed: {error_message}" + + return json.dumps(data, indent=2) + + except Exception as e: + return f"Error executing action {self.action_name}: {str(e)}" + + +class EnterpriseActionKitToolAdapter: + """Adapter that creates BaseTool instances for enterprise actions.""" + + def __init__( + self, + enterprise_action_token: str, + enterprise_api_base_url: Optional[str] = None, + ): + """Initialize the adapter with an enterprise action token.""" + self._set_enterprise_action_token(enterprise_action_token) + self._actions_schema = {} + self._tools = None + self.enterprise_api_base_url = enterprise_api_base_url or get_enterprise_api_base_url() + + def tools(self) -> List[BaseTool]: + """Get the list of tools created from enterprise actions.""" + if self._tools is None: + self._fetch_actions() + self._create_tools() + return self._tools or [] + + def _fetch_actions(self): + """Fetch available actions from the API.""" + try: + + actions_url = f"{self.enterprise_api_base_url}/actions" + headers = {"Authorization": f"Bearer {self.enterprise_action_token}"} + + response = requests.get( + actions_url, headers=headers, timeout=30 + ) + response.raise_for_status() + + raw_data = response.json() + if "actions" not in raw_data: + print(f"Unexpected API response structure: {raw_data}") + return + + parsed_schema = {} + action_categories = raw_data["actions"] + + for integration_type, action_list in action_categories.items(): + if isinstance(action_list, list): + for action in action_list: + action_name = action.get("name") + if action_name: + action_schema = { + "function": { + "name": action_name, + "description": action.get("description", f"Execute {action_name}"), + "parameters": action.get("parameters", {}) + } + } + parsed_schema[action_name] = action_schema + + self._actions_schema = parsed_schema + + except Exception as e: + print(f"Error fetching actions: {e}") + import traceback + + traceback.print_exc() + + def _generate_detailed_description( + self, schema: Dict[str, Any], indent: int = 0 + ) -> List[str]: + """Generate detailed description for nested schema structures.""" + descriptions = [] + indent_str = " " * indent + + schema_type = schema.get("type", "string") + + if schema_type == "object": + properties = schema.get("properties", {}) + required_fields = schema.get("required", []) + + if properties: + descriptions.append(f"{indent_str}Object with properties:") + for prop_name, prop_schema in properties.items(): + prop_desc = prop_schema.get("description", "") + is_required = prop_name in required_fields + req_str = " (required)" if is_required else " (optional)" + descriptions.append( + f"{indent_str} - {prop_name}: {prop_desc}{req_str}" + ) + + if prop_schema.get("type") == "object": + descriptions.extend( + self._generate_detailed_description(prop_schema, indent + 2) + ) + elif prop_schema.get("type") == "array": + items_schema = prop_schema.get("items", {}) + if items_schema.get("type") == "object": + descriptions.append(f"{indent_str} Array of objects:") + descriptions.extend( + self._generate_detailed_description( + items_schema, indent + 3 + ) + ) + elif "enum" in items_schema: + descriptions.append( + f"{indent_str} Array of enum values: {items_schema['enum']}" + ) + elif "enum" in prop_schema: + descriptions.append( + f"{indent_str} Enum values: {prop_schema['enum']}" + ) + + return descriptions + + def _create_tools(self): + """Create BaseTool instances for each action.""" + tools = [] + + for action_name, action_schema in self._actions_schema.items(): + function_details = action_schema.get("function", {}) + description = function_details.get("description", f"Execute {action_name}") + + parameters = function_details.get("parameters", {}) + param_descriptions = [] + + if parameters.get("properties"): + param_descriptions.append("\nDetailed Parameter Structure:") + param_descriptions.extend( + self._generate_detailed_description(parameters) + ) + + full_description = description + "\n".join(param_descriptions) + + tool = EnterpriseActionTool( + name=action_name.lower().replace(" ", "_"), + description=full_description, + action_name=action_name, + action_schema=action_schema, + enterprise_action_token=self.enterprise_action_token, + enterprise_api_base_url=self.enterprise_api_base_url, + ) + + tools.append(tool) + + self._tools = tools + + def _set_enterprise_action_token(self, enterprise_action_token: Optional[str]): + if enterprise_action_token and not enterprise_action_token.startswith("PK_"): + warnings.warn( + "Legacy token detected, please consider using the new Enterprise Action Auth token. Check out our docs for more information https://docs.crewai.com/en/enterprise/features/integrations.", + DeprecationWarning, + stacklevel=2 + ) + + token = enterprise_action_token or os.environ.get("CREWAI_ENTERPRISE_TOOLS_TOKEN") + + self.enterprise_action_token = token + + def __enter__(self): + return self.tools() + + def __exit__(self, exc_type, exc_val, exc_tb): + pass diff --git a/crewai_tools/adapters/lancedb_adapter.py b/crewai_tools/adapters/lancedb_adapter.py new file mode 100644 index 000000000..c91423048 --- /dev/null +++ b/crewai_tools/adapters/lancedb_adapter.py @@ -0,0 +1,56 @@ +from pathlib import Path +from typing import Any, Callable + +from lancedb import DBConnection as LanceDBConnection +from lancedb import connect as lancedb_connect +from lancedb.table import Table as LanceDBTable +from openai import Client as OpenAIClient +from pydantic import Field, PrivateAttr + +from crewai_tools.tools.rag.rag_tool import Adapter + + +def _default_embedding_function(): + client = OpenAIClient() + + def _embedding_function(input): + rs = client.embeddings.create(input=input, model="text-embedding-ada-002") + return [record.embedding for record in rs.data] + + return _embedding_function + + +class LanceDBAdapter(Adapter): + uri: str | Path + table_name: str + embedding_function: Callable = Field(default_factory=_default_embedding_function) + top_k: int = 3 + vector_column_name: str = "vector" + text_column_name: str = "text" + + _db: LanceDBConnection = PrivateAttr() + _table: LanceDBTable = PrivateAttr() + + def model_post_init(self, __context: Any) -> None: + self._db = lancedb_connect(self.uri) + self._table = self._db.open_table(self.table_name) + + super().model_post_init(__context) + + def query(self, question: str) -> str: + query = self.embedding_function([question])[0] + results = ( + self._table.search(query, vector_column_name=self.vector_column_name) + .limit(self.top_k) + .select([self.text_column_name]) + .to_list() + ) + values = [result[self.text_column_name] for result in results] + return "\n".join(values) + + def add( + self, + *args: Any, + **kwargs: Any, + ) -> None: + self._table.add(*args, **kwargs) diff --git a/crewai_tools/adapters/mcp_adapter.py b/crewai_tools/adapters/mcp_adapter.py new file mode 100644 index 000000000..8e602f376 --- /dev/null +++ b/crewai_tools/adapters/mcp_adapter.py @@ -0,0 +1,163 @@ +from __future__ import annotations + +import logging +from typing import TYPE_CHECKING, Any + +from crewai.tools import BaseTool +from crewai_tools.adapters.tool_collection import ToolCollection +""" +MCPServer for CrewAI. + + +""" +logger = logging.getLogger(__name__) + +if TYPE_CHECKING: + from mcp import StdioServerParameters + from mcpadapt.core import MCPAdapt + from mcpadapt.crewai_adapter import CrewAIAdapter + + +try: + from mcp import StdioServerParameters + from mcpadapt.core import MCPAdapt + from mcpadapt.crewai_adapter import CrewAIAdapter + + MCP_AVAILABLE = True +except ImportError: + MCP_AVAILABLE = False + + +class MCPServerAdapter: + """Manages the lifecycle of an MCP server and make its tools available to CrewAI. + + Note: tools can only be accessed after the server has been started with the + `start()` method. + + Attributes: + tools: The CrewAI tools available from the MCP server. + + Usage: + # context manager + stdio + with MCPServerAdapter(...) as tools: + # tools is now available + + # context manager + sse + with MCPServerAdapter({"url": "http://localhost:8000/sse"}) as tools: + # tools is now available + + # context manager with filtered tools + with MCPServerAdapter(..., "tool1", "tool2") as filtered_tools: + # only tool1 and tool2 are available + + # context manager with custom connect timeout (60 seconds) + with MCPServerAdapter(..., connect_timeout=60) as tools: + # tools is now available with longer timeout + + # manually stop mcp server + try: + mcp_server = MCPServerAdapter(...) + tools = mcp_server.tools # all tools + + # or with filtered tools and custom timeout + mcp_server = MCPServerAdapter(..., "tool1", "tool2", connect_timeout=45) + filtered_tools = mcp_server.tools # only tool1 and tool2 + ... + finally: + mcp_server.stop() + + # Best practice is ensure cleanup is done after use. + mcp_server.stop() # run after crew().kickoff() + """ + + def __init__( + self, + serverparams: StdioServerParameters | dict[str, Any], + *tool_names: str, + connect_timeout: int = 30, + ): + """Initialize the MCP Server + + Args: + serverparams: The parameters for the MCP server it supports either a + `StdioServerParameters` or a `dict` respectively for STDIO and SSE. + *tool_names: Optional names of tools to filter. If provided, only tools with + matching names will be available. + connect_timeout: Connection timeout in seconds to the MCP server (default is 30s). + + """ + + super().__init__() + self._adapter = None + self._tools = None + self._tool_names = list(tool_names) if tool_names else None + + if not MCP_AVAILABLE: + import click + + if click.confirm( + "You are missing the 'mcp' package. Would you like to install it?" + ): + import subprocess + + try: + subprocess.run(["uv", "add", "mcp crewai-tools[mcp]"], check=True) + + except subprocess.CalledProcessError: + raise ImportError("Failed to install mcp package") + else: + raise ImportError( + "`mcp` package not found, please run `uv add crewai-tools[mcp]`" + ) + + try: + self._serverparams = serverparams + self._adapter = MCPAdapt(self._serverparams, CrewAIAdapter(), connect_timeout) + self.start() + + except Exception as e: + if self._adapter is not None: + try: + self.stop() + except Exception as stop_e: + logger.error(f"Error during stop cleanup: {stop_e}") + raise RuntimeError(f"Failed to initialize MCP Adapter: {e}") from e + + def start(self): + """Start the MCP server and initialize the tools.""" + self._tools = self._adapter.__enter__() + + def stop(self): + """Stop the MCP server""" + self._adapter.__exit__(None, None, None) + + @property + def tools(self) -> ToolCollection[BaseTool]: + """The CrewAI tools available from the MCP server. + + Raises: + ValueError: If the MCP server is not started. + + Returns: + The CrewAI tools available from the MCP server. + """ + if self._tools is None: + raise ValueError( + "MCP server not started, run `mcp_server.start()` first before accessing `tools`" + ) + + tools_collection = ToolCollection(self._tools) + if self._tool_names: + return tools_collection.filter_by_names(self._tool_names) + return tools_collection + + def __enter__(self): + """ + Enter the context manager. Note that `__init__()` already starts the MCP server. + So tools should already be available. + """ + return self.tools + + def __exit__(self, exc_type, exc_value, traceback): + """Exit the context manager.""" + return self._adapter.__exit__(exc_type, exc_value, traceback) diff --git a/crewai_tools/adapters/pdf_embedchain_adapter.py b/crewai_tools/adapters/pdf_embedchain_adapter.py new file mode 100644 index 000000000..aa682c84f --- /dev/null +++ b/crewai_tools/adapters/pdf_embedchain_adapter.py @@ -0,0 +1,41 @@ +from typing import Any, Optional + +from crewai_tools.tools.rag.rag_tool import Adapter + +try: + from embedchain import App + EMBEDCHAIN_AVAILABLE = True +except ImportError: + EMBEDCHAIN_AVAILABLE = False + + +class PDFEmbedchainAdapter(Adapter): + embedchain_app: Any # Will be App when embedchain is available + summarize: bool = False + src: Optional[str] = None + + def __init__(self, **data): + if not EMBEDCHAIN_AVAILABLE: + raise ImportError("embedchain is not installed. Please install it with `pip install crewai-tools[embedchain]`") + super().__init__(**data) + + def query(self, question: str) -> str: + where = ( + {"app_id": self.embedchain_app.config.id, "source": self.src} + if self.src + else None + ) + result, sources = self.embedchain_app.query( + question, citations=True, dry_run=(not self.summarize), where=where + ) + if self.summarize: + return result + return "\n\n".join([source[0] for source in sources]) + + def add( + self, + *args: Any, + **kwargs: Any, + ) -> None: + self.src = args[0] if args else None + self.embedchain_app.add(*args, **kwargs) diff --git a/crewai_tools/adapters/rag_adapter.py b/crewai_tools/adapters/rag_adapter.py new file mode 100644 index 000000000..78011328c --- /dev/null +++ b/crewai_tools/adapters/rag_adapter.py @@ -0,0 +1,41 @@ +from typing import Any, Optional + +from crewai_tools.rag.core import RAG +from crewai_tools.tools.rag.rag_tool import Adapter + + +class RAGAdapter(Adapter): + def __init__( + self, + collection_name: str = "crewai_knowledge_base", + persist_directory: Optional[str] = None, + embedding_model: str = "text-embedding-3-small", + top_k: int = 5, + embedding_api_key: Optional[str] = None, + **embedding_kwargs + ): + super().__init__() + + # Prepare embedding configuration + embedding_config = { + "api_key": embedding_api_key, + **embedding_kwargs + } + + self._adapter = RAG( + collection_name=collection_name, + persist_directory=persist_directory, + embedding_model=embedding_model, + top_k=top_k, + embedding_config=embedding_config + ) + + def query(self, question: str) -> str: + return self._adapter.query(question) + + def add( + self, + *args: Any, + **kwargs: Any, + ) -> None: + self._adapter.add(*args, **kwargs) diff --git a/crewai_tools/adapters/tool_collection.py b/crewai_tools/adapters/tool_collection.py new file mode 100644 index 000000000..291fa8f82 --- /dev/null +++ b/crewai_tools/adapters/tool_collection.py @@ -0,0 +1,74 @@ +from typing import List, Optional, Union, TypeVar, Generic, Dict, Callable +from crewai.tools import BaseTool + +T = TypeVar('T', bound=BaseTool) + +class ToolCollection(list, Generic[T]): + """ + A collection of tools that can be accessed by index or name + + This class extends the built-in list to provide dictionary-like + access to tools based on their name property. + + Usage: + tools = ToolCollection(list_of_tools) + # Access by index (regular list behavior) + first_tool = tools[0] + # Access by name (new functionality) + search_tool = tools["search"] + """ + + def __init__(self, tools: Optional[List[T]] = None): + super().__init__(tools or []) + self._name_cache: Dict[str, T] = {} + self._build_name_cache() + + def _build_name_cache(self) -> None: + self._name_cache = {tool.name.lower(): tool for tool in self} + + def __getitem__(self, key: Union[int, str]) -> T: + if isinstance(key, str): + return self._name_cache[key.lower()] + return super().__getitem__(key) + + def append(self, tool: T) -> None: + super().append(tool) + self._name_cache[tool.name.lower()] = tool + + def extend(self, tools: List[T]) -> None: + super().extend(tools) + self._build_name_cache() + + def insert(self, index: int, tool: T) -> None: + super().insert(index, tool) + self._name_cache[tool.name.lower()] = tool + + def remove(self, tool: T) -> None: + super().remove(tool) + if tool.name.lower() in self._name_cache: + del self._name_cache[tool.name.lower()] + + def pop(self, index: int = -1) -> T: + tool = super().pop(index) + if tool.name.lower() in self._name_cache: + del self._name_cache[tool.name.lower()] + return tool + + def filter_by_names(self, names: Optional[List[str]] = None) -> "ToolCollection[T]": + if names is None: + return self + + return ToolCollection( + [ + tool + for name in names + if (tool := self._name_cache.get(name.lower())) is not None + ] + ) + + def filter_where(self, func: Callable[[T], bool]) -> "ToolCollection[T]": + return ToolCollection([tool for tool in self if func(tool)]) + + def clear(self) -> None: + super().clear() + self._name_cache.clear() \ No newline at end of file diff --git a/crewai_tools/adapters/zapier_adapter.py b/crewai_tools/adapters/zapier_adapter.py new file mode 100644 index 000000000..78c996964 --- /dev/null +++ b/crewai_tools/adapters/zapier_adapter.py @@ -0,0 +1,122 @@ +import os +import logging +from typing import List + +import requests +from crewai.tools import BaseTool +from pydantic import Field, create_model + +ACTIONS_URL = "https://actions.zapier.com/api/v2/ai-actions" + +logger = logging.getLogger(__name__) + + +class ZapierActionTool(BaseTool): + """ + A tool that wraps a Zapier action + """ + + name: str = Field(description="Tool name") + description: str = Field(description="Tool description") + action_id: str = Field(description="Zapier action ID") + api_key: str = Field(description="Zapier API key") + + def _run(self, **kwargs) -> str: + """Execute the Zapier action""" + headers = {"x-api-key": self.api_key, "Content-Type": "application/json"} + + instructions = kwargs.pop( + "instructions", "Execute this action with the provided parameters" + ) + + if not kwargs: + action_params = {"instructions": instructions, "params": {}} + else: + formatted_params = {} + for key, value in kwargs.items(): + formatted_params[key] = { + "value": value, + "mode": "guess", + } + action_params = {"instructions": instructions, "params": formatted_params} + + execute_url = f"{ACTIONS_URL}/{self.action_id}/execute/" + response = requests.request( + "POST", execute_url, headers=headers, json=action_params + ) + + response.raise_for_status() + + return response.json() + + +class ZapierActionsAdapter: + """ + Adapter for Zapier Actions + """ + + api_key: str + + def __init__(self, api_key: str = None): + self.api_key = api_key or os.getenv("ZAPIER_API_KEY") + if not self.api_key: + logger.error("Zapier Actions API key is required") + raise ValueError("Zapier Actions API key is required") + + def get_zapier_actions(self): + headers = { + "x-api-key": self.api_key, + } + response = requests.request("GET", ACTIONS_URL, headers=headers) + response.raise_for_status() + + response_json = response.json() + return response_json + + def tools(self) -> List[BaseTool]: + """Convert Zapier actions to BaseTool instances""" + actions_response = self.get_zapier_actions() + tools = [] + + for action in actions_response.get("results", []): + tool_name = ( + action["meta"]["action_label"] + .replace(" ", "_") + .replace(":", "") + .lower() + ) + + params = action.get("params", {}) + args_fields = {} + + args_fields["instructions"] = ( + str, + Field(description="Instructions for how to execute this action"), + ) + + for param_name, param_info in params.items(): + field_type = ( + str # Default to string, could be enhanced based on param_info + ) + field_description = ( + param_info.get("description", "") + if isinstance(param_info, dict) + else "" + ) + args_fields[param_name] = ( + field_type, + Field(description=field_description), + ) + + args_schema = create_model(f"{tool_name.title()}Schema", **args_fields) + + tool = ZapierActionTool( + name=tool_name, + description=action["description"], + action_id=action["id"], + api_key=self.api_key, + args_schema=args_schema, + ) + tools.append(tool) + + return tools diff --git a/crewai_tools/aws/__init__.py b/crewai_tools/aws/__init__.py new file mode 100644 index 000000000..b2d279078 --- /dev/null +++ b/crewai_tools/aws/__init__.py @@ -0,0 +1,16 @@ +from .s3 import S3ReaderTool, S3WriterTool +from .bedrock import ( + BedrockKBRetrieverTool, + BedrockInvokeAgentTool, + create_browser_toolkit, + create_code_interpreter_toolkit, +) + +__all__ = [ + "S3ReaderTool", + "S3WriterTool", + "BedrockKBRetrieverTool", + "BedrockInvokeAgentTool", + "create_browser_toolkit", + "create_code_interpreter_toolkit" +] diff --git a/crewai_tools/aws/bedrock/__init__.py b/crewai_tools/aws/bedrock/__init__.py new file mode 100644 index 000000000..58fc5bca9 --- /dev/null +++ b/crewai_tools/aws/bedrock/__init__.py @@ -0,0 +1,11 @@ +from .knowledge_base.retriever_tool import BedrockKBRetrieverTool +from .agents.invoke_agent_tool import BedrockInvokeAgentTool +from .browser import create_browser_toolkit +from .code_interpreter import create_code_interpreter_toolkit + +__all__ = [ + "BedrockKBRetrieverTool", + "BedrockInvokeAgentTool", + "create_browser_toolkit", + "create_code_interpreter_toolkit" +] diff --git a/crewai_tools/aws/bedrock/agents/README.md b/crewai_tools/aws/bedrock/agents/README.md new file mode 100644 index 000000000..7aa43b65d --- /dev/null +++ b/crewai_tools/aws/bedrock/agents/README.md @@ -0,0 +1,181 @@ +# BedrockInvokeAgentTool + +The `BedrockInvokeAgentTool` enables CrewAI agents to invoke Amazon Bedrock Agents and leverage their capabilities within your workflows. + +## Installation + +```bash +pip install 'crewai[tools]' +``` + +## Requirements + +- AWS credentials configured (either through environment variables or AWS CLI) +- `boto3` and `python-dotenv` packages +- Access to Amazon Bedrock Agents + +## Usage + +Here's how to use the tool with a CrewAI agent: + +```python +from crewai import Agent, Task, Crew +from crewai_tools.aws.bedrock.agents.invoke_agent_tool import BedrockInvokeAgentTool + +# Initialize the tool +agent_tool = BedrockInvokeAgentTool( + agent_id="your-agent-id", + agent_alias_id="your-agent-alias-id" +) + +# Create a CrewAI agent that uses the tool +aws_expert = Agent( + role='AWS Service Expert', + goal='Help users understand AWS services and quotas', + backstory='I am an expert in AWS services and can provide detailed information about them.', + tools=[agent_tool], + verbose=True +) + +# Create a task for the agent +quota_task = Task( + description="Find out the current service quotas for EC2 in us-west-2 and explain any recent changes.", + agent=aws_expert +) + +# Create a crew with the agent +crew = Crew( + agents=[aws_expert], + tasks=[quota_task], + verbose=2 +) + +# Run the crew +result = crew.kickoff() +print(result) +``` + +## Tool Arguments + +| Argument | Type | Required | Default | Description | +|----------|------|----------|---------|-------------| +| agent_id | str | Yes | None | The unique identifier of the Bedrock agent | +| agent_alias_id | str | Yes | None | The unique identifier of the agent alias | +| session_id | str | No | timestamp | The unique identifier of the session | +| enable_trace | bool | No | False | Whether to enable trace for debugging | +| end_session | bool | No | False | Whether to end the session after invocation | +| description | str | No | None | Custom description for the tool | + +## Environment Variables + +```bash +BEDROCK_AGENT_ID=your-agent-id # Alternative to passing agent_id +BEDROCK_AGENT_ALIAS_ID=your-agent-alias-id # Alternative to passing agent_alias_id +AWS_REGION=your-aws-region # Defaults to us-west-2 +AWS_ACCESS_KEY_ID=your-access-key # Required for AWS authentication +AWS_SECRET_ACCESS_KEY=your-secret-key # Required for AWS authentication +``` + +## Advanced Usage + +### Multi-Agent Workflow with Session Management + +```python +from crewai import Agent, Task, Crew, Process +from crewai_tools.aws.bedrock.agents.invoke_agent_tool import BedrockInvokeAgentTool + +# Initialize tools with session management +initial_tool = BedrockInvokeAgentTool( + agent_id="your-agent-id", + agent_alias_id="your-agent-alias-id", + session_id="custom-session-id" +) + +followup_tool = BedrockInvokeAgentTool( + agent_id="your-agent-id", + agent_alias_id="your-agent-alias-id", + session_id="custom-session-id" +) + +final_tool = BedrockInvokeAgentTool( + agent_id="your-agent-id", + agent_alias_id="your-agent-alias-id", + session_id="custom-session-id", + end_session=True +) + +# Create agents for different stages +researcher = Agent( + role='AWS Service Researcher', + goal='Gather information about AWS services', + backstory='I am specialized in finding detailed AWS service information.', + tools=[initial_tool] +) + +analyst = Agent( + role='Service Compatibility Analyst', + goal='Analyze service compatibility and requirements', + backstory='I analyze AWS services for compatibility and integration possibilities.', + tools=[followup_tool] +) + +summarizer = Agent( + role='Technical Documentation Writer', + goal='Create clear technical summaries', + backstory='I specialize in creating clear, concise technical documentation.', + tools=[final_tool] +) + +# Create tasks +research_task = Task( + description="Find all available AWS services in us-west-2 region.", + agent=researcher +) + +analysis_task = Task( + description="Analyze which services support IPv6 and their implementation requirements.", + agent=analyst +) + +summary_task = Task( + description="Create a summary of IPv6-compatible services and their key features.", + agent=summarizer +) + +# Create a crew with the agents and tasks +crew = Crew( + agents=[researcher, analyst, summarizer], + tasks=[research_task, analysis_task, summary_task], + process=Process.sequential, + verbose=2 +) + +# Run the crew +result = crew.kickoff() +``` + +## Use Cases + +### Hybrid Multi-Agent Collaborations +- Create workflows where CrewAI agents collaborate with managed Bedrock agents running as services in AWS +- Enable scenarios where sensitive data processing happens within your AWS environment while other agents operate externally +- Bridge on-premises CrewAI agents with cloud-based Bedrock agents for distributed intelligence workflows + +### Data Sovereignty and Compliance +- Keep data-sensitive agentic workflows within your AWS environment while allowing external CrewAI agents to orchestrate tasks +- Maintain compliance with data residency requirements by processing sensitive information only within your AWS account +- Enable secure multi-agent collaborations where some agents cannot access your organization's private data + +### Seamless AWS Service Integration +- Access any AWS service through Amazon Bedrock Actions without writing complex integration code +- Enable CrewAI agents to interact with AWS services through natural language requests +- Leverage pre-built Bedrock agent capabilities to interact with AWS services like Bedrock Knowledge Bases, Lambda, and more + +### Scalable Hybrid Agent Architectures +- Offload computationally intensive tasks to managed Bedrock agents while lightweight tasks run in CrewAI +- Scale agent processing by distributing workloads between local CrewAI agents and cloud-based Bedrock agents + +### Cross-Organizational Agent Collaboration +- Enable secure collaboration between your organization's CrewAI agents and partner organizations' Bedrock agents +- Create workflows where external expertise from Bedrock agents can be incorporated without exposing sensitive data +- Build agent ecosystems that span organizational boundaries while maintaining security and data control \ No newline at end of file diff --git a/crewai_tools/aws/bedrock/agents/__init__.py b/crewai_tools/aws/bedrock/agents/__init__.py new file mode 100644 index 000000000..b1f799872 --- /dev/null +++ b/crewai_tools/aws/bedrock/agents/__init__.py @@ -0,0 +1,3 @@ +from .invoke_agent_tool import BedrockInvokeAgentTool + +__all__ = ["BedrockInvokeAgentTool"] diff --git a/crewai_tools/aws/bedrock/agents/invoke_agent_tool.py b/crewai_tools/aws/bedrock/agents/invoke_agent_tool.py new file mode 100644 index 000000000..65280fe7b --- /dev/null +++ b/crewai_tools/aws/bedrock/agents/invoke_agent_tool.py @@ -0,0 +1,176 @@ +from typing import Type, Optional, Dict, Any, List +import os +import json +import uuid +import time +from datetime import datetime, timezone +from dotenv import load_dotenv + +from crewai.tools import BaseTool +from pydantic import BaseModel, Field + +from ..exceptions import BedrockAgentError, BedrockValidationError + +# Load environment variables from .env file +load_dotenv() + + +class BedrockInvokeAgentToolInput(BaseModel): + """Input schema for BedrockInvokeAgentTool.""" + query: str = Field(..., description="The query to send to the agent") + + +class BedrockInvokeAgentTool(BaseTool): + name: str = "Bedrock Agent Invoke Tool" + description: str = "An agent responsible for policy analysis." + args_schema: Type[BaseModel] = BedrockInvokeAgentToolInput + agent_id: str = None + agent_alias_id: str = None + session_id: str = None + enable_trace: bool = False + end_session: bool = False + package_dependencies: List[str] = ["boto3"] + + def __init__( + self, + agent_id: str = None, + agent_alias_id: str = None, + session_id: str = None, + enable_trace: bool = False, + end_session: bool = False, + description: Optional[str] = None, + **kwargs + ): + """Initialize the BedrockInvokeAgentTool with agent configuration. + + Args: + agent_id (str): The unique identifier of the Bedrock agent + agent_alias_id (str): The unique identifier of the agent alias + session_id (str): The unique identifier of the session + enable_trace (bool): Whether to enable trace for the agent invocation + end_session (bool): Whether to end the session with the agent + description (Optional[str]): Custom description for the tool + """ + super().__init__(**kwargs) + + # Get values from environment variables if not provided + self.agent_id = agent_id or os.getenv('BEDROCK_AGENT_ID') + self.agent_alias_id = agent_alias_id or os.getenv('BEDROCK_AGENT_ALIAS_ID') + self.session_id = session_id or str(int(time.time())) # Use timestamp as session ID if not provided + self.enable_trace = enable_trace + self.end_session = end_session + + # Update the description if provided + if description: + self.description = description + + # Validate parameters + self._validate_parameters() + + def _validate_parameters(self): + """Validate the parameters according to AWS API requirements.""" + try: + # Validate agent_id + if not self.agent_id: + raise BedrockValidationError("agent_id cannot be empty") + if not isinstance(self.agent_id, str): + raise BedrockValidationError("agent_id must be a string") + + # Validate agent_alias_id + if not self.agent_alias_id: + raise BedrockValidationError("agent_alias_id cannot be empty") + if not isinstance(self.agent_alias_id, str): + raise BedrockValidationError("agent_alias_id must be a string") + + # Validate session_id if provided + if self.session_id and not isinstance(self.session_id, str): + raise BedrockValidationError("session_id must be a string") + + except BedrockValidationError as e: + raise BedrockValidationError(f"Parameter validation failed: {str(e)}") + + def _run(self, query: str) -> str: + try: + import boto3 + from botocore.exceptions import ClientError + except ImportError: + raise ImportError("`boto3` package not found, please run `uv add boto3`") + + try: + # Initialize the Bedrock Agent Runtime client + bedrock_agent = boto3.client( + "bedrock-agent-runtime", + region_name=os.getenv('AWS_REGION', os.getenv('AWS_DEFAULT_REGION', 'us-west-2')) + ) + + # Format the prompt with current time + current_utc = datetime.now(timezone.utc) + prompt = f""" +The current time is: {current_utc} + +Below is the users query or task. Complete it and answer it consicely and to the point: +{query} +""" + + # Invoke the agent + response = bedrock_agent.invoke_agent( + agentId=self.agent_id, + agentAliasId=self.agent_alias_id, + sessionId=self.session_id, + inputText=prompt, + enableTrace=self.enable_trace, + endSession=self.end_session + ) + + # Process the response + completion = "" + + # Check if response contains a completion field + if 'completion' in response: + # Process streaming response format + for event in response.get('completion', []): + if 'chunk' in event and 'bytes' in event['chunk']: + chunk_bytes = event['chunk']['bytes'] + if isinstance(chunk_bytes, (bytes, bytearray)): + completion += chunk_bytes.decode('utf-8') + else: + completion += str(chunk_bytes) + + # If no completion found in streaming format, try direct format + if not completion and 'chunk' in response and 'bytes' in response['chunk']: + chunk_bytes = response['chunk']['bytes'] + if isinstance(chunk_bytes, (bytes, bytearray)): + completion = chunk_bytes.decode('utf-8') + else: + completion = str(chunk_bytes) + + # If still no completion, return debug info + if not completion: + debug_info = { + "error": "Could not extract completion from response", + "response_keys": list(response.keys()) + } + + # Add more debug info + if 'chunk' in response: + debug_info["chunk_keys"] = list(response['chunk'].keys()) + + raise BedrockAgentError(f"Failed to extract completion: {json.dumps(debug_info, indent=2)}") + + return completion + + except ClientError as e: + error_code = "Unknown" + error_message = str(e) + + # Try to extract error code if available + if hasattr(e, 'response') and 'Error' in e.response: + error_code = e.response['Error'].get('Code', 'Unknown') + error_message = e.response['Error'].get('Message', str(e)) + + raise BedrockAgentError(f"Error ({error_code}): {error_message}") + except BedrockAgentError: + # Re-raise BedrockAgentError exceptions + raise + except Exception as e: + raise BedrockAgentError(f"Unexpected error: {str(e)}") \ No newline at end of file diff --git a/crewai_tools/aws/bedrock/browser/README.md b/crewai_tools/aws/bedrock/browser/README.md new file mode 100644 index 000000000..7f0188bbb --- /dev/null +++ b/crewai_tools/aws/bedrock/browser/README.md @@ -0,0 +1,158 @@ +# AWS Bedrock Browser Tools + +This toolkit provides a set of tools for interacting with web browsers through AWS Bedrock Browser. It enables your CrewAI agents to navigate websites, extract content, click elements, and more. + +## Features + +- Navigate to URLs and browse the web +- Extract text and hyperlinks from pages +- Click on elements using CSS selectors +- Navigate back through browser history +- Get information about the current webpage +- Multiple browser sessions with thread-based isolation + +## Installation + +Ensure you have the necessary dependencies: + +```bash +uv add crewai-tools bedrock-agentcore beautifulsoup4 playwright nest-asyncio +``` + +## Usage + +### Basic Usage + +```python +from crewai import Agent, Task, Crew, LLM +from crewai_tools.aws.bedrock.browser import create_browser_toolkit + +# Create the browser toolkit +toolkit, browser_tools = create_browser_toolkit(region="us-west-2") + +# Create the Bedrock LLM +llm = LLM( + model="bedrock/us.anthropic.claude-3-7-sonnet-20250219-v1:0", + region_name="us-west-2", +) + +# Create a CrewAI agent that uses the browser tools +research_agent = Agent( + role="Web Researcher", + goal="Research and summarize web content", + backstory="You're an expert at finding information online.", + tools=browser_tools, + llm=llm +) + +# Create a task for the agent +research_task = Task( + description="Navigate to https://example.com and extract all text content. Summarize the main points.", + expected_output="A list of bullet points containing the most important information on https://example.com. Plus, a description of the tool calls used, and actions performed to get to the page.", + agent=research_agent +) + +# Create and run the crew +crew = Crew( + agents=[research_agent], + tasks=[research_task] +) +result = crew.kickoff() + +print(f"\n***Final result:***\n\n{result}") + +# Clean up browser resources when done +toolkit.sync_cleanup() +``` + +### Available Tools + +The toolkit provides the following tools: + +1. `navigate_browser` - Navigate to a URL +2. `click_element` - Click on an element using CSS selectors +3. `extract_text` - Extract all text from the current webpage +4. `extract_hyperlinks` - Extract all hyperlinks from the current webpage +5. `get_elements` - Get elements matching a CSS selector +6. `navigate_back` - Navigate to the previous page +7. `current_webpage` - Get information about the current webpage + +### Advanced Usage (with async) + +```python +import asyncio +from crewai import Agent, Task, Crew, LLM +from crewai_tools.aws.bedrock.browser import create_browser_toolkit + +async def main(): + + # Create the browser toolkit with specific AWS region + toolkit, browser_tools = create_browser_toolkit(region="us-west-2") + tools_by_name = toolkit.get_tools_by_name() + + # Create the Bedrock LLM + llm = LLM( + model="bedrock/us.anthropic.claude-3-7-sonnet-20250219-v1:0", + region_name="us-west-2", + ) + + # Create agents with specific tools + navigator_agent = Agent( + role="Navigator", + goal="Find specific information across websites", + backstory="You navigate through websites to locate information.", + tools=[ + tools_by_name["navigate_browser"], + tools_by_name["click_element"], + tools_by_name["navigate_back"] + ], + llm=llm + ) + + content_agent = Agent( + role="Content Extractor", + goal="Extract and analyze webpage content", + backstory="You extract and analyze content from webpages.", + tools=[ + tools_by_name["extract_text"], + tools_by_name["extract_hyperlinks"], + tools_by_name["get_elements"] + ], + llm=llm + ) + + # Create tasks for the agents + navigation_task = Task( + description="Navigate to https://example.com, then click on the the 'More information...' link.", + expected_output="The status of the tool calls for this task.", + agent=navigator_agent, + ) + + extraction_task = Task( + description="Extract all text from the current page and summarize it.", + expected_output="The summary of the page, and a description of the tool calls used, and actions performed to get to the page.", + agent=content_agent, + ) + + # Create and run the crew + crew = Crew( + agents=[navigator_agent, content_agent], + tasks=[navigation_task, extraction_task] + ) + + result = await crew.kickoff_async() + + # Clean up browser resources when done + toolkit.sync_cleanup() + + return result + +if __name__ == "__main__": + result = asyncio.run(main()) + print(f"\n***Final result:***\n\n{result}") +``` + +## Requirements + +- AWS account with access to Bedrock AgentCore API +- Properly configured AWS credentials \ No newline at end of file diff --git a/crewai_tools/aws/bedrock/browser/__init__.py b/crewai_tools/aws/bedrock/browser/__init__.py new file mode 100644 index 000000000..e82666ebc --- /dev/null +++ b/crewai_tools/aws/bedrock/browser/__init__.py @@ -0,0 +1,3 @@ +from .browser_toolkit import BrowserToolkit, create_browser_toolkit + +__all__ = ["BrowserToolkit", "create_browser_toolkit"] \ No newline at end of file diff --git a/crewai_tools/aws/bedrock/browser/browser_session_manager.py b/crewai_tools/aws/bedrock/browser/browser_session_manager.py new file mode 100644 index 000000000..d4652c320 --- /dev/null +++ b/crewai_tools/aws/bedrock/browser/browser_session_manager.py @@ -0,0 +1,260 @@ +from __future__ import annotations + +import logging +from typing import TYPE_CHECKING, Dict, Tuple + +if TYPE_CHECKING: + from playwright.async_api import Browser as AsyncBrowser + from playwright.sync_api import Browser as SyncBrowser + from bedrock_agentcore.tools.browser_client import BrowserClient + +logger = logging.getLogger(__name__) + + +class BrowserSessionManager: + """ + Manages browser sessions for different threads. + + This class maintains separate browser sessions for different threads, + enabling concurrent usage of browsers in multi-threaded environments. + Browsers are created lazily only when needed by tools. + """ + + def __init__(self, region: str = "us-west-2"): + """ + Initialize the browser session manager. + + Args: + region: AWS region for browser client + """ + self.region = region + self._async_sessions: Dict[str, Tuple[BrowserClient, AsyncBrowser]] = {} + self._sync_sessions: Dict[str, Tuple[BrowserClient, SyncBrowser]] = {} + + async def get_async_browser(self, thread_id: str) -> AsyncBrowser: + """ + Get or create an async browser for the specified thread. + + Args: + thread_id: Unique identifier for the thread requesting the browser + + Returns: + An async browser instance specific to the thread + """ + if thread_id in self._async_sessions: + return self._async_sessions[thread_id][1] + + return await self._create_async_browser_session(thread_id) + + def get_sync_browser(self, thread_id: str) -> SyncBrowser: + """ + Get or create a sync browser for the specified thread. + + Args: + thread_id: Unique identifier for the thread requesting the browser + + Returns: + A sync browser instance specific to the thread + """ + if thread_id in self._sync_sessions: + return self._sync_sessions[thread_id][1] + + return self._create_sync_browser_session(thread_id) + + async def _create_async_browser_session(self, thread_id: str) -> AsyncBrowser: + """ + Create a new async browser session for the specified thread. + + Args: + thread_id: Unique identifier for the thread + + Returns: + The newly created async browser instance + + Raises: + Exception: If browser session creation fails + """ + from bedrock_agentcore.tools.browser_client import BrowserClient + browser_client = BrowserClient(region=self.region) + + try: + # Start browser session + browser_client.start() + + # Get WebSocket connection info + ws_url, headers = browser_client.generate_ws_headers() + + logger.info( + f"Connecting to async WebSocket endpoint for thread {thread_id}: {ws_url}" + ) + + from playwright.async_api import async_playwright + + # Connect to browser using Playwright + playwright = await async_playwright().start() + browser = await playwright.chromium.connect_over_cdp( + endpoint_url=ws_url, headers=headers, timeout=30000 + ) + logger.info( + f"Successfully connected to async browser for thread {thread_id}" + ) + + # Store session resources + self._async_sessions[thread_id] = (browser_client, browser) + + return browser + + except Exception as e: + logger.error( + f"Failed to create async browser session for thread {thread_id}: {e}" + ) + + # Clean up resources if session creation fails + if browser_client: + try: + browser_client.stop() + except Exception as cleanup_error: + logger.warning(f"Error cleaning up browser client: {cleanup_error}") + + raise + + def _create_sync_browser_session(self, thread_id: str) -> SyncBrowser: + """ + Create a new sync browser session for the specified thread. + + Args: + thread_id: Unique identifier for the thread + + Returns: + The newly created sync browser instance + + Raises: + Exception: If browser session creation fails + """ + from bedrock_agentcore.tools.browser_client import BrowserClient + browser_client = BrowserClient(region=self.region) + + try: + # Start browser session + browser_client.start() + + # Get WebSocket connection info + ws_url, headers = browser_client.generate_ws_headers() + + logger.info( + f"Connecting to sync WebSocket endpoint for thread {thread_id}: {ws_url}" + ) + + from playwright.sync_api import sync_playwright + + # Connect to browser using Playwright + playwright = sync_playwright().start() + browser = playwright.chromium.connect_over_cdp( + endpoint_url=ws_url, headers=headers, timeout=30000 + ) + logger.info( + f"Successfully connected to sync browser for thread {thread_id}" + ) + + # Store session resources + self._sync_sessions[thread_id] = (browser_client, browser) + + return browser + + except Exception as e: + logger.error( + f"Failed to create sync browser session for thread {thread_id}: {e}" + ) + + # Clean up resources if session creation fails + if browser_client: + try: + browser_client.stop() + except Exception as cleanup_error: + logger.warning(f"Error cleaning up browser client: {cleanup_error}") + + raise + + async def close_async_browser(self, thread_id: str) -> None: + """ + Close the async browser session for the specified thread. + + Args: + thread_id: Unique identifier for the thread + """ + if thread_id not in self._async_sessions: + logger.warning(f"No async browser session found for thread {thread_id}") + return + + browser_client, browser = self._async_sessions[thread_id] + + # Close browser + if browser: + try: + await browser.close() + except Exception as e: + logger.warning( + f"Error closing async browser for thread {thread_id}: {e}" + ) + + # Stop browser client + if browser_client: + try: + browser_client.stop() + except Exception as e: + logger.warning( + f"Error stopping browser client for thread {thread_id}: {e}" + ) + + # Remove session from dictionary + del self._async_sessions[thread_id] + logger.info(f"Async browser session cleaned up for thread {thread_id}") + + def close_sync_browser(self, thread_id: str) -> None: + """ + Close the sync browser session for the specified thread. + + Args: + thread_id: Unique identifier for the thread + """ + if thread_id not in self._sync_sessions: + logger.warning(f"No sync browser session found for thread {thread_id}") + return + + browser_client, browser = self._sync_sessions[thread_id] + + # Close browser + if browser: + try: + browser.close() + except Exception as e: + logger.warning( + f"Error closing sync browser for thread {thread_id}: {e}" + ) + + # Stop browser client + if browser_client: + try: + browser_client.stop() + except Exception as e: + logger.warning( + f"Error stopping browser client for thread {thread_id}: {e}" + ) + + # Remove session from dictionary + del self._sync_sessions[thread_id] + logger.info(f"Sync browser session cleaned up for thread {thread_id}") + + async def close_all_browsers(self) -> None: + """Close all browser sessions.""" + # Close all async browsers + async_thread_ids = list(self._async_sessions.keys()) + for thread_id in async_thread_ids: + await self.close_async_browser(thread_id) + + # Close all sync browsers + sync_thread_ids = list(self._sync_sessions.keys()) + for thread_id in sync_thread_ids: + self.close_sync_browser(thread_id) + + logger.info("All browser sessions closed") \ No newline at end of file diff --git a/crewai_tools/aws/bedrock/browser/browser_toolkit.py b/crewai_tools/aws/bedrock/browser/browser_toolkit.py new file mode 100644 index 000000000..2939bbb00 --- /dev/null +++ b/crewai_tools/aws/bedrock/browser/browser_toolkit.py @@ -0,0 +1,587 @@ +"""Toolkit for navigating web with AWS browser.""" + +import json +import logging +import asyncio +from typing import Dict, List, Tuple, Any, Type +from urllib.parse import urlparse + +from crewai.tools import BaseTool +from pydantic import BaseModel, Field + +from .browser_session_manager import BrowserSessionManager +from .utils import aget_current_page, get_current_page + +logger = logging.getLogger(__name__) + + +# Input schemas +class NavigateToolInput(BaseModel): + """Input for NavigateTool.""" + url: str = Field(description="URL to navigate to") + thread_id: str = Field(default="default", description="Thread ID for the browser session") + + +class ClickToolInput(BaseModel): + """Input for ClickTool.""" + selector: str = Field(description="CSS selector for the element to click on") + thread_id: str = Field(default="default", description="Thread ID for the browser session") + + +class GetElementsToolInput(BaseModel): + """Input for GetElementsTool.""" + selector: str = Field(description="CSS selector for elements to get") + thread_id: str = Field(default="default", description="Thread ID for the browser session") + + +class ExtractTextToolInput(BaseModel): + """Input for ExtractTextTool.""" + thread_id: str = Field(default="default", description="Thread ID for the browser session") + + +class ExtractHyperlinksToolInput(BaseModel): + """Input for ExtractHyperlinksTool.""" + thread_id: str = Field(default="default", description="Thread ID for the browser session") + + +class NavigateBackToolInput(BaseModel): + """Input for NavigateBackTool.""" + thread_id: str = Field(default="default", description="Thread ID for the browser session") + + +class CurrentWebPageToolInput(BaseModel): + """Input for CurrentWebPageTool.""" + thread_id: str = Field(default="default", description="Thread ID for the browser session") + + +# Base tool class +class BrowserBaseTool(BaseTool): + """Base class for browser tools.""" + + def __init__(self, session_manager: BrowserSessionManager): + """Initialize with a session manager.""" + super().__init__() + self._session_manager = session_manager + + if self._is_in_asyncio_loop() and hasattr(self, '_arun'): + self._original_run = self._run + # Override _run to use _arun when in an asyncio loop + def patched_run(*args, **kwargs): + try: + import nest_asyncio + loop = asyncio.get_event_loop() + nest_asyncio.apply(loop) + return asyncio.get_event_loop().run_until_complete( + self._arun(*args, **kwargs) + ) + except Exception as e: + return f"Error in patched _run: {str(e)}" + self._run = patched_run + + async def get_async_page(self, thread_id: str) -> Any: + """Get or create a page for the specified thread.""" + browser = await self._session_manager.get_async_browser(thread_id) + page = await aget_current_page(browser) + return page + + def get_sync_page(self, thread_id: str) -> Any: + """Get or create a page for the specified thread.""" + browser = self._session_manager.get_sync_browser(thread_id) + page = get_current_page(browser) + return page + + def _is_in_asyncio_loop(self) -> bool: + """Check if we're currently in an asyncio event loop.""" + try: + loop = asyncio.get_event_loop() + return loop.is_running() + except RuntimeError: + return False + + +# Tool classes +class NavigateTool(BrowserBaseTool): + """Tool for navigating a browser to a URL.""" + + name: str = "navigate_browser" + description: str = "Navigate a browser to the specified URL" + args_schema: Type[BaseModel] = NavigateToolInput + + def _run(self, url: str, thread_id: str = "default", **kwargs) -> str: + """Use the sync tool.""" + try: + # Get page for this thread + page = self.get_sync_page(thread_id) + + # Validate URL scheme + parsed_url = urlparse(url) + if parsed_url.scheme not in ("http", "https"): + raise ValueError("URL scheme must be 'http' or 'https'") + + # Navigate to URL + response = page.goto(url) + status = response.status if response else "unknown" + return f"Navigating to {url} returned status code {status}" + except Exception as e: + return f"Error navigating to {url}: {str(e)}" + + async def _arun(self, url: str, thread_id: str = "default", **kwargs) -> str: + """Use the async tool.""" + try: + # Get page for this thread + page = await self.get_async_page(thread_id) + + # Validate URL scheme + parsed_url = urlparse(url) + if parsed_url.scheme not in ("http", "https"): + raise ValueError("URL scheme must be 'http' or 'https'") + + # Navigate to URL + response = await page.goto(url) + status = response.status if response else "unknown" + return f"Navigating to {url} returned status code {status}" + except Exception as e: + return f"Error navigating to {url}: {str(e)}" + + +class ClickTool(BrowserBaseTool): + """Tool for clicking on an element with the given CSS selector.""" + + name: str = "click_element" + description: str = "Click on an element with the given CSS selector" + args_schema: Type[BaseModel] = ClickToolInput + + visible_only: bool = True + """Whether to consider only visible elements.""" + playwright_strict: bool = False + """Whether to employ Playwright's strict mode when clicking on elements.""" + playwright_timeout: float = 1_000 + """Timeout (in ms) for Playwright to wait for element to be ready.""" + + def _selector_effective(self, selector: str) -> str: + if not self.visible_only: + return selector + return f"{selector} >> visible=1" + + def _run(self, selector: str, thread_id: str = "default", **kwargs) -> str: + """Use the sync tool.""" + try: + # Get the current page + page = self.get_sync_page(thread_id) + + # Click on the element + selector_effective = self._selector_effective(selector=selector) + from playwright.sync_api import TimeoutError as PlaywrightTimeoutError + + try: + page.click( + selector_effective, + strict=self.playwright_strict, + timeout=self.playwright_timeout, + ) + except PlaywrightTimeoutError: + return f"Unable to click on element '{selector}'" + except Exception as click_error: + return f"Unable to click on element '{selector}': {str(click_error)}" + + return f"Clicked element '{selector}'" + except Exception as e: + return f"Error clicking on element: {str(e)}" + + async def _arun(self, selector: str, thread_id: str = "default", **kwargs) -> str: + """Use the async tool.""" + try: + # Get the current page + page = await self.get_async_page(thread_id) + + # Click on the element + selector_effective = self._selector_effective(selector=selector) + from playwright.async_api import TimeoutError as PlaywrightTimeoutError + + try: + await page.click( + selector_effective, + strict=self.playwright_strict, + timeout=self.playwright_timeout, + ) + except PlaywrightTimeoutError: + return f"Unable to click on element '{selector}'" + except Exception as click_error: + return f"Unable to click on element '{selector}': {str(click_error)}" + + return f"Clicked element '{selector}'" + except Exception as e: + return f"Error clicking on element: {str(e)}" + + +class NavigateBackTool(BrowserBaseTool): + """Tool for navigating back in browser history.""" + name: str = "navigate_back" + description: str = "Navigate back to the previous page" + args_schema: Type[BaseModel] = NavigateBackToolInput + + def _run(self, thread_id: str = "default", **kwargs) -> str: + """Use the sync tool.""" + try: + # Get the current page + page = self.get_sync_page(thread_id) + + # Navigate back + try: + page.go_back() + return "Navigated back to the previous page" + except Exception as nav_error: + return f"Unable to navigate back: {str(nav_error)}" + except Exception as e: + return f"Error navigating back: {str(e)}" + + async def _arun(self, thread_id: str = "default", **kwargs) -> str: + """Use the async tool.""" + try: + # Get the current page + page = await self.get_async_page(thread_id) + + # Navigate back + try: + await page.go_back() + return "Navigated back to the previous page" + except Exception as nav_error: + return f"Unable to navigate back: {str(nav_error)}" + except Exception as e: + return f"Error navigating back: {str(e)}" + + +class ExtractTextTool(BrowserBaseTool): + """Tool for extracting text from a webpage.""" + name: str = "extract_text" + description: str = "Extract all the text on the current webpage" + args_schema: Type[BaseModel] = ExtractTextToolInput + + def _run(self, thread_id: str = "default", **kwargs) -> str: + """Use the sync tool.""" + try: + # Import BeautifulSoup + try: + from bs4 import BeautifulSoup + except ImportError: + return ( + "The 'beautifulsoup4' package is required to use this tool." + " Please install it with 'pip install beautifulsoup4'." + ) + + # Get the current page + page = self.get_sync_page(thread_id) + + # Extract text + content = page.content() + soup = BeautifulSoup(content, "html.parser") + return soup.get_text(separator="\n").strip() + except Exception as e: + return f"Error extracting text: {str(e)}" + + async def _arun(self, thread_id: str = "default", **kwargs) -> str: + """Use the async tool.""" + try: + # Import BeautifulSoup + try: + from bs4 import BeautifulSoup + except ImportError: + return ( + "The 'beautifulsoup4' package is required to use this tool." + " Please install it with 'pip install beautifulsoup4'." + ) + + # Get the current page + page = await self.get_async_page(thread_id) + + # Extract text + content = await page.content() + soup = BeautifulSoup(content, "html.parser") + return soup.get_text(separator="\n").strip() + except Exception as e: + return f"Error extracting text: {str(e)}" + + +class ExtractHyperlinksTool(BrowserBaseTool): + """Tool for extracting hyperlinks from a webpage.""" + name: str = "extract_hyperlinks" + description: str = "Extract all hyperlinks on the current webpage" + args_schema: Type[BaseModel] = ExtractHyperlinksToolInput + + def _run(self, thread_id: str = "default", **kwargs) -> str: + """Use the sync tool.""" + try: + # Import BeautifulSoup + try: + from bs4 import BeautifulSoup + except ImportError: + return ( + "The 'beautifulsoup4' package is required to use this tool." + " Please install it with 'pip install beautifulsoup4'." + ) + + # Get the current page + page = self.get_sync_page(thread_id) + + # Extract hyperlinks + content = page.content() + soup = BeautifulSoup(content, "html.parser") + links = [] + for link in soup.find_all("a", href=True): + text = link.get_text().strip() + href = link["href"] + if href.startswith("http") or href.startswith("https"): + links.append({"text": text, "url": href}) + + if not links: + return "No hyperlinks found on the current page." + + return json.dumps(links, indent=2) + except Exception as e: + return f"Error extracting hyperlinks: {str(e)}" + + async def _arun(self, thread_id: str = "default", **kwargs) -> str: + """Use the async tool.""" + try: + # Import BeautifulSoup + try: + from bs4 import BeautifulSoup + except ImportError: + return ( + "The 'beautifulsoup4' package is required to use this tool." + " Please install it with 'pip install beautifulsoup4'." + ) + + # Get the current page + page = await self.get_async_page(thread_id) + + # Extract hyperlinks + content = await page.content() + soup = BeautifulSoup(content, "html.parser") + links = [] + for link in soup.find_all("a", href=True): + text = link.get_text().strip() + href = link["href"] + if href.startswith("http") or href.startswith("https"): + links.append({"text": text, "url": href}) + + if not links: + return "No hyperlinks found on the current page." + + return json.dumps(links, indent=2) + except Exception as e: + return f"Error extracting hyperlinks: {str(e)}" + + +class GetElementsTool(BrowserBaseTool): + """Tool for getting elements from a webpage.""" + name: str = "get_elements" + description: str = "Get elements from the webpage using a CSS selector" + args_schema: Type[BaseModel] = GetElementsToolInput + + def _run(self, selector: str, thread_id: str = "default", **kwargs) -> str: + """Use the sync tool.""" + try: + # Get the current page + page = self.get_sync_page(thread_id) + + # Get elements + elements = page.query_selector_all(selector) + if not elements: + return f"No elements found with selector '{selector}'" + + elements_text = [] + for i, element in enumerate(elements): + text = element.text_content() + elements_text.append(f"Element {i+1}: {text.strip()}") + + return "\n".join(elements_text) + except Exception as e: + return f"Error getting elements: {str(e)}" + + async def _arun(self, selector: str, thread_id: str = "default", **kwargs) -> str: + """Use the async tool.""" + try: + # Get the current page + page = await self.get_async_page(thread_id) + + # Get elements + elements = await page.query_selector_all(selector) + if not elements: + return f"No elements found with selector '{selector}'" + + elements_text = [] + for i, element in enumerate(elements): + text = await element.text_content() + elements_text.append(f"Element {i+1}: {text.strip()}") + + return "\n".join(elements_text) + except Exception as e: + return f"Error getting elements: {str(e)}" + + +class CurrentWebPageTool(BrowserBaseTool): + """Tool for getting information about the current webpage.""" + name: str = "current_webpage" + description: str = "Get information about the current webpage" + args_schema: Type[BaseModel] = CurrentWebPageToolInput + + def _run(self, thread_id: str = "default", **kwargs) -> str: + """Use the sync tool.""" + try: + # Get the current page + page = self.get_sync_page(thread_id) + + # Get information + url = page.url + title = page.title() + return f"URL: {url}\nTitle: {title}" + except Exception as e: + return f"Error getting current webpage info: {str(e)}" + + async def _arun(self, thread_id: str = "default", **kwargs) -> str: + """Use the async tool.""" + try: + # Get the current page + page = await self.get_async_page(thread_id) + + # Get information + url = page.url + title = await page.title() + return f"URL: {url}\nTitle: {title}" + except Exception as e: + return f"Error getting current webpage info: {str(e)}" + + +class BrowserToolkit: + """Toolkit for navigating web with AWS Bedrock browser. + + This toolkit provides a set of tools for working with a remote browser + and supports multiple threads by maintaining separate browser sessions + for each thread ID. Browsers are created lazily only when needed. + + Example: + ```python + from crewai import Agent, Task, Crew + from crewai_tools.aws.bedrock.browser import create_browser_toolkit + + # Create the browser toolkit + toolkit, browser_tools = create_browser_toolkit(region="us-west-2") + + # Create a CrewAI agent that uses the browser tools + research_agent = Agent( + role="Web Researcher", + goal="Research and summarize web content", + backstory="You're an expert at finding information online.", + tools=browser_tools + ) + + # Create a task for the agent + research_task = Task( + description="Navigate to https://example.com and extract all text content. Summarize the main points.", + agent=research_agent + ) + + # Create and run the crew + crew = Crew( + agents=[research_agent], + tasks=[research_task] + ) + result = crew.kickoff() + + # Clean up browser resources when done + import asyncio + asyncio.run(toolkit.cleanup()) + ``` + """ + + def __init__(self, region: str = "us-west-2"): + """ + Initialize the toolkit + + Args: + region: AWS region for the browser client + """ + self.region = region + self.session_manager = BrowserSessionManager(region=region) + self.tools: List[BaseTool] = [] + self._nest_current_loop() + self._setup_tools() + + def _nest_current_loop(self): + """Apply nest_asyncio if we're in an asyncio loop.""" + try: + loop = asyncio.get_event_loop() + if loop.is_running(): + try: + import nest_asyncio + nest_asyncio.apply(loop) + except Exception as e: + logger.warning(f"Failed to apply nest_asyncio: {str(e)}") + except RuntimeError: + pass + + def _setup_tools(self) -> None: + """Initialize tools without creating any browsers.""" + self.tools = [ + NavigateTool(session_manager=self.session_manager), + ClickTool(session_manager=self.session_manager), + NavigateBackTool(session_manager=self.session_manager), + ExtractTextTool(session_manager=self.session_manager), + ExtractHyperlinksTool(session_manager=self.session_manager), + GetElementsTool(session_manager=self.session_manager), + CurrentWebPageTool(session_manager=self.session_manager) + ] + + def get_tools(self) -> List[BaseTool]: + """ + Get the list of browser tools + + Returns: + List of CrewAI tools + """ + return self.tools + + def get_tools_by_name(self) -> Dict[str, BaseTool]: + """ + Get a dictionary of tools mapped by their names + + Returns: + Dictionary of {tool_name: tool} + """ + return {tool.name: tool for tool in self.tools} + + async def cleanup(self) -> None: + """Clean up all browser sessions asynchronously""" + await self.session_manager.close_all_browsers() + logger.info("All browser sessions cleaned up") + + def sync_cleanup(self) -> None: + """Clean up all browser sessions from synchronous code""" + import asyncio + + try: + loop = asyncio.get_event_loop() + if loop.is_running(): + asyncio.create_task(self.cleanup()) + else: + loop.run_until_complete(self.cleanup()) + except RuntimeError: + asyncio.run(self.cleanup()) + + +def create_browser_toolkit( + region: str = "us-west-2", +) -> Tuple[BrowserToolkit, List[BaseTool]]: + """ + Create a BrowserToolkit + + Args: + region: AWS region for browser client + + Returns: + Tuple of (toolkit, tools) + """ + toolkit = BrowserToolkit(region=region) + tools = toolkit.get_tools() + return toolkit, tools diff --git a/crewai_tools/aws/bedrock/browser/utils.py b/crewai_tools/aws/bedrock/browser/utils.py new file mode 100644 index 000000000..6e8b48e3a --- /dev/null +++ b/crewai_tools/aws/bedrock/browser/utils.py @@ -0,0 +1,43 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Union + +if TYPE_CHECKING: + from playwright.async_api import Browser as AsyncBrowser + from playwright.async_api import Page as AsyncPage + from playwright.sync_api import Browser as SyncBrowser + from playwright.sync_api import Page as SyncPage + + +async def aget_current_page(browser: Union[AsyncBrowser, Any]) -> AsyncPage: + """ + Asynchronously get the current page of the browser. + Args: + browser: The browser (AsyncBrowser) to get the current page from. + Returns: + AsyncPage: The current page. + """ + if not browser.contexts: + context = await browser.new_context() + return await context.new_page() + context = browser.contexts[0] + if not context.pages: + return await context.new_page() + return context.pages[-1] + + +def get_current_page(browser: Union[SyncBrowser, Any]) -> SyncPage: + """ + Get the current page of the browser. + Args: + browser: The browser to get the current page from. + Returns: + SyncPage: The current page. + """ + if not browser.contexts: + context = browser.new_context() + return context.new_page() + context = browser.contexts[0] + if not context.pages: + return context.new_page() + return context.pages[-1] \ No newline at end of file diff --git a/crewai_tools/aws/bedrock/code_interpreter/README.md b/crewai_tools/aws/bedrock/code_interpreter/README.md new file mode 100644 index 000000000..92e8ec5b2 --- /dev/null +++ b/crewai_tools/aws/bedrock/code_interpreter/README.md @@ -0,0 +1,217 @@ +# AWS Bedrock Code Interpreter Tools + +This toolkit provides a set of tools for interacting with the AWS Bedrock Code Interpreter environment. It enables your CrewAI agents to execute code, run shell commands, manage files, and perform computational tasks in a secure, isolated environment. + +## Features + +- Execute code in various languages (primarily Python) +- Run shell commands in the environment +- Read, write, list, and delete files +- Manage long-running tasks asynchronously +- Multiple code interpreter sessions with thread-based isolation + +## Installation + +Ensure you have the necessary dependencies: + +```bash +uv add crewai-tools bedrock-agentcore +``` + +## Usage + +### Basic Usage + +```python +from crewai import Agent, Task, Crew, LLM +from crewai_tools.aws import create_code_interpreter_toolkit + +# Create the code interpreter toolkit +toolkit, code_tools = create_code_interpreter_toolkit(region="us-west-2") + +# Create the Bedrock LLM +llm = LLM( + model="bedrock/us.anthropic.claude-3-7-sonnet-20250219-v1:0", + region_name="us-west-2", +) + +# Create a CrewAI agent that uses the code interpreter tools +developer_agent = Agent( + role="Python Developer", + goal="Create and execute Python code to solve problems.", + backstory="You're a skilled Python developer with expertise in data analysis.", + tools=code_tools, + llm=llm +) + +# Create a task for the agent +coding_task = Task( + description="Write a Python function that calculates the factorial of a number and test it. Do not use any imports from outside the Python standard library.", + expected_output="The Python function created, and the test results.", + agent=developer_agent +) + +# Create and run the crew +crew = Crew( + agents=[developer_agent], + tasks=[coding_task] +) +result = crew.kickoff() + +print(f"\n***Final result:***\n\n{result}") + +# Clean up resources when done +import asyncio +asyncio.run(toolkit.cleanup()) +``` + +### Available Tools + +The toolkit provides the following tools: + +1. `execute_code` - Run code in various languages (primarily Python) +2. `execute_command` - Run shell commands in the environment +3. `read_files` - Read content of files in the environment +4. `list_files` - List files in directories +5. `delete_files` - Remove files from the environment +6. `write_files` - Create or update files +7. `start_command_execution` - Start long-running commands asynchronously +8. `get_task` - Check status of async tasks +9. `stop_task` - Stop running tasks + +### Advanced Usage + +```python +from crewai import Agent, Task, Crew, LLM +from crewai_tools.aws import create_code_interpreter_toolkit + +# Create the code interpreter toolkit +toolkit, code_tools = create_code_interpreter_toolkit(region="us-west-2") +tools_by_name = toolkit.get_tools_by_name() + +# Create the Bedrock LLM +llm = LLM( + model="bedrock/us.anthropic.claude-3-7-sonnet-20250219-v1:0", + region_name="us-west-2", +) + +# Create agents with specific tools +code_agent = Agent( + role="Code Developer", + goal="Write and execute code", + backstory="You write and test code to solve complex problems.", + tools=[ + # Use specific tools by name + tools_by_name["execute_code"], + tools_by_name["execute_command"], + tools_by_name["read_files"], + tools_by_name["write_files"] + ], + llm=llm +) + +file_agent = Agent( + role="File Manager", + goal="Manage files in the environment", + backstory="You help organize and manage files in the code environment.", + tools=[ + # Use specific tools by name + tools_by_name["list_files"], + tools_by_name["read_files"], + tools_by_name["write_files"], + tools_by_name["delete_files"] + ], + llm=llm +) + +# Create tasks for the agents +coding_task = Task( + description="Write a Python script to analyze data from a CSV file. Do not use any imports from outside the Python standard library.", + expected_output="The Python function created.", + agent=code_agent +) + +file_task = Task( + description="Organize the created files into separate directories.", + agent=file_agent +) + +# Create and run the crew +crew = Crew( + agents=[code_agent, file_agent], + tasks=[coding_task, file_task] +) +result = crew.kickoff() + +print(f"\n***Final result:***\n\n{result}") + +# Clean up code interpreter resources when done +import asyncio +asyncio.run(toolkit.cleanup()) +``` + +### Example: Data Analysis with Python + +```python +from crewai import Agent, Task, Crew, LLM +from crewai_tools.aws import create_code_interpreter_toolkit + +# Create toolkit and tools +toolkit, code_tools = create_code_interpreter_toolkit(region="us-west-2") + +# Create the Bedrock LLM +llm = LLM( + model="bedrock/us.anthropic.claude-3-7-sonnet-20250219-v1:0", + region_name="us-west-2", +) + +# Create a data analyst agent +analyst_agent = Agent( + role="Data Analyst", + goal="Analyze data using Python", + backstory="You're an expert data analyst who uses Python for data processing.", + tools=code_tools, + llm=llm +) + +# Create a task for the agent +analysis_task = Task( + description=""" + For all of the below, do not use any imports from outside the Python standard library. + 1. Create a sample dataset with random data + 2. Perform statistical analysis on the dataset + 3. Generate visualizations of the results + 4. Save the results and visualizations to files + """, + agent=analyst_agent +) + +# Create and run the crew +crew = Crew( + agents=[analyst_agent], + tasks=[analysis_task] +) +result = crew.kickoff() + +print(f"\n***Final result:***\n\n{result}") + +# Clean up resources +import asyncio +asyncio.run(toolkit.cleanup()) +``` + +## Resource Cleanup + +Always clean up code interpreter resources when done to prevent resource leaks: + +```python +import asyncio + +# Clean up all code interpreter sessions +asyncio.run(toolkit.cleanup()) +``` + +## Requirements + +- AWS account with access to Bedrock AgentCore API +- Properly configured AWS credentials \ No newline at end of file diff --git a/crewai_tools/aws/bedrock/code_interpreter/__init__.py b/crewai_tools/aws/bedrock/code_interpreter/__init__.py new file mode 100644 index 000000000..903c84e24 --- /dev/null +++ b/crewai_tools/aws/bedrock/code_interpreter/__init__.py @@ -0,0 +1,3 @@ +from .code_interpreter_toolkit import CodeInterpreterToolkit, create_code_interpreter_toolkit + +__all__ = ["CodeInterpreterToolkit", "create_code_interpreter_toolkit"] \ No newline at end of file diff --git a/crewai_tools/aws/bedrock/code_interpreter/code_interpreter_toolkit.py b/crewai_tools/aws/bedrock/code_interpreter/code_interpreter_toolkit.py new file mode 100644 index 000000000..4e697cafe --- /dev/null +++ b/crewai_tools/aws/bedrock/code_interpreter/code_interpreter_toolkit.py @@ -0,0 +1,543 @@ +"""Toolkit for working with AWS Bedrock Code Interpreter.""" +from __future__ import annotations + +import json +import logging +from typing import TYPE_CHECKING, Dict, List, Tuple, Optional, Type, Any + +from crewai.tools import BaseTool +from pydantic import BaseModel, Field + +if TYPE_CHECKING: + from bedrock_agentcore.tools.code_interpreter_client import CodeInterpreter + +logger = logging.getLogger(__name__) + + +def extract_output_from_stream(response): + """ + Extract output from code interpreter response stream + + Args: + response: Response from code interpreter execution + + Returns: + Extracted output as string + """ + output = [] + for event in response["stream"]: + if "result" in event: + result = event["result"] + for content_item in result["content"]: + if content_item["type"] == "text": + output.append(content_item["text"]) + if content_item["type"] == "resource": + resource = content_item["resource"] + if "text" in resource: + file_path = resource["uri"].replace("file://", "") + file_content = resource["text"] + output.append(f"==== File: {file_path} ====\n{file_content}\n") + else: + output.append(json.dumps(resource)) + + return "\n".join(output) + + +# Input schemas +class ExecuteCodeInput(BaseModel): + """Input for ExecuteCode.""" + code: str = Field(description="The code to execute") + language: str = Field(default="python", description="The programming language of the code") + clear_context: bool = Field(default=False, description="Whether to clear execution context") + thread_id: str = Field(default="default", description="Thread ID for the code interpreter session") + + +class ExecuteCommandInput(BaseModel): + """Input for ExecuteCommand.""" + command: str = Field(description="The command to execute") + thread_id: str = Field(default="default", description="Thread ID for the code interpreter session") + + +class ReadFilesInput(BaseModel): + """Input for ReadFiles.""" + paths: List[str] = Field(description="List of file paths to read") + thread_id: str = Field(default="default", description="Thread ID for the code interpreter session") + + +class ListFilesInput(BaseModel): + """Input for ListFiles.""" + directory_path: str = Field(default="", description="Path to the directory to list") + thread_id: str = Field(default="default", description="Thread ID for the code interpreter session") + + +class DeleteFilesInput(BaseModel): + """Input for DeleteFiles.""" + paths: List[str] = Field(description="List of file paths to delete") + thread_id: str = Field(default="default", description="Thread ID for the code interpreter session") + + +class WriteFilesInput(BaseModel): + """Input for WriteFiles.""" + files: List[Dict[str, str]] = Field(description="List of dictionaries with path and text fields") + thread_id: str = Field(default="default", description="Thread ID for the code interpreter session") + + +class StartCommandInput(BaseModel): + """Input for StartCommand.""" + command: str = Field(description="The command to execute asynchronously") + thread_id: str = Field(default="default", description="Thread ID for the code interpreter session") + + +class GetTaskInput(BaseModel): + """Input for GetTask.""" + task_id: str = Field(description="The ID of the task to check") + thread_id: str = Field(default="default", description="Thread ID for the code interpreter session") + + +class StopTaskInput(BaseModel): + """Input for StopTask.""" + task_id: str = Field(description="The ID of the task to stop") + thread_id: str = Field(default="default", description="Thread ID for the code interpreter session") + + +# Tool classes +class ExecuteCodeTool(BaseTool): + """Tool for executing code in various languages.""" + name: str = "execute_code" + description: str = "Execute code in various languages (primarily Python)" + args_schema: Type[BaseModel] = ExecuteCodeInput + toolkit: Any = Field(default=None, exclude=True) + + def __init__(self, toolkit): + super().__init__() + self.toolkit = toolkit + + def _run(self, code: str, language: str = "python", clear_context: bool = False, thread_id: str = "default") -> str: + try: + # Get or create code interpreter + code_interpreter = self.toolkit._get_or_create_interpreter(thread_id=thread_id) + + # Execute code + response = code_interpreter.invoke( + method="executeCode", + params={"code": code, "language": language, "clearContext": clear_context}, + ) + + return extract_output_from_stream(response) + except Exception as e: + return f"Error executing code: {str(e)}" + + async def _arun(self, code: str, language: str = "python", clear_context: bool = False, thread_id: str = "default") -> str: + # Use _run as we're working with a synchronous API that's thread-safe + return self._run(code=code, language=language, clear_context=clear_context, thread_id=thread_id) + + +class ExecuteCommandTool(BaseTool): + """Tool for running shell commands in the code interpreter environment.""" + name: str = "execute_command" + description: str = "Run shell commands in the code interpreter environment" + args_schema: Type[BaseModel] = ExecuteCommandInput + toolkit: Any = Field(default=None, exclude=True) + + def __init__(self, toolkit): + super().__init__() + self.toolkit = toolkit + + def _run(self, command: str, thread_id: str = "default") -> str: + try: + # Get or create code interpreter + code_interpreter = self.toolkit._get_or_create_interpreter(thread_id=thread_id) + + # Execute command + response = code_interpreter.invoke( + method="executeCommand", params={"command": command} + ) + + return extract_output_from_stream(response) + except Exception as e: + return f"Error executing command: {str(e)}" + + async def _arun(self, command: str, thread_id: str = "default") -> str: + # Use _run as we're working with a synchronous API that's thread-safe + return self._run(command=command, thread_id=thread_id) + + +class ReadFilesTool(BaseTool): + """Tool for reading content of files in the environment.""" + name: str = "read_files" + description: str = "Read content of files in the environment" + args_schema: Type[BaseModel] = ReadFilesInput + toolkit: Any = Field(default=None, exclude=True) + + def __init__(self, toolkit): + super().__init__() + self.toolkit = toolkit + + def _run(self, paths: List[str], thread_id: str = "default") -> str: + try: + # Get or create code interpreter + code_interpreter = self.toolkit._get_or_create_interpreter(thread_id=thread_id) + + # Read files + response = code_interpreter.invoke(method="readFiles", params={"paths": paths}) + + return extract_output_from_stream(response) + except Exception as e: + return f"Error reading files: {str(e)}" + + async def _arun(self, paths: List[str], thread_id: str = "default") -> str: + # Use _run as we're working with a synchronous API that's thread-safe + return self._run(paths=paths, thread_id=thread_id) + + +class ListFilesTool(BaseTool): + """Tool for listing files in directories in the environment.""" + name: str = "list_files" + description: str = "List files in directories in the environment" + args_schema: Type[BaseModel] = ListFilesInput + toolkit: Any = Field(default=None, exclude=True) + + def __init__(self, toolkit): + super().__init__() + self.toolkit = toolkit + + def _run(self, directory_path: str = "", thread_id: str = "default") -> str: + try: + # Get or create code interpreter + code_interpreter = self.toolkit._get_or_create_interpreter(thread_id=thread_id) + + # List files + response = code_interpreter.invoke( + method="listFiles", params={"directoryPath": directory_path} + ) + + return extract_output_from_stream(response) + except Exception as e: + return f"Error listing files: {str(e)}" + + async def _arun(self, directory_path: str = "", thread_id: str = "default") -> str: + # Use _run as we're working with a synchronous API that's thread-safe + return self._run(directory_path=directory_path, thread_id=thread_id) + + +class DeleteFilesTool(BaseTool): + """Tool for removing files from the environment.""" + name: str = "delete_files" + description: str = "Remove files from the environment" + args_schema: Type[BaseModel] = DeleteFilesInput + toolkit: Any = Field(default=None, exclude=True) + + def __init__(self, toolkit): + super().__init__() + self.toolkit = toolkit + + def _run(self, paths: List[str], thread_id: str = "default") -> str: + try: + # Get or create code interpreter + code_interpreter = self.toolkit._get_or_create_interpreter(thread_id=thread_id) + + # Remove files + response = code_interpreter.invoke( + method="removeFiles", params={"paths": paths} + ) + + return extract_output_from_stream(response) + except Exception as e: + return f"Error deleting files: {str(e)}" + + async def _arun(self, paths: List[str], thread_id: str = "default") -> str: + # Use _run as we're working with a synchronous API that's thread-safe + return self._run(paths=paths, thread_id=thread_id) + + +class WriteFilesTool(BaseTool): + """Tool for creating or updating files in the environment.""" + name: str = "write_files" + description: str = "Create or update files in the environment" + args_schema: Type[BaseModel] = WriteFilesInput + toolkit: Any = Field(default=None, exclude=True) + + def __init__(self, toolkit): + super().__init__() + self.toolkit = toolkit + + def _run(self, files: List[Dict[str, str]], thread_id: str = "default") -> str: + try: + # Get or create code interpreter + code_interpreter = self.toolkit._get_or_create_interpreter(thread_id=thread_id) + + # Write files + response = code_interpreter.invoke( + method="writeFiles", params={"content": files} + ) + + return extract_output_from_stream(response) + except Exception as e: + return f"Error writing files: {str(e)}" + + async def _arun(self, files: List[Dict[str, str]], thread_id: str = "default") -> str: + # Use _run as we're working with a synchronous API that's thread-safe + return self._run(files=files, thread_id=thread_id) + + +class StartCommandTool(BaseTool): + """Tool for starting long-running commands asynchronously.""" + name: str = "start_command_execution" + description: str = "Start long-running commands asynchronously" + args_schema: Type[BaseModel] = StartCommandInput + toolkit: Any = Field(default=None, exclude=True) + + def __init__(self, toolkit): + super().__init__() + self.toolkit = toolkit + + def _run(self, command: str, thread_id: str = "default") -> str: + try: + # Get or create code interpreter + code_interpreter = self.toolkit._get_or_create_interpreter(thread_id=thread_id) + + # Start command execution + response = code_interpreter.invoke( + method="startCommandExecution", params={"command": command} + ) + + return extract_output_from_stream(response) + except Exception as e: + return f"Error starting command: {str(e)}" + + async def _arun(self, command: str, thread_id: str = "default") -> str: + # Use _run as we're working with a synchronous API that's thread-safe + return self._run(command=command, thread_id=thread_id) + + +class GetTaskTool(BaseTool): + """Tool for checking status of async tasks.""" + name: str = "get_task" + description: str = "Check status of async tasks" + args_schema: Type[BaseModel] = GetTaskInput + toolkit: Any = Field(default=None, exclude=True) + + def __init__(self, toolkit): + super().__init__() + self.toolkit = toolkit + + def _run(self, task_id: str, thread_id: str = "default") -> str: + try: + # Get or create code interpreter + code_interpreter = self.toolkit._get_or_create_interpreter(thread_id=thread_id) + + # Get task status + response = code_interpreter.invoke(method="getTask", params={"taskId": task_id}) + + return extract_output_from_stream(response) + except Exception as e: + return f"Error getting task status: {str(e)}" + + async def _arun(self, task_id: str, thread_id: str = "default") -> str: + # Use _run as we're working with a synchronous API that's thread-safe + return self._run(task_id=task_id, thread_id=thread_id) + + +class StopTaskTool(BaseTool): + """Tool for stopping running tasks.""" + name: str = "stop_task" + description: str = "Stop running tasks" + args_schema: Type[BaseModel] = StopTaskInput + toolkit: Any = Field(default=None, exclude=True) + + def __init__(self, toolkit): + super().__init__() + self.toolkit = toolkit + + def _run(self, task_id: str, thread_id: str = "default") -> str: + try: + # Get or create code interpreter + code_interpreter = self.toolkit._get_or_create_interpreter(thread_id=thread_id) + + # Stop task + response = code_interpreter.invoke( + method="stopTask", params={"taskId": task_id} + ) + + return extract_output_from_stream(response) + except Exception as e: + return f"Error stopping task: {str(e)}" + + async def _arun(self, task_id: str, thread_id: str = "default") -> str: + # Use _run as we're working with a synchronous API that's thread-safe + return self._run(task_id=task_id, thread_id=thread_id) + + +class CodeInterpreterToolkit: + """Toolkit for working with AWS Bedrock code interpreter environment. + + This toolkit provides a set of tools for working with a remote code interpreter environment: + + * execute_code - Run code in various languages (primarily Python) + * execute_command - Run shell commands + * read_files - Read content of files in the environment + * list_files - List files in directories + * delete_files - Remove files from the environment + * write_files - Create or update files + * start_command_execution - Start long-running commands asynchronously + * get_task - Check status of async tasks + * stop_task - Stop running tasks + + The toolkit lazily initializes the code interpreter session on first use. + It supports multiple threads by maintaining separate code interpreter sessions for each thread ID. + + Example: + ```python + from crewai import Agent, Task, Crew + from crewai_tools.aws.bedrock.code_interpreter import create_code_interpreter_toolkit + + # Create the code interpreter toolkit + toolkit, code_tools = create_code_interpreter_toolkit(region="us-west-2") + + # Create a CrewAI agent that uses the code interpreter tools + developer_agent = Agent( + role="Python Developer", + goal="Create and execute Python code to solve problems", + backstory="You're a skilled Python developer with expertise in data analysis.", + tools=code_tools + ) + + # Create a task for the agent + coding_task = Task( + description="Write a Python function that calculates the factorial of a number and test it.", + agent=developer_agent + ) + + # Create and run the crew + crew = Crew( + agents=[developer_agent], + tasks=[coding_task] + ) + result = crew.kickoff() + + # Clean up resources when done + import asyncio + asyncio.run(toolkit.cleanup()) + ``` + """ + + def __init__(self, region: str = "us-west-2"): + """ + Initialize the toolkit + + Args: + region: AWS region for the code interpreter + """ + self.region = region + self._code_interpreters: Dict[str, CodeInterpreter] = {} + self.tools: List[BaseTool] = [] + self._setup_tools() + + def _setup_tools(self) -> None: + """Initialize tools without creating any code interpreter sessions.""" + self.tools = [ + ExecuteCodeTool(self), + ExecuteCommandTool(self), + ReadFilesTool(self), + ListFilesTool(self), + DeleteFilesTool(self), + WriteFilesTool(self), + StartCommandTool(self), + GetTaskTool(self), + StopTaskTool(self) + ] + + def _get_or_create_interpreter( + self, thread_id: str = "default" + ) -> CodeInterpreter: + """Get or create a code interpreter for the specified thread. + + Args: + thread_id: Thread ID for the code interpreter session + + Returns: + CodeInterpreter instance + """ + if thread_id in self._code_interpreters: + return self._code_interpreters[thread_id] + + # Create a new code interpreter for this thread + from bedrock_agentcore.tools.code_interpreter_client import CodeInterpreter + code_interpreter = CodeInterpreter(region=self.region) + code_interpreter.start() + logger.info( + f"Started code interpreter with session_id:{code_interpreter.session_id} for thread:{thread_id}" + ) + + # Store the interpreter + self._code_interpreters[thread_id] = code_interpreter + return code_interpreter + + + def get_tools(self) -> List[BaseTool]: + """ + Get the list of code interpreter tools + + Returns: + List of CrewAI tools + """ + return self.tools + + def get_tools_by_name(self) -> Dict[str, BaseTool]: + """ + Get a dictionary of tools mapped by their names + + Returns: + Dictionary of {tool_name: tool} + """ + return {tool.name: tool for tool in self.tools} + + async def cleanup(self, thread_id: Optional[str] = None) -> None: + """Clean up resources + + Args: + thread_id: Optional thread ID to clean up. If None, cleans up all sessions. + """ + if thread_id: + # Clean up a specific thread's session + if thread_id in self._code_interpreters: + try: + self._code_interpreters[thread_id].stop() + del self._code_interpreters[thread_id] + logger.info( + f"Code interpreter session for thread {thread_id} cleaned up" + ) + except Exception as e: + logger.warning( + f"Error stopping code interpreter for thread {thread_id}: {e}" + ) + else: + # Clean up all sessions + thread_ids = list(self._code_interpreters.keys()) + for tid in thread_ids: + try: + self._code_interpreters[tid].stop() + except Exception as e: + logger.warning( + f"Error stopping code interpreter for thread {tid}: {e}" + ) + + self._code_interpreters = {} + logger.info("All code interpreter sessions cleaned up") + + +def create_code_interpreter_toolkit( + region: str = "us-west-2", +) -> Tuple[CodeInterpreterToolkit, List[BaseTool]]: + """ + Create a CodeInterpreterToolkit + + Args: + region: AWS region for code interpreter + + Returns: + Tuple of (toolkit, tools) + """ + toolkit = CodeInterpreterToolkit(region=region) + tools = toolkit.get_tools() + return toolkit, tools \ No newline at end of file diff --git a/crewai_tools/aws/bedrock/exceptions.py b/crewai_tools/aws/bedrock/exceptions.py new file mode 100644 index 000000000..d1aa2623c --- /dev/null +++ b/crewai_tools/aws/bedrock/exceptions.py @@ -0,0 +1,17 @@ +"""Custom exceptions for AWS Bedrock integration.""" + +class BedrockError(Exception): + """Base exception for Bedrock-related errors.""" + pass + +class BedrockAgentError(BedrockError): + """Exception raised for errors in the Bedrock Agent operations.""" + pass + +class BedrockKnowledgeBaseError(BedrockError): + """Exception raised for errors in the Bedrock Knowledge Base operations.""" + pass + +class BedrockValidationError(BedrockError): + """Exception raised for validation errors in Bedrock operations.""" + pass \ No newline at end of file diff --git a/crewai_tools/aws/bedrock/knowledge_base/README.md b/crewai_tools/aws/bedrock/knowledge_base/README.md new file mode 100644 index 000000000..6da54f848 --- /dev/null +++ b/crewai_tools/aws/bedrock/knowledge_base/README.md @@ -0,0 +1,159 @@ +# BedrockKBRetrieverTool + +The `BedrockKBRetrieverTool` enables CrewAI agents to retrieve information from Amazon Bedrock Knowledge Bases using natural language queries. + +## Installation + +```bash +pip install 'crewai[tools]' +``` + +## Requirements + +- AWS credentials configured (either through environment variables or AWS CLI) +- `boto3` and `python-dotenv` packages +- Access to Amazon Bedrock Knowledge Base + +## Usage + +Here's how to use the tool with a CrewAI agent: + +```python +from crewai import Agent, Task, Crew +from crewai_tools.aws.bedrock.knowledge_base.retriever_tool import BedrockKBRetrieverTool + +# Initialize the tool +kb_tool = BedrockKBRetrieverTool( + knowledge_base_id="your-kb-id", + number_of_results=5 +) + +# Create a CrewAI agent that uses the tool +researcher = Agent( + role='Knowledge Base Researcher', + goal='Find information about company policies', + backstory='I am a researcher specialized in retrieving and analyzing company documentation.', + tools=[kb_tool], + verbose=True +) + +# Create a task for the agent +research_task = Task( + description="Find our company's remote work policy and summarize the key points.", + agent=researcher +) + +# Create a crew with the agent +crew = Crew( + agents=[researcher], + tasks=[research_task], + verbose=2 +) + +# Run the crew +result = crew.kickoff() +print(result) +``` + +## Tool Arguments + +| Argument | Type | Required | Default | Description | +|----------|------|----------|---------|-------------| +| knowledge_base_id | str | Yes | None | The unique identifier of the knowledge base (0-10 alphanumeric characters) | +| number_of_results | int | No | 5 | Maximum number of results to return | +| retrieval_configuration | dict | No | None | Custom configurations for the knowledge base query | +| guardrail_configuration | dict | No | None | Content filtering settings | +| next_token | str | No | None | Token for pagination | + +## Environment Variables + +```bash +BEDROCK_KB_ID=your-knowledge-base-id # Alternative to passing knowledge_base_id +AWS_REGION=your-aws-region # Defaults to us-east-1 +AWS_ACCESS_KEY_ID=your-access-key # Required for AWS authentication +AWS_SECRET_ACCESS_KEY=your-secret-key # Required for AWS authentication +``` + +## Response Format + +The tool returns results in JSON format: + +```json +{ + "results": [ + { + "content": "Retrieved text content", + "content_type": "text", + "source_type": "S3", + "source_uri": "s3://bucket/document.pdf", + "score": 0.95, + "metadata": { + "additional": "metadata" + } + } + ], + "nextToken": "pagination-token", + "guardrailAction": "NONE" +} +``` + +## Advanced Usage + +### Custom Retrieval Configuration + +```python +kb_tool = BedrockKBRetrieverTool( + knowledge_base_id="your-kb-id", + retrieval_configuration={ + "vectorSearchConfiguration": { + "numberOfResults": 10, + "overrideSearchType": "HYBRID" + } + } +) + +policy_expert = Agent( + role='Policy Expert', + goal='Analyze company policies in detail', + backstory='I am an expert in corporate policy analysis with deep knowledge of regulatory requirements.', + tools=[kb_tool] +) +``` + +## Supported Data Sources + +- Amazon S3 +- Confluence +- Salesforce +- SharePoint +- Web pages +- Custom document locations +- Amazon Kendra +- SQL databases + +## Use Cases + +### Enterprise Knowledge Integration +- Enable CrewAI agents to access your organization's proprietary knowledge without exposing sensitive data +- Allow agents to make decisions based on your company's specific policies, procedures, and documentation +- Create agents that can answer questions based on your internal documentation while maintaining data security + +### Specialized Domain Knowledge +- Connect CrewAI agents to domain-specific knowledge bases (legal, medical, technical) without retraining models +- Leverage existing knowledge repositories that are already maintained in your AWS environment +- Combine CrewAI's reasoning with domain-specific information from your knowledge bases + +### Data-Driven Decision Making +- Ground CrewAI agent responses in your actual company data rather than general knowledge +- Ensure agents provide recommendations based on your specific business context and documentation +- Reduce hallucinations by retrieving factual information from your knowledge bases + +### Scalable Information Access +- Access terabytes of organizational knowledge without embedding it all into your models +- Dynamically query only the relevant information needed for specific tasks +- Leverage AWS's scalable infrastructure to handle large knowledge bases efficiently + +### Compliance and Governance +- Ensure CrewAI agents provide responses that align with your company's approved documentation +- Create auditable trails of information sources used by your agents +- Maintain control over what information sources your agents can access \ No newline at end of file diff --git a/crewai_tools/aws/bedrock/knowledge_base/__init__.py b/crewai_tools/aws/bedrock/knowledge_base/__init__.py new file mode 100644 index 000000000..013d94cf3 --- /dev/null +++ b/crewai_tools/aws/bedrock/knowledge_base/__init__.py @@ -0,0 +1,3 @@ +from .retriever_tool import BedrockKBRetrieverTool + +__all__ = ["BedrockKBRetrieverTool"] diff --git a/crewai_tools/aws/bedrock/knowledge_base/retriever_tool.py b/crewai_tools/aws/bedrock/knowledge_base/retriever_tool.py new file mode 100644 index 000000000..06fd3ce38 --- /dev/null +++ b/crewai_tools/aws/bedrock/knowledge_base/retriever_tool.py @@ -0,0 +1,248 @@ +from typing import Type, Optional, List, Dict, Any +import os +import json +from dotenv import load_dotenv + +from crewai.tools import BaseTool +from pydantic import BaseModel, Field + +from ..exceptions import BedrockKnowledgeBaseError, BedrockValidationError + +# Load environment variables from .env file +load_dotenv() + + +class BedrockKBRetrieverToolInput(BaseModel): + """Input schema for BedrockKBRetrieverTool.""" + query: str = Field(..., description="The query to retrieve information from the knowledge base") + + +class BedrockKBRetrieverTool(BaseTool): + name: str = "Bedrock Knowledge Base Retriever Tool" + description: str = "Retrieves information from an Amazon Bedrock Knowledge Base given a query" + args_schema: Type[BaseModel] = BedrockKBRetrieverToolInput + knowledge_base_id: str = None + number_of_results: Optional[int] = 5 + retrieval_configuration: Optional[Dict[str, Any]] = None + guardrail_configuration: Optional[Dict[str, Any]] = None + next_token: Optional[str] = None + package_dependencies: List[str] = ["boto3"] + + def __init__( + self, + knowledge_base_id: str = None, + number_of_results: Optional[int] = 5, + retrieval_configuration: Optional[Dict[str, Any]] = None, + guardrail_configuration: Optional[Dict[str, Any]] = None, + next_token: Optional[str] = None, + **kwargs + ): + """Initialize the BedrockKBRetrieverTool with knowledge base configuration. + + Args: + knowledge_base_id (str): The unique identifier of the knowledge base to query + number_of_results (Optional[int], optional): The maximum number of results to return. Defaults to 5. + retrieval_configuration (Optional[Dict[str, Any]], optional): Configurations for the knowledge base query and retrieval process. Defaults to None. + guardrail_configuration (Optional[Dict[str, Any]], optional): Guardrail settings. Defaults to None. + next_token (Optional[str], optional): Token for retrieving the next batch of results. Defaults to None. + """ + super().__init__(**kwargs) + + # Get knowledge_base_id from environment variable if not provided + self.knowledge_base_id = knowledge_base_id or os.getenv('BEDROCK_KB_ID') + self.number_of_results = number_of_results + self.guardrail_configuration = guardrail_configuration + self.next_token = next_token + + # Initialize retrieval_configuration with provided parameters or use the one provided + if retrieval_configuration is None: + self.retrieval_configuration = self._build_retrieval_configuration() + else: + self.retrieval_configuration = retrieval_configuration + + # Validate parameters + self._validate_parameters() + + # Update the description to include the knowledge base details + self.description = f"Retrieves information from Amazon Bedrock Knowledge Base '{self.knowledge_base_id}' given a query" + + def _build_retrieval_configuration(self) -> Dict[str, Any]: + """Build the retrieval configuration based on provided parameters. + + Returns: + Dict[str, Any]: The constructed retrieval configuration + """ + vector_search_config = {} + + # Add number of results if provided + if self.number_of_results is not None: + vector_search_config["numberOfResults"] = self.number_of_results + + return {"vectorSearchConfiguration": vector_search_config} + + def _validate_parameters(self): + """Validate the parameters according to AWS API requirements.""" + try: + # Validate knowledge_base_id + if not self.knowledge_base_id: + raise BedrockValidationError("knowledge_base_id cannot be empty") + if not isinstance(self.knowledge_base_id, str): + raise BedrockValidationError("knowledge_base_id must be a string") + if len(self.knowledge_base_id) > 10: + raise BedrockValidationError("knowledge_base_id must be 10 characters or less") + if not all(c.isalnum() for c in self.knowledge_base_id): + raise BedrockValidationError("knowledge_base_id must contain only alphanumeric characters") + + # Validate next_token if provided + if self.next_token: + if not isinstance(self.next_token, str): + raise BedrockValidationError("next_token must be a string") + if len(self.next_token) < 1 or len(self.next_token) > 2048: + raise BedrockValidationError("next_token must be between 1 and 2048 characters") + if ' ' in self.next_token: + raise BedrockValidationError("next_token cannot contain spaces") + + # Validate number_of_results if provided + if self.number_of_results is not None: + if not isinstance(self.number_of_results, int): + raise BedrockValidationError("number_of_results must be an integer") + if self.number_of_results < 1: + raise BedrockValidationError("number_of_results must be greater than 0") + + except BedrockValidationError as e: + raise BedrockValidationError(f"Parameter validation failed: {str(e)}") + + def _process_retrieval_result(self, result: Dict[str, Any]) -> Dict[str, Any]: + """Process a single retrieval result from Bedrock Knowledge Base. + + Args: + result (Dict[str, Any]): Raw result from Bedrock Knowledge Base + + Returns: + Dict[str, Any]: Processed result with standardized format + """ + # Extract content + content_obj = result.get('content', {}) + content = content_obj.get('text', '') + content_type = content_obj.get('type', 'text') + + # Extract location information + location = result.get('location', {}) + location_type = location.get('type', 'unknown') + source_uri = None + + # Map for location types and their URI fields + location_mapping = { + 's3Location': {'field': 'uri', 'type': 'S3'}, + 'confluenceLocation': {'field': 'url', 'type': 'Confluence'}, + 'salesforceLocation': {'field': 'url', 'type': 'Salesforce'}, + 'sharePointLocation': {'field': 'url', 'type': 'SharePoint'}, + 'webLocation': {'field': 'url', 'type': 'Web'}, + 'customDocumentLocation': {'field': 'id', 'type': 'CustomDocument'}, + 'kendraDocumentLocation': {'field': 'uri', 'type': 'KendraDocument'}, + 'sqlLocation': {'field': 'query', 'type': 'SQL'} + } + + # Extract the URI based on location type + for loc_key, config in location_mapping.items(): + if loc_key in location: + source_uri = location[loc_key].get(config['field']) + if not location_type or location_type == 'unknown': + location_type = config['type'] + break + + # Create result object + result_object = { + 'content': content, + 'content_type': content_type, + 'source_type': location_type, + 'source_uri': source_uri + } + + # Add optional fields if available + if 'score' in result: + result_object['score'] = result['score'] + + if 'metadata' in result: + result_object['metadata'] = result['metadata'] + + # Handle byte content if present + if 'byteContent' in content_obj: + result_object['byte_content'] = content_obj['byteContent'] + + # Handle row content if present + if 'row' in content_obj: + result_object['row_content'] = content_obj['row'] + + return result_object + + def _run(self, query: str) -> str: + try: + import boto3 + from botocore.exceptions import ClientError + except ImportError: + raise ImportError("`boto3` package not found, please run `uv add boto3`") + + try: + # Initialize the Bedrock Agent Runtime client + bedrock_agent_runtime = boto3.client( + 'bedrock-agent-runtime', + region_name=os.getenv('AWS_REGION', os.getenv('AWS_DEFAULT_REGION', 'us-east-1')), + # AWS SDK will automatically use AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY from environment + ) + + # Prepare the request parameters + retrieve_params = { + 'knowledgeBaseId': self.knowledge_base_id, + 'retrievalQuery': { + 'text': query + } + } + + # Add optional parameters if provided + if self.retrieval_configuration: + retrieve_params['retrievalConfiguration'] = self.retrieval_configuration + + if self.guardrail_configuration: + retrieve_params['guardrailConfiguration'] = self.guardrail_configuration + + if self.next_token: + retrieve_params['nextToken'] = self.next_token + + # Make the retrieve API call + response = bedrock_agent_runtime.retrieve(**retrieve_params) + + # Process the response + results = [] + for result in response.get('retrievalResults', []): + processed_result = self._process_retrieval_result(result) + results.append(processed_result) + + # Build the response object + response_object = {} + if results: + response_object["results"] = results + else: + response_object["message"] = "No results found for the given query." + + if "nextToken" in response: + response_object["nextToken"] = response["nextToken"] + + if "guardrailAction" in response: + response_object["guardrailAction"] = response["guardrailAction"] + + # Return the results as a JSON string + return json.dumps(response_object, indent=2) + + except ClientError as e: + error_code = "Unknown" + error_message = str(e) + + # Try to extract error code if available + if hasattr(e, 'response') and 'Error' in e.response: + error_code = e.response['Error'].get('Code', 'Unknown') + error_message = e.response['Error'].get('Message', str(e)) + + raise BedrockKnowledgeBaseError(f"Error ({error_code}): {error_message}") + except Exception as e: + raise BedrockKnowledgeBaseError(f"Unexpected error: {str(e)}") \ No newline at end of file diff --git a/crewai_tools/aws/s3/README.md b/crewai_tools/aws/s3/README.md new file mode 100644 index 000000000..ffd74d88c --- /dev/null +++ b/crewai_tools/aws/s3/README.md @@ -0,0 +1,52 @@ +# AWS S3 Tools + +## Description + +These tools provide a way to interact with Amazon S3, a cloud storage service. + +## Installation + +Install the crewai_tools package + +```shell +pip install 'crewai[tools]' +``` + +## AWS Connectivity + +The tools use `boto3` to connect to AWS S3. +You can configure your environment to use AWS IAM roles, see [AWS IAM Roles documentation](https://docs.aws.amazon.com/sdk-for-python/v1/developer-guide/iam-roles.html#creating-an-iam-role) + +Set the following environment variables: + +- `CREW_AWS_REGION` +- `CREW_AWS_ACCESS_KEY_ID` +- `CREW_AWS_SEC_ACCESS_KEY` + +## Usage + +To use the AWS S3 tools in your CrewAI agents, import the necessary tools and include them in your agent's configuration: + +```python +from crewai_tools.aws.s3 import S3ReaderTool, S3WriterTool + +# For reading from S3 +@agent +def file_retriever(self) -> Agent: + return Agent( + config=self.agents_config['file_retriever'], + verbose=True, + tools=[S3ReaderTool()] + ) + +# For writing to S3 +@agent +def file_uploader(self) -> Agent: + return Agent( + config=self.agents_config['file_uploader'], + verbose=True, + tools=[S3WriterTool()] + ) +``` + +These tools can be used to read from and write to S3 buckets within your CrewAI workflows. Make sure you have properly configured your AWS credentials as mentioned in the AWS Connectivity section above. diff --git a/crewai_tools/aws/s3/__init__.py b/crewai_tools/aws/s3/__init__.py new file mode 100644 index 000000000..4c858837c --- /dev/null +++ b/crewai_tools/aws/s3/__init__.py @@ -0,0 +1,2 @@ +from .reader_tool import S3ReaderTool +from .writer_tool import S3WriterTool \ No newline at end of file diff --git a/crewai_tools/aws/s3/reader_tool.py b/crewai_tools/aws/s3/reader_tool.py new file mode 100644 index 000000000..c3f1fa4eb --- /dev/null +++ b/crewai_tools/aws/s3/reader_tool.py @@ -0,0 +1,47 @@ +from typing import Any, Type, List +import os + +from crewai.tools import BaseTool +from pydantic import BaseModel, Field + + +class S3ReaderToolInput(BaseModel): + """Input schema for S3ReaderTool.""" + + file_path: str = Field(..., description="S3 file path (e.g., 's3://bucket-name/file-name')") + + +class S3ReaderTool(BaseTool): + name: str = "S3 Reader Tool" + description: str = "Reads a file from Amazon S3 given an S3 file path" + args_schema: Type[BaseModel] = S3ReaderToolInput + package_dependencies: List[str] = ["boto3"] + + def _run(self, file_path: str) -> str: + try: + import boto3 + from botocore.exceptions import ClientError + except ImportError: + raise ImportError("`boto3` package not found, please run `uv add boto3`") + + try: + bucket_name, object_key = self._parse_s3_path(file_path) + + s3 = boto3.client( + 's3', + region_name=os.getenv('CREW_AWS_REGION', 'us-east-1'), + aws_access_key_id=os.getenv('CREW_AWS_ACCESS_KEY_ID'), + aws_secret_access_key=os.getenv('CREW_AWS_SEC_ACCESS_KEY') + ) + + # Read file content from S3 + response = s3.get_object(Bucket=bucket_name, Key=object_key) + file_content = response['Body'].read().decode('utf-8') + + return file_content + except ClientError as e: + return f"Error reading file from S3: {str(e)}" + + def _parse_s3_path(self, file_path: str) -> tuple: + parts = file_path.replace("s3://", "").split("/", 1) + return parts[0], parts[1] diff --git a/crewai_tools/aws/s3/writer_tool.py b/crewai_tools/aws/s3/writer_tool.py new file mode 100644 index 000000000..2e1528d13 --- /dev/null +++ b/crewai_tools/aws/s3/writer_tool.py @@ -0,0 +1,43 @@ +from typing import Type, List +import os + +from crewai.tools import BaseTool +from pydantic import BaseModel, Field + +class S3WriterToolInput(BaseModel): + """Input schema for S3WriterTool.""" + file_path: str = Field(..., description="S3 file path (e.g., 's3://bucket-name/file-name')") + content: str = Field(..., description="Content to write to the file") + + +class S3WriterTool(BaseTool): + name: str = "S3 Writer Tool" + description: str = "Writes content to a file in Amazon S3 given an S3 file path" + args_schema: Type[BaseModel] = S3WriterToolInput + package_dependencies: List[str] = ["boto3"] + + def _run(self, file_path: str, content: str) -> str: + try: + import boto3 + from botocore.exceptions import ClientError + except ImportError: + raise ImportError("`boto3` package not found, please run `uv add boto3`") + + try: + bucket_name, object_key = self._parse_s3_path(file_path) + + s3 = boto3.client( + 's3', + region_name=os.getenv('CREW_AWS_REGION', 'us-east-1'), + aws_access_key_id=os.getenv('CREW_AWS_ACCESS_KEY_ID'), + aws_secret_access_key=os.getenv('CREW_AWS_SEC_ACCESS_KEY') + ) + + s3.put_object(Bucket=bucket_name, Key=object_key, Body=content.encode('utf-8')) + return f"Successfully wrote content to {file_path}" + except ClientError as e: + return f"Error writing file to S3: {str(e)}" + + def _parse_s3_path(self, file_path: str) -> tuple: + parts = file_path.replace("s3://", "").split("/", 1) + return parts[0], parts[1] diff --git a/crewai_tools/printer.py b/crewai_tools/printer.py new file mode 100644 index 000000000..c67005ddd --- /dev/null +++ b/crewai_tools/printer.py @@ -0,0 +1,131 @@ +"""Utility for colored console output.""" + +from typing import Optional + + +class Printer: + """Handles colored console output formatting.""" + + @staticmethod + def print(content: str, color: Optional[str] = None) -> None: + """Prints content with optional color formatting. + + Args: + content: The string to be printed. + color: Optional color name to format the output. If provided, + must match one of the _print_* methods available in this class. + If not provided or if the color is not supported, prints without + formatting. + """ + if hasattr(Printer, f"_print_{color}"): + getattr(Printer, f"_print_{color}")(content) + else: + print(content) + + @staticmethod + def _print_bold_purple(content: str) -> None: + """Prints content in bold purple color. + + Args: + content: The string to be printed in bold purple. + """ + print("\033[1m\033[95m {}\033[00m".format(content)) + + @staticmethod + def _print_bold_green(content: str) -> None: + """Prints content in bold green color. + + Args: + content: The string to be printed in bold green. + """ + print("\033[1m\033[92m {}\033[00m".format(content)) + + @staticmethod + def _print_purple(content: str) -> None: + """Prints content in purple color. + + Args: + content: The string to be printed in purple. + """ + print("\033[95m {}\033[00m".format(content)) + + @staticmethod + def _print_red(content: str) -> None: + """Prints content in red color. + + Args: + content: The string to be printed in red. + """ + print("\033[91m {}\033[00m".format(content)) + + @staticmethod + def _print_bold_blue(content: str) -> None: + """Prints content in bold blue color. + + Args: + content: The string to be printed in bold blue. + """ + print("\033[1m\033[94m {}\033[00m".format(content)) + + @staticmethod + def _print_yellow(content: str) -> None: + """Prints content in yellow color. + + Args: + content: The string to be printed in yellow. + """ + print("\033[93m {}\033[00m".format(content)) + + @staticmethod + def _print_bold_yellow(content: str) -> None: + """Prints content in bold yellow color. + + Args: + content: The string to be printed in bold yellow. + """ + print("\033[1m\033[93m {}\033[00m".format(content)) + + @staticmethod + def _print_cyan(content: str) -> None: + """Prints content in cyan color. + + Args: + content: The string to be printed in cyan. + """ + print("\033[96m {}\033[00m".format(content)) + + @staticmethod + def _print_bold_cyan(content: str) -> None: + """Prints content in bold cyan color. + + Args: + content: The string to be printed in bold cyan. + """ + print("\033[1m\033[96m {}\033[00m".format(content)) + + @staticmethod + def _print_magenta(content: str) -> None: + """Prints content in magenta color. + + Args: + content: The string to be printed in magenta. + """ + print("\033[35m {}\033[00m".format(content)) + + @staticmethod + def _print_bold_magenta(content: str) -> None: + """Prints content in bold magenta color. + + Args: + content: The string to be printed in bold magenta. + """ + print("\033[1m\033[35m {}\033[00m".format(content)) + + @staticmethod + def _print_green(content: str) -> None: + """Prints content in green color. + + Args: + content: The string to be printed in green. + """ + print("\033[32m {}\033[00m".format(content)) diff --git a/crewai_tools/rag/__init__.py b/crewai_tools/rag/__init__.py new file mode 100644 index 000000000..8d08b2907 --- /dev/null +++ b/crewai_tools/rag/__init__.py @@ -0,0 +1,8 @@ +from crewai_tools.rag.core import RAG, EmbeddingService +from crewai_tools.rag.data_types import DataType + +__all__ = [ + "RAG", + "EmbeddingService", + "DataType", +] diff --git a/crewai_tools/rag/base_loader.py b/crewai_tools/rag/base_loader.py new file mode 100644 index 000000000..e38d6f8c1 --- /dev/null +++ b/crewai_tools/rag/base_loader.py @@ -0,0 +1,37 @@ +from abc import ABC, abstractmethod +from typing import Any, Dict, Optional +from pydantic import BaseModel, Field + +from crewai_tools.rag.misc import compute_sha256 +from crewai_tools.rag.source_content import SourceContent + + +class LoaderResult(BaseModel): + content: str = Field(description="The text content of the source") + source: str = Field(description="The source of the content", default="unknown") + metadata: Dict[str, Any] = Field(description="The metadata of the source", default_factory=dict) + doc_id: str = Field(description="The id of the document") + + +class BaseLoader(ABC): + def __init__(self, config: Optional[Dict[str, Any]] = None): + self.config = config or {} + + @abstractmethod + def load(self, content: SourceContent, **kwargs) -> LoaderResult: + ... + + def generate_doc_id(self, source_ref: str | None = None, content: str | None = None) -> str: + """ + Generate a unique document id based on the source reference and content. + If the source reference is not provided, the content is used as the source reference. + If the content is not provided, the source reference is used as the content. + If both are provided, the source reference is used as the content. + + Both are optional because the TEXT content type does not have a source reference. In this case, the content is used as the source reference. + """ + + source_ref = source_ref or "" + content = content or "" + + return compute_sha256(source_ref + content) diff --git a/crewai_tools/rag/chunkers/__init__.py b/crewai_tools/rag/chunkers/__init__.py new file mode 100644 index 000000000..f48483391 --- /dev/null +++ b/crewai_tools/rag/chunkers/__init__.py @@ -0,0 +1,15 @@ +from crewai_tools.rag.chunkers.base_chunker import BaseChunker +from crewai_tools.rag.chunkers.default_chunker import DefaultChunker +from crewai_tools.rag.chunkers.text_chunker import TextChunker, DocxChunker, MdxChunker +from crewai_tools.rag.chunkers.structured_chunker import CsvChunker, JsonChunker, XmlChunker + +__all__ = [ + "BaseChunker", + "DefaultChunker", + "TextChunker", + "DocxChunker", + "MdxChunker", + "CsvChunker", + "JsonChunker", + "XmlChunker", +] diff --git a/crewai_tools/rag/chunkers/base_chunker.py b/crewai_tools/rag/chunkers/base_chunker.py new file mode 100644 index 000000000..deafbfc7a --- /dev/null +++ b/crewai_tools/rag/chunkers/base_chunker.py @@ -0,0 +1,167 @@ +from typing import List, Optional +import re + +class RecursiveCharacterTextSplitter: + """ + A text splitter that recursively splits text based on a hierarchy of separators. + """ + + def __init__( + self, + chunk_size: int = 4000, + chunk_overlap: int = 200, + separators: Optional[List[str]] = None, + keep_separator: bool = True, + ): + """ + Initialize the RecursiveCharacterTextSplitter. + + Args: + chunk_size: Maximum size of each chunk + chunk_overlap: Number of characters to overlap between chunks + separators: List of separators to use for splitting (in order of preference) + keep_separator: Whether to keep the separator in the split text + """ + if chunk_overlap >= chunk_size: + raise ValueError(f"Chunk overlap ({chunk_overlap}) cannot be >= chunk size ({chunk_size})") + + self._chunk_size = chunk_size + self._chunk_overlap = chunk_overlap + self._keep_separator = keep_separator + + self._separators = separators or [ + "\n\n", + "\n", + " ", + "", + ] + + def split_text(self, text: str) -> List[str]: + return self._split_text(text, self._separators) + + def _split_text(self, text: str, separators: List[str]) -> List[str]: + separator = separators[-1] + new_separators = [] + + for i, sep in enumerate(separators): + if sep == "": + separator = sep + break + if re.search(re.escape(sep), text): + separator = sep + new_separators = separators[i + 1:] + break + + splits = self._split_text_with_separator(text, separator) + + good_splits = [] + + for split in splits: + if len(split) < self._chunk_size: + good_splits.append(split) + else: + if new_separators: + other_info = self._split_text(split, new_separators) + good_splits.extend(other_info) + else: + good_splits.extend(self._split_by_characters(split)) + + return self._merge_splits(good_splits, separator) + + def _split_text_with_separator(self, text: str, separator: str) -> List[str]: + if separator == "": + return list(text) + + if self._keep_separator and separator in text: + parts = text.split(separator) + splits = [] + + for i, part in enumerate(parts): + if i == 0: + splits.append(part) + elif i == len(parts) - 1: + if part: + splits.append(separator + part) + else: + if part: + splits.append(separator + part) + else: + if splits: + splits[-1] += separator + + return [s for s in splits if s] + else: + return text.split(separator) + + def _split_by_characters(self, text: str) -> List[str]: + chunks = [] + for i in range(0, len(text), self._chunk_size): + chunks.append(text[i:i + self._chunk_size]) + return chunks + + def _merge_splits(self, splits: List[str], separator: str) -> List[str]: + """Merge splits into chunks with proper overlap.""" + docs = [] + current_doc = [] + total = 0 + + for split in splits: + split_len = len(split) + + if total + split_len > self._chunk_size and current_doc: + if separator == "": + doc = "".join(current_doc) + else: + doc = separator.join(current_doc) + + if doc: + docs.append(doc) + + # Handle overlap by keeping some of the previous content + while total > self._chunk_overlap and len(current_doc) > 1: + removed = current_doc.pop(0) + total -= len(removed) + if separator != "": + total -= len(separator) + + current_doc.append(split) + total += split_len + if separator != "" and len(current_doc) > 1: + total += len(separator) + + if current_doc: + if separator == "": + doc = "".join(current_doc) + else: + doc = separator.join(current_doc) + + if doc: + docs.append(doc) + + return docs + +class BaseChunker: + def __init__(self, chunk_size: int = 1000, chunk_overlap: int = 200, separators: Optional[List[str]] = None, keep_separator: bool = True): + """ + Initialize the Chunker + + Args: + chunk_size: Maximum size of each chunk + chunk_overlap: Number of characters to overlap between chunks + separators: List of separators to use for splitting + keep_separator: Whether to keep separators in the chunks + """ + + self._splitter = RecursiveCharacterTextSplitter( + chunk_size=chunk_size, + chunk_overlap=chunk_overlap, + separators=separators, + keep_separator=keep_separator, + ) + + + def chunk(self, text: str) -> List[str]: + if not text or not text.strip(): + return [] + + return self._splitter.split_text(text) diff --git a/crewai_tools/rag/chunkers/default_chunker.py b/crewai_tools/rag/chunkers/default_chunker.py new file mode 100644 index 000000000..0d0ec6935 --- /dev/null +++ b/crewai_tools/rag/chunkers/default_chunker.py @@ -0,0 +1,6 @@ +from crewai_tools.rag.chunkers.base_chunker import BaseChunker +from typing import List, Optional + +class DefaultChunker(BaseChunker): + def __init__(self, chunk_size: int = 2000, chunk_overlap: int = 20, separators: Optional[List[str]] = None, keep_separator: bool = True): + super().__init__(chunk_size, chunk_overlap, separators, keep_separator) diff --git a/crewai_tools/rag/chunkers/structured_chunker.py b/crewai_tools/rag/chunkers/structured_chunker.py new file mode 100644 index 000000000..483f92588 --- /dev/null +++ b/crewai_tools/rag/chunkers/structured_chunker.py @@ -0,0 +1,49 @@ +from crewai_tools.rag.chunkers.base_chunker import BaseChunker +from typing import List, Optional + + +class CsvChunker(BaseChunker): + def __init__(self, chunk_size: int = 1200, chunk_overlap: int = 100, separators: Optional[List[str]] = None, keep_separator: bool = True): + if separators is None: + separators = [ + "\nRow ", # Row boundaries (from CSVLoader format) + "\n", # Line breaks + " | ", # Column separators + ", ", # Comma separators + " ", # Word breaks + "", # Character level + ] + super().__init__(chunk_size, chunk_overlap, separators, keep_separator) + + +class JsonChunker(BaseChunker): + def __init__(self, chunk_size: int = 2000, chunk_overlap: int = 200, separators: Optional[List[str]] = None, keep_separator: bool = True): + if separators is None: + separators = [ + "\n\n", # Object/array boundaries + "\n", # Line breaks + "},", # Object endings + "],", # Array endings + ", ", # Property separators + ": ", # Key-value separators + " ", # Word breaks + "", # Character level + ] + super().__init__(chunk_size, chunk_overlap, separators, keep_separator) + + +class XmlChunker(BaseChunker): + def __init__(self, chunk_size: int = 2500, chunk_overlap: int = 250, separators: Optional[List[str]] = None, keep_separator: bool = True): + if separators is None: + separators = [ + "\n\n", # Element boundaries + "\n", # Line breaks + ">", # Tag endings + ". ", # Sentence endings (for text content) + "! ", # Exclamation endings + "? ", # Question endings + ", ", # Comma separators + " ", # Word breaks + "", # Character level + ] + super().__init__(chunk_size, chunk_overlap, separators, keep_separator) diff --git a/crewai_tools/rag/chunkers/text_chunker.py b/crewai_tools/rag/chunkers/text_chunker.py new file mode 100644 index 000000000..2e76df8ab --- /dev/null +++ b/crewai_tools/rag/chunkers/text_chunker.py @@ -0,0 +1,59 @@ +from crewai_tools.rag.chunkers.base_chunker import BaseChunker +from typing import List, Optional + + +class TextChunker(BaseChunker): + def __init__(self, chunk_size: int = 1500, chunk_overlap: int = 150, separators: Optional[List[str]] = None, keep_separator: bool = True): + if separators is None: + separators = [ + "\n\n\n", # Multiple line breaks (sections) + "\n\n", # Paragraph breaks + "\n", # Line breaks + ". ", # Sentence endings + "! ", # Exclamation endings + "? ", # Question endings + "; ", # Semicolon breaks + ", ", # Comma breaks + " ", # Word breaks + "", # Character level + ] + super().__init__(chunk_size, chunk_overlap, separators, keep_separator) + + +class DocxChunker(BaseChunker): + def __init__(self, chunk_size: int = 2500, chunk_overlap: int = 250, separators: Optional[List[str]] = None, keep_separator: bool = True): + if separators is None: + separators = [ + "\n\n\n", # Multiple line breaks (major sections) + "\n\n", # Paragraph breaks + "\n", # Line breaks + ". ", # Sentence endings + "! ", # Exclamation endings + "? ", # Question endings + "; ", # Semicolon breaks + ", ", # Comma breaks + " ", # Word breaks + "", # Character level + ] + super().__init__(chunk_size, chunk_overlap, separators, keep_separator) + + +class MdxChunker(BaseChunker): + def __init__(self, chunk_size: int = 3000, chunk_overlap: int = 300, separators: Optional[List[str]] = None, keep_separator: bool = True): + if separators is None: + separators = [ + "\n## ", # H2 headers (major sections) + "\n### ", # H3 headers (subsections) + "\n#### ", # H4 headers (sub-subsections) + "\n\n", # Paragraph breaks + "\n```", # Code block boundaries + "\n", # Line breaks + ". ", # Sentence endings + "! ", # Exclamation endings + "? ", # Question endings + "; ", # Semicolon breaks + ", ", # Comma breaks + " ", # Word breaks + "", # Character level + ] + super().__init__(chunk_size, chunk_overlap, separators, keep_separator) diff --git a/crewai_tools/rag/chunkers/web_chunker.py b/crewai_tools/rag/chunkers/web_chunker.py new file mode 100644 index 000000000..2712a6c69 --- /dev/null +++ b/crewai_tools/rag/chunkers/web_chunker.py @@ -0,0 +1,20 @@ +from crewai_tools.rag.chunkers.base_chunker import BaseChunker +from typing import List, Optional + + +class WebsiteChunker(BaseChunker): + def __init__(self, chunk_size: int = 2500, chunk_overlap: int = 250, separators: Optional[List[str]] = None, keep_separator: bool = True): + if separators is None: + separators = [ + "\n\n\n", # Major section breaks + "\n\n", # Paragraph breaks + "\n", # Line breaks + ". ", # Sentence endings + "! ", # Exclamation endings + "? ", # Question endings + "; ", # Semicolon breaks + ", ", # Comma breaks + " ", # Word breaks + "", # Character level + ] + super().__init__(chunk_size, chunk_overlap, separators, keep_separator) diff --git a/crewai_tools/rag/core.py b/crewai_tools/rag/core.py new file mode 100644 index 000000000..0aa4b666c --- /dev/null +++ b/crewai_tools/rag/core.py @@ -0,0 +1,232 @@ +import logging +from pathlib import Path +from typing import Any, Dict, List, Optional, Union +from uuid import uuid4 + +import chromadb +import litellm +from pydantic import BaseModel, Field, PrivateAttr + +from crewai_tools.tools.rag.rag_tool import Adapter +from crewai_tools.rag.data_types import DataType +from crewai_tools.rag.base_loader import BaseLoader +from crewai_tools.rag.chunkers.base_chunker import BaseChunker +from crewai_tools.rag.source_content import SourceContent +from crewai_tools.rag.misc import compute_sha256 + +logger = logging.getLogger(__name__) + + +class EmbeddingService: + def __init__(self, model: str = "text-embedding-3-small", **kwargs): + self.model = model + self.kwargs = kwargs + + def embed_text(self, text: str) -> List[float]: + try: + response = litellm.embedding( + model=self.model, + input=[text], + **self.kwargs + ) + return response.data[0]['embedding'] + except Exception as e: + logger.error(f"Error generating embedding: {e}") + raise + + def embed_batch(self, texts: List[str]) -> List[List[float]]: + if not texts: + return [] + + try: + response = litellm.embedding( + model=self.model, + input=texts, + **self.kwargs + ) + return [data['embedding'] for data in response.data] + except Exception as e: + logger.error(f"Error generating batch embeddings: {e}") + raise + + +class Document(BaseModel): + id: str = Field(default_factory=lambda: str(uuid4())) + content: str + metadata: Dict[str, Any] = Field(default_factory=dict) + data_type: DataType = DataType.TEXT + source: Optional[str] = None + + +class RAG(Adapter): + collection_name: str = "crewai_knowledge_base" + persist_directory: Optional[str] = None + embedding_model: str = "text-embedding-3-large" + summarize: bool = False + top_k: int = 5 + embedding_config: Dict[str, Any] = Field(default_factory=dict) + + _client: Any = PrivateAttr() + _collection: Any = PrivateAttr() + _embedding_service: EmbeddingService = PrivateAttr() + + def model_post_init(self, __context: Any) -> None: + try: + if self.persist_directory: + self._client = chromadb.PersistentClient(path=self.persist_directory) + else: + self._client = chromadb.Client() + + self._collection = self._client.get_or_create_collection( + name=self.collection_name, + metadata={"hnsw:space": "cosine", "description": "CrewAI Knowledge Base"} + ) + + self._embedding_service = EmbeddingService(model=self.embedding_model, **self.embedding_config) + except Exception as e: + logger.error(f"Failed to initialize ChromaDB: {e}") + raise + + super().model_post_init(__context) + + def add( + self, + content: str | Path, + data_type: Optional[Union[str, DataType]] = None, + metadata: Optional[Dict[str, Any]] = None, + loader: Optional[BaseLoader] = None, + chunker: Optional[BaseChunker] = None, + **kwargs: Any + ) -> None: + source_content = SourceContent(content) + + data_type = self._get_data_type(data_type=data_type, content=source_content) + + if not loader: + loader = data_type.get_loader() + + if not chunker: + chunker = data_type.get_chunker() + + loader_result = loader.load(source_content) + doc_id = loader_result.doc_id + + existing_doc = self._collection.get(where={"source": source_content.source_ref}, limit=1) + existing_doc_id = existing_doc and existing_doc['metadatas'][0]['doc_id'] if existing_doc['metadatas'] else None + + if existing_doc_id == doc_id: + logger.warning(f"Document with source {loader_result.source} already exists") + return + + # Document with same source ref does exists but the content has changed, deleting the oldest reference + if existing_doc_id and existing_doc_id != loader_result.doc_id: + logger.warning(f"Deleting old document with doc_id {existing_doc_id}") + self._collection.delete(where={"doc_id": existing_doc_id}) + + documents = [] + + chunks = chunker.chunk(loader_result.content) + for i, chunk in enumerate(chunks): + doc_metadata = (metadata or {}).copy() + doc_metadata['chunk_index'] = i + documents.append(Document( + id=compute_sha256(chunk), + content=chunk, + metadata=doc_metadata, + data_type=data_type, + source=loader_result.source + )) + + if not documents: + logger.warning("No documents to add") + return + + contents = [doc.content for doc in documents] + try: + embeddings = self._embedding_service.embed_batch(contents) + except Exception as e: + logger.error(f"Failed to generate embeddings: {e}") + return + + ids = [doc.id for doc in documents] + metadatas = [] + + for doc in documents: + doc_metadata = doc.metadata.copy() + doc_metadata.update({ + "data_type": doc.data_type.value, + "source": doc.source, + "doc_id": doc_id + }) + metadatas.append(doc_metadata) + + try: + self._collection.add( + ids=ids, + embeddings=embeddings, + documents=contents, + metadatas=metadatas, + ) + logger.info(f"Added {len(documents)} documents to knowledge base") + except Exception as e: + logger.error(f"Failed to add documents to ChromaDB: {e}") + + def query(self, question: str, where: Optional[Dict[str, Any]] = None) -> str: + try: + question_embedding = self._embedding_service.embed_text(question) + + results = self._collection.query( + query_embeddings=[question_embedding], + n_results=self.top_k, + where=where, + include=["documents", "metadatas", "distances"] + ) + + if not results or not results.get("documents") or not results["documents"][0]: + return "No relevant content found." + + documents = results["documents"][0] + metadatas = results.get("metadatas", [None])[0] or [] + distances = results.get("distances", [None])[0] or [] + + # Return sources with relevance scores + formatted_results = [] + for i, doc in enumerate(documents): + metadata = metadatas[i] if i < len(metadatas) else {} + distance = distances[i] if i < len(distances) else 1.0 + source = metadata.get("source", "unknown") if metadata else "unknown" + score = 1 - distance if distance is not None else 0 # Convert distance to similarity + formatted_results.append(f"[Source: {source}, Relevance: {score:.3f}]\n{doc}") + + return "\n\n".join(formatted_results) + except Exception as e: + logger.error(f"Query failed: {e}") + return f"Error querying knowledge base: {e}" + + def delete_collection(self) -> None: + try: + self._client.delete_collection(self.collection_name) + logger.info(f"Deleted collection: {self.collection_name}") + except Exception as e: + logger.error(f"Failed to delete collection: {e}") + + def get_collection_info(self) -> Dict[str, Any]: + try: + count = self._collection.count() + return { + "name": self.collection_name, + "count": count, + "embedding_model": self.embedding_model + } + except Exception as e: + logger.error(f"Failed to get collection info: {e}") + return {"error": str(e)} + + def _get_data_type(self, content: SourceContent, data_type: str | DataType | None = None) -> DataType: + try: + if isinstance(data_type, str): + return DataType(data_type) + except Exception as e: + pass + + return content.data_type diff --git a/crewai_tools/rag/data_types.py b/crewai_tools/rag/data_types.py new file mode 100644 index 000000000..d2d265cce --- /dev/null +++ b/crewai_tools/rag/data_types.py @@ -0,0 +1,137 @@ +from enum import Enum +from pathlib import Path +from urllib.parse import urlparse +import os +from crewai_tools.rag.chunkers.base_chunker import BaseChunker +from crewai_tools.rag.base_loader import BaseLoader + +class DataType(str, Enum): + PDF_FILE = "pdf_file" + TEXT_FILE = "text_file" + CSV = "csv" + JSON = "json" + XML = "xml" + DOCX = "docx" + MDX = "mdx" + + # Database types + MYSQL = "mysql" + POSTGRES = "postgres" + + # Repository types + GITHUB = "github" + DIRECTORY = "directory" + + # Web types + WEBSITE = "website" + DOCS_SITE = "docs_site" + + # Raw types + TEXT = "text" + + + def get_chunker(self) -> BaseChunker: + from importlib import import_module + + chunkers = { + DataType.TEXT_FILE: ("text_chunker", "TextChunker"), + DataType.TEXT: ("text_chunker", "TextChunker"), + DataType.DOCX: ("text_chunker", "DocxChunker"), + DataType.MDX: ("text_chunker", "MdxChunker"), + + # Structured formats + DataType.CSV: ("structured_chunker", "CsvChunker"), + DataType.JSON: ("structured_chunker", "JsonChunker"), + DataType.XML: ("structured_chunker", "XmlChunker"), + + DataType.WEBSITE: ("web_chunker", "WebsiteChunker"), + } + + module_name, class_name = chunkers.get(self, ("default_chunker", "DefaultChunker")) + module_path = f"crewai_tools.rag.chunkers.{module_name}" + + try: + module = import_module(module_path) + return getattr(module, class_name)() + except Exception as e: + raise ValueError(f"Error loading chunker for {self}: {e}") + + def get_loader(self) -> BaseLoader: + from importlib import import_module + + loaders = { + DataType.TEXT_FILE: ("text_loader", "TextFileLoader"), + DataType.TEXT: ("text_loader", "TextLoader"), + DataType.XML: ("xml_loader", "XMLLoader"), + DataType.WEBSITE: ("webpage_loader", "WebPageLoader"), + DataType.MDX: ("mdx_loader", "MDXLoader"), + DataType.JSON: ("json_loader", "JSONLoader"), + DataType.DOCX: ("docx_loader", "DOCXLoader"), + DataType.CSV: ("csv_loader", "CSVLoader"), + DataType.DIRECTORY: ("directory_loader", "DirectoryLoader"), + } + + module_name, class_name = loaders.get(self, ("text_loader", "TextLoader")) + module_path = f"crewai_tools.rag.loaders.{module_name}" + try: + module = import_module(module_path) + return getattr(module, class_name)() + except Exception as e: + raise ValueError(f"Error loading loader for {self}: {e}") + +class DataTypes: + @staticmethod + def from_content(content: str | Path | None = None) -> DataType: + if content is None: + return DataType.TEXT + + if isinstance(content, Path): + content = str(content) + + is_url = False + if isinstance(content, str): + try: + url = urlparse(content) + is_url = (url.scheme and url.netloc) or url.scheme == "file" + except Exception: + pass + + def get_file_type(path: str) -> DataType | None: + mapping = { + ".pdf": DataType.PDF_FILE, + ".csv": DataType.CSV, + ".mdx": DataType.MDX, + ".md": DataType.MDX, + ".docx": DataType.DOCX, + ".json": DataType.JSON, + ".xml": DataType.XML, + ".txt": DataType.TEXT_FILE, + } + for ext, dtype in mapping.items(): + if path.endswith(ext): + return dtype + return None + + if is_url: + dtype = get_file_type(url.path) + if dtype: + return dtype + + if "docs" in url.netloc or ("docs" in url.path and url.scheme != "file"): + return DataType.DOCS_SITE + if "github.com" in url.netloc: + return DataType.GITHUB + + return DataType.WEBSITE + + if os.path.isfile(content): + dtype = get_file_type(content) + if dtype: + return dtype + + if os.path.exists(content): + return DataType.TEXT_FILE + elif os.path.isdir(content): + return DataType.DIRECTORY + + return DataType.TEXT diff --git a/crewai_tools/rag/loaders/__init__.py b/crewai_tools/rag/loaders/__init__.py new file mode 100644 index 000000000..503651468 --- /dev/null +++ b/crewai_tools/rag/loaders/__init__.py @@ -0,0 +1,20 @@ +from crewai_tools.rag.loaders.text_loader import TextFileLoader, TextLoader +from crewai_tools.rag.loaders.xml_loader import XMLLoader +from crewai_tools.rag.loaders.webpage_loader import WebPageLoader +from crewai_tools.rag.loaders.mdx_loader import MDXLoader +from crewai_tools.rag.loaders.json_loader import JSONLoader +from crewai_tools.rag.loaders.docx_loader import DOCXLoader +from crewai_tools.rag.loaders.csv_loader import CSVLoader +from crewai_tools.rag.loaders.directory_loader import DirectoryLoader + +__all__ = [ + "TextFileLoader", + "TextLoader", + "XMLLoader", + "WebPageLoader", + "MDXLoader", + "JSONLoader", + "DOCXLoader", + "CSVLoader", + "DirectoryLoader", +] diff --git a/crewai_tools/rag/loaders/csv_loader.py b/crewai_tools/rag/loaders/csv_loader.py new file mode 100644 index 000000000..e389123a7 --- /dev/null +++ b/crewai_tools/rag/loaders/csv_loader.py @@ -0,0 +1,72 @@ +import csv +from io import StringIO + +from crewai_tools.rag.base_loader import BaseLoader, LoaderResult +from crewai_tools.rag.source_content import SourceContent + + +class CSVLoader(BaseLoader): + def load(self, source_content: SourceContent, **kwargs) -> LoaderResult: + source_ref = source_content.source_ref + + content_str = source_content.source + if source_content.is_url(): + content_str = self._load_from_url(content_str, kwargs) + elif source_content.path_exists(): + content_str = self._load_from_file(content_str) + + return self._parse_csv(content_str, source_ref) + + + def _load_from_url(self, url: str, kwargs: dict) -> str: + import requests + + headers = kwargs.get("headers", { + "Accept": "text/csv, application/csv, text/plain", + "User-Agent": "Mozilla/5.0 (compatible; crewai-tools CSVLoader)" + }) + + try: + response = requests.get(url, headers=headers, timeout=30) + response.raise_for_status() + return response.text + except Exception as e: + raise ValueError(f"Error fetching CSV from URL {url}: {str(e)}") + + def _load_from_file(self, path: str) -> str: + with open(path, "r", encoding="utf-8") as file: + return file.read() + + def _parse_csv(self, content: str, source_ref: str) -> LoaderResult: + try: + csv_reader = csv.DictReader(StringIO(content)) + + text_parts = [] + headers = csv_reader.fieldnames + + if headers: + text_parts.append("Headers: " + " | ".join(headers)) + text_parts.append("-" * 50) + + for row_num, row in enumerate(csv_reader, 1): + row_text = " | ".join([f"{k}: {v}" for k, v in row.items() if v]) + text_parts.append(f"Row {row_num}: {row_text}") + + text = "\n".join(text_parts) + + metadata = { + "format": "csv", + "columns": headers, + "rows": len(text_parts) - 2 if headers else 0 + } + + except Exception as e: + text = content + metadata = {"format": "csv", "parse_error": str(e)} + + return LoaderResult( + content=text, + source=source_ref, + metadata=metadata, + doc_id=self.generate_doc_id(source_ref=source_ref, content=text) + ) diff --git a/crewai_tools/rag/loaders/directory_loader.py b/crewai_tools/rag/loaders/directory_loader.py new file mode 100644 index 000000000..7bc5f298b --- /dev/null +++ b/crewai_tools/rag/loaders/directory_loader.py @@ -0,0 +1,142 @@ +import os +from pathlib import Path +from typing import List + +from crewai_tools.rag.base_loader import BaseLoader, LoaderResult +from crewai_tools.rag.source_content import SourceContent + + +class DirectoryLoader(BaseLoader): + def load(self, source_content: SourceContent, **kwargs) -> LoaderResult: + """ + Load and process all files from a directory recursively. + + Args: + source: Directory path or URL to a directory listing + **kwargs: Additional options: + - recursive: bool (default True) - Whether to search recursively + - include_extensions: list - Only include files with these extensions + - exclude_extensions: list - Exclude files with these extensions + - max_files: int - Maximum number of files to process + """ + source_ref = source_content.source_ref + + if source_content.is_url(): + raise ValueError("URL directory loading is not supported. Please provide a local directory path.") + + if not os.path.exists(source_ref): + raise FileNotFoundError(f"Directory does not exist: {source_ref}") + + if not os.path.isdir(source_ref): + raise ValueError(f"Path is not a directory: {source_ref}") + + return self._process_directory(source_ref, kwargs) + + def _process_directory(self, dir_path: str, kwargs: dict) -> LoaderResult: + recursive = kwargs.get("recursive", True) + include_extensions = kwargs.get("include_extensions", None) + exclude_extensions = kwargs.get("exclude_extensions", None) + max_files = kwargs.get("max_files", None) + + files = self._find_files(dir_path, recursive, include_extensions, exclude_extensions) + + if max_files and len(files) > max_files: + files = files[:max_files] + + all_contents = [] + processed_files = [] + errors = [] + + for file_path in files: + try: + result = self._process_single_file(file_path) + if result: + all_contents.append(f"=== File: {file_path} ===\n{result.content}") + processed_files.append({ + "path": file_path, + "metadata": result.metadata, + "source": result.source + }) + except Exception as e: + error_msg = f"Error processing {file_path}: {str(e)}" + errors.append(error_msg) + all_contents.append(f"=== File: {file_path} (ERROR) ===\n{error_msg}") + + combined_content = "\n\n".join(all_contents) + + metadata = { + "format": "directory", + "directory_path": dir_path, + "total_files": len(files), + "processed_files": len(processed_files), + "errors": len(errors), + "file_details": processed_files, + "error_details": errors + } + + return LoaderResult( + content=combined_content, + source=dir_path, + metadata=metadata, + doc_id=self.generate_doc_id(source_ref=dir_path, content=combined_content) + ) + + def _find_files(self, dir_path: str, recursive: bool, include_ext: List[str] | None = None, exclude_ext: List[str] | None = None) -> List[str]: + """Find all files in directory matching criteria.""" + files = [] + + if recursive: + for root, dirs, filenames in os.walk(dir_path): + dirs[:] = [d for d in dirs if not d.startswith('.')] + + for filename in filenames: + if self._should_include_file(filename, include_ext, exclude_ext): + files.append(os.path.join(root, filename)) + else: + try: + for item in os.listdir(dir_path): + item_path = os.path.join(dir_path, item) + if os.path.isfile(item_path) and self._should_include_file(item, include_ext, exclude_ext): + files.append(item_path) + except PermissionError: + pass + + return sorted(files) + + def _should_include_file(self, filename: str, include_ext: List[str] = None, exclude_ext: List[str] = None) -> bool: + """Determine if a file should be included based on criteria.""" + if filename.startswith('.'): + return False + + _, ext = os.path.splitext(filename.lower()) + + if include_ext: + if ext not in [e.lower() if e.startswith('.') else f'.{e.lower()}' for e in include_ext]: + return False + + if exclude_ext: + if ext in [e.lower() if e.startswith('.') else f'.{e.lower()}' for e in exclude_ext]: + return False + + return True + + def _process_single_file(self, file_path: str) -> LoaderResult: + from crewai_tools.rag.data_types import DataTypes + + data_type = DataTypes.from_content(Path(file_path)) + + loader = data_type.get_loader() + + result = loader.load(SourceContent(file_path)) + + if result.metadata is None: + result.metadata = {} + + result.metadata.update({ + "file_path": file_path, + "file_size": os.path.getsize(file_path), + "data_type": str(data_type), + "loader_type": loader.__class__.__name__ + }) + + return result diff --git a/crewai_tools/rag/loaders/docx_loader.py b/crewai_tools/rag/loaders/docx_loader.py new file mode 100644 index 000000000..2f5df23af --- /dev/null +++ b/crewai_tools/rag/loaders/docx_loader.py @@ -0,0 +1,72 @@ +import os +import tempfile + +from crewai_tools.rag.base_loader import BaseLoader, LoaderResult +from crewai_tools.rag.source_content import SourceContent + + +class DOCXLoader(BaseLoader): + def load(self, source_content: SourceContent, **kwargs) -> LoaderResult: + try: + from docx import Document as DocxDocument + except ImportError: + raise ImportError("python-docx is required for DOCX loading. Install with: 'uv pip install python-docx' or pip install crewai-tools[rag]") + + source_ref = source_content.source_ref + + if source_content.is_url(): + temp_file = self._download_from_url(source_ref, kwargs) + try: + return self._load_from_file(temp_file, source_ref, DocxDocument) + finally: + os.unlink(temp_file) + elif source_content.path_exists(): + return self._load_from_file(source_ref, source_ref, DocxDocument) + else: + raise ValueError(f"Source must be a valid file path or URL, got: {source_content.source}") + + def _download_from_url(self, url: str, kwargs: dict) -> str: + import requests + + headers = kwargs.get("headers", { + "Accept": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + "User-Agent": "Mozilla/5.0 (compatible; crewai-tools DOCXLoader)" + }) + + try: + response = requests.get(url, headers=headers, timeout=30) + response.raise_for_status() + + # Create temporary file to save the DOCX content + with tempfile.NamedTemporaryFile(suffix='.docx', delete=False) as temp_file: + temp_file.write(response.content) + return temp_file.name + except Exception as e: + raise ValueError(f"Error fetching DOCX from URL {url}: {str(e)}") + + def _load_from_file(self, file_path: str, source_ref: str, DocxDocument) -> LoaderResult: + try: + doc = DocxDocument(file_path) + + text_parts = [] + for paragraph in doc.paragraphs: + if paragraph.text.strip(): + text_parts.append(paragraph.text) + + content = "\n".join(text_parts) + + metadata = { + "format": "docx", + "paragraphs": len(doc.paragraphs), + "tables": len(doc.tables) + } + + return LoaderResult( + content=content, + source=source_ref, + metadata=metadata, + doc_id=self.generate_doc_id(source_ref=source_ref, content=content) + ) + + except Exception as e: + raise ValueError(f"Error loading DOCX file: {str(e)}") diff --git a/crewai_tools/rag/loaders/json_loader.py b/crewai_tools/rag/loaders/json_loader.py new file mode 100644 index 000000000..6efab393a --- /dev/null +++ b/crewai_tools/rag/loaders/json_loader.py @@ -0,0 +1,69 @@ +import json + +from crewai_tools.rag.source_content import SourceContent +from crewai_tools.rag.base_loader import BaseLoader, LoaderResult + + +class JSONLoader(BaseLoader): + def load(self, source_content: SourceContent, **kwargs) -> LoaderResult: + source_ref = source_content.source_ref + content = source_content.source + + if source_content.is_url(): + content = self._load_from_url(source_ref, kwargs) + elif source_content.path_exists(): + content = self._load_from_file(source_ref) + + return self._parse_json(content, source_ref) + + def _load_from_url(self, url: str, kwargs: dict) -> str: + import requests + + headers = kwargs.get("headers", { + "Accept": "application/json", + "User-Agent": "Mozilla/5.0 (compatible; crewai-tools JSONLoader)" + }) + + try: + response = requests.get(url, headers=headers, timeout=30) + response.raise_for_status() + return response.text if not self._is_json_response(response) else json.dumps(response.json(), indent=2) + except Exception as e: + raise ValueError(f"Error fetching JSON from URL {url}: {str(e)}") + + def _is_json_response(self, response) -> bool: + try: + response.json() + return True + except ValueError: + return False + + def _load_from_file(self, path: str) -> str: + with open(path, "r", encoding="utf-8") as file: + return file.read() + + def _parse_json(self, content: str, source_ref: str) -> LoaderResult: + try: + data = json.loads(content) + if isinstance(data, dict): + text = "\n".join(f"{k}: {json.dumps(v, indent=0)}" for k, v in data.items()) + elif isinstance(data, list): + text = "\n".join(json.dumps(item, indent=0) for item in data) + else: + text = json.dumps(data, indent=0) + + metadata = { + "format": "json", + "type": type(data).__name__, + "size": len(data) if isinstance(data, (list, dict)) else 1 + } + except json.JSONDecodeError as e: + text = content + metadata = {"format": "json", "parse_error": str(e)} + + return LoaderResult( + content=text, + source=source_ref, + metadata=metadata, + doc_id=self.generate_doc_id(source_ref=source_ref, content=text) + ) diff --git a/crewai_tools/rag/loaders/mdx_loader.py b/crewai_tools/rag/loaders/mdx_loader.py new file mode 100644 index 000000000..6da9dc896 --- /dev/null +++ b/crewai_tools/rag/loaders/mdx_loader.py @@ -0,0 +1,59 @@ +import re + +from crewai_tools.rag.base_loader import BaseLoader, LoaderResult +from crewai_tools.rag.source_content import SourceContent + +class MDXLoader(BaseLoader): + def load(self, source_content: SourceContent, **kwargs) -> LoaderResult: + source_ref = source_content.source_ref + content = source_content.source + + if source_content.is_url(): + content = self._load_from_url(source_ref, kwargs) + elif source_content.path_exists(): + content = self._load_from_file(source_ref) + + return self._parse_mdx(content, source_ref) + + def _load_from_url(self, url: str, kwargs: dict) -> str: + import requests + + headers = kwargs.get("headers", { + "Accept": "text/markdown, text/x-markdown, text/plain", + "User-Agent": "Mozilla/5.0 (compatible; crewai-tools MDXLoader)" + }) + + try: + response = requests.get(url, headers=headers, timeout=30) + response.raise_for_status() + return response.text + except Exception as e: + raise ValueError(f"Error fetching MDX from URL {url}: {str(e)}") + + def _load_from_file(self, path: str) -> str: + with open(path, "r", encoding="utf-8") as file: + return file.read() + + def _parse_mdx(self, content: str, source_ref: str) -> LoaderResult: + cleaned_content = content + + # Remove import statements + cleaned_content = re.sub(r'^import\s+.*?\n', '', cleaned_content, flags=re.MULTILINE) + + # Remove export statements + cleaned_content = re.sub(r'^export\s+.*?(?:\n|$)', '', cleaned_content, flags=re.MULTILINE) + + # Remove JSX tags (simple approach) + cleaned_content = re.sub(r'<[^>]+>', '', cleaned_content) + + # Clean up extra whitespace + cleaned_content = re.sub(r'\n\s*\n\s*\n', '\n\n', cleaned_content) + cleaned_content = cleaned_content.strip() + + metadata = {"format": "mdx"} + return LoaderResult( + content=cleaned_content, + source=source_ref, + metadata=metadata, + doc_id=self.generate_doc_id(source_ref=source_ref, content=cleaned_content) + ) diff --git a/crewai_tools/rag/loaders/text_loader.py b/crewai_tools/rag/loaders/text_loader.py new file mode 100644 index 000000000..a97cf29f4 --- /dev/null +++ b/crewai_tools/rag/loaders/text_loader.py @@ -0,0 +1,28 @@ + +from crewai_tools.rag.base_loader import BaseLoader, LoaderResult +from crewai_tools.rag.source_content import SourceContent + + +class TextFileLoader(BaseLoader): + def load(self, source_content: SourceContent, **kwargs) -> LoaderResult: + source_ref = source_content.source_ref + if not source_content.path_exists(): + raise FileNotFoundError(f"The following file does not exist: {source_content.source}") + + with open(source_content.source, "r", encoding="utf-8") as file: + content = file.read() + + return LoaderResult( + content=content, + source=source_ref, + doc_id=self.generate_doc_id(source_ref=source_ref, content=content) + ) + + +class TextLoader(BaseLoader): + def load(self, source_content: SourceContent, **kwargs) -> LoaderResult: + return LoaderResult( + content=source_content.source, + source=source_content.source_ref, + doc_id=self.generate_doc_id(content=source_content.source) + ) diff --git a/crewai_tools/rag/loaders/webpage_loader.py b/crewai_tools/rag/loaders/webpage_loader.py new file mode 100644 index 000000000..4fcb1e0c4 --- /dev/null +++ b/crewai_tools/rag/loaders/webpage_loader.py @@ -0,0 +1,47 @@ +import re +import requests +from bs4 import BeautifulSoup + +from crewai_tools.rag.base_loader import BaseLoader, LoaderResult +from crewai_tools.rag.source_content import SourceContent + +class WebPageLoader(BaseLoader): + def load(self, source_content: SourceContent, **kwargs) -> LoaderResult: + url = source_content.source + headers = kwargs.get("headers", { + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.110 Safari/537.36", + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9", + "Accept-Language": "en-US,en;q=0.9", + }) + + try: + response = requests.get(url, timeout=15, headers=headers) + response.encoding = response.apparent_encoding + + soup = BeautifulSoup(response.text, "html.parser") + + for script in soup(["script", "style"]): + script.decompose() + + text = soup.get_text(" ") + text = re.sub("[ \t]+", " ", text) + text = re.sub("\\s+\n\\s+", "\n", text) + text = text.strip() + + title = soup.title.string.strip() if soup.title and soup.title.string else "" + metadata = { + "url": url, + "title": title, + "status_code": response.status_code, + "content_type": response.headers.get("content-type", "") + } + + return LoaderResult( + content=text, + source=url, + metadata=metadata, + doc_id=self.generate_doc_id(source_ref=url, content=text) + ) + + except Exception as e: + raise ValueError(f"Error loading webpage {url}: {str(e)}") diff --git a/crewai_tools/rag/loaders/xml_loader.py b/crewai_tools/rag/loaders/xml_loader.py new file mode 100644 index 000000000..ffafdb9d9 --- /dev/null +++ b/crewai_tools/rag/loaders/xml_loader.py @@ -0,0 +1,61 @@ +import os +import xml.etree.ElementTree as ET + +from crewai_tools.rag.base_loader import BaseLoader, LoaderResult +from crewai_tools.rag.source_content import SourceContent + +class XMLLoader(BaseLoader): + def load(self, source_content: SourceContent, **kwargs) -> LoaderResult: + source_ref = source_content.source_ref + content = source_content.source + + if source_content.is_url(): + content = self._load_from_url(source_ref, kwargs) + elif os.path.exists(source_ref): + content = self._load_from_file(source_ref) + + return self._parse_xml(content, source_ref) + + def _load_from_url(self, url: str, kwargs: dict) -> str: + import requests + + headers = kwargs.get("headers", { + "Accept": "application/xml, text/xml, text/plain", + "User-Agent": "Mozilla/5.0 (compatible; crewai-tools XMLLoader)" + }) + + try: + response = requests.get(url, headers=headers, timeout=30) + response.raise_for_status() + return response.text + except Exception as e: + raise ValueError(f"Error fetching XML from URL {url}: {str(e)}") + + def _load_from_file(self, path: str) -> str: + with open(path, "r", encoding="utf-8") as file: + return file.read() + + def _parse_xml(self, content: str, source_ref: str) -> LoaderResult: + try: + if content.strip().startswith('<'): + root = ET.fromstring(content) + else: + root = ET.parse(source_ref).getroot() + + text_parts = [] + for text_content in root.itertext(): + if text_content and text_content.strip(): + text_parts.append(text_content.strip()) + + text = "\n".join(text_parts) + metadata = {"format": "xml", "root_tag": root.tag} + except ET.ParseError as e: + text = content + metadata = {"format": "xml", "parse_error": str(e)} + + return LoaderResult( + content=text, + source=source_ref, + metadata=metadata, + doc_id=self.generate_doc_id(source_ref=source_ref, content=text) + ) diff --git a/crewai_tools/rag/misc.py b/crewai_tools/rag/misc.py new file mode 100644 index 000000000..5b95f804e --- /dev/null +++ b/crewai_tools/rag/misc.py @@ -0,0 +1,4 @@ +import hashlib + +def compute_sha256(content: str) -> str: + return hashlib.sha256(content.encode("utf-8")).hexdigest() diff --git a/crewai_tools/rag/source_content.py b/crewai_tools/rag/source_content.py new file mode 100644 index 000000000..59530c8d8 --- /dev/null +++ b/crewai_tools/rag/source_content.py @@ -0,0 +1,46 @@ +import os +from urllib.parse import urlparse +from typing import TYPE_CHECKING +from pathlib import Path +from functools import cached_property + +from crewai_tools.rag.misc import compute_sha256 + +if TYPE_CHECKING: + from crewai_tools.rag.data_types import DataType + + +class SourceContent: + def __init__(self, source: str | Path): + self.source = str(source) + + def is_url(self) -> bool: + if not isinstance(self.source, str): + return False + try: + parsed_url = urlparse(self.source) + return bool(parsed_url.scheme and parsed_url.netloc) + except Exception: + return False + + def path_exists(self) -> bool: + return os.path.exists(self.source) + + @cached_property + def data_type(self) -> "DataType": + from crewai_tools.rag.data_types import DataTypes + + return DataTypes.from_content(self.source) + + @cached_property + def source_ref(self) -> str: + """" + Returns the source reference for the content. + If the content is a URL or a local file, returns the source. + Otherwise, returns the hash of the content. + """ + + if self.is_url() or self.path_exists(): + return self.source + + return compute_sha256(self.source) diff --git a/crewai_tools/tools/__init__.py b/crewai_tools/tools/__init__.py new file mode 100644 index 000000000..2b0bb968a --- /dev/null +++ b/crewai_tools/tools/__init__.py @@ -0,0 +1,127 @@ +from .ai_mind_tool.ai_mind_tool import AIMindTool +from .apify_actors_tool.apify_actors_tool import ApifyActorsTool +from .arxiv_paper_tool.arxiv_paper_tool import ArxivPaperTool +from .brave_search_tool.brave_search_tool import BraveSearchTool +from .brightdata_tool import ( + BrightDataDatasetTool, + BrightDataSearchTool, + BrightDataWebUnlockerTool, +) +from .browserbase_load_tool.browserbase_load_tool import BrowserbaseLoadTool +from .code_docs_search_tool.code_docs_search_tool import CodeDocsSearchTool +from .code_interpreter_tool.code_interpreter_tool import CodeInterpreterTool +from .composio_tool.composio_tool import ComposioTool +from .contextualai_create_agent_tool.contextual_create_agent_tool import ( + ContextualAICreateAgentTool, +) +from .contextualai_parse_tool.contextual_parse_tool import ContextualAIParseTool +from .contextualai_query_tool.contextual_query_tool import ContextualAIQueryTool +from .contextualai_rerank_tool.contextual_rerank_tool import ContextualAIRerankTool +from .couchbase_tool.couchbase_tool import CouchbaseFTSVectorSearchTool +from .crewai_enterprise_tools.crewai_enterprise_tools import CrewaiEnterpriseTools +from .crewai_platform_tools.crewai_platform_tools import CrewaiPlatformTools +from .csv_search_tool.csv_search_tool import CSVSearchTool +from .dalle_tool.dalle_tool import DallETool +from .databricks_query_tool.databricks_query_tool import DatabricksQueryTool +from .directory_read_tool.directory_read_tool import DirectoryReadTool +from .directory_search_tool.directory_search_tool import DirectorySearchTool +from .docx_search_tool.docx_search_tool import DOCXSearchTool +from .exa_tools.exa_search_tool import EXASearchTool +from .file_read_tool.file_read_tool import FileReadTool +from .file_writer_tool.file_writer_tool import FileWriterTool +from .files_compressor_tool.files_compressor_tool import FileCompressorTool +from .firecrawl_crawl_website_tool.firecrawl_crawl_website_tool import ( + FirecrawlCrawlWebsiteTool, +) +from .firecrawl_scrape_website_tool.firecrawl_scrape_website_tool import ( + FirecrawlScrapeWebsiteTool, +) +from .firecrawl_search_tool.firecrawl_search_tool import FirecrawlSearchTool +from .generate_crewai_automation_tool.generate_crewai_automation_tool import ( + GenerateCrewaiAutomationTool, +) +from .github_search_tool.github_search_tool import GithubSearchTool +from .hyperbrowser_load_tool.hyperbrowser_load_tool import HyperbrowserLoadTool +from .invoke_crewai_automation_tool.invoke_crewai_automation_tool import ( + InvokeCrewAIAutomationTool, +) +from .json_search_tool.json_search_tool import JSONSearchTool +from .linkup.linkup_search_tool import LinkupSearchTool +from .llamaindex_tool.llamaindex_tool import LlamaIndexTool +from .mdx_search_tool.mdx_search_tool import MDXSearchTool +from .mongodb_vector_search_tool import ( + MongoDBToolSchema, + MongoDBVectorSearchConfig, + MongoDBVectorSearchTool, +) +from .multion_tool.multion_tool import MultiOnTool +from .mysql_search_tool.mysql_search_tool import MySQLSearchTool +from .nl2sql.nl2sql_tool import NL2SQLTool +from .ocr_tool.ocr_tool import OCRTool +from .oxylabs_amazon_product_scraper_tool.oxylabs_amazon_product_scraper_tool import ( + OxylabsAmazonProductScraperTool, +) +from .oxylabs_amazon_search_scraper_tool.oxylabs_amazon_search_scraper_tool import ( + OxylabsAmazonSearchScraperTool, +) +from .oxylabs_google_search_scraper_tool.oxylabs_google_search_scraper_tool import ( + OxylabsGoogleSearchScraperTool, +) +from .oxylabs_universal_scraper_tool.oxylabs_universal_scraper_tool import ( + OxylabsUniversalScraperTool, +) +from .patronus_eval_tool import ( + PatronusEvalTool, + PatronusLocalEvaluatorTool, + PatronusPredefinedCriteriaEvalTool, +) +from .pdf_search_tool.pdf_search_tool import PDFSearchTool +from .pg_search_tool.pg_search_tool import PGSearchTool +from .qdrant_vector_search_tool.qdrant_search_tool import QdrantVectorSearchTool +from .rag.rag_tool import RagTool +from .scrape_element_from_website.scrape_element_from_website import ( + ScrapeElementFromWebsiteTool, +) +from .scrape_website_tool.scrape_website_tool import ScrapeWebsiteTool +from .scrapegraph_scrape_tool.scrapegraph_scrape_tool import ( + ScrapegraphScrapeTool, + ScrapegraphScrapeToolSchema, +) +from .scrapfly_scrape_website_tool.scrapfly_scrape_website_tool import ( + ScrapflyScrapeWebsiteTool, +) +from .selenium_scraping_tool.selenium_scraping_tool import SeleniumScrapingTool +from .serpapi_tool.serpapi_google_search_tool import SerpApiGoogleSearchTool +from .serpapi_tool.serpapi_google_shopping_tool import SerpApiGoogleShoppingTool +from .serper_dev_tool.serper_dev_tool import SerperDevTool +from .serper_scrape_website_tool.serper_scrape_website_tool import ( + SerperScrapeWebsiteTool, +) +from .serply_api_tool.serply_job_search_tool import SerplyJobSearchTool +from .serply_api_tool.serply_news_search_tool import SerplyNewsSearchTool +from .serply_api_tool.serply_scholar_search_tool import SerplyScholarSearchTool +from .serply_api_tool.serply_web_search_tool import SerplyWebSearchTool +from .serply_api_tool.serply_webpage_to_markdown_tool import SerplyWebpageToMarkdownTool +from .singlestore_search_tool import SingleStoreSearchTool +from .snowflake_search_tool import ( + SnowflakeConfig, + SnowflakeSearchTool, + SnowflakeSearchToolInput, +) +from .spider_tool.spider_tool import SpiderTool +from .stagehand_tool.stagehand_tool import StagehandTool +from .tavily_extractor_tool.tavily_extractor_tool import TavilyExtractorTool +from .tavily_search_tool.tavily_search_tool import TavilySearchTool +from .txt_search_tool.txt_search_tool import TXTSearchTool +from .vision_tool.vision_tool import VisionTool +from .weaviate_tool.vector_search import WeaviateVectorSearchTool +from .website_search.website_search_tool import WebsiteSearchTool +from .xml_search_tool.xml_search_tool import XMLSearchTool +from .youtube_channel_search_tool.youtube_channel_search_tool import ( + YoutubeChannelSearchTool, +) +from .youtube_video_search_tool.youtube_video_search_tool import YoutubeVideoSearchTool +from .zapier_action_tool.zapier_action_tool import ZapierActionTools +from .parallel_tools import ( + ParallelSearchTool, +) diff --git a/crewai_tools/tools/ai_mind_tool/README.md b/crewai_tools/tools/ai_mind_tool/README.md new file mode 100644 index 000000000..95d2deb42 --- /dev/null +++ b/crewai_tools/tools/ai_mind_tool/README.md @@ -0,0 +1,79 @@ +# AIMind Tool + +## Description + +[Minds](https://mindsdb.com/minds) are AI systems provided by [MindsDB](https://mindsdb.com/) that work similarly to large language models (LLMs) but go beyond by answering any question from any data. + +This is accomplished by selecting the most relevant data for an answer using parametric search, understanding the meaning and providing responses within the correct context through semantic search, and finally, delivering precise answers by analyzing data and using machine learning (ML) models. + +The `AIMindTool` can be used to query data sources in natural language by simply configuring their connection parameters. + +## Installation + +1. Install the `crewai[tools]` package: + +```shell +pip install 'crewai[tools]' +``` + +2. Install the Minds SDK: + +```shell +pip install minds-sdk +``` + +3. Sign for a Minds account [here](https://mdb.ai/register), and obtain an API key. + +4. Set the Minds API key in an environment variable named `MINDS_API_KEY`. + +## Usage + +```python +from crewai_tools import AIMindTool + + +# Initialize the AIMindTool. +aimind_tool = AIMindTool( + datasources=[ + { + "description": "house sales data", + "engine": "postgres", + "connection_data": { + "user": "demo_user", + "password": "demo_password", + "host": "samples.mindsdb.com", + "port": 5432, + "database": "demo", + "schema": "demo_data" + }, + "tables": ["house_sales"] + } + ] +) + +aimind_tool.run("How many 3 bedroom houses were sold in 2008?") +``` + +The `datasources` parameter is a list of dictionaries, each containing the following keys: + +- `description`: A description of the data contained in the datasource. +- `engine`: The engine (or type) of the datasource. Find a list of supported engines in the link below. +- `connection_data`: A dictionary containing the connection parameters for the datasource. Find a list of connection parameters for each engine in the link below. +- `tables`: A list of tables that the data source will use. This is optional and can be omitted if all tables in the data source are to be used. + +A list of supported data sources and their connection parameters can be found [here](https://docs.mdb.ai/docs/data_sources). + +```python +from crewai import Agent +from crewai.project import agent + + +# Define an agent with the AIMindTool. +@agent +def researcher(self) -> Agent: + return Agent( + config=self.agents_config["researcher"], + allow_delegation=False, + tools=[aimind_tool] + ) +``` diff --git a/crewai_tools/tools/ai_mind_tool/__init__.py b/crewai_tools/tools/ai_mind_tool/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/crewai_tools/tools/ai_mind_tool/ai_mind_tool.py b/crewai_tools/tools/ai_mind_tool/ai_mind_tool.py new file mode 100644 index 000000000..ea6b19281 --- /dev/null +++ b/crewai_tools/tools/ai_mind_tool/ai_mind_tool.py @@ -0,0 +1,91 @@ +import os +import secrets +from typing import Any, Dict, List, Optional, Type + +from crewai.tools import BaseTool, EnvVar +from openai import OpenAI +from pydantic import BaseModel, Field + + +class AIMindToolConstants: + MINDS_API_BASE_URL = "https://mdb.ai/" + MIND_NAME_PREFIX = "crwai_mind_" + DATASOURCE_NAME_PREFIX = "crwai_ds_" + + +class AIMindToolInputSchema(BaseModel): + """Input for AIMind Tool.""" + + query: str = Field(description="Question in natural language to ask the AI-Mind") + + +class AIMindTool(BaseTool): + name: str = "AIMind Tool" + description: str = ( + "A wrapper around [AI-Minds](https://mindsdb.com/minds). " + "Useful for when you need answers to questions from your data, stored in " + "data sources including PostgreSQL, MySQL, MariaDB, ClickHouse, Snowflake " + "and Google BigQuery. " + "Input should be a question in natural language." + ) + args_schema: Type[BaseModel] = AIMindToolInputSchema + api_key: Optional[str] = None + datasources: Optional[List[Dict[str, Any]]] = None + mind_name: Optional[str] = None + package_dependencies: List[str] = ["minds-sdk"] + env_vars: List[EnvVar] = [ + EnvVar(name="MINDS_API_KEY", description="API key for AI-Minds", required=True), + ] + + def __init__(self, api_key: Optional[str] = None, **kwargs): + super().__init__(**kwargs) + self.api_key = api_key or os.getenv("MINDS_API_KEY") + if not self.api_key: + raise ValueError("API key must be provided either through constructor or MINDS_API_KEY environment variable") + + try: + from minds.client import Client # type: ignore + from minds.datasources import DatabaseConfig # type: ignore + except ImportError: + raise ImportError( + "`minds_sdk` package not found, please run `pip install minds-sdk`" + ) + + minds_client = Client(api_key=self.api_key) + + # Convert the datasources to DatabaseConfig objects. + datasources = [] + for datasource in self.datasources: + config = DatabaseConfig( + name=f"{AIMindToolConstants.DATASOURCE_NAME_PREFIX}_{secrets.token_hex(5)}", + engine=datasource["engine"], + description=datasource["description"], + connection_data=datasource["connection_data"], + tables=datasource["tables"], + ) + datasources.append(config) + + # Generate a random name for the Mind. + name = f"{AIMindToolConstants.MIND_NAME_PREFIX}_{secrets.token_hex(5)}" + + mind = minds_client.minds.create( + name=name, datasources=datasources, replace=True + ) + + self.mind_name = mind.name + + def _run( + self, + query: str + ): + # Run the query on the AI-Mind. + # The Minds API is OpenAI compatible and therefore, the OpenAI client can be used. + openai_client = OpenAI(base_url=AIMindToolConstants.MINDS_API_BASE_URL, api_key=self.api_key) + + completion = openai_client.chat.completions.create( + model=self.mind_name, + messages=[{"role": "user", "content": query}], + stream=False, + ) + + return completion.choices[0].message.content diff --git a/crewai_tools/tools/apify_actors_tool/README.md b/crewai_tools/tools/apify_actors_tool/README.md new file mode 100644 index 000000000..c00891deb --- /dev/null +++ b/crewai_tools/tools/apify_actors_tool/README.md @@ -0,0 +1,96 @@ +# ApifyActorsTool + +Integrate [Apify Actors](https://apify.com/actors) into your CrewAI workflows. + +## Description + +The `ApifyActorsTool` connects [Apify Actors](https://apify.com/actors), cloud-based programs for web scraping and automation, to your CrewAI workflows. +Use any of the 4,000+ Actors on [Apify Store](https://apify.com/store) for use cases such as extracting data from social media, search engines, online maps, e-commerce sites, travel portals, or general websites. + +For details, see the [Apify CrewAI integration](https://docs.apify.com/platform/integrations/crewai) in Apify documentation. + +## Installation + +To use `ApifyActorsTool`, install the necessary packages and set up your Apify API token. Follow the [Apify API documentation](https://docs.apify.com/platform/integrations/api) for steps to obtain the token. + +### Steps + +1. **Install dependencies** + Install `crewai[tools]` and `langchain-apify`: + ```bash + pip install 'crewai[tools]' langchain-apify + ``` + +2. **Set your API token** + Export the token as an environment variable: + ```bash + export APIFY_API_TOKEN='your-api-token-here' + ``` + +## Usage example + +Use the `ApifyActorsTool` manually to run the [RAG Web Browser Actor](https://apify.com/apify/rag-web-browser) to perform a web search: + +```python +from crewai_tools import ApifyActorsTool + +# Initialize the tool with an Apify Actor +tool = ApifyActorsTool(actor_name="apify/rag-web-browser") + +# Run the tool with input parameters +results = tool.run(run_input={"query": "What is CrewAI?", "maxResults": 5}) + +# Process the results +for result in results: + print(f"URL: {result['metadata']['url']}") + print(f"Content: {result.get('markdown', 'N/A')[:100]}...") +``` + +### Expected output + +Here is the output from running the code above: + +```text +URL: https://www.example.com/crewai-intro +Content: CrewAI is a framework for building AI-powered workflows... +URL: https://docs.crewai.com/ +Content: Official documentation for CrewAI... +``` + +The `ApifyActorsTool` automatically fetches the Actor definition and input schema from Apify using the provided `actor_name` and then constructs the tool description and argument schema. This means you need to specify only a valid `actor_name`, and the tool handles the rest when used with agents—no need to specify the `run_input`. Here's how it works: + +```python +from crewai import Agent +from crewai_tools import ApifyActorsTool + +rag_browser = ApifyActorsTool(actor_name="apify/rag-web-browser") + +agent = Agent( + role="Research Analyst", + goal="Find and summarize information about specific topics", + backstory="You are an experienced researcher with attention to detail", + tools=[rag_browser], +) +``` + +You can run other Actors from [Apify Store](https://apify.com/store) simply by changing the `actor_name` and, when using it manually, adjusting the `run_input` based on the Actor input schema. + +For an example of usage with agents, see the [CrewAI Actor template](https://apify.com/templates/python-crewai). + +## Configuration + +The `ApifyActorsTool` requires these inputs to work: + +- **`actor_name`** + The ID of the Apify Actor to run, e.g., `"apify/rag-web-browser"`. Browse all Actors on [Apify Store](https://apify.com/store). +- **`run_input`** + A dictionary of input parameters for the Actor when running the tool manually. + - For example, for the `apify/rag-web-browser` Actor: `{"query": "search term", "maxResults": 5}` + - See the Actor's [input schema](https://apify.com/apify/rag-web-browser/input-schema) for the list of input parameters. + +## Resources + +- **[Apify](https://apify.com/)**: Explore the Apify platform. +- **[How to build an AI agent on Apify](https://blog.apify.com/how-to-build-an-ai-agent/)** - A complete step-by-step guide to creating, publishing, and monetizing AI agents on the Apify platform. +- **[RAG Web Browser Actor](https://apify.com/apify/rag-web-browser)**: A popular Actor for web search for LLMs. +- **[CrewAI Integration Guide](https://docs.apify.com/platform/integrations/crewai)**: Follow the official guide for integrating Apify and CrewAI. diff --git a/crewai_tools/tools/apify_actors_tool/apify_actors_tool.py b/crewai_tools/tools/apify_actors_tool/apify_actors_tool.py new file mode 100644 index 000000000..127169676 --- /dev/null +++ b/crewai_tools/tools/apify_actors_tool/apify_actors_tool.py @@ -0,0 +1,96 @@ +from crewai.tools import BaseTool, EnvVar +from pydantic import Field +from typing import TYPE_CHECKING, Any, Dict, List +import os + +if TYPE_CHECKING: + from langchain_apify import ApifyActorsTool as _ApifyActorsTool + +class ApifyActorsTool(BaseTool): + env_vars: List[EnvVar] = [ + EnvVar(name="APIFY_API_TOKEN", description="API token for Apify platform access", required=True), + ] + """Tool that runs Apify Actors. + + To use, you should have the environment variable `APIFY_API_TOKEN` set + with your API key. + + For details, see https://docs.apify.com/platform/integrations/crewai + + Args: + actor_name (str): The name of the Apify Actor to run. + *args: Variable length argument list passed to BaseTool. + **kwargs: Arbitrary keyword arguments passed to BaseTool. + + Returns: + List[Dict[str, Any]]: Results from the Actor execution. + + Raises: + ValueError: If `APIFY_API_TOKEN` is not set or if the tool is not initialized. + ImportError: If `langchain_apify` package is not installed. + + Example: + .. code-block:: python + from crewai_tools import ApifyActorsTool + + tool = ApifyActorsTool(actor_name="apify/rag-web-browser") + + results = tool.run(run_input={"query": "What is CrewAI?", "maxResults": 5}) + for result in results: + print(f"URL: {result['metadata']['url']}") + print(f"Content: {result.get('markdown', 'N/A')[:100]}...") + """ + actor_tool: '_ApifyActorsTool' = Field(description="Apify Actor Tool") + package_dependencies: List[str] = ["langchain-apify"] + + def __init__( + self, + actor_name: str, + *args: Any, + **kwargs: Any + ) -> None: + if not os.environ.get("APIFY_API_TOKEN"): + msg = ( + "APIFY_API_TOKEN environment variable is not set. " + "Please set it to your API key, to learn how to get it, " + "see https://docs.apify.com/platform/integrations/api" + ) + raise ValueError(msg) + + try: + from langchain_apify import ApifyActorsTool as _ApifyActorsTool + except ImportError: + raise ImportError( + "Could not import langchain_apify python package. " + "Please install it with `pip install langchain-apify` or `uv add langchain-apify`." + ) + actor_tool = _ApifyActorsTool(actor_name) + + kwargs.update( + { + "name": actor_tool.name, + "description": actor_tool.description, + "args_schema": actor_tool.args_schema, + "actor_tool": actor_tool, + } + ) + super().__init__(*args, **kwargs) + + def _run(self, run_input: Dict[str, Any]) -> List[Dict[str, Any]]: + """Run the Actor tool with the given input. + + Returns: + List[Dict[str, Any]]: Results from the Actor execution. + + Raises: + ValueError: If 'actor_tool' is not initialized. + """ + try: + return self.actor_tool._run(run_input) + except Exception as e: + msg = ( + f'Failed to run ApifyActorsTool {self.name}. ' + 'Please check your Apify account Actor run logs for more details.' + f'Error: {e}' + ) + raise RuntimeError(msg) from e diff --git a/crewai_tools/tools/arxiv_paper_tool/Examples.md b/crewai_tools/tools/arxiv_paper_tool/Examples.md new file mode 100644 index 000000000..676fa4106 --- /dev/null +++ b/crewai_tools/tools/arxiv_paper_tool/Examples.md @@ -0,0 +1,80 @@ +### Example 1: Fetching Research Papers from arXiv with CrewAI + +This example demonstrates how to build a simple CrewAI workflow that automatically searches for and downloads academic papers from [arXiv.org](https://arxiv.org). The setup uses: + +* A custom `ArxivPaperTool` to fetch metadata and download PDFs +* A single `Agent` tasked with locating relevant papers based on a given research topic +* A `Task` to define the data retrieval and download process +* A sequential `Crew` to orchestrate execution + +The downloaded PDFs are saved to a local directory (`./DOWNLOADS`). Filenames are optionally based on sanitized paper titles, ensuring compatibility with your operating system. + +> The saved PDFs can be further used in **downstream tasks**, such as: +> +> * **RAG (Retrieval-Augmented Generation)** +> * **Summarization** +> * **Citation extraction** +> * **Embedding-based search or analysis** + +--- + + +``` +from crewai import Agent, Task, Crew, Process, LLM +from crewai_tools import ArxivPaperTool + + + +llm = LLM( + model="ollama/llama3.1", + base_url="http://localhost:11434", + temperature=0.1 +) + + +topic = "Crew AI" +max_results = 3 +save_dir = "./DOWNLOADS" +use_title_as_filename = True + +tool = ArxivPaperTool( + download_pdfs=True, + save_dir=save_dir, + use_title_as_filename=True +) +tool.result_as_answer = True #Required,otherwise + + +arxiv_paper_fetch = Agent( + role="Arxiv Data Fetcher", + goal=f"Retrieve relevant papers from arXiv based on a research topic {topic} and maximum number of papers to be downloaded is{max_results},try to use title as filename {use_title_as_filename} and download PDFs to {save_dir},", + backstory="An expert in scientific data retrieval, skilled in extracting academic content from arXiv.", + # tools=[ArxivPaperTool()], + llm=llm, + verbose=True, + allow_delegation=False +) +fetch_task = Task( + description=( + f"Search arXiv for the topic '{topic}' and fetch up to {max_results} papers. " + f"Download PDFs for analysis and store them at {save_dir}." + ), + expected_output="PDFs saved to disk for downstream agents.", + agent=arxiv_paper_fetch, + tools=[tool], # Use the actual tool instance here + +) + + +pdf_qa_crew = Crew( + agents=[arxiv_paper_fetch], + tasks=[fetch_task], + process=Process.sequential, + verbose=True, +) + + +result = pdf_qa_crew.kickoff() + +print(f"\n🤖 Answer:\n\n{result.raw}\n") +``` diff --git a/crewai_tools/tools/arxiv_paper_tool/README.md b/crewai_tools/tools/arxiv_paper_tool/README.md new file mode 100644 index 000000000..f9ef56bdc --- /dev/null +++ b/crewai_tools/tools/arxiv_paper_tool/README.md @@ -0,0 +1,142 @@ +# ArxivPaperTool + + +# 📚 ArxivPaperTool + +The **ArxivPaperTool** is a utility for fetching metadata and optionally downloading PDFs of academic papers from the [arXiv](https://arxiv.org) platform using its public API. It supports configurable queries, batch retrieval, PDF downloading, and clean formatting for summaries and metadata. This tool is particularly useful for researchers, students, academic agents, and AI tools performing automated literature reviews. + +--- + +## Description + +This tool: + +* Accepts a **search query** and retrieves a list of papers from arXiv. +* Allows configuration of the **maximum number of results** to fetch. +* Optionally downloads the **PDFs** of the matched papers. +* Lets you specify whether to name PDF files using the **arXiv ID** or **paper title**. +* Saves downloaded files into a **custom or default directory**. +* Returns structured summaries of all fetched papers including metadata. + +--- + +## Arguments + +| Argument | Type | Required | Description | +| ----------------------- | ------ | -------- | --------------------------------------------------------------------------------- | +| `search_query` | `str` | ✅ | Search query string (e.g., `"transformer neural network"`). | +| `max_results` | `int` | ✅ | Number of results to fetch (between 1 and 100). | +| `download_pdfs` | `bool` | ❌ | Whether to download the corresponding PDFs. Defaults to `False`. | +| `save_dir` | `str` | ❌ | Directory to save PDFs (created if it doesn’t exist). Defaults to `./arxiv_pdfs`. | +| `use_title_as_filename` | `bool` | ❌ | Use the paper title as the filename (sanitized). Defaults to `False`. | + +--- + +## 📄 `ArxivPaperTool` Usage Examples + +This document shows how to use the `ArxivPaperTool` to fetch research paper metadata from arXiv and optionally download PDFs. + +### 🔧 Tool Initialization + +```python +from crewai_tools import ArxivPaperTool +``` + +--- + +### Example 1: Fetch Metadata Only (No Downloads) + +```python +tool = ArxivPaperTool() +result = tool._run( + search_query="deep learning", + max_results=1 +) +print(result) +``` + +--- + +### Example 2: Fetch and Download PDFs (arXiv ID as Filename) + +```python +tool = ArxivPaperTool(download_pdfs=True) +result = tool._run( + search_query="transformer models", + max_results=2 +) +print(result) +``` + +--- + +### Example 3: Download PDFs into a Custom Directory + +```python +tool = ArxivPaperTool( + download_pdfs=True, + save_dir="./my_papers" +) +result = tool._run( + search_query="graph neural networks", + max_results=2 +) +print(result) +``` + +--- + +### Example 4: Use Paper Titles as Filenames + +```python +tool = ArxivPaperTool( + download_pdfs=True, + use_title_as_filename=True +) +result = tool._run( + search_query="vision transformers", + max_results=1 +) +print(result) +``` + +--- + +### Example 5: All Options Combined + +```python +tool = ArxivPaperTool( + download_pdfs=True, + save_dir="./downloads", + use_title_as_filename=True +) +result = tool._run( + search_query="stable diffusion", + max_results=3 +) +print(result) +``` + +--- + +### Run via `__main__` + +Your file can also include: + +```python +if __name__ == "__main__": + tool = ArxivPaperTool( + download_pdfs=True, + save_dir="./downloads2", + use_title_as_filename=False + ) + result = tool._run( + search_query="deep learning", + max_results=1 + ) + print(result) +``` + +--- + + diff --git a/crewai_tools/tools/arxiv_paper_tool/arxiv_paper_tool.py b/crewai_tools/tools/arxiv_paper_tool/arxiv_paper_tool.py new file mode 100644 index 000000000..acd6bbe77 --- /dev/null +++ b/crewai_tools/tools/arxiv_paper_tool/arxiv_paper_tool.py @@ -0,0 +1,152 @@ +import re +import time +import urllib.request +import urllib.parse +import urllib.error +import xml.etree.ElementTree as ET +from typing import Type, List, Optional, ClassVar +from pydantic import BaseModel, Field +from crewai.tools import BaseTool,EnvVar +import logging +from pathlib import Path + +logger = logging.getLogger(__file__) + +class ArxivToolInput(BaseModel): + search_query: str = Field(..., description="Search query for Arxiv, e.g., 'transformer neural network'") + max_results: int = Field(5, ge=1, le=100, description="Max results to fetch; must be between 1 and 100") + +class ArxivPaperTool(BaseTool): + BASE_API_URL: ClassVar[str] = "http://export.arxiv.org/api/query" + SLEEP_DURATION: ClassVar[int] = 1 + SUMMARY_TRUNCATE_LENGTH: ClassVar[int] = 300 + ATOM_NAMESPACE: ClassVar[str] = "{http://www.w3.org/2005/Atom}" + REQUEST_TIMEOUT: ClassVar[int] = 10 + name: str = "Arxiv Paper Fetcher and Downloader" + description: str = "Fetches metadata from Arxiv based on a search query and optionally downloads PDFs." + args_schema: Type[BaseModel] = ArxivToolInput + model_config = {"extra": "allow"} + package_dependencies: List[str] = ["pydantic"] + env_vars: List[EnvVar] = [] + + def __init__(self, download_pdfs=False, save_dir="./arxiv_pdfs", use_title_as_filename=False): + super().__init__() + self.download_pdfs = download_pdfs + self.save_dir = save_dir + self.use_title_as_filename = use_title_as_filename + + def _run(self, search_query: str, max_results: int = 5) -> str: + try: + args = ArxivToolInput(search_query=search_query, max_results=max_results) + logger.info(f"Running Arxiv tool: query='{args.search_query}', max_results={args.max_results}, " + f"download_pdfs={self.download_pdfs}, save_dir='{self.save_dir}', " + f"use_title_as_filename={self.use_title_as_filename}") + + papers = self.fetch_arxiv_data(args.search_query, args.max_results) + + if self.download_pdfs: + save_dir = self._validate_save_path(self.save_dir) + for paper in papers: + if paper['pdf_url']: + if self.use_title_as_filename: + safe_title = re.sub(r'[\\/*?:"<>|]', "_", paper['title']).strip() + filename_base = safe_title or paper['arxiv_id'] + else: + filename_base = paper['arxiv_id'] + filename = f"{filename_base[:500]}.pdf" + save_path = Path(save_dir) / filename + + self.download_pdf(paper['pdf_url'], save_path) + time.sleep(self.SLEEP_DURATION) + + results = [self._format_paper_result(p) for p in papers] + return "\n\n" + "-" * 80 + "\n\n".join(results) + + except Exception as e: + logger.error(f"ArxivTool Error: {str(e)}") + return f"Failed to fetch or download Arxiv papers: {str(e)}" + + + def fetch_arxiv_data(self, search_query: str, max_results: int) -> List[dict]: + api_url = f"{self.BASE_API_URL}?search_query={urllib.parse.quote(search_query)}&start=0&max_results={max_results}" + logger.info(f"Fetching data from Arxiv API: {api_url}") + + try: + with urllib.request.urlopen(api_url, timeout=self.REQUEST_TIMEOUT) as response: + if response.status != 200: + raise Exception(f"HTTP {response.status}: {response.reason}") + data = response.read().decode('utf-8') + except urllib.error.URLError as e: + logger.error(f"Error fetching data from Arxiv: {e}") + raise + + root = ET.fromstring(data) + papers = [] + + for entry in root.findall(self.ATOM_NAMESPACE + "entry"): + raw_id = self._get_element_text(entry, "id") + arxiv_id = raw_id.split('/')[-1].replace('.', '_') if raw_id else "unknown" + + title = self._get_element_text(entry, "title") or "No Title" + summary = self._get_element_text(entry, "summary") or "No Summary" + published = self._get_element_text(entry, "published") or "No Publish Date" + authors = [ + self._get_element_text(author, "name") or "Unknown" + for author in entry.findall(self.ATOM_NAMESPACE + "author") + ] + + pdf_url = self._extract_pdf_url(entry) + + papers.append({ + "arxiv_id": arxiv_id, + "title": title, + "summary": summary, + "authors": authors, + "published_date": published, + "pdf_url": pdf_url + }) + + return papers + + @staticmethod + def _get_element_text(entry: ET.Element, element_name: str) -> Optional[str]: + elem = entry.find(f'{ArxivPaperTool.ATOM_NAMESPACE}{element_name}') + return elem.text.strip() if elem is not None and elem.text else None + + def _extract_pdf_url(self, entry: ET.Element) -> Optional[str]: + for link in entry.findall(self.ATOM_NAMESPACE + "link"): + if link.attrib.get('title', '').lower() == 'pdf': + return link.attrib.get('href') + for link in entry.findall(self.ATOM_NAMESPACE + "link"): + href = link.attrib.get('href') + if href and 'pdf' in href: + return href + return None + + def _format_paper_result(self, paper: dict) -> str: + summary = (paper['summary'][:self.SUMMARY_TRUNCATE_LENGTH] + '...') \ + if len(paper['summary']) > self.SUMMARY_TRUNCATE_LENGTH else paper['summary'] + authors_str = ', '.join(paper['authors']) + return (f"Title: {paper['title']}\n" + f"Authors: {authors_str}\n" + f"Published: {paper['published_date']}\n" + f"PDF: {paper['pdf_url'] or 'N/A'}\n" + f"Summary: {summary}") + + @staticmethod + def _validate_save_path(path: str) -> Path: + save_path = Path(path).resolve() + save_path.mkdir(parents=True, exist_ok=True) + return save_path + + def download_pdf(self, pdf_url: str, save_path: str): + try: + logger.info(f"Downloading PDF from {pdf_url} to {save_path}") + urllib.request.urlretrieve(pdf_url, str(save_path)) + logger.info(f"PDF saved: {save_path}") + except urllib.error.URLError as e: + logger.error(f"Network error occurred while downloading {pdf_url}: {e}") + raise + except OSError as e: + logger.error(f"File save error for {save_path}: {e}") + raise diff --git a/crewai_tools/tools/arxiv_paper_tool/arxiv_paper_tool_test.py b/crewai_tools/tools/arxiv_paper_tool/arxiv_paper_tool_test.py new file mode 100644 index 000000000..4f8747d2f --- /dev/null +++ b/crewai_tools/tools/arxiv_paper_tool/arxiv_paper_tool_test.py @@ -0,0 +1,113 @@ +import pytest +import urllib.error +from unittest.mock import patch, MagicMock, mock_open +from pathlib import Path +import xml.etree.ElementTree as ET +from crewai_tools import ArxivPaperTool + +@pytest.fixture +def tool(): + return ArxivPaperTool(download_pdfs=False) + +def mock_arxiv_response(): + return ''' + + + http://arxiv.org/abs/1234.5678 + Sample Paper + This is a summary of the sample paper. + 2022-01-01T00:00:00Z + John Doe + + + ''' + +@patch("urllib.request.urlopen") +def test_fetch_arxiv_data(mock_urlopen, tool): + mock_response = MagicMock() + mock_response.status = 200 + mock_response.read.return_value = mock_arxiv_response().encode("utf-8") + mock_urlopen.return_value.__enter__.return_value = mock_response + + results = tool.fetch_arxiv_data("transformer", 1) + assert isinstance(results, list) + assert results[0]['title'] == "Sample Paper" + +@patch("urllib.request.urlopen", side_effect=urllib.error.URLError("Timeout")) +def test_fetch_arxiv_data_network_error(mock_urlopen, tool): + with pytest.raises(urllib.error.URLError): + tool.fetch_arxiv_data("transformer", 1) + +@patch("urllib.request.urlretrieve") +def test_download_pdf_success(mock_urlretrieve): + tool = ArxivPaperTool() + tool.download_pdf("http://arxiv.org/pdf/1234.5678.pdf", Path("test.pdf")) + mock_urlretrieve.assert_called_once() + +@patch("urllib.request.urlretrieve", side_effect=OSError("Permission denied")) +def test_download_pdf_oserror(mock_urlretrieve): + tool = ArxivPaperTool() + with pytest.raises(OSError): + tool.download_pdf("http://arxiv.org/pdf/1234.5678.pdf", Path("/restricted/test.pdf")) + +@patch("urllib.request.urlopen") +@patch("urllib.request.urlretrieve") +def test_run_with_download(mock_urlretrieve, mock_urlopen): + mock_response = MagicMock() + mock_response.status = 200 + mock_response.read.return_value = mock_arxiv_response().encode("utf-8") + mock_urlopen.return_value.__enter__.return_value = mock_response + + tool = ArxivPaperTool(download_pdfs=True) + output = tool._run("transformer", 1) + assert "Title: Sample Paper" in output + mock_urlretrieve.assert_called_once() + +@patch("urllib.request.urlopen") +def test_run_no_download(mock_urlopen): + mock_response = MagicMock() + mock_response.status = 200 + mock_response.read.return_value = mock_arxiv_response().encode("utf-8") + mock_urlopen.return_value.__enter__.return_value = mock_response + + tool = ArxivPaperTool(download_pdfs=False) + result = tool._run("transformer", 1) + assert "Title: Sample Paper" in result + +@patch("pathlib.Path.mkdir") +def test_validate_save_path_creates_directory(mock_mkdir): + path = ArxivPaperTool._validate_save_path("new_folder") + mock_mkdir.assert_called_once_with(parents=True, exist_ok=True) + assert isinstance(path, Path) + +@patch("urllib.request.urlopen") +def test_run_handles_exception(mock_urlopen): + mock_urlopen.side_effect = Exception("API failure") + tool = ArxivPaperTool() + result = tool._run("transformer", 1) + assert "Failed to fetch or download Arxiv papers" in result + + +@patch("urllib.request.urlopen") +def test_invalid_xml_response(mock_urlopen, tool): + mock_response = MagicMock() + mock_response.read.return_value = b"" + mock_response.status = 200 + mock_urlopen.return_value.__enter__.return_value = mock_response + + with pytest.raises(ET.ParseError): + tool.fetch_arxiv_data("quantum", 1) + +@patch.object(ArxivPaperTool, "fetch_arxiv_data") +def test_run_with_max_results(mock_fetch, tool): + mock_fetch.return_value = [{ + "arxiv_id": f"test_{i}", + "title": f"Title {i}", + "summary": "Summary", + "authors": ["Author"], + "published_date": "2023-01-01", + "pdf_url": None + } for i in range(100)] + + result = tool._run(search_query="test", max_results=100) + assert result.count("Title:") == 100 diff --git a/crewai_tools/tools/brave_search_tool/README.md b/crewai_tools/tools/brave_search_tool/README.md new file mode 100644 index 000000000..a66210491 --- /dev/null +++ b/crewai_tools/tools/brave_search_tool/README.md @@ -0,0 +1,30 @@ +# BraveSearchTool Documentation + +## Description +This tool is designed to perform a web search for a specified query from a text's content across the internet. It utilizes the Brave Web Search API, which is a REST API to query Brave Search and get back search results from the web. The following sections describe how to curate requests, including parameters and headers, to Brave Web Search API and get a JSON response back. + +## Installation +To incorporate this tool into your project, follow the installation instructions below: +```shell +pip install 'crewai[tools]' +``` + +## Example +The following example demonstrates how to initialize the tool and execute a search with a given query: + +```python +from crewai_tools import BraveSearchTool + +# Initialize the tool for internet searching capabilities +tool = BraveSearchTool() +``` + +## Steps to Get Started +To effectively use the `BraveSearchTool`, follow these steps: + +1. **Package Installation**: Confirm that the `crewai[tools]` package is installed in your Python environment. +2. **API Key Acquisition**: Acquire a API key [here](https://api.search.brave.com/app/keys). +3. **Environment Configuration**: Store your obtained API key in an environment variable named `BRAVE_API_KEY` to facilitate its use by the tool. + +## Conclusion +By integrating the `BraveSearchTool` into Python projects, users gain the ability to conduct real-time, relevant searches across the internet directly from their applications. By adhering to the setup and usage guidelines provided, incorporating this tool into projects is streamlined and straightforward. diff --git a/crewai_tools/tools/brave_search_tool/__init__.py b/crewai_tools/tools/brave_search_tool/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/crewai_tools/tools/brave_search_tool/brave_search_tool.py b/crewai_tools/tools/brave_search_tool/brave_search_tool.py new file mode 100644 index 000000000..1f96d452a --- /dev/null +++ b/crewai_tools/tools/brave_search_tool/brave_search_tool.py @@ -0,0 +1,121 @@ +import datetime +import os +import time +from typing import Any, ClassVar, List, Optional, Type + +import requests +from crewai.tools import BaseTool, EnvVar +from pydantic import BaseModel, Field + + +def _save_results_to_file(content: str) -> None: + """Saves the search results to a file.""" + filename = f"search_results_{datetime.now().strftime('%Y-%m-%d_%H-%M-%S')}.txt" + with open(filename, "w") as file: + file.write(content) + print(f"Results saved to {filename}") + + +class BraveSearchToolSchema(BaseModel): + """Input for BraveSearchTool.""" + + search_query: str = Field( + ..., description="Mandatory search query you want to use to search the internet" + ) + + +class BraveSearchTool(BaseTool): + """ + BraveSearchTool - A tool for performing web searches using the Brave Search API. + + This module provides functionality to search the internet using Brave's Search API, + supporting customizable result counts and country-specific searches. + + Dependencies: + - requests + - pydantic + - python-dotenv (for API key management) + """ + + name: str = "Brave Web Search the internet" + description: str = ( + "A tool that can be used to search the internet with a search_query." + ) + args_schema: Type[BaseModel] = BraveSearchToolSchema + search_url: str = "https://api.search.brave.com/res/v1/web/search" + country: Optional[str] = "" + n_results: int = 10 + save_file: bool = False + _last_request_time: ClassVar[float] = 0 + _min_request_interval: ClassVar[float] = 1.0 # seconds + env_vars: List[EnvVar] = [ + EnvVar(name="BRAVE_API_KEY", description="API key for Brave Search", required=True), + ] + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + if "BRAVE_API_KEY" not in os.environ: + raise ValueError( + "BRAVE_API_KEY environment variable is required for BraveSearchTool" + ) + + def _run( + self, + **kwargs: Any, + ) -> Any: + current_time = time.time() + if (current_time - self._last_request_time) < self._min_request_interval: + time.sleep( + self._min_request_interval - (current_time - self._last_request_time) + ) + BraveSearchTool._last_request_time = time.time() + try: + search_query = kwargs.get("search_query") or kwargs.get("query") + if not search_query: + raise ValueError("Search query is required") + + save_file = kwargs.get("save_file", self.save_file) + n_results = kwargs.get("n_results", self.n_results) + + payload = {"q": search_query, "count": n_results} + + if self.country != "": + payload["country"] = self.country + + headers = { + "X-Subscription-Token": os.environ["BRAVE_API_KEY"], + "Accept": "application/json", + } + + response = requests.get(self.search_url, headers=headers, params=payload) + response.raise_for_status() # Handle non-200 responses + results = response.json() + + if "web" in results: + results = results["web"]["results"] + string = [] + for result in results: + try: + string.append( + "\n".join( + [ + f"Title: {result['title']}", + f"Link: {result['url']}", + f"Snippet: {result['description']}", + "---", + ] + ) + ) + except KeyError: + continue + + content = "\n".join(string) + except requests.RequestException as e: + return f"Error performing search: {str(e)}" + except KeyError as e: + return f"Error parsing search results: {str(e)}" + if save_file: + _save_results_to_file(content) + return f"\nSearch results: {content}\n" + else: + return content diff --git a/crewai_tools/tools/brightdata_tool/README.md b/crewai_tools/tools/brightdata_tool/README.md new file mode 100644 index 000000000..f16b5ac73 --- /dev/null +++ b/crewai_tools/tools/brightdata_tool/README.md @@ -0,0 +1,79 @@ +# BrightData Tools Documentation + +## Description + +A comprehensive suite of CrewAI tools that leverage Bright Data's powerful infrastructure for web scraping, data extraction, and search operations. These tools provide three distinct capabilities: + +- **BrightDataDatasetTool**: Extract structured data from popular data feeds (Amazon, LinkedIn, Instagram, etc.) using pre-built datasets +- **BrightDataSearchTool**: Perform web searches across multiple search engines with geo-targeting and device simulation +- **BrightDataWebUnlockerTool**: Scrape any website content while bypassing bot protection mechanisms + +## Installation + +To incorporate these tools into your project, follow the installation instructions below: + +```shell +pip install crewai[tools] aiohttp requests +``` + +## Examples + +### Dataset Tool - Extract Amazon Product Data +```python +from crewai_tools import BrightDataDatasetTool + +# Initialize with specific dataset and URL +tool = BrightDataDatasetTool( + dataset_type="amazon_product", + url="https://www.amazon.com/dp/B08QB1QMJ5/" +) +result = tool.run() +``` + +### Search Tool - Perform Web Search +```python +from crewai_tools import BrightDataSearchTool + +# Initialize with search query +tool = BrightDataSearchTool( + query="latest AI trends 2025", + search_engine="google", + country="us" +) +result = tool.run() +``` + +### Web Unlocker Tool - Scrape Website Content +```python +from crewai_tools import BrightDataWebUnlockerTool + +# Initialize with target URL +tool = BrightDataWebUnlockerTool( + url="https://example.com", + data_format="markdown" +) +result = tool.run() +``` + +## Steps to Get Started + +To effectively use the BrightData Tools, follow these steps: + +1. **Package Installation**: Confirm that the `crewai[tools]` package is installed in your Python environment. + +2. **API Key Acquisition**: Register for a Bright Data account at `https://brightdata.com/` and obtain your API credentials from your account settings. + +3. **Environment Configuration**: Set up the required environment variables: + ```bash + export BRIGHT_DATA_API_KEY="your_api_key_here" + export BRIGHT_DATA_ZONE="your_zone_here" + ``` + +4. **Tool Selection**: Choose the appropriate tool based on your needs: + - Use **DatasetTool** for structured data from supported platforms + - Use **SearchTool** for web search operations + - Use **WebUnlockerTool** for general website scraping + +## Conclusion + +By integrating BrightData Tools into your CrewAI agents, you gain access to enterprise-grade web scraping and data extraction capabilities. These tools handle complex challenges like bot protection, geo-restrictions, and data parsing, allowing you to focus on building your applications rather than managing scraping infrastructure. \ No newline at end of file diff --git a/crewai_tools/tools/brightdata_tool/__init__.py b/crewai_tools/tools/brightdata_tool/__init__.py new file mode 100644 index 000000000..0842e97ea --- /dev/null +++ b/crewai_tools/tools/brightdata_tool/__init__.py @@ -0,0 +1,9 @@ +from .brightdata_dataset import BrightDataDatasetTool +from .brightdata_serp import BrightDataSearchTool +from .brightdata_unlocker import BrightDataWebUnlockerTool + +__all__ = [ + "BrightDataDatasetTool", + "BrightDataSearchTool", + "BrightDataWebUnlockerTool" +] \ No newline at end of file diff --git a/crewai_tools/tools/brightdata_tool/brightdata_dataset.py b/crewai_tools/tools/brightdata_tool/brightdata_dataset.py new file mode 100644 index 000000000..88ca65077 --- /dev/null +++ b/crewai_tools/tools/brightdata_tool/brightdata_dataset.py @@ -0,0 +1,570 @@ +import asyncio +import os +from typing import Any, Dict, Optional, Type + +import aiohttp +from crewai.tools import BaseTool +from pydantic import BaseModel, Field + +class BrightDataConfig(BaseModel): + API_URL: str = "https://api.brightdata.com" + DEFAULT_TIMEOUT: int = 600 + DEFAULT_POLLING_INTERVAL: int = 1 + + @classmethod + def from_env(cls): + return cls( + API_URL=os.environ.get("BRIGHTDATA_API_URL", "https://api.brightdata.com"), + DEFAULT_TIMEOUT=int(os.environ.get("BRIGHTDATA_DEFAULT_TIMEOUT", "600")), + DEFAULT_POLLING_INTERVAL=int(os.environ.get("BRIGHTDATA_DEFAULT_POLLING_INTERVAL", "1")) + ) +class BrightDataDatasetToolException(Exception): + """Exception raised for custom error in the application.""" + + def __init__(self, message, error_code): + self.message = message + super().__init__(message) + self.error_code = error_code + + def __str__(self): + return f"{self.message} (Error Code: {self.error_code})" + + +class BrightDataDatasetToolSchema(BaseModel): + """ + Schema for validating input parameters for the BrightDataDatasetTool. + + Attributes: + dataset_type (str): Required Bright Data Dataset Type used to specify which dataset to access. + format (str): Response format (json by default). Multiple formats exist - json, ndjson, jsonl, csv + url (str): The URL from which structured data needs to be extracted. + zipcode (Optional[str]): An optional ZIP code to narrow down the data geographically. + additional_params (Optional[Dict]): Extra parameters for the Bright Data API call. + """ + + dataset_type: str = Field(..., description="The Bright Data Dataset Type") + format: Optional[str] = Field( + default="json", description="Response format (json by default)" + ) + url: str = Field(..., description="The URL to extract data from") + zipcode: Optional[str] = Field(default=None, description="Optional zipcode") + additional_params: Optional[Dict[str, Any]] = Field( + default=None, description="Additional params if any" + ) + +config = BrightDataConfig.from_env() + +BRIGHTDATA_API_URL = config.API_URL +timeout = config.DEFAULT_TIMEOUT + +datasets = [ + { + "id": "amazon_product", + "dataset_id": "gd_l7q7dkf244hwjntr0", + "description": "\n".join( + [ + "Quickly read structured amazon product data.", + "Requires a valid product URL with /dp/ in it.", + "This can be a cache lookup, so it can be more reliable than scraping", + ] + ), + "inputs": ["url"], + }, + { + "id": "amazon_product_reviews", + "dataset_id": "gd_le8e811kzy4ggddlq", + "description": "\n".join( + [ + "Quickly read structured amazon product review data.", + "Requires a valid product URL with /dp/ in it.", + "This can be a cache lookup, so it can be more reliable than scraping", + ] + ), + "inputs": ["url"], + }, + { + "id": "amazon_product_search", + "dataset_id": "gd_lwdb4vjm1ehb499uxs", + "description": "\n".join( + [ + "Quickly read structured amazon product search data.", + "Requires a valid search keyword and amazon domain URL.", + "This can be a cache lookup, so it can be more reliable than scraping", + ] + ), + "inputs": ["keyword", "url", "pages_to_search"], + "defaults": {"pages_to_search": "1"}, + }, + { + "id": "walmart_product", + "dataset_id": "gd_l95fol7l1ru6rlo116", + "description": "\n".join( + [ + "Quickly read structured walmart product data.", + "Requires a valid product URL with /ip/ in it.", + "This can be a cache lookup, so it can be more reliable than scraping", + ] + ), + "inputs": ["url"], + }, + { + "id": "walmart_seller", + "dataset_id": "gd_m7ke48w81ocyu4hhz0", + "description": "\n".join( + [ + "Quickly read structured walmart seller data.", + "Requires a valid walmart seller URL.", + "This can be a cache lookup, so it can be more reliable than scraping", + ] + ), + "inputs": ["url"], + }, + { + "id": "ebay_product", + "dataset_id": "gd_ltr9mjt81n0zzdk1fb", + "description": "\n".join( + [ + "Quickly read structured ebay product data.", + "Requires a valid ebay product URL.", + "This can be a cache lookup, so it can be more reliable than scraping", + ] + ), + "inputs": ["url"], + }, + { + "id": "homedepot_products", + "dataset_id": "gd_lmusivh019i7g97q2n", + "description": "\n".join( + [ + "Quickly read structured homedepot product data.", + "Requires a valid homedepot product URL.", + "This can be a cache lookup, so it can be more reliable than scraping", + ] + ), + "inputs": ["url"], + }, + { + "id": "zara_products", + "dataset_id": "gd_lct4vafw1tgx27d4o0", + "description": "\n".join( + [ + "Quickly read structured zara product data.", + "Requires a valid zara product URL.", + "This can be a cache lookup, so it can be more reliable than scraping", + ] + ), + "inputs": ["url"], + }, + { + "id": "etsy_products", + "dataset_id": "gd_ltppk0jdv1jqz25mz", + "description": "\n".join( + [ + "Quickly read structured etsy product data.", + "Requires a valid etsy product URL.", + "This can be a cache lookup, so it can be more reliable than scraping", + ] + ), + "inputs": ["url"], + }, + { + "id": "bestbuy_products", + "dataset_id": "gd_ltre1jqe1jfr7cccf", + "description": "\n".join( + [ + "Quickly read structured bestbuy product data.", + "Requires a valid bestbuy product URL.", + "This can be a cache lookup, so it can be more reliable than scraping", + ] + ), + "inputs": ["url"], + }, + { + "id": "linkedin_person_profile", + "dataset_id": "gd_l1viktl72bvl7bjuj0", + "description": "\n".join( + [ + "Quickly read structured linkedin people profile data.", + "This can be a cache lookup, so it can be more reliable than scraping", + ] + ), + "inputs": ["url"], + }, + { + "id": "linkedin_company_profile", + "dataset_id": "gd_l1vikfnt1wgvvqz95w", + "description": "\n".join( + [ + "Quickly read structured linkedin company profile data", + "This can be a cache lookup, so it can be more reliable than scraping", + ] + ), + "inputs": ["url"], + }, + { + "id": "linkedin_job_listings", + "dataset_id": "gd_lpfll7v5hcqtkxl6l", + "description": "\n".join( + [ + "Quickly read structured linkedin job listings data", + "This can be a cache lookup, so it can be more reliable than scraping", + ] + ), + "inputs": ["url"], + }, + { + "id": "linkedin_posts", + "dataset_id": "gd_lyy3tktm25m4avu764", + "description": "\n".join( + [ + "Quickly read structured linkedin posts data", + "This can be a cache lookup, so it can be more reliable than scraping", + ] + ), + "inputs": ["url"], + }, + { + "id": "linkedin_people_search", + "dataset_id": "gd_m8d03he47z8nwb5xc", + "description": "\n".join( + [ + "Quickly read structured linkedin people search data", + "This can be a cache lookup, so it can be more reliable than scraping", + ] + ), + "inputs": ["url", "first_name", "last_name"], + }, + { + "id": "crunchbase_company", + "dataset_id": "gd_l1vijqt9jfj7olije", + "description": "\n".join( + [ + "Quickly read structured crunchbase company data", + "This can be a cache lookup, so it can be more reliable than scraping", + ] + ), + "inputs": ["url"], + }, + { + "id": "zoominfo_company_profile", + "dataset_id": "gd_m0ci4a4ivx3j5l6nx", + "description": "\n".join( + [ + "Quickly read structured ZoomInfo company profile data.", + "Requires a valid ZoomInfo company URL.", + "This can be a cache lookup, so it can be more reliable than scraping", + ] + ), + "inputs": ["url"], + }, + { + "id": "instagram_profiles", + "dataset_id": "gd_l1vikfch901nx3by4", + "description": "\n".join( + [ + "Quickly read structured Instagram profile data.", + "Requires a valid Instagram URL.", + "This can be a cache lookup, so it can be more reliable than scraping", + ] + ), + "inputs": ["url"], + }, + { + "id": "instagram_posts", + "dataset_id": "gd_lk5ns7kz21pck8jpis", + "description": "\n".join( + [ + "Quickly read structured Instagram post data.", + "Requires a valid Instagram URL.", + "This can be a cache lookup, so it can be more reliable than scraping", + ] + ), + "inputs": ["url"], + }, + { + "id": "instagram_reels", + "dataset_id": "gd_lyclm20il4r5helnj", + "description": "\n".join( + [ + "Quickly read structured Instagram reel data.", + "Requires a valid Instagram URL.", + "This can be a cache lookup, so it can be more reliable than scraping", + ] + ), + "inputs": ["url"], + }, + { + "id": "instagram_comments", + "dataset_id": "gd_ltppn085pokosxh13", + "description": "\n".join( + [ + "Quickly read structured Instagram comments data.", + "Requires a valid Instagram URL.", + "This can be a cache lookup, so it can be more reliable than scraping", + ] + ), + "inputs": ["url"], + }, + { + "id": "facebook_posts", + "dataset_id": "gd_lyclm1571iy3mv57zw", + "description": "\n".join( + [ + "Quickly read structured Facebook post data.", + "Requires a valid Facebook post URL.", + "This can be a cache lookup, so it can be more reliable than scraping", + ] + ), + "inputs": ["url"], + }, + { + "id": "facebook_marketplace_listings", + "dataset_id": "gd_lvt9iwuh6fbcwmx1a", + "description": "\n".join( + [ + "Quickly read structured Facebook marketplace listing data.", + "Requires a valid Facebook marketplace listing URL.", + "This can be a cache lookup, so it can be more reliable than scraping", + ] + ), + "inputs": ["url"], + }, + { + "id": "facebook_company_reviews", + "dataset_id": "gd_m0dtqpiu1mbcyc2g86", + "description": "\n".join( + [ + "Quickly read structured Facebook company reviews data.", + "Requires a valid Facebook company URL and number of reviews.", + "This can be a cache lookup, so it can be more reliable than scraping", + ] + ), + "inputs": ["url", "num_of_reviews"], + }, + { + "id": "facebook_events", + "dataset_id": "gd_m14sd0to1jz48ppm51", + "description": "\n".join( + [ + "Quickly read structured Facebook events data.", + "Requires a valid Facebook event URL.", + "This can be a cache lookup, so it can be more reliable than scraping", + ] + ), + "inputs": ["url"], + }, + { + "id": "tiktok_profiles", + "dataset_id": "gd_l1villgoiiidt09ci", + "description": "\n".join( + [ + "Quickly read structured Tiktok profiles data.", + "Requires a valid Tiktok profile URL.", + "This can be a cache lookup, so it can be more reliable than scraping", + ] + ), + "inputs": ["url"], + }, + { + "id": "tiktok_posts", + "dataset_id": "gd_lu702nij2f790tmv9h", + "description": "\n".join( + [ + "Quickly read structured Tiktok post data.", + "Requires a valid Tiktok post URL.", + "This can be a cache lookup, so it can be more reliable than scraping", + ] + ), + "inputs": ["url"], + }, + { + "id": "tiktok_shop", + "dataset_id": "gd_m45m1u911dsa4274pi", + "description": "\n".join( + [ + "Quickly read structured Tiktok shop data.", + "Requires a valid Tiktok shop product URL.", + "This can be a cache lookup...", + ] + ), + "inputs": ["url"], + }, +] + + +class BrightDataDatasetTool(BaseTool): + """ + CrewAI-compatible tool for scraping structured data using Bright Data Datasets. + + Attributes: + name (str): Tool name displayed in the CrewAI environment. + description (str): Tool description shown to agents or users. + args_schema (Type[BaseModel]): Pydantic schema for validating input arguments. + """ + + name: str = "Bright Data Dataset Tool" + description: str = "Scrapes structured data using Bright Data Dataset API from a URL and optional input parameters" + args_schema: Type[BaseModel] = BrightDataDatasetToolSchema + dataset_type: Optional[str] = None + url: Optional[str] = None + format: str = "json" + zipcode: Optional[str] = None + additional_params: Optional[Dict[str, Any]] = None + + def __init__(self, dataset_type: str = None, url: str = None, format: str = "json", zipcode: str = None, additional_params: Dict[str, Any] = None): + super().__init__() + self.dataset_type = dataset_type + self.url = url + self.format = format + self.zipcode = zipcode + self.additional_params = additional_params + + def filter_dataset_by_id(self, target_id): + return [dataset for dataset in datasets if dataset["id"] == target_id] + + async def get_dataset_data_async( + self, + dataset_type: str, + output_format: str, + url: str, + zipcode: Optional[str] = None, + additional_params: Optional[Dict[str, Any]] = None, + polling_interval: int = 1, + ) -> Dict: + """ + Asynchronously trigger and poll Bright Data dataset scraping. + + Args: + dataset_type (str): Bright Data Dataset Type. + url (str): Target URL to scrape. + zipcode (Optional[str]): Optional ZIP code for geo-specific data. + additional_params (Optional[Dict]): Extra API parameters. + polling_interval (int): Time interval in seconds between polling attempts. + + Returns: + Dict: Structured dataset result from Bright Data. + + Raises: + Exception: If any API step fails or the job fails. + TimeoutError: If polling times out before job completion. + """ + request_data = {"url": url} + if zipcode is not None: + request_data["zipcode"] = zipcode + + # Set additional parameters dynamically depending upon the dataset that is being requested + if additional_params: + request_data.update(additional_params) + + api_key = os.getenv("BRIGHT_DATA_API_KEY") + + headers = { + "Authorization": f"Bearer {api_key}", + "Content-Type": "application/json", + } + + dataset_id = "" + dataset = self.filter_dataset_by_id(dataset_type) + + if len(dataset) == 1: + dataset_id = dataset[0]["dataset_id"] + else: + raise ValueError( + f"Unable to find the dataset for {dataset_type}. Please make sure to pass a valid one" + ) + + async with aiohttp.ClientSession() as session: + # Step 1: Trigger job + async with session.post( + f"{BRIGHTDATA_API_URL}/datasets/v3/trigger", + params={"dataset_id": dataset_id, "include_errors": "true"}, + json=[request_data], + headers=headers, + ) as trigger_response: + if trigger_response.status != 200: + raise BrightDataDatasetToolException( + f"Trigger failed: {await trigger_response.text()}", + trigger_response.status, + ) + trigger_data = await trigger_response.json() + print(trigger_data) + snapshot_id = trigger_data.get("snapshot_id") + + # Step 2: Poll for completion + elapsed = 0 + while elapsed < timeout: + await asyncio.sleep(polling_interval) + elapsed += polling_interval + + async with session.get( + f"{BRIGHTDATA_API_URL}/datasets/v3/progress/{snapshot_id}", + headers=headers, + ) as status_response: + if status_response.status != 200: + raise BrightDataDatasetToolException( + f"Status check failed: {await status_response.text()}", + status_response.status, + ) + status_data = await status_response.json() + if status_data.get("status") == "ready": + print("Job is ready") + break + elif status_data.get("status") == "error": + raise BrightDataDatasetToolException( + f"Job failed: {status_data}", 0 + ) + else: + raise TimeoutError("Polling timed out before job completed.") + + # Step 3: Retrieve result + async with session.get( + f"{BRIGHTDATA_API_URL}/datasets/v3/snapshot/{snapshot_id}", + params={"format": output_format}, + headers=headers, + ) as snapshot_response: + if snapshot_response.status != 200: + raise BrightDataDatasetToolException( + f"Result fetch failed: {await snapshot_response.text()}", + snapshot_response.status, + ) + + return await snapshot_response.text() + + def _run(self, url: str = None, dataset_type: str = None, format: str = None, zipcode: str = None, additional_params: Dict[str, Any] = None, **kwargs: Any) -> Any: + dataset_type = dataset_type or self.dataset_type + output_format = format or self.format + url = url or self.url + zipcode = zipcode or self.zipcode + additional_params = additional_params or self.additional_params + + if not dataset_type: + raise ValueError("dataset_type is required either in constructor or method call") + if not url: + raise ValueError("url is required either in constructor or method call") + + valid_output_formats = {"json", "ndjson", "jsonl", "csv"} + if output_format not in valid_output_formats: + raise ValueError( + f"Unsupported output format: {output_format}. Must be one of {', '.join(valid_output_formats)}." + ) + + api_key = os.getenv("BRIGHT_DATA_API_KEY") + if not api_key: + raise ValueError("BRIGHT_DATA_API_KEY environment variable is required.") + + try: + return asyncio.run( + self.get_dataset_data_async( + dataset_type=dataset_type, + output_format=output_format, + url=url, + zipcode=zipcode, + additional_params=additional_params, + ) + ) + except TimeoutError as e: + return f"Timeout Exception occured in method : get_dataset_data_async. Details - {str(e)}" + except BrightDataDatasetToolException as e: + return f"Exception occured in method : get_dataset_data_async. Details - {str(e)}" + except Exception as e: + return f"Bright Data API error: {str(e)}" diff --git a/crewai_tools/tools/brightdata_tool/brightdata_serp.py b/crewai_tools/tools/brightdata_tool/brightdata_serp.py new file mode 100644 index 000000000..ae197ce0f --- /dev/null +++ b/crewai_tools/tools/brightdata_tool/brightdata_serp.py @@ -0,0 +1,207 @@ +import os +import urllib.parse +from typing import Any, Optional, Type + +import requests +from crewai.tools import BaseTool +from pydantic import BaseModel, Field + +class BrightDataConfig(BaseModel): + API_URL: str = "https://api.brightdata.com/request" + + @classmethod + def from_env(cls): + return cls( + API_URL=os.environ.get("BRIGHTDATA_API_URL", "https://api.brightdata.com/request") + ) + +class BrightDataSearchToolSchema(BaseModel): + """ + Schema that defines the input arguments for the BrightDataSearchToolSchema. + + Attributes: + query (str): The search query to be executed (e.g., "latest AI news"). + search_engine (Optional[str]): The search engine to use ("google", "bing", "yandex"). Default is "google". + country (Optional[str]): Two-letter country code for geo-targeting (e.g., "us", "in"). Default is "us". + language (Optional[str]): Language code for search results (e.g., "en", "es"). Default is "en". + search_type (Optional[str]): Type of search, such as "isch" (images), "nws" (news), "jobs", etc. + device_type (Optional[str]): Device type to simulate ("desktop", "mobile", "ios", "android"). Default is "desktop". + parse_results (Optional[bool]): If True, results will be returned in structured JSON. If False, raw HTML. Default is True. + """ + + query: str = Field(..., description="Search query to perform") + search_engine: Optional[str] = Field( + default="google", + description="Search engine domain (e.g., 'google', 'bing', 'yandex')", + ) + country: Optional[str] = Field( + default="us", + description="Two-letter country code for geo-targeting (e.g., 'us', 'gb')", + ) + language: Optional[str] = Field( + default="en", + description="Language code (e.g., 'en', 'es') used in the query URL", + ) + search_type: Optional[str] = Field( + default=None, + description="Type of search (e.g., 'isch' for images, 'nws' for news)", + ) + device_type: Optional[str] = Field( + default="desktop", + description="Device type to simulate (e.g., 'mobile', 'desktop', 'ios')", + ) + parse_results: Optional[bool] = Field( + default=True, + description="Whether to parse and return JSON (True) or raw HTML/text (False)", + ) + + +class BrightDataSearchTool(BaseTool): + """ + A web search tool that utilizes Bright Data's SERP API to perform queries and return either structured results + or raw page content from search engines like Google or Bing. + + Attributes: + name (str): Tool name used by the agent. + description (str): A brief explanation of what the tool does. + args_schema (Type[BaseModel]): Schema class for validating tool arguments. + base_url (str): The Bright Data API endpoint used for making the POST request. + api_key (str): Bright Data API key loaded from the environment variable 'BRIGHT_DATA_API_KEY'. + zone (str): Zone identifier from Bright Data, loaded from the environment variable 'BRIGHT_DATA_ZONE'. + + Raises: + ValueError: If API key or zone environment variables are not set. + """ + + name: str = "Bright Data SERP Search" + description: str = "Tool to perform web search using Bright Data SERP API." + args_schema: Type[BaseModel] = BrightDataSearchToolSchema + _config = BrightDataConfig.from_env() + base_url: str = "" + api_key: str = "" + zone: str = "" + query: Optional[str] = None + search_engine: str = "google" + country: str = "us" + language: str = "en" + search_type: Optional[str] = None + device_type: str = "desktop" + parse_results: bool = True + + def __init__(self, query: str = None, search_engine: str = "google", country: str = "us", language: str = "en", search_type: str = None, device_type: str = "desktop", parse_results: bool = True): + super().__init__() + self.base_url = self._config.API_URL + self.query = query + self.search_engine = search_engine + self.country = country + self.language = language + self.search_type = search_type + self.device_type = device_type + self.parse_results = parse_results + + self.api_key = os.getenv("BRIGHT_DATA_API_KEY") + self.zone = os.getenv("BRIGHT_DATA_ZONE") + if not self.api_key: + raise ValueError("BRIGHT_DATA_API_KEY environment variable is required.") + if not self.zone: + raise ValueError("BRIGHT_DATA_ZONE environment variable is required.") + + def get_search_url(self, engine: str, query: str): + if engine == "yandex": + return f"https://yandex.com/search/?text=${query}" + elif engine == "bing": + return f"https://www.bing.com/search?q=${query}" + return f"https://www.google.com/search?q=${query}" + + def _run(self, query: str = None, search_engine: str = None, country: str = None, language: str = None, search_type: str = None, device_type: str = None, parse_results: bool = None, **kwargs) -> Any: + """ + Executes a search query using Bright Data SERP API and returns results. + + Args: + query (str): The search query string (URL encoded internally). + search_engine (str): The search engine to use (default: "google"). + country (str): Country code for geotargeting (default: "us"). + language (str): Language code for the query (default: "en"). + search_type (str): Optional type of search such as "nws", "isch", "jobs". + device_type (str): Optional device type to simulate (e.g., "mobile", "ios", "desktop"). + parse_results (bool): If True, returns structured data; else raw page (default: True). + results_count (str or int): Number of search results to fetch (default: "10"). + + Returns: + dict or str: Parsed JSON data from Bright Data if available, otherwise error message. + """ + + query = query or self.query + search_engine = search_engine or self.search_engine + country = country or self.country + language = language or self.language + search_type = search_type or self.search_type + device_type = device_type or self.device_type + parse_results = parse_results if parse_results is not None else self.parse_results + results_count = kwargs.get("results_count", "10") + + # Validate required parameters + if not query: + raise ValueError("query is required either in constructor or method call") + + # Build the search URL + query = urllib.parse.quote(query) + url = self.get_search_url(search_engine, query) + + # Add parameters to the URL + params = [] + + if country: + params.append(f"gl={country}") + + if language: + params.append(f"hl={language}") + + if results_count: + params.append(f"num={results_count}") + + if parse_results: + params.append(f"brd_json=1") + + if search_type: + if search_type == "jobs": + params.append("ibp=htl;jobs") + else: + params.append(f"tbm={search_type}") + + if device_type: + if device_type == "mobile": + params.append("brd_mobile=1") + elif device_type == "ios": + params.append("brd_mobile=ios") + elif device_type == "android": + params.append("brd_mobile=android") + + # Combine parameters with the URL + if params: + url += "&" + "&".join(params) + + # Set up the API request parameters + request_params = {"zone": self.zone, "url": url, "format": "raw"} + + request_params = {k: v for k, v in request_params.items() if v is not None} + + headers = { + "Authorization": f"Bearer {self.api_key}", + "Content-Type": "application/json", + } + + try: + response = requests.post( + self.base_url, json=request_params, headers=headers + ) + + print(f"Status code: {response.status_code}") + response.raise_for_status() + + return response.text + + except requests.RequestException as e: + return f"Error performing BrightData search: {str(e)}" + except Exception as e: + return f"Error fetching results: {str(e)}" diff --git a/crewai_tools/tools/brightdata_tool/brightdata_unlocker.py b/crewai_tools/tools/brightdata_tool/brightdata_unlocker.py new file mode 100644 index 000000000..27864cb97 --- /dev/null +++ b/crewai_tools/tools/brightdata_tool/brightdata_unlocker.py @@ -0,0 +1,122 @@ +import os +from typing import Any, Optional, Type + +import requests +from crewai.tools import BaseTool +from pydantic import BaseModel, Field + +class BrightDataConfig(BaseModel): + API_URL: str = "https://api.brightdata.com/request" + + @classmethod + def from_env(cls): + return cls( + API_URL=os.environ.get("BRIGHTDATA_API_URL", "https://api.brightdata.com/request") + ) + +class BrightDataUnlockerToolSchema(BaseModel): + """ + Pydantic schema for input parameters used by the BrightDataWebUnlockerTool. + + This schema defines the structure and validation for parameters passed when performing + a web scraping request using Bright Data's Web Unlocker. + + Attributes: + url (str): The target URL to scrape. + format (Optional[str]): Format of the response returned by Bright Data. Default 'raw' format. + data_format (Optional[str]): Response data format (html by default). markdown is one more option. + """ + + url: str = Field(..., description="URL to perform the web scraping") + format: Optional[str] = Field( + default="raw", description="Response format (raw is standard)" + ) + data_format: Optional[str] = Field( + default="markdown", description="Response data format (html by default)" + ) + + +class BrightDataWebUnlockerTool(BaseTool): + """ + A tool for performing web scraping using the Bright Data Web Unlocker API. + + This tool allows automated and programmatic access to web pages by routing requests + through Bright Data's unlocking and proxy infrastructure, which can bypass bot + protection mechanisms like CAPTCHA, geo-restrictions, and anti-bot detection. + + Attributes: + name (str): Name of the tool. + description (str): Description of what the tool does. + args_schema (Type[BaseModel]): Pydantic model schema for expected input arguments. + base_url (str): Base URL of the Bright Data Web Unlocker API. + api_key (str): Bright Data API key (must be set in the BRIGHT_DATA_API_KEY environment variable). + zone (str): Bright Data zone identifier (must be set in the BRIGHT_DATA_ZONE environment variable). + + Methods: + _run(**kwargs: Any) -> Any: + Sends a scraping request to Bright Data's Web Unlocker API and returns the result. + """ + + name: str = "Bright Data Web Unlocker Scraping" + description: str = "Tool to perform web scraping using Bright Data Web Unlocker" + args_schema: Type[BaseModel] = BrightDataUnlockerToolSchema + _config = BrightDataConfig.from_env() + base_url: str = "" + api_key: str = "" + zone: str = "" + url: Optional[str] = None + format: str = "raw" + data_format: str = "markdown" + + def __init__(self, url: str = None, format: str = "raw", data_format: str = "markdown"): + super().__init__() + self.base_url = self._config.API_URL + self.url = url + self.format = format + self.data_format = data_format + + self.api_key = os.getenv("BRIGHT_DATA_API_KEY") + self.zone = os.getenv("BRIGHT_DATA_ZONE") + if not self.api_key: + raise ValueError("BRIGHT_DATA_API_KEY environment variable is required.") + if not self.zone: + raise ValueError("BRIGHT_DATA_ZONE environment variable is required.") + + def _run(self, url: str = None, format: str = None, data_format: str = None, **kwargs: Any) -> Any: + url = url or self.url + format = format or self.format + data_format = data_format or self.data_format + + if not url: + raise ValueError("url is required either in constructor or method call") + + payload = { + "url": url, + "zone": self.zone, + "format": format, + } + valid_data_formats = {"html", "markdown"} + if data_format not in valid_data_formats: + raise ValueError( + f"Unsupported data format: {data_format}. Must be one of {', '.join(valid_data_formats)}." + ) + + if data_format == "markdown": + payload["data_format"] = "markdown" + + headers = { + "Authorization": f"Bearer {self.api_key}", + "Content-Type": "application/json", + } + + try: + response = requests.post(self.base_url, json=payload, headers=headers) + print(f"Status Code: {response.status_code}") + response.raise_for_status() + + return response.text + + except requests.RequestException as e: + return f"HTTP Error performing BrightData Web Unlocker Scrape: {e}\nResponse: {getattr(e.response, 'text', '')}" + except Exception as e: + return f"Error fetching results: {str(e)}" diff --git a/crewai_tools/tools/browserbase_load_tool/README.md b/crewai_tools/tools/browserbase_load_tool/README.md new file mode 100644 index 000000000..bd562da0d --- /dev/null +++ b/crewai_tools/tools/browserbase_load_tool/README.md @@ -0,0 +1,38 @@ +# BrowserbaseLoadTool + +## Description + +[Browserbase](https://browserbase.com) is a developer platform to reliably run, manage, and monitor headless browsers. + + Power your AI data retrievals with: + - [Serverless Infrastructure](https://docs.browserbase.com/under-the-hood) providing reliable browsers to extract data from complex UIs + - [Stealth Mode](https://docs.browserbase.com/features/stealth-mode) with included fingerprinting tactics and automatic captcha solving + - [Session Debugger](https://docs.browserbase.com/features/sessions) to inspect your Browser Session with networks timeline and logs + - [Live Debug](https://docs.browserbase.com/guides/session-debug-connection/browser-remote-control) to quickly debug your automation + +## Installation + +- Get an API key and Project ID from [browserbase.com](https://browserbase.com) and set it in environment variables (`BROWSERBASE_API_KEY`, `BROWSERBASE_PROJECT_ID`). +- Install the [Browserbase SDK](http://github.com/browserbase/python-sdk) along with `crewai[tools]` package: + +``` +pip install browserbase 'crewai[tools]' +``` + +## Example + +Utilize the BrowserbaseLoadTool as follows to allow your agent to load websites: + +```python +from crewai_tools import BrowserbaseLoadTool + +tool = BrowserbaseLoadTool() +``` + +## Arguments + +- `api_key` Optional. Browserbase API key. Default is `BROWSERBASE_API_KEY` env variable. +- `project_id` Optional. Browserbase Project ID. Default is `BROWSERBASE_PROJECT_ID` env variable. +- `text_content` Retrieve only text content. Default is `False`. +- `session_id` Optional. Provide an existing Session ID. +- `proxy` Optional. Enable/Disable Proxies." diff --git a/crewai_tools/tools/browserbase_load_tool/browserbase_load_tool.py b/crewai_tools/tools/browserbase_load_tool/browserbase_load_tool.py new file mode 100644 index 000000000..b6b3612dc --- /dev/null +++ b/crewai_tools/tools/browserbase_load_tool/browserbase_load_tool.py @@ -0,0 +1,67 @@ +import os +from typing import Any, Optional, Type, List + +from crewai.tools import BaseTool, EnvVar +from pydantic import BaseModel, Field + + +class BrowserbaseLoadToolSchema(BaseModel): + url: str = Field(description="Website URL") + + +class BrowserbaseLoadTool(BaseTool): + name: str = "Browserbase web load tool" + description: str = "Load webpages url in a headless browser using Browserbase and return the contents" + args_schema: Type[BaseModel] = BrowserbaseLoadToolSchema + api_key: Optional[str] = os.getenv("BROWSERBASE_API_KEY") + project_id: Optional[str] = os.getenv("BROWSERBASE_PROJECT_ID") + text_content: Optional[bool] = False + session_id: Optional[str] = None + proxy: Optional[bool] = None + browserbase: Optional[Any] = None + package_dependencies: List[str] = ["browserbase"] + env_vars: List[EnvVar] = [ + EnvVar(name="BROWSERBASE_API_KEY", description="API key for Browserbase services", required=False), + EnvVar(name="BROWSERBASE_PROJECT_ID", description="Project ID for Browserbase services", required=False), + ] + + def __init__( + self, + api_key: Optional[str] = None, + project_id: Optional[str] = None, + text_content: Optional[bool] = False, + session_id: Optional[str] = None, + proxy: Optional[bool] = None, + **kwargs, + ): + super().__init__(**kwargs) + if not self.api_key: + raise EnvironmentError( + "BROWSERBASE_API_KEY environment variable is required for initialization" + ) + try: + from browserbase import Browserbase # type: ignore + except ImportError: + import click + + if click.confirm( + "`browserbase` package not found, would you like to install it?" + ): + import subprocess + + subprocess.run(["uv", "add", "browserbase"], check=True) + from browserbase import Browserbase # type: ignore + else: + raise ImportError( + "`browserbase` package not found, please run `uv add browserbase`" + ) + + self.browserbase = Browserbase(api_key=self.api_key) + self.text_content = text_content + self.session_id = session_id + self.proxy = proxy + + def _run(self, url: str): + return self.browserbase.load_url( + url, self.text_content, self.session_id, self.proxy + ) diff --git a/crewai_tools/tools/code_docs_search_tool/README.md b/crewai_tools/tools/code_docs_search_tool/README.md new file mode 100644 index 000000000..f90398a11 --- /dev/null +++ b/crewai_tools/tools/code_docs_search_tool/README.md @@ -0,0 +1,56 @@ +# CodeDocsSearchTool + +## Description +The CodeDocsSearchTool is a powerful RAG (Retrieval-Augmented Generation) tool designed for semantic searches within code documentation. It enables users to efficiently find specific information or topics within code documentation. By providing a `docs_url` during initialization, the tool narrows down the search to that particular documentation site. Alternatively, without a specific `docs_url`, it searches across a wide array of code documentation known or discovered throughout its execution, making it versatile for various documentation search needs. + +## Installation +To start using the CodeDocsSearchTool, first, install the crewai_tools package via pip: +```shell +pip install 'crewai[tools]' +``` + +## Example +Utilize the CodeDocsSearchTool as follows to conduct searches within code documentation: +```python +from crewai_tools import CodeDocsSearchTool + +# To search any code documentation content if the URL is known or discovered during its execution: +tool = CodeDocsSearchTool() + +# OR + +# To specifically focus your search on a given documentation site by providing its URL: +tool = CodeDocsSearchTool(docs_url='https://docs.example.com/reference') +``` +Note: Substitute 'https://docs.example.com/reference' with your target documentation URL and 'How to use search tool' with the search query relevant to your needs. + +## Arguments +- `docs_url`: Optional. Specifies the URL of the code documentation to be searched. Providing this during the tool's initialization focuses the search on the specified documentation content. + +## Custom model and embeddings + +By default, the tool uses OpenAI for both embeddings and summarization. To customize the model, you can use a config dictionary as follows: + +```python +tool = CodeDocsSearchTool( + config=dict( + llm=dict( + provider="ollama", # or google, openai, anthropic, llama2, ... + config=dict( + model="llama2", + # temperature=0.5, + # top_p=1, + # stream=true, + ), + ), + embedder=dict( + provider="google", + config=dict( + model="models/embedding-001", + task_type="retrieval_document", + # title="Embeddings", + ), + ), + ) +) +``` diff --git a/crewai_tools/tools/code_docs_search_tool/code_docs_search_tool.py b/crewai_tools/tools/code_docs_search_tool/code_docs_search_tool.py new file mode 100644 index 000000000..155b4390d --- /dev/null +++ b/crewai_tools/tools/code_docs_search_tool/code_docs_search_tool.py @@ -0,0 +1,56 @@ +from typing import Any, Optional, Type + +try: + from embedchain.models.data_type import DataType + EMBEDCHAIN_AVAILABLE = True +except ImportError: + EMBEDCHAIN_AVAILABLE = False + +from pydantic import BaseModel, Field + +from ..rag.rag_tool import RagTool + + +class FixedCodeDocsSearchToolSchema(BaseModel): + """Input for CodeDocsSearchTool.""" + + search_query: str = Field( + ..., + description="Mandatory search query you want to use to search the Code Docs content", + ) + + +class CodeDocsSearchToolSchema(FixedCodeDocsSearchToolSchema): + """Input for CodeDocsSearchTool.""" + + docs_url: str = Field(..., description="Mandatory docs_url path you want to search") + + +class CodeDocsSearchTool(RagTool): + name: str = "Search a Code Docs content" + description: str = ( + "A tool that can be used to semantic search a query from a Code Docs content." + ) + args_schema: Type[BaseModel] = CodeDocsSearchToolSchema + + def __init__(self, docs_url: Optional[str] = None, **kwargs): + super().__init__(**kwargs) + if docs_url is not None: + self.add(docs_url) + self.description = f"A tool that can be used to semantic search a query the {docs_url} Code Docs content." + self.args_schema = FixedCodeDocsSearchToolSchema + self._generate_description() + + def add(self, docs_url: str) -> None: + if not EMBEDCHAIN_AVAILABLE: + raise ImportError("embedchain is not installed. Please install it with `pip install crewai-tools[embedchain]`") + super().add(docs_url, data_type=DataType.DOCS_SITE) + + def _run( + self, + search_query: str, + docs_url: Optional[str] = None, + ) -> str: + if docs_url is not None: + self.add(docs_url) + return super()._run(query=search_query) diff --git a/crewai_tools/tools/code_interpreter_tool/Dockerfile b/crewai_tools/tools/code_interpreter_tool/Dockerfile new file mode 100644 index 000000000..4df22ca58 --- /dev/null +++ b/crewai_tools/tools/code_interpreter_tool/Dockerfile @@ -0,0 +1,6 @@ +FROM python:3.12-alpine + +RUN pip install requests beautifulsoup4 + +# Set the working directory +WORKDIR /workspace diff --git a/crewai_tools/tools/code_interpreter_tool/README.md b/crewai_tools/tools/code_interpreter_tool/README.md new file mode 100644 index 000000000..ab0cbf44b --- /dev/null +++ b/crewai_tools/tools/code_interpreter_tool/README.md @@ -0,0 +1,53 @@ +# CodeInterpreterTool + +## Description +This tool is used to give the Agent the ability to run code (Python3) from the code generated by the Agent itself. The code is executed in a sandboxed environment, so it is safe to run any code. + +It is incredible useful since it allows the Agent to generate code, run it in the same environment, get the result and use it to make decisions. + +## Requirements + +- Docker + +## Installation +Install the crewai_tools package +```shell +pip install 'crewai[tools]' +``` + +## Example + +Remember that when using this tool, the code must be generated by the Agent itself. The code must be a Python3 code. And it will take some time for the first time to run because it needs to build the Docker image. + +```python +from crewai_tools import CodeInterpreterTool + +Agent( + ... + tools=[CodeInterpreterTool()], +) +``` + +Or if you need to pass your own Dockerfile just do this + +```python +from crewai_tools import CodeInterpreterTool + +Agent( + ... + tools=[CodeInterpreterTool(user_dockerfile_path="")], +) +``` + +If it is difficult to connect to docker daemon automatically (especially for macOS users), you can do this to setup docker host manually + +```python +from crewai_tools import CodeInterpreterTool + +Agent( + ... + tools=[CodeInterpreterTool(user_docker_base_url="", + user_dockerfile_path="")], +) + +``` diff --git a/crewai_tools/tools/code_interpreter_tool/code_interpreter_tool.py b/crewai_tools/tools/code_interpreter_tool/code_interpreter_tool.py new file mode 100644 index 000000000..95559f2a7 --- /dev/null +++ b/crewai_tools/tools/code_interpreter_tool/code_interpreter_tool.py @@ -0,0 +1,373 @@ +"""Code Interpreter Tool for executing Python code in isolated environments. + +This module provides a tool for executing Python code either in a Docker container for +safe isolation or directly in a restricted sandbox. It includes mechanisms for blocking +potentially unsafe operations and importing restricted modules. +""" + +import importlib.util +import os +from types import ModuleType +from typing import Any, Dict, List, Optional, Type + +from crewai.tools import BaseTool +from docker import DockerClient +from docker import from_env as docker_from_env +from docker.errors import ImageNotFound, NotFound +from docker.models.containers import Container +from pydantic import BaseModel, Field + +from crewai_tools.printer import Printer + + +class CodeInterpreterSchema(BaseModel): + """Schema for defining inputs to the CodeInterpreterTool. + + This schema defines the required parameters for code execution, + including the code to run and any libraries that need to be installed. + """ + + code: str = Field( + ..., + description="Python3 code used to be interpreted in the Docker container. ALWAYS PRINT the final result and the output of the code", + ) + + libraries_used: List[str] = Field( + ..., + description="List of libraries used in the code with proper installing names separated by commas. Example: numpy,pandas,beautifulsoup4", + ) + + +class SandboxPython: + """A restricted Python execution environment for running code safely. + + This class provides methods to safely execute Python code by restricting access to + potentially dangerous modules and built-in functions. It creates a sandboxed + environment where harmful operations are blocked. + """ + + BLOCKED_MODULES = { + "os", + "sys", + "subprocess", + "shutil", + "importlib", + "inspect", + "tempfile", + "sysconfig", + "builtins", + } + + UNSAFE_BUILTINS = { + "exec", + "eval", + "open", + "compile", + "input", + "globals", + "locals", + "vars", + "help", + "dir", + } + + @staticmethod + def restricted_import( + name: str, + custom_globals: Optional[Dict[str, Any]] = None, + custom_locals: Optional[Dict[str, Any]] = None, + fromlist: Optional[List[str]] = None, + level: int = 0, + ) -> ModuleType: + """A restricted import function that blocks importing of unsafe modules. + + Args: + name: The name of the module to import. + custom_globals: Global namespace to use. + custom_locals: Local namespace to use. + fromlist: List of items to import from the module. + level: The level value passed to __import__. + + Returns: + The imported module if allowed. + + Raises: + ImportError: If the module is in the blocked modules list. + """ + if name in SandboxPython.BLOCKED_MODULES: + raise ImportError(f"Importing '{name}' is not allowed.") + return __import__(name, custom_globals, custom_locals, fromlist or (), level) + + @staticmethod + def safe_builtins() -> Dict[str, Any]: + """Creates a dictionary of built-in functions with unsafe ones removed. + + Returns: + A dictionary of safe built-in functions and objects. + """ + import builtins + + safe_builtins = { + k: v + for k, v in builtins.__dict__.items() + if k not in SandboxPython.UNSAFE_BUILTINS + } + safe_builtins["__import__"] = SandboxPython.restricted_import + return safe_builtins + + @staticmethod + def exec(code: str, locals: Dict[str, Any]) -> None: + """Executes Python code in a restricted environment. + + Args: + code: The Python code to execute as a string. + locals: A dictionary that will be used for local variable storage. + """ + exec(code, {"__builtins__": SandboxPython.safe_builtins()}, locals) + + +class CodeInterpreterTool(BaseTool): + """A tool for executing Python code in isolated environments. + + This tool provides functionality to run Python code either in a Docker container + for safe isolation or directly in a restricted sandbox. It can handle installing + Python packages and executing arbitrary Python code. + """ + + name: str = "Code Interpreter" + description: str = "Interprets Python3 code strings with a final print statement." + args_schema: Type[BaseModel] = CodeInterpreterSchema + default_image_tag: str = "code-interpreter:latest" + code: Optional[str] = None + user_dockerfile_path: Optional[str] = None + user_docker_base_url: Optional[str] = None + unsafe_mode: bool = False + + @staticmethod + def _get_installed_package_path() -> str: + """Gets the installation path of the crewai_tools package. + + Returns: + The directory path where the package is installed. + """ + spec = importlib.util.find_spec("crewai_tools") + return os.path.dirname(spec.origin) + + def _verify_docker_image(self) -> None: + """Verifies if the Docker image is available or builds it if necessary. + + Checks if the required Docker image exists. If not, builds it using either a + user-provided Dockerfile or the default one included with the package. + + Raises: + FileNotFoundError: If the Dockerfile cannot be found. + """ + + client = ( + docker_from_env() + if self.user_docker_base_url is None + else DockerClient(base_url=self.user_docker_base_url) + ) + + try: + client.images.get(self.default_image_tag) + + except ImageNotFound: + if self.user_dockerfile_path and os.path.exists(self.user_dockerfile_path): + dockerfile_path = self.user_dockerfile_path + else: + package_path = self._get_installed_package_path() + dockerfile_path = os.path.join( + package_path, "tools/code_interpreter_tool" + ) + if not os.path.exists(dockerfile_path): + raise FileNotFoundError( + f"Dockerfile not found in {dockerfile_path}" + ) + + client.images.build( + path=dockerfile_path, + tag=self.default_image_tag, + rm=True, + ) + + def _run(self, **kwargs) -> str: + """Runs the code interpreter tool with the provided arguments. + + Args: + **kwargs: Keyword arguments that should include 'code' and 'libraries_used'. + + Returns: + The output of the executed code as a string. + """ + code = kwargs.get("code", self.code) + libraries_used = kwargs.get("libraries_used", []) + + if self.unsafe_mode: + return self.run_code_unsafe(code, libraries_used) + else: + return self.run_code_safety(code, libraries_used) + + def _install_libraries(self, container: Container, libraries: List[str]) -> None: + """Installs required Python libraries in the Docker container. + + Args: + container: The Docker container where libraries will be installed. + libraries: A list of library names to install using pip. + """ + for library in libraries: + container.exec_run(["pip", "install", library]) + + def _init_docker_container(self) -> Container: + """Initializes and returns a Docker container for code execution. + + Stops and removes any existing container with the same name before creating + a new one. Maps the current working directory to /workspace in the container. + + Returns: + A Docker container object ready for code execution. + """ + container_name = "code-interpreter" + client = docker_from_env() + current_path = os.getcwd() + + # Check if the container is already running + try: + existing_container = client.containers.get(container_name) + existing_container.stop() + existing_container.remove() + except NotFound: + pass # Container does not exist, no need to remove + + return client.containers.run( + self.default_image_tag, + detach=True, + tty=True, + working_dir="/workspace", + name=container_name, + volumes={current_path: {"bind": "/workspace", "mode": "rw"}}, # type: ignore + ) + + def _check_docker_available(self) -> bool: + """Checks if Docker is available and running on the system. + + Attempts to run the 'docker info' command to verify Docker availability. + Prints appropriate messages if Docker is not installed or not running. + + Returns: + True if Docker is available and running, False otherwise. + """ + import subprocess + + try: + subprocess.run( + ["docker", "info"], + check=True, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + timeout=1, + ) + return True + except (subprocess.CalledProcessError, subprocess.TimeoutExpired): + Printer.print( + "Docker is installed but not running or inaccessible.", + color="bold_purple", + ) + return False + except FileNotFoundError: + Printer.print("Docker is not installed", color="bold_purple") + return False + + def run_code_safety(self, code: str, libraries_used: List[str]) -> str: + """Runs code in the safest available environment. + + Attempts to run code in Docker if available, falls back to a restricted + sandbox if Docker is not available. + + Args: + code: The Python code to execute as a string. + libraries_used: A list of Python library names to install before execution. + + Returns: + The output of the executed code as a string. + """ + if self._check_docker_available(): + return self.run_code_in_docker(code, libraries_used) + else: + return self.run_code_in_restricted_sandbox(code) + + def run_code_in_docker(self, code: str, libraries_used: List[str]) -> str: + """Runs Python code in a Docker container for safe isolation. + + Creates a Docker container, installs the required libraries, executes the code, + and then cleans up by stopping and removing the container. + + Args: + code: The Python code to execute as a string. + libraries_used: A list of Python library names to install before execution. + + Returns: + The output of the executed code as a string, or an error message if execution failed. + """ + Printer.print("Running code in Docker environment", color="bold_blue") + self._verify_docker_image() + container = self._init_docker_container() + self._install_libraries(container, libraries_used) + + exec_result = container.exec_run(["python3", "-c", code]) + + container.stop() + container.remove() + + if exec_result.exit_code != 0: + return f"Something went wrong while running the code: \n{exec_result.output.decode('utf-8')}" + return exec_result.output.decode("utf-8") + + def run_code_in_restricted_sandbox(self, code: str) -> str: + """Runs Python code in a restricted sandbox environment. + + Executes the code with restricted access to potentially dangerous modules and + built-in functions for basic safety when Docker is not available. + + Args: + code: The Python code to execute as a string. + + Returns: + The value of the 'result' variable from the executed code, + or an error message if execution failed. + """ + Printer.print("Running code in restricted sandbox", color="yellow") + exec_locals = {} + try: + SandboxPython.exec(code=code, locals=exec_locals) + return exec_locals.get("result", "No result variable found.") + except Exception as e: + return f"An error occurred: {str(e)}" + + def run_code_unsafe(self, code: str, libraries_used: List[str]) -> str: + """Runs code directly on the host machine without any safety restrictions. + + WARNING: This mode is unsafe and should only be used in trusted environments + with code from trusted sources. + + Args: + code: The Python code to execute as a string. + libraries_used: A list of Python library names to install before execution. + + Returns: + The value of the 'result' variable from the executed code, + or an error message if execution failed. + """ + + Printer.print("WARNING: Running code in unsafe mode", color="bold_magenta") + # Install libraries on the host machine + for library in libraries_used: + os.system(f"pip install {library}") + + # Execute the code + try: + exec_locals = {} + exec(code, {}, exec_locals) + return exec_locals.get("result", "No result variable found.") + except Exception as e: + return f"An error occurred: {str(e)}" diff --git a/crewai_tools/tools/composio_tool/README.md b/crewai_tools/tools/composio_tool/README.md new file mode 100644 index 000000000..18045e7f1 --- /dev/null +++ b/crewai_tools/tools/composio_tool/README.md @@ -0,0 +1,72 @@ +# ComposioTool Documentation + +## Description + +This tools is a wrapper around the composio toolset and gives your agent access to a wide variety of tools from the composio SDK. + +## Installation + +To incorporate this tool into your project, follow the installation instructions below: + +```shell +pip install composio-core +pip install 'crewai[tools]' +``` + +after the installation is complete, either run `composio login` or export your composio API key as `COMPOSIO_API_KEY`. + +## Example + +The following example demonstrates how to initialize the tool and execute a github action: + +1. Initialize toolset + +```python +from composio import App +from crewai_tools import ComposioTool +from crewai import Agent, Task + + +tools = [ComposioTool.from_action(action=Action.GITHUB_ACTIVITY_STAR_REPO_FOR_AUTHENTICATED_USER)] +``` + +If you don't know what action you want to use, use `from_app` and `tags` filter to get relevant actions + +```python +tools = ComposioTool.from_app(App.GITHUB, tags=["important"]) +``` + +or use `use_case` to search relevant actions + +```python +tools = ComposioTool.from_app(App.GITHUB, use_case="Star a github repository") +``` + +2. Define agent + +```python +crewai_agent = Agent( + role="Github Agent", + goal="You take action on Github using Github APIs", + backstory=( + "You are AI agent that is responsible for taking actions on Github " + "on users behalf. You need to take action on Github using Github APIs" + ), + verbose=True, + tools=tools, +) +``` + +3. Execute task + +```python +task = Task( + description="Star a repo ComposioHQ/composio on GitHub", + agent=crewai_agent, + expected_output="if the star happened", +) + +task.execute() +``` + +* More detailed list of tools can be found [here](https://app.composio.dev) diff --git a/crewai_tools/tools/composio_tool/composio_tool.py b/crewai_tools/tools/composio_tool/composio_tool.py new file mode 100644 index 000000000..019b7895c --- /dev/null +++ b/crewai_tools/tools/composio_tool/composio_tool.py @@ -0,0 +1,124 @@ +""" +Composio tools wrapper. +""" + +import typing as t + +import typing_extensions as te +from crewai.tools import BaseTool, EnvVar + + +class ComposioTool(BaseTool): + """Wrapper for composio tools.""" + + composio_action: t.Callable + env_vars: t.List[EnvVar] = [ + EnvVar(name="COMPOSIO_API_KEY", description="API key for Composio services", required=True), + ] + + def _run(self, *args: t.Any, **kwargs: t.Any) -> t.Any: + """Run the composio action with given arguments.""" + return self.composio_action(*args, **kwargs) + + @staticmethod + def _check_connected_account(tool: t.Any, toolset: t.Any) -> None: + """Check if connected account is required and if required it exists or not.""" + from composio import Action + from composio.client.collections import ConnectedAccountModel + + tool = t.cast(Action, tool) + if tool.no_auth: + return + + connections = t.cast( + t.List[ConnectedAccountModel], + toolset.client.connected_accounts.get(), + ) + if tool.app not in [connection.appUniqueId for connection in connections]: + raise RuntimeError( + f"No connected account found for app `{tool.app}`; " + f"Run `composio add {tool.app}` to fix this" + ) + + @classmethod + def from_action( + cls, + action: t.Any, + **kwargs: t.Any, + ) -> te.Self: + """Wrap a composio tool as crewAI tool.""" + + from composio import Action, ComposioToolSet + from composio.constants import DEFAULT_ENTITY_ID + from composio.utils.shared import json_schema_to_model + + toolset = ComposioToolSet() + if not isinstance(action, Action): + action = Action(action) + + action = t.cast(Action, action) + cls._check_connected_account( + tool=action, + toolset=toolset, + ) + + (action_schema,) = toolset.get_action_schemas(actions=[action]) + schema = action_schema.model_dump(exclude_none=True) + entity_id = kwargs.pop("entity_id", DEFAULT_ENTITY_ID) + + def function(**kwargs: t.Any) -> t.Dict: + """Wrapper function for composio action.""" + return toolset.execute_action( + action=Action(schema["name"]), + params=kwargs, + entity_id=entity_id, + ) + + function.__name__ = schema["name"] + function.__doc__ = schema["description"] + + return cls( + name=schema["name"], + description=schema["description"], + args_schema=json_schema_to_model( + action_schema.parameters.model_dump( + exclude_none=True, + ) + ), + composio_action=function, + **kwargs, + ) + + @classmethod + def from_app( + cls, + *apps: t.Any, + tags: t.Optional[t.List[str]] = None, + use_case: t.Optional[str] = None, + **kwargs: t.Any, + ) -> t.List[te.Self]: + """Create toolset from an app.""" + if len(apps) == 0: + raise ValueError("You need to provide at least one app name") + + if use_case is None and tags is None: + raise ValueError("Both `use_case` and `tags` cannot be `None`") + + if use_case is not None and tags is not None: + raise ValueError( + "Cannot use both `use_case` and `tags` to filter the actions" + ) + + from composio import ComposioToolSet + + toolset = ComposioToolSet() + if use_case is not None: + return [ + cls.from_action(action=action, **kwargs) + for action in toolset.find_actions_by_use_case(*apps, use_case=use_case) + ] + + return [ + cls.from_action(action=action, **kwargs) + for action in toolset.find_actions_by_tags(*apps, tags=tags) + ] diff --git a/crewai_tools/tools/contextualai_create_agent_tool/README.md b/crewai_tools/tools/contextualai_create_agent_tool/README.md new file mode 100644 index 000000000..ee08bd23c --- /dev/null +++ b/crewai_tools/tools/contextualai_create_agent_tool/README.md @@ -0,0 +1,58 @@ +# ContextualAICreateAgentTool + +## Description +This tool is designed to integrate Contextual AI's enterprise-grade RAG agents with CrewAI. This tool enables you to create a new Contextual RAG agent. It uploads your documents to create a datastore and returns the Contextual agent ID and datastore ID. + +## Installation +To incorporate this tool into your project, follow the installation instructions below: + +``` +pip install 'crewai[tools]' contextual-client +``` + +**Note**: You'll need a Contextual AI API key. Sign up at [app.contextual.ai](https://app.contextual.ai) to get your free API key. + +## Example + +```python +from crewai_tools import ContextualAICreateAgentTool + +# Initialize the tool +tool = ContextualAICreateAgentTool(api_key="your_api_key_here") + +# Create agent with documents +result = tool._run( + agent_name="Financial Analysis Agent", + agent_description="Agent for analyzing financial documents", + datastore_name="Financial Reports", + document_paths=["/path/to/report1.pdf", "/path/to/report2.pdf"], +) +print(result) +``` + +## Parameters +- `api_key`: Your Contextual AI API key +- `agent_name`: Name for the new agent +- `agent_description`: Description of the agent's purpose +- `datastore_name`: Name for the document datastore +- `document_paths`: List of file paths to upload + +Example result: + +``` +Successfully created agent 'Research Analyst' with ID: {created_agent_ID} and datastore ID: {created_datastore_ID}. Uploaded 5 documents. +``` + +You can use `ContextualAIQueryTool` with the returned IDs to query the knowledge base and retrieve relevant information from your documents. + +## Key Features +- **Complete Pipeline Setup**: Creates datastore, uploads documents, and configures agent in one operation +- **Document Processing**: Leverages Contextual AI's powerful parser to ingest complex PDFs and documents +- **Vector Storage**: Use Contextual AI's datastore for large document collections + +## Use Cases +- Set up new RAG agents from scratch with complete automation +- Upload and organize document collections into structured datastores +- Create specialized domain agents for legal, financial, technical, or research workflows + +For more detailed information about Contextual AI's capabilities, visit the [official documentation](https://docs.contextual.ai). \ No newline at end of file diff --git a/crewai_tools/tools/contextualai_create_agent_tool/contextual_create_agent_tool.py b/crewai_tools/tools/contextualai_create_agent_tool/contextual_create_agent_tool.py new file mode 100644 index 000000000..7c531273e --- /dev/null +++ b/crewai_tools/tools/contextualai_create_agent_tool/contextual_create_agent_tool.py @@ -0,0 +1,71 @@ +from typing import Any, Optional, Type, List +from crewai.tools import BaseTool +from pydantic import BaseModel, Field +import os + + +class ContextualAICreateAgentSchema(BaseModel): + """Schema for contextual create agent tool.""" + agent_name: str = Field(..., description="Name for the new agent") + agent_description: str = Field(..., description="Description for the new agent") + datastore_name: str = Field(..., description="Name for the new datastore") + document_paths: List[str] = Field(..., description="List of file paths to upload") + + +class ContextualAICreateAgentTool(BaseTool): + """Tool to create Contextual AI RAG agents with documents.""" + + name: str = "Contextual AI Create Agent Tool" + description: str = "Create a new Contextual AI RAG agent with documents and datastore" + args_schema: Type[BaseModel] = ContextualAICreateAgentSchema + + api_key: str + contextual_client: Any = None + package_dependencies: List[str] = ["contextual-client"] + + def __init__(self, **kwargs): + super().__init__(**kwargs) + try: + from contextual import ContextualAI + self.contextual_client = ContextualAI(api_key=self.api_key) + except ImportError: + raise ImportError( + "contextual-client package is required. Install it with: pip install contextual-client" + ) + + def _run( + self, + agent_name: str, + agent_description: str, + datastore_name: str, + document_paths: List[str] + ) -> str: + """Create a complete RAG pipeline with documents.""" + try: + import os + + # Create datastore + datastore = self.contextual_client.datastores.create(name=datastore_name) + datastore_id = datastore.id + + # Upload documents + document_ids = [] + for doc_path in document_paths: + if not os.path.exists(doc_path): + raise FileNotFoundError(f"Document not found: {doc_path}") + + with open(doc_path, 'rb') as f: + ingestion_result = self.contextual_client.datastores.documents.ingest(datastore_id, file=f) + document_ids.append(ingestion_result.id) + + # Create agent + agent = self.contextual_client.agents.create( + name=agent_name, + description=agent_description, + datastore_ids=[datastore_id] + ) + + return f"Successfully created agent '{agent_name}' with ID: {agent.id} and datastore ID: {datastore_id}. Uploaded {len(document_ids)} documents." + + except Exception as e: + return f"Failed to create agent with documents: {str(e)}" diff --git a/crewai_tools/tools/contextualai_parse_tool/README.md b/crewai_tools/tools/contextualai_parse_tool/README.md new file mode 100644 index 000000000..da4bc8821 --- /dev/null +++ b/crewai_tools/tools/contextualai_parse_tool/README.md @@ -0,0 +1,68 @@ +# ContextualAIParseTool + +## Description +This tool is designed to integrate Contextual AI's enterprise-grade document parsing capabilities with CrewAI, enabling you to leverage advanced AI-powered document understanding for complex layouts, tables, and figures. Use this tool to extract structured content from your documents using Contextual AI's powerful document parser. + +## Installation +To incorporate this tool into your project, follow the installation instructions below: + +``` +pip install 'crewai[tools]' contextual-client +``` + +**Note**: You'll need a Contextual AI API key. Sign up at [app.contextual.ai](https://app.contextual.ai) to get your free API key. + +## Example + +```python +from crewai_tools import ContextualAIParseTool + +tool = ContextualAIParseTool(api_key="your_api_key_here") + +result = tool._run( + file_path="/path/to/document.pdf", + parse_mode="standard", + page_range="0-5", + output_types=["markdown-per-page"] +) +print(result) +``` + +The result will show the parsed contents of your document. For example: +``` +{ + "file_name": "attention_is_all_you_need.pdf", + "status": "completed", + "pages": [ + { + "index": 0, + "markdown": "Provided proper attribution ... + }, + { + "index": 1, + "markdown": "## 1 Introduction ... + }, + ... + ] +} +``` +## Parameters +- `api_key`: Your Contextual AI API key +- `file_path`: Path to document to parse +- `parse_mode`: Parsing mode (default: "standard") +- `figure_caption_mode`: Figure caption handling (default: "concise") +- `enable_document_hierarchy`: Enable hierarchy detection (default: True) +- `page_range`: Pages to parse (e.g., "0-5", None for all) +- `output_types`: Output formats (default: ["markdown-per-page"]) + +## Key Features +- **Advanced Document Understanding**: Handles complex PDF layouts, tables, and multi-column documents +- **Figure and Table Extraction**: Intelligent extraction of figures, charts, and tabular data +- **Page Range Selection**: Parse specific pages or entire documents + +## Use Cases +- Extract structured content from complex PDFs and research papers +- Parse financial reports, legal documents, and technical manuals +- Convert documents to markdown for further processing in RAG pipelines + +For more detailed information about Contextual AI's capabilities, visit the [official documentation](https://docs.contextual.ai). \ No newline at end of file diff --git a/crewai_tools/tools/contextualai_parse_tool/contextual_parse_tool.py b/crewai_tools/tools/contextualai_parse_tool/contextual_parse_tool.py new file mode 100644 index 000000000..5985b60f1 --- /dev/null +++ b/crewai_tools/tools/contextualai_parse_tool/contextual_parse_tool.py @@ -0,0 +1,92 @@ +from typing import Any, Optional, Type, List +from crewai.tools import BaseTool +from pydantic import BaseModel, Field + + +class ContextualAIParseSchema(BaseModel): + """Schema for contextual parse tool.""" + file_path: str = Field(..., description="Path to the document to parse") + parse_mode: str = Field(default="standard", description="Parsing mode") + figure_caption_mode: str = Field(default="concise", description="Figure caption mode") + enable_document_hierarchy: bool = Field(default=True, description="Enable document hierarchy") + page_range: Optional[str] = Field(default=None, description="Page range to parse (e.g., '0-5')") + output_types: List[str] = Field(default=["markdown-per-page"], description="List of output types") + + +class ContextualAIParseTool(BaseTool): + """Tool to parse documents using Contextual AI's parser.""" + + name: str = "Contextual AI Document Parser" + description: str = "Parse documents using Contextual AI's advanced document parser" + args_schema: Type[BaseModel] = ContextualAIParseSchema + + api_key: str + package_dependencies: List[str] = ["contextual-client"] + + def _run( + self, + file_path: str, + parse_mode: str = "standard", + figure_caption_mode: str = "concise", + enable_document_hierarchy: bool = True, + page_range: Optional[str] = None, + output_types: List[str] = ["markdown-per-page"] + ) -> str: + """Parse a document using Contextual AI's parser.""" + try: + import requests + import json + import os + from time import sleep + + if not os.path.exists(file_path): + raise FileNotFoundError(f"Document not found: {file_path}") + + base_url = "https://api.contextual.ai/v1" + headers = { + "accept": "application/json", + "authorization": f"Bearer {self.api_key}" + } + + # Submit parse job + url = f"{base_url}/parse" + config = { + "parse_mode": parse_mode, + "figure_caption_mode": figure_caption_mode, + "enable_document_hierarchy": enable_document_hierarchy, + } + + if page_range: + config["page_range"] = page_range + + with open(file_path, "rb") as fp: + file = {"raw_file": fp} + result = requests.post(url, headers=headers, data=config, files=file) + response = json.loads(result.text) + job_id = response['job_id'] + + # Monitor job status + status_url = f"{base_url}/parse/jobs/{job_id}/status" + while True: + result = requests.get(status_url, headers=headers) + parse_response = json.loads(result.text)['status'] + + if parse_response == "completed": + break + elif parse_response == "failed": + raise RuntimeError("Document parsing failed") + + sleep(5) + + # Get parse results + results_url = f"{base_url}/parse/jobs/{job_id}/results" + result = requests.get( + results_url, + headers=headers, + params={"output_types": ",".join(output_types)}, + ) + + return json.dumps(json.loads(result.text), indent=2) + + except Exception as e: + return f"Failed to parse document: {str(e)}" diff --git a/crewai_tools/tools/contextualai_query_tool/README.md b/crewai_tools/tools/contextualai_query_tool/README.md new file mode 100644 index 000000000..ef939572b --- /dev/null +++ b/crewai_tools/tools/contextualai_query_tool/README.md @@ -0,0 +1,54 @@ +# ContextualAIQueryTool + +## Description +This tool is designed to integrate Contextual AI's enterprise-grade RAG agents with CrewAI. Run this tool to query existing Contextual AI RAG agents that have been pre-configured with documents and knowledge bases. + +## Installation +To incorporate this tool into your project, follow the installation instructions below: + +```shell +pip install 'crewai[tools]' contextual-client +``` + +**Note**: You'll need a Contextual AI API key. Sign up at [app.contextual.ai](https://app.contextual.ai) to get your free API key. + +## Example + +Make sure you have already created a Contextual agent and ingested documents into the datastore before using this tool. + +```python +from crewai_tools import ContextualAIQueryTool + +# Initialize the tool +tool = ContextualAIQueryTool(api_key="your_api_key_here") + +# Query the agent with IDs +result = tool._run( + query="What are the key findings in the financial report?", + agent_id="your_agent_id_here", + datastore_id="your_datastore_id_here" # Optional: for document readiness checking +) +print(result) +``` + +The result will contain the generated answer to the user's query. + +## Parameters +**Initialization:** +- `api_key`: Your Contextual AI API key + +**Query (_run method):** +- `query`: The question or query to send to the agent +- `agent_id`: ID of the existing Contextual AI agent to query (required) +- `datastore_id`: Optional datastore ID for document readiness verification (if not provided, document status checking is disabled with a warning) + +## Key Features +- **Document Readiness Checking**: Automatically waits for documents to be processed before querying +- **Grounded Responses**: Built-in grounding ensures factual, source-attributed answers + +## Use Cases +- Query pre-configured RAG agents with document collections +- Access enterprise knowledge bases through user queries +- Build specialized domain experts with access to curated documents + +For more detailed information about Contextual AI's capabilities, visit the [official documentation](https://docs.contextual.ai). \ No newline at end of file diff --git a/crewai_tools/tools/contextualai_query_tool/contextual_query_tool.py b/crewai_tools/tools/contextualai_query_tool/contextual_query_tool.py new file mode 100644 index 000000000..955ba6a39 --- /dev/null +++ b/crewai_tools/tools/contextualai_query_tool/contextual_query_tool.py @@ -0,0 +1,99 @@ +from typing import Any, Optional, Type, List +from crewai.tools import BaseTool +from pydantic import BaseModel, Field +import asyncio +import requests +import os + + +class ContextualAIQuerySchema(BaseModel): + """Schema for contextual query tool.""" + query: str = Field(..., description="Query to send to the Contextual AI agent.") + agent_id: str = Field(..., description="ID of the Contextual AI agent to query") + datastore_id: Optional[str] = Field(None, description="Optional datastore ID for document readiness verification") + + +class ContextualAIQueryTool(BaseTool): + """Tool to query Contextual AI RAG agents.""" + + name: str = "Contextual AI Query Tool" + description: str = "Use this tool to query a Contextual AI RAG agent with access to your documents" + args_schema: Type[BaseModel] = ContextualAIQuerySchema + + api_key: str + contextual_client: Any = None + package_dependencies: List[str] = ["contextual-client"] + + def __init__(self, **kwargs): + super().__init__(**kwargs) + try: + from contextual import ContextualAI + self.contextual_client = ContextualAI(api_key=self.api_key) + except ImportError: + raise ImportError( + "contextual-client package is required. Install it with: pip install contextual-client" + ) + + def _check_documents_ready(self, datastore_id: str) -> bool: + """Synchronous check if all documents are ready.""" + url = f"https://api.contextual.ai/v1/datastores/{datastore_id}/documents" + headers = {"Authorization": f"Bearer {self.api_key}"} + response = requests.get(url, headers=headers) + if response.status_code == 200: + data = response.json() + documents = data.get('documents', []) + return not any(doc.get('status') in ('processing', 'pending') for doc in documents) + return True + + async def _wait_for_documents_async(self, datastore_id: str, max_attempts: int = 20, interval: float = 30.0) -> bool: + """Asynchronously poll until documents are ready, exiting early if possible.""" + for attempt in range(max_attempts): + ready = await asyncio.to_thread(self._check_documents_ready, datastore_id) + if ready: + return True + await asyncio.sleep(interval) + print("Processing documents ...") + return True # give up but don't fail hard + + def _run(self, query: str, agent_id: str, datastore_id: Optional[str] = None) -> str: + if not agent_id: + raise ValueError("Agent ID is required to query the Contextual AI agent") + + if datastore_id: + ready = self._check_documents_ready(datastore_id) + if not ready: + try: + # If no running event loop, use asyncio.run + loop = asyncio.get_running_loop() + except RuntimeError: + loop = None + + if loop and loop.is_running(): + # Already inside an event loop + try: + import nest_asyncio + nest_asyncio.apply(loop) + loop.run_until_complete(self._wait_for_documents_async(datastore_id)) + except Exception as e: + print(f"Failed to apply nest_asyncio: {str(e)}") + else: + asyncio.run(self._wait_for_documents_async(datastore_id)) + else: + print("Warning: No datastore_id provided. Document status checking disabled.") + + try: + response = self.contextual_client.agents.query.create( + agent_id=agent_id, + messages=[{"role": "user", "content": query}] + ) + if hasattr(response, 'content'): + return response.content + elif hasattr(response, 'message'): + return response.message.content if hasattr(response.message, 'content') else str(response.message) + elif hasattr(response, 'messages') and len(response.messages) > 0: + last_message = response.messages[-1] + return last_message.content if hasattr(last_message, 'content') else str(last_message) + else: + return str(response) + except Exception as e: + return f"Error querying Contextual AI agent: {str(e)}" diff --git a/crewai_tools/tools/contextualai_rerank_tool/README.md b/crewai_tools/tools/contextualai_rerank_tool/README.md new file mode 100644 index 000000000..d8c8a9ed8 --- /dev/null +++ b/crewai_tools/tools/contextualai_rerank_tool/README.md @@ -0,0 +1,72 @@ +# ContextualAIRerankTool + +## Description +This tool is designed to integrate Contextual AI's enterprise-grade instruction-following reranker with CrewAI, enabling you to intelligently reorder documents based on relevance and custom criteria. Use this tool to enhance search result quality and document retrieval for RAG systems using Contextual AI's reranking models that understand context and follow specific instructions for optimal document ordering. + +## Installation +To incorporate this tool into your project, follow the installation instructions below: + +```shell +pip install 'crewai[tools]' contextual-client +``` + +**Note**: You'll need a Contextual AI API key. Sign up at [app.contextual.ai](https://app.contextual.ai) to get your free API key. + +## Example + +```python +from crewai_tools import ContextualAIRerankTool + +tool = ContextualAIRerankTool(api_key="your_api_key_here") + +result = tool._run( + query="financial performance and revenue metrics", + documents=[ + "Q1 report content with revenue data", + "Q2 report content with growth metrics", + "News article about market trends" + ], + instruction="Prioritize documents with specific financial metrics and quantitative data" +) +print(result) +``` + +The result will contain the document ranking. For example: +``` +Rerank Result: +{ + "results": [ + { + "index": 1, + "relevance_score": 0.88227631 + }, + { + "index": 0, + "relevance_score": 0.61159354 + }, + { + "index": 2, + "relevance_score": 0.28579462 + } + ] +} +``` + +## Parameters +- `api_key`: Your Contextual AI API key +- `query`: Search query for reranking +- `documents`: List of document texts to rerank +- `instruction`: Optional reranking instruction for custom criteria +- `metadata`: Optional metadata for each document +- `model`: Reranker model (default: "ctxl-rerank-en-v1-instruct") + +## Key Features +- **Instruction-Following Reranking**: Follows custom instructions for domain-specific document ordering +- **Metadata Integration**: Incorporates document metadata for enhanced ranking decisions + +## Use Cases +- Improve search result relevance in document collections +- Reorder documents by custom business criteria (recency, authority, relevance) +- Filter and prioritize documents for research and analysis workflows + +For more detailed information about Contextual AI's capabilities, visit the [official documentation](https://docs.contextual.ai). \ No newline at end of file diff --git a/crewai_tools/tools/contextualai_rerank_tool/contextual_rerank_tool.py b/crewai_tools/tools/contextualai_rerank_tool/contextual_rerank_tool.py new file mode 100644 index 000000000..c0bcab8a2 --- /dev/null +++ b/crewai_tools/tools/contextualai_rerank_tool/contextual_rerank_tool.py @@ -0,0 +1,68 @@ +from typing import Any, Optional, Type, List +from crewai.tools import BaseTool +from pydantic import BaseModel, Field + + +class ContextualAIRerankSchema(BaseModel): + """Schema for contextual rerank tool.""" + query: str = Field(..., description="The search query to rerank documents against") + documents: List[str] = Field(..., description="List of document texts to rerank") + instruction: Optional[str] = Field(default=None, description="Optional instruction for reranking behavior") + metadata: Optional[List[str]] = Field(default=None, description="Optional metadata for each document") + model: str = Field(default="ctxl-rerank-en-v1-instruct", description="Reranker model to use") + + +class ContextualAIRerankTool(BaseTool): + """Tool to rerank documents using Contextual AI's instruction-following reranker.""" + + name: str = "Contextual AI Document Reranker" + description: str = "Rerank documents using Contextual AI's instruction-following reranker" + args_schema: Type[BaseModel] = ContextualAIRerankSchema + + api_key: str + package_dependencies: List[str] = ["contextual-client"] + + def _run( + self, + query: str, + documents: List[str], + instruction: Optional[str] = None, + metadata: Optional[List[str]] = None, + model: str = "ctxl-rerank-en-v1-instruct" + ) -> str: + """Rerank documents using Contextual AI's instruction-following reranker.""" + try: + import requests + import json + + base_url = "https://api.contextual.ai/v1" + headers = { + "accept": "application/json", + "content-type": "application/json", + "authorization": f"Bearer {self.api_key}" + } + + payload = { + "query": query, + "documents": documents, + "model": model + } + + if instruction: + payload["instruction"] = instruction + + if metadata: + if len(metadata) != len(documents): + raise ValueError("Metadata list must have the same length as documents list") + payload["metadata"] = metadata + + rerank_url = f"{base_url}/rerank" + result = requests.post(rerank_url, json=payload, headers=headers) + + if result.status_code != 200: + raise RuntimeError(f"Reranker API returned status {result.status_code}: {result.text}") + + return json.dumps(result.json(), indent=2) + + except Exception as e: + return f"Failed to rerank documents: {str(e)}" diff --git a/crewai_tools/tools/couchbase_tool/README.md b/crewai_tools/tools/couchbase_tool/README.md new file mode 100644 index 000000000..382f6eae0 --- /dev/null +++ b/crewai_tools/tools/couchbase_tool/README.md @@ -0,0 +1,62 @@ +# CouchbaseFTSVectorSearchTool +## Description +Couchbase is a NoSQL database with vector search capabilities. Users can store and query vector embeddings. You can learn more about Couchbase vector search here: https://docs.couchbase.com/cloud/vector-search/vector-search.html + +This tool is specifically crafted for performing semantic search using Couchbase. Use this tool to find semantically similar docs to a given query. + +## Installation +Install the crewai_tools package by executing the following command in your terminal: + +```shell +uv pip install 'crewai[tools]' +``` + +## Setup +Before instantiating the tool, you need a Couchbase cluster. +- Create a cluster on [Couchbase Capella](https://docs.couchbase.com/cloud/get-started/create-account.html), Couchbase's cloud database solution. +- Create a [local Couchbase server](https://docs.couchbase.com/server/current/getting-started/start-here.html). + +You will need to create a bucket, scope and collection on the cluster. Then, [follow this guide](https://docs.couchbase.com/python-sdk/current/hello-world/start-using-sdk.html) to create a Couchbase Cluster object and load documents into your collection. + +Follow the docs below to create a vector search index on Couchbase. +- [Create a vector search index on Couchbase Capella.](https://docs.couchbase.com/cloud/vector-search/create-vector-search-index-ui.html) +- [Create a vector search index on your local Couchbase server.](https://docs.couchbase.com/server/current/vector-search/create-vector-search-index-ui.html) + +Ensure that the `Dimension` field in the index matches the embedding model. For example, OpenAI's `text-embedding-3-small` model has an embedding dimension of 1536 dimensions, and so the `Dimension` field must be 1536 in the index. + +## Example +To utilize the CouchbaseFTSVectorSearchTool for different use cases, follow these examples: + +```python +from crewai_tools import CouchbaseFTSVectorSearchTool + +# Instantiate a Couchbase Cluster object from the Couchbase SDK + +tool = CouchbaseFTSVectorSearchTool( + cluster=cluster, + collection_name="collection", + scope_name="scope", + bucket_name="bucket", + index_name="index", + embedding_function=embed_fn +) + +# Adding the tool to an agent +rag_agent = Agent( + name="rag_agent", + role="You are a helpful assistant that can answer questions with the help of the CouchbaseFTSVectorSearchTool.", + llm="gpt-4o-mini", + tools=[tool], +) +``` + +## Arguments +- `cluster`: An initialized Couchbase `Cluster` instance. +- `bucket_name`: The name of the Couchbase bucket. +- `scope_name`: The name of the scope within the bucket. +- `collection_name`: The name of the collection within the scope. +- `index_name`: The name of the search index (vector index). +- `embedding_function`: A function that takes a string and returns its embedding (list of floats). +- `embedding_key`: Name of the field in the search index storing the vector. (Optional, defaults to 'embedding') +- `scoped_index`: Whether the index is scoped (True) or cluster-level (False). (Optional, defaults to True) +- `limit`: The maximum number of search results to return. (Optional, defaults to 3) \ No newline at end of file diff --git a/crewai_tools/tools/couchbase_tool/couchbase_tool.py b/crewai_tools/tools/couchbase_tool/couchbase_tool.py new file mode 100644 index 000000000..3017f694f --- /dev/null +++ b/crewai_tools/tools/couchbase_tool/couchbase_tool.py @@ -0,0 +1,241 @@ +import json +import os +from typing import Any, Optional, Type, List, Dict, Callable + +try: + import couchbase.search as search + from couchbase.cluster import Cluster + from couchbase.options import SearchOptions + from couchbase.vector_search import VectorQuery, VectorSearch + + COUCHBASE_AVAILABLE = True +except ImportError: + COUCHBASE_AVAILABLE = False + search = Any + Cluster = Any + SearchOptions = Any + VectorQuery = Any + VectorSearch = Any + +from crewai.tools import BaseTool +from pydantic import BaseModel, Field, SkipValidation + + +class CouchbaseToolSchema(BaseModel): + """Input for CouchbaseTool.""" + + query: str = Field( + ..., + description="The query to search retrieve relevant information from the Couchbase database. Pass only the query, not the question.", + ) + +class CouchbaseFTSVectorSearchTool(BaseTool): + """Tool to search the Couchbase database""" + + model_config = {"arbitrary_types_allowed": True} + name: str = "CouchbaseFTSVectorSearchTool" + description: str = "A tool to search the Couchbase database for relevant information on internal documents." + args_schema: Type[BaseModel] = CouchbaseToolSchema + cluster: SkipValidation[Optional[Cluster]] = None + collection_name: Optional[str] = None, + scope_name: Optional[str] = None, + bucket_name: Optional[str] = None, + index_name: Optional[str] = None, + embedding_key: Optional[str] = Field( + default="embedding", + description="Name of the field in the search index that stores the vector" + ) + scoped_index: Optional[bool] = Field( + default=True, + description="Specify whether the index is scoped. Is True by default." + ), + limit: Optional[int] = Field(default=3) + embedding_function: SkipValidation[Callable[[str], List[float]]] = Field( + default=None, + description="A function that takes a string and returns a list of floats. This is used to embed the query before searching the database." + ) + + def _check_bucket_exists(self) -> bool: + """Check if the bucket exists in the linked Couchbase cluster""" + bucket_manager = self.cluster.buckets() + try: + bucket_manager.get_bucket(self.bucket_name) + return True + except Exception: + return False + + def _check_scope_and_collection_exists(self) -> bool: + """Check if the scope and collection exists in the linked Couchbase bucket + Raises a ValueError if either is not found""" + scope_collection_map: Dict[str, Any] = {} + + # Get a list of all scopes in the bucket + for scope in self._bucket.collections().get_all_scopes(): + scope_collection_map[scope.name] = [] + + # Get a list of all the collections in the scope + for collection in scope.collections: + scope_collection_map[scope.name].append(collection.name) + + # Check if the scope exists + if self.scope_name not in scope_collection_map.keys(): + raise ValueError( + f"Scope {self.scope_name} not found in Couchbase " + f"bucket {self.bucket_name}" + ) + + # Check if the collection exists in the scope + if self.collection_name not in scope_collection_map[self.scope_name]: + raise ValueError( + f"Collection {self.collection_name} not found in scope " + f"{self.scope_name} in Couchbase bucket {self.bucket_name}" + ) + + return True + + def _check_index_exists(self) -> bool: + """Check if the Search index exists in the linked Couchbase cluster + Raises a ValueError if the index does not exist""" + if self.scoped_index: + all_indexes = [ + index.name for index in self._scope.search_indexes().get_all_indexes() + ] + if self.index_name not in all_indexes: + raise ValueError( + f"Index {self.index_name} does not exist. " + " Please create the index before searching." + ) + else: + all_indexes = [ + index.name for index in self.cluster.search_indexes().get_all_indexes() + ] + if self.index_name not in all_indexes: + raise ValueError( + f"Index {self.index_name} does not exist. " + " Please create the index before searching." + ) + + return True + + def __init__(self, **kwargs): + """Initialize the CouchbaseFTSVectorSearchTool. + + Args: + **kwargs: Keyword arguments to pass to the BaseTool constructor and + to configure the Couchbase connection and search parameters. + Requires 'cluster', 'bucket_name', 'scope_name', + 'collection_name', 'index_name', and 'embedding_function'. + + Raises: + ValueError: If required parameters are missing, the Couchbase cluster + cannot be reached, or the specified bucket, scope, + collection, or index does not exist. + """ + super().__init__(**kwargs) + if COUCHBASE_AVAILABLE: + try: + if not self.cluster: + raise ValueError("Cluster instance must be provided") + + if not self.bucket_name: + raise ValueError("Bucket name must be provided") + + if not self.scope_name: + raise ValueError("Scope name must be provided") + + if not self.collection_name: + raise ValueError("Collection name must be provided") + + if not self.index_name: + raise ValueError("Index name must be provided") + + if not self.embedding_function: + raise ValueError("Embedding function must be provided") + + self._bucket = self.cluster.bucket(self.bucket_name) + self._scope = self._bucket.scope(self.scope_name) + self._collection = self._scope.collection(self.collection_name) + except Exception as e: + raise ValueError( + "Error connecting to couchbase. " + "Please check the connection and credentials" + ) from e + + # check if bucket exists + if not self._check_bucket_exists(): + raise ValueError( + f"Bucket {self.bucket_name} does not exist. " + " Please create the bucket before searching." + ) + + self._check_scope_and_collection_exists() + self._check_index_exists() + else: + import click + + if click.confirm( + "The 'couchbase' package is required to use the CouchbaseFTSVectorSearchTool. " + "Would you like to install it?" + ): + import subprocess + + subprocess.run(["uv", "add", "couchbase"], check=True) + else: + raise ImportError( + "The 'couchbase' package is required to use the CouchbaseFTSVectorSearchTool. " + "Please install it with: uv add couchbase" + ) + + def _run(self, query: str) -> str: + """Execute a vector search query against the Couchbase index. + + Args: + query: The search query string. + + Returns: + A JSON string containing the search results. + + Raises: + ValueError: If the search query fails or returns results without fields. + """ + query_embedding = self.embedding_function(query) + fields = ["*"] + + search_req = search.SearchRequest.create( + VectorSearch.from_vector_query( + VectorQuery( + self.embedding_key, + query_embedding, + self.limit + ) + ) + ) + + try: + if self.scoped_index: + search_iter = self._scope.search( + self.index_name, + search_req, + SearchOptions( + limit=self.limit, + fields=fields, + ) + ) + else: + search_iter = self.cluster.search( + self.index_name, + search_req, + SearchOptions( + limit=self.limit, + fields=fields + ) + ) + + json_response = [] + + for row in search_iter.rows(): + json_response.append(row.fields) + except Exception as e: + return f"Search failed with error: {e}" + + return json.dumps(json_response, indent=2) \ No newline at end of file diff --git a/crewai_tools/tools/crewai_enterprise_tools/crewai_enterprise_tools.py b/crewai_tools/tools/crewai_enterprise_tools/crewai_enterprise_tools.py new file mode 100644 index 000000000..f5ac47643 --- /dev/null +++ b/crewai_tools/tools/crewai_enterprise_tools/crewai_enterprise_tools.py @@ -0,0 +1,88 @@ +""" +Crewai Enterprise Tools +""" + +import os +import typing as t +import logging +import json +from crewai.tools import BaseTool +from crewai_tools.adapters.enterprise_adapter import EnterpriseActionKitToolAdapter +from crewai_tools.adapters.tool_collection import ToolCollection + +logger = logging.getLogger(__name__) + + +def CrewaiEnterpriseTools( + enterprise_token: t.Optional[str] = None, + actions_list: t.Optional[t.List[str]] = None, + enterprise_action_kit_project_id: t.Optional[str] = None, + enterprise_action_kit_project_url: t.Optional[str] = None, +) -> ToolCollection[BaseTool]: + """Factory function that returns crewai enterprise tools. + + Args: + enterprise_token: The token for accessing enterprise actions. + If not provided, will try to use CREWAI_ENTERPRISE_TOOLS_TOKEN env var. + actions_list: Optional list of specific tool names to include. + If provided, only tools with these names will be returned. + enterprise_action_kit_project_id: Optional ID of the Enterprise Action Kit project. + enterprise_action_kit_project_url: Optional URL of the Enterprise Action Kit project. + + Returns: + A ToolCollection of BaseTool instances for enterprise actions + """ + + import warnings + warnings.warn( + "CrewaiEnterpriseTools will be removed in v1.0.0. Considering use `Agent(apps=[...])` instead.", + DeprecationWarning, + stacklevel=2 + ) + + if enterprise_token is None or enterprise_token == "": + enterprise_token = os.environ.get("CREWAI_ENTERPRISE_TOOLS_TOKEN") + if not enterprise_token: + logger.warning("No enterprise token provided") + + adapter_kwargs = {"enterprise_action_token": enterprise_token} + + if enterprise_action_kit_project_id is not None: + adapter_kwargs["enterprise_action_kit_project_id"] = ( + enterprise_action_kit_project_id + ) + if enterprise_action_kit_project_url is not None: + adapter_kwargs["enterprise_action_kit_project_url"] = ( + enterprise_action_kit_project_url + ) + + adapter = EnterpriseActionKitToolAdapter(**adapter_kwargs) + all_tools = adapter.tools() + parsed_actions_list = _parse_actions_list(actions_list) + + # Filter tools based on the provided list + return ToolCollection(all_tools).filter_by_names(parsed_actions_list) + + +# ENTERPRISE INJECTION ONLY +def _parse_actions_list(actions_list: t.Optional[t.List[str]]) -> t.List[str] | None: + """Parse a string representation of a list of tool names to a list of tool names. + + Args: + actions_list: A string representation of a list of tool names. + + Returns: + A list of tool names. + """ + if actions_list is not None: + return actions_list + + actions_list_from_env = os.environ.get("CREWAI_ENTERPRISE_TOOLS_ACTIONS_LIST") + if actions_list_from_env is None: + return None + + try: + return json.loads(actions_list_from_env) + except json.JSONDecodeError: + logger.warning(f"Failed to parse actions_list as JSON: {actions_list_from_env}") + return None diff --git a/crewai_tools/tools/crewai_platform_tools/__init__.py b/crewai_tools/tools/crewai_platform_tools/__init__.py new file mode 100644 index 000000000..55db598c5 --- /dev/null +++ b/crewai_tools/tools/crewai_platform_tools/__init__.py @@ -0,0 +1,16 @@ +"""CrewAI Platform Tools + +This module provides tools for integrating with various platform applications +through the CrewAI platform API. +""" + +from crewai_tools.tools.crewai_platform_tools.crewai_platform_tools import CrewaiPlatformTools +from crewai_tools.tools.crewai_platform_tools.crewai_platform_action_tool import CrewAIPlatformActionTool +from crewai_tools.tools.crewai_platform_tools.crewai_platform_tool_builder import CrewaiPlatformToolBuilder + + +__all__ = [ + "CrewaiPlatformTools", + "CrewAIPlatformActionTool", + "CrewaiPlatformToolBuilder", +] diff --git a/crewai_tools/tools/crewai_platform_tools/crewai_platform_action_tool.py b/crewai_tools/tools/crewai_platform_tools/crewai_platform_action_tool.py new file mode 100644 index 000000000..8df877408 --- /dev/null +++ b/crewai_tools/tools/crewai_platform_tools/crewai_platform_action_tool.py @@ -0,0 +1,233 @@ +""" +Crewai Enterprise Tools +""" +import re +import json +import requests +from typing import Dict, Any, List, Type, Optional, Union, get_origin, cast, Literal +from pydantic import Field, create_model +from crewai.tools import BaseTool +from crewai_tools.tools.crewai_platform_tools.misc import get_platform_api_base_url, get_platform_integration_token + + +class CrewAIPlatformActionTool(BaseTool): + action_name: str = Field(default="", description="The name of the action") + action_schema: Dict[str, Any] = Field( + default_factory=dict, description="The schema of the action" + ) + + def __init__( + self, + description: str, + action_name: str, + action_schema: Dict[str, Any], + ): + self._model_registry = {} + self._base_name = self._sanitize_name(action_name) + + schema_props, required = self._extract_schema_info(action_schema) + + field_definitions = {} + for param_name, param_details in schema_props.items(): + param_desc = param_details.get("description", "") + is_required = param_name in required + + try: + field_type = self._process_schema_type( + param_details, self._sanitize_name(param_name).title() + ) + except Exception as e: + field_type = str + + field_definitions[param_name] = self._create_field_definition( + field_type, is_required, param_desc + ) + + if field_definitions: + try: + args_schema = create_model( + f"{self._base_name}Schema", **field_definitions + ) + except Exception as e: + print(f"Warning: Could not create main schema model: {e}") + args_schema = create_model( + f"{self._base_name}Schema", + input_text=(str, Field(description="Input for the action")), + ) + else: + args_schema = create_model( + f"{self._base_name}Schema", + input_text=(str, Field(description="Input for the action")), + ) + + super().__init__(name=action_name.lower().replace(" ", "_"), description=description, args_schema=args_schema) + self.action_name = action_name + self.action_schema = action_schema + + def _sanitize_name(self, name: str) -> str: + name = name.lower().replace(" ", "_") + sanitized = re.sub(r"[^a-zA-Z0-9_]", "", name) + parts = sanitized.split("_") + return "".join(word.capitalize() for word in parts if word) + + def _extract_schema_info( + self, action_schema: Dict[str, Any] + ) -> tuple[Dict[str, Any], List[str]]: + schema_props = ( + action_schema.get("function", {}) + .get("parameters", {}) + .get("properties", {}) + ) + required = ( + action_schema.get("function", {}).get("parameters", {}).get("required", []) + ) + return schema_props, required + + def _process_schema_type(self, schema: Dict[str, Any], type_name: str) -> Type[Any]: + if "anyOf" in schema: + any_of_types = schema["anyOf"] + is_nullable = any(t.get("type") == "null" for t in any_of_types) + non_null_types = [t for t in any_of_types if t.get("type") != "null"] + + if non_null_types: + base_type = self._process_schema_type(non_null_types[0], type_name) + return Optional[base_type] if is_nullable else base_type + return cast(Type[Any], Optional[str]) + + if "oneOf" in schema: + return self._process_schema_type(schema["oneOf"][0], type_name) + + if "allOf" in schema: + return self._process_schema_type(schema["allOf"][0], type_name) + + json_type = schema.get("type", "string") + + if "enum" in schema: + enum_values = schema["enum"] + if not enum_values: + return self._map_json_type_to_python(json_type) + return Literal[tuple(enum_values)] + + if json_type == "array": + items_schema = schema.get("items", {"type": "string"}) + item_type = self._process_schema_type(items_schema, f"{type_name}Item") + return List[item_type] + + if json_type == "object": + return self._create_nested_model(schema, type_name) + + return self._map_json_type_to_python(json_type) + + def _create_nested_model(self, schema: Dict[str, Any], model_name: str) -> Type[Any]: + full_model_name = f"{self._base_name}{model_name}" + + if full_model_name in self._model_registry: + return self._model_registry[full_model_name] + + properties = schema.get("properties", {}) + required_fields = schema.get("required", []) + + if not properties: + return dict + + field_definitions = {} + for prop_name, prop_schema in properties.items(): + prop_desc = prop_schema.get("description", "") + is_required = prop_name in required_fields + + try: + prop_type = self._process_schema_type( + prop_schema, f"{model_name}{self._sanitize_name(prop_name).title()}" + ) + except Exception as e: + prop_type = str + + field_definitions[prop_name] = self._create_field_definition( + prop_type, is_required, prop_desc + ) + + try: + nested_model = create_model(full_model_name, **field_definitions) + self._model_registry[full_model_name] = nested_model + return nested_model + except Exception as e: + print(f"Warning: Could not create nested model {full_model_name}: {e}") + return dict + + def _create_field_definition( + self, field_type: Type[Any], is_required: bool, description: str + ) -> tuple: + if is_required: + return (field_type, Field(description=description)) + else: + if get_origin(field_type) is Union: + return (field_type, Field(default=None, description=description)) + else: + return ( + Optional[field_type], + Field(default=None, description=description), + ) + + def _map_json_type_to_python(self, json_type: str) -> Type[Any]: + type_mapping = { + "string": str, + "integer": int, + "number": float, + "boolean": bool, + "array": list, + "object": dict, + "null": type(None), + } + return type_mapping.get(json_type, str) + + def _get_required_nullable_fields(self) -> List[str]: + schema_props, required = self._extract_schema_info(self.action_schema) + + required_nullable_fields = [] + for param_name in required: + param_details = schema_props.get(param_name, {}) + if self._is_nullable_type(param_details): + required_nullable_fields.append(param_name) + + return required_nullable_fields + + def _is_nullable_type(self, schema: Dict[str, Any]) -> bool: + if "anyOf" in schema: + return any(t.get("type") == "null" for t in schema["anyOf"]) + return schema.get("type") == "null" + + def _run(self, **kwargs) -> str: + try: + cleaned_kwargs = {} + for key, value in kwargs.items(): + if value is not None: + cleaned_kwargs[key] = value + + required_nullable_fields = self._get_required_nullable_fields() + + for field_name in required_nullable_fields: + if field_name not in cleaned_kwargs: + cleaned_kwargs[field_name] = None + + + api_url = f"{get_platform_api_base_url()}/actions/{self.action_name}/execute" + token = get_platform_integration_token() + headers = { + "Authorization": f"Bearer {token}", + "Content-Type": "application/json", + } + payload = cleaned_kwargs + + response = requests.post( + url=api_url, headers=headers, json=payload, timeout=60 + ) + + data = response.json() + if not response.ok: + error_message = data.get("error", {}).get("message", json.dumps(data)) + return f"API request failed: {error_message}" + + return json.dumps(data, indent=2) + + except Exception as e: + return f"Error executing action {self.action_name}: {str(e)}" diff --git a/crewai_tools/tools/crewai_platform_tools/crewai_platform_tool_builder.py b/crewai_tools/tools/crewai_platform_tools/crewai_platform_tool_builder.py new file mode 100644 index 000000000..9a8feb94c --- /dev/null +++ b/crewai_tools/tools/crewai_platform_tools/crewai_platform_tool_builder.py @@ -0,0 +1,135 @@ + +import requests +from typing import List, Any, Dict +from crewai.tools import BaseTool +from crewai_tools.tools.crewai_platform_tools.misc import get_platform_api_base_url, get_platform_integration_token +from crewai_tools.tools.crewai_platform_tools.crewai_platform_action_tool import CrewAIPlatformActionTool + + +class CrewaiPlatformToolBuilder: + def __init__( + self, + apps: list[str], + ): + self._apps = apps + self._actions_schema = {} + self._tools = None + + def tools(self) -> list[BaseTool]: + if self._tools is None: + self._fetch_actions() + self._create_tools() + return self._tools if self._tools is not None else [] + + def _fetch_actions(self): + actions_url = f"{get_platform_api_base_url()}/actions" + headers = {"Authorization": f"Bearer {get_platform_integration_token()}"} + + try: + response = requests.get( + actions_url, headers=headers, timeout=30, params={"apps": ",".join(self._apps)} + ) + response.raise_for_status() + except Exception as e: + return + + + raw_data = response.json() + + self._actions_schema = {} + action_categories = raw_data.get("actions", {}) + + for app, action_list in action_categories.items(): + if isinstance(action_list, list): + for action in action_list: + if action_name := action.get("name"): + action_schema = { + "function": { + "name": action_name, + "description": action.get("description", f"Execute {action_name}"), + "parameters": action.get("parameters", {}), + "app": app, + } + } + self._actions_schema[action_name] = action_schema + + def _generate_detailed_description( + self, schema: Dict[str, Any], indent: int = 0 + ) -> List[str]: + descriptions = [] + indent_str = " " * indent + + schema_type = schema.get("type", "string") + + if schema_type == "object": + properties = schema.get("properties", {}) + required_fields = schema.get("required", []) + + if properties: + descriptions.append(f"{indent_str}Object with properties:") + for prop_name, prop_schema in properties.items(): + prop_desc = prop_schema.get("description", "") + is_required = prop_name in required_fields + req_str = " (required)" if is_required else " (optional)" + descriptions.append( + f"{indent_str} - {prop_name}: {prop_desc}{req_str}" + ) + + if prop_schema.get("type") == "object": + descriptions.extend( + self._generate_detailed_description(prop_schema, indent + 2) + ) + elif prop_schema.get("type") == "array": + items_schema = prop_schema.get("items", {}) + if items_schema.get("type") == "object": + descriptions.append(f"{indent_str} Array of objects:") + descriptions.extend( + self._generate_detailed_description( + items_schema, indent + 3 + ) + ) + elif "enum" in items_schema: + descriptions.append( + f"{indent_str} Array of enum values: {items_schema['enum']}" + ) + elif "enum" in prop_schema: + descriptions.append( + f"{indent_str} Enum values: {prop_schema['enum']}" + ) + + return descriptions + + def _create_tools(self): + tools = [] + + for action_name, action_schema in self._actions_schema.items(): + function_details = action_schema.get("function", {}) + description = function_details.get("description", f"Execute {action_name}") + + parameters = function_details.get("parameters", {}) + param_descriptions = [] + + if parameters.get("properties"): + param_descriptions.append("\nDetailed Parameter Structure:") + param_descriptions.extend( + self._generate_detailed_description(parameters) + ) + + full_description = description + "\n".join(param_descriptions) + + tool = CrewAIPlatformActionTool( + description=full_description, + action_name=action_name, + action_schema=action_schema, + ) + + tools.append(tool) + + self._tools = tools + + + def __enter__(self): + return self.tools() + + def __exit__(self, exc_type, exc_val, exc_tb): + pass diff --git a/crewai_tools/tools/crewai_platform_tools/crewai_platform_tools.py b/crewai_tools/tools/crewai_platform_tools/crewai_platform_tools.py new file mode 100644 index 000000000..8bfa1073a --- /dev/null +++ b/crewai_tools/tools/crewai_platform_tools/crewai_platform_tools.py @@ -0,0 +1,28 @@ +import re +import os +import typing as t +from typing import Literal +import logging +import json +from crewai.tools import BaseTool +from crewai_tools.tools.crewai_platform_tools.crewai_platform_tool_builder import CrewaiPlatformToolBuilder +from crewai_tools.adapters.tool_collection import ToolCollection + +logger = logging.getLogger(__name__) + + + +def CrewaiPlatformTools( + apps: list[str], +) -> ToolCollection[BaseTool]: + """Factory function that returns crewai platform tools. + Args: + apps: List of platform apps to get tools that are available on the platform. + + Returns: + A list of BaseTool instances for platform actions + """ + + builder = CrewaiPlatformToolBuilder(apps=apps) + + return builder.tools() diff --git a/crewai_tools/tools/crewai_platform_tools/misc.py b/crewai_tools/tools/crewai_platform_tools/misc.py new file mode 100644 index 000000000..0839719d7 --- /dev/null +++ b/crewai_tools/tools/crewai_platform_tools/misc.py @@ -0,0 +1,13 @@ +import os + +def get_platform_api_base_url() -> str: + """Get the platform API base URL from environment or use default.""" + base_url = os.getenv("CREWAI_PLUS_URL", "https://app.crewai.com") + return f"{base_url}/crewai_plus/api/v1/integrations" + +def get_platform_integration_token() -> str: + """Get the platform API base URL from environment or use default.""" + token = os.getenv("CREWAI_PLATFORM_INTEGRATION_TOKEN") or "" + if not token: + raise ValueError("No platform integration token found, please set the CREWAI_PLATFORM_INTEGRATION_TOKEN environment variable") + return token # TODO: Use context manager to get token diff --git a/crewai_tools/tools/csv_search_tool/README.md b/crewai_tools/tools/csv_search_tool/README.md new file mode 100644 index 000000000..c0bcbae3d --- /dev/null +++ b/crewai_tools/tools/csv_search_tool/README.md @@ -0,0 +1,59 @@ +# CSVSearchTool + +## Description + +This tool is used to perform a RAG (Retrieval-Augmented Generation) search within a CSV file's content. It allows users to semantically search for queries in the content of a specified CSV file. This feature is particularly useful for extracting information from large CSV datasets where traditional search methods might be inefficient. All tools with "Search" in their name, including CSVSearchTool, are RAG tools designed for searching different sources of data. + +## Installation + +Install the crewai_tools package + +```shell +pip install 'crewai[tools]' +``` + +## Example + +```python +from crewai_tools import CSVSearchTool + +# Initialize the tool with a specific CSV file. This setup allows the agent to only search the given CSV file. +tool = CSVSearchTool(csv='path/to/your/csvfile.csv') + +# OR + +# Initialize the tool without a specific CSV file. Agent will need to provide the CSV path at runtime. +tool = CSVSearchTool() +``` + +## Arguments + +- `csv` : The path to the CSV file you want to search. This is a mandatory argument if the tool was initialized without a specific CSV file; otherwise, it is optional. + +## Custom model and embeddings + +By default, the tool uses OpenAI for both embeddings and summarization. To customize the model, you can use a config dictionary as follows: + +```python +tool = CSVSearchTool( + config=dict( + llm=dict( + provider="ollama", # or google, openai, anthropic, llama2, ... + config=dict( + model="llama2", + # temperature=0.5, + # top_p=1, + # stream=true, + ), + ), + embedder=dict( + provider="google", + config=dict( + model="models/embedding-001", + task_type="retrieval_document", + # title="Embeddings", + ), + ), + ) +) +``` diff --git a/crewai_tools/tools/csv_search_tool/csv_search_tool.py b/crewai_tools/tools/csv_search_tool/csv_search_tool.py new file mode 100644 index 000000000..4be84efdd --- /dev/null +++ b/crewai_tools/tools/csv_search_tool/csv_search_tool.py @@ -0,0 +1,56 @@ +from typing import Optional, Type + +try: + from embedchain.models.data_type import DataType + EMBEDCHAIN_AVAILABLE = True +except ImportError: + EMBEDCHAIN_AVAILABLE = False + +from pydantic import BaseModel, Field + +from ..rag.rag_tool import RagTool + + +class FixedCSVSearchToolSchema(BaseModel): + """Input for CSVSearchTool.""" + + search_query: str = Field( + ..., + description="Mandatory search query you want to use to search the CSV's content", + ) + + +class CSVSearchToolSchema(FixedCSVSearchToolSchema): + """Input for CSVSearchTool.""" + + csv: str = Field(..., description="File path or URL of a CSV file to be searched") + + +class CSVSearchTool(RagTool): + name: str = "Search a CSV's content" + description: str = ( + "A tool that can be used to semantic search a query from a CSV's content." + ) + args_schema: Type[BaseModel] = CSVSearchToolSchema + + def __init__(self, csv: Optional[str] = None, **kwargs): + super().__init__(**kwargs) + if csv is not None: + self.add(csv) + self.description = f"A tool that can be used to semantic search a query the {csv} CSV's content." + self.args_schema = FixedCSVSearchToolSchema + self._generate_description() + + def add(self, csv: str) -> None: + if not EMBEDCHAIN_AVAILABLE: + raise ImportError("embedchain is not installed. Please install it with `pip install crewai-tools[embedchain]`") + super().add(csv, data_type=DataType.CSV) + + def _run( + self, + search_query: str, + csv: Optional[str] = None, + ) -> str: + if csv is not None: + self.add(csv) + return super()._run(query=search_query) diff --git a/crewai_tools/tools/dalle_tool/README.MD b/crewai_tools/tools/dalle_tool/README.MD new file mode 100644 index 000000000..8f65e78e5 --- /dev/null +++ b/crewai_tools/tools/dalle_tool/README.MD @@ -0,0 +1,41 @@ +# DALL-E Tool + +## Description +This tool is used to give the Agent the ability to generate images using the DALL-E model. It is a transformer-based model that generates images from textual descriptions. This tool allows the Agent to generate images based on the text input provided by the user. + +## Installation +Install the crewai_tools package +```shell +pip install 'crewai[tools]' +``` + +## Example + +Remember that when using this tool, the text must be generated by the Agent itself. The text must be a description of the image you want to generate. + +```python +from crewai_tools import DallETool + +Agent( + ... + tools=[DallETool()], +) +``` + +If needed you can also tweak the parameters of the DALL-E model by passing them as arguments to the `DallETool` class. For example: + +```python +from crewai_tools import DallETool + +dalle_tool = DallETool(model="dall-e-3", + size="1024x1024", + quality="standard", + n=1) + +Agent( + ... + tools=[dalle_tool] +) +``` + +The parameters are based on the `client.images.generate` method from the OpenAI API. For more information on the parameters, please refer to the [OpenAI API documentation](https://platform.openai.com/docs/guides/images/introduction?lang=python). diff --git a/crewai_tools/tools/dalle_tool/dalle_tool.py b/crewai_tools/tools/dalle_tool/dalle_tool.py new file mode 100644 index 000000000..6a4a9e84f --- /dev/null +++ b/crewai_tools/tools/dalle_tool/dalle_tool.py @@ -0,0 +1,52 @@ +import json +from typing import List, Type + +from crewai.tools import BaseTool, EnvVar +from openai import OpenAI +from pydantic import BaseModel, Field + + +class ImagePromptSchema(BaseModel): + """Input for Dall-E Tool.""" + + image_description: str = Field(description="Description of the image to be generated by Dall-E.") + + +class DallETool(BaseTool): + name: str = "Dall-E Tool" + description: str = "Generates images using OpenAI's Dall-E model." + args_schema: Type[BaseModel] = ImagePromptSchema + + model: str = "dall-e-3" + size: str = "1024x1024" + quality: str = "standard" + n: int = 1 + + env_vars: List[EnvVar] = [ + EnvVar(name="OPENAI_API_KEY", description="API key for OpenAI services", required=True), + ] + + def _run(self, **kwargs) -> str: + client = OpenAI() + + image_description = kwargs.get("image_description") + + if not image_description: + return "Image description is required." + + response = client.images.generate( + model=self.model, + prompt=image_description, + size=self.size, + quality=self.quality, + n=self.n, + ) + + image_data = json.dumps( + { + "image_url": response.data[0].url, + "image_description": response.data[0].revised_prompt, + } + ) + + return image_data diff --git a/crewai_tools/tools/databricks_query_tool/README.md b/crewai_tools/tools/databricks_query_tool/README.md new file mode 100644 index 000000000..b5f4880c6 --- /dev/null +++ b/crewai_tools/tools/databricks_query_tool/README.md @@ -0,0 +1,66 @@ +# Databricks Query Tool + +## Description + +This tool allows AI agents to execute SQL queries against Databricks workspace tables and retrieve the results. It provides a simple interface for querying data from Databricks tables using SQL, making it easy for agents to access and analyze data stored in Databricks. + +## Installation + +Install the crewai_tools package with the databricks extra: + +```shell +pip install 'crewai[tools]' 'databricks-sdk' +``` + +## Authentication + +The tool requires Databricks authentication credentials. You can provide these in two ways: + +1. **Using Databricks CLI profile**: + - Set the `DATABRICKS_CONFIG_PROFILE` environment variable to your profile name. + +2. **Using direct credentials**: + - Set both `DATABRICKS_HOST` and `DATABRICKS_TOKEN` environment variables. + +Example: +```shell +export DATABRICKS_HOST="https://your-workspace.cloud.databricks.com" +export DATABRICKS_TOKEN="dapi1234567890abcdef" +``` + +## Usage + +```python +from crewai_tools import DatabricksQueryTool + +# Basic usage +databricks_tool = DatabricksQueryTool() + +# With default parameters for catalog, schema, and warehouse +databricks_tool = DatabricksQueryTool( + default_catalog="my_catalog", + default_schema="my_schema", + default_warehouse_id="warehouse_id" +) + +# Example in a CrewAI agent +@agent +def data_analyst(self) -> Agent: + return Agent( + config=self.agents_config["data_analyst"], + allow_delegation=False, + tools=[databricks_tool] + ) +``` + +## Parameters + +When executing queries, you can provide the following parameters: + +- `query` (required): SQL query to execute against the Databricks workspace +- `catalog` (optional): Databricks catalog name +- `schema` (optional): Databricks schema name +- `warehouse_id` (optional): Databricks SQL warehouse ID +- `row_limit` (optional): Maximum number of rows to return (default: 1000) + +If not provided, the tool will use the default values set during initialization. \ No newline at end of file diff --git a/crewai_tools/tools/databricks_query_tool/__init__.py b/crewai_tools/tools/databricks_query_tool/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/crewai_tools/tools/databricks_query_tool/databricks_query_tool.py b/crewai_tools/tools/databricks_query_tool/databricks_query_tool.py new file mode 100644 index 000000000..fe73179cb --- /dev/null +++ b/crewai_tools/tools/databricks_query_tool/databricks_query_tool.py @@ -0,0 +1,670 @@ +import os +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Type, Union + +from crewai.tools import BaseTool +from pydantic import BaseModel, Field, model_validator + +if TYPE_CHECKING: + from databricks.sdk import WorkspaceClient + +class DatabricksQueryToolSchema(BaseModel): + """Input schema for DatabricksQueryTool.""" + + query: str = Field( + ..., description="SQL query to execute against the Databricks workspace table" + ) + catalog: Optional[str] = Field( + None, description="Databricks catalog name (optional, defaults to configured catalog)" + ) + db_schema: Optional[str] = Field( + None, description="Databricks schema name (optional, defaults to configured schema)" + ) + warehouse_id: Optional[str] = Field( + None, description="Databricks SQL warehouse ID (optional, defaults to configured warehouse)" + ) + row_limit: Optional[int] = Field( + 1000, description="Maximum number of rows to return (default: 1000)" + ) + + @model_validator(mode='after') + def validate_input(self) -> 'DatabricksQueryToolSchema': + """Validate the input parameters.""" + # Ensure the query is not empty + if not self.query or not self.query.strip(): + raise ValueError("Query cannot be empty") + + # Add a LIMIT clause to the query if row_limit is provided and query doesn't have one + if self.row_limit and "limit" not in self.query.lower(): + self.query = f"{self.query.rstrip(';')} LIMIT {self.row_limit};" + + return self + + +class DatabricksQueryTool(BaseTool): + """ + A tool for querying Databricks workspace tables using SQL. + + This tool executes SQL queries against Databricks tables and returns the results. + It requires Databricks authentication credentials to be set as environment variables. + + Authentication can be provided via: + - Databricks CLI profile: Set DATABRICKS_CONFIG_PROFILE environment variable + - Direct credentials: Set DATABRICKS_HOST and DATABRICKS_TOKEN environment variables + + Example: + >>> tool = DatabricksQueryTool() + >>> results = tool.run(query="SELECT * FROM my_table LIMIT 10") + """ + + name: str = "Databricks SQL Query" + description: str = ( + "Execute SQL queries against Databricks workspace tables and return the results." + " Provide a 'query' parameter with the SQL query to execute." + ) + args_schema: Type[BaseModel] = DatabricksQueryToolSchema + + # Optional default parameters + default_catalog: Optional[str] = None + default_schema: Optional[str] = None + default_warehouse_id: Optional[str] = None + + _workspace_client: Optional["WorkspaceClient"] = None + package_dependencies: List[str] = ["databricks-sdk"] + + def __init__( + self, + default_catalog: Optional[str] = None, + default_schema: Optional[str] = None, + default_warehouse_id: Optional[str] = None, + **kwargs: Any, + ) -> None: + """ + Initialize the DatabricksQueryTool. + + Args: + default_catalog (Optional[str]): Default catalog to use for queries. + default_schema (Optional[str]): Default schema to use for queries. + default_warehouse_id (Optional[str]): Default SQL warehouse ID to use. + **kwargs: Additional keyword arguments passed to BaseTool. + """ + super().__init__(**kwargs) + self.default_catalog = default_catalog + self.default_schema = default_schema + self.default_warehouse_id = default_warehouse_id + self._validate_credentials() + + def _validate_credentials(self) -> None: + """Validate that Databricks credentials are available.""" + has_profile = "DATABRICKS_CONFIG_PROFILE" in os.environ + has_direct_auth = "DATABRICKS_HOST" in os.environ and "DATABRICKS_TOKEN" in os.environ + + if not (has_profile or has_direct_auth): + raise ValueError( + "Databricks authentication credentials are required. " + "Set either DATABRICKS_CONFIG_PROFILE or both DATABRICKS_HOST and DATABRICKS_TOKEN environment variables." + ) + + @property + def workspace_client(self) -> "WorkspaceClient": + """Get or create a Databricks WorkspaceClient instance.""" + if self._workspace_client is None: + try: + from databricks.sdk import WorkspaceClient + self._workspace_client = WorkspaceClient() + except ImportError: + raise ImportError( + "`databricks-sdk` package not found, please run `uv add databricks-sdk`" + ) + return self._workspace_client + + def _format_results(self, results: List[Dict[str, Any]]) -> str: + """Format query results as a readable string.""" + if not results: + return "Query returned no results." + + # Get column names from the first row + if not results[0]: + return "Query returned empty rows with no columns." + + columns = list(results[0].keys()) + + # If we have rows but they're all empty, handle that case + if not columns: + return "Query returned rows but with no column data." + + # Calculate column widths based on data + col_widths = {col: len(col) for col in columns} + for row in results: + for col in columns: + # Convert value to string and get its length + # Handle None values gracefully + value_str = str(row[col]) if row[col] is not None else "NULL" + col_widths[col] = max(col_widths[col], len(value_str)) + + # Create header row + header = " | ".join(f"{col:{col_widths[col]}}" for col in columns) + separator = "-+-".join("-" * col_widths[col] for col in columns) + + # Format data rows + data_rows = [] + for row in results: + # Handle None values by displaying "NULL" + row_values = {col: str(row[col]) if row[col] is not None else "NULL" for col in columns} + data_row = " | ".join(f"{row_values[col]:{col_widths[col]}}" for col in columns) + data_rows.append(data_row) + + # Add row count information + result_info = f"({len(results)} row{'s' if len(results) != 1 else ''} returned)" + + # Combine all parts + return f"{header}\n{separator}\n" + "\n".join(data_rows) + f"\n\n{result_info}" + + def _run( + self, + **kwargs: Any, + ) -> str: + """ + Execute a SQL query against Databricks and return the results. + + Args: + query (str): SQL query to execute + catalog (Optional[str]): Databricks catalog name + db_schema (Optional[str]): Databricks schema name + warehouse_id (Optional[str]): SQL warehouse ID + row_limit (Optional[int]): Maximum number of rows to return + + Returns: + str: Formatted query results + """ + try: + # Get parameters with fallbacks to default values + query = kwargs.get("query") + catalog = kwargs.get("catalog") or self.default_catalog + db_schema = kwargs.get("db_schema") or self.default_schema + warehouse_id = kwargs.get("warehouse_id") or self.default_warehouse_id + row_limit = kwargs.get("row_limit", 1000) + + # Validate schema and query + validated_input = DatabricksQueryToolSchema( + query=query, + catalog=catalog, + db_schema=db_schema, + warehouse_id=warehouse_id, + row_limit=row_limit + ) + + # Extract validated parameters + query = validated_input.query + catalog = validated_input.catalog + db_schema = validated_input.db_schema + warehouse_id = validated_input.warehouse_id + + # Setup SQL context with catalog/schema if provided + context = {} + if catalog: + context["catalog"] = catalog + if db_schema: + context["schema"] = db_schema + + # Execute query + statement = self.workspace_client.statement_execution + + try: + # Execute the statement + execution = statement.execute_statement( + warehouse_id=warehouse_id, + statement=query, + **context + ) + + statement_id = execution.statement_id + except Exception as execute_error: + # Handle immediate execution errors + return f"Error starting query execution: {str(execute_error)}" + + # Poll for results with better error handling + import time + result = None + timeout = 300 # 5 minutes timeout + start_time = time.time() + poll_count = 0 + previous_state = None # Track previous state to detect changes + + while time.time() - start_time < timeout: + poll_count += 1 + try: + # Get statement status + result = statement.get_statement(statement_id) + + # Check if finished - be very explicit about state checking + if hasattr(result, 'status') and hasattr(result.status, 'state'): + state_value = str(result.status.state) # Convert to string to handle both string and enum + + # Track state changes for debugging + if previous_state != state_value: + previous_state = state_value + + # Check if state indicates completion + if "SUCCEEDED" in state_value: + break + elif "FAILED" in state_value: + # Extract error message with more robust handling + error_info = "No detailed error info" + try: + # First try direct access to error.message + if hasattr(result.status, 'error') and result.status.error: + if hasattr(result.status.error, 'message'): + error_info = result.status.error.message + # Some APIs may have a different structure + elif hasattr(result.status.error, 'error_message'): + error_info = result.status.error.error_message + # Last resort, try to convert the whole error object to string + else: + error_info = str(result.status.error) + except Exception as err_extract_error: + # If all else fails, try to get any info we can + error_info = f"Error details unavailable: {str(err_extract_error)}" + + # Return immediately on first FAILED state detection + return f"Query execution failed: {error_info}" + elif "CANCELED" in state_value: + return "Query was canceled" + + except Exception as poll_error: + # Don't immediately fail - try again a few times + if poll_count > 3: + return f"Error checking query status: {str(poll_error)}" + + # Wait before polling again + time.sleep(2) + + # Check if we timed out + if result is None: + return "Query returned no result (likely timed out or failed)" + + if not hasattr(result, 'status') or not hasattr(result.status, 'state'): + return "Query completed but returned an invalid result structure" + + # Convert state to string for comparison + state_value = str(result.status.state) + if not any(state in state_value for state in ["SUCCEEDED", "FAILED", "CANCELED"]): + return f"Query timed out after 5 minutes (last state: {state_value})" + + # Get results - adapt this based on the actual structure of the result object + chunk_results = [] + + # Check if we have results and a schema in a very defensive way + has_schema = (hasattr(result, 'manifest') and result.manifest is not None and + hasattr(result.manifest, 'schema') and result.manifest.schema is not None) + has_result = (hasattr(result, 'result') and result.result is not None) + + if has_schema and has_result: + try: + # Get schema for column names + columns = [col.name for col in result.manifest.schema.columns] + + # Debug info for schema + + # Keep track of all dynamic columns we create + all_columns = set(columns) + + # Dump the raw structure of result data to help troubleshoot + if hasattr(result.result, 'data_array'): + # Add defensive check for None data_array + if result.result.data_array is None: + print("data_array is None - likely an empty result set or DDL query") + # Return empty result handling rather than trying to process null data + return "Query executed successfully (no data returned)" + + # IMPROVED DETECTION LOGIC: Check if we're possibly dealing with rows where each item + # contains a single value or character (which could indicate incorrect row structure) + is_likely_incorrect_row_structure = False + + # Only try to analyze sample if data_array exists and has content + if hasattr(result.result, 'data_array') and result.result.data_array and len(result.result.data_array) > 0 and len(result.result.data_array[0]) > 0: + sample_size = min(20, len(result.result.data_array[0])) + + if sample_size > 0: + single_char_count = 0 + single_digit_count = 0 + total_items = 0 + + for i in range(sample_size): + val = result.result.data_array[0][i] + total_items += 1 + if isinstance(val, str) and len(val) == 1 and not val.isdigit(): + single_char_count += 1 + elif isinstance(val, str) and len(val) == 1 and val.isdigit(): + single_digit_count += 1 + + # If a significant portion of the first values are single characters or digits, + # this likely indicates data is being incorrectly structured + if total_items > 0 and (single_char_count + single_digit_count) / total_items > 0.5: + is_likely_incorrect_row_structure = True + + # Additional check: if many rows have just 1 item when we expect multiple columns + rows_with_single_item = 0 + if hasattr(result.result, 'data_array') and result.result.data_array and len(result.result.data_array) > 0: + sample_size_for_rows = min(sample_size, len(result.result.data_array[0])) if 'sample_size' in locals() else min(20, len(result.result.data_array[0])) + rows_with_single_item = sum(1 for row in result.result.data_array[0][:sample_size_for_rows] if isinstance(row, list) and len(row) == 1) + if rows_with_single_item > sample_size_for_rows * 0.5 and len(columns) > 1: + is_likely_incorrect_row_structure = True + + # Check if we're getting primarily single characters or the data structure seems off, + # we should use special handling + if 'is_likely_incorrect_row_structure' in locals() and is_likely_incorrect_row_structure: + print("Data appears to be malformed - will use special row reconstruction") + needs_special_string_handling = True + else: + needs_special_string_handling = False + + # Process results differently based on detection + if 'needs_special_string_handling' in locals() and needs_special_string_handling: + # We're dealing with data where the rows may be incorrectly structured + print("Using row reconstruction processing mode") + + # Collect all values into a flat list + all_values = [] + if hasattr(result.result, 'data_array') and result.result.data_array: + # Flatten all values into a single list + for chunk in result.result.data_array: + for item in chunk: + if isinstance(item, (list, tuple)): + all_values.extend(item) + else: + all_values.append(item) + + # Get the expected column count from schema + expected_column_count = len(columns) + + # Try to reconstruct rows using pattern recognition + reconstructed_rows = [] + + # PATTERN RECOGNITION APPROACH + # Look for likely indicators of row boundaries in the data + # For Netflix data, we expect IDs as numbers, titles as text strings, etc. + + # Use regex pattern to identify ID columns that likely start a new row + import re + id_pattern = re.compile(r'^\d{5,9}$') # Netflix IDs are often 5-9 digits + id_indices = [] + + for i, val in enumerate(all_values): + if isinstance(val, str) and id_pattern.match(val): + # This value looks like an ID, might be the start of a row + if i < len(all_values) - 1: + next_few_values = all_values[i+1:i+5] + # If following values look like they could be part of a title + if any(isinstance(v, str) and len(v) > 1 for v in next_few_values): + id_indices.append(i) + + if id_indices: + + # If we found potential row starts, use them to extract rows + for i in range(len(id_indices)): + start_idx = id_indices[i] + end_idx = id_indices[i+1] if i+1 < len(id_indices) else len(all_values) + + # Extract values for this row + row_values = all_values[start_idx:end_idx] + + # Special handling for Netflix title data + # Titles might be split into individual characters + if 'Title' in columns and len(row_values) > expected_column_count: + + # Try to reconstruct by looking for patterns + # We know ID is first, then Title (which may be split) + # Then other fields like Genre, etc. + + # Take first value as ID + row_dict = {columns[0]: row_values[0]} + + # Look for Genre or other non-title fields to determine where title ends + title_end_idx = 1 + for j in range(2, min(100, len(row_values))): + val = row_values[j] + # Check for common genres or non-title markers + if isinstance(val, str) and val in ['Comedy', 'Drama', 'Action', 'Horror', 'Thriller', 'Documentary']: + # Likely found the Genre field + title_end_idx = j + break + + # Reconstruct title from individual characters + if title_end_idx > 1: + title_chars = row_values[1:title_end_idx] + # Check if they're individual characters + if all(isinstance(c, str) and len(c) == 1 for c in title_chars): + title = ''.join(title_chars) + row_dict['Title'] = title + + # Assign remaining values to columns + remaining_values = row_values[title_end_idx:] + for j, col_name in enumerate(columns[2:], 2): + if j-2 < len(remaining_values): + row_dict[col_name] = remaining_values[j-2] + else: + row_dict[col_name] = None + else: + # Fallback: simple mapping + for j, col_name in enumerate(columns): + if j < len(row_values): + row_dict[col_name] = row_values[j] + else: + row_dict[col_name] = None + else: + # Standard mapping + row_dict = {} + for j, col_name in enumerate(columns): + if j < len(row_values): + row_dict[col_name] = row_values[j] + else: + row_dict[col_name] = None + + reconstructed_rows.append(row_dict) + else: + # More intelligent chunking - try to detect where columns like Title might be split + title_idx = columns.index('Title') if 'Title' in columns else -1 + + if title_idx >= 0: + print("Attempting title reconstruction method") + # Try to detect if title is split across multiple values + i = 0 + while i < len(all_values): + # Check if this could be an ID (start of a row) + if isinstance(all_values[i], str) and id_pattern.match(all_values[i]): + row_dict = {columns[0]: all_values[i]} + i += 1 + + # Try to reconstruct title if it appears to be split + title_chars = [] + while (i < len(all_values) and + isinstance(all_values[i], str) and + len(all_values[i]) <= 1 and + len(title_chars) < 100): # Cap title length + title_chars.append(all_values[i]) + i += 1 + + if title_chars: + row_dict[columns[title_idx]] = ''.join(title_chars) + + # Add remaining fields + for j in range(title_idx + 1, len(columns)): + if i < len(all_values): + row_dict[columns[j]] = all_values[i] + i += 1 + else: + row_dict[columns[j]] = None + + reconstructed_rows.append(row_dict) + else: + i += 1 + + # If we still don't have rows, use simple chunking as fallback + if not reconstructed_rows: + print("Falling back to basic chunking approach") + chunks = [all_values[i:i+expected_column_count] for i in range(0, len(all_values), expected_column_count)] + + for chunk in chunks: + # Skip chunks that seem to be partial/incomplete rows + if len(chunk) < expected_column_count * 0.75: # Allow for some missing values + continue + + row_dict = {} + + # Map values to column names + for i, col in enumerate(columns): + if i < len(chunk): + row_dict[col] = chunk[i] + else: + row_dict[col] = None + + reconstructed_rows.append(row_dict) + + # Apply post-processing to fix known issues + if reconstructed_rows and 'Title' in columns: + print("Applying post-processing to improve data quality") + for row in reconstructed_rows: + # Fix titles that might still have issues + if isinstance(row.get('Title'), str) and len(row.get('Title')) <= 1: + # This is likely still a fragmented title - mark as potentially incomplete + row['Title'] = f"[INCOMPLETE] {row.get('Title')}" + + # Ensure we respect the row limit + if row_limit and len(reconstructed_rows) > row_limit: + reconstructed_rows = reconstructed_rows[:row_limit] + + chunk_results = reconstructed_rows + else: + # Process normal result structure as before + print("Using standard processing mode") + + # Check different result structures + if hasattr(result.result, 'data_array') and result.result.data_array: + # Check if data appears to be malformed within chunks + for chunk_idx, chunk in enumerate(result.result.data_array): + + # Check if chunk might actually contain individual columns of a single row + # This is another way data might be malformed - check the first few values + if len(chunk) > 0 and len(columns) > 1: + # If there seems to be a mismatch between chunk structure and expected columns + first_few_values = chunk[:min(5, len(chunk))] + if all(isinstance(val, (str, int, float)) and not isinstance(val, (list, dict)) for val in first_few_values): + if len(chunk) > len(columns) * 3: # Heuristic: if chunk has way more items than columns + print("Chunk appears to contain individual values rather than rows - switching to row reconstruction") + + # This chunk might actually be values of multiple rows - try to reconstruct + values = chunk # All values in this chunk + reconstructed_rows = [] + + # Try to create rows based on expected column count + for i in range(0, len(values), len(columns)): + if i + len(columns) <= len(values): # Ensure we have enough values + row_values = values[i:i+len(columns)] + row_dict = {col: val for col, val in zip(columns, row_values)} + reconstructed_rows.append(row_dict) + + if reconstructed_rows: + chunk_results.extend(reconstructed_rows) + continue # Skip normal processing for this chunk + + # Special case: when chunk contains exactly the right number of values for a single row + # This handles the case where instead of a list of rows, we just got all values in a flat list + if all(isinstance(val, (str, int, float)) and not isinstance(val, (list, dict)) for val in chunk): + if len(chunk) == len(columns) or (len(chunk) > 0 and len(chunk) % len(columns) == 0): + + # Process flat list of values as rows + for i in range(0, len(chunk), len(columns)): + row_values = chunk[i:i+len(columns)] + if len(row_values) == len(columns): # Only process complete rows + row_dict = {col: val for col, val in zip(columns, row_values)} + chunk_results.append(row_dict) + + # Skip regular row processing for this chunk + continue + + # Normal processing for typical row structure + for row_idx, row in enumerate(chunk): + # Ensure row is actually a collection of values + if not isinstance(row, (list, tuple, dict)): + # This might be a single value; skip it or handle specially + continue + + # Convert each row to a dictionary with column names as keys + row_dict = {} + + # Handle dict rows directly + if isinstance(row, dict): + # Use the existing column mapping + row_dict = dict(row) + elif isinstance(row, (list, tuple)): + # Map list of values to columns + for i, val in enumerate(row): + if i < len(columns): # Only process if we have a matching column + row_dict[columns[i]] = val + else: + # Extra values without column names + dynamic_col = f"Column_{i}" + row_dict[dynamic_col] = val + all_columns.add(dynamic_col) + + # If we have fewer values than columns, set missing values to None + for col in columns: + if col not in row_dict: + row_dict[col] = None + + chunk_results.append(row_dict) + + elif hasattr(result.result, 'data') and result.result.data: + # Alternative data structure + + for row_idx, row in enumerate(result.result.data): + # Debug info + + # Safely create dictionary matching column names to values + row_dict = {} + for i, val in enumerate(row): + if i < len(columns): # Only process if we have a matching column + row_dict[columns[i]] = val + else: + # Extra values without column names + dynamic_col = f"Column_{i}" + row_dict[dynamic_col] = val + all_columns.add(dynamic_col) + + # If we have fewer values than columns, set missing values to None + for i, col in enumerate(columns): + if i >= len(row): + row_dict[col] = None + + chunk_results.append(row_dict) + + # After processing all rows, ensure all rows have all columns + normalized_results = [] + for row in chunk_results: + # Create a new row with all columns, defaulting to None for missing ones + normalized_row = {col: row.get(col, None) for col in all_columns} + normalized_results.append(normalized_row) + + # Replace the original results with normalized ones + chunk_results = normalized_results + + except Exception as results_error: + # Enhanced error message with more context + import traceback + error_details = traceback.format_exc() + return f"Error processing query results: {str(results_error)}\n\nDetails:\n{error_details}" + + # If we have no results but the query succeeded (e.g., for DDL statements) + if not chunk_results and hasattr(result, 'status'): + state_value = str(result.status.state) + if "SUCCEEDED" in state_value: + return "Query executed successfully (no results to display)" + + # Format and return results + return self._format_results(chunk_results) + + except Exception as e: + # Include more details in the error message to help with debugging + import traceback + error_details = traceback.format_exc() + return f"Error executing Databricks query: {str(e)}\n\nDetails:\n{error_details}" diff --git a/crewai_tools/tools/directory_read_tool/README.md b/crewai_tools/tools/directory_read_tool/README.md new file mode 100644 index 000000000..9305fd1a3 --- /dev/null +++ b/crewai_tools/tools/directory_read_tool/README.md @@ -0,0 +1,40 @@ +```markdown +# DirectoryReadTool + +## Description +The DirectoryReadTool is a highly efficient utility designed for the comprehensive listing of directory contents. It recursively navigates through the specified directory, providing users with a detailed enumeration of all files, including those nested within subdirectories. This tool is indispensable for tasks requiring a thorough inventory of directory structures or for validating the organization of files within directories. + +## Installation +Install the `crewai_tools` package to use the DirectoryReadTool in your project. If you haven't added this package to your environment, you can easily install it with pip using the following command: + +```shell +pip install 'crewai[tools]' +``` + +This installs the latest version of the `crewai_tools` package, allowing access to the DirectoryReadTool and other utilities. + +## Example +The DirectoryReadTool is simple to use. The code snippet below shows how to set up and use the tool to list the contents of a specified directory: + +```python +from crewai_tools import DirectoryReadTool + +# Initialize the tool with the directory you want to explore +tool = DirectoryReadTool(directory='/path/to/your/directory') + +# Use the tool to list the contents of the specified directory +directory_contents = tool.run() +print(directory_contents) +``` + +This example demonstrates the essential steps to utilize the DirectoryReadTool effectively, highlighting its simplicity and user-friendly design. + +## Arguments +The DirectoryReadTool requires minimal configuration for use. The essential argument for this tool is as follows: + +- `directory`: A mandatory argument that specifies the path to the directory whose contents you wish to list. It accepts both absolute and relative paths, guiding the tool to the desired directory for content listing. + +The DirectoryReadTool provides a user-friendly and efficient way to list directory contents, making it an invaluable tool for managing and inspecting directory structures. +``` + +This revised documentation for the DirectoryReadTool maintains the structure and content requirements as outlined, with adjustments made for clarity, consistency, and adherence to the high-quality standards exemplified in the provided documentation example. diff --git a/crewai_tools/tools/directory_read_tool/directory_read_tool.py b/crewai_tools/tools/directory_read_tool/directory_read_tool.py new file mode 100644 index 000000000..8488f391e --- /dev/null +++ b/crewai_tools/tools/directory_read_tool/directory_read_tool.py @@ -0,0 +1,47 @@ +import os +from typing import Any, Optional, Type + +from crewai.tools import BaseTool +from pydantic import BaseModel, Field + + +class FixedDirectoryReadToolSchema(BaseModel): + """Input for DirectoryReadTool.""" + + +class DirectoryReadToolSchema(FixedDirectoryReadToolSchema): + """Input for DirectoryReadTool.""" + + directory: str = Field(..., description="Mandatory directory to list content") + + +class DirectoryReadTool(BaseTool): + name: str = "List files in directory" + description: str = ( + "A tool that can be used to recursively list a directory's content." + ) + args_schema: Type[BaseModel] = DirectoryReadToolSchema + directory: Optional[str] = None + + def __init__(self, directory: Optional[str] = None, **kwargs): + super().__init__(**kwargs) + if directory is not None: + self.directory = directory + self.description = f"A tool that can be used to list {directory}'s content." + self.args_schema = FixedDirectoryReadToolSchema + self._generate_description() + + def _run( + self, + **kwargs: Any, + ) -> Any: + directory = kwargs.get("directory", self.directory) + if directory[-1] == "/": + directory = directory[:-1] + files_list = [ + f"{directory}/{(os.path.join(root, filename).replace(directory, '').lstrip(os.path.sep))}" + for root, dirs, files in os.walk(directory) + for filename in files + ] + files = "\n- ".join(files_list) + return f"File paths: \n-{files}" diff --git a/crewai_tools/tools/directory_search_tool/README.md b/crewai_tools/tools/directory_search_tool/README.md new file mode 100644 index 000000000..b39e9fe96 --- /dev/null +++ b/crewai_tools/tools/directory_search_tool/README.md @@ -0,0 +1,55 @@ +# DirectorySearchTool + +## Description +This tool is designed to perform a semantic search for queries within the content of a specified directory. Utilizing the RAG (Retrieval-Augmented Generation) methodology, it offers a powerful means to semantically navigate through the files of a given directory. The tool can be dynamically set to search any directory specified at runtime or can be pre-configured to search within a specific directory upon initialization. + +## Installation +To start using the DirectorySearchTool, you need to install the crewai_tools package. Execute the following command in your terminal: + +```shell +pip install 'crewai[tools]' +``` + +## Example +The following examples demonstrate how to initialize the DirectorySearchTool for different use cases and how to perform a search: + +```python +from crewai_tools import DirectorySearchTool + +# To enable searching within any specified directory at runtime +tool = DirectorySearchTool() + +# Alternatively, to restrict searches to a specific directory +tool = DirectorySearchTool(directory='/path/to/directory') +``` + +## Arguments +- `directory` : This string argument specifies the directory within which to search. It is mandatory if the tool has not been initialized with a directory; otherwise, the tool will only search within the initialized directory. + +## Custom model and embeddings + +By default, the tool uses OpenAI for both embeddings and summarization. To customize the model, you can use a config dictionary as follows: + +```python +tool = DirectorySearchTool( + config=dict( + llm=dict( + provider="ollama", # or google, openai, anthropic, llama2, ... + config=dict( + model="llama2", + # temperature=0.5, + # top_p=1, + # stream=true, + ), + ), + embedder=dict( + provider="google", + config=dict( + model="models/embedding-001", + task_type="retrieval_document", + # title="Embeddings", + ), + ), + ) +) +``` diff --git a/crewai_tools/tools/directory_search_tool/directory_search_tool.py b/crewai_tools/tools/directory_search_tool/directory_search_tool.py new file mode 100644 index 000000000..30fdd52cc --- /dev/null +++ b/crewai_tools/tools/directory_search_tool/directory_search_tool.py @@ -0,0 +1,59 @@ +from typing import Optional, Type + +try: + from embedchain.loaders.directory_loader import DirectoryLoader + EMBEDCHAIN_AVAILABLE = True +except ImportError: + EMBEDCHAIN_AVAILABLE = False + +from pydantic import BaseModel, Field + +from ..rag.rag_tool import RagTool + + +class FixedDirectorySearchToolSchema(BaseModel): + """Input for DirectorySearchTool.""" + + search_query: str = Field( + ..., + description="Mandatory search query you want to use to search the directory's content", + ) + + +class DirectorySearchToolSchema(FixedDirectorySearchToolSchema): + """Input for DirectorySearchTool.""" + + directory: str = Field(..., description="Mandatory directory you want to search") + + +class DirectorySearchTool(RagTool): + name: str = "Search a directory's content" + description: str = ( + "A tool that can be used to semantic search a query from a directory's content." + ) + args_schema: Type[BaseModel] = DirectorySearchToolSchema + + def __init__(self, directory: Optional[str] = None, **kwargs): + if not EMBEDCHAIN_AVAILABLE: + raise ImportError("embedchain is not installed. Please install it with `pip install crewai-tools[embedchain]`") + super().__init__(**kwargs) + if directory is not None: + self.add(directory) + self.description = f"A tool that can be used to semantic search a query the {directory} directory's content." + self.args_schema = FixedDirectorySearchToolSchema + self._generate_description() + + def add(self, directory: str) -> None: + super().add( + directory, + loader=DirectoryLoader(config=dict(recursive=True)), + ) + + def _run( + self, + search_query: str, + directory: Optional[str] = None, + ) -> str: + if directory is not None: + self.add(directory) + return super()._run(query=search_query) diff --git a/crewai_tools/tools/docx_search_tool/README.md b/crewai_tools/tools/docx_search_tool/README.md new file mode 100644 index 000000000..c99a4984e --- /dev/null +++ b/crewai_tools/tools/docx_search_tool/README.md @@ -0,0 +1,57 @@ +# DOCXSearchTool + +## Description +The DOCXSearchTool is a RAG tool designed for semantic searching within DOCX documents. It enables users to effectively search and extract relevant information from DOCX files using query-based searches. This tool is invaluable for data analysis, information management, and research tasks, streamlining the process of finding specific information within large document collections. + +## Installation +Install the crewai_tools package by running the following command in your terminal: + +```shell +pip install 'crewai[tools]' +``` + +## Example +The following example demonstrates initializing the DOCXSearchTool to search within any DOCX file's content or with a specific DOCX file path. + +```python +from crewai_tools import DOCXSearchTool + +# Initialize the tool to search within any DOCX file's content +tool = DOCXSearchTool() + +# OR + +# Initialize the tool with a specific DOCX file, so the agent can only search the content of the specified DOCX file +tool = DOCXSearchTool(docx='path/to/your/document.docx') +``` + +## Arguments +- `docx`: An optional file path to a specific DOCX document you wish to search. If not provided during initialization, the tool allows for later specification of any DOCX file's content path for searching. + +## Custom model and embeddings + +By default, the tool uses OpenAI for both embeddings and summarization. To customize the model, you can use a config dictionary as follows: + +```python +tool = DOCXSearchTool( + config=dict( + llm=dict( + provider="ollama", # or google, openai, anthropic, llama2, ... + config=dict( + model="llama2", + # temperature=0.5, + # top_p=1, + # stream=true, + ), + ), + embedder=dict( + provider="google", + config=dict( + model="models/embedding-001", + task_type="retrieval_document", + # title="Embeddings", + ), + ), + ) +) +``` diff --git a/crewai_tools/tools/docx_search_tool/docx_search_tool.py b/crewai_tools/tools/docx_search_tool/docx_search_tool.py new file mode 100644 index 000000000..97dab02cd --- /dev/null +++ b/crewai_tools/tools/docx_search_tool/docx_search_tool.py @@ -0,0 +1,62 @@ +from typing import Any, Optional, Type + +try: + from embedchain.models.data_type import DataType + EMBEDCHAIN_AVAILABLE = True +except ImportError: + EMBEDCHAIN_AVAILABLE = False + +from pydantic import BaseModel, Field + +from ..rag.rag_tool import RagTool + + +class FixedDOCXSearchToolSchema(BaseModel): + """Input for DOCXSearchTool.""" + + docx: Optional[str] = Field( + ..., description="File path or URL of a DOCX file to be searched" + ) + search_query: str = Field( + ..., + description="Mandatory search query you want to use to search the DOCX's content", + ) + + +class DOCXSearchToolSchema(FixedDOCXSearchToolSchema): + """Input for DOCXSearchTool.""" + + search_query: str = Field( + ..., + description="Mandatory search query you want to use to search the DOCX's content", + ) + + +class DOCXSearchTool(RagTool): + name: str = "Search a DOCX's content" + description: str = ( + "A tool that can be used to semantic search a query from a DOCX's content." + ) + args_schema: Type[BaseModel] = DOCXSearchToolSchema + + def __init__(self, docx: Optional[str] = None, **kwargs): + super().__init__(**kwargs) + if docx is not None: + self.add(docx) + self.description = f"A tool that can be used to semantic search a query the {docx} DOCX's content." + self.args_schema = FixedDOCXSearchToolSchema + self._generate_description() + + def add(self, docx: str) -> None: + if not EMBEDCHAIN_AVAILABLE: + raise ImportError("embedchain is not installed. Please install it with `pip install crewai-tools[embedchain]`") + super().add(docx, data_type=DataType.DOCX) + + def _run( + self, + search_query: str, + docx: Optional[str] = None, + ) -> Any: + if docx is not None: + self.add(docx) + return super()._run(query=search_query) diff --git a/crewai_tools/tools/exa_tools/README.md b/crewai_tools/tools/exa_tools/README.md new file mode 100644 index 000000000..1d1d20150 --- /dev/null +++ b/crewai_tools/tools/exa_tools/README.md @@ -0,0 +1,30 @@ +# EXASearchTool Documentation + +## Description +This tool is designed to perform a semantic search for a specified query from a text's content across the internet. It utilizes the `https://exa.ai/` API to fetch and display the most relevant search results based on the query provided by the user. + +## Installation +To incorporate this tool into your project, follow the installation instructions below: +```shell +uv add crewai[tools] exa_py +``` + +## Example +The following example demonstrates how to initialize the tool and execute a search with a given query: + +```python +from crewai_tools import EXASearchTool + +# Initialize the tool for internet searching capabilities +tool = EXASearchTool(api_key="your_api_key") +``` + +## Steps to Get Started +To effectively use the `EXASearchTool`, follow these steps: + +1. **Package Installation**: Confirm that the `crewai[tools]` package is installed in your Python environment. +2. **API Key Acquisition**: Acquire a `https://exa.ai/` API key by registering for a free account at `https://exa.ai/`. +3. **Environment Configuration**: Store your obtained API key in an environment variable named `EXA_API_KEY` to facilitate its use by the tool. + +## Conclusion +By integrating the `EXASearchTool` into Python projects, users gain the ability to conduct real-time, relevant searches across the internet directly from their applications. By adhering to the setup and usage guidelines provided, incorporating this tool into projects is streamlined and straightforward. diff --git a/crewai_tools/tools/exa_tools/exa_search_tool.py b/crewai_tools/tools/exa_tools/exa_search_tool.py new file mode 100644 index 000000000..332576039 --- /dev/null +++ b/crewai_tools/tools/exa_tools/exa_search_tool.py @@ -0,0 +1,108 @@ +import os +from typing import Any, List, Optional, Type + +from crewai.tools import BaseTool, EnvVar +from pydantic import BaseModel, Field + +try: + from exa_py import Exa + + EXA_INSTALLED = True +except ImportError: + Exa = Any + EXA_INSTALLED = False + + +class EXABaseToolSchema(BaseModel): + search_query: str = Field( + ..., description="Mandatory search query you want to use to search the internet" + ) + start_published_date: Optional[str] = Field( + None, description="Start date for the search" + ) + end_published_date: Optional[str] = Field( + None, description="End date for the search" + ) + include_domains: Optional[list[str]] = Field( + None, description="List of domains to include in the search" + ) + + +class EXASearchTool(BaseTool): + model_config = {"arbitrary_types_allowed": True} + name: str = "EXASearchTool" + description: str = "Search the internet using Exa" + args_schema: Type[BaseModel] = EXABaseToolSchema + client: Optional["Exa"] = None + content: Optional[bool] = False + summary: Optional[bool] = False + type: Optional[str] = "auto" + package_dependencies: List[str] = ["exa_py"] + api_key: Optional[str] = Field( + default_factory=lambda: os.getenv("EXA_API_KEY"), + description="API key for Exa services", + json_schema_extra={"required": False}, + ) + env_vars: List[EnvVar] = [ + EnvVar( + name="EXA_API_KEY", description="API key for Exa services", required=False + ), + ] + + def __init__( + self, + content: Optional[bool] = False, + summary: Optional[bool] = False, + type: Optional[str] = "auto", + **kwargs, + ): + super().__init__( + **kwargs, + ) + if not EXA_INSTALLED: + import click + + if click.confirm( + "You are missing the 'exa_py' package. Would you like to install it?" + ): + import subprocess + + subprocess.run(["uv", "add", "exa_py"], check=True) + + else: + raise ImportError( + "You are missing the 'exa_py' package. Would you like to install it?" + ) + self.client = Exa(api_key=self.api_key) + self.content = content + self.summary = summary + self.type = type + + def _run( + self, + search_query: str, + start_published_date: Optional[str] = None, + end_published_date: Optional[str] = None, + include_domains: Optional[list[str]] = None, + ) -> Any: + if self.client is None: + raise ValueError("Client not initialized") + + search_params = { + "type": self.type, + } + + if start_published_date: + search_params["start_published_date"] = start_published_date + if end_published_date: + search_params["end_published_date"] = end_published_date + if include_domains: + search_params["include_domains"] = include_domains + + if self.content: + results = self.client.search_and_contents( + search_query, summary=self.summary, **search_params + ) + else: + results = self.client.search(search_query, **search_params) + return results diff --git a/crewai_tools/tools/file_read_tool/README.md b/crewai_tools/tools/file_read_tool/README.md new file mode 100644 index 000000000..7b8a15488 --- /dev/null +++ b/crewai_tools/tools/file_read_tool/README.md @@ -0,0 +1,40 @@ +# FileReadTool + +## Description + +The FileReadTool is a versatile component of the crewai_tools package, designed to streamline the process of reading and retrieving content from files. It is particularly useful in scenarios such as batch text file processing, runtime configuration file reading, and data importation for analytics. This tool supports various text-based file formats including `.txt`, `.csv`, `.json`, and adapts its functionality based on the file type, for instance, converting JSON content into a Python dictionary for easy use. + +The tool also supports reading specific chunks of a file by specifying a starting line and the number of lines to read, which is helpful when working with large files that don't need to be loaded entirely into memory. + +## Installation + +Install the crewai_tools package to use the FileReadTool in your projects: + +```shell +pip install 'crewai[tools]' +``` + +## Example + +To get started with the FileReadTool: + +```python +from crewai_tools import FileReadTool + +# Initialize the tool to read any files the agents knows or lean the path for +file_read_tool = FileReadTool() + +# OR + +# Initialize the tool with a specific file path, so the agent can only read the content of the specified file +file_read_tool = FileReadTool(file_path='path/to/your/file.txt') + +# Read a specific chunk of the file (lines 100-149) +partial_content = file_read_tool.run(file_path='path/to/your/file.txt', start_line=100, line_count=50) +``` + +## Arguments + +- `file_path`: The path to the file you want to read. It accepts both absolute and relative paths. Ensure the file exists and you have the necessary permissions to access it. +- `start_line`: (Optional) The line number to start reading from (1-indexed). Defaults to 1 (the first line). +- `line_count`: (Optional) The number of lines to read. If not provided, reads from the start_line to the end of the file. diff --git a/crewai_tools/tools/file_read_tool/file_read_tool.py b/crewai_tools/tools/file_read_tool/file_read_tool.py new file mode 100644 index 000000000..4e04e3a7d --- /dev/null +++ b/crewai_tools/tools/file_read_tool/file_read_tool.py @@ -0,0 +1,97 @@ +from typing import Any, Optional, Type + +from crewai.tools import BaseTool +from pydantic import BaseModel, Field + + +class FileReadToolSchema(BaseModel): + """Input for FileReadTool.""" + + file_path: str = Field(..., description="Mandatory file full path to read the file") + start_line: Optional[int] = Field(1, description="Line number to start reading from (1-indexed)") + line_count: Optional[int] = Field(None, description="Number of lines to read. If None, reads the entire file") + + +class FileReadTool(BaseTool): + """A tool for reading file contents. + + This tool inherits its schema handling from BaseTool to avoid recursive schema + definition issues. The args_schema is set to FileReadToolSchema which defines + the required file_path parameter. The schema should not be overridden in the + constructor as it would break the inheritance chain and cause infinite loops. + + The tool supports two ways of specifying the file path: + 1. At construction time via the file_path parameter + 2. At runtime via the file_path parameter in the tool's input + + Args: + file_path (Optional[str]): Path to the file to be read. If provided, + this becomes the default file path for the tool. + **kwargs: Additional keyword arguments passed to BaseTool. + + Example: + >>> tool = FileReadTool(file_path="/path/to/file.txt") + >>> content = tool.run() # Reads /path/to/file.txt + >>> content = tool.run(file_path="/path/to/other.txt") # Reads other.txt + >>> content = tool.run(file_path="/path/to/file.txt", start_line=100, line_count=50) # Reads lines 100-149 + """ + + name: str = "Read a file's content" + description: str = "A tool that reads the content of a file. To use this tool, provide a 'file_path' parameter with the path to the file you want to read. Optionally, provide 'start_line' to start reading from a specific line and 'line_count' to limit the number of lines read." + args_schema: Type[BaseModel] = FileReadToolSchema + file_path: Optional[str] = None + + def __init__(self, file_path: Optional[str] = None, **kwargs: Any) -> None: + """Initialize the FileReadTool. + + Args: + file_path (Optional[str]): Path to the file to be read. If provided, + this becomes the default file path for the tool. + **kwargs: Additional keyword arguments passed to BaseTool. + """ + if file_path is not None: + kwargs["description"] = ( + f"A tool that reads file content. The default file is {file_path}, but you can provide a different 'file_path' parameter to read another file. You can also specify 'start_line' and 'line_count' to read specific parts of the file." + ) + + super().__init__(**kwargs) + self.file_path = file_path + + def _run( + self, + file_path: Optional[str] = None, + start_line: Optional[int] = 1, + line_count: Optional[int] = None, + ) -> str: + file_path = file_path or self.file_path + start_line = start_line or 1 + line_count = line_count or None + + if file_path is None: + return ( + "Error: No file path provided. Please provide a file path either in the constructor or as an argument." + ) + + try: + with open(file_path, "r") as file: + if start_line == 1 and line_count is None: + return file.read() + + start_idx = max(start_line - 1, 0) + + selected_lines = [ + line + for i, line in enumerate(file) + if i >= start_idx and (line_count is None or i < start_idx + line_count) + ] + + if not selected_lines and start_idx > 0: + return f"Error: Start line {start_line} exceeds the number of lines in the file." + + return "".join(selected_lines) + except FileNotFoundError: + return f"Error: File not found at path: {file_path}" + except PermissionError: + return f"Error: Permission denied when trying to read file: {file_path}" + except Exception as e: + return f"Error: Failed to read file {file_path}. {str(e)}" diff --git a/crewai_tools/tools/file_writer_tool/README.md b/crewai_tools/tools/file_writer_tool/README.md new file mode 100644 index 000000000..e93e5c682 --- /dev/null +++ b/crewai_tools/tools/file_writer_tool/README.md @@ -0,0 +1,35 @@ +Here's the rewritten README for the `FileWriterTool`: + +# FileWriterTool Documentation + +## Description +The `FileWriterTool` is a component of the crewai_tools package, designed to simplify the process of writing content to files. It is particularly useful in scenarios such as generating reports, saving logs, creating configuration files, and more. This tool supports creating new directories if they don't exist, making it easier to organize your output. + +## Installation +Install the crewai_tools package to use the `FileWriterTool` in your projects: + +```shell +pip install 'crewai[tools]' +``` + +## Example +To get started with the `FileWriterTool`: + +```python +from crewai_tools import FileWriterTool + +# Initialize the tool +file_writer_tool = FileWriterTool() + +# Write content to a file in a specified directory +result = file_writer_tool._run('example.txt', 'This is a test content.', 'test_directory') +print(result) +``` + +## Arguments +- `filename`: The name of the file you want to create or overwrite. +- `content`: The content to write into the file. +- `directory` (optional): The path to the directory where the file will be created. Defaults to the current directory (`.`). If the directory does not exist, it will be created. + +## Conclusion +By integrating the `FileWriterTool` into your crews, the agents can execute the process of writing content to files and creating directories. This tool is essential for tasks that require saving output data, creating structured file systems, and more. By adhering to the setup and usage guidelines provided, incorporating this tool into projects is straightforward and efficient. diff --git a/crewai_tools/tools/file_writer_tool/file_writer_tool.py b/crewai_tools/tools/file_writer_tool/file_writer_tool.py new file mode 100644 index 000000000..8b9ca5225 --- /dev/null +++ b/crewai_tools/tools/file_writer_tool/file_writer_tool.py @@ -0,0 +1,62 @@ +import os +from typing import Any, Optional, Type + +from crewai.tools import BaseTool +from pydantic import BaseModel + + +def strtobool(val) -> bool: + if isinstance(val, bool): + return val + val = val.lower() + if val in ("y", "yes", "t", "true", "on", "1"): + return True + elif val in ("n", "no", "f", "false", "off", "0"): + return False + else: + raise ValueError(f"invalid value to cast to bool: {val!r}") + + +class FileWriterToolInput(BaseModel): + filename: str + directory: Optional[str] = "./" + overwrite: str | bool = False + content: str + + +class FileWriterTool(BaseTool): + name: str = "File Writer Tool" + description: str = ( + "A tool to write content to a specified file. Accepts filename, content, and optionally a directory path and overwrite flag as input." + ) + args_schema: Type[BaseModel] = FileWriterToolInput + + def _run(self, **kwargs: Any) -> str: + try: + # Create the directory if it doesn't exist + if kwargs.get("directory") and not os.path.exists(kwargs["directory"]): + os.makedirs(kwargs["directory"]) + + # Construct the full path + filepath = os.path.join(kwargs.get("directory") or "", kwargs["filename"]) + + # Convert overwrite to boolean + kwargs["overwrite"] = strtobool(kwargs["overwrite"]) + + # Check if file exists and overwrite is not allowed + if os.path.exists(filepath) and not kwargs["overwrite"]: + return f"File {filepath} already exists and overwrite option was not passed." + + # Write content to the file + mode = "w" if kwargs["overwrite"] else "x" + with open(filepath, mode) as file: + file.write(kwargs["content"]) + return f"Content successfully written to {filepath}" + except FileExistsError: + return ( + f"File {filepath} already exists and overwrite option was not passed." + ) + except KeyError as e: + return f"An error occurred while accessing key: {str(e)}" + except Exception as e: + return f"An error occurred while writing to the file: {str(e)}" diff --git a/crewai_tools/tools/file_writer_tool/tests/test_file_writer_tool.py b/crewai_tools/tools/file_writer_tool/tests/test_file_writer_tool.py new file mode 100644 index 000000000..d75ed30f2 --- /dev/null +++ b/crewai_tools/tools/file_writer_tool/tests/test_file_writer_tool.py @@ -0,0 +1,138 @@ +import os +import shutil +import tempfile + +import pytest + +from crewai_tools.tools.file_writer_tool.file_writer_tool import FileWriterTool + + +@pytest.fixture +def tool(): + return FileWriterTool() + + +@pytest.fixture +def temp_env(): + temp_dir = tempfile.mkdtemp() + test_file = "test.txt" + test_content = "Hello, World!" + + yield { + "temp_dir": temp_dir, + "test_file": test_file, + "test_content": test_content, + } + + shutil.rmtree(temp_dir, ignore_errors=True) + + +def get_test_path(filename, directory): + return os.path.join(directory, filename) + + +def read_file(path): + with open(path, "r") as f: + return f.read() + + +def test_basic_file_write(tool, temp_env): + result = tool._run( + filename=temp_env["test_file"], + directory=temp_env["temp_dir"], + content=temp_env["test_content"], + overwrite=True, + ) + + path = get_test_path(temp_env["test_file"], temp_env["temp_dir"]) + assert os.path.exists(path) + assert read_file(path) == temp_env["test_content"] + assert "successfully written" in result + + +def test_directory_creation(tool, temp_env): + new_dir = os.path.join(temp_env["temp_dir"], "nested_dir") + result = tool._run( + filename=temp_env["test_file"], + directory=new_dir, + content=temp_env["test_content"], + overwrite=True, + ) + + path = get_test_path(temp_env["test_file"], new_dir) + assert os.path.exists(new_dir) + assert os.path.exists(path) + assert "successfully written" in result + + +@pytest.mark.parametrize( + "overwrite", + ["y", "yes", "t", "true", "on", "1", True], +) +def test_overwrite_true(tool, temp_env, overwrite): + path = get_test_path(temp_env["test_file"], temp_env["temp_dir"]) + with open(path, "w") as f: + f.write("Original content") + + result = tool._run( + filename=temp_env["test_file"], + directory=temp_env["temp_dir"], + content="New content", + overwrite=overwrite, + ) + + assert read_file(path) == "New content" + assert "successfully written" in result + + +def test_invalid_overwrite_value(tool, temp_env): + result = tool._run( + filename=temp_env["test_file"], + directory=temp_env["temp_dir"], + content=temp_env["test_content"], + overwrite="invalid", + ) + assert "invalid value" in result + + +def test_missing_required_fields(tool, temp_env): + result = tool._run( + directory=temp_env["temp_dir"], + content=temp_env["test_content"], + overwrite=True, + ) + assert "An error occurred while accessing key: 'filename'" in result + + +def test_empty_content(tool, temp_env): + result = tool._run( + filename=temp_env["test_file"], + directory=temp_env["temp_dir"], + content="", + overwrite=True, + ) + + path = get_test_path(temp_env["test_file"], temp_env["temp_dir"]) + assert os.path.exists(path) + assert read_file(path) == "" + assert "successfully written" in result + + +@pytest.mark.parametrize( + "overwrite", + ["n", "no", "f", "false", "off", "0", False], +) +def test_file_exists_error_handling(tool, temp_env, overwrite): + path = get_test_path(temp_env["test_file"], temp_env["temp_dir"]) + with open(path, "w") as f: + f.write("Pre-existing content") + + result = tool._run( + filename=temp_env["test_file"], + directory=temp_env["temp_dir"], + content="Should not be written", + overwrite=overwrite, + ) + + assert "already exists and overwrite option was not passed" in result + assert read_file(path) == "Pre-existing content" diff --git a/crewai_tools/tools/files_compressor_tool/README.md b/crewai_tools/tools/files_compressor_tool/README.md new file mode 100644 index 000000000..01fdeee7d --- /dev/null +++ b/crewai_tools/tools/files_compressor_tool/README.md @@ -0,0 +1,119 @@ +# 📦 FileCompressorTool + +The **FileCompressorTool** is a utility for compressing individual files or entire directories (including nested subdirectories) into different archive formats, such as `.zip` or `.tar` (including `.tar.gz`, `.tar.bz2`, and `.tar.xz`). This tool is useful for archiving logs, documents, datasets, or backups in a compact format, and ensures flexibility in how the archives are created. + +--- + +## Description + +This tool: +- Accepts a **file or directory** as input. +- Supports **recursive compression** of subdirectories. +- Lets you define a **custom output archive path** or defaults to the current directory. +- Handles **overwrite protection** to avoid unintentional data loss. +- Supports multiple compression formats: `.zip`, `.tar`, `.tar.gz`, `.tar.bz2`, and `.tar.xz`. + +--- + +## Arguments + +| Argument | Type | Required | Description | +|---------------|-----------|----------|-----------------------------------------------------------------------------| +| `input_path` | `str` | ✅ | Path to the file or directory you want to compress. | +| `output_path` | `str` | ❌ | Optional path for the resulting archive file. Defaults to `./.`. | +| `overwrite` | `bool` | ❌ | Whether to overwrite an existing archive file. Defaults to `False`. | +| `format` | `str` | ❌ | Compression format to use. Can be one of `zip`, `tar`, `tar.gz`, `tar.bz2`, `tar.xz`. Defaults to `zip`. | + +--- + + +## Usage Example + +```python +from crewai_tools import FileCompressorTool + +# Initialize the tool +tool = FileCompressorTool() + +# Compress a directory with subdirectories and files into a zip archive +result = tool._run( + input_path="./data/project_docs", # Folder containing subfolders & files + output_path="./output/project_docs.zip", # Optional output path (defaults to zip format) + overwrite=True # Allow overwriting if file exists +) +print(result) +# Example output: Successfully compressed './data/project_docs' into './output/project_docs.zip' + +``` + +--- + +## Example Scenarios + +### Compress a single file into a zip archive: +```python +# Compress a single file into a zip archive +result = tool._run(input_path="report.pdf") +# Example output: Successfully compressed 'report.pdf' into './report.zip' +``` + +### Compress a directory with nested folders into a zip archive: +```python +# Compress a directory containing nested subdirectories and files +result = tool._run(input_path="./my_data", overwrite=True) +# Example output: Successfully compressed 'my_data' into './my_data.zip' +``` + +### Use a custom output path with a zip archive: +```python +# Compress a directory and specify a custom zip output location +result = tool._run(input_path="./my_data", output_path="./backups/my_data_backup.zip", overwrite=True) +# Example output: Successfully compressed 'my_data' into './backups/my_data_backup.zip' +``` + +### Prevent overwriting an existing zip file: +```python +# Try to compress a directory without overwriting an existing zip file +result = tool._run(input_path="./my_data", output_path="./backups/my_data_backup.zip", overwrite=False) +# Example output: Output zip './backups/my_data_backup.zip' already exists and overwrite is set to False. +``` + +### Compress into a tar archive: +```python +# Compress a directory into a tar archive +result = tool._run(input_path="./my_data", format="tar", overwrite=True) +# Example output: Successfully compressed 'my_data' into './my_data.tar' +``` + +### Compress into a tar.gz archive: +```python +# Compress a directory into a tar.gz archive +result = tool._run(input_path="./my_data", format="tar.gz", overwrite=True) +# Example output: Successfully compressed 'my_data' into './my_data.tar.gz' +``` + +### Compress into a tar.bz2 archive: +```python +# Compress a directory into a tar.bz2 archive +result = tool._run(input_path="./my_data", format="tar.bz2", overwrite=True) +# Example output: Successfully compressed 'my_data' into './my_data.tar.bz2' +``` + +### Compress into a tar.xz archive: +```python +# Compress a directory into a tar.xz archive +result = tool._run(input_path="./my_data", format="tar.xz", overwrite=True) +# Example output: Successfully compressed 'my_data' into './my_data.tar.xz' +``` + +--- + +## Error Handling and Validations + +- **File Extension Validation**: The tool ensures that the output file extension matches the selected format (e.g., `.zip` for `zip` format, `.tar` for `tar` format, etc.). +- **File/Directory Existence**: If the input path does not exist, an error message will be returned. +- **Overwrite Protection**: If a file already exists at the output path, the tool checks the `overwrite` flag before proceeding. If `overwrite=False`, it prevents overwriting the existing file. + +--- + +This tool provides a flexible and robust way to handle file and directory compression across multiple formats for efficient storage and backups. diff --git a/crewai_tools/tools/files_compressor_tool/files_compressor_tool.py b/crewai_tools/tools/files_compressor_tool/files_compressor_tool.py new file mode 100644 index 000000000..c86fd64e0 --- /dev/null +++ b/crewai_tools/tools/files_compressor_tool/files_compressor_tool.py @@ -0,0 +1,117 @@ +import os +import zipfile +import tarfile +from typing import Type, Optional +from pydantic import BaseModel, Field +from crewai.tools import BaseTool + + +class FileCompressorToolInput(BaseModel): + """Input schema for FileCompressorTool.""" + input_path: str = Field(..., description="Path to the file or directory to compress.") + output_path: Optional[str] = Field(default=None, description="Optional output archive filename.") + overwrite: bool = Field(default=False, description="Whether to overwrite the archive if it already exists.") + format: str = Field(default="zip", description="Compression format ('zip', 'tar', 'tar.gz', 'tar.bz2', 'tar.xz').") + + +class FileCompressorTool(BaseTool): + name: str = "File Compressor Tool" + description: str = ( + "Compresses a file or directory into an archive (.zip currently supported). " + "Useful for archiving logs, documents, or backups." + ) + args_schema: Type[BaseModel] = FileCompressorToolInput + + + def _run(self, input_path: str, output_path: Optional[str] = None, overwrite: bool = False, format: str = "zip") -> str: + + if not os.path.exists(input_path): + return f"Input path '{input_path}' does not exist." + + if not output_path: + output_path = self._generate_output_path(input_path, format) + + FORMAT_EXTENSION = { + "zip": ".zip", + "tar": ".tar", + "tar.gz": ".tar.gz", + "tar.bz2": ".tar.bz2", + "tar.xz": ".tar.xz" + } + + if format not in FORMAT_EXTENSION: + return f"Compression format '{format}' is not supported. Allowed formats: {', '.join(FORMAT_EXTENSION.keys())}" + elif not output_path.endswith(FORMAT_EXTENSION[format]): + return f"Error: If '{format}' format is chosen, output file must have a '{FORMAT_EXTENSION[format]}' extension." + if not self._prepare_output(output_path, overwrite): + return f"Output '{output_path}' already exists and overwrite is set to False." + + try: + format_compression = { + "zip": self._compress_zip, + "tar": self._compress_tar, + "tar.gz": self._compress_tar, + "tar.bz2": self._compress_tar, + "tar.xz": self._compress_tar + } + if format == "zip": + format_compression[format](input_path, output_path) + else: + format_compression[format](input_path, output_path, format) + + return f"Successfully compressed '{input_path}' into '{output_path}'" + except FileNotFoundError: + return f"Error: File not found at path: {input_path}" + except PermissionError: + return f"Error: Permission denied when accessing '{input_path}' or writing '{output_path}'" + except Exception as e: + return f"An unexpected error occurred during compression: {str(e)}" + + + def _generate_output_path(self, input_path: str, format: str) -> str: + """Generates output path based on input path and format.""" + if os.path.isfile(input_path): + base_name = os.path.splitext(os.path.basename(input_path))[0] # Remove extension + else: + base_name = os.path.basename(os.path.normpath(input_path)) # Directory name + return os.path.join(os.getcwd(), f"{base_name}.{format}") + + def _prepare_output(self, output_path: str, overwrite: bool) -> bool: + """Ensures output path is ready for writing.""" + output_dir = os.path.dirname(output_path) + if output_dir and not os.path.exists(output_dir): + os.makedirs(output_dir) + if os.path.exists(output_path) and not overwrite: + return False + return True + + def _compress_zip(self, input_path: str, output_path: str): + """Compresses input into a zip archive.""" + with zipfile.ZipFile(output_path, 'w', zipfile.ZIP_DEFLATED) as zipf: + if os.path.isfile(input_path): + zipf.write(input_path, os.path.basename(input_path)) + else: + for root, _, files in os.walk(input_path): + for file in files: + full_path = os.path.join(root, file) + arcname = os.path.relpath(full_path, start=input_path) + zipf.write(full_path, arcname) + + + def _compress_tar(self, input_path: str, output_path: str, format: str): + """Compresses input into a tar archive with the given format.""" + format_mode = { + "tar": "w", + "tar.gz": "w:gz", + "tar.bz2": "w:bz2", + "tar.xz": "w:xz" + } + + if format not in format_mode: + raise ValueError(f"Unsupported tar format: {format}") + + mode = format_mode[format] + + with tarfile.open(output_path, mode) as tarf: + arcname = os.path.basename(input_path) + tarf.add(input_path, arcname=arcname) diff --git a/crewai_tools/tools/files_compressor_tool/files_compressor_tool_test2.py b/crewai_tools/tools/files_compressor_tool/files_compressor_tool_test2.py new file mode 100644 index 000000000..b30199842 --- /dev/null +++ b/crewai_tools/tools/files_compressor_tool/files_compressor_tool_test2.py @@ -0,0 +1,93 @@ + +import os +import pytest +from crewai_tools.tools.files_compressor_tool import FileCompressorTool +from unittest.mock import patch, MagicMock + +@pytest.fixture +def tool(): + return FileCompressorTool() + +@patch("os.path.exists", return_value=False) +def test_input_path_does_not_exist(mock_exists, tool): + result = tool._run("nonexistent_path") + assert "does not exist" in result + +@patch("os.path.exists", return_value=True) +@patch("os.getcwd", return_value="/mocked/cwd") +@patch.object(FileCompressorTool, "_compress_zip") # Mock actual compression +@patch.object(FileCompressorTool, "_prepare_output", return_value=True) +def test_generate_output_path_default(mock_prepare, mock_compress, mock_cwd, mock_exists, tool): + result = tool._run(input_path="mydir", format="zip") + assert "Successfully compressed" in result + mock_compress.assert_called_once() + +@patch("os.path.exists", return_value=True) +@patch.object(FileCompressorTool, "_compress_zip") +@patch.object(FileCompressorTool, "_prepare_output", return_value=True) +def test_zip_compression(mock_prepare, mock_compress, mock_exists, tool): + result = tool._run(input_path="some/path", output_path="archive.zip", format="zip", overwrite=True) + assert "Successfully compressed" in result + mock_compress.assert_called_once() + +@patch("os.path.exists", return_value=True) +@patch.object(FileCompressorTool, "_compress_tar") +@patch.object(FileCompressorTool, "_prepare_output", return_value=True) +def test_tar_gz_compression(mock_prepare, mock_compress, mock_exists, tool): + result = tool._run(input_path="some/path", output_path="archive.tar.gz", format="tar.gz", overwrite=True) + assert "Successfully compressed" in result + mock_compress.assert_called_once() + +@pytest.mark.parametrize("format", ["tar", "tar.bz2", "tar.xz"]) +@patch("os.path.exists", return_value=True) +@patch.object(FileCompressorTool, "_compress_tar") +@patch.object(FileCompressorTool, "_prepare_output", return_value=True) +def test_other_tar_formats(mock_prepare, mock_compress, mock_exists, format, tool): + result = tool._run(input_path="path/to/input", output_path=f"archive.{format}", format=format, overwrite=True) + assert "Successfully compressed" in result + mock_compress.assert_called_once() + +@pytest.mark.parametrize("format", ["rar", "7z"]) +@patch("os.path.exists", return_value=True) #Ensure input_path exists +def test_unsupported_format(_, tool, format): + result = tool._run(input_path="some/path", output_path=f"archive.{format}", format=format) + assert "not supported" in result + +@patch("os.path.exists", return_value=True) +def test_extension_mismatch(_ , tool): + result = tool._run(input_path="some/path", output_path="archive.zip", format="tar.gz") + assert "must have a '.tar.gz' extension" in result + +@patch("os.path.exists", return_value=True) +@patch("os.path.isfile", return_value=True) +@patch("os.path.exists", return_value=True) +def test_existing_output_no_overwrite(_, __, ___, tool): + result = tool._run(input_path="some/path", output_path="archive.zip", format="zip", overwrite=False) + assert "overwrite is set to False" in result + +@patch("os.path.exists", return_value=True) +@patch("zipfile.ZipFile", side_effect=PermissionError) +def test_permission_error(mock_zip, _, tool): + result = tool._run(input_path="file.txt", output_path="file.zip", format="zip", overwrite=True) + assert "Permission denied" in result + +@patch("os.path.exists", return_value=True) +@patch("zipfile.ZipFile", side_effect=FileNotFoundError) +def test_file_not_found_during_zip(mock_zip, _, tool): + result = tool._run(input_path="file.txt", output_path="file.zip", format="zip", overwrite=True) + assert "File not found" in result + +@patch("os.path.exists", return_value=True) +@patch("zipfile.ZipFile", side_effect=Exception("Unexpected")) +def test_general_exception_during_zip(mock_zip, _, tool): + result = tool._run(input_path="file.txt", output_path="file.zip", format="zip", overwrite=True) + assert "unexpected error" in result + +# Test: Output directory is created when missing +@patch("os.makedirs") +@patch("os.path.exists", return_value=False) +def test_prepare_output_makes_dir(mock_exists, mock_makedirs): + tool = FileCompressorTool() + result = tool._prepare_output("some/missing/path/file.zip", overwrite=True) + assert result is True + mock_makedirs.assert_called_once() diff --git a/crewai_tools/tools/firecrawl_crawl_website_tool/README.md b/crewai_tools/tools/firecrawl_crawl_website_tool/README.md new file mode 100644 index 000000000..3edb73f02 --- /dev/null +++ b/crewai_tools/tools/firecrawl_crawl_website_tool/README.md @@ -0,0 +1,60 @@ +# FirecrawlCrawlWebsiteTool + +## Description + +[Firecrawl](https://firecrawl.dev) is a platform for crawling and convert any website into clean markdown or structured data. + +## Version Compatibility + +This implementation is compatible with FireCrawl API v1 + +## Installation + +- Get an API key from [firecrawl.dev](https://firecrawl.dev) and set it in environment variables (`FIRECRAWL_API_KEY`). +- Install the [Firecrawl SDK](https://github.com/mendableai/firecrawl) along with `crewai[tools]` package: + +``` +pip install firecrawl-py 'crewai[tools]' +``` + +## Example + +Utilize the FirecrawlScrapeFromWebsiteTool as follows to allow your agent to load websites: + +```python +from crewai_tools import FirecrawlCrawlWebsiteTool +from firecrawl import ScrapeOptions + +tool = FirecrawlCrawlWebsiteTool( + config={ + "limit": 100, + "scrape_options": ScrapeOptions(formats=["markdown", "html"]), + "poll_interval": 30, + } +) +tool.run(url="firecrawl.dev") +``` + +## Arguments + +- `api_key`: Optional. Specifies Firecrawl API key. Defaults is the `FIRECRAWL_API_KEY` environment variable. +- `config`: Optional. It contains Firecrawl API parameters. + +This is the default configuration + +```python +from firecrawl import ScrapeOptions + +{ + "max_depth": 2, + "ignore_sitemap": True, + "limit": 100, + "allow_backward_links": False, + "allow_external_links": False, + "scrape_options": ScrapeOptions( + formats=["markdown", "screenshot", "links"], + only_main_content=True, + timeout=30000, + ), +} +``` diff --git a/crewai_tools/tools/firecrawl_crawl_website_tool/firecrawl_crawl_website_tool.py b/crewai_tools/tools/firecrawl_crawl_website_tool/firecrawl_crawl_website_tool.py new file mode 100644 index 000000000..9c99fe8d4 --- /dev/null +++ b/crewai_tools/tools/firecrawl_crawl_website_tool/firecrawl_crawl_website_tool.py @@ -0,0 +1,115 @@ +from typing import Any, Optional, Type, List, TYPE_CHECKING + +from crewai.tools import BaseTool, EnvVar +from pydantic import BaseModel, ConfigDict, Field, PrivateAttr + +if TYPE_CHECKING: + from firecrawl import FirecrawlApp + +try: + from firecrawl import FirecrawlApp + + FIRECRAWL_AVAILABLE = True +except ImportError: + FIRECRAWL_AVAILABLE = False + + +class FirecrawlCrawlWebsiteToolSchema(BaseModel): + url: str = Field(description="Website URL") + + +class FirecrawlCrawlWebsiteTool(BaseTool): + """ + Tool for crawling websites using Firecrawl. To run this tool, you need to have a Firecrawl API key. + + Args: + api_key (str): Your Firecrawl API key. + config (dict): Optional. It contains Firecrawl API parameters. + + Default configuration options: + max_depth (int): Maximum depth to crawl. Default: 2 + ignore_sitemap (bool): Whether to ignore sitemap. Default: True + limit (int): Maximum number of pages to crawl. Default: 100 + allow_backward_links (bool): Allow crawling backward links. Default: False + allow_external_links (bool): Allow crawling external links. Default: False + scrape_options (ScrapeOptions): Options for scraping content + - formats (list[str]): Content formats to return. Default: ["markdown", "screenshot", "links"] + - only_main_content (bool): Only return main content. Default: True + - timeout (int): Timeout in milliseconds. Default: 30000 + """ + + model_config = ConfigDict( + arbitrary_types_allowed=True, validate_assignment=True, frozen=False + ) + name: str = "Firecrawl web crawl tool" + description: str = "Crawl webpages using Firecrawl and return the contents" + args_schema: Type[BaseModel] = FirecrawlCrawlWebsiteToolSchema + api_key: Optional[str] = None + config: Optional[dict[str, Any]] = Field( + default_factory=lambda: { + "maxDepth": 2, + "ignoreSitemap": True, + "limit": 10, + "allowBackwardLinks": False, + "allowExternalLinks": False, + "scrapeOptions": { + "formats": ["markdown", "screenshot", "links"], + "onlyMainContent": True, + "timeout": 10000, + }, + } + ) + _firecrawl: Optional["FirecrawlApp"] = PrivateAttr(None) + package_dependencies: List[str] = ["firecrawl-py"] + env_vars: List[EnvVar] = [ + EnvVar(name="FIRECRAWL_API_KEY", description="API key for Firecrawl services", required=True), + ] + + def __init__(self, api_key: Optional[str] = None, **kwargs): + super().__init__(**kwargs) + self.api_key = api_key + self._initialize_firecrawl() + + def _initialize_firecrawl(self) -> None: + try: + from firecrawl import FirecrawlApp # type: ignore + + self._firecrawl = FirecrawlApp(api_key=self.api_key) + except ImportError: + import click + + if click.confirm( + "You are missing the 'firecrawl-py' package. Would you like to install it?" + ): + import subprocess + + try: + subprocess.run(["uv", "add", "firecrawl-py"], check=True) + from firecrawl import FirecrawlApp + + self._firecrawl = FirecrawlApp(api_key=self.api_key) + except subprocess.CalledProcessError: + raise ImportError("Failed to install firecrawl-py package") + else: + raise ImportError( + "`firecrawl-py` package not found, please run `uv add firecrawl-py`" + ) + + def _run(self, url: str): + if not self._firecrawl: + raise RuntimeError("FirecrawlApp not properly initialized") + + return self._firecrawl.crawl_url(url, poll_interval=2, params=self.config) + + +try: + from firecrawl import FirecrawlApp + + # Only rebuild if the class hasn't been initialized yet + if not hasattr(FirecrawlCrawlWebsiteTool, "_model_rebuilt"): + FirecrawlCrawlWebsiteTool.model_rebuild() + FirecrawlCrawlWebsiteTool._model_rebuilt = True +except ImportError: + """ + When this tool is not used, then exception can be ignored. + """ diff --git a/crewai_tools/tools/firecrawl_scrape_website_tool/README.md b/crewai_tools/tools/firecrawl_scrape_website_tool/README.md new file mode 100644 index 000000000..ebcea2f53 --- /dev/null +++ b/crewai_tools/tools/firecrawl_scrape_website_tool/README.md @@ -0,0 +1,46 @@ +# FirecrawlScrapeWebsiteTool + +## Description + +[Firecrawl](https://firecrawl.dev) is a platform for crawling and convert any website into clean markdown or structured data. + +## Installation + +- Get an API key from [firecrawl.dev](https://firecrawl.dev) and set it in environment variables (`FIRECRAWL_API_KEY`). +- Install the [Firecrawl SDK](https://github.com/mendableai/firecrawl) along with `crewai[tools]` package: + +``` +pip install firecrawl-py 'crewai[tools]' +``` + +## Example + +Utilize the FirecrawlScrapeWebsiteTool as follows to allow your agent to load websites: + +```python +from crewai_tools import FirecrawlScrapeWebsiteTool + +tool = FirecrawlScrapeWebsiteTool(config={"formats": ['html']}) +tool.run(url="firecrawl.dev") +``` + +## Arguments + +- `api_key`: Optional. Specifies Firecrawl API key. Defaults is the `FIRECRAWL_API_KEY` environment variable. +- `config`: Optional. It contains Firecrawl API parameters. + + +This is the default configuration + +```python +{ + "formats": ["markdown"], + "only_main_content": True, + "include_tags": [], + "exclude_tags": [], + "headers": {}, + "wait_for": 0, +} +``` + + diff --git a/crewai_tools/tools/firecrawl_scrape_website_tool/firecrawl_scrape_website_tool.py b/crewai_tools/tools/firecrawl_scrape_website_tool/firecrawl_scrape_website_tool.py new file mode 100644 index 000000000..816e40159 --- /dev/null +++ b/crewai_tools/tools/firecrawl_scrape_website_tool/firecrawl_scrape_website_tool.py @@ -0,0 +1,103 @@ +from typing import Any, Optional, Type, Dict, List, TYPE_CHECKING + +from crewai.tools import BaseTool, EnvVar +from pydantic import BaseModel, ConfigDict, Field, PrivateAttr + +if TYPE_CHECKING: + from firecrawl import FirecrawlApp + +try: + from firecrawl import FirecrawlApp + + FIRECRAWL_AVAILABLE = True +except ImportError: + FIRECRAWL_AVAILABLE = False + + +class FirecrawlScrapeWebsiteToolSchema(BaseModel): + url: str = Field(description="Website URL") + + +class FirecrawlScrapeWebsiteTool(BaseTool): + """ + Tool for scraping webpages using Firecrawl. To run this tool, you need to have a Firecrawl API key. + + Args: + api_key (str): Your Firecrawl API key. + config (dict): Optional. It contains Firecrawl API parameters. + + Default configuration options: + formats (list[str]): Content formats to return. Default: ["markdown"] + onlyMainContent (bool): Only return main content. Default: True + includeTags (list[str]): Tags to include. Default: [] + excludeTags (list[str]): Tags to exclude. Default: [] + headers (dict): Headers to include. Default: {} + waitFor (int): Time to wait for page to load in ms. Default: 0 + json_options (dict): Options for JSON extraction. Default: None + """ + + model_config = ConfigDict( + arbitrary_types_allowed=True, validate_assignment=True, frozen=False + ) + name: str = "Firecrawl web scrape tool" + description: str = "Scrape webpages using Firecrawl and return the contents" + args_schema: Type[BaseModel] = FirecrawlScrapeWebsiteToolSchema + api_key: Optional[str] = None + config: Dict[str, Any] = Field( + default_factory=lambda: { + "formats": ["markdown"], + "onlyMainContent": True, + "includeTags": [], + "excludeTags": [], + "headers": {}, + "waitFor": 0, + } + ) + + _firecrawl: Optional["FirecrawlApp"] = PrivateAttr(None) + package_dependencies: List[str] = ["firecrawl-py"] + env_vars: List[EnvVar] = [ + EnvVar(name="FIRECRAWL_API_KEY", description="API key for Firecrawl services", required=True), + ] + + def __init__(self, api_key: Optional[str] = None, **kwargs): + super().__init__(**kwargs) + try: + from firecrawl import FirecrawlApp # type: ignore + except ImportError: + import click + + if click.confirm( + "You are missing the 'firecrawl-py' package. Would you like to install it?" + ): + import subprocess + + subprocess.run(["uv", "add", "firecrawl-py"], check=True) + from firecrawl import ( + FirecrawlApp, + ) + else: + raise ImportError( + "`firecrawl-py` package not found, please run `uv add firecrawl-py`" + ) + + self._firecrawl = FirecrawlApp(api_key=api_key) + + def _run(self, url: str): + if not self._firecrawl: + raise RuntimeError("FirecrawlApp not properly initialized") + + return self._firecrawl.scrape_url(url, params=self.config) + + +try: + from firecrawl import FirecrawlApp + + # Must rebuild model after class is defined + if not hasattr(FirecrawlScrapeWebsiteTool, "_model_rebuilt"): + FirecrawlScrapeWebsiteTool.model_rebuild() + FirecrawlScrapeWebsiteTool._model_rebuilt = True +except ImportError: + """ + When this tool is not used, then exception can be ignored. + """ diff --git a/crewai_tools/tools/firecrawl_search_tool/README.md b/crewai_tools/tools/firecrawl_search_tool/README.md new file mode 100644 index 000000000..a2037e951 --- /dev/null +++ b/crewai_tools/tools/firecrawl_search_tool/README.md @@ -0,0 +1,44 @@ +# FirecrawlSearchTool + +## Description + +[Firecrawl](https://firecrawl.dev) is a platform for crawling and convert any website into clean markdown or structured data. + +## Installation + +- Get an API key from [firecrawl.dev](https://firecrawl.dev) and set it in environment variables (`FIRECRAWL_API_KEY`). +- Install the [Firecrawl SDK](https://github.com/mendableai/firecrawl) along with `crewai[tools]` package: + +``` +pip install firecrawl-py 'crewai[tools]' +``` + +## Example + +Utilize the FirecrawlSearchTool as follows to allow your agent to load websites: + +```python +from crewai_tools import FirecrawlSearchTool + +tool = FirecrawlSearchTool(config={"limit": 5}) +tool.run(query="firecrawl web scraping") +``` + +## Arguments + +- `api_key`: Optional. Specifies Firecrawl API key. Defaults is the `FIRECRAWL_API_KEY` environment variable. +- `config`: Optional. It contains Firecrawl API parameters. + + +This is the default configuration + +```python +{ + "limit": 5, + "tbs": None, + "lang": "en", + "country": "us", + "location": None, + "timeout": 60000, +} +``` diff --git a/crewai_tools/tools/firecrawl_search_tool/firecrawl_search_tool.py b/crewai_tools/tools/firecrawl_search_tool/firecrawl_search_tool.py new file mode 100644 index 000000000..ba4d4c242 --- /dev/null +++ b/crewai_tools/tools/firecrawl_search_tool/firecrawl_search_tool.py @@ -0,0 +1,119 @@ +from typing import TYPE_CHECKING, Any, Dict, Optional, Type, List + +from crewai.tools import BaseTool, EnvVar +from pydantic import BaseModel, ConfigDict, Field, PrivateAttr + +if TYPE_CHECKING: + from firecrawl import FirecrawlApp + + +try: + from firecrawl import FirecrawlApp + + FIRECRAWL_AVAILABLE = True +except ImportError: + FIRECRAWL_AVAILABLE = False + + +class FirecrawlSearchToolSchema(BaseModel): + query: str = Field(description="Search query") + + +class FirecrawlSearchTool(BaseTool): + """ + Tool for searching webpages using Firecrawl. To run this tool, you need to have a Firecrawl API key. + + Args: + api_key (str): Your Firecrawl API key. + config (dict): Optional. It contains Firecrawl API parameters. + + Default configuration options: + limit (int): Maximum number of pages to crawl. Default: 5 + tbs (str): Time before search. Default: None + lang (str): Language. Default: "en" + country (str): Country. Default: "us" + location (str): Location. Default: None + timeout (int): Timeout in milliseconds. Default: 60000 + """ + + model_config = ConfigDict( + arbitrary_types_allowed=True, validate_assignment=True, frozen=False + ) + model_config = ConfigDict( + arbitrary_types_allowed=True, validate_assignment=True, frozen=False + ) + name: str = "Firecrawl web search tool" + description: str = "Search webpages using Firecrawl and return the results" + args_schema: Type[BaseModel] = FirecrawlSearchToolSchema + api_key: Optional[str] = None + config: Optional[dict[str, Any]] = Field( + default_factory=lambda: { + "limit": 5, + "tbs": None, + "lang": "en", + "country": "us", + "location": None, + "timeout": 60000, + } + ) + _firecrawl: Optional["FirecrawlApp"] = PrivateAttr(None) + package_dependencies: List[str] = ["firecrawl-py"] + env_vars: List[EnvVar] = [ + EnvVar(name="FIRECRAWL_API_KEY", description="API key for Firecrawl services", required=True), + ] + + def __init__(self, api_key: Optional[str] = None, **kwargs): + super().__init__(**kwargs) + self.api_key = api_key + self._initialize_firecrawl() + + def _initialize_firecrawl(self) -> None: + try: + from firecrawl import FirecrawlApp # type: ignore + + self._firecrawl = FirecrawlApp(api_key=self.api_key) + except ImportError: + import click + + if click.confirm( + "You are missing the 'firecrawl-py' package. Would you like to install it?" + ): + import subprocess + + try: + subprocess.run(["uv", "add", "firecrawl-py"], check=True) + from firecrawl import FirecrawlApp + + self._firecrawl = FirecrawlApp(api_key=self.api_key) + except subprocess.CalledProcessError: + raise ImportError("Failed to install firecrawl-py package") + else: + raise ImportError( + "`firecrawl-py` package not found, please run `uv add firecrawl-py`" + ) + + def _run( + self, + query: str, + ) -> Any: + if not self._firecrawl: + raise RuntimeError("FirecrawlApp not properly initialized") + + return self._firecrawl.search( + query=query, + params=self.config, + ) + + +try: + from firecrawl import FirecrawlApp # type: ignore + + # Only rebuild if the class hasn't been initialized yet + if not hasattr(FirecrawlSearchTool, "_model_rebuilt"): + FirecrawlSearchTool.model_rebuild() + FirecrawlSearchTool._model_rebuilt = True +except ImportError: + """ + When this tool is not used, then exception can be ignored. + """ + pass diff --git a/crewai_tools/tools/generate_crewai_automation_tool/README.md b/crewai_tools/tools/generate_crewai_automation_tool/README.md new file mode 100644 index 000000000..c70741ca4 --- /dev/null +++ b/crewai_tools/tools/generate_crewai_automation_tool/README.md @@ -0,0 +1,50 @@ +# GenerateCrewaiAutomationTool + +## Description + +The GenerateCrewaiAutomationTool integrates with CrewAI Studio API to generate complete CrewAI automations from natural language descriptions. It translates high-level requirements into functional CrewAI implementations and returns direct links to Studio projects. + +## Environment Variables + +Set your CrewAI Personal Access Token (CrewAI Enterprise > Settings > Account > Personal Access Token): + +```bash +export CREWAI_PERSONAL_ACCESS_TOKEN="your_personal_access_token_here" +export CREWAI_PLUS_URL="https://app.crewai.com" # optional +``` + +## Example + +```python +from crewai_tools import GenerateCrewaiAutomationTool +from crewai import Agent, Task, Crew + +# Initialize tool +tool = GenerateCrewaiAutomationTool() + +# Generate automation +result = tool.run( + prompt="Generate a CrewAI automation that scrapes websites and stores data in a database", + organization_id="org_123" # optional but recommended +) + +print(result) +# Output: Generated CrewAI Studio project URL: https://studio.crewai.com/project/abc123 + +# Use with agent +agent = Agent( + role="Automation Architect", + goal="Generate CrewAI automations", + backstory="Expert at creating automated workflows", + tools=[tool] +) + +task = Task( + description="Create a lead qualification automation", + agent=agent, + expected_output="Studio project URL" +) + +crew = Crew(agents=[agent], tasks=[task]) +result = crew.kickoff() +``` \ No newline at end of file diff --git a/crewai_tools/tools/generate_crewai_automation_tool/generate_crewai_automation_tool.py b/crewai_tools/tools/generate_crewai_automation_tool/generate_crewai_automation_tool.py new file mode 100644 index 000000000..3d52ae3fa --- /dev/null +++ b/crewai_tools/tools/generate_crewai_automation_tool/generate_crewai_automation_tool.py @@ -0,0 +1,70 @@ +import os +from typing import List, Optional, Type + +import requests +from crewai.tools import BaseTool, EnvVar +from pydantic import BaseModel, Field + + +class GenerateCrewaiAutomationToolSchema(BaseModel): + prompt: str = Field( + description="The prompt to generate the CrewAI automation, e.g. 'Generate a CrewAI automation that will scrape the website and store the data in a database.'" + ) + organization_id: Optional[str] = Field( + default=None, + description="The identifier for the CrewAI Enterprise organization. If not specified, a default organization will be used.", + ) + + +class GenerateCrewaiAutomationTool(BaseTool): + name: str = "Generate CrewAI Automation" + description: str = ( + "A tool that leverages CrewAI Studio's capabilities to automatically generate complete CrewAI " + "automations based on natural language descriptions. It translates high-level requirements into " + "functional CrewAI implementations." + ) + args_schema: Type[BaseModel] = GenerateCrewaiAutomationToolSchema + crewai_enterprise_url: str = Field( + default_factory=lambda: os.getenv("CREWAI_PLUS_URL", "https://app.crewai.com"), + description="The base URL of CrewAI Enterprise. If not provided, it will be loaded from the environment variable CREWAI_PLUS_URL with default https://app.crewai.com.", + ) + personal_access_token: Optional[str] = Field( + default_factory=lambda: os.getenv("CREWAI_PERSONAL_ACCESS_TOKEN"), + description="The user's Personal Access Token to access CrewAI Enterprise API. If not provided, it will be loaded from the environment variable CREWAI_PERSONAL_ACCESS_TOKEN.", + ) + env_vars: List[EnvVar] = [ + EnvVar( + name="CREWAI_PERSONAL_ACCESS_TOKEN", + description="Personal Access Token for CrewAI Enterprise API", + required=True, + ), + EnvVar( + name="CREWAI_PLUS_URL", + description="Base URL for CrewAI Enterprise API", + required=False, + ), + ] + + def _run(self, **kwargs) -> str: + input_data = GenerateCrewaiAutomationToolSchema(**kwargs) + response = requests.post( + f"{self.crewai_enterprise_url}/crewai_plus/api/v1/studio", + headers=self._get_headers(input_data.organization_id), + json={"prompt": input_data.prompt}, + ) + + response.raise_for_status() + studio_project_url = response.json().get("url") + return f"Generated CrewAI Studio project URL: {studio_project_url}" + + def _get_headers(self, organization_id: Optional[str] = None) -> dict: + headers = { + "Authorization": f"Bearer {self.personal_access_token}", + "Content-Type": "application/json", + "Accept": "application/json", + } + + if organization_id: + headers["X-Crewai-Organization-Id"] = organization_id + + return headers diff --git a/crewai_tools/tools/github_search_tool/README.md b/crewai_tools/tools/github_search_tool/README.md new file mode 100644 index 000000000..c77e494c8 --- /dev/null +++ b/crewai_tools/tools/github_search_tool/README.md @@ -0,0 +1,67 @@ +# GithubSearchTool + +## Description +The GithubSearchTool is a Retrieval Augmented Generation (RAG) tool specifically designed for conducting semantic searches within GitHub repositories. Utilizing advanced semantic search capabilities, it sifts through code, pull requests, issues, and repositories, making it an essential tool for developers, researchers, or anyone in need of precise information from GitHub. + +## Installation +To use the GithubSearchTool, first ensure the crewai_tools package is installed in your Python environment: + +```shell +pip install 'crewai[tools]' +``` + +This command installs the necessary package to run the GithubSearchTool along with any other tools included in the crewai_tools package. + +## Example +Here’s how you can use the GithubSearchTool to perform semantic searches within a GitHub repository: +```python +from crewai_tools import GithubSearchTool + +# Initialize the tool for semantic searches within a specific GitHub repository +tool = GithubSearchTool( + gh_token='...', + github_repo='https://github.com/example/repo', + content_types=['code', 'issue'] # Options: code, repo, pr, issue +) + +# OR + +# Initialize the tool for semantic searches within a specific GitHub repository, so the agent can search any repository if it learns about during its execution +tool = GithubSearchTool( + gh_token='...', + content_types=['code', 'issue'] # Options: code, repo, pr, issue +) +``` + +## Arguments +- `gh_token` : The GitHub token used to authenticate the search. This is a mandatory field and allows the tool to access the GitHub API for conducting searches. +- `github_repo` : The URL of the GitHub repository where the search will be conducted. This is a mandatory field and specifies the target repository for your search. +- `content_types` : Specifies the types of content to include in your search. You must provide a list of content types from the following options: `code` for searching within the code, `repo` for searching within the repository's general information, `pr` for searching within pull requests, and `issue` for searching within issues. This field is mandatory and allows tailoring the search to specific content types within the GitHub repository. + +## Custom model and embeddings + +By default, the tool uses OpenAI for both embeddings and summarization. To customize the model, you can use a config dictionary as follows: + +```python +tool = GithubSearchTool( + config=dict( + llm=dict( + provider="ollama", # or google, openai, anthropic, llama2, ... + config=dict( + model="llama2", + # temperature=0.5, + # top_p=1, + # stream=true, + ), + ), + embedder=dict( + provider="google", + config=dict( + model="models/embedding-001", + task_type="retrieval_document", + # title="Embeddings", + ), + ), + ) +) +``` diff --git a/crewai_tools/tools/github_search_tool/github_search_tool.py b/crewai_tools/tools/github_search_tool/github_search_tool.py new file mode 100644 index 000000000..afde4fe92 --- /dev/null +++ b/crewai_tools/tools/github_search_tool/github_search_tool.py @@ -0,0 +1,88 @@ +from typing import List, Optional, Type, Any + +try: + from embedchain.loaders.github import GithubLoader + EMBEDCHAIN_AVAILABLE = True +except ImportError: + EMBEDCHAIN_AVAILABLE = False + +from pydantic import BaseModel, Field, PrivateAttr + +from ..rag.rag_tool import RagTool + + +class FixedGithubSearchToolSchema(BaseModel): + """Input for GithubSearchTool.""" + + search_query: str = Field( + ..., + description="Mandatory search query you want to use to search the github repo's content", + ) + + +class GithubSearchToolSchema(FixedGithubSearchToolSchema): + """Input for GithubSearchTool.""" + + github_repo: str = Field(..., description="Mandatory github you want to search") + content_types: List[str] = Field( + ..., + description="Mandatory content types you want to be included search, options: [code, repo, pr, issue]", + ) + + +class GithubSearchTool(RagTool): + name: str = "Search a github repo's content" + description: str = ( + "A tool that can be used to semantic search a query from a github repo's content. This is not the GitHub API, but instead a tool that can provide semantic search capabilities." + ) + summarize: bool = False + gh_token: str + args_schema: Type[BaseModel] = GithubSearchToolSchema + content_types: List[str] = Field( + default_factory=lambda: ["code", "repo", "pr", "issue"], + description="Content types you want to be included search, options: [code, repo, pr, issue]", + ) + _loader: Any | None = PrivateAttr(default=None) + + def __init__( + self, + github_repo: Optional[str] = None, + content_types: Optional[List[str]] = None, + **kwargs, + ): + if not EMBEDCHAIN_AVAILABLE: + raise ImportError("embedchain is not installed. Please install it with `pip install crewai-tools[embedchain]`") + super().__init__(**kwargs) + self._loader = GithubLoader(config={"token": self.gh_token}) + + if github_repo and content_types: + self.add(repo=github_repo, content_types=content_types) + self.description = f"A tool that can be used to semantic search a query the {github_repo} github repo's content. This is not the GitHub API, but instead a tool that can provide semantic search capabilities." + self.args_schema = FixedGithubSearchToolSchema + self._generate_description() + + def add( + self, + repo: str, + content_types: Optional[List[str]] = None, + ) -> None: + content_types = content_types or self.content_types + + super().add( + f"repo:{repo} type:{','.join(content_types)}", + data_type="github", + loader=self._loader, + ) + + def _run( + self, + search_query: str, + github_repo: Optional[str] = None, + content_types: Optional[List[str]] = None, + ) -> str: + if github_repo: + self.add( + repo=github_repo, + content_types=content_types, + ) + return super()._run(query=search_query) diff --git a/crewai_tools/tools/hyperbrowser_load_tool/README.md b/crewai_tools/tools/hyperbrowser_load_tool/README.md new file mode 100644 index 000000000..e95864f5a --- /dev/null +++ b/crewai_tools/tools/hyperbrowser_load_tool/README.md @@ -0,0 +1,42 @@ +# HyperbrowserLoadTool + +## Description + +[Hyperbrowser](https://hyperbrowser.ai) is a platform for running and scaling headless browsers. It lets you launch and manage browser sessions at scale and provides easy to use solutions for any webscraping needs, such as scraping a single page or crawling an entire site. + +Key Features: +- Instant Scalability - Spin up hundreds of browser sessions in seconds without infrastructure headaches +- Simple Integration - Works seamlessly with popular tools like Puppeteer and Playwright +- Powerful APIs - Easy to use APIs for scraping/crawling any site, and much more +- Bypass Anti-Bot Measures - Built-in stealth mode, ad blocking, automatic CAPTCHA solving, and rotating proxies + +For more information about Hyperbrowser, please visit the [Hyperbrowser website](https://hyperbrowser.ai) or if you want to check out the docs, you can visit the [Hyperbrowser docs](https://docs.hyperbrowser.ai). + +## Installation + +- Head to [Hyperbrowser](https://app.hyperbrowser.ai/) to sign up and generate an API key. Once you've done this set the `HYPERBROWSER_API_KEY` environment variable or you can pass it to the `HyperbrowserLoadTool` constructor. +- Install the [Hyperbrowser SDK](https://github.com/hyperbrowserai/python-sdk): + +``` +pip install hyperbrowser 'crewai[tools]' +``` + +## Example + +Utilize the HyperbrowserLoadTool as follows to allow your agent to load websites: + +```python +from crewai_tools import HyperbrowserLoadTool + +tool = HyperbrowserLoadTool() +``` + +## Arguments + +`__init__` arguments: +- `api_key`: Optional. Specifies Hyperbrowser API key. Defaults to the `HYPERBROWSER_API_KEY` environment variable. + +`run` arguments: +- `url`: The base URL to start scraping or crawling from. +- `operation`: Optional. Specifies the operation to perform on the website. Either 'scrape' or 'crawl'. Defaults is 'scrape'. +- `params`: Optional. Specifies the params for the operation. For more information on the supported params, visit https://docs.hyperbrowser.ai/reference/sdks/python/scrape#start-scrape-job-and-wait or https://docs.hyperbrowser.ai/reference/sdks/python/crawl#start-crawl-job-and-wait. diff --git a/crewai_tools/tools/hyperbrowser_load_tool/hyperbrowser_load_tool.py b/crewai_tools/tools/hyperbrowser_load_tool/hyperbrowser_load_tool.py new file mode 100644 index 000000000..a2571b94b --- /dev/null +++ b/crewai_tools/tools/hyperbrowser_load_tool/hyperbrowser_load_tool.py @@ -0,0 +1,107 @@ +import os +from typing import Any, Optional, Type, Dict, Literal, Union, List + +from crewai.tools import BaseTool, EnvVar +from pydantic import BaseModel, Field + + +class HyperbrowserLoadToolSchema(BaseModel): + url: str = Field(description="Website URL") + operation: Literal['scrape', 'crawl'] = Field(description="Operation to perform on the website. Either 'scrape' or 'crawl'") + params: Optional[Dict] = Field(description="Optional params for scrape or crawl. For more information on the supported params, visit https://docs.hyperbrowser.ai/reference/sdks/python/scrape#start-scrape-job-and-wait or https://docs.hyperbrowser.ai/reference/sdks/python/crawl#start-crawl-job-and-wait") + +class HyperbrowserLoadTool(BaseTool): + """HyperbrowserLoadTool. + + Scrape or crawl web pages and load the contents with optional parameters for configuring content extraction. + Requires the `hyperbrowser` package. + Get your API Key from https://app.hyperbrowser.ai/ + + Args: + api_key: The Hyperbrowser API key, can be set as an environment variable `HYPERBROWSER_API_KEY` or passed directly + """ + name: str = "Hyperbrowser web load tool" + description: str = "Scrape or crawl a website using Hyperbrowser and return the contents in properly formatted markdown or html" + args_schema: Type[BaseModel] = HyperbrowserLoadToolSchema + api_key: Optional[str] = None + hyperbrowser: Optional[Any] = None + package_dependencies: List[str] = ["hyperbrowser"] + env_vars: List[EnvVar] = [ + EnvVar(name="HYPERBROWSER_API_KEY", description="API key for Hyperbrowser services", required=False), + ] + + def __init__(self, api_key: Optional[str] = None, **kwargs): + super().__init__(**kwargs) + self.api_key = api_key or os.getenv('HYPERBROWSER_API_KEY') + if not api_key: + raise ValueError( + "`api_key` is required, please set the `HYPERBROWSER_API_KEY` environment variable or pass it directly" + ) + + try: + from hyperbrowser import Hyperbrowser + except ImportError: + raise ImportError("`hyperbrowser` package not found, please run `pip install hyperbrowser`") + + if not self.api_key: + raise ValueError("HYPERBROWSER_API_KEY is not set. Please provide it either via the constructor with the `api_key` argument or by setting the HYPERBROWSER_API_KEY environment variable.") + + self.hyperbrowser = Hyperbrowser(api_key=self.api_key) + + def _prepare_params(self, params: Dict) -> Dict: + """Prepare session and scrape options parameters.""" + try: + from hyperbrowser.models.session import CreateSessionParams + from hyperbrowser.models.scrape import ScrapeOptions + except ImportError: + raise ImportError( + "`hyperbrowser` package not found, please run `pip install hyperbrowser`" + ) + + if "scrape_options" in params: + if "formats" in params["scrape_options"]: + formats = params["scrape_options"]["formats"] + if not all(fmt in ["markdown", "html"] for fmt in formats): + raise ValueError("formats can only contain 'markdown' or 'html'") + + if "session_options" in params: + params["session_options"] = CreateSessionParams(**params["session_options"]) + if "scrape_options" in params: + params["scrape_options"] = ScrapeOptions(**params["scrape_options"]) + return params + + def _extract_content(self, data: Union[Any, None]): + """Extract content from response data.""" + content = "" + if data: + content = data.markdown or data.html or "" + return content + + def _run(self, url: str, operation: Literal['scrape', 'crawl'] = 'scrape', params: Optional[Dict] = {}): + try: + from hyperbrowser.models.scrape import StartScrapeJobParams + from hyperbrowser.models.crawl import StartCrawlJobParams + except ImportError: + raise ImportError( + "`hyperbrowser` package not found, please run `pip install hyperbrowser`" + ) + + params = self._prepare_params(params) + + if operation == 'scrape': + scrape_params = StartScrapeJobParams(url=url, **params) + scrape_resp = self.hyperbrowser.scrape.start_and_wait(scrape_params) + content = self._extract_content(scrape_resp.data) + return content + else: + crawl_params = StartCrawlJobParams(url=url, **params) + crawl_resp = self.hyperbrowser.crawl.start_and_wait(crawl_params) + content = "" + if crawl_resp.data: + for page in crawl_resp.data: + page_content = self._extract_content(page) + if page_content: + content += ( + f"\n{'-'*50}\nUrl: {page.url}\nContent:\n{page_content}\n" + ) + return content diff --git a/crewai_tools/tools/invoke_crewai_automation_tool/README.md b/crewai_tools/tools/invoke_crewai_automation_tool/README.md new file mode 100644 index 000000000..58ab4bbcc --- /dev/null +++ b/crewai_tools/tools/invoke_crewai_automation_tool/README.md @@ -0,0 +1,159 @@ +# InvokeCrewAIAutomationTool + +## Description + +The InvokeCrewAIAutomationTool provides CrewAI Platform API integration with external crew services. This tool allows you to invoke and interact with CrewAI Platform automations from within your CrewAI agents, enabling seamless integration between different crew workflows. + +## Features + +- **Dynamic Input Schema**: Configure custom input parameters for different crew automations +- **Automatic Polling**: Automatically polls for task completion with configurable timeout +- **Bearer Token Authentication**: Secure API authentication using bearer tokens +- **Comprehensive Error Handling**: Robust error handling for API failures and timeouts +- **Flexible Configuration**: Support for both simple and complex crew automation workflows + +## Installation + +Install the required dependencies: + +```shell +pip install 'crewai[tools]' +``` + +## Example + +### Basic Usage + +```python +from crewai_tools import InvokeCrewAIAutomationTool + +# Basic crew automation tool +tool = InvokeCrewAIAutomationTool( + crew_api_url="https://data-analysis-crew-[...].crewai.com", + crew_bearer_token="your_bearer_token_here", + crew_name="Data Analysis Crew", + crew_description="Analyzes data and generates insights" +) + +# Use the tool +result = tool.run() +``` + +### Advanced Usage with Custom Inputs + +```python +from crewai_tools import InvokeCrewAIAutomationTool +from pydantic import Field + +# Define custom input schema +custom_inputs = { + "year": Field(..., description="Year to retrieve the report for (integer)"), + "region": Field(default="global", description="Geographic region for analysis"), + "format": Field(default="summary", description="Report format (summary, detailed, raw)") +} + +# Create tool with custom inputs +tool = InvokeCrewAIAutomationTool( + crew_api_url="https://state-of-ai-report-crew-[...].crewai.com", + crew_bearer_token="your_bearer_token_here", + crew_name="State of AI Report", + crew_description="Retrieves a comprehensive report on state of AI for a given year and region", + crew_inputs=custom_inputs, + max_polling_time=15 * 60 # 15 minutes timeout +) + +# Use with custom parameters +result = tool.run(year=2024, region="north-america", format="detailed") +``` + +### Integration with CrewAI Agents + +```python +from crewai import Agent, Task, Crew +from crewai_tools import InvokeCrewAIAutomationTool + +# Create the automation tool +market_research_tool = InvokeCrewAIAutomationTool( + crew_api_url="https://market-research-automation-crew-[...].crewai.com", + crew_bearer_token="your_bearer_token_here", + crew_name="Market Research Automation", + crew_description="Conducts comprehensive market research analysis", + inputs={ + "year": Field(..., description="Year to use for the market research"), + } +) + +# Create an agent with the tool +research_agent = Agent( + role="Research Coordinator", + goal="Coordinate and execute market research tasks", + backstory="You are an expert at coordinating research tasks and leveraging automation tools.", + tools=[market_research_tool], + verbose=True +) + +# Create and execute a task +research_task = Task( + description="Conduct market research on AI tools market for 2024", + agent=research_agent, + expected_output="Comprehensive market research report" +) + +crew = Crew( + agents=[research_agent], + tasks=[research_task] +) + +result = crew.kickoff() +``` + +## Arguments + +### Required Parameters + +- `crew_api_url` (str): Base URL of the CrewAI Platform automation API +- `crew_bearer_token` (str): Bearer token for API authentication +- `crew_name` (str): Name of the crew automation +- `crew_description` (str): Description of what the crew automation does + +### Optional Parameters + +- `max_polling_time` (int): Maximum time in seconds to wait for task completion (default: 600 seconds = 10 minutes) +- `crew_inputs` (dict): Dictionary defining custom input schema fields using Pydantic Field objects + +## Custom Input Schema + +When defining `crew_inputs`, use Pydantic Field objects to specify the input parameters. These have to be compatible with the crew automation you are invoking: + +```python +from pydantic import Field + +crew_inputs = { + "required_param": Field(..., description="This parameter is required"), + "optional_param": Field(default="default_value", description="This parameter is optional"), + "typed_param": Field(..., description="Integer parameter", ge=1, le=100) # With validation +} +``` + +## Error Handling + +The tool provides comprehensive error handling for common scenarios: + +- **API Connection Errors**: Network connectivity issues +- **Authentication Errors**: Invalid or expired bearer tokens +- **Timeout Errors**: Tasks that exceed the maximum polling time +- **Task Failures**: Crew automations that fail during execution + +## API Endpoints + +The tool interacts with two main API endpoints: + +- `POST {crew_api_url}/kickoff`: Starts a new crew automation task +- `GET {crew_api_url}/status/{crew_id}`: Checks the status of a running task + +## Notes + +- The tool automatically polls the status endpoint every second until completion or timeout +- Successful tasks return the result directly, while failed tasks return error information +- The bearer token should be kept secure and not hardcoded in production environments +- Consider using environment variables for sensitive configuration like bearer tokens \ No newline at end of file diff --git a/crewai_tools/tools/invoke_crewai_automation_tool/invoke_crewai_automation_tool.py b/crewai_tools/tools/invoke_crewai_automation_tool/invoke_crewai_automation_tool.py new file mode 100644 index 000000000..09b076cc1 --- /dev/null +++ b/crewai_tools/tools/invoke_crewai_automation_tool/invoke_crewai_automation_tool.py @@ -0,0 +1,176 @@ +from crewai.tools import BaseTool +from pydantic import BaseModel, Field, create_model +from typing import Any, Type +import requests +import time + +class InvokeCrewAIAutomationInput(BaseModel): + """Input schema for InvokeCrewAIAutomationTool.""" + prompt: str = Field(..., description="The prompt or query to send to the crew") + +class InvokeCrewAIAutomationTool(BaseTool): + """ + A CrewAI tool for invoking external crew/flows APIs. + + This tool provides CrewAI Platform API integration with external crew services, supporting: + - Dynamic input schema configuration + - Automatic polling for task completion + - Bearer token authentication + - Comprehensive error handling + + Example: + Basic usage: + >>> tool = InvokeCrewAIAutomationTool( + ... crew_api_url="https://api.example.com", + ... crew_bearer_token="your_token", + ... crew_name="My Crew", + ... crew_description="Description of what the crew does" + ... ) + + With custom inputs: + >>> custom_inputs = { + ... "param1": Field(..., description="Description of param1"), + ... "param2": Field(default="default_value", description="Description of param2") + ... } + >>> tool = InvokeCrewAIAutomationTool( + ... crew_api_url="https://api.example.com", + ... crew_bearer_token="your_token", + ... crew_name="My Crew", + ... crew_description="Description of what the crew does", + ... crew_inputs=custom_inputs + ... ) + + Example: + >>> tools=[ + ... InvokeCrewAIAutomationTool( + ... crew_api_url="https://canary-crew-[...].crewai.com", + ... crew_bearer_token="[Your token: abcdef012345]", + ... crew_name="State of AI Report", + ... crew_description="Retrieves a report on state of AI for a given year.", + ... crew_inputs={ + ... "year": Field(..., description="Year to retrieve the report for (integer)") + ... } + ... ) + ... ] + """ + name: str = "invoke_amp_automation" + description: str = "Invokes an CrewAI Platform Automation using API" + args_schema: Type[BaseModel] = InvokeCrewAIAutomationInput + + crew_api_url: str + crew_bearer_token: str + max_polling_time: int = 10 * 60 # 10 minutes + + def __init__( + self, + crew_api_url: str, + crew_bearer_token: str, + crew_name: str, + crew_description: str, + max_polling_time: int = 10 * 60, + crew_inputs: dict[str, Any] = None): + """ + Initialize the InvokeCrewAIAutomationTool. + + Args: + crew_api_url: Base URL of the crew API service + crew_bearer_token: Bearer token for API authentication + crew_name: Name of the crew to invoke + crew_description: Description of the crew to invoke + max_polling_time: Maximum time in seconds to wait for task completion (default: 600 seconds = 10 minutes) + crew_inputs: Optional dictionary defining custom input schema fields + """ + # Create dynamic args_schema if custom inputs provided + if crew_inputs: + # Start with the base prompt field + fields = {} + + # Add custom fields + for field_name, field_def in crew_inputs.items(): + if isinstance(field_def, tuple): + fields[field_name] = field_def + else: + # Assume it's a Field object, extract type from annotation if available + fields[field_name] = (str, field_def) + + # Create dynamic model + args_schema = create_model('DynamicInvokeCrewAIAutomationInput', **fields) + else: + args_schema = InvokeCrewAIAutomationInput + + # Initialize the parent class with proper field values + super().__init__( + name=crew_name, + description=crew_description, + args_schema=args_schema, + crew_api_url=crew_api_url, + crew_bearer_token=crew_bearer_token, + max_polling_time=max_polling_time + ) + + def _kickoff_crew(self, inputs: dict[str, Any]) -> dict[str, Any]: + """Start a new crew task + + Args: + inputs: Dictionary containing the query and other input parameters + + Returns: + Dictionary containing the crew task response. The response will contain the crew id which needs to be returned to check the status of the crew. + """ + response = requests.post( + f"{self.crew_api_url}/kickoff", + headers={ + "Authorization": f"Bearer {self.crew_bearer_token}", + "Content-Type": "application/json", + }, + json={"inputs": inputs}, + ) + response_json = response.json() + return response_json + + def _get_crew_status(self, crew_id: str) -> dict[str, Any]: + """Get the status of a crew task + + Args: + crew_id: The ID of the crew task to check + + Returns: + Dictionary containing the crew task status + """ + response = requests.get( + f"{self.crew_api_url}/status/{crew_id}", + headers={ + "Authorization": f"Bearer {self.crew_bearer_token}", + "Content-Type": "application/json", + }, + ) + return response.json() + + def _run(self, **kwargs) -> str: + """Execute the crew invocation tool.""" + if kwargs is None: + kwargs = {} + + # Start the crew + response = self._kickoff_crew(inputs=kwargs) + + if response.get("kickoff_id") is None: + return f"Error: Failed to kickoff crew. Response: {response}" + + kickoff_id = response.get("kickoff_id") + + # Poll for completion + for i in range(self.max_polling_time): + try: + status_response = self._get_crew_status(crew_id=kickoff_id) + if status_response.get("state", "").lower() == "success": + return status_response.get("result", "No result returned") + elif status_response.get("state", "").lower() == "failed": + return f"Error: Crew task failed. Response: {status_response}" + except Exception as e: + if i == self.max_polling_time - 1: # Last attempt + return f"Error: Failed to get crew status after {self.max_polling_time} attempts. Last error: {e}" + + time.sleep(1) + + return f"Error: Crew did not complete within {self.max_polling_time} seconds" diff --git a/crewai_tools/tools/jina_scrape_website_tool/README.md b/crewai_tools/tools/jina_scrape_website_tool/README.md new file mode 100644 index 000000000..0278e5aa0 --- /dev/null +++ b/crewai_tools/tools/jina_scrape_website_tool/README.md @@ -0,0 +1,38 @@ +# JinaScrapeWebsiteTool + +## Description +A tool designed to extract and read the content of a specified website by using Jina.ai reader. It is capable of handling various types of web pages by making HTTP requests and parsing the received HTML content. This tool can be particularly useful for web scraping tasks, data collection, or extracting specific information from websites. + +## Installation +Install the crewai_tools package +```shell +pip install 'crewai[tools]' +``` + +## Example +```python +from crewai_tools import JinaScrapeWebsiteTool + +# To enable scraping any website it finds during its execution +tool = JinaScrapeWebsiteTool(api_key='YOUR_API_KEY') + +# Initialize the tool with the website URL, so the agent can only scrape the content of the specified website +tool = JinaScrapeWebsiteTool(website_url='https://www.example.com') + +# With custom headers +tool = JinaScrapeWebsiteTool( + website_url='https://www.example.com', + custom_headers={'X-Target-Selector': 'body, .class, #id'} +) +``` + +## Authentication +The tool uses Jina.ai's reader service. While it can work without an API key, Jina.ai may apply rate limiting or blocking to unauthenticated requests. For production use, it's recommended to provide an API key. + +## Arguments +- `website_url`: Mandatory website URL to read the file. This is the primary input for the tool, specifying which website's content should be scraped and read. +- `api_key`: Optional Jina.ai API key for authenticated access to the reader service. +- `custom_headers`: Optional dictionary of HTTP headers to use when making requests. + +## Note +This tool is an alternative to the standard `ScrapeWebsiteTool` that specifically uses Jina.ai's reader service for enhanced content extraction. Choose this tool when you need more sophisticated content parsing capabilities. \ No newline at end of file diff --git a/crewai_tools/tools/jina_scrape_website_tool/jina_scrape_website_tool.py b/crewai_tools/tools/jina_scrape_website_tool/jina_scrape_website_tool.py new file mode 100644 index 000000000..86f771cd0 --- /dev/null +++ b/crewai_tools/tools/jina_scrape_website_tool/jina_scrape_website_tool.py @@ -0,0 +1,52 @@ +from typing import Optional, Type + +import requests +from crewai.tools import BaseTool +from pydantic import BaseModel, Field + + +class JinaScrapeWebsiteToolInput(BaseModel): + """Input schema for JinaScrapeWebsiteTool.""" + + website_url: str = Field(..., description="Mandatory website url to read the file") + + +class JinaScrapeWebsiteTool(BaseTool): + name: str = "JinaScrapeWebsiteTool" + description: str = "A tool that can be used to read a website content using Jina.ai reader and return markdown content." + args_schema: Type[BaseModel] = JinaScrapeWebsiteToolInput + website_url: Optional[str] = None + api_key: Optional[str] = None + headers: dict = {} + + def __init__( + self, + website_url: Optional[str] = None, + api_key: Optional[str] = None, + custom_headers: Optional[dict] = None, + **kwargs, + ): + super().__init__(**kwargs) + if website_url is not None: + self.website_url = website_url + self.description = f"A tool that can be used to read {website_url}'s content and return markdown content." + self._generate_description() + + if custom_headers is not None: + self.headers = custom_headers + + if api_key is not None: + self.headers["Authorization"] = f"Bearer {api_key}" + + def _run(self, website_url: Optional[str] = None) -> str: + url = website_url or self.website_url + if not url: + raise ValueError( + "Website URL must be provided either during initialization or execution" + ) + + response = requests.get( + f"https://r.jina.ai/{url}", headers=self.headers, timeout=15 + ) + response.raise_for_status() + return response.text diff --git a/crewai_tools/tools/json_search_tool/README.md b/crewai_tools/tools/json_search_tool/README.md new file mode 100644 index 000000000..51510932e --- /dev/null +++ b/crewai_tools/tools/json_search_tool/README.md @@ -0,0 +1,55 @@ +# JSONSearchTool + +## Description +This tool is used to perform a RAG search within a JSON file's content. It allows users to initiate a search with a specific JSON path, focusing the search operation within that particular JSON file. If the path is provided at initialization, the tool restricts its search scope to the specified JSON file, thereby enhancing the precision of search results. + +## Installation +Install the crewai_tools package by executing the following command in your terminal: + +```shell +pip install 'crewai[tools]' +``` + +## Example +Below are examples demonstrating how to use the JSONSearchTool for searching within JSON files. You can either search any JSON content or restrict the search to a specific JSON file. + +```python +from crewai_tools import JSONSearchTool + +# Example 1: Initialize the tool for a general search across any JSON content. This is useful when the path is known or can be discovered during execution. +tool = JSONSearchTool() + +# Example 2: Initialize the tool with a specific JSON path, limiting the search to a particular JSON file. +tool = JSONSearchTool(json_path='./path/to/your/file.json') +``` + +## Arguments +- `json_path` (str): An optional argument that defines the path to the JSON file to be searched. This parameter is only necessary if the tool is initialized without a specific JSON path. Providing this argument restricts the search to the specified JSON file. + +## Custom model and embeddings + +By default, the tool uses OpenAI for both embeddings and summarization. To customize the model, you can use a config dictionary as follows: + +```python +tool = JSONSearchTool( + config=dict( + llm=dict( + provider="ollama", # or google, openai, anthropic, llama2, ... + config=dict( + model="llama2", + # temperature=0.5, + # top_p=1, + # stream=true, + ), + ), + embedder=dict( + provider="google", + config=dict( + model="models/embedding-001", + task_type="retrieval_document", + # title="Embeddings", + ), + ), + ) +) +``` diff --git a/crewai_tools/tools/json_search_tool/json_search_tool.py b/crewai_tools/tools/json_search_tool/json_search_tool.py new file mode 100644 index 000000000..820323eec --- /dev/null +++ b/crewai_tools/tools/json_search_tool/json_search_tool.py @@ -0,0 +1,47 @@ +from typing import Optional, Type + +from pydantic import BaseModel, Field + +from ..rag.rag_tool import RagTool + + +class FixedJSONSearchToolSchema(BaseModel): + """Input for JSONSearchTool.""" + + search_query: str = Field( + ..., + description="Mandatory search query you want to use to search the JSON's content", + ) + + +class JSONSearchToolSchema(FixedJSONSearchToolSchema): + """Input for JSONSearchTool.""" + + json_path: str = Field( + ..., description="File path or URL of a JSON file to be searched" + ) + + +class JSONSearchTool(RagTool): + name: str = "Search a JSON's content" + description: str = ( + "A tool that can be used to semantic search a query from a JSON's content." + ) + args_schema: Type[BaseModel] = JSONSearchToolSchema + + def __init__(self, json_path: Optional[str] = None, **kwargs): + super().__init__(**kwargs) + if json_path is not None: + self.add(json_path) + self.description = f"A tool that can be used to semantic search a query the {json_path} JSON's content." + self.args_schema = FixedJSONSearchToolSchema + self._generate_description() + + def _run( + self, + search_query: str, + json_path: Optional[str] = None, + ) -> str: + if json_path is not None: + self.add(json_path) + return super()._run(query=search_query) diff --git a/crewai_tools/tools/linkup/README.md b/crewai_tools/tools/linkup/README.md new file mode 100644 index 000000000..c51946a11 --- /dev/null +++ b/crewai_tools/tools/linkup/README.md @@ -0,0 +1,98 @@ +# Linkup Search Tool + +## Description + +The `LinkupSearchTool` is a tool designed for integration with the CrewAI framework. It provides the ability to query the Linkup API for contextual information and retrieve structured results. This tool is ideal for enriching workflows with up-to-date and reliable information from Linkup. + +--- + +## Features + +- Perform API queries to the Linkup platform using customizable parameters (`query`, `depth`, `output_type`). +- Gracefully handles API errors and provides structured feedback. +- Returns well-structured results for seamless integration into CrewAI processes. + +--- + +## Installation + +### Prerequisites + +- Linkup API Key + +### Steps + +1. ```shell + pip install 'crewai[tools]' + ``` + +2. Create a `.env` file in your project root and add your Linkup API Key: + ```plaintext + LINKUP_API_KEY=your_linkup_api_key + ``` + +--- + +## Usage + +### Basic Example + +Here is how to use the `LinkupSearchTool` in a CrewAI project: + +1. **Import and Initialize**: + ```python + from tools.linkup_tools import LinkupSearchTool + import os + from dotenv import load_dotenv + + load_dotenv() + + linkup_tool = LinkupSearchTool(api_key=os.getenv("LINKUP_API_KEY")) + ``` + +2. **Set Up an Agent and Task**: + ```python + from crewai import Agent, Task, Crew + + # Define the agent + research_agent = Agent( + role="Information Researcher", + goal="Fetch relevant results from Linkup.", + backstory="An expert in online information retrieval...", + tools=[linkup_tool], + verbose=True + ) + + # Define the task + search_task = Task( + expected_output="A detailed list of Nobel Prize-winning women in physics with their achievements.", + description="Search for women who have won the Nobel Prize in Physics.", + agent=research_agent + ) + + # Create and run the crew + crew = Crew( + agents=[research_agent], + tasks=[search_task] + ) + + result = crew.kickoff() + print(result) + ``` + +### Advanced Configuration + +You can customize the parameters for the `LinkupSearchTool`: + +- `query`: The search term or phrase. +- `depth`: The search depth (`"standard"` by default). +- `output_type`: The type of output (`"searchResults"` by default). + +Example: +```python +response = linkup_tool._run( + query="Women Nobel Prize Physics", + depth="standard", + output_type="searchResults" +) +``` \ No newline at end of file diff --git a/crewai_tools/tools/linkup/assets/icon.png b/crewai_tools/tools/linkup/assets/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..4848d4c6b19b1da998326dbd6a3efdf3671a48a3 GIT binary patch literal 32966 zcmX_HbwHEt*WL!BJ4Q-3qa-C2DFs15fg!PtR9aGUq%=x7Bn1VO8Xy9KFj}Pql#m*r zfKmgbVf*&Jzt``N#Xt9R-zV;Ku5+F9B;7VOq^IGg0RRB>H;nWw0045*UmzJ31?i`% zJ}4Og5CGiJyLKm}V6!mz9D4T=aj|VNXeZvdRq2<6(1&C&Ik*qi-2EKO(dgIO$hnLop7P7v-E^>WlE-rS5_VXL}3+D@GNT2zOcE`F$Ticu2 zii&O{S4RDdEUx`|eiIMNfK<>70eC>hWk1KTWvCBji}%Q+OE1JxGG)XFqDORF8Bsi% z7RBwWU;7aH2(ZPTj(L%#TyS1>tHdJ)2hBX#RNu=`oz(mjDSqj)g*Q zjDkv5r-m<;uK-3`#mNjgtDg3m%qYtAK&<$NloDSZgJzs0nJIV#;7wjjwi@dt_T&}n zzmufWSpe7`)6RIWc2Z*$RobGc@Xl~)laI9qvqKhwU#M*8L`UsC;DFK9@E+KlTu&m>&%AQjIalNkas`r^({)`?PSps2h`Czk?nE|cR_ z1a=4Ih?oQ(>CMVw)X*>fTTi9%a7d(spX{8C_RU(%uBOV$P9x#P7rVn(5NIHr-d7AS zw!BG}H6{ek{JY~*S8g?yW6|80QMZ1MMDW_nt6J_|9hNRwyFV8Q94{^n>pB);^ZlDW z+y6{OC3sWM{nW9KoVW;LWTO4F4=wt|2qxMVKXZ@(K}tQsIceFBZB3Xh(zIRZ1^#Cl zCZxzT<1yHmV>dPGIc||LS|{aKq{YeYw-z`-oD%f#8kZB#zkcw~S1TIO0h$|J=89t6 zvwr(!QM#B=I%=M>J(z@r7%~5S;wydE+2PknAVHgaldYzz!t>w^V!;UFd_(Iw8Mg%!1|-; zPkrt0!VPgd(OC-M9=nu>L5*|tylIl;<)!|{Ga`Sr2M=+MLLEepX3CNW3m=af4789P z3*#BlGupk1#~&c+7u?cat&?O7zz$kFN=+l)A&jLy%8eF((Mb+X8Cl~Z`+oYxt_;89Wa^=$0{Q8%a&mX`u(P%-D>2PO|;pNt$_Ea6w9|KwK@GOH1T` zSBy#6R3h9IDA#R5Xk#bzMEGE1mbBsxUg)&aaa)60e6`6u8ND$Lo| z8v9?r2~DPbVH|CSjKu2?pE`oW6Q+ARbMTH3QB)Rm;uIT(;X1!!LM?C1t13{1*I}$q zI@I9v*8gepX>pEI0E$B_B{u|2cQkygk0)OK51vn0%3PssJt&>z<5&}lJpFI z8QLzlTr|L=Y)7Z7noi7p$mp-*=mzLpd|Y7NKML;ps?!Lioi;gqzJ4}ugTv0ptcZ`Q zdeQ&}230RzL@MD0hYEBGW{O`mu?TowVVqV~>&OQD73RGd8!`hR`>HLEsGi%ui6b)^OKKG^uE$~3uG|huO&*Q9jNDw zS5gl&XM~AiBIm3aK-66*k{}}19wc;3Qi(vC)aogo9T|UfUFzvKZrn8-JKfQTzk$LW zw2+jy*~YXJ^I&o&O7$d4P-TBBKz&6=iUuHeMq!iOsc|Et;>lfV{8Q~*`~NOyFO@l1 z`}1<;pCva%QIs?d1VscNzVLg;pWuj+dma&O3FBoHsA7RL9NTr$%KY_SwcV0H`2CYr zcexNTw<-^G*wstNmsK_#U!9~9)mRsNFkuIQkOAPwGV)`!wEJxFfA3>?JH+Oe%8SOl z8RB4>Q`7z~he8$_N3cRhYuB-qq)P(3>B#?@gZztxNC#U8YV-B4%x+?hO=#x>Ur$Lu zgJoZ~m?2s7Q+sNr_YT$I1$JJuBiMh^^cVQ%hJZqVzWibVuJn2w-u^Neoix|2NIufS zm7A#R(QQeV;CF)zOz#=y9>Wmp+O+>EAFzDBYY33O?=`t-0=!LWN*3f?kCCKBjrhJA zD#GR{jqEjt@$tnXWPIt~ul+rue6%k=P6$Gq2de>$kn>%C?<)zux^{dq!>U$1k_`w( ze!i3#tMLEHj4U(eC8G#soV*0Ar~X>^_rcliNdm+3Q{<=U z+=o8?b#J7eUvX41c!BEp%CGLI%k1_N=Vwt-ezXAmVF+GA1B%jLoqD)YKPl%`%RN5%cj``Vz(3wB!T(AU|C*}Y=lFA6GVF^p8u%X{8zq)pv!GCoj+)s|DeNK7c(KN@P zDXDI1;eIEZNFZ6(dfdHhOe*NQ`@G*3%-sNJv((>fp&%5VZGh-3B(_LXLQrL8P9nqo zIjcUtCi=zvr5F1qXFHVBiqEf(8vi|7!Ws)@%wXeJ`QlwyBcObUD=O>b>gT8DMB(01 zNfYKgN-dxNIZf|J%b@6r80ts&$f7{+)^VJUpoN$dbjn@>7|#PQR+;^Fzq)w?C5*wP zd(SN0j0sYzA|7OWWp0R-x>!TouI2QHfQuX+s`zd0e=jimZ~IW)8Fm0G+BX2?OLsCD zCWQy#ih57+YAhWTgCgYxui?LdQDmOnHO7PExKU}gp*nJgff&tDC`w86s_5>oGx6qq zG-!iNgDeb~(VfjY7(E|NALI11UI4~%Da_^0b{onVbr%a8Qx17#_qs$~>VL-Pq+LJ_ zm#!*bID$6|A8O_uKYJ#HXW4w@_KAk+J$ZqiCqd_$Cp}Jqzgsl?&=PwY&WLNma-fQl z*MzfviQWT?dFIA66o%bwF{7_)76%f^HJPWGzFGdKCr*rpuT0^|MJKv1CM*1WaFhkN zSid$66}$SpS zUA!?k8gUXx-0in_6vKond|SGF!>%qx@5L2cqC&o zo=fd(B#pU4_+*r%%9Vyk&jzZTJ|8$bU8F8Vk@RM-z#7q!8Zz0O14fO09nmiq2X5#O ze4!(*V|09le<>1Rt{acN40LuZda^GmL*I3M!dfO$Moz42ajI$(;wdMf$dhp2@TNLl zI!f=FM(I@*tBNuD+N_^~Casl6j#PpP*_ZRE>w3bEGK9BnY;)>jhP_Qe*1%#A-?hSV z30%yE1&#%GecJQ7xbA?!@9PdrZ+N88d(|=2k#Yc2@`XzmIvF2?L*@D@qb84-2U^#I z9+gD(ap#Rvprqp+Q=nbVlIOLKqp#IBvay&eL2e5_!t$cT2 zzx{`o1Ft1SR1*85{p=w%M)vJv2=#qQ@y1L@V<~HR=a&4dPm$9afjWn^A#-F3d?x00 zw~g{nX%yPyt^u?X9V>PMrpsG4Na!?Cu+)Txl#QC5KsR%*Rr&Z!t)AHdCvVGMGk+`g zNAz%19j}J_iB&|+NB@j~WbbR!=FpoJGNM(`H`|f;-wtnJ1YOgGFTpe0FdP30ft;` z09FzTl`dO^#TU@v6>+J(>I#}IB@s%HGlUOZc#pPCpLry8j@~Y=Pr!z{AReq^%j)It z^QSepA08_hRkCZV+1SOlx*#9_YG;-@WinF2_2P+2&-kYc{N5&AIJFJ`F<*Vmi<=65 zRR86&>sLve-UImvcYX<0#v>s4O-T<|6n||viT(EJ5@It`Y;l8;$@VTA*#QdDz_;(E zfGY~b(+lRPU$}JjBoVC))-DuW1u=ALZ}9;EnXjI znfzrt&hGq?V{&}PYarckev#XNG{k6XT;bo^~A(Wpsz3%XC;u3ao z>{8O%Q|He7;MPJldU)RD8`PxI7$hd&<-vZUUd-()#vRT;_M!v>3K3Qb!O#5hepa1a zW*acVBi}c6*cF$}Q#7exF(f{sN_)JwonT(L*7GL?)t`_KnxZbc6r2#?ZBda(xRycEC=Zyyil zptBUp`9O&CR=fSD{lZ)ZX#9a{4p`C6RIb=MnV6_ zL$*(LsgxD~lB-Ct-$M|-JQ{vZl>WXL-`qg28&6-WO6fpvXS^S+*L)LIT?<;;GdHQ^ z>knm}JedLpkA=uJFf6+&xL_ZX&=luRy!(inG$0pv2H!>0!u>QFC{p%!AyC-_a!!TM zM!0EV& z4Io?jHCl&^r)-AUppbSlMTk3wv9mu42l7LD z^vpgb=9n|G`1^-@FLYOnxg46Q>d}7Y05vnPx0ttF0|l48Ksw`|2Ao#pOX8R=%|GSI zE4a=wgMMmtF_oV)vDIA?*}oV+{)kipIYU#h zu3abM@}ekO!85qx*?w33_iwM8unRJ^ew5~7~=zMqj>^ALH%FNB5}N_!>bA9%fssVSCC6T^kXHe0oAd=(MTr~&Q8uS z*?ETd5sOW|H{;YH^8`!mM5(_SLbvm5Q~ctn0#msd_zshwE>MNVX>+QcUT+ zng@zBQhCwa3$%?PTvMO{2CB_rw@lI-L zM>`pPAhLv9s;!ZWs{yqLaXK0**B5?@De?KPJ9*MTZO$z@Y1EP{FB^azx~)H*5HJfl z343@uN>)FyNEl3F%o3ybY@l12jN{+wC+#F2Nj^{G61hj_NWNX-SF&krg=qWsP#BWv z?S@>8Y}Pa?|7|=8CTdZ)3rjfWUjBx1WT74DNtFh^L686F>xC$76^*HHMzm3enYn=F z=TaL$*u`%@ZFn;@X*z>aXbpCLjlQB9^M45-A(;BaNRec0^ON(~V$*|q{}DbmfdE9@g?IYa#aM6ks~ohVj=e7YAz-D}9Hc8+x5$*3Puy4gvWP+kgs zcOmf)A?KW|=soj)Hb(wfe+I(+MBo1PmZ$m_*&4_V-krKGgU|}0BvE@)X#U8YzL*x; zUogt~qWm+4p|3>h=~SXj?`mS?(94{wkZ_{9YrQXj+CllZ`Zrih3@2re_PUwFS|HZU zo$bl^w*K*=?3fTOz})o>dXFL5+T!&nGYLfZejJwuUsV&BSgx1d1xOS@cg|8xx zSAcQbjC>L@gNn4p164^ov24K7)aq{JV>Xx$DHf8X)J=R^#t#9ElD!k=t8C)7I~f`* zj-GgmACp@kx1-U_F;F}fI`@d8+N5k}%+E6$ufg&2xp6lt9Rq#kQe-&B=cwll39rqs z7$Oek?tsq0$9%hiO<_gKIC=Q>KZK;cJ$cC62KV3SWmq5yEt1T6Lrw-DrhwQx>6M)S zPR%cixnN>7YTOi7ck_X|ombTJ?&;SvG>V0Q@VYqLWkAf0WM20+jV}J^to!`uvd;HB zSKCbMkC0DVVpeG1TE&UXKn6bw=*{5Yx3RQHop!v#u2`cMIYKo5mCa(z7>w1#gP`4^ zR)WU2vzsXF8<-eHN%{T$U|NY71E?jhEr}4o`ku^+%~C5E%{93I)ih2!USd~9k*}}) zOQ;xRI!FP;P@O##Zm?UQv=V1e{+Jr^1_yy78NwNCI#DYpjUCL#A{aS5Eye!1-LBr9 z1V`3^%kE8#Xcl-B&d6Z!_sf_(W1@I{+jYfmw;;|kCu%_hO2%VY-@;C*+lCIKrVU=< z->q(5mm?(`_$I#$Ebgi*eH0bXR~dP3$(hf(V4HLor(%Z>N$Zs45n!CQ&~Tj&WJlg!94@KD(;?;;rSLkX9?ex_@7Qzi0_eDQW2U@@s<6ruZ2 zyt`AU>XAuhI6Gb)`f8CaP4|a@>$rW3Jr;SRCC^t{YP#(uSYETuO zS%!q(3{`A!B;zql;XR&V7HPJHbQg(#u0|XuEc6AS(&;s^=thv>f?Jwd&v~6VSDO2# z3+}|`YC8wwXwvQDKKaKg1ldpJv7Pf{hUYlf5C?`jgsMp1-n`d&(ivPM(K|3hl z#gro4qp9FlK~zV6Nx(|#sIxVS9<@^cHB1@D?1{BAhQf-5dBy1JKph6Wg4$eavR+(l zMFWv?+>K0c-*3YxYgHa#rm(N#`4!pehg^*76>xgZmi~+L6LL%00qi&c?#d6I+co5o+!04JhGSel z*l-yC^)mT2v2B=J>m2)vAXI)*wZ7!u_L>fdm-VFE&nfljZ#8Z}p5~c>98tI|bg?@w z)*PYi%Pcm1=5IWigg_a{$Z z=syOd8UyY)*S7|0Snd}iV&6(S6N)*CXBGMBAzw<`0x^uf@_y+j&^f9CJ*OeS)52#?V%wA~`6rLN3u)iND&g91 zDUphijNpy6hrAUgA%&;hxipNnjK>#6!w48d(69TyPE#ngjNh-lwE%gJKYmDU&c?F- zZ)mZ-oee698P?=a7%o^Cyo*E`!QIyZmr#3r07t0!`#*7=DEC0*yrm~a3 z_ivltpVvQ$%P)Y#T+U zk&XGa6(_YR1`MQ*WYH}r`Qk5gIvHT>lzekq%JWUt%@`eo7UpGS?kGwHbM&N*oQHD;`7l-*~%vfb~&`{(5Ux zFk-Tki*Noy4$ZQp)&L&h39`Ggr_}&{{wGv31AN`?dt2#2P8dpOY~g4BRE7l|`M(Di z?0;U$2FL&r5<4hAn9`SAB~gHS7sGdsb)qzbW3W(JOAt+_|_?o%5b zD?83`8rnkij$d4R2ESQr-j%}zD_W%Z08RYgK})09NMRGy=A-~>kSUA>qw`@bf%>%nDJiqEvQ5UZl9rfuC{Fi2d98>`dBf# zPEt=~kepOm{)48}o|sfP+^_G*a;cQiMLz;z3N|?$2TG^w5a;AytcI9q3y3t~Jo^`Q z`owr9Swn?ElUJOpINQzcpvdH2C0^Tf^W*1*%iD*e3u?>)MbO01_N*~P8Be>6rXlV3eZsTkr&raH zu`=YyeCge%3lDVsB#86X<15=T*q$+MMpjC; z;P#9?vz_9bH=1ckT}|Pn*jFw~47dQ>H9>xHSNZ6NQMexLivYJgg(T``qA9QQ7^UG# zWM@xkC=kNt*F$%9qWzdm8lw)4V-hN~BEwig+}SLmn08jfgL7qFY(-?*cRn&+m6frsxau2RZ)s-t8LH^reCn_)IaRXWi*&r}pQ! zNuy7mn{uRozjU-A2<`8dR@`J|a4n90WzK6Q4fsZ;AQksK=wof1X(li9tL0=)bY4t) zRzcG`czAnp727er`Pm8M@tv8+w}v7Yz~dBQJ>6VGv7JP4q_>Nw$I7NnI$o*?*6-2t zC0y1vHka*wC+q>O5?T)wSatO>_^$DkS6poI+1n$xxy9GsiudDb#;jKUTw7d2{0`GS zBtJ~f{ZOBMzyruf)^5x*Q8?E*C8OFDM7!={^=$<%`^y$N_I&aR9cn9^d*?oPY7Gydj6m$K9-#4N!aFVJP~Ya7Gsl zFm7E)G=IUd8t8Jz)C}IOyPzVTC)s~i=&KA{dGGIMs3hlZ z$q&Z#C=8;)qG{_pcG-sJrSHUu49e(T@?cy5x6?GX_KGVs`l~PcF^#~%@4wmd7tgGzSpDD#COQGsS2#ZncH0Xp;^-u&X(ND zE7x$MY|j$ZB~hI$gOg3FlRG9aHz(x63qC}=-OM;h2}b4)I1W^U!|!o6M6Sb>DDHf0 zI0t!{yvg>@edS0Brw7_pG|zr$(Z=$|6YJ8nbuy?iOpDgkAu}hJPd0Y_DJ;g3mGFA8 z2fdLu?+1=dEnYz*lVyDydyg_Zy(?staU`ZIQdVVv;?{!pI}fv2nfy$-m7ASvfR>`| zq8DwkTNd!vbH1(QVc)r{bE@$Xxy4aMeGxF`gT@)fDDsvZ8>X8SP}y03S? z4?6DRy5_J)zk4)&`CaJ?AnO5Nj!tgaXngm|kD$c(ce%`RT9(?U9BAktN`J8B>t%be z2;{TJUwqV8O*=wxHPcF;O<^7@jd+5>X=%r&O!KW6J1UgPNeGglC80k}mE?I|aDKEQ z0l&}qd7RPJD8pN-SAMx(O?SW7``P7XuP_Jj*bxq49ld~ap^@T_zQ zUxn8F4Z;cA+}81#7#w-%S-Rwc&B2<S5T4IFyEt9;kOZVMyY0SL z-<)x^eg9%{YF`3Jj$=g2D>4@--uh^k`}*1ET)f?jrX?ka@6i-mZdEaVQ*kr!nsY_^ z!Q&`^1Ak5aOI4>gS8yS?;&q>ko9PPnRQaj7Nf6`aYug5eH}QF%M-6|J3(L>8IoNFn zKE8Pe3+?hoE1yu?2+SZ}#Yk2bQ=U`%i_auzzoReIE*XGF(!=omYt5+@SL$_!44nv; zvgdAjkBn3xw4}Kn|L8?ZVm!Cu(~LHG#ad0dc?)$~6pSJqA|x#xc9%&|Z-DNmQ6b17 zu~oPo-49E#>PU@!i!rj)-b|(!GV2o3JGq_a#Vte(uhYzZ>TE z`BCVt=N86+ZEBe@5qzx=IE$`7ug+{DO4`0SJ+j%Xb>j#bx-1`kP+^Vm>J__CyDv_I zl2SPEs>UCV$=9qek{$AWLnkRiX`Z3i_HbI}JwQS$GtB#XG#gwgur*KD4w z6pbYTEPIC5_ap$n(Z%o?O3Dv7fuU1Gr)GMXc^_Ai0CyH}59)w#Z+B)He%cy~yLV2I66ku!ZwWj`527MTPit4<``yV;dTuigYu zQ(Jk*hu=|x-^E1os!!77KL5>#wd#8BoNeYlub@fQ&b-eP+s6fjYi_&AwNj=$p-bzk zdT!oTnz23K$a0WCfCcc=)Z#BkCL2=@U#Vwc9z*=rBajMIj%+a|X?7> zlg_}MWJ`5Y2mx(mG4y8x>O`!pj_v}PiZ-t}n?oqrX6(5F7{`XIp2vP7WNZk-lM+J_%YH)8twW}aZmpnztTOXHQ-41 zmKHRH$px8BPB8UbB1(9c2b6&_?1PQ~j0{w(9mfU`Y2aro*RkiY2*CmN?z<(mfzEl;*Z@yz_~swaa|rgjuXGcOu+-?l30s=y_uX}KuifQTYAz1(#d6~* zs?8#4{p`wgP2}~LzSb*DtHZ@{9@q)@a0gP=4Jq}n>1CrMUe{LD6^uJj@W(#I4oEm0 z*Rj&>G2ILOxn=#lmE!O;!mJ;R?nCC80Ej3su6&TehKBMl{xwMAt z_*Q0@r;~i&N&QufJ}^lp(xgW%E)Q+3i9 z6`|lBp*V>Ghqp4a=Vr)`XJ*~5W*YQr`T8yInR&wJuQ~+@?3~Rh`ES5s{qR{U>v6|e z@ah5A%Z7JHD|*MglgRGgqa=3NQqVlh&HwO zr>=p+pd}zq6L%9jeKd?79P#|)_}rt(EK{Z{8<1rVYo~Q_(q40fJLxcHYP{La%iOU#4*Obr_fN6<>S4Iv zwSkAQeF%;cJF1|+FN_|4ls>ZI3+7oPLb8atPJY-!$|4?_Pmzt*5m{}IV_)%npg0bG zC?&Wr1!!0`4czMOoA&IkROU36_s^MXo|Ol45Ulmjh%x=^-DT*Q2R4;XG$_sJn8_ZB zSB_vzg$9O$<(Nmn!;P9a2o8ltjL6-d{oI|C*!cf=VivOPUABV4~PL-o!^5Re4v zdcwXwwv04NqhRN%nEQbzz4w$TB>XwW^JQ91IqC>OMx<7!#eZWchMl?LT&8hRbK|-X z?7oNf|Kh8|#u0C|X^sfnOg`VLzwvG2J{uvTeRl9DP-b%KQU>ge7FbP2ElO^s?~c52 zs~Pg)`+L_ys-Ks?-DvMN__OP5320e8xH&Y1{OY^ZX}RB!g7Kv3EHl5(R{oqR_C-S8 zxE-l5mCEJw=jU!paVbkyW_U7e@4`UpY;!7S?hsULe*CN+JIn%|bK~;SFam9Tmvv@P zcZA0GtxlT!oJIQlybn)p5Vw^AO>XSbz#sGRI{qk2mdw}IE4W$hzO>k=^ntm`?S*j_ zQ>kJ)MNy&C+3je%p1AW-L_OI8#hk&WMQ*|RZ;qx)x;4OKcx@oZ6Rm?Fhi;Eqkbjh| zoGrhrwvlKGs3iRr!yt<$w4kelTychneV@o>GWf?py)!pOVgHFV>%f<4B93}Ex>l@M zl4#Z2t(@&|1?R$W%&=R#9llf3O$64n=XMA1;BF~VoDYr%eXHlnC`J-829V{1B?Wcn zKl{rALDD^7oSq5S8O+nQay{}}-F+WVA*^Lr(n40XF%KDOZZd1Sd|mxW!1Z9)1=(k@ zz9JpxK1#D`f1!k@hX#i7bxkjBDPtvV$}^gs{9MH>-bweUo!_Z0@aaK5IG_m*3l&ni zVJ{r?)8l3vC*TD!Di?r~)w`=8C{>rC56kQF{(WzKu6PAGWVq$Z%`V467>u6!#tosw zU2FJua>b-&A@-q?+OSRX@c6a;Q^@HThp~MYYQfW6;N!qQS~H9;=9AV6(yx!4vx*lcOH{C zEt=f-BQ8azu6;gWZ9sT_Ulz>eZAW0$_Nddvws?IDrT#tg5PrL!<>hmJ{NmW7=ccZ% zB&hLBZP~kRt1ITvj;ES`l|u2>@#x^FLTCMkdupiW(2>hmDoTvClk3Yz(}L5Tp2;X0 zm5pYF`V*uEVSIvgeu``Ex_K%oGqq;w)cbztH~zGlc-c#o+{L&LQ^Ip;Iyx$~o6}Ef zwL42zk`BKrsD9O|4?V7SkB~dR=t2T7CFkwunzt|^(Z2g38HJh3BdPun*QEVh$YKH_ zv1h4^j1&)0t#dW^TMgYpnD)}@ZA9~(XI^}ZirhaEF3y_`B&94ila*v9LPkGGDB;?a z$oVr`ekCi+`>SvF+#8COT%%O8Ry8t z*+N9qTC)ZdnwJ*RYx*hD1}3J75I;VY69)VE!`?1inkIwgI&T7OkzM)tNPTHWih%mY zBiS7i;EwN{FL<7H22!=E-0tN^#a|u5_4}#(nrXR}pl#!@>x_d!aqIjG){mGRIS4((i4F}$WF%py=k&7sTRw@EB$^_hNEMOj=qI2d|}KU!Njt1Yhc_TMNt^WhoZ#LkeFbc0L}-HxhX)-;IUbN*pmd z)Y!rwZF%+HSG}&f@4Yy1t$oy^K4`-kx06Wb#WB_q_t{6Dqbas*=XKWZFkb@yN6P}Q zdF1SmNOvyzEr0NfZ`rL;_gvT>|5M-;iLb&d`DYSP-B{E7`gLb+p9Q9LgwcB4v!>@n!h78Xt zSPu=~Pd&BQBmPc4JBub&fL#>Bh&P=Go=zHtA(43EqXsiOdf_aa&s!XDn&1j5Q4~KB zj6Hp619YwjObf11&pVz%JP!lV;Y+R2v4LTUli z%$Dd|w`(ajeqDK21D6HpChj^z2ET=0E>k|?X%Vr4u*ubKuEP=UIBv(f}EJe%@7s zQU0ecF+JM@P|q9kFn!CcXFQx*us>s8zR^SVOwL%tN`xKSea0anCX8xR=QH4~cL?Z* zV58t2ReVs7nC1NlBEPS|1;8T8-n_!{eGuv-eURn^k$P7P~Azx1bE@O z$!E&cHo-pgjKDPL=||~@Gf10>Ti202pTgu32mLoy9}FQP@L?G3mP!>LzKzjh<8G~1 znBmV2{=!}IT0jf9CEKRVHYlAmr!W^?AycEqY`lV}s+q7%+{-$gv!38Ym(3Fbhd=@w zVG|xwHoj7c8;Hvqh7IY(Nx!Q(`NrpLKbM5?h@JBh&|4)%E)8NG`>u+FzJwf)4?voP|m z%(U8>z-UtRpvh}5GWGX|R_+kPKM}Rx61a9l51^hFMB-G_OOQapg=2c4`xX@?{0PpS ziom_bTDMIT2hgKYA0_D0^@N{gZo%F|{3$m-NM65GnI#9n4Nq5pEE$xETW4HYTTG`{ zmqP2##nz^&S6JE&<*MUDlDXcdk!ce&IrC2z(+m*pm8YsH!>A9J8hv4D7AiF66y>OU z3i_L#=@$kk^EFP_ZbTV&cm@pna{Sb)S5rQ~XDC7BCy19W7h!pR=hr{Bp;)S5C-rA( zJijgxbSNC6OjKq$E(p7((z+=bV5V7<0q+!C+(u>E6sd7MVL#Lsl%is-mWC7Jc z*r5F|RNIHI3LL0U4iG6}vlQ)#A!3h`u*2i13^ssnD$fy!pCM9} zoQB)ayMMVTK*M2|>Og~)4x=aKjo1@|?j3|G9qP~15E1;rw7w#EGUTi4GvbwToPp;T zXNn};bmZsT_bFzOB}yddlk_s09jA}Zx23Oq*$Pm*N0#rV83LZLvE#mwUAbNW2m%pk z6)6WNx>{~5w%VwJhtD(kK3$6@b}-O>d|F}ZW4tygs=qh4u2@z4CtgeLDO0J*9h4&G zPw|Sx+sS&Y<$Nlv`|Qv?o4SNOYx?35EhPL(KpynrDnBk1I=v{VB(*-Mrp=DD!>97l zqVYuW{>GjA3C-~t@Z>@8UaR5DrK4a;NCjQVf|Tu+3c92q=d7L$6*+WSxq;OtV1ZR3 zYrXwiXgTKQ=}nRI44%LtniBg621+>%viySBX|WjggxZN8`41kp{jjyBWZNG9>neWx9k(QfejGVH$`6ZibId|3`h zq{R$}K$8i>$gOGOQ!|K9pNNV=3*ERqi4o3)KQ5r`ptl5>SjsNGp@v6PPs}_X5&Gi* zXh%=?T}$P7mQ`oElhpTxQ{*u)qqOxTDs!S94khJ_CD-aGTKky(BpP3*w*g_i{4HMT z7Qpg)Nj$x$mG&JacG2@#tyq2_G4Urm%`8bJ2Ok%<4$Q!V*u+x1!H(Gt< z={9t+C&9gO?H7pSeY6Z%tCwi4KCTe7Ntyj`L(#d z<9+h5)Pry_y^|L5V?qh7`NMXm?Q(;^vjAJ~nbdSn7rdYVx2Tfbw*Y*WcOtbsjE6*8 zTBhIjP%sk1m>x%9H-4EMce17&Mv1P@u1$&2qISDBh_~Ka1CFu%lw)m*oQ_}ARmhrd z^>0br_zvc1Nt0YYbuQ)=)?e|L^F3vTh=S&sU{ZghkXF-b=DwAoD<(8h9ul|nsVho< zy%*ky^q8>R!{ zX_*7ex~Z}{FL)yk1$Ga&-+?ktDnS{_)zxH0War{(yDyW2R3CAMXuBfhpD9V2U^~z%omHIKn=c!-eJC@V2SBLnE|BTO%$?nRGrM7C&CGRAxnW3>GUcP z42OKR*%Cm#T2TW*gNpnnEH|-$oxDY$-Mw6x0dApd_Q;Vbaa^BgEp0A{UQk>Eda69? zE1|vo<4*JnMq%0n(_z&3f9-u$R9r#VB~9ZV+zAq#;Oa?%>sB4v=j>Ck4-*Xc-}M_Lv*DV7XTj4i#Sl~lS7v6Kp!sR5`#E({6im6qq80Orl8tU2jTfziSYY*Ch$<`rv)#2cK znwnrU@nab*;g6sxM6dcTY(( zHA2*B=${VbTZGx#z7jbto+U4f=Hb3JHFGVrydBdqsKq&m{G+mc?e&(Fv7n70xV|_`!Y`z@i@Nb4kJMn zLG;`(*z{e~iZC0T-~$`iEH`zt%Z1hV69SJcrGM9fQ6h>hrLL;*9Am1OJ_}*6n|t38Z}Wp;bUS7-77PhcGg=%mw!hkg>8e*xNArD0kp+R2!c^0YqcQ!^e=s1^(Etgt0K0@H{KIG+id#8DQjO~(mA%Y zUzstQnJHZUN}}hXQeqlMM1;~RgkdlSp1fY<{SGUI!_3ezQiP8&ya?9lyIS-~$7rvC z4Wi~@&l$dMe+;%az?=kU3ym?hepSVyT-b-R>CQO@0Lu7Bv5Po#Sn|jhzcS?QJCS7- z^IE>g&8p3$lI^Pq;NUxOZsVOsAwmd9oXzGRUP!qPP;T6iu-U+qMat&GBV>;74rb{U^zyqt1 zevSmSW7dP&-O@kmq-G=ca7hN?PRcMii3XQbg(?t zLWF5T`s#+#5sXOOr^}GRTJYK$>H@TufDT@fKQg(_T^1HrWB}p8b_n~o+t~ij{f-)4v$qEz zyup6LlbhxtkQ*{cu)}3D&-=OupI|uf`}h2TmcglklE5jv+<2SfFW-_VEKAe>?9stDgAgpTR9UtVLAxHCOASsw{CZ0NcCL$IL5Cp=ust3=mO zv%8<5o>0u8OG1O+Bq3jV;?tJRr@~V?V*gxmE8P$6a-9PxBg8*$EhT5_z)A18y-@Rs zQ&|Z+88mLc^@}?*?#!-sg)_y*dSaO}qlGxYn?DhM@Niq2JurWKRmv1spaS%va`%h zRI!w%L_Es}&8HRoH>?O5md!5ywnVN)f*2rj;zpSF%##=f$eumtyl;*fPx*mUa;Mo4 zLyVeW6$gw2<%s>9r08#scPL%}CwdoDnxPxZJBeg}H`Qw}|Hvdil3V)kIsLBxDoSlr zl#@QI#FEIi@mhBxf7>L>4Az7-k`t4}FAIJ91D@p@5TE?Gi6Dj|L~i5!#*&b1@xg#T zmJNY***w6pi4B36%c6`~W(RiF5Cd}w$qmxrXVu$^5tM+eiB6<)WIo97D{hzZ|shV&OuGrwMaia4vTR)&mNq z4V{*3FqlmSLdBOj+Yja0W_lpJZi2DzrvauCGx444$5_ZU?EvW{i9* zM|4V_{904$uQsib+D!{30@Cdvvax8E zCNYoEm!Y(S!-YvLh1cT~3^z5aroxb(?_iz@cpZ~r74U6N>;xhWQb>FROnN7ELvUdB z;v&rcFJvTQ>!2m1ZjkOl67} z%;@O}wk-;}qL=nZL+h=0s9+mi5t{c*y0A!tM_C#sWs z7VuFm1+l$sBEAF<2X|8!gu??55qSBUz{`PQM?HnJ#Sub8MWi(%qVsATC$u2Uc!Ilk z?BHhHn891S38tqpD`qzVAC+tF=FNq3orZfCVow0GvTdhW3&JP?5Mkn|m!fK>iU0b8 zO2>o|%X`44TIpC{1ngG}!4Q)@Uv;%r^WQ4|lCroGzO8}fE;T1Jvb`#=_Zp#9pgR>${TO=F>D%o`! zdmpT_e)TiOn5@1?N)`@>l$bfG`J3qej>~O06Q7Juwpn8PTa?y+ zPRKU$5W1W%@YSH;fP5Deu3MuPhJFGuw~ecAFnD$3(!M@xBqhcbVEHlio{tTJ=oXlP ziMIUi=$=+rWjh$tXDTgQWOm%3|AndkY`5D6e73}JEi$qCc`xC4u{2m6H6$D*22KXT z9_zJrzVnxa*X_XsQ`ixXcD?x;w2mmC@Pa3R?oLTOCy$s#O1M_?l$nXm<-7A6)g*;> zU}JY_u%?YfpXu1d%K7 z@}sbxs0}>Zf=@l9zcaVfV6b2cu>Udf5j{%sZ!l4Ffgj1W?O!9-Ng$KuB}sv^99%v& z&Nm1I@L%Jo2y>}&%Y0Sm(cxliAis98jPG%CDsnfcm&dHc4@d-}n1E=lQ82B8g#+|% zH3lbO@j3`KUYH;j%egP?VPv)^Dh+7KukBfg;K}x;iI-<+;jq@wggNlZofz8uuoAs? zi6aKUT-_Uh;^-?U-wRY&(3mibmta$sS2JupK`JbE>BM}wjezx{*dy#hYtxXlJYiqzsWAu z(1uBQk0*-oYmHI~-tqwFl%O&ncmbwSi|?OH_@!!0hP|omhzn~o0#Kt;{%I85EPvlj zrKKI7G)T|xk&j%1Itn2;FawLwycwWcixoIU62g!WWRftlImGdEHR#}gam65K(tx>9;Z}h};ykT+ZTPv8@wwHAfj6fdZTJ+p1 zr1;F97D+y0^U++uVh0D5AJ%hRz>-$b&Pk^@+Q1QG+24VJ&J;o>Tr*yA5tX*jS<%YO zCmNE#J$x74m0CBa3nodqZB@2}t`XVz_TUI%p1i3U&PHI-6@gW|?Iz&c82zBAJ@KWR zBQsifJcx3-=)xx7tJ*Em0T!vW2#zC+^O{b`KI=RtYO?j3 zgi)nfeU?oZG0|ykDdc~?K-=jRCj2RZDMUc_+K$jp-xEzHVive@UtD!oUQ+_=VWCM( z4!kf)?CY|z8S?KU7wWxv=GtWP1cxZy7CGgcutQyd5)pN!EMm5#Ruh(uV|I6 zEp9nZi~~$E;s@WZ#~k)vZ1@~w5nmdJA<37{Is5!w9{#+E-c7m?l$~&(L>1!eWh^Lr z605tK?`Gr6SgW!Kjh3 zmr1_%>i#$Ah4QEomTc#|%=TLY#h&YO@W%omR)xUAXdgougc3c#`4t4|Z3S>y>jKQvD(bHq*Xe6q-4W-`k2Ga_zTe0T}mwuCYMFr=2tX8{K$O2;tMe1yu3 zDMseKZG?R(pFk;hNBK1a6sPHjVB=()@yql%1|BkqjP!5b(M7xRY<)-+!L}yf=SheR zl@LW%Jo#CZH*&kS3tk2N%}?C+$I$lJ?NV^Ea>!u?S6BiZYgJcNfZ2{dX9+TAo<5X$ zzf@{W@p-Ji(E6`~PD2dQo4oP)oc?WO{0+z|?k;&b1iwQz6HP3EO*y^fFGAYs5?<*w zDkmD&+4$2NJ@BqQns~l_`VxJiWai74eS<3iP2{#^i4nHLuLYWd@(_~CatOD*Hs0|6 zDs&3k{LD-(MQRh55$t1ykKUoWBkYY)JCZ+OT7aIO-;IhfpS3uYMVw#lgF>ybb3awQ zgODY)ImeTyx7+INHPEMgD9t|j0sNT^w=qU&g-zoX`hIjfl;`i)1JqmToM||}>N9OJ ztTJ)y%0*1%n=heFO79gPtBIdmEJO?;mW=q7`bvJr?brn?TI=}Du^%qxl)Tko0H2~) zQTGiDb31Z^q?vD+wE1CvY!(VbdSR86;Rn^Co+%tz3*K>*2( z+BWWVQ8d{PzY5(A3~GM(Fauid;7L3`q+(k?MDjqGP%!)p#O&oJUbCSujMg~EEbB35 z(#Rh$3OoB4vdQ&^jsv1_nl&YVBA#u089wdmij_Dg@L2x&Yy= zf5-&{6#szZ$qLWxik+hQn8sUduZGyiM{{yzfNo^rAvl#yBnSVP|5c;y7*~`sKb)mX z^E%YTuU8*${mH=ld~T!}3Itl~xp8 zQX47OmJ;#nRr-6fv}wv~M}`dH(X`ps3hKL_3Q_s`%A#sby*CsvxccajtG%jaj(lPR zOAxt8)@CDrORq)kMn+B@)+E?&!)R}nVExFx2C&zOIXnFDv4>vv52XRwuT0YOf2Ug7 zvLdcKZdC*5ZUW@tj@%lX)%z&#IUYJqKrpG{AkBe?P);}YQf`eUjfha-e4B0ZaH+QH zh}I_IT*W6?VMXRsEc0yW`|N(22qwRzzqP(mkL!O*C@QfaKbCz%2sjO6&Qfx zhPCHT^^F#>3RlibKVA4AIlEDqSCb3ybO&L%ZAf9@_;@%9Jd$$i_@<=q86%zgo_{=( zoLzu`uX<9(Q+zc;Y5V-b8)YUDM^YP71*BJ|`pkpszL(6WsJ`g8gcOu}8bM0d@?`thg-5Znc$`UB zSvrm+j|c41Iq>3B3`lYWV(lFoVX0E&IQ3+1oEwh&zv$yiPp#7^Ud;jfg)vU>}+gHi~uR`*or zvw76v#CvdR#r`&74%K7n1iKrcM;KwYby#3*Yj1W?y!XmV!^ie1&wz9WdiAqnaP1@^ zTDFdv7aW{g;r)Z(Q}b|oIRG1<6O{*5Dm&3k$`=(ZG1_e8H6ruw-eo&qsmMI#*vrBt zV)lt9DFVk=jzva$bUNt+<0Rf_`;W>mK2er=dlb_Qfh;_CFwpymTzdg?y^>q*!Ivws zm?xxyA<%*2=rWRrTgox51Sk5 zHM&lLR=IA`f5ZtbB6E?Nx-TM)Oy%Jli`sE?1P^d1g%IelZML+#pt2NUD(&P|e-RX2 zgroa;I#A9-KZbvpLSv<_6t-g)478NvhIYS=zB8hF{+EbTLuki}Rzi471%w8+s zRyZA%I_EQYiKYvYcS+&$a7YY(B)v}i^4vrkG4oYTC#@pN--r*-B{>25FAeg&aR`a$6>SJXCtm9ea|+Ep06D)S4BVpGW9}WG%yJW2c9R7j^CeZYH1d zu71T$lC{77I7ZkhDS&7ir}f(I_)@=g7K}|^`d;==|K79=w+$t+2VC;R$vL0tX*i#T zjdI0+HFO=z$$7SWA{x-_(~mu$3Mtx{>_sQoxc6vyN)KEsjKC)EAU{~k$&+;x`>Yz)sq!N( zK#FvzBOXmPAf1=w{hrsqc;zUMNXp?7sI3R0BprwgcV>#sZ)i;KvW`zDd>P6l_a5CV zQjKn<{=NI(xd6=Gg!xcW4-_KdBV5q%M zFTrnZ=anSzNbex#fYNBq-1|uHI`@4yxC4YOo5;B@ahL#P79L0C79+&p63 zhAgD)HR91JJ$8;`gXv!<49{Nlj`yk1ls@&ONuq&dmTj5ZMv8s@W3~g;P@{2V?^-`| zn$0>R5H82yuc|r4^AU{@R9Qv%wlDEDC7c#fgF7>6+^`}_D@0uZ2V_dW?8z+tXfUeD z%Q{j?1^ZG5SJAKZh*hX1>>$rfAK#4he>0uF{_<<_XZJo$0z1Do5t3QVZ}BPO?r!+B z^b~8MIN$SjBoozMEch^+?sCy}IB8IBF&J7JnD9-O@$w@MCInM%S#BD8OpgT`)&Y6* zetDphXTel&tC-ubH1nl@`qtd36kUyUQ2gu*E<%VR-6u=9rOmEC_ux;a99g@HF6AQXDiycp zDhK%X0IR5pw+%BZQSu?$s!T#8mQ+v!Y1Xc7qWR%on?G&UI_9CHf_qZMA5^T5BHHFF z)}uGFkT&I;7v(sYj2wQzNbSS;zE!K!?tMqIurpQllyiHh<+9PV|A#}?bPUqa!6Zb9 zz$v8wQyVS)sdnitFSi`9apl7VLEIK(es^eWZM~`Lc^cBIR=6K}vb8~)nOm*A`!H4X zH9ns?gtC<5O`@|?j26tQD?i3F1ofJu$s^2%jJhYqH>d~Ek01x@>Vr2#sxwhG1@E{u zu`iR`3{p}x1-E5+myL16-aLrRW>t#di!S+{BiBUdt?5m7KTsb>w`yR^Ohj^OTM3 zLBXCHZ!L2k|Ga3Lht5w{U=aDt6}Yu6XegZYgZoMo@>f;U! z>k#d-S-*Fk4|+$Nna~2*xn`d-yl6Pa@?^*{7B*&#WflPucE3nz8J^4$M}DvRs9`|c3Rz;lD~l~-}x*pha)IAydc4>-1YTv^LsqM zMk~){o#L)-pA?6#i+M=r#~=Qoe2 z!Xu`b=ke)T@#jl29rz2T3h2|!>>cp`q9Z$j>8Me@P6hsLk5i^2>?(%!z{rCzRkBh+ zwdnncR83MkJE*5BE;`E#+Xhw;t_)#JgW;YvBIZB4YWC3DiV%zh;MkZMe^Ew^dNgDm z5MY)MrK-B%X~aVQnk?fJX!wL<_m-JWpg}-{R(llBK#3{rswh2zRcY>P25o7muNQ%2 z>|y6?_pD!EY#0}HsKW4{DGhOju48vr_T`IRnt!=gc(Jh-H|ioBA)f=Me&C;BlpS{n5RSel4(M$D3xA>%bBsHwpz zz=~7H--Jv_kHr=1ciIWV{z}dZ9g=O*WDPc}p(M1cs8FnvP$^9DNS5t4>7pIgDHhjc z3^{sH!qyMH-}j$I+;JHc+Aw)c3u2zS4jrRO&TNxbuXRTqUlr3AVdC?PHM$q@3~&Bm--iS>uqeCF!rstSWnoLo7_HWJ<~bgmlDxz^DNk+Lt& z-whQ45wB!f)_haGJL2XNY^#DvNb7>B<>>~k5nE4#&{zcx7EV-wY9ehz4^5)i*k#gs zt-LmR-p8-|X)FDNNmQ8aVrt}s zvbr>7ZFW5**r(gN)4O3fSoyGK)4x5{*Sboci;pV&mi`epPs%{>3Xai6x#w7Ju{?!h zOt!|_Fjd_Z=wk}QOe2Yf?J)_b@RB0BtKn%%xqBpDXzz(* zMGg%BK^Yz#urI*TQ>an!%BLt*Kk_9Rl(|yP+?b=H-)|Qo7H)h4 z#Ziz{;!VNiXh3R}z@l6ONs`m7yP~JBGb{Au%4wF2?BvF7W(2Q*k2HpQ^(-&&D}U9a zQim@JkTBL$?C-VG^FV!nNao;|XTR`IpXiUk4nd!3Cu`b^FWnzM-3<;Hf>BdYOk=L~ zoL_)>xItgu)9q-#lMr&bS1;;}Y!V#Xv1W~)b{g7Ox9n8U1tuf;$frVU$Fve=M+3@Y zbU&Q7#!8>R*MS6M|1F1NRdz-n33Us5dI`A1U7m57tnVoqF3u#9D}PpgzVX55Rfa)D z4DPzWrEZ<}R6>6ZYGhATPnYQ_hRa&NEwc}-ZCi?c3HHD=w4+pLUL0}Cy?7!Qv>|JY z-6EZ%czg~fw#m3iOjxaDgLdW&> z?7sCs`!m!C#4qX0R_2L+6~JG1lLCMSQN%2oM?|>or~PIZat~?wn<9(5+|zi zS|4E6ml_xc()PExOwgik6%&%?j$@hkobPvKb3T1LG6h@BOg`3ENC=-keI9+1j%qYP z&POTXdYY1LU1CaQ=~Zlz6hC>r9C1NOo8OtMWjFt_I2_94z>Iw3k;7{xvM?oyCOmzQ zRPa$?OkFUm;nW($Wj7G((kE`mqPtWApTQ@%mj&lM99D088;_rQI*ZfCri?dSmtgYc| zg|XbUn?LG_=H`?OTL@zUu9XJtQ>6%fkeikDZ`HlHmKn>Wp7`pY;EDu|z=FEeHv|B29jAo=!~il z`g~KTo=7|YFwXd!bRf|@C;B@g+OJgxgM-cfLfc;`{FJXLv;Jmi?6 zw)yw(>G8(-Q+|6;ZIp6qK@y^yx>Jx#dc4=_r}`S1Rcct#wr@WCY>&aDp)oO{?;|w| zW+D7ff7cMzrJa#z5+laSrxVG%Uv(9p!n+ljwfNNJcY}Y>rw*Kr@!AQVQteHP?J0SzH0@4}Ot<%9ORHkWQ=ZZg9&lSH8 zuoI`0{&B*sqO6cN3M&jNLH_TqmD9^#UF~*E_y+VNi0l%{XGVPA9zj-_9li7y!{KOj zJDXNtvXs8=bD_fSQOcmT*|v=m@0m)=rUdYtwj9^{zQlu{7yHdVXNizVpe-^6g9OG3 zak9L?AB?8f4+cFA_AR}_$hH2nuiOVQf!@J7_#vbX6ow-i1|Oq)^ic(*Aur7{{N7^Y zJRMn!D?jC@f`5RGH5B*y8LgMrv}gXIuJBXz?}d=T<|=yb)yKOl%?f@KRY*SdS15F% zIK^}~P058Hg=d+C@~!OtG_}*k<%z_qvI-+eF+ACte%lf^A)BMFO^$+FZxBQWKaMOh z-^B`pgI$fGi&F+z8kGS3ArRsc)?NJgs@zlAb8d!)w3(>&5*nMX?@C9ZZ-QtA_inh+ zfn0|bt&=PyHZlKsHbcyo|g=d#hifaxMb#{bg~|vglIn% z|K`t~M>oky9i&;8Ou zl-IOg9))v$V)(I-9Jb>s0ko?0@U!=rozIl{M)wU=0O-6g@bbvg<$9XwF2DiS{h9bM z4`S7TdU@bFy@rTUnX2wj@YvPf2Dp7jDj1NHz6UG7O5`49e`jy)i}=muGWl?-z4TYG zoJaNB)uTGpAR;oF=v3qd6o0jZiUW^rhwWEpqCkQ;%;s;jS7j?Ja|=M&ju=p$1$Dt{;^1$^0j{9sD*nS7ye z3K2Dh>_S%8Qj5c99I)9Ir*`#WEGEvb~{m4l3Q8&A0(85G8$k((Hl+uQN@Y*Q0K@}RD? zy)R_l5aim4D&I}*b9KKgL$*Zol3(;Hrr@NNXi z*yuxF60<D-ZMEs8HnvK~2(snWcOOG{v1I8c5iKTUpY@b8ypFYd1*-GGj^l%8drY zJ*^GknH&he7*hX_l#`~%=9?@M@R}P1SInX#WIs4#+mP2JuOe-(!+ijmnlwi+>U6*! zm31e&JzuNxQ~pnVWD@$)J9T*6gI+}_I0GDsG@k289X^5QrREP&?Oy!H?8_uE^Dxg;WA_VS|>&1fex%H{Q6H z!p(=k6e^-pFr^1#5cR^J&=i$Ym-V_HwRsJ}I1=rW4KvZGegIw+jGf%rRaOC}S>eOt zAhc`Qm};pKNN01ysS`Jo@0UYGsg zO{EQqp-B{PXYzbPDK!u|E2@%_`3SlBd}3p6DrkS_u3Xx@Gi?)hC+3N{a~Q`ueij%)Xl@HorKsTu3s{%I%NjNv`_W)&S2a_2=$QkJb=wA`KXtXWu zL<=KVU!t^03tV+`%cG$}9>KiFg#-vK3L6076|*nOI;b151#M!1g7_Z;nG8gG9=0r* zb2*9NxVyK*-jyb_E?i0+5*YJHHUID|IhKd8r%dI|D~we6s#o4mv^oBrp_UgTJOn#V z*)(qTo+@mWFQW5Qd7qSPxP=J|Ry|{RL;)t_FtsY(f_s$}1b|XZ4cMhCP`%7hmNO5b zxdL;M25v}XJDhwniB}@$swl;gIx2$h8l_SdwjAs-yHj6l?Z3rPxDdr5fAl_N&z9Pk zJ~mvVXCtJg^q9B;odZXc17*77Jj%Y_V-QWD6MB)CW5V)Pgpn4r}CSM zR#Ay(8uL4-;ABm5%jrMI>Y?QD=_r+1h5ke_#uOv@=ew2Y5D6HRs-xdK_zb=cOB*#r z!Z|h0Z95hv|E3Z|xD0F+#W4YOzhC0I_1s2YZX-o1Ne{$m{UL_>K&f?#i@M5uhFG>Y zpsAF1i`Z00_6>ba9k8a;Sp2@M;Fp-=bfzeQ{Ifcfwz^5I%c#i5!bv3TP0X~DwAJvRFaW!PXb?l097(>CTsPiGK6!a`B#D&XZJ-+PIT2pp z@Tb}J2E9=hhWhewH&rbSyrs7_+5XM^Kx9)Ju4I_gh4Db{VcGr0hIgIQp9Nskp5m-L+*op5)eeE!BV&&N6RB#uBz01EGd$*Y_ zbi6cXMzwQTw%W@qcOg)0*+-SI(2FyphPX=G@H+oa7V40S?NqJ z)KrX*bW2f%r=U!|po86&S{Ny9e~)T?pZ!oXF=QK6xN^x!W)KuiF&7&3DiN=)PiZ;T%UMD7QSvIB_(W_n=pRdP>0pL0$MuXdR`O4dMukJ-&+pvY+_ zFkHnqp=H&#(3mSPFzQ|rv_qGJjI~B~DW6@|E-bJZ#HRlsF8sdifOyVMJP2ic-;de@ z#VI0bkKLn6MS%LmuUCKXlX0?V;%nlq7~q8n{e>zTF~GMG2@*nvDkb_UHMr4$LF#^M zRMZdf*KNPF%>H7y2-k1e%km35M`%>!hBi0)uUEWE$CY=g98e|Bbt+HLJQ&_rj&dd}(YnJ6UC^*j;_4m`&1GJ-eE7&P)2Y z$-&&@+?ghV=%R>n_sAUA%XUq;AJM6}_s?;9Rg4ng{gf-6>8l(`uxrZ)rA}A#(|4`s1+25|y1+A zEXh|8O1zj8pfFKNiHIe)jzWk1;?vO17)*QhiG!>$>3B%OT!rpXlK%)t@f!bK^sHL}92)oO8OwLoD% z+|N9@brU>mB@~tbJs9O~K(x_=AIgZ}Bn=^a_^3G&MLy1lBYS#3e5Lt?S7~G#2V`62 zgMjqN@4H+9H{D%6Zo@009X~tG?~3=!w4e`2hjP1_?zv-hnz~9EXakwR=8iBcy6^X}U*F1XW$K{|qo8$$O#BO~NqOP1#SX2=m?Ar+5 zeI6-GCyS0RV{UueV3(*Jj^sJwoYGPpNg%V%kl8a&A6fRAQ0AR?V8-sv1s3Ucdgb@~ z$gKdTRr)9qq^qq+k~UcO{R~MagN;g^!fL}*DXumMZ7L?FMbg@n^3*bmj-ec2qS5U> zWSe}IQb%uQq%3XD4W8{{@R+q3pW19|h3fbx>#CgL1W|Wb8Hds)(z=`gcO(yt((Th8 z%n0cCZY@iN`a;g*iMGT_A1MX|bSZX@rDg zjf#+hXYq0-(SpnazlQxH!FpKaxbt5_DD zD<2e|mnk3=xG=!MMB%nu;P_p~aQ_x=KmP|?$W__3LqT|6i@R zgMS!Jc~HCzvnRAHc`zMiUvb0D9(ezWlIcl+c>;?%=|A23Zvy`r5pKX2Ft63G|Id&A zxyZ&)A^fi!@b;u!k&Y?Im6?yJe0F`k%iEe&Iz~h7bE+-x2^m2>)j^E6{%* x?f-ZAzs^|p$A7B`2nZ%01uS97eGWSWBn{11m~!=C^E literal 0 HcmV?d00001 diff --git a/crewai_tools/tools/linkup/linkup_search_tool.py b/crewai_tools/tools/linkup/linkup_search_tool.py new file mode 100644 index 000000000..634ba2863 --- /dev/null +++ b/crewai_tools/tools/linkup/linkup_search_tool.py @@ -0,0 +1,78 @@ +import os +from typing import Any, List + +from crewai.tools import BaseTool, EnvVar + +try: + from linkup import LinkupClient + + LINKUP_AVAILABLE = True +except ImportError: + LINKUP_AVAILABLE = False + LinkupClient = Any # type placeholder when package is not available + +from pydantic import PrivateAttr + + +class LinkupSearchTool(BaseTool): + name: str = "Linkup Search Tool" + description: str = ( + "Performs an API call to Linkup to retrieve contextual information." + ) + _client: LinkupClient = PrivateAttr() # type: ignore + description: str = ( + "Performs an API call to Linkup to retrieve contextual information." + ) + _client: LinkupClient = PrivateAttr() # type: ignore + package_dependencies: List[str] = ["linkup-sdk"] + env_vars: List[EnvVar] = [ + EnvVar(name="LINKUP_API_KEY", description="API key for Linkup", required=True), + ] + + def __init__(self, api_key: str | None = None): + """ + Initialize the tool with an API key. + """ + super().__init__() + try: + from linkup import LinkupClient + except ImportError: + import click + + if click.confirm( + "You are missing the 'linkup-sdk' package. Would you like to install it?" + ): + import subprocess + + subprocess.run(["uv", "add", "linkup-sdk"], check=True) + from linkup import LinkupClient + + else: + raise ImportError( + "The 'linkup-sdk' package is required to use the LinkupSearchTool. " + "Please install it with: uv add linkup-sdk" + ) + self._client = LinkupClient(api_key=api_key or os.getenv("LINKUP_API_KEY")) + + def _run( + self, query: str, depth: str = "standard", output_type: str = "searchResults" + ) -> dict: + """ + Executes a search using the Linkup API. + + :param query: The query to search for. + :param depth: Search depth (default is "standard"). + :param output_type: Desired result type (default is "searchResults"). + :return: A dictionary containing the results or an error message. + """ + try: + response = self._client.search( + query=query, depth=depth, output_type=output_type + ) + results = [ + {"name": result.name, "url": result.url, "content": result.content} + for result in response.results + ] + return {"success": True, "results": results} + except Exception as e: + return {"success": False, "error": str(e)} diff --git a/crewai_tools/tools/llamaindex_tool/README.md b/crewai_tools/tools/llamaindex_tool/README.md new file mode 100644 index 000000000..cd8f4cd99 --- /dev/null +++ b/crewai_tools/tools/llamaindex_tool/README.md @@ -0,0 +1,53 @@ +# LlamaIndexTool Documentation + +## Description +This tool is designed to be a general wrapper around LlamaIndex tools and query engines, enabling you to leverage LlamaIndex resources +in terms of RAG/agentic pipelines as tools to plug into CrewAI agents. + +## Installation +To incorporate this tool into your project, follow the installation instructions below: +```shell +pip install 'crewai[tools]' +``` + +## Example +The following example demonstrates how to initialize the tool and execute a search with a given query: + +```python +from crewai_tools import LlamaIndexTool + +# Initialize the tool from a LlamaIndex Tool + +## Example 1: Initialize from FunctionTool +from llama_index.core.tools import FunctionTool + +your_python_function = lambda ...: ... +og_tool = FunctionTool.from_defaults(your_python_function, name="", description='') +tool = LlamaIndexTool.from_tool(og_tool) + +## Example 2: Initialize from LlamaHub Tools +from llama_index.tools.wolfram_alpha import WolframAlphaToolSpec +wolfram_spec = WolframAlphaToolSpec(app_id="") +wolfram_tools = wolfram_spec.to_tool_list() +tools = [LlamaIndexTool.from_tool(t) for t in wolfram_tools] + + +# Initialize Tool from a LlamaIndex Query Engine + +## NOTE: LlamaIndex has a lot of query engines, define whatever query engine you want +query_engine = index.as_query_engine() +query_tool = LlamaIndexTool.from_query_engine( + query_engine, + name="Uber 2019 10K Query Tool", + description="Use this tool to lookup the 2019 Uber 10K Annual Report" +) + +``` + +## Steps to Get Started +To effectively use the `LlamaIndexTool`, follow these steps: + +1. **Install CrewAI**: Confirm that the `crewai[tools]` package is installed in your Python environment. +2. **Install and use LlamaIndex**: Follow LlamaIndex documentation (https://docs.llamaindex.ai/) to setup a RAG/agent pipeline. + + diff --git a/crewai_tools/tools/llamaindex_tool/llamaindex_tool.py b/crewai_tools/tools/llamaindex_tool/llamaindex_tool.py new file mode 100644 index 000000000..ba2605816 --- /dev/null +++ b/crewai_tools/tools/llamaindex_tool/llamaindex_tool.py @@ -0,0 +1,82 @@ +from typing import Any, Optional, Type, cast + +from crewai.tools import BaseTool +from pydantic import BaseModel, Field + + +class LlamaIndexTool(BaseTool): + """Tool to wrap LlamaIndex tools/query engines.""" + + llama_index_tool: Any + + def _run( + self, + *args: Any, + **kwargs: Any, + ) -> Any: + """Run tool.""" + from llama_index.core.tools import BaseTool as LlamaBaseTool + + tool = cast(LlamaBaseTool, self.llama_index_tool) + + if self.result_as_answer: + return tool(*args, **kwargs).content + + return tool(*args, **kwargs) + + @classmethod + def from_tool(cls, tool: Any, **kwargs: Any) -> "LlamaIndexTool": + from llama_index.core.tools import BaseTool as LlamaBaseTool + + if not isinstance(tool, LlamaBaseTool): + raise ValueError(f"Expected a LlamaBaseTool, got {type(tool)}") + tool = cast(LlamaBaseTool, tool) + + if tool.metadata.fn_schema is None: + raise ValueError( + "The LlamaIndex tool does not have an fn_schema specified." + ) + args_schema = cast(Type[BaseModel], tool.metadata.fn_schema) + + return cls( + name=tool.metadata.name, + description=tool.metadata.description, + args_schema=args_schema, + llama_index_tool=tool, + **kwargs, + ) + + @classmethod + def from_query_engine( + cls, + query_engine: Any, + name: Optional[str] = None, + description: Optional[str] = None, + return_direct: bool = False, + **kwargs: Any, + ) -> "LlamaIndexTool": + from llama_index.core.query_engine import BaseQueryEngine + from llama_index.core.tools import QueryEngineTool + + if not isinstance(query_engine, BaseQueryEngine): + raise ValueError(f"Expected a BaseQueryEngine, got {type(query_engine)}") + + # NOTE: by default the schema expects an `input` variable. However this + # confuses crewAI so we are renaming to `query`. + class QueryToolSchema(BaseModel): + """Schema for query tool.""" + + query: str = Field(..., description="Search query for the query tool.") + + # NOTE: setting `resolve_input_errors` to True is important because the schema expects `input` but we are using `query` + query_engine_tool = QueryEngineTool.from_defaults( + query_engine, + name=name, + description=description, + return_direct=return_direct, + resolve_input_errors=True, + ) + # HACK: we are replacing the schema with our custom schema + query_engine_tool.metadata.fn_schema = QueryToolSchema + + return cls.from_tool(query_engine_tool, **kwargs) diff --git a/crewai_tools/tools/mdx_search_tool/README.md b/crewai_tools/tools/mdx_search_tool/README.md new file mode 100644 index 000000000..71b58131a --- /dev/null +++ b/crewai_tools/tools/mdx_search_tool/README.md @@ -0,0 +1,57 @@ +# MDXSearchTool + +## Description +The MDX Search Tool, a key component of the `crewai_tools` package, is designed for advanced market data extraction, offering invaluable support to researchers and analysts requiring immediate market insights in the AI sector. With its ability to interface with various data sources and tools, it streamlines the process of acquiring, reading, and organizing market data efficiently. + +## Installation +To utilize the MDX Search Tool, ensure the `crewai_tools` package is installed. If not already present, install it using the following command: + +```shell +pip install 'crewai[tools]' +``` + +## Example +Configuring and using the MDX Search Tool involves setting up environment variables and utilizing the tool within a crewAI project for market research. Here's a simple example: + +```python +from crewai_tools import MDXSearchTool + +# Initialize the tool so the agent can search any MDX content if it learns about during its execution +tool = MDXSearchTool() + +# OR + +# Initialize the tool with a specific MDX file path for exclusive search within that document +tool = MDXSearchTool(mdx='path/to/your/document.mdx') +``` + +## Arguments +- mdx: **Optional** The MDX path for the search. Can be provided at initialization + +## Custom model and embeddings + +By default, the tool uses OpenAI for both embeddings and summarization. To customize the model, you can use a config dictionary as follows: + +```python +tool = MDXSearchTool( + config=dict( + llm=dict( + provider="ollama", # or google, openai, anthropic, llama2, ... + config=dict( + model="llama2", + # temperature=0.5, + # top_p=1, + # stream=true, + ), + ), + embedder=dict( + provider="google", + config=dict( + model="models/embedding-001", + task_type="retrieval_document", + # title="Embeddings", + ), + ), + ) +) +``` diff --git a/crewai_tools/tools/mdx_search_tool/mdx_search_tool.py b/crewai_tools/tools/mdx_search_tool/mdx_search_tool.py new file mode 100644 index 000000000..807da62fe --- /dev/null +++ b/crewai_tools/tools/mdx_search_tool/mdx_search_tool.py @@ -0,0 +1,56 @@ +from typing import Optional, Type + +from pydantic import BaseModel, Field + +try: + from embedchain.models.data_type import DataType + EMBEDCHAIN_AVAILABLE = True +except ImportError: + EMBEDCHAIN_AVAILABLE = False + +from ..rag.rag_tool import RagTool + + +class FixedMDXSearchToolSchema(BaseModel): + """Input for MDXSearchTool.""" + + search_query: str = Field( + ..., + description="Mandatory search query you want to use to search the MDX's content", + ) + + +class MDXSearchToolSchema(FixedMDXSearchToolSchema): + """Input for MDXSearchTool.""" + + mdx: str = Field(..., description="File path or URL of a MDX file to be searched") + + +class MDXSearchTool(RagTool): + name: str = "Search a MDX's content" + description: str = ( + "A tool that can be used to semantic search a query from a MDX's content." + ) + args_schema: Type[BaseModel] = MDXSearchToolSchema + + def __init__(self, mdx: Optional[str] = None, **kwargs): + super().__init__(**kwargs) + if mdx is not None: + self.add(mdx) + self.description = f"A tool that can be used to semantic search a query the {mdx} MDX's content." + self.args_schema = FixedMDXSearchToolSchema + self._generate_description() + + def add(self, mdx: str) -> None: + if not EMBEDCHAIN_AVAILABLE: + raise ImportError("embedchain is not installed. Please install it with `pip install crewai-tools[embedchain]`") + super().add(mdx, data_type=DataType.MDX) + + def _run( + self, + search_query: str, + mdx: Optional[str] = None, + ) -> str: + if mdx is not None: + self.add(mdx) + return super()._run(query=search_query) diff --git a/crewai_tools/tools/mongodb_vector_search_tool/README.md b/crewai_tools/tools/mongodb_vector_search_tool/README.md new file mode 100644 index 000000000..c66dfcf43 --- /dev/null +++ b/crewai_tools/tools/mongodb_vector_search_tool/README.md @@ -0,0 +1,87 @@ +# MongoDBVectorSearchTool + +## Description +This tool is specifically crafted for conducting vector searches within docs within a MongoDB database. Use this tool to find semantically similar docs to a given query. + +MongoDB can act as a vector database that is used to store and query vector embeddings. You can follow the docs here: +https://www.mongodb.com/docs/atlas/atlas-vector-search/vector-search-overview/ + +## Installation +Install the crewai_tools package with MongoDB support by executing the following command in your terminal: + +```shell +pip install crewai-tools[mongodb] +``` + +or + +``` +uv add crewai-tools --extra mongodb +``` + +## Example +To utilize the MongoDBVectorSearchTool for different use cases, follow these examples: + +```python +from crewai_tools import MongoDBVectorSearchTool + +# To enable the tool to search any website the agent comes across or learns about during its operation +tool = MongoDBVectorSearchTool( + database_name="example_database', + collection_name='example_collections', + connection_string="", +) +``` + +or + +```python +from crewai_tools import MongoDBVectorSearchConfig, MongoDBVectorSearchTool + +# Setup custom embedding model and customize the parameters. +query_config = MongoDBVectorSearchConfig(limit=10, oversampling_factor=2) +tool = MongoDBVectorSearchTool( + database_name="example_database', + collection_name='example_collections', + connection_string="", + query_config=query_config, + index_name="my_vector_index", + generative_model="gpt-4o-mini" +) + +# Adding the tool to an agent +rag_agent = Agent( + name="rag_agent", + role="You are a helpful assistant that can answer questions with the help of the MongoDBVectorSearchTool.", + goal="...", + backstory="...", + llm="gpt-4o-mini", + tools=[tool], +) +``` + +Preloading the MongoDB database with documents: + +```python +from crewai_tools import MongoDBVectorSearchTool + +# Generate the documents and add them to the MongoDB database +test_docs = client.collections.get("example_collections") + +# Create the tool. +tool = MongoDBVectorSearchTool( + database_name="example_database', + collection_name='example_collections', + connection_string="", +) + +# Add the text from a set of CrewAI knowledge documents. +texts = [] +for d in os.listdir("knowledge"): + with open(os.path.join("knowledge", d), "r") as f: + texts.append(f.read()) +tool.add_texts(text) + +# Create the vector search index (if it wasn't already created in Atlas). +tool.create_vector_search_index(dimensions=3072) +``` diff --git a/crewai_tools/tools/mongodb_vector_search_tool/__init__.py b/crewai_tools/tools/mongodb_vector_search_tool/__init__.py new file mode 100644 index 000000000..c7e991472 --- /dev/null +++ b/crewai_tools/tools/mongodb_vector_search_tool/__init__.py @@ -0,0 +1,11 @@ +from .vector_search import ( + MongoDBToolSchema, + MongoDBVectorSearchConfig, + MongoDBVectorSearchTool, +) + +__all__ = [ + "MongoDBVectorSearchConfig", + "MongoDBVectorSearchTool", + "MongoDBToolSchema", +] diff --git a/crewai_tools/tools/mongodb_vector_search_tool/utils.py b/crewai_tools/tools/mongodb_vector_search_tool/utils.py new file mode 100644 index 000000000..a66586f6f --- /dev/null +++ b/crewai_tools/tools/mongodb_vector_search_tool/utils.py @@ -0,0 +1,120 @@ +from __future__ import annotations + +from time import monotonic, sleep +from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional + +if TYPE_CHECKING: + from pymongo.collection import Collection + + +def _vector_search_index_definition( + dimensions: int, + path: str, + similarity: str, + filters: Optional[List[str]] = None, + **kwargs: Any, +) -> Dict[str, Any]: + # https://www.mongodb.com/docs/atlas/atlas-vector-search/vector-search-type/ + fields = [ + { + "numDimensions": dimensions, + "path": path, + "similarity": similarity, + "type": "vector", + }, + ] + if filters: + for field in filters: + fields.append({"type": "filter", "path": field}) + definition = {"fields": fields} + definition.update(kwargs) + return definition + + +def create_vector_search_index( + collection: Collection, + index_name: str, + dimensions: int, + path: str, + similarity: str, + filters: Optional[List[str]] = None, + *, + wait_until_complete: Optional[float] = None, + **kwargs: Any, +) -> None: + """Experimental Utility function to create a vector search index + + Args: + collection (Collection): MongoDB Collection + index_name (str): Name of Index + dimensions (int): Number of dimensions in embedding + path (str): field with vector embedding + similarity (str): The similarity score used for the index + filters (List[str]): Fields/paths to index to allow filtering in $vectorSearch + wait_until_complete (Optional[float]): If provided, number of seconds to wait + until search index is ready. + kwargs: Keyword arguments supplying any additional options to SearchIndexModel. + """ + from pymongo.operations import SearchIndexModel + + if collection.name not in collection.database.list_collection_names(): + collection.database.create_collection(collection.name) + + result = collection.create_search_index( + SearchIndexModel( + definition=_vector_search_index_definition( + dimensions=dimensions, + path=path, + similarity=similarity, + filters=filters, + **kwargs, + ), + name=index_name, + type="vectorSearch", + ) + ) + + if wait_until_complete: + _wait_for_predicate( + predicate=lambda: _is_index_ready(collection, index_name), + err=f"{index_name=} did not complete in {wait_until_complete}!", + timeout=wait_until_complete, + ) + + +def _is_index_ready(collection: Collection, index_name: str) -> bool: + """Check for the index name in the list of available search indexes to see if the + specified index is of status READY + + Args: + collection (Collection): MongoDB Collection to for the search indexes + index_name (str): Vector Search Index name + + Returns: + bool : True if the index is present and READY false otherwise + """ + for index in collection.list_search_indexes(index_name): + if index["status"] == "READY": + return True + return False + + +def _wait_for_predicate( + predicate: Callable, err: str, timeout: float = 120, interval: float = 0.5 +) -> None: + """Generic to block until the predicate returns true + + Args: + predicate (Callable[, bool]): A function that returns a boolean value + err (str): Error message to raise if nothing occurs + timeout (float, optional): Wait time for predicate. Defaults to TIMEOUT. + interval (float, optional): Interval to check predicate. Defaults to DELAY. + + Raises: + TimeoutError: _description_ + """ + start = monotonic() + while not predicate(): + if monotonic() - start > timeout: + raise TimeoutError(err) + sleep(interval) diff --git a/crewai_tools/tools/mongodb_vector_search_tool/vector_search.py b/crewai_tools/tools/mongodb_vector_search_tool/vector_search.py new file mode 100644 index 000000000..4112aa500 --- /dev/null +++ b/crewai_tools/tools/mongodb_vector_search_tool/vector_search.py @@ -0,0 +1,327 @@ +import os +from importlib.metadata import version +from logging import getLogger +from typing import Any, Dict, Iterable, List, Optional, Type + +from crewai.tools import BaseTool, EnvVar +from openai import AzureOpenAI, Client +from pydantic import BaseModel, Field + +from crewai_tools.tools.mongodb_vector_search_tool.utils import ( + create_vector_search_index, +) + +try: + import pymongo # noqa: F403 + + MONGODB_AVAILABLE = True +except ImportError: + MONGODB_AVAILABLE = False + +logger = getLogger(__name__) + + +class MongoDBVectorSearchConfig(BaseModel): + """Configuration for MongoDB vector search queries.""" + + limit: Optional[int] = Field( + default=4, description="number of documents to return." + ) + pre_filter: Optional[dict[str, Any]] = Field( + default=None, + description="List of MQL match expressions comparing an indexed field", + ) + post_filter_pipeline: Optional[list[dict]] = Field( + default=None, + description="Pipeline of MongoDB aggregation stages to filter/process results after $vectorSearch.", + ) + oversampling_factor: int = Field( + default=10, + description="Multiple of limit used when generating number of candidates at each step in the HNSW Vector Search", + ) + include_embeddings: bool = Field( + default=False, + description="Whether to include the embedding vector of each result in metadata.", + ) + + +class MongoDBToolSchema(BaseModel): + """Input for MongoDBTool.""" + + query: str = Field( + ..., + description="The query to search retrieve relevant information from the MongoDB database. Pass only the query, not the question.", + ) + + +class MongoDBVectorSearchTool(BaseTool): + """Tool to perfrom a vector search the MongoDB database""" + + name: str = "MongoDBVectorSearchTool" + description: str = "A tool to perfrom a vector search on a MongoDB database for relevant information on internal documents." + + args_schema: Type[BaseModel] = MongoDBToolSchema + query_config: Optional[MongoDBVectorSearchConfig] = Field( + default=None, description="MongoDB Vector Search query configuration" + ) + embedding_model: str = Field( + default="text-embedding-3-large", + description="Text OpenAI embedding model to use", + ) + vector_index_name: str = Field( + default="vector_index", description="Name of the Atlas Search vector index" + ) + text_key: str = Field( + default="text", + description="MongoDB field that will contain the text for each document", + ) + embedding_key: str = Field( + default="embedding", + description="Field that will contain the embedding for each document", + ) + database_name: str = Field(..., description="The name of the MongoDB database") + collection_name: str = Field(..., description="The name of the MongoDB collection") + connection_string: str = Field( + ..., + description="The connection string of the MongoDB cluster", + ) + dimensions: int = Field( + default=1536, + description="Number of dimensions in the embedding vector", + ) + env_vars: List[EnvVar] = [ + EnvVar( + name="BROWSERBASE_API_KEY", + description="API key for Browserbase services", + required=False, + ), + EnvVar( + name="BROWSERBASE_PROJECT_ID", + description="Project ID for Browserbase services", + required=False, + ), + ] + package_dependencies: List[str] = ["mongdb"] + + def __init__(self, **kwargs): + super().__init__(**kwargs) + if not MONGODB_AVAILABLE: + import click + + if click.confirm( + "You are missing the 'mongodb' crewai tool. Would you like to install it?" + ): + import subprocess + + subprocess.run(["uv", "add", "pymongo"], check=True) + + else: + raise ImportError("You are missing the 'mongodb' crewai tool.") + + if "AZURE_OPENAI_ENDPOINT" in os.environ: + self._openai_client = AzureOpenAI() + elif "OPENAI_API_KEY" in os.environ: + self._openai_client = Client() + else: + raise ValueError( + "OPENAI_API_KEY environment variable is required for MongoDBVectorSearchTool and it is mandatory to use the tool." + ) + + from pymongo import MongoClient + from pymongo.driver_info import DriverInfo + + self._client = MongoClient( + self.connection_string, + driver=DriverInfo(name="CrewAI", version=version("crewai-tools")), + ) + self._coll = self._client[self.database_name][self.collection_name] + + def create_vector_search_index( + self, + *, + dimensions: int, + relevance_score_fn: str = "cosine", + auto_index_timeout: int = 15, + ) -> None: + """Convenience function to create a vector search index. + + Args: + dimensions: Number of dimensions in embedding. If the value is set and + the index does not exist, an index will be created. + relevance_score_fn: The similarity score used for the index + Currently supported: 'euclidean', 'cosine', and 'dotProduct' + auto_index_timeout: Timeout in seconds to wait for an auto-created index + to be ready. + """ + + create_vector_search_index( + collection=self._coll, + index_name=self.vector_index_name, + dimensions=dimensions, + path=self.embedding_key, + similarity=relevance_score_fn, + wait_until_complete=auto_index_timeout, + ) + + def add_texts( + self, + texts: Iterable[str], + metadatas: Optional[List[Dict[str, Any]]] = None, + ids: Optional[List[str]] = None, + batch_size: int = 100, + **kwargs: Any, + ) -> List[str]: + """Add texts, create embeddings, and add to the Collection and index. + + Important notes on ids: + - If _id or id is a key in the metadatas dicts, one must + pop them and provide as separate list. + - They must be unique. + - If they are not provided, the VectorStore will create unique ones, + stored as bson.ObjectIds internally, and strings in Langchain. + These will appear in Document.metadata with key, '_id'. + + Args: + texts: Iterable of strings to add to the vectorstore. + metadatas: Optional list of metadatas associated with the texts. + ids: Optional list of unique ids that will be used as index in VectorStore. + See note on ids. + batch_size: Number of documents to insert at a time. + Tuning this may help with performance and sidestep MongoDB limits. + + Returns: + List of ids added to the vectorstore. + """ + from bson import ObjectId + + _metadatas = metadatas or [{} for _ in texts] + ids = [str(ObjectId()) for _ in range(len(list(texts)))] + metadatas_batch = _metadatas + + result_ids = [] + texts_batch = [] + metadatas_batch = [] + size = 0 + i = 0 + for j, (text, metadata) in enumerate(zip(texts, _metadatas)): + size += len(text) + len(metadata) + texts_batch.append(text) + metadatas_batch.append(metadata) + if (j + 1) % batch_size == 0 or size >= 47_000_000: + batch_res = self._bulk_embed_and_insert_texts( + texts_batch, metadatas_batch, ids[i : j + 1] + ) + result_ids.extend(batch_res) + texts_batch = [] + metadatas_batch = [] + size = 0 + i = j + 1 + if texts_batch: + batch_res = self._bulk_embed_and_insert_texts( + texts_batch, metadatas_batch, ids[i : j + 1] + ) + result_ids.extend(batch_res) + return result_ids + + def _embed_texts(self, texts: List[str]) -> List[List[float]]: + return [ + i.embedding + for i in self._openai_client.embeddings.create( + input=texts, + model=self.embedding_model, + dimensions=self.dimensions, + ).data + ] + + def _bulk_embed_and_insert_texts( + self, + texts: List[str], + metadatas: List[dict], + ids: List[str], + ) -> List[str]: + """Bulk insert single batch of texts, embeddings, and ids.""" + from bson import ObjectId + from pymongo.operations import ReplaceOne + + if not texts: + return [] + # Compute embedding vectors + embeddings = self._embed_texts(texts) + docs = [ + { + "_id": ObjectId(i), + self.text_key: t, + self.embedding_key: embedding, + **m, + } + for i, t, m, embedding in zip(ids, texts, metadatas, embeddings) + ] + operations = [ReplaceOne({"_id": doc["_id"]}, doc, upsert=True) for doc in docs] + # insert the documents in MongoDB Atlas + result = self._coll.bulk_write(operations) + assert result.upserted_ids is not None + return [str(_id) for _id in result.upserted_ids.values()] + + def _run(self, query: str) -> str: + from bson import json_util + + try: + query_config = self.query_config or MongoDBVectorSearchConfig() + limit = query_config.limit + oversampling_factor = query_config.oversampling_factor + pre_filter = query_config.pre_filter + include_embeddings = query_config.include_embeddings + post_filter_pipeline = query_config.post_filter_pipeline + + # Create the embedding for the query + query_vector = self._embed_texts([query])[0] + + # Atlas Vector Search, potentially with filter + stage = { + "index": self.vector_index_name, + "path": self.embedding_key, + "queryVector": query_vector, + "numCandidates": limit * oversampling_factor, + "limit": limit, + } + if pre_filter: + stage["filter"] = pre_filter + + pipeline = [ + {"$vectorSearch": stage}, + {"$set": {"score": {"$meta": "vectorSearchScore"}}}, + ] + + # Remove embeddings unless requested + if not include_embeddings: + pipeline.append({"$project": {self.embedding_key: 0}}) + + # Post-processing + if post_filter_pipeline is not None: + pipeline.extend(post_filter_pipeline) + + # Execution + cursor = self._coll.aggregate(pipeline) # type: ignore[arg-type] + docs = [] + + # Format + for doc in cursor: + docs.append(doc) + return json_util.dumps(docs) + except Exception as e: + logger.error(f"Error: {e}") + return "" + + def __del__(self): + """Cleanup clients on deletion.""" + try: + if hasattr(self, "_client") and self._client: + self._client.close() + except Exception as e: + logger.error(f"Error: {e}") + + try: + if hasattr(self, "_openai_client") and self._openai_client: + self._openai_client.close() + except Exception as e: + logger.error(f"Error: {e}") diff --git a/crewai_tools/tools/multion_tool/README.md b/crewai_tools/tools/multion_tool/README.md new file mode 100644 index 000000000..da92a0682 --- /dev/null +++ b/crewai_tools/tools/multion_tool/README.md @@ -0,0 +1,53 @@ +# MultiOnTool Documentation + +## Description +The MultiOnTool, integrated within the crewai_tools package, empowers CrewAI agents with the capability to navigate and interact with the web through natural language instructions. Leveraging the Multion API, this tool facilitates seamless web browsing, making it an essential asset for projects requiring dynamic web data interaction. + +## Installation +Ensure the `crewai[tools]` package is installed in your environment to use the MultiOnTool. If it's not already installed, you can add it using the command below: +```shell +pip install 'crewai[tools]' +``` + +## Example +The following example demonstrates how to initialize the tool and execute a search with a given query: + +```python +from crewai import Agent, Task, Crew +from crewai_tools import MultiOnTool + +# Initialize the tool from a MultiOn Tool +multion_tool = MultiOnTool(api_key= "YOUR_MULTION_API_KEY", local=False) + +Browser = Agent( + role="Browser Agent", + goal="control web browsers using natural language ", + backstory="An expert browsing agent.", + tools=[multion_remote_tool], + verbose=True, +) + +# example task to search and summarize news +browse = Task( + description="Summarize the top 3 trending AI News headlines", + expected_output="A summary of the top 3 trending AI News headlines", + agent=Browser, +) + +crew = Crew(agents=[Browser], tasks=[browse]) + +crew.kickoff() +``` + +## Arguments + +- `api_key`: Specifies MultiOn API key. Default is the `MULTION_API_KEY` environment variable. +- `local`: Use the local flag set as "true" to run the agent locally on your browser. Make sure the multion browser extension is installed and API Enabled is checked. +- `max_steps`: Optional. Set the max_steps the multion agent can take for a command + +## Steps to Get Started +To effectively use the `MultiOnTool`, follow these steps: + +1. **Install CrewAI**: Confirm that the `crewai[tools]` package is installed in your Python environment. +2. **Install and use MultiOn**: Follow MultiOn documentation for installing the MultiOn Browser Extension (https://docs.multion.ai/learn/browser-extension). +3. **Enable API Usage**: Click on the MultiOn extension in the extensions folder of your browser (not the hovering MultiOn icon on the web page) to open the extension configurations. Click the API Enabled toggle to enable the API diff --git a/crewai_tools/tools/multion_tool/example.py b/crewai_tools/tools/multion_tool/example.py new file mode 100644 index 000000000..ec69e5cdf --- /dev/null +++ b/crewai_tools/tools/multion_tool/example.py @@ -0,0 +1,29 @@ +import os + +from crewai import Agent, Crew, Task +from multion_tool import MultiOnTool + +os.environ["OPENAI_API_KEY"] = "Your Key" + +multion_browse_tool = MultiOnTool(api_key="Your Key") + +# Create a new agent +Browser = Agent( + role="Browser Agent", + goal="control web browsers using natural language ", + backstory="An expert browsing agent.", + tools=[multion_browse_tool], + verbose=True, +) + +# Define tasks +browse = Task( + description="Summarize the top 3 trending AI News headlines", + expected_output="A summary of the top 3 trending AI News headlines", + agent=Browser, +) + + +crew = Crew(agents=[Browser], tasks=[browse]) + +crew.kickoff() diff --git a/crewai_tools/tools/multion_tool/multion_tool.py b/crewai_tools/tools/multion_tool/multion_tool.py new file mode 100644 index 000000000..cf652c324 --- /dev/null +++ b/crewai_tools/tools/multion_tool/multion_tool.py @@ -0,0 +1,80 @@ +"""Multion tool spec.""" + +import os +from typing import Any, Optional, List + +from crewai.tools import BaseTool, EnvVar + + +class MultiOnTool(BaseTool): + """Tool to wrap MultiOn Browse Capabilities.""" + + name: str = "Multion Browse Tool" + description: str = """Multion gives the ability for LLMs to control web browsers using natural language instructions. + If the status is 'CONTINUE', reissue the same instruction to continue execution + """ + multion: Optional[Any] = None + session_id: Optional[str] = None + local: bool = False + max_steps: int = 3 + package_dependencies: List[str] = ["multion"] + env_vars: List[EnvVar] = [ + EnvVar(name="MULTION_API_KEY", description="API key for Multion", required=True), + ] + + def __init__( + self, + api_key: Optional[str] = None, + local: bool = False, + max_steps: int = 3, + **kwargs, + ): + super().__init__(**kwargs) + try: + from multion.client import MultiOn # type: ignore + except ImportError: + import click + + if click.confirm( + "You are missing the 'multion' package. Would you like to install it?" + ): + import subprocess + + subprocess.run(["uv", "add", "multion"], check=True) + from multion.client import MultiOn + else: + raise ImportError( + "`multion` package not found, please run `uv add multion`" + ) + self.session_id = None + self.local = local + self.multion = MultiOn(api_key=api_key or os.getenv("MULTION_API_KEY")) + self.max_steps = max_steps + + def _run( + self, + cmd: str, + *args: Any, + **kwargs: Any, + ) -> str: + """ + Run the Multion client with the given command. + + Args: + cmd (str): The detailed and specific natural language instructrion for web browsing + + *args (Any): Additional arguments to pass to the Multion client + **kwargs (Any): Additional keyword arguments to pass to the Multion client + """ + + browse = self.multion.browse( + cmd=cmd, + session_id=self.session_id, + local=self.local, + max_steps=self.max_steps, + *args, + **kwargs, + ) + self.session_id = browse.session_id + + return browse.message + "\n\n STATUS: " + browse.status diff --git a/crewai_tools/tools/mysql_search_tool/README.md b/crewai_tools/tools/mysql_search_tool/README.md new file mode 100644 index 000000000..b31d7120b --- /dev/null +++ b/crewai_tools/tools/mysql_search_tool/README.md @@ -0,0 +1,56 @@ +# MySQLSearchTool + +## Description +This tool is designed to facilitate semantic searches within MySQL database tables. Leveraging the RAG (Retrieve and Generate) technology, the MySQLSearchTool provides users with an efficient means of querying database table content, specifically tailored for MySQL databases. It simplifies the process of finding relevant data through semantic search queries, making it an invaluable resource for users needing to perform advanced queries on extensive datasets within a MySQL database. + +## Installation +To install the `crewai_tools` package and utilize the MySQLSearchTool, execute the following command in your terminal: + +```shell +pip install 'crewai[tools]' +``` + +## Example +Below is an example showcasing how to use the MySQLSearchTool to conduct a semantic search on a table within a MySQL database: + +```python +from crewai_tools import MySQLSearchTool + +# Initialize the tool with the database URI and the target table name +tool = MySQLSearchTool(db_uri='mysql://user:password@localhost:3306/mydatabase', table_name='employees') + +``` + +## Arguments +The MySQLSearchTool requires the following arguments for its operation: + +- `db_uri`: A string representing the URI of the MySQL database to be queried. This argument is mandatory and must include the necessary authentication details and the location of the database. +- `table_name`: A string specifying the name of the table within the database on which the semantic search will be performed. This argument is mandatory. + +## Custom model and embeddings + +By default, the tool uses OpenAI for both embeddings and summarization. To customize the model, you can use a config dictionary as follows: + +```python +tool = MySQLSearchTool( + config=dict( + llm=dict( + provider="ollama", # or google, openai, anthropic, llama2, ... + config=dict( + model="llama2", + # temperature=0.5, + # top_p=1, + # stream=true, + ), + ), + embedder=dict( + provider="google", + config=dict( + model="models/embedding-001", + task_type="retrieval_document", + # title="Embeddings", + ), + ), + ) +) +``` diff --git a/crewai_tools/tools/mysql_search_tool/mysql_search_tool.py b/crewai_tools/tools/mysql_search_tool/mysql_search_tool.py new file mode 100644 index 000000000..8c2c5ef5d --- /dev/null +++ b/crewai_tools/tools/mysql_search_tool/mysql_search_tool.py @@ -0,0 +1,51 @@ +from typing import Any, Type + +try: + from embedchain.loaders.mysql import MySQLLoader + EMBEDCHAIN_AVAILABLE = True +except ImportError: + EMBEDCHAIN_AVAILABLE = False + +from pydantic import BaseModel, Field + +from ..rag.rag_tool import RagTool + + +class MySQLSearchToolSchema(BaseModel): + """Input for MySQLSearchTool.""" + + search_query: str = Field( + ..., + description="Mandatory semantic search query you want to use to search the database's content", + ) + + +class MySQLSearchTool(RagTool): + name: str = "Search a database's table content" + description: str = "A tool that can be used to semantic search a query from a database table's content." + args_schema: Type[BaseModel] = MySQLSearchToolSchema + db_uri: str = Field(..., description="Mandatory database URI") + + def __init__(self, table_name: str, **kwargs): + if not EMBEDCHAIN_AVAILABLE: + raise ImportError("embedchain is not installed. Please install it with `pip install crewai-tools[embedchain]`") + super().__init__(**kwargs) + kwargs["data_type"] = "mysql" + kwargs["loader"] = MySQLLoader(config=dict(url=self.db_uri)) + self.add(table_name) + self.description = f"A tool that can be used to semantic search a query the {table_name} database table's content." + self._generate_description() + + def add( + self, + table_name: str, + **kwargs: Any, + ) -> None: + super().add(f"SELECT * FROM {table_name};", **kwargs) + + def _run( + self, + search_query: str, + **kwargs: Any, + ) -> Any: + return super()._run(query=search_query) diff --git a/crewai_tools/tools/nl2sql/README.md b/crewai_tools/tools/nl2sql/README.md new file mode 100644 index 000000000..932867c90 --- /dev/null +++ b/crewai_tools/tools/nl2sql/README.md @@ -0,0 +1,73 @@ +# NL2SQL Tool + +## Description + +This tool is used to convert natural language to SQL queries. When passed to the agent it will generate queries and then use them to interact with the database. + +This enables multiple workflows like having an Agent to access the database fetch information based on the goal and then use the information to generate a response, report or any other output. Along with that provides the ability for the Agent to update the database based on its goal. + +**Attention**: Make sure that the Agent has access to a Read-Replica or that is okay for the Agent to run insert/update queries on the database. + +## Requirements + +- SqlAlchemy +- Any DB compatible library (e.g. psycopg2, mysql-connector-python) + +## Installation +Install the crewai_tools package +```shell +pip install 'crewai[tools]' +``` + +## Usage + +In order to use the NL2SQLTool, you need to pass the database URI to the tool. The URI should be in the format `dialect+driver://username:password@host:port/database`. + +```python +from crewai_tools import NL2SQLTool + +# psycopg2 was installed to run this example with PostgreSQL +nl2sql = NL2SQLTool(db_uri="postgresql://example@localhost:5432/test_db") + +@agent +def researcher(self) -> Agent: + return Agent( + config=self.agents_config["researcher"], + allow_delegation=False, + tools=[nl2sql] + ) +``` + +## Example + +The primary task goal was: + +"Retrieve the average, maximum, and minimum monthly revenue for each city, but only include cities that have more than one user. Also, count the number of users in each city and sort the results by the average monthly revenue in descending order" + +So the Agent tried to get information from the DB, the first one is wrong so the Agent tries again and gets the correct information and passes to the next agent. + +![alt text](images/image-2.png) +![alt text](images/image-3.png) + + +The second task goal was: + +"Review the data and create a detailed report, and then create the table on the database with the fields based on the data provided. +Include information on the average, maximum, and minimum monthly revenue for each city, but only include cities that have more than one user. Also, count the number of users in each city and sort the results by the average monthly revenue in descending order." + +Now things start to get interesting, the Agent generates the SQL query to not only create the table but also insert the data into the table. And in the end the Agent still returns the final report which is exactly what was in the database. + +![alt text](images/image-4.png) +![alt text](images/image-5.png) + +![alt text](images/image-9.png) +![alt text](images/image-7.png) + + +This is a simple example of how the NL2SQLTool can be used to interact with the database and generate reports based on the data in the database. + +The Tool provides endless possibilities on the logic of the Agent and how it can interact with the database. + +``` + DB -> Agent -> ... -> Agent -> DB +``` diff --git a/crewai_tools/tools/nl2sql/images/image-2.png b/crewai_tools/tools/nl2sql/images/image-2.png new file mode 100644 index 0000000000000000000000000000000000000000..b3844f0ddc25e4d407143b7d886f4b3c2ccebc55 GIT binary patch literal 84676 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
mokaBRcn6GcpE7lcTv# zzfr}G`O~zzBHE~&YSZu9M^?75A*U9@mALnL!ZS9F=7NygHTD=}<-aVoy(rusX^T~g z`;+mRYUKOrSSLDnScI6OPhIbXhNwP6;(XeN?>feHle$6?wbgCuKa%0Ey6U-nHCZ$t z&zU&V=Zw8Y9r}%bU(<6co>@YFeQV((E_qwBTGXy3Pbx*9rop5U=9sWA`e{2C1l8_# zJNPP9fFuOfK{kV`tn<$wH81Q5=&G~)bfJ9}n+7omZzHZ&G_BShEy^T;i zZI`$EwVPqpkvea`iN7Yr8jy!?)e+$gJwMCi^J6qy?*5NgVrKU6W3@*#+?&)Yyf5qW zSaeed0gsN%HuKxwi^Y~%P?tCb0;CYFNuG?S&CwWGKVJ4%1KSjY53P?jIkQl?VmOhq zQ=?4Piyc&-8u>TmFLm}SQw3;aHupv)55D5fT;6|HlB)8qm@}HJ&HKeX4pIw}@xxgD zN)?4-z~K`dQy^|Nu{4u6l9UhvyP?Vl*9)r+)?My-bXxpTXMLX| zsg)OQ_m&lS121JjXzQ=FZaS{fa38W7PjP8hk6LFM!O?=?)

sCkMaGpo(tFx)`qEmOh!`BHrJ7~j?g*mWNr zIka<_yh`j+{ift3XQR{vx3R;%l0(7@;v9SbzQVTnC~~6q z1KmyOYHc@+QBZa4FHeD58Vr5H3^sP!grUMIYeDrS*qqjx`4a=457#pzWFSr)w~Xi` z#Txc)n^%N1>hb=8r<0nq5<|^(Orl%cMKrDakRYjd8&gB(8~SX$hb9L`-zKd;3Eh!= zlm3m{YyYkOTj-|V*wqqEaN5aRZb1&CNQNKFJmCRLX(&DKuwsi2VuwO@S}vU95J;h> z{FZZplz~BI_JsaQ>nE5pL+z?8VyZNWFp9d5t{9ka`o~B}Bl49lcoLfJB4Yy~Z++8j)Z`5(+)2pJ2;(+yo^Z!ebj*uj<|!x9+d23N?Ctf% z$&ipS>W1m*!umi5QIh9r7kblZqGmZmDto@)o)VHSaG%)clZEx;@Q?}Orh0J=GmR}t zU*|g{G0b!DsxZRj7(C>Zrg4c@b74XQqq($PG*uye^4&?@qut;0G?SD%kUoVzhXDqDYO^GRxUB`g%E!`}S3 ztyR8}WnWOS@WuXG+9P_LeV_XNjvTU`tV?gZw$s6Fx#B-oVXXk9O5sk?sB1n1qYsRJ zKb&=6t?oG|WXUTfuH2fd>GUBXUA+@10Eq}OtPO~gS?PbT2-c_5;g)|+fVI%YZAGRb zlx>j=M3&mqa2+?yy9m;4W!ZVFNbb(;=h66+hEObuq{G7uXO)bpU@ONJi^!&&*JkaG zboa737d&uH2ArW0e>yDZS%g#<<+m@=vFuVKvpe0w9R>(&nPv^&N{W|q#zkyyoq(}n z6Xif(6OG85f8@W%YW+dUxU)hxgBKoa0Y}Gj2$vb>YQP_7g#*af5J+J7XH)){e3ud+ z^(_C$7ae{ixzHCN_{xnm%Fd8!Rn*CgYnIeX=aVK6JA<#XdcI+U-F?lax;RE=`~AYOX&f(m z)&l=3+t0L<*dupJ*9=#t0m^$Q3NOx`{uWqp1|F1QOG zjPOZ2czyn0)j2xmYKBe~NCm%ST!CD>mQZ1ic9FzAb>Je)+;Al8t|M?(yvv@NkqHK8 zCl)PXrQMY~-k+Q+!QB^5PyX2}b?Lxwob4#)tnK1usAyt+CZ;;9epRRT+KfX#r2&WZ zGhK;s>B9|ARpQv&Fikd0;xG9Q$vz=o_mA|V@;bP1wh#u8O#>)tAYB5x8@h*WUYbs; zaZ_l)vr;7D)`lX4PdM-VEq2@7XN9Jf!4D{K@LFuI5j^ggU0dDZlg}eZM3Eb_`g9Wg zOCLLXw9^)+X4ldXP^NGXjk444Bq^cH(}GkP(Mz4~2yIO!906z>^Su{nF-GF#^zIntolZ_hnF?9xJXTb)5MTN*g&VrAdxoDz;&KE|KNu}`@Y)lfoV2_)X|2>*$i zCqq(+)Vm3ilZV}MF?nv#!^Gi1k5&s?QNTUEL z=@N#-?-+}B(=f`&mLY09m~@nA%_fwSqH#r>J3CN9M0mhZo8U~IOz8u{XIQr$<0cK=w;Fg+b`7YHLS4GH3C}Q zKe9tE0X(L^tkm0Y7F!vQ+U>^^EvBucF9Ju7DR(@^>b+_hYtW z2CM$O=CH&b_sFi-q7)Ph7R&w%8j6{mU?@(JiN_ocpcK8HQNlT@lWGb7U6R6Hgr$XU z8g!t1Z&fIxcor}SvgTn0b$%y+Hq%!$o6xib&lHR=Xe60cYx=omV9g_CAZ$cNIHf+hYOeQehK5Fh+y^6Or=mPBH2t z-pSa0Q(W1h$Vw9HL#09tD0Ptzb&xNKnTUrtr~U+)GXaLWv9jdl1fN5F(+X5@5W zF9kVDgspqoMUsMR_@E&2eD>u7u1|Uka7b#K2P$d-s0eoM9D9*i-8h&P!Hb?omKpY& zOI9NLxs$i=CTmi1q;_0kHrnlw;c(dqPFXb?^hGh!kOa#^yS>I_ zx0qABMAW!TH(pdMp*CR*Fgspq{$!Maapwv-lB<2|y5d}RYJTqIqFOdNIeO~{+iN2o z(-4P0eX7;VvqM%u$?XGYJoVgVn77nzgC>~o(*K9Kfa8>$WrVdw3`d;Up@BLAo^ zq+u6I#msMtYRP*7J*(=2cVp_p8?j$3(KQdXz8f8OBJtyFETl-ak zms(BUo+giO77F$<-11DtpC{8j0nh>XM7Fq+a|iYVXuPQfDumVC8 zV1U6p(Q#-kdf+5!&sWF#=`bt3;zSxdXEq+!q%q-d3otEe+_-e2xSnE#Iqm(0J4X>F zoFR*DAGeHl%nHQz?&wdt#WHY8ADJvCbhc@`U?$=J0|EcRa8Mwd8V9DIZYCN+gmm{;%pC5}p=np#w!;@x<6|^rjPVnVn zC#q?zFiP!mjZTz(R&QVzFy4An|KI=|8!a1Fp>sf-Qc2R9&?>4ZaJKiRs}*}A6s{Zh zF7QH&Io9(j!M6r^5D|=fcKK{f$mW@p@8>JD!w1~ot}E>a+`RN}LS7LgY9sZfGLRo)Rw>gpt#VSk9S+(nOkzHTQ*;_BO%NytzO9K!>e?)p#} z-lPi?IOU;iUR)rlPCI-hk@=0I{BVtKdzWLBp_@aTg-1L4k;iG@?d8ol57idYuS%68 z?+HKy%pH%%O$anxK|}(Y@xMVn9^ZLMl)^Fh*ZBiincj>LTfQf4y-dDByKp0|A-Z;9K0=Q%u%*~*v@ zri2fiZ4KlPNuq1>ewjR?e6wcEYkn12;)VG-@vMR54G;gjsEC9`90>?KEsQSlG)s7!3fli}x&ezZ4J9V7-5 z`m!W0?iyKI%~qeTHhSJ-3;#mWd57`(?+4L)&n~W=$y+GkpKrmPjDMF^D-v+yE7u$4 z<9O0IK`%e-*#(3G|M$C#^x`Xmv&*(Y^Ndr%^Q}}F?c8aV2|@j> zneF`*3qX>f?xpd)upP`fqa5O5ObA7Ca4awHl-RtIN@~|ih;L;x$bi?ZhG=BX;x0o) z^Xf_ODi(fPYI1|+`ObxAzG9aHI+*BQ1D+3oJ3$2SrRSLb4P7+qolegkd)eh~Ng1*h zgM}D_wnAW?J%j^Jo2|$GdAlchboe$b$XBzr-?i6#f$=+uUK>g4z74p=G7V<4KaE+R zoY?Gdi?-0A!(P_#?pJ}AZxl3kJ6Q~`dH~5N!liDc3NSumT(#tgGVKXEF}WU-!M*%3m@&) z2&@52@t@MSNg$>vnnU*Y1Dh1^kA|N-`#l~}$qoAnphHY~aj@b`$UADD_MMjc77>Y8 z=;);V&K-4d;!U3I=u~T5LO%+Bb`x5ykD#UiJKi(Yo-FxtbSAa8!;=4Ka;q{EaC|PG zP5ZGj;ivg4;1xa>~uwr_j-(u@P8Hrc|E6@h7^gFb51av^;j~&yx4;x?(!0FgUCpXZTN|s-)HTX0 zRI8$FdMxwD`wYP94s@Q(QukNi$>k1|eZsXcD|Rii;7c&1c_`{0V`_zj(LyuUW%SVQ)WOYZ-}0V)1~u z=YHg9G+Vp{LVqxTSPvX3B=d_vGCj*s{?F+So#thSt4j3t-7%_zh=&ra^z_-|w4g-! zEYEl>J6|nFhA%D+)XMg(5$q1^$C>U;`dkl9Zh>2+97|p5!?Vy13;Gmk63bUWZs`u8 zM;wJOMFqcHtZ+)5YqamH#u&iB-6Oj^%8YFvLtJ0v~Xn;A4E~e|@Tx?;Js{AiekL-s*AEP8|QcNNL3bk<$@%AhTMg z4bJsQ0p>wpk9rO9GDGmF1c|lTBxZs3WYL4SS7Mk}i*Hh;y_DS2E-6%bGN#R35aVVb z>slgKu^>)RJgJmn>sgQ?ow0AarRURRjwGmCCZ3~s4hI@F?yK}fj<~6Bd>A<;*E?Rj zXBf(51!4N72oH`JB9R1YHv_VzXu?LgpJvxxpNd)#E)ch5QN&WXNNo$AP|X}o8Bk?0 zj=qE>Z7~3YlUMXm$GT}X*vsxlJAuvg1}Xk)@poxOeLCo3!nItHf}E4c&oB3`Ol_oF zde&^1PC1rsh>bDc#A}9GCV5eQhM>Dcq4jivSZNn3N8z$bm?39rN zq-ODO0?SHrFmobvu?7GM$Mqb3{9RIk=>NH-R0uS9jMef3(V1|wqk*CUuu(mhZC9tJ z8>Wl@oG3ZQcQi2jyOuUo+z_UDjiH2Anh>s1*hysTN_F028`q(eIBYLP5XKrEN!au* z5QBf!plJEH88y?{+{vSgxn+niI;6Dzno;`wEiic;-FoUe-ByGWN>P}zDqH$D&i3^a z{EEkg5}hGOP%5LEwXtFad{xVdmGgz^Qr80AYr=DqoC(aj)%&yP6DNjAxpN5mK z8D+{5x1j!zncMGw>x0QjlqH$WQA33{5BO!G5s=lO$LPS|L$zxh>7y~`yY`L<&r`I( zqRIYiAuTlMNb(D6@8Btwb`mUZA@5$dq{s=3Ve<#^&2#Wc;&JEiWMqI^^+a1>l^o_ zi{OvSTq3%=%YAsmKN~!kY>DfDW=Q&3E@;iJ>3#jdrWO)2YymFDOod09@gIFBNuZpn zyET+`U<<=Ee~s4~N4=A1o`>D+4f9E7i0261=|$1oFW!b(jbYTt_-eQDAr32G7z(A!8nQ1R(v zrgVRzb^@jS(TX_C+ZX-K;?h~Tt~5ai!t*9Y9+}hpe>i&!raIeYYd67y6C}6ECgcXubay9Rf6cfNSLUy z7RTaUFWx_|-Y)O@^DYLI-D_5ikL*)7MeE zonpt`qTTa-+_4&-FFij*=tEFVZS??l>SjVJ7qcY{5__#haue#KOAw_c3~icrY62#E2B8XqT`uiK~y^~+GrdB=bR~sN|o;?V0rl<*%`MgWN?<; zW{F6%dKv#I4HLs_rRrl{d$rUZD~;-+>+Yd-75Cdh_+9UB9(eaf-+{|fR5zXD7tq_f z$+^(e7_-@(X^BysLdvF!c*+9hsX0VZ<*Nx={P~(uSKEkB5>>$inSK#`#r?Q)J5`bR zjASM*P%GVb3Y?in2EJr-513^*rxZI0$;*UX@?ovp7S>Lbn zL&714odd`QQ0|a_(GS3T8d*kz8zET^G*jCA(vC7APb4ef@BMsR3D>7PSNXbjRV&F@ z6@R7G;)zwZ-1uqJ!ze~12~6pCx3U)3y<8_#d9=!yAa)G&+jFx&2t~#z+ppW}JbiH2 ztSW1Ix_Qq79jjYO@ZdL7t^{~a(bB3@Muj7z+!R7@D!2IL&sE|Cd&-ltXg3{T)hEQH zc!Pn%4VSIfMxFM7Zx-nw+*P8%!%K=W<5YoH8*73rER7HGRrhtR{3XKR1LchVXr8w4 z&F<4Qvv_>Tm`uGwVtUZ=RmzxsTQ--h;?T(!uJ}k_*&-TvV%zdX|5?X;kb^lOGV!Yv zC$M_3FwpLN%NFHkTS)Ips$&n{y_y$u*$cBfM6~qRJDtreh*Pa~B0ixU4&d>9MZ-WE z+@*wMiRam64$pypD-zaH;fMg@%y7`IHLYL(CLZwsiT@r~ZM-$lR*sFSXDZ~Ov#myX zq&RABCJ}(r&NfKBw3%^mgiVracs)8}MdkZUQ#2lpE-!k{y^F~!w6Pt@5me|xsTDfA zoTC%yLQ?NMA2Kf8lXNQeo)~Hq7{OhMdX%n+3pzOB=>KxTwGZqOpBj!@`sE%Dz{f&# zi~*;FCSv?R&He(K5ua4A-~GbczgG%EMklqM(th|P)3|*(D4;PWneR9X+!B=L$cbYY zeK2P{5DkB?De@lTERgv7qsN~D32AsM+_74XUb5J?8&2{ymHJcU36W# zVpj7N*t@I7=~$3Itmy30b;J!6-f=$HmVoEE73C1-*?PkY5KrfUFRr1D#xv!Kkp?@i z8=big#sFKH+C>705btI80vaaz6%{g;EBI4t@Vy!f9{W~7(T#;h`%$7<$Y5fP3ZJ|F?Fuwk_nD5H{+R52Lt`%sTL4E>Y)pacP?`r35yok>flCv z=826WDbdJM0l5A>F4bcx8GT4j8DVtqnXrC_SM-XAxRZdA^`w*1dW zrTQOk(r#u8>v6L4xa`;9UHgJ>56!#10{V(@!Us}{f`PoJqpu&^?mUQ=8tl|tvOt@f z;B)*&OEFxwFPKYQ4UD*bAY&7?)kg~V+oK=nVv|g+W0;?{u@Gisi?-5yKiyyW3TLlBr;EB;e8wkeQ)=-v+;L+t z9Z+E^FnpbGVv8A4KzUhehcSis8ornt$=8}NtvK@H`*k_iF79o;!-%qk) zBY^Z727Pjx(w5-K+q7pO22&2lF`bt}`)e_f$rxkICm7JQ7;!mMr3LTQRHGV%&3Qpg zyfaM);Q36pG8&KI5~gk=45V%ozs}F1b5Ym!i$)-;y#SWI=j&~67gs5S6k~x;(eekL z6(pr4mD>ki7Iv)KElFp)(GSY_TYFOtAK=|<%Hs*d1MjgPQM^QEjR7@JySu|vO4?=O zsFMJzY$yU;bR}=y6<8~C&u2MTnMd-v%E&X|7k~`N+*p;qA?0QR>`xDo!$Bx8@4W2h z_v(%4^7}-xs1!Gs3{$Fcg?isFaEV2cz~uD6VZLA<91(TsXM5#r0K(07q0xIzp7s^i zAE5bY1<7&9TgZKF?5N+$jyi(((15jrWVMGEw!yt*Uqcwd*J7&h&TDmcLiO}-T8E-o zB6E&zBWM^NxwaUnUmXE5pht>)GquqXYfqb%?EB#fN5TETsew~lc5m$_T}XW2_R2s! zb=$RGUi3LT!M!>st@^(zx;fclno7s-i`1>)FUF&(x~`0o)^JrfQNrhiI%vS2YhifR+NbmjKo17#^Sdl~G&!|J+AfT>gko9bI-$I77AM|)Vg ztIzfMr;B@8H04X6c=hr=zb~4x42rKSgmfJyA0p$YZ`qQ`d-xzNFX(SPHhVdHq*9-H zaPuJ#b8km~az|=`Ewe;2`A>XM*tP)|Xq0rJ$87fp4a(3$qebnGa^`9x^RQlCHMiiF zwb`+G6jEQ%(WS);?}j_V{QVrmRnm`h5}opXQf;$+QGV18)V>E3kf`~16Y-> z%WWQ?%CdyAXE&R>f?sEQGdjXPEE64TRM$dFNd52<5jVcTaGF*)?SeNT!3vDN)k5GD zY+Ra%I-WlJM%w3p7_W12pSj%HV|#9Y+%Bb~~?lsJ5tg5@Q9D3}K(~T`8fM@%&wmJ22L!*k1FY zFhaezCNdYIwO+2|ps=We62pe0yp*BUU-ovQiBxM?g6S9Fuk~iLSO~rv9hY6FMGnBJ z8$y#%x)h1~4@Ve?4GJ@nT3k%!f;J%Z7e4E&TNu1#H;H~wu}2+2Uf}xeU3peA zOtg_Cq@8vaNPIfG_G=Hzr5iC?DUdUtd}UCQzqNL|i=g>%G5xnY2HT?3oW(a;07LC^ z!sUH7Z5e=iUkZ`$ioFc#9bjNUF=;k$N3Q$UqAt#hIq%n5MDy~_p9-A+<-6a(=$uMde zPzX%z>)7Mz5M1ZqF3B~_^7sw#+pQ&9ib9`zhz$=u!0v2Y`P81I`KkoSBRTGJy=?UE zsE{3O5c=Q0l&e-kUc2UV$+C3N;yO$hM-Um zE#Psi{YCcp!PGtffMQ#BGJJIJB^HA@^@sp?zY^0KIhM={kgl4&4xg+<4?Ohc&^jZV zo_!2!n>$(R$@mf`!nj|)U&V^G%ri4)ZB?gmolj%b)GQzUaX?DcB(Kx1O%A*HSh1du zp@*NHAezm1A^czx{m2Pcl#4XU@%`pxWcf?eoND=C!26ryj^*QsHdEabKHaJqgSXsTrE~LmI}yL^HxiE`Qi+x9 zE-yXoKUcFv&gCWqAsj+R3BZ0}og~)E-Yt`<$c0n^5o4X4E(Oi5~hp znl7|L(`uDII8A&?VR%Ff@_?*b(Kpofc|T*Lmp2r2pwkbp%?%~ds)F(DmJZBCyBwN; z{u!2T`7Cane;v4Xp-1WCWm^^MrU*4za+=mSg|IZq$!DvVLu-WKkFwyMLg~FjCSX%wSWdLz^_tk_a}^#JTK}}Csic8XWOPM< z+CzL?%kwkdMDfyjTa>2Ia8pG_UfZSdSSLfo+D@ZFa52h@`!PglZj4^aht)$K{DT&M zv1SfzgsG5MFN9e%&j&9T;Js z@4l|!>enyVwIgJTg?3JZV|7HPHP_agUwvzm=2Hg?0bl`oX=3|d%S;-GwW|rKXB0xP#neqbg6Hjmm>OxREI!_#Ef>$SgYIxtE(ZjaI2mYPp;u*Ox1f z*Ni5$laC6~2cs7C?nu^=Nmon3O{`B#pV{d-5QR04r*rriI)0N7e8O$EkOAq>;n7&c zgr`2_O`?W5O59(z*{)p@TtDtLEzhgWYI5xkrHGPZqJZBOv{7VKO9--$o95>tF|-lrCnd&S!{FYqruYFRA8X@-t`d{*d>iv)mj+H6|9BJ z-Xdgrr3i&Pc${C7bSw=zuAOVvvGU~AuFn<=$K*76VqK)RxL$`ZqR^;zB9_o=g8UR2 zl*J#{;zRgzdiVttBCT}gvJ%=TG|jBSog9+G=`U3l%CWY=iiGOt?D#V?jD#0t$tAQU zT0{oSS{h!x`xgm1zis4v;=eAhe=%yfuY*hV6a~2*ZJ0E;Z^GtCCZZ=ANfwrO9?VjX zEsenG98PFf>`uR@h-!UD^WesD#N{S6AFkruuQ#3xrrF|vXNu^xRBXOgkM?-{s(yy$ zK5H5whY`yv4MjgwR=&BeK^r5cjL-N`u>G#`U7>PCl-O=65R-blZ1hc}75W?yto0y7 zCZ0^sglvq6bD!<=&JfeeLV4hl!bF%-GfDGVSrwko`+lItYxhKc!D&!#vHF!9;)jf- z7vq*~BfD;?RY(c1de>+@g$%YFMEfu@B55>AgN7$jwt^H*~wy1TrbMNdHGDa>$UjE^J# zY4JGO27DWph*)CEwNi_mKs&~hy9s>sRi*ok94WQ3+rZ<1T>*T|kA$I-IY@2B}D<)z+-VXu@m3-FF@+eB$O zqThNG4fth;HK}@XIZVgkPY(0%I4#XBgkaDPjN=a-cqDhXpUHU&Fv{8&g(Y2Om&$Xd zvuKjF`(WZChcnxp?q9BwXDO!9ZQyRL2*m0LPxmNbtxP-GJ!h*sEgiYR@HN|ED3nNV z{^=OWX=nSRV4-y{Z+2RDd-W36bX<67)YVncn(d)v1rwZScmp!dPffb|K!%yHl^xeN zz=lC%A(Ri4cwFBxFhlKlY|oE*k;pXFsXq>Kni;zwrzf!Qdl9E;maH^urC2Em=n6od zXT7N4@%bbXu=uJM5NWYSHvG3D&#SYtJq%N2^3>y^XG|nVQHBYM=l66oc{D7Drguu6 zr%pvS#t`jA{?;R5#B1q_*J4cs1#*+YLz@J#9j%&MDCY|~xV)N8;{|Y^=ni8B4HHrc zrW%Ykxb=0dSz}fPg8&Z$pWTqlKVh&mYH(o<{fz&k>4}{QaW%QUMF$;@sIWq~6_fP%2)D?Y$c{G{ z>+ZrK+`d$*U{Hy*`^zjY)&zD;2{@@OTEG&8YUIP0#_@~_tSO@_y@F=zOU|O*6R5+L z@9%P$vMeVKh{T(=UpjmyI`IzD$6oJx_443i9A|t!m8byndB}%b<6vEmTtX2Jlv`&!kuoVdOA-asJR+C)4Mje}gw1G_2(`ZMANNBD3V+v*1f+uNbj(WVlc3!sgz-XHt5g?i*>YH+1~EXjwC* z6J&2TYHZx5{|&ulH01p3iAZ) z&tQ0DEzmh(3#PpUymo%B#YcShs*b)8nf%qiQ;u|rA|hr_(P>grVz}+P~A5t zN9JS`xi#*e-a!)cZp&h%EzUL>|+lAF0wr5{Pp zV1X;_i8}TDvD>96^Yn=G$t1vZi?!;9b7S-#%;x zG%^)bNMA+oYY$6e@JeGDd}` z+NCCs_Tl+npUP%gJEb$Y4&38U-1~BHEk`1yekg3t_x#S`Hs$STLY)i~Ow!%O0!4cOrJh4zU&elH{(%T&yzQ+R+ax9=}dYIb%=nBGUt?De!ieD&?2 z;Q20*&xj={HCsdFL3ZNzg$@1dF}PK>2*k{IZkl2L-Fn^qyW4iLO)&8SYMk7K9~;c+ z0;x#JfiY-6lWOV?*Jc!En4aiK$(PHT>XRhrUo@S-1a^sn?BaldB|e58aqxrRd4re4 zYd7?_T5H1}pl@(N$U*Sp<^<1=>}PY{rIn$e?IPUgmaR)MiS(n`wccUzA)yhKJVn2bfb@*ERog$I;PQ@_St2Hk$8@B ziUS*NyKEaV)x;pxfA1CBr{%5Yp|4qePtTvK|k7<(!5?l$kx9OFbz)1Bi}{YX0s{- z_sAJj;}b?vVGjJdB{P&$zhfN$$J>Gc5GK1#l#`&xuEA)Q+Hc_k8QHlA3g^bP(Qwm4 z62aw2yFTykHWY9u8*NM876pcBz_9?SDt}DR&<+gaeI|oq9OanD`&#)_qC~4%slXSz z05fvD!GBGV7Qz3TAWhgKz7s{*e_&b+e?gTj==UInG#*q}ZmW~H-&De{OxUl}brH=_ z+i#^9xP-+Yo)CE`MzUA&b!#jvcdhC4)$){8w1iDx4`@d1l_5xGY7A68gJmjs-XMmQ z$u$|^qn;K`XgoLWkP}UlU3sMNeDpg1xGs)t)Z^u)8W_P8Wx~t!QcFQ5dT*|muVkrQ zcOJU$8CKRAzTIn<%DK)A}$YBI@Me1h%Lnd>@59Ff%ecVHySB*5pN^l z|25;H-(5hLJg<`RdyBwC-(s$JpNLhCvpK|CCF2Q4iBsu>o2{$)bT*mM-jK7m(fa82 z8C6jcd0vwN1*jeBg!)KWT*D@j|7~HyQr4p9mM+)Vo7Z3&75GJ8#U0Lf#{)W$J}npl zwr{1C(XBv?3rm7DJvhENt&>%wl;~Cx~64$@ma|2V+ ze?HepJ#x5+=TP^9M47Bq07=$JA1llADQ_djirGP=l#^9Q@K?rZ$m-qC=k?61k-82? zt@+tlVHY~je5ai~RCo^!VNfE}<0;{K5vEg|!~}sdtm<9FhSh=m6?%`_EhW@Cz4@op_KN4@187BbIM8Q<6-y3_+G2z;YNu5FYG0$=AkdGY)92@QY+fa@`^9K?G*rE_YeWX89*69o z@3v$TH949VSgqxqVF8!tKH8QOKT>1EV-8NpgY>5@ebpIAzD-xS2J9`Yg_j<$!{}>{N)y9fSPQh zKZ-GUFJo>9b43?pm&5PUy-gqi4;D*Khkev0Yp$>VSwmpC4tzU%vpfvddn55GCE|09 zK13n$?-M9R&-{cfDOzTeyi+yT)sTXCh6$_|vk^?w{&t?J+^WOZ#q^_!FN{Ov9sTI1 z{QDX`Ytk{F#jvUS-5d3)tj;phyIAQGKJpr0mZJ-5riC`@NfXhP2u^4(CZ2Xn82pTH zmOZXssVSL+Cq45rEn)devDbd!{HXAU=k(hxs{cQ`DXBmPO)=todOK zzph_lf39aiqI+C7pB9shHj88V7K+O$CT{i`?h#jiA^%`h!m{Dn79?75UKzbym9(=9MOVKfpOYX6BZYe)-E;nRf_ zkLUihBzM!Ruu;u|QQp^POxNZA->XNuZd2#D+fhr8QSnQdf*sT$U-E>iM zloShlu^D}|CdqKHP^8@ugf%1uJ@m+hL2R^fikCnB1jJo&My)xnMg+>|9!V2I>Wj?T z2xx|4uh8DkQjqlr%K1AbzJp)$ei#Xor}BU`7=YVcaLe&o5t#wEW2gW1)sx}ATZ{%B ziIOS7XPQ=P3N{f13zvXKbIw&64?t&k#E0W^z8$>@qSPD6eOrCiSrIgy9*bo8Q?rzW zYY+nKx?F`3BH#G+hmwEz^&l5$Q5WTm zPci2M-Wt>8y6>SYiVDM1q9Q>PF|q8goOFL?;7U@QgU?XOr^j zQ)_>Vr464xs~xtM5#wGSQp<-117-o$!Y4KDo?vR1kuLcIL!D$MyLL9K^wVUxn*!#B z6zTb2&=5ZR6Ic9U@-Io=wGLA{k~dF*ryRx^vuq*MHU7^7KFyO&cM;81mn;AllWxuC)DTszui zlU?OCm**;ZXos9OmSHepouu;sjtB2EAo_tIy6za=!jvNgV8MBdl5b;q0A*O}t5^$> zg|_Nsu0H)j<1p+>g%r<_bl3xFwmN1y;Q3)#ktQH>sMHS*nd98?S_D19T(46dp1U7{e** zuIg;};z5?`VMl(_f#8y{YSWKy-=8L(#<7(ikgJ7t4N^Y$hWOKuRaj=dv{*1|G=fk!sYriw4gyPebW^UF0){T0-;4b5wWE)m8bSQG_q$abx!x za7~pu4H)}+OHo^U<;l5%raUBr$s#=9&@Dy?Qvchie#-HbsQo{(P{Y2UB;x~;%pOqHGLdMavpw~J%<|JpBAAblUa(`xeIC}UeO7b3r)4x>un$NF< zjJ@BLdMi;34XWB-IL)IQGLIxXHCp`XkraY=DQL9D7GH31MU4ctQKfj>OnPqIua+1i z>S2OZkN*d$@0r~MG&GmoC&Nh*@Y@mv;BDrX{|;|EPH2LUK)?Km_gqiVKIL%XH_#^4R&{Q( zbn9SmUDR+kk&%ZJ-mc!miQi*KX2nv@jblg$H}4%Y>&j?c%Cv# z9ua=j$79583pdN>@tqZTMqvdrD&%@AZlk^bTHKi|^|#t!;K{92H_s+WsWbv6jgS+)80-ua0~G4S`8c=72xx4{93L=mJ0*g6E8v)@9R3A-L&B8&KY{N*w@D`! z{Z;?arK;?sT{iIva|5A2Uf8$;7swAVir;51A_ir`2FE9irD+2J zCVZ#saqu$<(<0AnWJ4sL$>X3<^vQzD%8tKs`=v>7hu(=~Cu6}W*8CBx1d3&5MNoTI z&f=sUNqOo${GTKEL>v930*k=Gy(1XFyJHLjJQ}#pZIeda-Sb>OJ3OLPQc8Z}2xHbO73w4W2Nr9*?IeBcOr-to%eA0&SSPMv5aL z&doW);!@;!Vj&HG0HGS4L4b zk%z+M1Q=sJ@*}M~pks~C9WNHNAqFifc%Giev#mW0yAz;4w{}t^MW5QiA*CdH%)VD9 zoLU0jE&TNhvLRlq>+PF5Rd|)o@KUEt=-)_X-*?$4M7#_97SDDB39MOBA(oFr?Tj10Pf~{ zA+ZV;i|hpXY0?iu;&5-u6^~?q>o$_9P4{DCLs4tm)R5qQm(dCWuKx(%zpf@-zCYq| zn|1}JU`XL{#MH`2rf8IdBO~0fTBC!l!s94oYK<>Db5pZpK6%^Nun%HBbu3(A$3C{c z*aM>UZvPP9LslC}CUIObi0r+Pnyx0{x2gtJnOKX@I*(?ovgAu$ zp7#(Zv~u_d=%sXu22cxv&@uHVUyL!>S^4S@etdbrt4(RMjDG$o;Y%0Vb}dJLE{lP- zo%p^r+qhGKwPQGT#y-V5`6|1Hp2PT1ppstnQ4eQT1Jk#LeHZ?G@|$k3L#Q7sx731< z$c3^T`J>iUD}uPjKPD^=n^=m4^KQLd&NdDWU#K6emKk8g=a2tyPbT?KPi7)mQK|EK zA!dwr%sHk;)od~|ZlYnNW=;oAkX^Nx$g1YQwrZKv36oO^nJq(; zXMPK=jsHOn25dLP&w*d#?wS-i7Z?&xPts}HM<>=Jklx0@RDm16%`Fo$>fP;mc+h zwCJSyKhX1%&oxMNY;_X~Z9>F|xF~lKowP&5nSw7~4p{aESSA1>jAhszqrc+gccI>H znBNh)#^p=-fz0XgY{c`wAal`G{8sq9F$=+BkE=tExB8Lt1{>~lxcsXlxRPvCqw~mA zet+Bcpu^a4J3x@A6-ukJ!##}Pn-+3J((mqg3GrJyYv*3ZR32c^@;U!8@?Fm zKl&2TV&@``^*PSQ2NB5gs1kvJq@kFhDS8%wYn$cF0ryAqCEAAk!;e`%`Dn4;v~tM^8KOTPrxNFM*))gd@!C`Ly>xT*n2qYAH z^5yn0%+UHygQZ%-e&Ah!jQw36MySS<{{aG!UfiMkl)=UJO(fSA{nLGFXNbCEsJhum z^&q~=z!E<8@whR2D9)u>Y}`NQ1n7s~)yTz(`=swn7O0&{Yw=$Nm4duD?Zrn2SfiER zA@s%6GOuGCi9P|gCQbLWE#ps}XyM(6UIPm~YEENcM{x2Q~!LAYx*G}firz7EU5w5gCZXv+WXAR%@o8)DREq}4D z5VxsDK1C-q`Z(q`_B~)haAmRixR5x=Z^xrXes^-e+Ta1F^=QIy)i?B`C&SNj7%V=3!*;dQ z`7@Fgz6^yIZC2$C`4nl4bx13ox}FUq6H3$2Rf+lQo~J+(nnW2`V=IB*MYHVEgO(Ch zniThS_$Hl7z7lwUsO?x?kem$MbN4Mq|+M?l*^u_K13aGf~7u`6!da;9%l zaAa85xl`QJd`b~5bg0QRo%`r_n%t<=Kk1>$KIQ_Pvao*oZYz==)1$NBY(CWosOIH~N+P1|AJn%36?Ey(G zaLait44xB`VsVz`i?`XU#;ksDW$k{)wzMe$n^U=-JTrCfGmV^3V~U;9;<|#ESl89C z94O~$lC8)cRkUm-kD=)SWBIL#ci7D)>MXCT2Du${jS!WQ&YUZT(8#pwb4587WQ!8L zJ#k46wy})_&-Fb}k9L#(RPKSaryF>NBkU^}{;qYB_!4Tdo-ig{Jby8jO1{tmy0EJb zYnnLi4#;eG%CqDBb}RMxYCyM!KS#KP)D&d4)(|>?OIgMuc&$}WKsxYI*m^YzafEKQ z!4Lkn4{Q|amxw}%FKzmA-A8lYNK8JI#gxTo);Nv^v#h_z6?9^u4_YW^D6y8J~J$euuNMPK%A$+TY;u0?A_K2GJIH_H63Punq z0zdmi@*_an`v;+*^jdrf`sw2zpCT}^4?L#9luuNx?jda(8fS4I?r_c)d{+)@n+VwI zg^fQyY=-bL=KLG}CfP!BY39VeAl~P0iwWG9a)xeD6JP+1``e%)g7W_29qHK0C0u=-g0|~-rnuzIsUw9tTh&i22S@A z%;AME1dge^I>o;&%UD+vc+fog2V9iklmaxIf2HErQXg?OuA--&vyuQbt-6t#zwt8=2yz)E zXROfgtLM<>hjDO(9n$?v9E;IFAGtblNPWNN0UDdI*fa;3;?g%VX(&LHIBnjo)LYD6 z*5S!*uGjT15cwnDUlQEu{}TzGX*qgf@PYWF2t=p6XydBQiSOmoDu?-|-5>ygx5||h zyU$YP1)xOjJOa`{ojo5QI^5Bc@4PXN`S?Hhbp!z8b87Pef9HfuncYL%?|&)v|A9;Y z7uxz426P1W$p7HKiEC}mF}=mU{yhAV<6h#e3;%B~0GK>qfM!qO|BOZA9n~1jfA5}4 z==NZ4y*>!}#pxQ%obQ7Dh{J9^h=xP@fBd_j4WJnWHTyHR{QmdP^1spD|K;QAKx{CB ziElEWOL6`G0nYy`%KecFA}WxIP$uv{F#rGkXkO50gHbrJhSYvVnxh?sA^Gd${?RW! z!hYIy)BBmuCs`519*$zkR0uTlO%S8Y4JgPpjL84_BmBRi@T0`P;T=ZxQ}%58W7(Kn z#xvkZ#K9Q!pK$?qYL)Md1qt;eH6<4X7zlHg08j_l_Z!e%v_66J#Ehy6<=EUDxZFM6 z26_8HZl?pSu0N9UgGQ_8r?54`ON$0GY97w3kvKw}BQ6hU%cV*ce07*dh~O>vr3_lH z-jwk|Wt;|=M|AfF=aS>5O>q4d&(NAWyz&l%4OF~;u<#$>18mpwJkJ|x5(9J$M#fd7H2Ns?XJ&nSI23SeBo3!1GWY5-Zhj{-?yfpD>zD)s#jbQ z1BoXeBCK`R9MfDTjs4+Gn0}wz^#MGZnWeGSw&-z*Su3LT?ujOl-GpJi!^!NVwpQVb zEY=O(z_BXc&0sfydhswlBC*wr%`o{_x^81N=3O~!^_BCH?xD~Q9{8Gx z?E!uhW3BWl@KOA2!;PO!qH-!;kB}QSb!N#n3w^g5(pUhyKL8-qEQg>o?uU1QB4&=Ei_#LXn*%4QRJQ>0bxL`H3J0sj3{x({KcW^wA8Nl_=usg0h zv!B_C07UX3EoSwWzk%z^I&gH>eQnf_Yd>)QFHm`ZO9BL4_EyY4P`NKL#l0{qOGC*X z7e*ARFr6sh&P{5woUt?W7w2`M(0b1M?q4PAx>4ccxZ+W%r^H=Rsk{!HZ<=K9>jBAp zoz8>(gTsnuhar6dYXJU_X0lEh|GXyRQ7uUs4swjVK*Si(&z7L173!-cKmxTQuO_u4IG?%~-4hezv7Q)CajJ4rmAy0_L7_?atE9+kj}lR@GM@1%)hK zit<}me(4AGVf*4+i=W`eh+XT|f%>^(81>&#;m#%v;LGF-h^{Q>5#pYkVSCFGP(E*u zJ2!iKQ|5BkY|F$$2Gs2#AxKOIJgHuMSGDB1V26LX*1xmqa$k(sD(QcZ>)0*}qwzXW zJ6zLl)RBnl29|^AX$80l=dneZ*x`h9C#5g5?S{y{;oSFORxV4fVI)gSRLr}mybwf% zZRZcuCHN#Y*M$RWQzT(RTnW$ei6Z7V{{sJYdFZBI4z()GZ?TbwkN@7>-(iB^GDIAU zIt@?$af)PYMP=ospILMeDQ>FtMbimUB z#np2TDNKuV6EbcQSa%I?I$OJqk`GK+ECNQd2`Qh%TK>$6{sb&newR=%aU*9d0Ab!6 z(ECq^dK_?WjGD-rtZ+)mSnVr!!h5{i4hgk}-j{ye)wpr5%}^^z%u;gCfIQckUsCTqoXy+CgmM_*qw+_1#9{o>SqJD;}&GJ(}o(qDd@yxB`1RCd~X5!$2QN zm_%6WX?DP>iypm{>Md$b`2tt~_c1lDIQ7CH4d@03=-pN)I&dz5YGs313RI6AVz?hk#*m!mT>9Q{UR#FruFL2=P#Mi6O^u$YTvbTEbyL^a-4*UULCp z;$e+=UJ=mxqu8M|wlCv+03N`*;@u+8z-f>CJ4R_fVl4HfDS_pUg};`iq}q#bdT(+xLkh$ktI3 zupaSbrrPVlk-uM(;RD1?qk!ZYhVe`;qy3}ig8PQ;S=w-QdmCOD)HnVc$W1BMhp`xq zXB4LT{`}xhkwV*!@i}qIzzN?ql>vGu!5{o{m#F&zTjzQ6>M%M4nfqka2yjJ;)3Sva2NkEufvPvY^0)|g|AT2HS$DbmZ~Tl^=> zf(pYB{*DHbKY#AKlcW4F#s=JXeBPDIUboo`Xr`mUI?g3g={asT)bc&NLG)=-jfyC= zxADikA1x&A`mxNFMmgc;}i@?;L}nkwbc#x)yWl$1GCKF9K2*N?{$_vm)xR+<(M=M)W-C#0nDD09+x>LuNc zG$Xt1CIJ^9EMfIDSePj>D`8B;R4=W~{vq!4e&~WYuQk1%#_#x)2GtWma<*?+4p+c;>vG zsMnh&Z(Wj4)y(aQ{V=7C9VVg4+5FZ=1;n`pIWiH=wHycImKGlA= zm8q?a_E25J?L7dyS$KCQ>(K6QJKjbLOzar^u?LXpdYxUaCPQoL78z~Q!=L*LG=PDe zTq8fE&3pSPZwqVKT@n(dpTqvibAsOtrW{S{`JmCko9{njGSN2!G2IC@7Qvz-fo!s& zYo4h;@}r1Wnx((E)n7?F%;i_%M_4$OnxOQRK2NwT#usg=o?N&DtDg*bjlw(A>0^LL zUs&T-SA+GA@GO;xp!jtetdjmZ62Mm)1xd{&ab9+t7fuOIk__k_x%7h#xQwuJ3${2@ z-?^mI#Gfn-pSX)onsJ`iCL^sAUv6-um(bo=q+ODpaI6B26D@Z8p)`5iNW)38)vVr1 z8x?y`?8m5G?LtS!efC;!>LMdmT(n&dX5ybSYd{C>8}r4NWVSML>~MM0P=PuVLvZ_y9t`QqRN%{t|Ob+)6tM$R~{3Q74^oDLfSJt)`6wqsLGHlQRX>4Ea)4|*6t2g&FFlL~oh<<$isQHr5kKQwV&q)Ub=En z6Tzf*rR^cH!l@vCAc@&295T+_tzp-F2yjtGZ}K1t2cg^dBI4t?m`|5-Td}&R3T1Ni z{lni^lIb#~^(N{3jJE{wk=-YB!^lZloZfM>nPRZ@L(7bnJ0YkM?|4%s0_&3Z5AjCi zqnd+=<`e{9!?Nw=vjLMr8n1oKxpU$l{_w^%95JP`l;sYo@Y!a~JapZ{tFLG#38&B8 zoo|@*58B5+r>p&xCr2^*wuaeujN{hO?|lk4=&`Dw-VfK-z{j#@`(muhOi zo6S`AB;yDaep*@SqRM_9Bx;HDEM`X?hvpJROZ0QHIso95`yz$tT_+yWYZnB$D&rULc#?25dB;rehjt z<39&WpH3BCk!QP&{H^R7A$k<1lcUvKz%E$yax~^ZN2qR zkr`9%2=bX%SlA}NOas(%Vy1I~@XjZIldjpy#GKMu_#cpSoA-Z0&bycb;?*W|m)JJJ z+VC(_m!C{f542A|-&0wGm7?>uiYPEyv|@>=u?G`s2NjYy!q6iu0wZkdV(+7IdiMT= z{aRjO+EkaTs-}*JTUIp=5S}c(H++#*`LoV)tFyM-XA?csHS_Zp9rCz5uM2R@__&Z4 zYHu96S+^`H+bfT3B^Yo-JFSBx7_8^P&!J%pm*d4i7 zp%-%MhD!bczs;EfLwZRjkPPLUD7B8s3vb=p*MeNLBEsXYYuU={GrrZ+z|WuZ@nU7iq6jqM5w%o3JPqlib!SDEJg7pUG1 zM*lwp)p`F3R4>Z1_nZMhb((15{{pIeq+Wq)#Wpf>zK?$Y0wN7^Gv4l`GWFM{-iR0> zPiD@4hr*LV8yOs?xDM+j#&uZwldpq?|0!#l4L1kIsQUx@bI!xhC`+dI-$wIz6|RQ3 zG6a8}a7S3Pqb{x9HnPL#M;dDvqD>>0+0{st9jdwm!Dp%KxSW3b&{uP5J@r3>kJtS^ z7(w#yH@EgpS*ylO`3IWmx<#`_#>Jus72uvSlf}o5IB$W1(6mG?j_OsQFTXKhq26*! zY0}FSBQ6$5H@rc)>1d;^Zg_sTwt+yGsGe2v^^$Fx{Yi8j#^D&d4pQ~L@?EBqw%LrM zR73oK^y8*8zQRp|uBfGDEvIPKzM!7@f1_?cKnn$9RfNK>V1=o^`@Jl@aFzA_`mk$v zZ_8#9sI$^Q@Y9Ln^I2N-_@U1s+W0-%cv|!r(V%Xl>tqA&&l;coWZGQG!!+;2`gkX@ zWXZ~UbfiGq1>k^pO*qny$# zUfgv?xr!Ctt~P1^cpI^PCj zP!UJ{+IP$;#%1Zoj`mkRoei343l{<`47>VeI|5`m8FW0P`hTwVi8wypT+zFXcwGtx zgazVhDVcxL|KGXoq0#>;_S)}#(17@k}aRA1bcvvEa9GqYt=E zxv9d!VevP-jNR`sP2Dm^>rXrh(-HQu`jdS5YLhI?7X9!*)Xgtl96HxpOC#D#SEH0ubqKF`jV1e;N(oLZGX)M{xY;C3O+|}%K z)orcCZ1ohHo}SKixt7jt_H8^P&`P^V^Y*5V5vFM*L3sCNpxT-2T}(*3FU-Yl1q?y^ zk#T9d7l*gzC>&@`&UIU?&WKjC+ z{gFJ9$`S+~Y^ds?gKhppWQhf=8v7AJJ$q6|&9m&m)69~<6Vr9%GWnjbs(oL#f#!5? zrc9>~(U!*vJhoISRvr+1!Rk|~`7$aIgK*ox6YV9_;FUN`&H-}S0eU!{my~!dTt%@13+n(DgFdnx(%PvmW8b1+>URd@lJRNFQ9Ko_G z;B3l2Yllo7#Hrlj4OktDH*v6|>DMY!JjBJrw9zft+qe9jRjF=x-IVbb#W zsF!-wh0UesP?HIxXSd_C_ysK?^PNso*Danen!sU)Xj00iZze-|;($s|RWD*;?I&Vc z*HW}{7`M%xMljQMbn+g_S6F<0Hy8>-Z+dO+pJH6unhLbBEs)7PWH{3cO!RVzqB!nU ztS;IbxNgDzo1Z)=xsN?&9bQPd?Q2Ws-4-S%!S5>fCy*ChyelAq=d zx$aMO!-{_9R_-Wz2r7iyr7BCxtqQN$x9J_$R129vg(zpyd%2Z2-b&pR?35;JceA8Va;1e7Dgl^Lm!=RuyuDUbWvy3sC7GaZQ|0G_oLI9 zcWXs$$Iibk4d6<#suqxH^!F=%`brzMyc%y1{P}PfPs?RTY$p=VTcT&|QX=RVC+>t+ z)WYXNYA`2H-48I1Aeb}IBUh}eeT7zbX*<&D);2-hh(i1B;ceM&9C?Ky(I1>-y`Ox# zkG^{$SYDLQk2o1N55LU)={kDS&!}>>ET)Fia4hbZ5TPvNZdN;C@7U7?EgvBJRlz8s z)>|Sz#LZYAu}TfI5+lhBfC&ml+nWhT`z%5g5-7r%ed&>Zm&8kOp?lx422J*u*SWK< zNqml2(C2GOeXpsgv4+)fY#Ee9zd3n79W^*Gxg6_qMX{g^aylOC(%3m<2DPEL8;1P- z#cKj@<)qax4$X@!Empb1oX=}HCly7!qI)8|#lA|u2y^EU%ej@5Bvcz#SqWRQVldv; zH7l11t3n-Q4~Cji#$Dn;RZ1?Rz>h>@eCqbo$tBKn-)t;bjgNG0QU(5T{j)ladF9OH zF>3wmZg<4qb-9p%c2m_%V4_o~kf4s$Hi{cqj_K+doOOb+goV{Mz=3(ytAXcGh1Uvj zZ(E>7k9t;GEQcCr_7Nft=4G*LjxP#_w?K&qJ~V#NKAUpy_-%S+&x(TF!Y7;9&~i#L z+~iX`O$7Wg0e9@f16q5_%9fAcvr&uxnMwz>Vnd^u{7YY}(b*x$hY_c<(j=J?hoya= z?s9)$Wv_J7C$_;2E7ZXinM4K8Af zn*f>^J5!(51C6;Kxms8!w_McRjI>;H*|04?9KFgTTb1&Wz0)OBkW#W;j!hTBtM1h$! z@MJd@q4noS&X=wD$*xe9B|5hCBvZ!r?{?26wV`4#g5XM&MeDM z(P|vUr#{|rFxP=kej&6yUS((*Hz{KLn`#)me)p~BB=F6SYOvJq^}^y~1wgkLw|6;W zPH(;HU7_~Qa0WLozVLRHeGq}9TS!T9%=OlA0_?YI0(fo~PYf6@g%dj})}a7p|LYFC z3@^}a#zQ8Bi=|PW$c}9KQtqD2ZcEfDx_Zmp@uFelflM?*T&B$`9$drCuP(wUqzGy& z^iqX`TGHiurmt}&)Wc)QFlEDuopT_x6i7>Pleb| z=A(9_?eZk@<ABZ-40`rSfzO{OzG%vu4Mo!`Md` zM9}uUJV)8TS_tyP?5M_6Di0AtcoQX?bDf|kD*6d+ZAn7*O;hm`_M|Gua#juUUd^9d zxZQ}hmKjoGcIsOM;Cl0j<@DEB+rCIb)9Z+maSm@IvX!CRvC;pKepm}GG~(C(T*!0j zj||6i3^8ov-btG(n6h9e9CRq2>hWrzHa=BvE9~6hv64?`k**We##%#SVY++N&fc720qQns-8cIcC_4k00LjbI?C`{!vc3ur zk2fZuOax4AKCGO?r#6^X1EwkPOVx|#iWub=siPQ@YWV7-KSa{pTm^&@=VBbh2cLnmbn(eyaq0Ee?!dJ9{UEktnTkA8C!ScJ7ndo+1}DZN_S z6p4H53FTD^K!#%vch~)?e6J_Dx%bDi(Q&j}ib1c5Y^mv|2xHLj>Xoy_RH)Mf_4jfJ z#SIR^s8TY6s^ajEA+zh0p`+5os#HeFW;v#s(|N@rLoFrefv#~wK5cHcknZ#TtS{QR z!(j4|M(3>~2cv>#HX4#|z1S8k_-b+*)AREtb+R85Bobdvgfy&b5ky?Dx_0+s=9jWT zyq3@En>@#R2~{%DU~kF^8Kj^H3s%iRLKlyt2=2n37ViF3i)GI1P%5E8*12I5c3xVmLQWQa~FLx-uGrHTO8)%r_7&Y(~I|FktVt z1t(rerZAF8Bpd|z(+N%|9baugWU?=O#q-gWRtDRA=aJI|tn)BOed0g4;V#-$rx02n zAN^olSQi96#w!m^Q^a}~tq^j`?`L*ttG0Vzh8UfZwwtSGy6AbR_6~=wlKbzn*gxXW0S`YmIa}=wmzSvqr4j0`QQ5%)XdFL$E!Z(>e+hy-g`>A zTbIv5Eqd8A{B8OVPT+tMs=4=G!Q!IZq)qpzqw8Esvm?o52f>;J^20tlj`NzVgj=)U zrSpPdS^~IcgfjNMk4bX$+I)zwNU1?&&!cC_4BhYOz79=h1W$Hv9G3jy_v0Lw?t;uo zf~DTxsMMba&QWHN-#$vb)sS9i0 zm9oj8Z^IGCHr&qm2Feh8u+19Q|E!!l%h~{|9gHpPTVx?neB4m((vU4D*`5@V%{v1% z_c?}X{&V!5p+(<(QfHK~-rd4EckA2053J9W{5RZebZR0 zi+>>6-v^QRe1xw@2x?C6-}quTrz?@1+o)wt9%Dsv4+AkjAqf`IYg|T<2nX@m{;7*Py#@7+5NtBp#6b-hXqs!V(q46~7=+$a&y;CLbvZTpLUT^aN$sYK%IeziEjFZQ*#|rqr#v@6&%kV zPd4955x<-GUdlf?;Fw!cV{p_icfBlI=NZ}FEx*8CWlRL-O|?0H!zt&4rI^N@!!jdD zq_6X~QZ7t{W~|rjlltaGQ^Aw^EsROBePocL=CCiY?1`5n3XB`K((_K@PjpS)Y+SdS zcXLN0?gF~;SqfzBK#0ptV25H}qJ>_e1_A^Lu0Gw$s$*cm8#{7cJh0u&75Zlk8(NJD zd6;TfvPf46%JBW1L&5;*@garRGk{IB1%hh?d2sAM|I_!y5l52{2a9EC(XC2Rucu}K zD3}a|&QPZmo^<+9vadvY@n=5sfaDQ%(o6*msI+!q4U-w`P1*%i^!@Ks{esoibcE_s zX}ZquR4VUI*Z#aep#}N&Y$9BoC2yU#>l2LH4tZPhf;Ovq@Y26xM!B@9{kzfy9;s)J zMGn8wo&_ON-^7qWbILjpObLegeCs+)1N8z%8l-*kW&D{kFK|AT!<2Dla+Yj76FHX}Gs78vZkRj20 z&5#gpRa_rv+En~vrB4;jP_cq&aUQh1P)^hZ1*e8l6jVIEd64r(D5tgco~=j8(J?}7 zn!p4Gfa_gs4@t)m&`;Bg4Z}kZ+}NrKxs8u|ofAJKN3iU++PYE`uN>n<{vvsOzB99z z_{da90zw$Ykex&cd1z2;aA5j1M3$1;8b=G_z>>lA%=@jAg#gPI`+J1Fm z4^zISElAH$^(MyYbHGzE%X<|a=l-n&F&bH zACHf0jCZzeO)?v#L5KEbuM&BZ;~;igIq25E?E#kahz!6ZO83E+v(=GPR&kW zXx$=V1$M~F-uV|nUwk)=vd3Y?sorSH*c-UPsJe=5&7b>hfHjA5C~z zciZHgTo@-}4YA+=DuUQv_(I_n>pV}~q%u3zBtpzW+lqw+E)CMxSc&I=lW@HQ=!s$BJ5{}1c?2+!31HsQaz4>AK zoTZ%?`vuegJk@r$Od#l4DgCPQC3GgbtaS?8_(`Q3JK;QA0& z|90dYg45tT4o7NG$t=|x3_7}aJ!(k7GZ=?(Du3K>b0o7F&`~M~#}MRq zI7yi7thIP*=VdA17mE{VZ~vpFYOUE^P99hIe9u3T65WMIpZQ6$T0{Wbbksg1?+;S; z1Jgx=U5}~b>4Th;6;bFhPrGCSbpokid9OFKuarX)o?Q0W$uoj@$9;-QyJ7iw*`-vZ zP07{a5%=3-FQ|Xn+1-}LpSat1H3t&l3*J~s*gEVnwtoZT2a*-9kDJp-k#i$=$Nm5> zjm;#-PTGi?>)f|PFn@K__0s1U)C}lkwTUtu<)uHthqPK_c%t+w0_vq1-$riGtm_Qd zDHMimzD?&^Ks)lVl-*^B3rbYi>{fEqqLItcxBp{yIm zJ~B*FT<%|KX@FV_9a+1>F+Frk`0p4Ry^m?~I&TM9J)=IImOAe_X!nJ-cC%y(JkVX9 z^CCPK_G=ARnT>IgVD z2SYihUSbV?wX9Qan#wGCllNYG==cVcLV8S)i5Lj!6%xo*biutUk(m=i4w??LJNHcL#%cl+{~0meLietZZ?ncG!WDcvioQNyJl^+#_?q zkt$tz-kmIc9*rculbDmwvnStoQ*M7W`Sf+(*=FLXb+A~)M4Pe_eAq*}jD+WxaoIw= znvOaZInGi0QM5I+hvq!eecBy{y^CuD7Zy`uh&VdT8L*pri)LFBmIX#)`E)>L&D*Mi zL5YN>cTnrU0^!m?mxuT+UXy-#r}+88>{4?kX7}RmvLOLlu} zgc8|;>xtgKCina3e}C;?26!rU6jk08QBS;qa>Z|0N%w>$7-9>sc2^2qbno=quRo|M zW%xQTF(Vr1d3A%%bZau~C4(rsN^Rae;qoa?0H=Amta_Iz+w~$+9YfqV~g)&^V4AfR22W+=(O{S-};gj^sqs-m+D>=;EF9 z$@oAMO_x!VB?m|EJJdS)d)%T2*-mx2`9N(|c^amdv{%7yMXHh+%l0-=n%Ll!r(WZ? zR*94;Fm#+X20xS67;7LAaCG_=q;_!1`QR(Wz8-!0N0kIC9acS2>MPlu2npGpZbqXc z5B@FSI4yqvvb9d+SGXgea~BY)zc>v}(p z)g}k-{mf73V{W26-AMu?nYv$Nxum7FPQHfOxc|W_iy=a*(<#!|bn!zD?b|EsCnf>T zjOd9ixY)ezP(GSN`$NcUA^e?QY5LXjD0AV|_RdoQ=_#aEeEF9Xi^if>>Zs_d1mXrp zaGeJMp(qMU1Gwp(ge&fmS({idEt8))=ZF zrA!=Ad*A1F+e*vtL`~Qn z-IsFxuk~BmD%wjW{!D?psEI&GK(GhR7ULTNyLOE|MHA`MNot&W^8^K_*tpmkiL&;J zB-6dN@Klye>;8RUKg<2h*w9zsM9?r6k8bw?0u>8$x@@5H_fq;S7K~7Wu8%Xo#`99; zM(SPbOvg8U#Y`2In2MD%1vcvfdbZm_xKcNk9O{wwSNq+XS|i3@BD+SP1AB)!%&F^ z-mx9hdJi1YG@nW^?G(gf%@;P5nA-Zy2Vyt~KJ*^8|t%>Ah+M~BP>3bw~eyBlvxmItKf#T zBBz8d6Wst#*)KJ_%PPzM=7QVZvv`+q@Q~*i&~E6!4Zj>xurR5SvaX{@vC-(>4u!vF zld;F#`~tIAN=m&|my||UBn$?#jO#^;qdBvNYPtd2LWQ+t;vNlsu6YkRtdufKzc^*c zj);;uJ(--Wp7PjMrOaK*Z@;wqj3fD?EUPpKz+5S;&vKu3#sray>4s#o*kMcr0oH$Z zd2snbJSCBo?*vDGapbC9&~3{i-g_=BX-yQ%unH4tO(Yr}t`jJ3Lw7D_fT2y?+F0cd z4Y4<)#TF8LX|Ymg?|84`@_Rync_Zz;jB|kJ9LdgS?s>>{+O_YDhy~d+kRYL+lz4_{ z!dLjYWT&Y07^Jf<^cUBs0Lm3)TNJR22vT$jxztvUI97b!C>4FKs^+cS1n6G|^WSLk zB>0b6=oWQ`@)gf*N&uzosn1Bwn!Nipsm7rE;n90}$yFtb@OtK5lk(~s4qlIYn5)!t zIA44o40F4U@Q_s%Bk8He`1{|;aG<3s*qE_}C}5EZuHY9|69A=aZm$Msh2wGs7ocIw zH0>l>8`=UMzZP{_{_HrmCw4PHI{Rlz?Ndo~W9Z(f<3t&uO82&A_!gV6_B zCldN4M91V7#hs!f{E#<51X$-)!<4xWp+Ezcsdk6p?2*!)`Ct-{iA9nF$!XAZPPJk4 zo@TZJ>lJN()7#%1n5J98B!+f4k5T%Z76n4?8Zm;iFH~pUQo8oGjD9t{Mj4d}&q>Zr<|P{?e-rR+d6C>>zb9XCJBS(2$8#Sy&3aU@%`eXi*F-!| z8Vld#_Wnkt+jw*zaCg+}?f?>upBo9#_=fW7TS}x@#DvCQwKWSk<;e=Ev91HtO!wUi z!jM7DXz#f_lF0x;f&0C!BtPE}=&ISRhx6YAEX>2M&n5<x4A;6Hwkfs5W6?8ssA}Uc+2_Jv0+#@qBII{Ep(*atv`%&|}4Wo$MGlx7i3i zs?&z-#5$zAIu8xpI7hpLs|9-YT7_+7{^jWG&oRj2=@7MEjyN&WZT=Gwgq`b%^eLU$ ztbA^0VJ>}h)JBURdlQ>A^H!Tcsx)mZjq1>3l`6K<&17}2b9m@(640Ywx6a*icN2d_ zg_NOwmpkgqKE*xlj&Dhv%A=SUNmw88CAXbviItzO5=b<_jCsF4kz{iF7Z3H z0*QmvwZG`ZTgYUw^S3Q;v+>x;LV@o>e&i4rf^C!v5UL(PTW0B^Ugp7TVvA}6x{*9S zf-39C53P#86F-ji@Q*7J$NgIa-{a?fJ>6yZg@LVHZ(n!HX)UWIFuIPq4YEu4JgR+^ zHpiRD8Zyl_N`tpP6z%4%B~NQU74K23)GJZIA*KSp1{DHCPwfJ4BDWseGdvA7c5+!# zTM7SCF~A-pwV!u0?frNRPHH_J+^&-vlBymxPq-(uY3|qDiu_hu+G;nWH=nv(?U|wM zp4-$>^u z!>bIg!J^*oJig;CM-_Q;msKam5)9EU%p z8+F#NjcCOkr9CKe_&L;OCBcDuGT)oO0ij9S{tC}&`Lm+sm_58OZI20U(QZp}U5Y)P ztol+9eplvwB)qLq?HZ%`ZwmR<`E83p`?BOj6G*4g@flauy~h6>tWx=upf6y6u)r~K zIl_kHYFFK$Sa>rVSyeVi6xukJ1`$nKK4!9;X`)3ibv{8ElV!(-b>_~RT9 z6sp3_N@Faz74mjbEg5U(pUIk**zI!-EfJ%7Quxit5nE{M8v@mhq* zSJZEZGXdNBeMjk66{7N^PPjLp{__NOs>JU2p`d#q59vI3mZmbI$BJT)uS>jg68~zw zoDb3{CrKKrUCsHDfwzLx6m~e$6A*euSZqsHKroqwuy3n#z1SVjgj(t5J zq(%|{T76@EW&2KSjX+XcBBzi++qC`1CnH9z;Z|l;W!utSn7YkxDX(`f8pLa)0huOF z8)kx;jQFPwV}Z#KAz@4hAH44ylt|hv#@Zy8D*GN|G=s>stwozE5n1~T;lnRpy2(UK z>@vh@uc%sNb~h8k{~2nj{A7ipVQxqIa*6n!kj4eUetLlQ0%4{dyIew zRWOP%%RYp2Fb26-B>jL%cItYV_X`FDs80XskSwkXe&yhu^Z2w(#{z!!Pk*j0N>@Ej zI7wqJK2_G7l1%cL$U61q7>&*1I$VvCMdB zLP&G)AfjNc!`;Uw2+CZy)Wvj?8H&H%q@#X18hiFX?S|uFiLRSb#|RSIVN!Ns<*C8- zc<%ujfU5LTsjyv>IT^gK+yjj~0li)>PPFyHsO`fu;}kT@h;3#!4e-v{AgoRfYIn>un1$8)KBt_+YzW^|8ke=!hfNpj) zNghX(NelV(li{uAPI&oatm+JtoimEZPNaX8DUyr7!@BCf6fZttJUeQ7Hak?rbWn=BGI`2V1O{TGD4-xsyqHd{gKnaU3(B}kDP z{=yv#_^CE_v7&Kzy>I;fPqYoT(6##Df8T%p)jV47o2-s}r|Msiz`kTO5TFpr&KsS% z^_1{G31`${mdx~)ah6QLY@G35(q|Cw%}(Up3DITWZ>}m4JwshC@c+*A`eQ`@)8tuZ z(2T6x0mu;iwFIs?;sTBy{r>;;UMcGLdH$-3?LyQL?L3mL(G>VGeJ-Rz#*83(_=fKC zcz9(Q&L*P&C(74I>^tT`*Lw<$-v7YA|KB*^|GUouf=(*kTiR^s|FECd(>m+Me&_?UA4toh8CpJTq+qy(aF3=_pb%t4YAS)WqNWwAeX` z`V-mlPa~oUa~&EKF7Ee!lY_PiKYF8$(Bcw zsNuIp6Y<7$`H=d@(vk12CF9Ds&hXjjy^vJtl@Tt!4U%VZM-0`F`r#3w0)27l$O46E zdM)OdjTH~+_BR{cue;neTL0@v0R+@9Dr-%wrXjjoj$P~tqR467Xz|sWXhP%iEHKn? zZ#cJ631Yg4Tf`1=`86I!Vi+X_8r|qY93M1^>wt zblsm{bo#^cPlT|uv7sCO!lGBT4(Bc~!ofcw`k!I%Nxx{)-Uia3A0WS@slL`vUzYmn zpK%=hAHD1w9qy1h5d4EK6X_;XDLgcPF zQOG+_cX+u6A2|Fbdu@H0DdY3jUPGZ_-+(Gm$A6~pT5zAZ7 zIDSAaU%NxW{n&P;1Zw>2j5qU-h>U7`r-8eM9@B6^FZn+x=>qj!lC{j&QSlZ~v(n zeo^(tGHW0AlbinaUai2V3ko%nDCf`X)5w7zOq&6dtj^VV_&>jkV2P!x!*vFR0(57R zMQy2-2p-rE-*r6Iph+-hV&v_dkx9b9>dBN%C6ektPq z4NYfb9Fz*!?L3e=5RTT0=lorP`_5qSv%sv-yB+P!_D~~BdeJk6QPy^|eriGY>DJNR z=LS`?#V=Tqk$h``H|*TH2>`C~`>(!zRcA z#_fP9?fPhwsL}X-w?s2;cC^lGMYKV~945$di<9BFvrcPVdVJHnV@hgTix)4*M$GwP z(`CdhAD0 zX5Ho`8P4^+;{e*{7L%T95cN|pGxp5G5hJrLn&9ereC%eIgIEYKJwF=gYJYUiOz*ty zK!n* z;p0S4@tHtI?jhm?#15HV`jbmt_!3g5hloCM=nD>}Vb96)qN_8foc1NZbNMvZ`&+F6 zDUd!-g}Oyge|Z|>RVE&@-sRF)s@RtIKuSK^Ncb@}fE!7q{)5(AYBm8lMtq}Cx|T+* zVzv*-1$+9D$NwRxuVnJVo zd7#>yL%*73A}^X8l@PIOr-dkqS`%o_PD34_B2Xw>1KyQyur1uqN#-c@*}RxTo!Ca2 zygI@dR(ZQLYra2k6gPZQ>P}nx`@R`LJMo$Sgt3D!7W9*1g#_wm%Xsl#Z2Gd#%DL_F zRMj=Kp8X;IE_MiAzWLrimf6O0PyO^wBg*_N`-X28}ah`u&g@o7wIu#9~x(gOWF7MSvL4AWdmh)hb z&YG){fX91r9=@6eHc?1?rEud9AY&TA7pRS9 zox|T!TF%OKm|Bs$;je}oDBofyw)(E#q}GWr>?}E&P+Ds>s?^$(z5KEXYSz#ZpdTt< zZ1}`kqtPg`uhPH|!&tj#Q(~LWGV^=BLm_a@?Xmf)^?dE)LNpf2Yz(2nT$HDIWwzYy z*44aG_si&+T+Qmnwk@FFWE)(rMwONGKOPG`p>=#;s{QnLBl}i74MSc1NYf^-d6`K$r-Bs z=Bc7_i0}VjZcl#a8Z5$TI)`-!y%d9k;8Tf$8XhTZ9njM=UJQz`p>0#<9 z7k+|dn_?4)iP0CXZKqE29cLgTl+u0+N*9C$+8l#km8N{EKtgfudOikiqSkwXY(>d1 zms~n0Q$-cw%4wu&>#g9NdvlOQleX;iF-B}lG^miwCx$F)u<$n$tEL3cg_-G=+A{i_vWJ^1)+Xm5R*_hF9 z63Hhvm}_lZ^m*%w|AGzDFifccMi%u2TeBKIJ-WG5X zIj_;$DV1Y2+l^!pQ)Hh;o7*`fvU`cVIvc zmn_)rim5QLiUO)=DXiwviw#?F-8%b^*AT(IrzxP%T7#<2+%@KpjvlyI?mG8yivRQ@ z1+5GE88tGmw4Q#3sMR7SjHqA`+5oEAd@Blexxq(mg|FB<*`|d;*)+}CiAG&StbtKB zCC{-g+}yU^W`5Th*NPRu$dQA3Nh3Cz8FG#x1p_|X6xFgP?rdURQx#ZlnTsDzNw7Lg z@$>0~&=gU}?7E&>iz$Q)77u!yZ{rD=WPaI58WW>&L)crbqB1G|8OzodtU?mbAzqsu zhi`VdDK#4hv5zKZN38NMNAlf`OfLq2MDaYISG{w_8eK{3D!f}8%vRkUb|Uf}$}0Tx8yGFe&e7hI+^+)&cSw4ez-(nbeUalq zFI&li80Q@cw=wo`HFTLje#cO{ha`SIIEQqxA#gIR#==a$kC+E}{rj)K*?)M*ha!Vce z``9}&6xXJ+=?r&nS#oE&^>^mE5292`F8x(Ll-7&-B-v;YIH^jQS*vg(x|&}m*xwxX zae*<;aS48pEhIMXnJ{bVsZ#K~5!lt(fY zPfrV_nDSkDKyrS{q3kiZ9LTV`v60ZI68vd(5ItR2qpbmH1o_Q8l$KAr+~7BU_{SI( ziUTng<}9zd&09{y+s?Dzy79Hkh|nT-DP|Mi7NVZl?P~kV-NCb8ag;RM_Hi*$%|mjk zg!We3CS)(prfP)pH0~PURI2DNixgyP2Z2Tapq%=cD;nJ-S9&3l?qizHy&BOl>$DSD zS|c=$cqy?H>FR<-QddyNlk{#l3C+W}_JHnf3w%}LNv`7jTlzmw7e@^+fdVQ9!6-_f z>CkDI287C|n|)`orBsxICGsu1?e@OLiTtO4b%CaB(TGjKfAN`SMla-vQT0ZtF&De{ zbY+<+!)g_tCGVO_e<0wL5f{PkFB9KhxM1@5SQVA_Qp$=k#*FB@Dm!(*@WdtkT9p-7 zePz&{$-c&G4kvP6BAx6hgg0^s#@=YgnQ1OUYh8Z8bavAo-+j>)c?XV8^nOz@cCLvM zFDB?GY2dz_@p7Sx?h|Al7W#3jj&MNSSu0s39_@DCzL;z1|9yicXU5UYt>h*N3UrRf^+rj zqUN;)NI?km+2InM-xHr_hTm-Rn8K)IkW{(UwM3M}zuaMItJ#KaLOA*EdX1TUj`6>6 zb~h;%6=2cy(e+pCcM~+vo|>z2`_8hFX3WIeoq13MH6y;0L7FbOVt*6z`+XyS$dS7n z*d1^~6mX&w^3_E~Isw2NroiRRr0 z9LZK12L+h+ce^(UFydV68+JcRVuXKE2hmm` zz9csJj*0Mf+atWtb6nkz3tTW$-*`{56e4tF@w$s-#ec@L^5sbhw3Y~eh|wX z3bbljW@m!GK5G@(-(rU(q8T=Zg9USb`{W77gn!aoj7yLdIKCou7Na#PQqE_gBD1PU zMS1?1-rwH=SJhrD>$>hw@@?2_c&W(- z+Lq7yGdlhxX5W6EKOQ@m5RUu_b+B|3nCJyG1s-FNnKgQktHpF9y?{6gKgc68;~&4$ z(n;wz42oIUe!#wlrG8*ORuIjx1&^10P|qCJ%Dl@jIYhXw&wF^)zZVhtb1VZk>5bus z96VS&(Jv-*TdlWbrlg#=UecY0{k$b#D1HApp+YUz`1X*w9M^08EkaUH{i&lPFm^L@ zuE%{@;c(}jNvjRiZ#j;Jc!3;oHTj1l&`0By{G+f_n@)SJYVXX`ir+PZmf9j50LbFMlpl!5+HLlvC zxWsxXY!XkGQKH@nb$eYPdkxT+~vwMbU*2Z)KdKb=s%(m_G3S;{R2PuV(zPaWRZh}td}b@Sk{yq-Oc_vm>n1bQXvjX6MT@~+7_ z2#P=UUB($;Hf*V;W6^M_m6fyPF{)?g%U%LE`0xd1+is)vwF!CLfS&q?*-5JnBq78P z1bsKb+kfF)tt=VxR3zrkCxNo#2UQvTz%Vj}T&sSXcf&)n0+RbVS4Z1UmHWTf&d6wf zv560>H>jDv?KrxF4(KpR3Xl?(zt=+Ws`Z6eB53=5*KO6rr1-4B zRfc#D6jKDjnES_M6_ft5`_YFR>L`LPUWWy=DW}m$u zLr<1(CG*?CpTMG*P`S|fLT9dc@QvtQUaNUodFj{V$f2cwBem}G^Tl7=YeJ@k{g+GR zw?%T29xQ)!$-2dB)hRRSlO-g9pXypmWYN;_=2WdlcssWyH*pGa6TS^rkw?h=05s!GXb|4KR7DgEA4;*`-R zVSn$wFoxTQ;yhM%zS+hnWv)}(%=+-`9gzq-h=jjCo^BGR?EU-&O;=pqB^qTWIMZAlB7?~2fnGe+W;v z#nC*>lwGN?YdVqJhmGWt+X$lEN=OLIzSN$stpg4 z>#4lV=xMHEp=(@1?W<|{ zu1PT_??Ez`oz8$=UPk`C`eVFe7kZ7S-^lh&_B)!-G}N{hrYZOC8?C$KoYX}t`c>v2o}U=?xa^?cX)@;sg2^Ow-MMS8uP+&J?8DDA9+;#{`1pAZN^ z5`sGfhrxmcC%9V(?iwt?26sYm*9q>y-3JTq?(Xgu9KM(Az3(~a);;&0`Zj+|)f6*S z^`h6?-K(G9vsj_Z@SARI!MFSP){+yL8mTQMcUBvTv|Ifmb6_8>&^C^i5AJ}v*t^f%PK`6@e3u7j>spJGA=H?L&=3m6Tk`ZER=NbDg)< zrWeypgWDTm@EzBtA?ndb#kItteS@j@hV*oP%!)$$7I5ftzF2Ph64)Sckx0pOJ|$)KVU65$r`4%oG4 zx!9J!{}jJ>gjV^v;8=904iGJr0XS){J6LzYM6~T8(FqVFKV-LPOIDnS)^Q<4sE^JF zO}=-KTnib_D_;t8|NH~iO{htCSK-pVRj#F>-C;`sJa;$>CrELEVg0#3mrWfnvZzKq zWD6NMcAy<>*{7|DOctlS-XA*uv;g_)Wzio=nE#CyI}Zr5tP(7^w~5z;qb^5h-i%u}Fbf~iD5 z(})bHnx1nhgyRokgf-dY?x1{md6-tG-6IsJ99@NRIQoF)DysJFCVwujETCpICGt{< z%%@2Pkyv@B@^nwL#(i8ehR?~iCsFtUeoevY;D?AW8ghd?DSJ8LkljCEacYf-AUs64 zvQ6(xDWY~2>w~X=UtWin2;ic`22jMLL|dJ*z++&4j<|A9Z_c0FquszKPjVS?C(HA= z(uEywn`JnCkjjK4obKYA!WtXmH_j8Q{TrY4XU-%KpGP}|_YKd<559@v_Dh15t%A-7 z^nEPiHtX2H?fQ`e#B4Mc?G35Rb$zPBu*0wdyCXZG?dCt1 zCK#W&NmMxurL5p07|B?Vq?GH9#H$H^ZOjvTYyC3423 zxH1=bYIfMo*cyC(dUvuzsTu<)w(C2?QLbf(pKymigy$c1n}2F;dvCn+jyTqa%T*&= z&jwS@D;g#x@ix7Ns*|xps42G?W}cR|x##p#77_bNjRd~hVAc_B4^iE*_e;b~$9ryQ z&pyjKrt~RwTp-dVdZ^3J#N}{YF?XsRAFi{sqwE>v`aSykC2Es%bgYEh=>{5`;k!<; ze1lnQAyqE`3r7RH+E=7r3rAV-uU7F-)Qhqo5>3N3o)jeow~+^`Hp_^Gs$j0qjET!f zX17zQ;b!p%CgH!bz_z}Y$m3i9P`x-5+qXleoljnq{R<+FlWpe0{FvDq8NDuYVrAj1 zWuv+gwpTMsIE^OJ&bVuS;T1$ww6eR_hbC)f$B$iINp-Am=n+akK#yg2G`6JgL~~A z1JvIEO4k{Oq?&|XXBI%C((ez;_qg~o9YyK!vc)@P0A+viLb*b+UBDRsq{C!H?MN)? z6{OU=kYsfx7k=vtbI@)!=T%<3GuAM4{I@?D!p_#eCk<6J48QJ81#i>yaG zA1=}sxcQXjWhpuN3qC9dN#l+&9@xFjQExMd3E6S+SoZgvXfC>U(0aFdiE~&f>4>ML z{DEUtX^G2Y+5nF6Hy+-=Z|>a_DFevVZ5$5~( zl_(^5e-qznzlrZs=|6~X4eF8CA6l(fO%c^;A_YJh_YSS6pNCmQIkF{^@U(#D$r?L~ zW#X!#MyFWlMMk;I3iq=PzjAaMxbBYgg;~5h&g~Upp|vJTIc5TB7Q-x6&Oqoe2j*7Y!&9%db+V`Nv+XrZ1@JwOKh6TvJ9;f-fNW1KI?e#Ab zllA>UY2c))6V??m!p1@&L)>DQOA3$TZ&88yGb}l}VjT9YG-|sQ5emI@cg9&uA6df; zB~wsMo;lmiaZLpv{_rwvvYPu?-4Qn6s)cyP3RiYtkGGD<)~Cu2A7kUa35!uOxCCw585j0 zYx;W8uz5D;Mrlr6x}Y0YSy|1^Lu;aZ!IEPWx(IBFw5&M z)>H&A$c6QrE`B5B^hSRn<#>=HD!?J-Y7rpHF)>s(%AeErx<28;0sI;Y=OcdBk}}CY ztNsk6&qA+odA$MQ@VCT>9XY|E0sYc0{G>M<8_)%5>{C$|Qm;X~OTnf1g6tD_f4c%h z*+=}~A~ZrhW#{Zo*6Cxm4b{Get6~M*2zrNT25q6P?Y(|U0M6g;&Ua0T1oghSm zXF@c`i}6D#jr`EIT&lC}sB9`63nK35C-I6v1(UcXyNV|{&z+fWkwAuzi1$ST9rA=| zdufmMg}|ci0|uVkQmG;)gEJ|w-&B^%-id@@T4;0_F~5`)G2Xj*1#+cX7OTHp)#VVJD{-#^^|D zqy;rxd?Eb9G0kR2fKpsKAj3_+BI6iD#WLgYoREuR-a7h1=!~>UO&xgW!ZT3pn9ABD z&6LQWAl$}IQz3Ryd?u$yuP*5R8wm$_=dbb-T#OYSXRzo<8w85(0vCov-)D=nb#o0u)iYWN-7sA?LJp3c`Mz?UCpJLP| ze@-#Zrp0-`8#B6e_pVsr>NYEl%e{{u@zXx0L-Ar^!tQ9+AGtDr^^@T_bYJ|UbHV4fh`YipSe$_Oeg6CI3TK=0eTVN(RDDnEH^xNQ8Er~&(XE0d;}6z!4a03irC zj1Gz@=4GJZOQeMgPQy@mgT~aWop!kQ%uVMk(cJ)=1&Os3I3-q}j+px^j^` zV|gY3?~VUJQEw(5GSM~nYGf+z_Uodn;;g0Cu7yyX#c5iT9u8DyI%Hy!VJS#d;ki?q zh$WlL)JoPV7K(<}urg|GK~&&Vkzr`GVh*?C^^38ZTr|-5IyM$IJHZqs#*RHvkaxWr z7)vudf9LL`O+!U#x71IxkChVYi2PKmG=d^gvfs0w0t6WWYf|bU z9M^{kV*3Ch47Blrxk}|4(n<|un*llHn+>wC%m=_vzg44n^2y=V&(pvk++rL=<&hw!-GT)K;SJBPmC7W$Ts};hpk%pF4x$#3_1E#j+!a>3| zCI57W@|HJfK49Z^k+a_o6?U)X)m_+(*#x}=Y!t=?z~j6%7e9P&tebXOZpDW}egar* z9^L6vK>6sxoN`Eglt@uokM2)NWxBim?!frbPgMXG9{WWtt4szR^LV|3RZKF6$RMw|9Czw_dOQ4_$mn$*%PY)2!=Ywf zLUq-MG}}gn7Q%F%YL~u<;YqFh`IC*?FAd7Np~>7&?cl3f8&`g~UMMy7b|qu!Fi>K3 zwIizCzQ7Dk)x+y#hs5}baS zGEtXP+4o`U?}&V>-GpkwT`=iiMEZy#>d>XU30=OG05ztL`f=jBGh^%^8Zo{uK`i5{ zw12=_RlldJ{U!)ck%OfWg1q40)7d|X#*M~MWqp573byo^Yd6AeLEx<^pfR9VmitgF%| zM$|S=+iAuYqAX?2jb%G>YklNzl$b4TTo|LqPD0!0H9M_-QE9Ta;{jG88a18vCH8xn zES82zaHpp&HzcK%#QKd70ZC$JZt*yT_Qlz-3pp9(y4X|$#bHmnxkA6Pu1m$RGH5L3 z(DH(0=e0{qt&2q8ImKul2t#6HzSa{^O35rEXO4*!L7!PcZMlJWdB>UOs-eM}6c;V5t>C_=_HY7AxgDya-?%dmF zFc27C6vz-}AaaHEn2a^1WKh}v$c{VEH=l^OIG1cDT18>;b*)Ix9XE)oY*=dRx6O{# z<2RB8@oeEmYc}yM*_C64Av*3}e13b9i)us0S+M`)a=SZVsPU83xx$cRv8YzCIl8hO z`^tttP!6!$N`~+orPt^13+wHs5e6tnEpRJ~^OvMcPZsRamw0qNzr09N(tic?o#2tJ zV8X|hO+2kB%px@%<(P2#Wy(pj5ssuS((aqJdp(49xh{T>{dlwQr=>OTU-hb~tm&d`3dO zVM7ZQ_k_7|xM+){Lj~7{XvqvQaJU#Tj~V>Jb6HJouQB~xFjsye0{@Dt7RG^0*v!3L zvM%&9Zq9)nd{xV8l#Z8_HigxA2x#5GS?*aSZV@X`oS(D~8&4GEJjuA-=HBGXtQk}W z)V@BIN$#`jNcKC*r%=vLV>t+kCn3MwZXRYRFBQz-?N+<}%<`sK|w_`-3Mh9)7BC8Y0WQmQA?eBU`Bv z)cL4z8*-Z)LU`!Hv1{Y)u?#3hsu=|y6dR<6I4)?G~S{-xy5xitd;@Le7{Xe zpiJ_eiBUC44cR79SGL!Mv{VZpYvYy+ztLe&61ie)Hrgw>p0Q_a;-oq;(tJgC0vMb- z?ha;R&+!H-!f3nB+|Ru`?lFDDA-}gt5jDfZd41Tq_c;}u0iAf3ilsK-&-wc`xW)_L zSzX#5H%d^>BD>wtv3TXIFektc6sWv`d`8R-GLn9HWduEg8ynOO@YvJB%tD)(eQG_R^3Hb!I`ZTr3 zek`ME2M3BuCeV>->VbQyc7s>XBE`C8KC0jb5U08}0%vwly0FhYTOabsR8mvyU9kZG zf>e4d3a#V2-=ZHT<3NnebDl_A%Kw7P87P8M;i#Eg5C4G6i&ayRZF5JTZ?${$1Ewqx zt@kj_&6I$chsgZni6+s5l4dTluu=0YEh|iVP;wT(Ub)J$XF-X)h6SumwUcGc#>#`N zEg@CwzDc3X59~oTdCH)SdFG&N8J|bN?5i!xplB&6Qsqn&=usT5uTc^4J4Rom$zr_y z@VfEQE&CXlTF*@5B9l;5?J#lDVl=~#_*JNw}a`AZXJBVXhT zO(@Q7rdHT3$fqz6*DP8kgd~{!_5eCOT=Ne)yj&iqH;DPG5l`o-4zN_%!vq^cY;*i2fXi#xD{#Te&zk&Ho2gKU1v#pA22ZAlA8 z3f^O#I_&)=ej(;(na`@e4n|k;pbaz&#@3{k zB_mdq39Uv}g*lwN`MQ*SrU!pqog6~!8>|gyRx3chZRIRy#A`udx3MeX;h0RAzL{V_ zoFT0x&{xSTk6URh6c*E{m6O`^<sgjA23)4iB}?RUiZ5Lt8(myLxeH$wo}d5bu?c zJg$*Wq)L7ovM83aL`OP5vG8a40;wS3LpgxCqq4cxtVoqaw8iMzprr{tt-3`PJ0ime zivz_vX|YuBUn%c@plkX^%CiC~4`At{@OoFAK>FUr^FD9Dw~GyXCM{;ZwBwvSab9>t zh7pNx_`O;$jU>$ddg%=bWVBO}JgO0V7(_bDcI$s6S1h8b78+_lF<{1-Z*H4(4;=Y_ zi(*o;kL7RXB4ZVic#YC!PYM8!^x-a=0LQ(YzF=4yfOxd%H(ip ztffl<>W|>A(SAvc^lF6k(@@^WUZ%lj9j$0z5 z$%uvXrBfIaOxA!06u1F?dEZ6*DE}Z=gZL@H!tW$r=ytWBiW1cs*`!Me?O3v`OF3jP z2ZxjPjeg|45dRTXHx7{(0NZ9?v_Xsh43g=6J|=rNG68J!Wt*tf1e%Y^~l@KNZ)!S*Yn{Z#R{{tW-fV3n4pj4CTyyh z*i(q9<6P*y5T-93ZDmu?WDIZ@pzcmpiuv?awhRr_I?>9V+EdrRo;zsWXH${9Wk)81 z-KfUazv)~fOWKURHzEn1A=UkY^@A%yvEqlS#qA84|frZ4!T@?xhct<%K0l2oky;n zSgm*fch5jV72CWi@C*6Y5gh(FzS?}^>S%^OT(@P41A%^pacKR!O*dOqkFG?zfs8MG zoDr)M))ss{>lZ72@XXRjpy$0{C+R#+?JDA(EknK%Z?7nO!C}kBA1qa5A_gwgwsZtFj{y58E~g$0LIt4!deXr#Q4mkMJtw2MBG{!A8udOH4Jv1_PYqFD+(ewa5zv{ z!(qfIn=M+zRpw}d3Y~M#J~m|o=4E+g_AbK|xtzgjOd@lgQIsMaWp5mIkKUE6wrPV= zS6xA%wIS?tj6dmi6T`P6@u~SA5fwjCj$nYceo-kVDHn~?E5F>qET$=Qzn_IA4svhq zQ8|Lr@P#5eDz`XHUA)V1!BJzZ(Pat&z0aB(J%N+L-}j+K3*Z#avfGL0JOZTQ?F9Vw z{Lh)H)&sU{^u@ly3^#4H9HBrku;_6-qxH$Ji#!>?UV9e7snDr`8Zx6 z^2Rrv@3fk1?&7~g7ByIz!Bpv>(6KJE>eOrW{X)~cWRU7ziS zDjiMS!qU1yz8)Q&YGb!%27jhf-9{PDZmm}0Dmdb_5tu(2mF2gv953hl=!Yx&DJr1!BcsCJz`0q$HMCFQBDu#UgT=j%6_$H4uJ|`A z{}Xb%qsf0B)#tdFb3m=pcWOSsFab5tcvk@ESJk-1*f%dztcv)oxJt?h3ZFq!xto`R z-lIrb04s5}%`I#@7b!1sQ1vZd#i8uRWopdxT3v@kz|w52o)CGjDua6iU;}piv}SUG zqn0)}*4emsb`-s`C{7ZNc!G5{sXStmgb^>fLGv|s zkohNWg}NAU?0#+&70sxj{bs*6>ayiZw*L9|w#HRi7* z9_mK_%0Qu_vpABFNgs-a#3udWbEBN=?mIIH+Y4csmD0;qtGV5HRz=)XwjGrs>1pAU zkcT4p%7U?KYiyihz3{F{8)0MBl6`itQuj)tCLy+nLvwBUnxINi_A&_SQ0dHwkY9Ec zB($Swx&#aDK{hg#h06e7cM2h_DQ`x%ZbVrQ@2LB&5vD&u_cVUR<^Ja#lf@g?c^Gs^ z=aLg&XWH}ei#)`wOUXcBb%gcP7|$=ycn1MQ?EmWNrjZOAsT*Vs%;>o!f^Mdn<23!8h3~PV{at!6Y3!%?PsN zxHj@(gc3$l)ZF^hcZ2pi}l~PZ)?(-1}1yCm&=|Ii}M6GBl zg;THDeGc4Ni_uT#+T=!jKvoP)ZrrQI2=%QCCixkB)`-zk=)u!kiNhnK7|IvKn%hjZ zbDlfCdun78x7f|3KDlLkxPJhgHD4Fe;Z|?C+1qyJ0Y}Hmd%&4N@w%X+wOOj53KQ6J z)@%FmRnG$J)n*ZS^lJ!w7hI}Ca#D)Ap^wH&8$q@nXrkX5ROwDT_{D2Wlaa92SJcE0|@pI-yq3c>)lf)6)L8zH}#q9_1eHenK+I%Hg!5T31Xi&Iqf#RCB^ zD=*)<5c?{` zdQq|-kNdRxweG?eR#|tx_8)|(R!E)h)MEfbKv{l;Cz#)sMo5GtfN_@CBaco`!aQ5qG zFZLY4xUQ8pAY`3eC)GK!&L@Ud(5Xrb4V=kyvW&i#{U(9BLE;eqUWJTdLmY_!tQ{iD z5llXpp+!k-wP;uKd%w4a&DlK9TW!Ys`U{%u?(5S)qe*)|zsBct?&!r~4cG>T}3;n8EWh=Tm8F+;RDH%2Zf64ghVn(Zk zCMWAac~+@sxv8#R3i6fWB3%h=D2XEM-~WlQM}zkP|JkEkm>!0wQ*6IbujRkGas!ao zzsJz|1pu@h<1I(e-$UmP*^&Sj=wjq*R3x~ z1htSCT@SeanxOwtVE?%n{O>RD!vK+nc*1r5XA|zAh2Bg*(UqAku3 zL>jGR3FmK)3z_UEq8*d#Y3=Ucnu%{=5FR59gGFHQIIdde1dk_Ao)$_73n>2ESp1*k z_t$$&6JJX{hot`{x&MQ||K=$Wb*Z0U4gTgBnSy~dX+P@UEMob4iA(+lh&qC*)&G}; w#y{fr#upVx6P(7YIlI3xl^!4Fu(0vNjBYf`y_eNC`nfPJO7Fde zqV&*PD1pEieBXWcKKtx*UElZrcwyGcT2EQCp8J`Zdu9k!QV2G+kImmWobzh zN`8nU*ZV9YqiKBj(cjtx(TcXEiEA`)l#w2s9=<Qc zw(7NyKMxjvCmqA*2%z*T`AHO$y@_%#hwg8Ydg%J@+O^y-PJ@Sj3BfWyhJVx`Qn~@8 z5XXS6Z=|ym)2(*w&AK3W5pb5b?-@kZiq&vXRwB!%z|;Ri<1oC*))+KF(rAM%1$ z>)w{m75!AzDcNgPyCHAFQpPlXfPY+=N2S^<^vxhHqFYpV6jx)-GT<5nAL4)Zi1EqD zF7}Sc`C3X<$J-#(HciVUKe6X1s|ICuMo=0tg!L2q{AR$<4{zL<7qxePSrMGvsX0L6 zxP>3&KM0@P^|m$=(&oyFEQHBKOV_H z)fUGWJ2Hphui?RQ(l)iwQ?yi3!DGc86XRXOzlTSFJHp5P!way%Bm8rWhsTWj#KR+q z{Den@`@DzyRZF|}kE=JQ(g^->jJI}mqok&^q9X27)9kH9bbLT^_bz$ zEik+1kM&g47^EHFS}+K5a&vM&7Qe;7z##V4+!CZABlmZ6+}ZQT)-c!`5Eqx5n;WMa zAE)D6D=r=p5fLtKUM^l<4%{6a&K?dh6L$^=XU4xe`A0u87S3jGZQj6a932?0`ZY0i zbb&p8{P=31|N8lBo)+#l{~XD|`R~WVJs{WB6)ql5Zm$2-3}$2bzcjnL@>jDz&-K@E zVppAk)NI@>>|e>)KygyVO--Dak5}x^F#qGqKPUQEOKoS1x6+PKTtk@nKVQq=jsNxH zzc>6dq~5;_DZtC~uVenprGII9^$Z~JTN~VPCRaif$F=z%-~D~R7}u4A|3%_|z0N=1 z;@+qDEitbDS{vfGtf?Jx@$g>YDauG{x#MrbZzcg`t1oU&F-LVZsT?WtPU+rLAQtsw zD7?nXuy8G7S8FLOBdiTh0OvfppUNOcF7utp?{nv4{NVQ=*qO`+dd(|Tc&Coxk-o`I znzLiikNb-c&@(BbeA>>mzK+m zJunPgTr&n~pSAZO7M`{7fNwLo5(^X3T_eR0cqje+(J%5h|M#bZ!{oy0**7M5+DMLj__?AS70Bel|1Zz<*SPwG*El;% zWMP#Q!~bt0`L}8RbxKY+<62H^TN3*JD&YUnTNN?kbQSTlHD0|?+&z8$|vXs{t^l){_5h^o446? z*GN9TtE~GZHC(-jd-eQ8gh^m~(#A<)UZ!kO#UUHI`vC2~7ly($e4!Yo_Z0vVDs!uV zsJzkdk*cuvj!zgDqYLcYX~bfGt&R@pslK1sLsawxJ4|kIFomF% zYRZ|DlRhcPI;_thiI|kqoM3}oM6GK=eDpmhx8$wJA5mX=eRql@!Cy*LM+nYhxv#01_=E&Mxpzq^$JE>(d68q~ zPkY~7NbS0p3ma!I7wq73VtRFUdKig-^ccp6#Xu{<8gl&w$;kCdX?2m z?z@S~SEW0i=%>2`o3&KkD~^3L)YWb`!cZ}Y-np%SJP`QWW?ajHNwT!J4tsAZgD6rQ zimZP?k{qtz7W(lSBwT{`!}pLRe%bjilTtF9l)gb%lKo+$dqbqByOU8h1Rmi*3e zb=0=L^x2%jlnrQZ#L!MraAHy~_ou|f#Qqe*nZrxdy7`g!)oL9@O-@T`4kmtD;K>(< zKJ`iT5>=JoIC@h8<9}8Jg?md6aBMt|$G$8n>sPHija7I#Z;#?{|3(6tTd?&n?rY07 zgUnAMI5gsWrq)%>)?6%0W&r^&d>FzV#d##ux{NvZZ8&4TZZpZU_y-`AOAsLTBEg=V}lqgi0FZ;zFsb+DtSV6CL2D}kI_^$*X!Gtr`Mff}i7 zmZM$S1t_=Mj>LLuW;})dU6RE6gpFF94)K#2CyMJLMS*WaBu4|xZ_)9bZCyaD)P<^p zEDLpt*AM{%hmOTb-#&((T7{(M8#M;_99K_?GaeY}Z>-YtF``C%{6*D~N5ybgHp(2b z&Ar=8iaF0PMy-_lgxd`!(zed04!5n0AJ_}|Zm#io?$3R!Dz~o*I9|VF)_k8paa91f zAAC%Hxn%Q4#!8s%N8Kymph6#lRT@J+?P@EcTr_hS%RlCnXx_V8y*I%e)kPyihtE@Q z161zWL4fmv&OL|LKkbPb=7TG$ zQQJhs{L!b?F!qR(8oZG6o+nt90ZO5#o!eW@S{g2G^kmDl+sQQg5$mzWaf$)~nvb{y z3pK|kv;ot}FRovIMo9)JNyie|<#|oG^21EQ({-nK(Hw;mgGFAkuKd>aZtH5JpT_gZ zMzkpl4HY-IC4Q=I$`T#XUzK!L9yn?BNQe7R7m0|E2XfA(c$4lci@(C4)9gj0z*@0W~=2igm19b2EmsdyHE zT$|44Km&)OLC4nMZ=<)~LB_YUIo7RnYW18U#E#5l2oG8hdn9j<0O1Wp0|`A9VK6V5 zUF>F>(evBZpNHQ+x3#Dj=tdj^3wWk7mtS%Tqk($&>zlfV+uu$}FuV57%us2*cM14< zM_q*fzzLh^klU2m726uHYnyGkkTz##t7>c=&vP5C#q`G|{vD{pSwjJgEL}8Y6i6^>DvbHn7U^08P$KTa? z&pwx0Rf<%4MN}`@o825a7Uy>V! zfYWpwKXwgT&x>J0me)yNW3q|f(xuJp0H|_Lb0?59XGS13ZR$3 zY%{)GZl{=FXXax;Sns~|T#PCm(9e`3I?#d|l+RMlX06gQUy_sT6&7yu#cT9*$gE?7 z->xvziTia10#;kpRdYx^n**kml7F&arfgjiK?H?NNV|;Q@c;CEnvx)jvd5AS7{XU$ zP9ym6jYzUukqbQ0jP9&FnkX5_eTgt@jM07^mQS#Dl8G93lR>q@YfQ{;KXpWRC8AOk zzw}H_8qAcp9>KXxksoc(myXwxY)8p@55q>0g>I7{KjypPb%X(PlfBlP80DbI@fES0 z(Pjhj)CBCa^3+;SjNzNtP0e;J-ZPRw%C$nMlm*w=ei3w-*{z|fS zdvA&_2StJ1jA-qrfEXc^`G`cXW>PsJdO}byO;6s1Q+F}{D=90uY5gf{*tS;}olZff z<@}Q8otXZJ46{W}^w#{jtyzE6!@}A1MroCVV^W$9`wn5V%)W@2cq}DGI6zD#3|K{{Svq0EcQ*#*eTq6pu<&t$bCx;n}aAa z&LXA;?Wiu=S3O_erVuOLgi?gkwvgPaC@h%+kCkp7o1eALMx8T=k}f2i)(GgYOm>9L zi$`!`!+4BDmu>(Eue8-|NQTbzK9{;jaPi{~$Y&y|KW9!9r0m+V$w z5PYkMDzCydo3_by_!V}IPa;mLP5LK9cVDpb*moJ4a3&XBLUZQ+nNc|1&KFIgK1UyYKQiM>cr2Q(r~8QsKR;bOwq8x`r!e?sd7U$!^0}@p;j^JsD zC$>HPVQ_ecKDJceuxZpSbm}D-SPMmX;wd-g1^QQi6*7J*DaRU?X4CvM2>%Df#$0I` zTu>a|k@{-$phB_u#<7_Nl6HItU5ZlfI(hSs{_GP*;~tjpVpJS9p}9J4Vm8ONf$B(w zSN6L!y~EU0g;>;nK0<}>{bu-9xE-hPWL-8$l?3JGl8BIRJ$~tOzfI+3fQ6hre)`WU z=G39sk~-(eTb3k9@_ErBzZv=lIBB1|t3WE|S?rPXLOgaYR3dqC2s3e6oO9O1+mmZ& z$=PIbAHI)_YE)9XG*p?#oY*l{i&TNtA>5|}F{`mEWbCJU)2556TscIs+1uL1(<(Jd zSU+Rcp!X%8kMdh_+e!ENBT&I|#dATSMCo!wTMs)+GNj(AUuHo361Yxnx!&gAaBHWb z?2~OM9och7`&Ts+iHybC1G1J08R*l-7Id}2WM7bEXY!Oie+g~ zv8tQ%e5V;G1BKW+_sW-~g4Jgw4-=Lpxqo>^;)Xb+_VuoX&$j{Ut^c#_UaH3yK`R<_@GkR-luqhKZ@sUeCdwM7^>V8dB{}$NT zYl*#-^U`Y*a&W#QHBsnzgKCC*xVd9K!RPHFWUg+MC1YO!{@`T?6>kT%%_X;Jr=B0i zF7V{(xW=p`Px!Fpa+fYZcG;vM+gUF^l==CMN@av|meQ!#C%dt;8_7+)+I{|lg!77y zCqr78@70p_-@{5glUD|2^1gft8!nHysnGt5slzj$zKmVt>^CTh>0y>_=SAjxa9q%+ zPc3N%kpVa%tM-vu%!=YZyMKSF00&C_MgURdu z7aue+YYTJg`T%rgAIX*KX#k z5$r6}>LW&OezQpD_F4Pl>fm6+Z#)^ev1bDYu{TFlWBm?L^Rw_(aLOFZ{ns4?{-kA# zyt?x|fvV@P$zSJg0Qhw_B96#vc(!|+__rb@U9N359BE%BzeT;q93F!gG`Z6C7#nO7 zA~Ut<6_NGqS33l1>2)@9j^mBVJ&(O`)x-Q9@NfHMb553ip$X2uhG-iNET6D{v4atX zvQMTT!v)8*9KyK@JT;%tb*F4`gJjuDg+|b0^uKKd&KbAH>lXu^hi^>s0Jla9$~#62 zGAz5Ru~q7YW9zMK-_2j<`occ@+Wm&)6UpWGrNe_J{RWCGEZ;4=H$cr!h^}kgiAxrS z+irNNi^{@A?=7g6ub4YO9e?zK9UNYlO}on85^|r<}9DY|@RQ{?#tA_jB@ z)UHB5LB6U1qK|AMFJ7!Ks(iO!#UVBbKd8DZ6-r+Y%oppu%_|WeidL3qB`a`LR*F;u zbsi+adZj#~I!k*V3kC!~Q3hk0bPt}EL!BQS7t(-HGW#e{8k>Svg7B9k*p7{kB ztGeh!a;7N*Yyr63e0jtSXBB^JmI=F~`nD)`9iM<1r}7!w-fbvQVETy5fih%S`)@o5 zh0J?R^@pvf&3*b{;Y`aapcvd5Q*8EsZP`Tj8zac`L<1@J=62H?0*!KKAzf&pUYy9J z?6eqHl(4<&u3GU1P#f8v5J*r3TfqP1i)!bpby*nO8RtaigB!_S#Ln?gOUSP$y={4s z;I3-iOwzy0>Wb1B>wf22e}*6` znNWjF>>R`ZS8v&k>gXkfU;Orzw$&9~B;q%Egqkwu%n?d0VDJ&ijLH`!9)IeX(6sb4 zT7DS8`Q`O@m+Xc;-ZGRQuK*AuynK#7C-?(n0F+OfQL?g0oWAC1cLS+Z;d@j7jmUKE zR<@k(rrdLE8uYs}oO@1gU`r@$_g4Nf@;Kor>i&4%PBc!@&@M7%`%n9QB|UIGORFIB5jQ(HA+axA z5AtJ~N_r*6A?e^Zg@q@~4m^8CIi61ezo2%L<3X>h|B8+3FO3ivknraH{TRLYSheB7 z&yT6J%gd)g-xK$ZOFstD{ARgBnmtEifU$z)jci81MubynTlnzZ&-ARU+5RWw_czEh zDOsYxJ4#j$CGdW+p0g)%7DGNsj68nGfcWmM1Sg(|IC_?mnJ3np%8yg8h=Jw109o!A zWtJD@ce2D&P;2I*H8QyKac{*ijWfw5$rYkby@3j)Wl6M35T6tgJSA%S+K)snA44Ns z=T5yJRpG~t3Gxu+iq(aYe-KOny-C{;#x3y{s`U-A1~tEnl(P?wf4Ojae+VE`s($CZ z|E_t~k-wN(+7(v6a744@dt{_3nLgl1rfcw@uW;pxAILr%b1MY2lf4WYI}=ULs@+om z!Xb8?m|b6Aqt*^3OOi~*nv}h`RvL4>2<}bmgZTZT$Op?TNqRq!>Yx&TA6(@dUQW0- z@puOY`?GW zf|G)o1K9wxmudf%)$S=nMt&yAUA9G5Nwh#U?t7B-v-`Rp)V#&VgBj)yihzX{Q= zme*hXT;S}o#XDv6bA5iG)c1>JV?(HTfnUG7gi0+8 zd!pi}q#n5P$n_x^WWi1Y3?@|H z9v^bn%@7U1&&)5lS1JuWf6_Y95FRG^v@ zj&v7&nFX%%74ADRqwdp&a>l9SAn@HJLLcg{J5F1ovo;)YDQh_GX>po*e?z$8ROUK$ zzL;MQHbdhMO!k>0R*o=A%E2Ev*zDG3r&aDQI*VPZ<6r+eS8uaEolCG)L^8SbWefRc z7g{#9-0fC_vFKAC#vUXLdtH1p;K+33Tu$QL4L$ve5z-V?q(_Z3VZY?PxDGnxZWBz)Lp?qx2aeV#IQ!QcM_4%(GIFK5Jo(d}y zz1N0wk%d1BZCn=^nzg@KW3r!&*2$L@%vLt*ZQ>{r^(Re18wzL|w4(+{pB{Dh?;XP; zVmP9^v_}cWAWqn(e_-v^uu}5NBX#3^|2Ch^%KZ3X_C%NlTQ+zvajNb#%_=i5^d-*= z6!%XJ;18E9IP2Ju@IJTiFjKVrw0Lan`qQBI_w6`t`@J9Su+v94E8Qn}HlDz>^t2qP zMQ`r&E*Q<;EJM#3woW?{&Gdm3b7h-<5DMJ1e3jp)*l{oCBhEIjMSqbqS?Oex^Ivk~hqbax&S*+fo<9a1xfifLT7<2EKm+mxqU zR9W6&bSCTEgO|BpCCQS;@HOSjZ{1N6MhcBYE;t+c+Z)wycBv{NTy1@Y2a~K1WDMc-%%ldqKjKYx`oQ`GIrI;D<^L;Hu}HwYBRs>uLOzB#GzLz}twO#eMK>Ww_CW8Eaa^z$uji znXenYm9&ku`A7E9stJ5ogR**kjx((Pb)#g{G_X+q*5RZ{jXFg*nSp4MLz5%ufvdx6 zH5Il7uDRR80w18XlbMebVrq*ZO1JugUlX#c!^_!_@LN;jm5h>|aRP@1L^46rbxW%( zC;rA)*PBpzot$f`BiT%69a$3XWfDe18Me@FiFgZPF0|tiX4qLVm6S$v;#eo|HPDBf zS5te6a|cJQpA~NHk7i`?oD-$S@-)1UPFe;;xr+9#ZJu02#{Z5* zS{!Jy)iZ*+4_KewcnVlCT$c_Pw*V#CyYPpKJ$rd{x!7r@bbV;3_Ti*yA8%{h_x6rN zy{UeVyLTe@_w?xlv1vQC;ive#V=@1~_h|%XhyxD*9 zAE0NBX6f?C7*95!C(64{30wHcR4}oXy^!8w>vDVS#|@zwK5eRWboD^7#te+$+Yun0 zYTK9wgUH#z8gXRtNbpXKAD>t+QmA`mHQmZK>yGLX8KXyab z5rq}K-%-R;;$2gT-dLO=A)+=D&U4!%CZfOe?wwlFABl=Rmh3z@?hO*@)3G$f9OgX| zHI3*BAgp;q_+=_c)@L&7*Za8GfVmhGq}-RB_507_$(5H*SAtnI@bQL>-;}(H#9UW0 zuGY5aPri)5JEU|WQhJ9+c>l$KQ017&Uc)b4euIl*iP9DSSdg4wU3YNywhQ%=@Eh}o zx3PYYrgh5ohx{bmQT&VfZ+#WvO$ad5CV!;mxqO}+NvkytC|y2py)dkZIN1vHESYX} zaqQ;2Co?ucxFqcG*~GVtZ!ALr;zE&?=p2~vv}wq`4wp2wLwf{^bQIbM(e{p3(*27A z!EhDxsV*$<@Tj%HzQyp4j)PPd&e$~NY=cO*8`=HP8>W{CEQPXCltyt4B8JPsdE zQ6nu844p!$%GU*DWA7MYeOU8A&x-nPK3;si;dK`;@m{@#m5?U_daL9TdY1M##-mmaKTJ$&9>&-nsw@wi0&;N5?=;x?sgk^3ixx{e%!j5OF8mlF+kb( zblG$#;JLV2H8sTe;|Y0d^Aih+&ti9|LF3MbUBXlWq$3q7#DIJ<$#O)rWGt;sFZy&E z0w&tICgp|5^IG1?)>)E-eMQEYz8f~tzX$xW93j5!`M*K3<@@-*L6Tvg8-O~;18l^{ z;Ph~5l*~K~u=F(DUu>YQ{Q6DD&ztq3tt(kLmh8p+@moWjDPQsPvBZy(0+PYN?F7v_ zVf6Mh8;Jy*yS?~bF+)_NQ4Xsb#5d*JinD(oIxR(Dg!07{`zb7*`$uz9Q+S8zi+k~* zf#Xw`ChU$7gcH_?x5jn+eTBx1^7cwYEkZSXmW`}5E7q?0c7lb=-1y(5mwZpu2ID_) zIO9}~7z3=R1igP2#sSbu@g7r`p@)&u=KA8Iz=@s=@r+K-jSOUnrqJ{TGQftMuf4Ki zMmkMSDs2u?gkNKWL)IQ9jp~K1y&v^ayOY+RUu=HXM_dhLW*j1rp}dcCuBB2evJUJB z2}=BTb8MzUpVW&1pIn6Wuq?a9#r&maq5F(2Aq0}?T4{)1bmm2Z6KaJB8%yOu;fHq6 zt#|78rW~}uzfNNv5P=foJ6eUxk6b(p9{lRnI`vs!7BQwx)9L3Mp{d4NNN2Sy`UlWB zfP3hmUb~YFYY{ue#&_>j(4oW^mQsA8mv<~l@%C?zMWF<^zrXkGtJ1u_Wuas^M)?)~ z($83|&)0J|J<-Wn~TDM3v&KHc*x{QMnk)mX- zC;LHlVhf`g`JUwa_Kb#Zxgf=tk3Ol~;qH&#sa{612b>sb$qN`lX4F6$kQb;jv4Km= zo6j+Q9&?nl`r_gUn~8*ybKgZ&t1~Wm++LP?(~44)t6D!jzP}MMDv^wi6-ZBWYI;;I zG8+y}ee03WVNJ7E-rw=z$@C$gB@NoJ_LkBptsuGkC>lF+C-+ zPyx&q=-isD5U2*Q$3C>^j~G4l$i=*;U_qVJY-#3bH!|y`$NFz^)_L$79}RCq1J+2; z1gWsDaotCrAtyo~R6%tWhSF=T)En_Yq!TA+vVFzI+0!eym5qzN41JDy@F_?)`uYHp zqW=!Uc}1$wZk~bx)Q!%5mKgJVXnWkw2F_N(bn6Vpx<|t{a1Jvx6ve#f-3=2~gvl37IB_dDSshD+ zVii(&)I72-NFuJZpJ8b$n*hvoH`%6IU*QKGgmsitsex6cgEN2D{{(PxLx4-}&4>Xd zWmQh1uSHU?>>pE_b|M;R-CLA0o)d zwzlAHu^=7g@~{eMya<985H6q5TRI4Euh-T%NE^U0r->*D-rRPZglmyN{4hJ>o0LdocDL+MM> z3&vV{yesR*i~W}mPd!am8Gc#yDmLZ^OwY;XMxzPwP`qe>-Tfnw%l6b<)a7gQk0fwo zU}hnCiMtS1C$%1DDMa<(^qo2Bi?tdGw$fy~gu$ysc@G)SNa7MI^R?8`Ds zyjm%M$|Q~(#E!|g$6P;4i=HjHi|Qc^#&hUrrwd=C`dlW&Q2)vD8qSqlmeXCuX;uO z?dA-lo)+&bHH@3Z(42Tf>JE?~{Fn0U(u3j_@xoKMs4F-q2zyz( zKdB)F zb-G+rk}STR?)2KYgc|wlU!;m43H_DDnz=mIVQ~0t`Y)SR-=T|#a~x6sQ%sSEd$}yzzk5UvWZNCb8P@6FZ?bp@ z=InS)LDfYUxG`4qZeZk?Gh?g0#Dy84Xao|>FylV>+WMECTzJiZqOC025@H6IL)84)e2Px@P>#zZt`ZQuLri4Z4nVrR*8+s}K%>=n)88e;Z|%d$@m*V?^hjw7QV2wM#uuOL3FGYf!5(*28hWROn3^nWzRr!j~Cz_nuw>CR@IYmNKkJP!$ zCu29hvdtGFW9jZ*xt?8791g^QR!VXZ`#|n?oD4+{`bY-Zc=@zZ)QkXoG+v{kJ{IKK z0!za|n(ttpku9*sS%{)p&lu!Z>Cx4LoKAW*gaF@#aHAMbP1;@8$D+N?@e(l86>SWv zsrx;uSIYKT56jpPhPw+^S~o=(-r5SMmC`y<597V}4&%yyA^ zQ(Reo8FjL_EgYrU=TUu&dH5>KB{C2!5siYlg#H(#*NH8h@YDT6(FuSnO$$)rLf%cs zvC@Pa_6@n;nk{CV2AA0!X~9(98DAZ66b-Mg{p-ZZDw}3q${}?^f)Av8PtlJQQWMgy zKpm#o!wl4=r=>Bt@t3~|pdx?(OUomBao#3*k$~fH<|xj;#d({caiwLRy^1lRO47)h z>?^yr=HoB8Hhjn7Ze1S;@h750$RV-e>4cennEB z;z;VzuM)AIA*QeQt)l)LoNce*Og7IQw2fVDJ4^*9NjJy2P@_?^kMw1{`|;-*o~NtQ zjtbr9d;Q7Hm6e)`a4NbF47ZYcV@8vDKA@Nmyk=MLy_xPB4Z`j}TFF+MYsXo=ABgj# z?3)VG*OoHCuPC$rBCXYnDwBl>#|7(udNVwXJ4X)M5PQT~YTytZ%Nf?~-)pjX95#DS z5)ZMp4;Rsho>ug8Tl$itQg}RA7*-l=^r=7Fj1hE$&$*6%V{sk-Ag<3xOe7c}elo*M z`mv`?5iuW3erD2?<@v7DZ;h1KlE-n_Dqa}D(I(ZMIMvjrXBxBGu*LVOgsNj_$@U80 zBl~NaA<&Hq@2|foy^DDuo#D;%>e}_Y?dq_(+)tX>m~0i62c093F;U54 z@%HATp_}=djm&hNt|7OKel=UO8mnEhLWlOz$yBZ{i`Rcz2%qc@MFVq-H%67t7CTVo zI~=>$m`~{C{ejbNqnE@&zc-r>cMA%oYTwk=q?Du`TRJSUC&lzlNj3O=RGpZ2<~2O) zqp(TUFF<^|)xOjU87|>Qj0nWqEuW$MW=`r=6)rdD?vdIzpx^W5zbv-y#4%r&pgmi~ zL^}!ZH9POBHk#l6rrC#{*u`%rgW=zqc9umH(0#c-A)zi>@7)`!vsKMhH>AmW8%ksalurt+tn_C;RAX;5qgh_}G^800ft5K0j}^_MgU(>^at}99koD z2|_r&$Lo}Mb3G1_SheC-~Ps z06zQ~(R4B67=wX$qwH3O{$X#2J`01=r~3z^nI#a5xI9VbKFmIDKMP>co9J-M@$$T_ z2ls85-cGs9$X71jPN&GoB)&lmSW&-Xn#S~YhjehcAIkcIr#O8+_>1>*XnWgt$_*Uf zgmDjvHg^bo{qA+KM-)9S;M?inOEy?;#2OYDTig`y65hZ8Sn}X3;h5b2)CnCguNE<9 zHvx5?8I`bfE0Uz=y8%wB0Cb~yKsR%#Y*w}QrE5C=cQn&sxtHm+->HC=y;$-|iRKx}g}7}w#J z{8J~b4syTJWm29N+c({6D<(~*JA5|s0@Lv|5LtGr^X|mX0TQX)G$>v@l>F%=#!Nal zA7zbbk~$-b|Cvw~$3+VeI2~AiScGD5W;$|vumjCi&&{G#9fV#vXVkh!qZanK#E-iG z|M*mcFtii;ok#D=Vx2PGo!TaT#U|=UM?cRM&@WgCVsW~@ojvCXJVtV5LYYWWV@{by z(ClW|E6WPBW4$U}bJUm9rw1SZ`*t7$i03fxOuSo39ppYV1{opbvS3Z!_6+2)Sv~V? ztIW$R;?}7}mYHxhzPTJ$b8cXzD4%~B7qm^(Pj=lBmGZqfJ;RJjtbdnoD#Ha@p6{Zk=8lql@EH#Ff3jKk zOqaFn-=YP54^1@qChSiAV|67m5%RTsC%EWVr$z6F!+{?{KU)0hKD*#D($dH3;^aPJ zb|Y@Vd7s^zG7Z_K%rSSP)TI|?{jG7O%}EmVjtSnz)!(X8Z>hi))6vYn4c7g&FqYig zifI?@_iY@z6$E-RCV?~b*@E9`h_AkTVYF<~`C)s`xKJk_@AW^_y4*3%K=h8RxuiE& zMka1q)jooeLtd=i;l9SAG-{gxJU@H`y2RW~FRt|1SQfgd?WStv*$fCAz;=M9j5N~q_DG5N+Uk{+wJ5BDg8(c zt4!(L%TI$t6$+um0P`Xtj|#x*!Z44`c&z*yE;!UuFW$sA3&Ct#CZJ=xm20pY7VTu) zo^5HtK&xA zD96o>^MX3cfn9tzSi?q}AMN9^vVxDg5Cyw@`l!I?8M3Xb{=u`KyGLuPE~G)H65gw` zECr)NkDQS>8sWCNf>UCVYt89QqbB?!l&&D%t2FudmK6HOPR!|q$56-!C&%7EUvLOm zX4wRE&-g(==rc`D%vWUW5!r-f`@L3UJ`P1&;vy(4z(b0g+oC+3LO!K^P+hYL=7EcN zkspz+EFGWX#;(!o@$8}F zf?~631E3c!)OUwtUf-sHcyM`eG1IMCsT6DSV2{hgJsZfk#}&|fSVI&uWHz4y4o-|e zIgbv|9p-XyADK6uXyEGG(cSkwtI%EBTAdip@AmHZaN>Y|Cj9-tCSv+8+u^~mL5BZYoVt`j4v$L)iy;i3sfA>xt{xd)c zo~h2AH)Zx zVF+0GYF#bjbq_7;w-PHm=QTaQhMlnnMf2}7B0H8;LR?1~-i?QjjMcyze1`5j;6 zL6zVN8UwjGX^49NHGz=`oUv`0h%>f_t?tPDRkR)Yq+&1~#X7hHx2dO~?XP~eZPeU7uW-%bt&GJ?(4@}3#j zd((MjIZxJNqc(W-78&CiQJ^-~@2^-wZ=XnSaT;OtXEI^-=ANAod(m&7k7s%^lmlb@ zDS{*m5Sz`1i4mhdakMgI<{DWqm7K2BDicnvuHwv#zqIOqN3t^ZYwK4AM6px&uW?)f zr~D5Dj8ElwQm*gNtH2`d`R-WOWtnH~WZmhDM&v`q4H&1r^lXqOGNVMd&~B{t4ttes z;wift+ac<{;g*symm1p)nmP~B37vg;eFbp*xb0Zx8Vzi!R=zzWH^Zf9vg=d zR{j&ACFEVgnS0JXUuOofo#p&YA)e+J=pG)F!-QdZSy^21Ewr2SJgZ?-l`(Fxd5)Mm zQCxQFFn8j7{P7zTV9MTlR1 zTW3u{c-pM+6{2EmdNYoo`a@-V&l_HUb79@>w+#JSf>oZN$pk~a9~4XsUr3_NfAEdC zd>{plCyGr@N=Xm$*jRS6$P%@HC>uei$T~$$8&vrqoDqyTTDTa>ciFZM=goDA zNM_^R`!tOR>65i##(H-jI^#0qZm26)Bny4OnrEi(tR{+uh=9emG9(W#>BWBful$`% zJ;*K4&7=67`+9FQSjIcLiz;%!B9(beBClC)HweO_TTijfrVFEymd~sZIP`E%{yE(r z;hsGpnu>Wxwow>Xk}*Fk(jnovseI3jyC;qiFzOTei)3rHy$lF3%C2prt{k+YhP+z% z`sF7rZw&YLl1@eCC3pXn z!KU4{e%d^uvC7%5p+uwM2K$f2((jqbABrI$pOr1PC`Ho0Y`5ZSh^=9SFIkRv-PcAN zpNEJPg8@D|T+^zT?r&ZtZ*2Shk;@D6HwOfS0=gZ4N{;zSuZ8e{lYB1l>X&PBJ+4jA ziO4ly?6Xt7o)Dg$f31a^{KKy$j{;sqN&h+@2u_RQD*7gHL|~k}b!{`jz!CHba7~5a z{ij_~4v*~xzs-bCDWaT!|54`s-tzPJv>`)jjbh_+KG?P@t`PkF`HRq=X!@NzG zGgXbDy|hh+-Ct7px6*XtyASn-18;ejZF@)5=YPJd|FzA(3F%)>!|z{%Mm}Cz z6DAyf^yWe!n;-LaZmxKX_ISkXxFNPgZ_`e3D6lWj?w^JKeRtkx;c^|<_$#-a7XGED z!QQ)XoU>LYT1vo)#;KsdfBTC8G-UWf%?@Il2Z0qM{@&{(9bcpwn(tb>O1l1|^4lM? zhhu3@jk&Ih6xV0$if%Uq|5|(2n0K7^czvijeA-39MgH#+?vOT}3>eqh!b4X2;jYX& z*VFzaoU~}3oMT26N6U!su%l$G zyzTSm*Ls?bRdFER9~iN)$JJ}xV3B13P?9*2#Q%KkLgS}9A-Blz+Ri!TXL9DW7VaUL zidOrvC*13NwVj0x<=#S9wK?NzeEG|B&?Pr{L9~cQe; zhGV?z&0?M~;oE>0mA5NIkEb;fE`GPz-1S-vd^>Gi&9&8-#YLI_uAb|B^J`T@KenTu z5?8QYomY&@DmuBU>8_@IDetC<9|&lCdp6o}*>-?1FZLhonzm-741fKm%QrYDg5`L> zcDCHcx8Hvg7uq0a<{023G!dtR;gb)A-_jF4GHXNqI zYNRGEF3uht{vY<9i+E_^cql5s)XKqC-fRR zh|+s+p-2gk&^v^-CqD1N##+l?x7T%soHs!@5pU9_ zZcW{z8ewPm@~EcHKnvlO8r@c`H^4-MkMR5#*|%WqBXhOlL_+uliT55rv;8IvZw zsr_fAQ?FP02Q^&OzDI)17 zRx9Vqm%VP6N)A5bVUbJd*6Dr3Z$=<(0nHrIxU)R`Xc1H0{Z7kyv3b2SC6@HF!402{t!Af3PRa!am6l^cHXbK()IHG-)d6ab zyKF1W4o9jjG1eVWs!XPBT`BS&XOB@e&be^9{L?Y(L$vTgeM#R@e8TatmS`%6!)vyT zkC8rUWiql!S&qX){jpXL?qcq3y#XVtk2Fg(;t;>(@oYtuedLDs?r1o?CBJzyut{Ln z;wzy&J&DBc?`-}6Yh<+eO&1a9vmv9nv9RBZm;;u4RCFM!3`R!@T+HA-hHRtpzF47` z{%_IC!40UBCok%(>Ux}ti`<$`dZROa5R*EU3UG(fmGY|ft%1LD+bd-y{+yFQS_a^5d$^b$gyZmQ^3

y(*9ez6FCwz7B~-x%B_k_Refazl?QIV9j%jR%P*dhARn9m^2>>z zcYSaRhm~2X%ysS4y-iUKiP3{JZsHaBI}-c*`o0=Z-WnadQ{2a&yf1~ zq)yiBv9ncYpO~MNG%l9CFS?yMvegJEz`(!!vTTwOZet*4u;Q=WgM&nkfB5BtPOfQ( zv@L%yp3Thr#|$HQGJFai$CT-Gyd8nDAoln)k&J?#0#b$g(L+C(-ucfF3Zq4~v`%Z2 ze$IK9Q-v6(v;JSS55Ow53PD84D@}3ug@TS!hb5R`#tI<-KsOkjZLp!XkgJ^|9CHE1 zAD(FOf$CC3oVSOew?j4+D1)X4pOW^fN1KqglBl#@Q`)D306gx0G%>gZ#U2XxbUIz}Z_H5|B`OJD-q$U)t>PH&~$MWf8Z94QRaar$)<9M?;9#zN&Be&wun7j zW*fjaqR!U(-09)EPP2BXQ7+$}`P6AA(q92By*cX9i?mfAQ8-K<%xacQrk3BIeUB!R zOE^RhnE~v(@p(A3MS9w2wE3+Q&%}0{9A&syF3_9L6RNEg0JzFx0G=HGATn)W5EHvB zFN}0QJ?Z`0|1rBJeF?r<@qLOD-aw1VK}mg*)dD6LZ+c8UqE1|GNDfsW03rqOAq}A| zqj0Rton@>qZ+tftKn2v06>_`x zOhkInhU%hjmZVcezm4o)FHt+$=WzQ13iUP<5xyGS8~yg=it~K=6Q&f?Iqx7AlwuR) z`(@=ELV2-4aJ7g1xFdDif6E=MquVOl?_nZ*aZ+K6Sp{*MTH0d9xmQsmgS!puGJ#i) zm}yPU_a>W`pOrv$A;f6kF8O2mTya)|E;&Jd>MZ?Pp~y##%3!^ty~u$z*@+neX^0Bm zI>o$@=MJ;5>lsDD?gkokXw8}S+h ziYvGu6#$AigC2Azu87;qy-wLLW?{I@D;eSSP1(UxIc>YNZDA^q!9MMrK^4fYX0+j%fb<)Gf0B->WL?8|ENQQwYcmBEoywXV2Fz(583=?>;|7>jC`aHLuvn>G; zc6|%r2(d1^?An$~-!*%5W@xR%2EWp1%zd%S?n%nMbV+cI>D>KyAyXJEJ9cleU_XVE2o7 ze9nQZ``*9R;hkv%FJ~$p{{%e(tH*eKK4K5ImxKuhwWkc+qPK8R5;o8r{kR~7)`}dr;M`RN_h%lft2DS$eS@S5zX%x`IRlsvPw-A0 zO=s|c({uylout8u6OD-U2F-V?$JT;b6^0<4U97PQQP*7`5b)9xt8J{heZ|#5fbHIa zr7Ec0jBpXMQveEUj!}i8R#&;AXHS^YpOp5+{eh!V8)h@{kZXM!8fVhZ%_~3%Iotwl zLTYQS-fOp!Lxv9eWdTQ;h6F?PDs+~@YMy}-CBRh7xxREYf9EJ!ZRJ5>>e&n6ygJol z7eNZR0HWT6p7~|)|6u=bf%ov{U9PZ z5^l(`>8wZP$}I*hG6@0OZvnZM&{+92)!2R6SZ|Xgea|UGVi$$1n+X}W-6`Vgc;o$N z%K7aqtbmfUE#nGi)aGP3Xy(00=%MsfsG|_C;16fQ;8AJIIjcRtWx>aAJYnuEZCzpu z^$F>2CYcLDO9w3u1RW^l*^yC?a%Iv18TP_GGViQ9Uqi|^I)s1>ZakgzA&7iCeoMA& z#W@QS)L$aYbbPlA4D5NxB5m@T@_gz8R^Q(-qe5>iMc3*>?$g9D4T~U6_uWzgc1-b9 z^Wq#i+M|gR35ctN#I0{8s{>S3E0b1`Zf;x@zeVb^rPmSrZ?Y7#K?$P`L4uFVz_4u- z(C~HQ1jLujl&vfGsh{vLqgvFNuS$(AZ>^QhbLFulU}#|Wb1G2`X(*=#jCWEeLH~aJqOGWbliUA9elBFz>JL_ zo;9d$)XXGgJ5?i)>u^*zXGr!R0{YeQI{GaFm8M0Ns65rco_^*rc2xG^ZjD&Zrspw? z-GEobRH9JuAFi7Cpe%|IS7k3q`uuYiNl8Mv^OOPveM>{veZ>4Ki92@wW&gZLl+%7y zNB0^qN=Z|v(nJ-#Xg3*uE=~Qr>)85%&rKrx>~~@LIS%yY47Q_Qext=VYSpKDnlEj6 zh0l804px*&Tix>`HE{tAADsXrLnztrU+1K--y{kRJiC7Z#`945GgQ%)?5(Nvx+Q+o zcmVl0;hC$OIQVuJoxJgaYB@N102viUB#n>#W7ZtXeUM`b#HI7@%Al5 zz54UJ=h>$bc9~k)DzRz;=j?3IXQOdgND9YVPB-1{um#Bt5wg(P~Ey=pqU z*rEFtw#A|VemG{~0rDLRw8S;v&jJwcfTu^tyO1xN4s^r0auTeeE3!a? z2gK!p~g=zJ?vSHIi>u0MeajWaOUxXrA4K{UI z*9H?VJ^GPz-Lf-Z$sUfnh=qG6Z`%!ySC5TAyZY{XwxcpyZxwhd8|9aXs1_6Qao=KA*Mf~BfZW4FHaH7?OdOr`3!>nV(lMjs_92dKWus*WQtxpw-eV06pXr8Z9#GNjhGorctOfGFm z?~Pojvf98K^y#W_aeL*Se4G1-R`ar)8nXQrO{&}a} zNyq+rh63c#%$!@C3DvOBYTzKpv8nzAd|qw1^(f(!nx#oJ6*dTSQx045o4(%F{_aaw z{iaBFyTRaaKG;}$hJ~0#>>wO!!b-!Y49XDk*_FHquVS0Or&_SO%NbC7rcmlMy=&Bg zBAQP~gy9Vxa!?{LK-pI2>18^1f2O7{J)E@H^_B=w-5nPc9FNe6YW&cAmAqTF!|9q* zXGYt2-l6jTDSw^hEm4v4SvN8Swb2^R6(-jbJnO~7c8pg24b*;zd&fW7uo6uA8tx_k z2ktzE7NgMVfL(Rc4Ek0tZVc69gY##{U3#V=qs`923qvCwBZ~TUVWuL=(iQqTOVMsf zMH5EBvb_g)Z->CFL`hlmBB+1#s;EA~(b^N6cy}w&>-Y46<33~rR_fqN3dnlSatu%o z5c!{p#n%aSz}bWxpm7hNcC`SE`)+@w@3FS z$DM%H{i-Uq!Xx#iSGXtWEZ^;eT41am&J?(-(nBu*RdOID#Ehfb=Gk*IEsG6phz)K} z0bYp9xJdhCm&>|#g3_h)hS&*zpN($IRR$?an*lk#>&B5G!cuhPa1{di%8mnZeBB7GHBsiV!5R#8r&_u!B6sYw>VY>cT)pw)|q zvWxhiP>r*fE#+I6-hc2{sH(8yUI*&XsZ&>tfnLA8@L}ZJ%F3gT8^LqaPw>mFa=lz1 z;a1bH&<0$rT}Cw^2A$|7EmF~NptfaRknbT`H5Zh84MgW-OM=b@d@|WAkq@lR9HBco z>wAF6-HEcRCTvuQ7^Mt@^0iicZy#J?2K)k!GnRbb5Lqwxsl0nFY3Yzh)YWPQUw=@z z)bKbGO$g#EZ-`iZ7ZM5>RX|m!!k=rGJQTqT@`ANKaaWblJO4!AEb44{ngjW;9Ht(21k~ z2VGViD;1)whN;EnZULO9_+OH6r$~?4=?PVaEZUWk>Fo1IuuTakP|fO{vAodMBpJr9 z`g#Md&qxaay8o4{d0#|wU7a_@OWN~~#SP9T6wPAvzr4IKU7Hid+y<(TaHI8Y;3jHag3jF(BI~XIK;9#XvHJB9@l|$|7P%Zju=7H7Ge-~pHn&i8Iw z;U^Oz6z4v7>eNaMQwl!~Jv1cQkM#x<04Yop{dgdq%JqH@xT5j}bI#!!iGZg0E7TGC z5D3j~9|Lxv!1U^~zZ;BmbHMfaN%~Yeqy@gIF$QjzYNyo>8B&fP&tj9nptMLe?7zH?h&^4gyB1FcV=$Ei2qzzL74q$+M zMWo04+&c6D0=nceWG9}F3%8f-kzSJm6CTojpaKstXml*d17;Y4AU=Ctlmp#F_yJ37 zZ1*pxENRf=kxizBc^-B=n^S9ofIl^Ct3s`9*4}rXCVjh$>J-Ac><|w^PehfS zT!mP(OJ4P^_)2xLBDBcv);F4?YgGlFcLnXv?BFU_eCOe8F_>)qaoc)hb(8>i46n36 zMVqRz7#=!%e@gAu?0D0Ixh#8!Q+gy!kEAfav&A`fMrhj1?HDd`;D`TkoCUhI#%B$c z2-z%AXY)3}_~TdQ(_K9fv+e3*v$bJHP_q8wu?Eb+{pr7b2?Q{4;G74vDp|B4ZWZ&j zN#Il&H`#KI2N^8T#rO?A^cpc;(g2YL)> zC`&H;xG5z)XF6Rn9l-r|{&2cqm<K8T7e%i8`_7dK zkL(LSOsib^z$>#joR@$Hwy*r?nFrjeS7HoUcVGG@6y!GGUF{ zInpt$+s=ts(Iv*wia&-@!FHX(OB`-25fE|a@~1w}3?IP^PN@@RTg2YoE09zEa=!v_ z6i@Bwl6vuisg&=C8(%6u0ND?aU|#K81DMgSEyI+t8n@LeuHL?&&F*izR&xKqDf>? zW$d382?v0Ak zU{AsLXqj@<3QRWDHL)hw;c9!iK&?sj-e=`_voVY6zRtaUpP<k-?bQ8>} z#fg_1aFSfZ_c(fF1ptHI17|H-t#_F3U-taIiz>}>nN+k*7y2$UI0E{0fDRz5A-t~O z!cU!6f5KISc;f1JoJ-H?yA)f5>#Ed6OVJ%09u`2yRN29PN0U3y?T)3W);oj?d0oQ+ z&&OQ&NSKQSkcdV~x43zzJrhTL((en{IxJr~EnS#=j z6dAa~Tx^85b3~-}BL4JK9&-c(INm>}Eu})@nck+ZY%0Jpsw)vpb+`+-NEw8nzW_e8 zg*pQMViL6Xp&XWZY9Y?>Jn-FXNL36CK&tLAcH`!g0@Mn76}vd=WYBRc=Sz`0v7C2( zN_+Jg3RPR^hkkJAjaSSMDtOfk@|j$nsAsDcFOqg=Sd0Mj3G@(>cv@)GYM;8q#Ty@61yY(hjSvwTUgS8~*zOo!t zvRyTaRk&T6rbx6U{1o4j8^N~^I7#Tcf4U&HWN77Oa~_oWMtKER1qp2x8rE&}80G!E z6LdaG8EO8ra*nEN_AGR`o||vb))s;={sJfnUy)R_rG;X4W~I$VvFX^z*gYY>OdWrb zv<0W@WnA>iF-3Y!IxkXe8jiEnkGC43%j*pOevyN0Z(B%uJzbgl#RM6I_Xyo>{ z=2MzJHl^q%2<+HC#nYZYEq?QT^DADC@vvZ7}t_23@x` z>{c470pI4?vw>igXzD9}FRt~MhRN0et+Lah$-_NoiTMX3azeQa9!AeZhhOweu0-wy z2FwRqIl!O9A+0-Uts*}W4e1uHg{Imp$iZ0NLK zuNc<#>74P)`-j47kQMASms&<)t@Tc@qHzz+2lTpI z7*T3s>v?*g_SKR>fVh`P_{FjPbPmwlR7qEp_t?>PI0SFIX05zZZKzIummGl~H_YD% z79;;upz3uh^65<}FO1M{=%AO*hkSxR_5*U?@#Y6lVH+(zGlE@KeUPsAK$qhtzhV}o zo=Dl{Fg=RI_NxBpWQ0QO7(xrZf6a~NE3tKqQ79CBJ?cuVhdqKzUo7*(iU$DI`lcr*s}pN)RXS7GyWQleIHXG;d?Td^N( zUs0n)4uW+XvPO-f9MzUvYM|l+^9>Zlk$AhN+c3K6{Wo_7Ypy5e)@AK|QIDWa=k1efO-5e9-$SPJB4uTcGe%oY_72rB5YEVx^AP2xu1%w&o{>S$XwZrC0Su zBeV*}Rvq0hCuH*)jwZ@I`wsMVzi%2AOnmJawNeRtb@f*H)W_gZ8_1_A*ZH)p3S0qY zPyXLRnxr33&0>)>>0PRp+}J?2S`+Wt+k}50=ptvp+A{nnS@SZyeV9S*8=c#9NOfM z8wa4`&qV31;_E;kdrMcsE?AHVk1J|{9d^h#dR23 z90Jjp2ac}+fPyAVL~p&G2|%6i4J9)V@EK(m`wH>f>jKZax#PxDa!46o8)w5gaVbBt zkjehnA#eOi+;+|ZuLvK|+q{nMHFkrlyGsMl883qw=SOVq_c_HRS9yd;iJgj$iM8Oa%Q5uIpCG zf9lqN&{cSL6MoCnA?&Z!%xbkKB9F5d`nCD6T4JXu7D}z=i81>F^Ccs7WCqoLW21a* zI;E|5XcinApK6cRsy$(wu@g{e$aqq6-;9wE^RyVQz1%!!8C~UQF?Rm@*US0Uv5&4M z<$iM5eonN?Tlhv;RN212+re?N-)%p9u@s~$n0SLqTjdmO@PQZJ^R8SKlL2~}o5i{m zIPc;dS)YAzp?KKE=65fCaQ4h?6@<6zh6jUOJwu)NNbzB^IZ$}RH>;^+2KE8?J=kZR$a;unK{r|G z*rx0omtJS^p6~ZDmYyiWBlQ9!l-S}g%k^ni6aN+ab}LKYh)AG&VsO8$>t8umO$Ckt z{2<=C0_Q)swn=M?kg~VDiyj}`7NNP2@1)F{?RwZg)Of~`0<)0I^cH`Wuw;dX+4G;? zw!^t|yJ2OloM(8KH~K9u+13Lt-olNWp1eWFZ6cs8rj*(UzV*pr>29yZ@445)*Iy^; z`*UE`1+MagRw`4MA8ro3S#lve`6B>ej4`_gjFm))cLnXh9LWt+#Z%K1hN4sEW_6s9 zb}?W~C>pR8_j1}0JTATmuxSDhGA07B&rRDsZe!mP zqG!v@zhNml17Du?yT^&@mcooK;kmw3%=Zoxum;J|Pgt)?<@HIsE|lLHUu%#7bR2&g zkeo>QiKR?%YJ!t>$E^xNkRlJFGYYs(4Gm?js0M&DoA~>I8G(3?Na1!J`=1HB5_QSp zEO7SP@mnL@e@;nWm9*vn{DO=<^GoZ{PMuzB!PrZ{T_E_c1|;uo=?3lCX_jXL!!GsCQ-HDN_ZCJ3&@qw6J+!So`m0c_g$n@9LnAecyOknR zq40exz>HNCSXaP=>3S$|=Z_!-csPj?F!yU+?&Avu=6>3L&;5c8#|GzDGdAlT{KWwE z_>;wE;nMF^XGfkO_Zr5puTq=$c4RPZ_iX*(5F}XLaq%U&rRY!K2{(HBM^LMn9DU*y zvs4Hm&Z-TEQWlXDsvPOPic{d%D8y2CR0_@RxY_nN|vKJq}PNFN4_0&>N*7a!ZHF6&BO6udt3w>6uj3< zwgav|3(17g#=;6t(Mq2tZ=}_|r_1KSH%ppwBa1E?@_la3dzQae--n)mSvwSs`3Aqu zhy9}WC%hrv$$DS9r-Ho?U%*>cJWfbjIyS2=vN!}&d`+}QVh0O4<5@QtE;ujV5*HjT ze%QSkCkwROB!GusJ*3u#$jZ1ZLNz~}q6K4*Sh}^9Aehxuj*MAt-)NqxORB3&AHX4H z^k^28-t;(}Y0DKf;!51%$r(>Z7%=j3ib{6OIo@5>pm={`o}-|`36qZ9A{2}rp*3;4 z$X2}|Y|dq*85Quf-f403`BuFJqn$u9YsmE((Z)YCf=gopurt!tlVQTUJ|sSK(8wYL zZA11X_fZ2^Rh`>DzUU72d*L7#&dzDMQ=d&ol|K2!EP6=BD9~&XzD)tU{gUA$oed zaUbmt=@NWa6JKe6suMa%M8E*4CdO*rdo!FnV+@=PKg>N(m(lKZ=J1==(aO-|kOBWC zU>RjHCFb7*v)G!_!6BC8Wt5M5L2E)o)^y5LsK4gx>F|(~xmQbA8MyJ7(k7rReV-VK z3B$M+ZBzLGPkd2v$ui1hVcMGk%rx?Z7`^%2Ds1T;xMrO>E)sKEB(-PurXJn*F5;2` z&W11oJ_nOGRF~x^KQ=2+EhPi#Zk#2#z*KiQ_^RT2%w@e+Fq$%RRmuClW$z+TnJ(mH zFA=@Go%n2d`VkL%Ycp~hs7<3x#GRNX`22yQ(=`2wxo#LxX-_cUR`MDYD16B8IP(th z9BiP~$gDwK0;j|@qreq;`zXwx@OxjF4UFU-wdJQb54=UVtP*}v8?AUXH^F+-@0Mj- znF#jScw&(@OboWvN_^z;TuqHQ><%gNcVs^$LTTMu{Q1Jo!}LU4EN*#`MqBg5SaoXM zx6SnqP|d6l`_fB{q;Q9iT!J2O#K6&*Et@l(Tu;5DJ$$X!ObjYpC`mOv-325U~Ly@lznX3n=V}_IJp!j zzD2roOC0AL^Ub*%SSaFM9zFPNtz^q*A>zK_g$>H3e!hYcFlZ8*`-skL4zOUhn5cu?&i3yt z;+vR&cQ=-o*sLM^jx4tJ?)%ca9c|A`*IA|I;Pp;wlxZKSb!?z)2zRtTAZ|@G+XWHi3jTkZnJ>Wu0hMH(24}-`L~To-J`4l1DED74?zo&(;Dvt zgdaV`->ENRYh>nlj(bi-PQ;-jQAu;&`Pcs5GWISFq*_iA;VDJFSU(V6v-)}_a?Szx zG3!&v75T4f>+}I??WREsrS-uj#D&&N>iPBQ7+7|0k)6U}9r=qIt9(n0_1M*y>G!NE z{Ol#$7bEW}Ll5=_9ke>P18GgHimirSvQ}0i9lhs0(gn6vn+77XoL)UZ@a9_6uJUv_ zq}Of-O6w*#C`7KLBar)ztjlvYA#naKr{6|1gK+lvP+Rih_hj0JZc?_OEb z?f0(TKeHOvPbcKhq=MKUeapyvZZS+frQ^M)Z+}Vig8ERJ^x5y!%)`^jN{Cg_5e*8~ zJy8`I>iIfV^ORZp==6?TbH7M?IEDg4NWS<8Gjnud8y{AF>h4!pdrE;?PpchxP;D)} zxEfA;aaCP5!5yb%8CYWd_qZ{k;L7c@? zEvKh)#&~$`*k>%2eOSjIq^c|yt)=@^h`;k(Ky5(iP~;I#5=Sfa8`>1@dg3Zxp7cUfp+nnQ--0Oc< zP3*Th@v8^hjV1)u0Zp8-?32&t8;s$-9+nqFKP80!UfwT8YQLo#n5ZgTRVaXWqy@aA zG)2Xh$2PyZg_RuA;Ccc?TP4wE)X!sWbwS$jII2tOHd--{_Y{E6>e;7CJYb!s`Uw1i|J z!Pr0h40Qs&m*QF|sN{`vpIPYUboEa`CV)0{?x+gCnXRiN(KbFx5iy&1+|zzfRmo%R zQ<*9%sVZhpT*?2qsQ&w}m`!i`Yy_Qdd~p6Z zR^!H(N2D~sQ=Q!eUj183&qZ+GSiRRQw4CmLvQvM5vdX+0;?C^dJuUy%(g$e*OK&LR z5k>iL?ekk;pRGRV5&m0C|GpPkdeZ%=%#HuiPW}Di*;#>o4&Jl>^lvS_;Ui$_;|8cq z{;hpB{{ZZ>R#dF-zqRxaD}beM^E_e}{I~Y`{|~YMyrcgg4zV;{E7HsJg7(g@Be%24 z=iwhqW-cybwF{V+ga3|Rn$Ke5css>9?{j^B{*!(Gk6}WwgjMv&__+Tni8-6DKKJC> z%r#7xO+R3ypE)`(H+Gb*ZQ++cazX!jOygM9;rO==W1M8aIOAR&*CK^+6PmrfQ3{jy4J`DwC$i%u4hqv$VINgp+zpD4M>QQ@k-mT*|$UI%HBG( z9uw+sQ#a*0MV$3>EWW(1pPSiVREE2pci@nD%Uq3aA^5l0^Jwaiz=%$PZ7PPxy8Pm| zp(t7~)QUvaVIM8Lw;@5g7l23aguON$4>pCkHuTnc`5cfBw}1YUD2MNCtk&HzZ$dAFC1YpBZ`m}6&7H<#4;Mv5_cAvFjp}!^})fP zn$ETUHlsF0vZPF1A3AW5%xhYiYh))euUjTCnlS|^_X!}!X==A+3U3)ov}WAsWH z4!d%nOyAe$6a>SpMmzdqzOnpQcPijU4LoZ5;>fy9F$|F{L)9}NZ`4lijv)vb=t z)VYODRE&@|%kgkTRAp-w$793_3r{a!jN)7za`fSxo-EH-x%ivUFHKvf>fFl(e#fd# ztr@}#)=O-VOYXlB$sK9r`Y~PCb_*t@#dpt;oR?uXIC)DSUixWk6~KPGCa#W!$asrN zw5!aUu1an8ipd$fHtJ*EiaSpuW>>u(pmL(N?<MJ5zA0OxD zL@IEGY*k;V@|KHAA|4~sr9{8yua^xz^1difcIMk2$(k^qBXE9bs?j+v-J{kCj`rxH zFAA+!9Cm|vSKM^2Q=IZ)m*48HXtK+Qu_Pfl)vdJ)r>llDY~QMmlHO;D;cG2Q?5`f> zO&Ddb7xKI=$N#SdPLL`MbEnPDZWq=@zB5&ryZ}}utNLcl)$!@$XBDU3`jmauE8m?z z@w#gkA4!O|*IDr@5&yM_q?l8-zfSyCioD0awIL*%}uc z?r!|>Vg4eXMF4vv<6dv?n0_h;=ebc8MWJw#R?=qGBC<%ygSoMPcH2CTnp>EpG*3c) zYZ`G9R?n`wYQ6v3yhJ;{xCk=y8h*s0O-JdVUK1_g5YrW1EL?{TVrdaU_{Dm!Rq< z=>9Ep)K2hh--|N7J`WyBjp})vh7Dftd)XT8?*$h?V8i{52c8 zaVlzl2~97VpU9!oiiZ(*4<|EiC8wIJ)ttdaF;yml*%$*x>t6O)LJ~w5%Tf-n>TtcJ zsnpD7j>xIPz*V*mt9p(|+ z6YW_mA=OTLQ7zQ(`&tU9B)J|QxtVy>#mG&u3#U9BkYB6HY(tM!_}=CIwduSyuD~i+ zio{T~WK6Yfk2N;lrJ7)KYTV>$q(Q&WO6(hzmt1rwU`f>W^IMVV$!TP-llD!<9H0%{ z6;Moqy{9Z5nY%XzWAa0f3|)>$Gcz)Zyo~(M&zG*4kc=-1Bk@WTUuC`#IoXVS5Ceg;pN75${#mo>&>o;mZ>!zFXXPXR}*-hj6O$M`5<{_ z&l+P*&buy5rrmtyL#GFpg>~zDszux`(TB>}i=ov8!C&EN@#Ux+YT2&;RaM^8JJr}s! zJ*DA3wk4HpxJ#!HkXR)3;=rs^g!WeR&V>mBUaKK-dOf^9gDLB2e0Cs?7F zy_|Y1XRD8^jNo5&-)Gk@Ov4lT8UtR&+oh>zYZ6kO5!0bmIA|{Rd^xV2^e8ubJ#)1* z#JNf-_VTCz82&h8K460%_s-Ikq#WlRpvQ)6o|)-#X-&tAy-cL4hXkH)7H7jkBl}(q zuR;t8br~SNFZ%g9Z8X%<7V=Gxo|Bmj|Mx{Fnpa(i*VkR9FJls(n>ryZ^ijvE?$jgEa#lxr+$U+<4KvqZD_TfyTUf#=qIb)sYq-wJ2UoO=GH*d!?!kps>2`335 z^eXYaDA@^<+*$2oGtO?rDwXbZ8!V%v)#y}V@k*xV zjT?f(P(5rC)0kGQjn~D=1apG#n)cUj0SSXA6euLR)Gr;2r0}I2BFgH7S-`9PwbY#G{{V#l1K1!tl4v=j38$$|+qD z1NW2OXwQ_xQ*)xq@3gnKh~6F~e8S#1h?^QH9OU~DicEYx7Y$j@G}(iwk3(Mf!)(tU zxpSJIdMf?d^sm6*I#$d}v=tFqjR5~wC+;tv_{JG;9<1d5$~|)NdpMR74wSQMjg8dj zsDix>qk>GNNAIT;w!Zs8nK`Cd2aYC2zrqOxL|hE?fNqoN$xmO#8zr)=Kk^q3dp||R z&b$ynH|DqB#XcH^6c}fsvWM%(^WC+uktKE zy~ovAY&Ja5v?>`Ys~xSvU!mLS^u80UDg{ybb$psB;kkmZGSHc`ssQp1I6#l(A{Z2_1*>!ir>M>N>wsQ>H$h?%Cp7 z$&$N2zjvyQ{9o+7c{rQ<*8ks8M>=S0m!fuymZCM*6jg0eQ(H<6p=zFMCK0MyI+<$> zQbkmWnCC=WRkI)ng0wY6#GFJL5x%$Xv!7?5=bXKt=bY>J&+od$CDnmfszmCt9b z_jZ7U4~tJpEQOs z@S5$9xRZ<;Pcu*6sj^e$tSHM_&6TB8$@|SX2e*QHyZ1SMNwvLB zJrZZP8}N0qw9-1|2os-_sd*9m>}MS#g$Ax*N|Ax51WE=_PGXlkm=HI^;XAumf3#G) zLuSGw>hm6-@=$~t^vrhgX^23pgyV?T*na64NNlGQ%rxEB)h5KRG9!K9Wfqsw45|E3 za>r)KLhx|lY4z5n8OpLy)`>LVS!DKv-1eZl5F>C8YOsx4zCZgxWmLnN%&QAeB|9fdkiq= zuzA;OPr2C*nn?rlC3BW3 z8^)&CDg!KJ(Q@(!p6i-;j+rj`UL3g32?ioY9F7Rrs=Gd)wP52ho`|*0GQV5T(=KBT zF*DiRX?Um&>+!%Z?$Jq7;8fh2b7{zU65Q7iHlieDHiLP?UdxRO*%N*9-8VgOSGJ~D>HR*rJ*`qW+FctK1|{gWeD~t z`%6*yaP#_J9$ulOd$k*bX?@M~l~hR&VhwuydoF?a=tJ2+M~|ImUl~Ot%3=4=1~p5z z_iI(CIVKAwLrjy<#8PQ^D!IMLEjA?(=32Jz(JKT3$Wuf{EGHHXmQcuWq?w_TOI$I0 z)H*}{a-I+Pu50CB0o<5Ww%dTDwBViC9W{EOMRk7Ol=oO5u8U@mra0W%OztLLFzz;F z-XjF+5KNY49%+JezTT1|G;UhN180TBibY579v+5Fvol=HLyB+PMD%QHi=*ZEXQS>s zEt&KcJO;7M+W}(i3*NAAY#d`YU0+%Iz<}y>l?r2BOQ8`Y3v?!n3(}?)uv%3xkJ{ zo93(E9hLiaqKxbe=L7_`paQ|u;eN5cy`N6W;Xo$RDe$Z=cYfu(*csAYC4;1>Fjk!! zokn<@RXC@pBB%lqQmgT~jc+QgV^`DHXI#>RrmM^r5>fjiy%|#Y*7GO5#saVJh=_xQ zv@1vy>X$ECq&o*9+??K=lu%U7+|2eKe)$$x6$N+02A4I=pP>jXyiHPe^a1PXqN{t# zrq12?eqFZ%x#v{YaX&U4BYB5qf9Oa)rrxDJ5GH?rHk(L_6E{FVk&`)jJ_Y7jM!(d= z8j+`c|8_eLjNZO{`X#qyUwC-ke4Oo=9UBGlsPFGru+}Pjtk~XFm#l+jUM(naGU8Qp z$7irjTZ=IYt2rmHu3eG0@rWc9D=@rw_W4c8g;^i>z$-{Tt0?kpoee=4OWo>olmC4B zeF^#1BC9kiA60gA1JF3ag=%|<5>h!vu&Y@b!;4x{jonArzif%!6f1xGx{K;N*3(Wmhm0=sa&Nb_?Ij^7R%;7)7tO$O(AzB}hh=_R(p~mJfzv@s>3PoslfCopyT=kz`nKaZ`mq6? z5@8Cw?rNXu3p2?5z8E*xXy0@?kf}VR&d4o7L+L{mcR`;R)dmp3nXKj>Q&E)B46TJb zXv7h+Iv_IT!LO0in3>ch_BDW=9CWBi7r|h;9=ggsdZpanm&R<%zeS9TbCaiUlz!12ONYmH#s0O5cy5QaW`KHK#Y@Ju z%2D5YpL_9gnu7aF9@uieo;i>>R@mB?lED4(U3gIJXzjHuY_% zlt+4$+;A(Y_%B%*${GUtMo$*_mbj<*%8ayH+AABXrf*f$Xg9{Z-5xctN3P>-QunLH zO8j8yK+`B>%&NdDO-Q0!+1<5j`o70|X0r#i7U`>j^LY5#7H z`5x}R(b`~}WA^zSZnMRuI|r7tGS+w&B;ZPBdSsrKHezu!)4ir=7iVO0RhBY1P^@vs z7+@+qEO~Xwxzg&g+q-#xjrLWG9-9|Qw z^6IRAp~3HH-K=-e`fOJg7#Y)s|0KaVi>+r z81Tpnpv0U7(q~Lqk@ubI>1jKf-HZS3a{S|8$6j0!^O4ki&J|5P?~zf%t!j1uG1wS> zQ2yfs&k=lUTz*qInP<6zdM`#q=Ir;&Bu@r z3u1?F5DYr^TO91}oO{=l$jzU0X$V_Nx&IN^B|2(6%^h32c``@Z;OJ9{`+xLK96UJ= z20e&$7hK$>GwVZg%P;#?=$2A9ea5WRul|A@A!EbCyk1)j0hU-?pS)&nZkCm8j)7oS z>{VuNMjg+*s}Pz z$LTs;w#lNUEl8Y)BY1+w;RD8#=aKW2zEYHxZRFPg?g01q$z7I(#x1P`W&Z;iKNlU^ zavD=z1L8g+A^dHPC#`8-alW6Yk$cb_^3R;f&K3%jf}0%wmR z`GmlBPR7OvCm41FNL%~*r2D#PX~+A-zAWtk)p(#5#(V00bYS!#>|%R1xxX5QFB^VM zw=N+@0T7XKtA|dj0>95@&2MYav~nl?@kS+d{6b)7#ZIg!1 zs0W_=cvf_Aq+H9rd{HeRyhuqsazIFjslQqt%Rv+s9`N&y`p}o@vs68lSiZ`dUC_-I69tgi-KpOd#j1Bbb?jm_YUiWDjATCdd4A)>K$N``O z_31Lecxo_QEbI2K)wud=Rp}yA@+f)Fzz|p#I5RzRj#u@{<3nBzHRgtDVLBC43M7r| ztw44z!BS^3ErBbuwj?xPx`pG zER}rElL^*2B3zyz5N)e;H?W*KV--fsC1?U23r|$Sp!g&!k+Xqhg-&6W>WA~E6&|fp z*k+%T*=khUfY6hzp^d^TDnA#xSKoGr=&Ti=x`T)doa*tNC1Pt$bQgCAZv9+Y$9x2i z`fR1W#a>&hTu$(;>S%x6c#aH*ZXZl39Vi=ZKqUU97C3mgbqr;>Q}pEg!R-a`nWc9i zmptCRd2E+kgp0iIR+hEc)p7r4E5gx*NCUK(HfSn9K(F!7)% zVD|7kBaUrwchd!ha0|~qYdj#yi;O^RRCzDsZ;jM}gsvz!LP(K{3=7cOXJoAJo4OlS zlY76ew7>JTn`fcR)W&n*T&2e2xz?ZtImI8v? z<}Bg+$JOcnUuO5hY}J|~0K#Rkja#g?p< z!TyIK0jjFMZI;tVR17StzKPRg9BiTR&W-P3dH5zUc6LHuv|t?W=iCoSlk@Ql8^TgA zW|vmV1+GWwx?kY1lCQrd4$IqL9sMAU%korWy7=yx0~QD7<7p-t(U1;)o!a!hHpz$0 zXZ@_Wcehbd{7$y(?0h2TO{Wz>af+HTvJRv$TS?MitygnQB_$ixu%TB>jvHQRuFZAl zmN*DxM+a~0QY*hYa*cXY#o+H_!TQf*A^9L~-C0Y6DSULZs6*$Ul|jGXcLlqPKSrqI z_G3UX)zb2mQ_t}5u&*JUs_)n;0(OecXfeAC2cSIdYYJ(nWt7lmyVD-U%yH(N=1W~Z z^15A*FfTLi3bzPeSJImzrf|wX-p7z&3{e(&F{Q&0ua(Bbnn5b)%^qU95J>E?-78Mw zxAI4J6$(D@&oRmuJ5>JQn_|Hwn-{_b^oc_nQZKW{Pe-}-sUN6ok%B7YeUs|d?1ck2 zk<(0|mFU$#11idIFCg&QTF7$mK$*1UUL13zcPA;ZL(+#}K+AO{heKHJT7Km^n=R2$Q=W8o zx0vAjr9tZS3ogcp$%g9&uAs{@Ul)CQY>;q;xldPw991Q8O{LtZkNPH#_+H;5!G<7$ z6J}cAod{r?O!2jG2mC64PDC&@L^rcAgVHYyU|VYPM8v@zS?1Xu4-VpJT77{If-3}M zF79g&pu70aTKnpbPk4ZVa2yW+GHRoABavzmK>umwOst9dyLp1r$)k<0wauxF`XJBg zD4yQ^j(@p=sO_tW56#n-zS)t}Gf%9FF9v7yx*D7hxNP$Sa_ap!QstB>LX(nxt+0zv zmMFn&$JU9sP(3p2G*#u?oyGBqX$HfXx}m86$QNQ%o6|y&@$7_$lA4SW=`KJ)0UstW7ic1{B^rQ0#> zt6%Q{eC-~ZVZL4cr-~HR?^Gp8AqiJGSL+J;@m8BVZ}=s4cx6c7Nh+5|*6YpG&L5bK zG>tw{^$BPm^|aBym_IlbBR9Wb%X_`Sf6RI~bHFmml3P90ORk|9S&5A57E|{>!(ZrS0u(c)<~%{ zXlR$c10?GFXIW#hU59nVF1p(XvX2{j7n4g|MBq7R*$|t5Ma9acG5mh)Nh1KzTsKeT{g>Y1 ze_rd4r|UoG@Q=Uz|HQ_BV&i|VGJjjB{(rGXy#a5O{VKjI=G6z5S#eml!%anD05zJi%58k+SQn}J-f2j5p zD0q(9@nY@lDG$c^8v=b11cKmQJI6eSf2T!klz2w!$8sI>|vT_m!^d9O#KW<gifK zJ~8LJ%*7L^n8dQ%=HgNnc^qGZexP;?gfR2Y>{eJv_~W!cxLF1{lSz)bh|_LsdA_ z1!zSr?pEC|cfpnw(^jN0Y&=Zr1?(IuWTd{$yeUpk>W zUr|`Tp}t~qNO8n}N|N}N^&rE=5LPi!(2JMd9yNu|i?+bqwmz|bUWN{;^zX2!b*)}+ z#5EpH^4}ntY10-uJrJRW5coK2BvqPE14=gUeeXb1)YHnoD#E_D$MMGg<+`f;=6v_A z+G>wbDFseXwNWwO{lJG|s8dc7Lsb|aF3B>>ow}0D8`p!FPRPlFB`*~(%9(}1>Kc|} z2YgDTgInma7>R&S}?ANe!RHb}a{1Z>H!dFnoITy@p6a87x(Aa<<9DV?)W1~fMp zlL5k;&$B@B5`}F!ktaD`DFu_ccM=iuBAdIEp^}c&{%-ADz?LW0rqY&c|1YNx$|zv0 zG!-IV#4vj;4Wl8deNVS|g#zW&eRd=F2yUsXucyVhPtV{BBVMJC02t|rxPzDcsfK~U zb(@uMVl`+|oM+!4z1_qdYq>>=Dnm?X`wCEgb8$S=g(&4?yY-#D48e63M#sM`eQPSS zV`^4kE?`-Y5#igCL;F6SvTLIymD%AmDZ`bRQ%;a?_zGf3alrxkWmGH=VQB22|9jo2 z=kq2?XoM1~AYoixWyW2?gXI6AdphHN6!OeT`pUXZd!EnKLccef^Rb{b%Qkd%x%Q5w zQ)X^TYQJXLM(+|L%iq7KxVSBN!H;kLZVvns!LP?k8@f91%q_ozte(zOMD%+6ay7lt z%H{dK{eAL5&ecP+Q``q-3I(15Vr$TcJ?bf1H~dn@9rxn8!Nj%zUm15Az5fs%vQ=KtP+KS(P~8ow{cz90$_E&N9Dm$^GRIdWNzB zDk-}q197QR{)SrU+m>Gs@2p2J_LJ9pOj(pj>pegV>FHq0UA>j{-uWn_?)Cec8ffbV z@ESb8BR5#u^@I1*;Ofp$Bj<&3Y~ILDsYZALzu&^c48Pgep_!zx4-I^G31Xp*Jb%P` znbp(%+9UDLdsfpnPxR387v-6Jgv%Y z2wiUTKhm@(!#Yf{cvKXpjuo>YcfV2-5wZ_#PRVb7rkm!Va*Y%QX4x0*5gXf_EZ@@> zh4oE%9rJ?uCSR*)e6@h=khO2k7w@m3Vqn;4f6X;s^&qv@EG9lS_3I@#NMXX(yjoH4 z)!R+2jajr|N_Cj~V+!j6rdfpd4HGWYcvg6QP`B@6NN*0MN+2smT=*VvJv$Wcv+Ak^JT(zcyr-XGG-(-@ake=5*xeuLT#1pP|C;_t{@k- zi}!6;T;$AFoNG1Q$xq%u{Yo$C4aMX5&)RI@YYT9j%*MjWK_hiOl&_N`7~0UVDIktK zf!T1+3*oGUwf_yDZXUa!pt1S9as_ip1MF2>fB*BoWtp#LXMaInwii~y<jb+O=5Ah=9aRaqaDr?@K{#DC)IAqbo@^x}?Y?c2L<%zx{w_ys~$k~>B+RWJERvs~TiZ4GT+SMG!?T6@@goZj7uIFl@Bd&TB zUZqa3*Kbc>&xb^m}v*Y&{s0UqMJxFSBG@2c^_9zZi zVr2-|BXBf&L*bWa8^5$63nMv~W_Q;h_U%}2Ue|lel%7{-OY&*NkH}A2@5LCw(A1ug zBF#P*9S}gF&0xO5jrvr|1^-xt{ZS|R&@28X6SPZwUg`sPsHEH3M=lj|XAzDwTtgHx z&{b@IRVQOiFGseCGg7t?;R~^JeqF`~dNZ^;>Gs6LM=d*}Stq4zthx_}Y~<8l6zjBO&aZr8L{MB9<8b5H{rrvqlLfr41Z&Xg^Jo1l+3Gj6@BmFs zwo(XssKhFvz-~r3)}?2)jdN;weLM*nPq%=EV5R=@L@%LveIrRl7o%|QJUNoOtOcW8 zPT&r$Nub0RI`<-=J>5G>x$kbFjThuxZ5*tu%wuH&K(Iv^qTql>nG5g2O@2eLD+qf8 z?&lM&SW_;t98}si)q0ys%6t>@4_vk%481*bQc`TjyWK@JfY$H#H!geNKnXsH{}E*5 zdXs^di?}xXi8VpQ;ias>WnK$K64df#V(gk9HFIAUUQLEQH)$eJKvSeVM8gfx>WZ1r zU?f=;ygDS9wyl%VF%jONn0l{TApZC<48$AHj9%bN2wB`sB0ml|=R4trdk#}ZPtAE^ z4>qpBDUYlzJp-Q&hGgJ--1-S024(DLe@1Z9A0wD@N!)RqMK1?F970b!Amvc15Xe^) z==y?y9l`EdI9D^!m3sm_+EGqIYZ0%(qW}bw(^Uhu%ep3*IJZ$9cB@87+CME3E8X~F5PJ@+Oh|)7Vt?!68h6T{-|F5om3lR z7_L8Qe1$V=yXwbGk|Fba?%6B_ePJE#arD3F_Rff#zsx1|ze7-1HV(0GVIT1c~2`hgJ0KGZ4GJ|aMXEV;6PJq89W5Q=y6EvLd!wZ=c^C$3qBo0 zAVm?7x2-UJ)oD_@G~m^Xt-E>UXW@F-26CSFCrR&>rFSVYd}JO!E3pV-KH2 zd>mD!!d-DQ7av#lbd<cg?ORl;e~4S!U!Jc8K)I}GV~$X)aX(GV%iY4_%x3E$%XG=QURIg)$zN~(vGA- z89kCP&<5V3YBh`N#LTAwEGo6)uCDzA@yYL4?R5aFRXO&c_zI(@xK)>Y=m?C|YWN!s zRS|;aQu#bJQ;>FA)l%D|dTbI@tu=Cuc@JnOFM5eEdRMG`nkL`no1xemi_(cuEap~L zja!-tT&PCj?&Xi)e*z1@YP!WBi~r=h>6|>=2WiHxGBmZ#OCo6lhHo0De!-H(Cxd4< z9_i8M9CxfLP%gIcg--tHZ`de@O;`s{hskLM06<_XjNb z$MnQ)f5ktAb1%2=YKVI2Pt^zk*xdY4){c%b&o`8CR3dFR*_!uZK>10K6K-{<53wWV z2D}cVZ)FRa?HeC)hpZfuoGeCT&St7qCvAoIKi5ADEF^ws|cX*^va~lU%t8XSlIIOQLSRd;5RoETNhkDa(O6}y=1=UAgdKP zcji(KNI!r4p%}6DDC0Zbz9z^s&;6pNclt)~=8JxkD8cA8GtCy}(SAJ0i9o6N4L)}t zDoc?ikBxO_9vW^LYb;=0a{Q&cu(NU=sft~CtL8ELPAfD(NsE{I5S27V$$m8-a3(W5 z$$xm`JwI!T8`tK$sdETt1x)ggUZ45VU-FROl=*E_MPcoedG%%&pUvw=Qn{7WR1QhS zT)HM8#5MHo$rsfyqGInYGvAF{VzkaneBI_Ug8Gl#l;r5|R*RGd=`nUYtLhk?yCi>D z6S~e@zW*S!`6Joj&^*+OTn>`#iW`vu)d9tO*B$4jCmZT+kMC@feQ_Pwol$oRZy3cg zmnvE_jFq?r5iY#`tgO1P)~G~c@$9YA5v}ofLlGcCe7^yRdGv9PI6*Cue{g+FZ0IiZ zNri62-RiVA10h*>0NDHdVX)VU-zSv*1Hc2@DIQwI5kOgabvEZH_9v**T*BbY`~zN2 zg(mqW3mTsG{+$)x;>#DC>KeIb!gI{V@B7ip7(0*a10@lfWtQP z``!`jdB|Evt!UUlL?q@pB&fNef_gSePHT^wYqv7Pg(Q#efod`tx*}U3h3sW&BbmO2 zp?nH!UAtR9`LaN1agR?rLGy9`=!O28Ym=Q5wWQC4x?=@95fJ1~`y;Kjfa} z-<2%M4gY2HSGo=J^>eOQ^g=eHam)z@s2h?kxYiKmx`AC9qrNn?VYeL}bT3Lnm+aIW zo$mrH$}jrylNNvpEMAq3yEOK$r@(WnZl44DAxAKl=U!|cgV?5Bs z&uYFpYj>V;Qvk;|!ily|rQ$d#V|Qc$?HgVu|O+ z!@lEVw(xfSD|W``?GeZrN^w}Nv*pX*X93`$H|m6^u#1hq#%_uTt|KJ_;4()S*QeKq z6xVD`OpW3KVIJ1N%TOgHneAYb`MdHv79-ciGEWL~1B&kt^S1kQy& zj`bRy5X(GN%NK$;uv1^aD?+Q%YIDQwy2J-VGFbzwc*5R^nzrcs-eI}+28{da&a+zG zv4c;mZXW-Xfb+jKZAZ%!j~7rn5I(y5V#x|>`C#;Tg_^tRfm zMZWgYJ?&FZwI%N?DjBU8bb3m3Dd&UcEnjD+0m95ECxi^Zl9P&ORqrhrkzm(N)2=8Q z9N4S`ov zI?PMukp%Rf@cboR;{r<3mBxw^mu{Hk+izfh^Q6RBo}G6sl}wPTpR+xyu?q*%=EjHH zZS=@;_?BrnLq_VpYT4OE{o?S=>~QnG1V|TZS=h!?(>H(qV)?F=j9KmtTb875V8-Lam&s zOi#IB(EWgcl!0%ie zC$frTx%Acj*a9oLl%$8#qGzVcaQZRa1&Y6O{Ar3N)GFKSV$XtGOBPCjOQ0&2A{Qe0 z6OLEd67|%-#OrVE+rA$;JHQ{|a8oTjcnn^7;foyso!-jrvWmX-K-w7%*TKquQr@@CQl^&UD2-qAvnlbbC`{2v}&1<#t;J{WJRDQZEf* z3%dg~NWq9L(~oz=6c0ScZ<9kaYzBVUDJb;#_DQjfoj5(z#tDc^uQb@Wm$Uqs> zpD~ejyL4lvoNz~D|H~8oU4S}!bpgoiAZx)|53cL4#2h1ve2a=(o5==JCd;!3U?SXK z`aw>q?pT|ce`5kn1>L=DoiG{^la+q5>ekLDrs&Zm3-w>!_UaR9qf5GJS7gQ>&iBz* zRnUTc(W;`p4Wcp*k5QvCW{O2a$9GTHel&-?Vs!M`D6R_jUk7}vFmo})c_)X+U;DUC zG3i1OQ!-%XWDm)&Kl#D;wyHY-=4Pmx_DwTNdAnkjQ~nt+^1M^h%`iOt1$=x3d#SgD zym9A9rfU7enbw8A0;pqZpD0qP3E4N4C`)+mS-%?X_t3$P4R|GomdgkX;SmLk+$Q6N zi<)l@@~j=-O(h%|%jr7NX)Vrn;}^mjLp}A=<==QmQI0Rs4@3bqo1~J~g1Sp&EZ7N> z>1SMQ3rXB6+k9y}X+{=`>Q#%FF@8p`&w?yQ)Ai6Zo6AhEGDnx`^%&_p1wSj}W_nUz>4KUD(;GTve^Np2~ROf#j#@FY-B{~_! z1q$6;BK_wNfZwcPk=1L5H)ZW5kQ6EYZ=ff%ECF;E7zfDA^nz2O2LyQn)?aNLS+eE5 z(g4$I*fW@mTMMb≶J%#x{XT{oO7QMg!~ueNBy47aG|kA;UIz?fCMLBXGd_tE{=j zlItJa@;NG1pZ<-O`&-a0&I54ILYL+HGcglmP-4UW#futy`v4szou-B=YNh9*J_an- zgkCklf-RXH#?vjfwRifUB9kEQ+G4Ghw!3I#^3zG)5wVdnAO`>1itwTI%;C|*%ne*f z;NHP6kbS<57tCEC2g=mrqSMp&OCuPm-~1{UX$DcZBo$To9Kk@vcZFz?``_+nKPAx2OAv{ zfrT$rMR00Y$;{zss+q$M_UOD%myv3YN%!X0dva$dty%1Oy|V*_;9~BbDcp*oEos>i z?q!jywD|N9A&J#gp?&jlDTyQ#e!-0b_AIk+dNuu>K=Hkyhqv<8dxjE$%7EXG61v^1 zx-Bbmi`D!BBBWDCkwP4m7hH;_PKHROk(;6|H4wY8F&gqb7uupeRI zRFT10EWSH>Ptpc(0rR$uJ$OtgY@tdn$C}HZv{EM|J~nX{Q>@kgH$o5MPJ*i1Fw%g= zXz#ZQ$=MyRQ4w4&3!@D(gAq3k#uNhHsEj8XlRknS%1?gM-;L$)vjH$QKgICIaYp)C{b;yOXXczr}XAwUaM$5pgt^Fo5YK|lg0%NZrv4&`w>l6 z8lc{lpp=7b<~5&%Tf}-tCK#=R3igS)g1U4;zwOO^!a{03K8 zYPnc%0oY95sWF)Y1-PHJ+e=v`x&IE;O71qEWU4zPvyvGAi20MbG`a1GYj&sEo`Bs3 zh}AI@to9c~0?(v+@9P#$cwbXBRt&n@9HBm6s}GTRG78k8{yLy4X%IdxBtq+P1iBS{s2UpfN(f7>I{? z#RFPk;sFgvL&Ktg`1D98G>s=ko|3y!prKv`rvumUdXAU-=(?~r-HpS#r441@oG+E% zyTS;D&$T$ua^0G}sxBTaDwyEX^X?U6HG0|F-Dh5)%&2%mzN&Nnc(GP~z{<2_OYV+P zKZHnAeTQ~u9BLx-iar>mR@l<=qB+g_xIZkw2RhW_KcOERdR(u%L$(~T;c2Vw_> zLqQa_<{vy092KBZfpckXAP&xMaAO()!*DD!92gvNUXE+V{e21{Wx6QQ2DZlPcVZi< z@>+Qs|7Ph51b$N2I zg-d9hLn0v45mqxal7{axkh*~E!j_+`<7vyc&O3TxZ<{#qHeM#st(~_3){A)J8et*0 zeDu=h(cr~sJ$l{%*y1Z-~8K9kp^e;}l{xarBT{PL@#wXktfKzBQz z?~+H^w-|%Ql@}W0<6sZ==q!DU=2LpP3bDoiZsd5Y6})p5|s3np+UZq1TO_7HfV8@sRMAyKQcm)(NAYaWgp z$;l@c=8r9mFE&1KGTt77F^J7lto)l@gBV*%shn6y$-HZDDDV`qjrA2O^F)nLLh^ff zq*>bigPaaS$rFx{s7}Bx)Dic;(@}}t-@w%af`x_5-1yxgBr*YQo`p|t zh5IXqqe~({-&+R;fm?Tvv!3e~Xuw*`L0&IU6{}xJ?7r|k$*&gA_Wl{@qdS1)V9S&C z(>Bx!G~%qgQJTe2%P5C|&6pCPz5?PqopVOYGBPTZ1(E2%hjwQHHnyKREijZ}PDPY; zD!~Xyv6Dq9n4ZxPc_Ww%!h&$-M;;>Uf{c2VMKXN#*`HFN5B>Jr*99yUk!C2c9CiB3 z7z4-FPgX8J_F?Q^ddOG}+aOVXkQ)4lXpPOufuYc~kKE_X)vE7!ZhksYko|EkQozgs za-yW8EdO}-trT=)g>DvGFL+gq0ZLKc{tFP&hidwF-Ky1v$_dC4XX}K_C z^l={iZ&q<6C+s~}XT;eKZA7YaEVoD4Z_bY*S7-Lw4l>6s+P(Ei1#>Q9N;%MmvMZCq z#eXFqqx~wQPn<#cGT-;5h*Vj9L!x7X5b>AxkerXXiXRM_HXIA2*(J^#52bQ=*U{O- z+0l9a)YaBA)fge-HK=d@-}yv>O5SscW(6`*@5#Kbo4?8XFrCM(0*VCidum)+v+v60 z>k4-fW&q#}u^v}JYdO}>XTNuj^2-`++4M`K6>rmCYnpJ)K>$pB{CWC8mAiq*ZP z&*>11gCpU(MtykymM=*8fAs~)L0makPAPe15`-PsM;gQ(4~@;xw&r;pesJ;EuUp~Q z8NlFV&^c1MAgv7Hl~)%r8(2n!-PF3l4XUBx`0}CJ# z8Zb9*Qqlf(3kKunH4Pj38FzL5;I%!f3(D_Dg0D=EX4ZC(`Uik0n8QAZNGpN}YpCvr zzShb<7v3>h(VrRaw7IMrEk~0x8m6;eE`3Ji4@BqREw6x}#jWkHaJyybaEVw=PIj)k z)3Ax}8`Hso-JksD2YRIq4J55&H@~yJsvAWD!|cLmK!31E^e&YJV7Ok*jU-Uw-3s@8 zuOkx=%+L1wMW0l}i*cW*<9y+p+#2IS6Sk4;AbZ7YZ<4u#Im0*WbzNsec~G(HUl*}! z3H#zKrBw}Zw)_{!Kg}miE(DEDo$K$V!xS~;t3$q?Ph&ej1rHRt#DX%@N5h&wdEBwX z7I_+uG@`|-*czeb@{C}_Nm&9?baVU|HM(1xK>5uLv*cq5Hu77jn^oWAz)26fl3BdD z9EC`}ZsZlGrNC(V)Nb7k>x!J4NX{!@Vc2d{0*fJ6v%+dNSa`SZ&Dv%ibw#He0HY1_ z=I~?v`&M?rV}En5Tx+Cv7|xcq?i#4YWOXUpT+!Gvt|0l}yum!FX}VGOQD*_jvVkGw zzZv1A=;PhmFuIj1`?aJCocT9{3K(s$PSMUvj@)6^VQ`TQ(|{@jw9Z+ZK9I~j)WK^V z)h*3%)LH;qYM&JGwC^Z_y5ycNm{TGTIe_Z#dp#n`Ln81o+TR@1{VzIEr0@SzC(5D6 z2X~AEjkV9eWRar)flB4VI|JQKs3sI+z4Y0l_ru~`ZcET;gIg%OENqbNeYI8DA6=Wf z_@GK(chRFBHY4u>dO8~UN>O8Psmnw=cm*zl{RNsbFn&HPq)TokgH;*>zuFD>%S|@f zvZ|bc^_si_Vc{)BV9}UFRgp)`)hRKQ&msUdN=myAnqA9s&hpI^o^yd$0N$1sNzb&@ zKb_8YS!LIVPGjHnb)(7>EpdFkW!o*e1_z>|*tz)<=f&;y!vjPFZLz+p73rWOWWl!6ot=G9SIzg}qe~rEIWveV!q{nK!KISPI06>SUrMzm#Mr}HxuTDx1w<%_Ik8nA{Mv`0+HoPi>XHWpyyP3e%y@% zUIrX>mU(p*B!aM%MI`%B0kCjAfOULrfBH>d;d`PQ_1}~!&Ks5<^nEbnj{$D_U%?Yj z12&QfoUvsc@WdGy&@{eEEA`h^;cg@AR6N&Rcse~T*L~1;v@&o{!9{hnSI6?s{M;Mv zQGd6<{UM3G^<&3wVS7Tt6Ve(jSaz9ybl3x;XhHiD)1Ts-cV?pL8Hi46_Q4&p_H29b z&>esXPs{9R(r2$uI7z>VOdlBM^-?3pUkir>pFf@Ux5Iw8bp9fc@Us>s_-^=CX{uttFsOk2aYhThlC_Ehr%vkd08)!%(t)uyo!lym&%jbs2!{t(Z1 z&m)qW1!(oX;n4J~0~%{N__w&u|N2lUk&7XB7VlL}y{v8q(YF7Ye7n_~cJ|or)Y&yC zeRG3GD)EUuD1--(__rP$exG;QIr0c>xmRI@D$~$2(^J8cxum>hI_csK`l|iXezCao za5*$k*nNEau)y!H;EeoGvJdk>`(C}D_MDvXpEkmQ2f_zWTt-H{yu1FVL+srU0xN(2 z>yCkhZw(FZ*+&_l;~@wokP-Kw{e-mrf8m9>;=S(_4Y23<*PWC4Gg#!GMaC7-?wAfI z$H}Mv?7=_1@5-eudyc--%d)?COaJ)n$-96(r@<%jL*Y7*dea9ZB-YGOiRSueNE)1dut6VFwrf!w~ zg_dq(W+g8kpME@dTJ`MVqkHx4eM*1APS5(Pr~yU*T41j zhi8vGN*If$yJZ*u8*lzUA75AWmUuy)VC1JSR$13Ek(B{A$Lm55VU~y1n{FLAw(sBk zPL5ulO`7X&EKsVP&zN0T4DNXG#y;guVTHMcy6b2KHjzDO0ChHM3%?~gfHr<WbyRz zl%V8^wS!*NEXgqp;wICyNJ2R>Y5azE(n`pi;C$+v6MiJs{N6JuFasJA@*=>eOYO(F z^Q>&i^^4ETNfL6+wMemsx=(k@c-;a68`KiGqCIWI45%^Z{aOmo%%B@nZyL9tmNS(+ zVr4D`T-mnkU%oxs_c_l#aAm05;n>_0*E0cR^Q4;r4`zy}AEp-$CD%K{1&Ys=cry&w z$bxdC=X^mp-zIIhE3b!CN}ei%RhV`Gizpu+`>3NNc~pSLXM-6z|f*7%gCy)-e>O^99whF^lHTdO} zn&{hYm%sN(yQZuppXilS3el@oR(-c!a)<%Ka)UWhB4?%VCso{+d`{-PPZ{yALXRuK zU+n*XoPA|bTW!1UTcH9i6ffRVJV>FqR*)jW6I@zc0>z!wDDDKe;_d_plH%?fw8cGm zfDkx&zy0mK&&)Z0zV}ClnapB_wRqO`Tyo#fjY|IWqFYnH|K1S4lg?b0+q>Bc>8w<% zaYmEpGq?WpC;ZnfHq%mbk=p>il}s)Y%Gp0@GuGD(yc# zL@$vgh1-pMR{)_#hUz&1vGY9RHfJLKrLD^Q^uPK{n$zr^*C9?6M?Xv^dI#aAo$i<8 z7UX_ZsQK8@Ut&6#OaK$DY;NcM&Ao1_k@IPtyV+0CJ4z5Pr~H<|_l9NrX2os@xnCe6 zDb~gT<8*=49E0WxXV%t0sDK9LABX8Ol;WH)l~!>W#YQ3r7gz5tD$Z68Mz}U(HO%Km zvCCrER%W-MYNSS_5Sg6^Y~I<3Q*J~M`jDx`T}u17HQX)aYb2Rkm=w06ZcyqS%9B#f zJHP)=wh`JdK~UAb*NK^%ibk%=CL-HoTPeO%-L!T}nhq?)hVBM3i2dPr{Twrj^cpSO zZg1%nT2;>^PR{^-y_HAi8}XVL?QZ9>t*O!Zp3&D!&r3Ilrsd-_Dq_b!a_;SmC}<=w z0{wbjLq8eauYG3Js>%$vN+|8lh}}}oM(5<`j|kZZ1G!s-95f?&TbeWcXT|3&K3mnQ zS}GQWT8OE7cX zQEoG)cEae_=XOX5Zj%ikrP0&hEDK-^Tl<1O=8CIF@~m+d7M&GbRS^8=Yq22dZb{tk zz4Vy~0Us|Vs|=hOC#mE`$rm zjVOX$E_XdHD1zQJ)M~rlo>~?4+5ql_M~^naMESAu)SY34HO_q?+SiO_($XVWDaTkd zg3Jji9#44}78E*N#vg||tDe&}x|fz-hQ4ge`kvqKUn+^Kx6!TBu2XQyo~u-I=^LSV zLc+kTPhES>Zn3HKZ7jkqnE+AYw%I*(zuf0UROB-L>%vK{Wt!u${_x`xS=;tqrc3ij z3z+Tq+vMu-*kk;pyR0Ejk3-?vUtU}`91g)koDs$f&IMev*%sl?{sAd?%=$a&THvow ze|Y@nXHus3^Tf!fM`sMA`rQi$r93jo_(P4Njj!jvIM6<9@|;#lzEX`?VH1S$MB&cM zZtXfxYPDTnHiKB8%;nrqcXZ=o5BkJXwyrD5S)OXOEK9DTv*>)rDKR9@D;=6mv9Srx z%^fkRF~K)OKXsTAcp7qgg>IKY#4db#S>f(Q8Z3~an>456{W%?XT4PGFg1cA3QDAUHUhJq;S=s-2orN$+i4g3N zZ!~%t?EQ05+KCcCwWgAT`5o_Cmsi38x$Gat2Z_K7XUfKrG%?=DANRPgf(5d!{4j5L zaVuwA+D#%^zh~Ukl}#x96IVS@DWmP>1@(X;dp*j%L&`9Q|9A)b>kF;&FWufUc7snl zQcdAR_VY3cqu1X83uDO9kS^?>}D(=;#^HeRkFXzYg?OV%<*&DUw zn{M`Zm?{{yZBt!XRuxaZD&JA$1nxxF*DTadR-Y$G4X?}~ATN|gDPB}>I0$pVtPd3T z@GzpFzHnKa=8A8z&dGO7R05!m4As*SPF!j2ZiuLb$I8vFVuuQ8?@N}NuQz=+K1n!4 z@&YzI-QrgkB99FRL)wG;r*e^21gc}Io^B24ashwB7zcefbSiyNeX{cnXSC~+R-FTHKT7=)7=s(V zux(TFRbp#Sdna+`Ikn+#7r&(BzMp+(R7Lz0Gi@^ltxDq_^Erp1r?`1F^}6S&D&C@w znBdyMU(+a}P~F7|9BFGCyP2?RW?s+66Gv4kA-3f_9mT-UW2XCAReere4xc2B+J;<* zkqQVxcg_8b@>CT(ait5}-VF_CiZs?$5@EvpG#mW1b1Bv0$!<6@y5KlFi-oNIzD(WQ zZy-)8oJfVruogEAW$kj3srhL8|1+-p$P z8XqQZL*F7t-P=1Pmc^4{Z}CqpV>VxI!?3KE1yw5kkhO^b_tr!79`ud?xKOZXp7H zsQZ+rno7p{IXx_vt@WDpRZCJ$i`_#|8ZRJ-ADK8R0^)DyF6zw7ymx2Nt2HHBaJlr< z*NiAek$0J^cYns}C~^=c%n^mYNRqulM;i0_C`(dzI2XxzuVSoq(NEpK%`GHYE`E30 zZ?W;2|Nfuv4S0e1`t$3wU;Yh^5Pgvx(gathVZKZEJI#k9^d2`^2A5+M$1lpH-Ka~* z-cTUoY8$;DZ;uovSd4buSZejQe4F@b#+Bv;A-Ozj2G#g}&Q?~ZeZoN`>IMSf~YUu`VZ+x6+AnQvxiYo}gPp;ypQiY|l^4GnNwN7Ro0lRs= zH)xkBH>s`PG6`d1A}7o^ks-^@7Ru0O_vD&xo~Qpgo59PSpf}HIViul)zZaM4Wn#}! zop=!M8LWBm~5?7`ZIbS$EcIEt4f$5~Gc!fyT$=S4tukw!W7dx$e zUap1aC5@t3Pn-fgTL)~c6gb0Y&vQsk2jfJSWL;=YOZYH&3xWdEO6x5sZTj|Y3@qg5 zQ~O$~SlIt$p3p2#fhuBM!c#7H=)#yRkx~G_RH?~ac#+{Gl>_fj2;G_bD=@J(vyzRH zzQDL8gto|NL*yFAZZ_{H^~nh1Q2^?u$oQI;nsAaO_%v=#?K*Bs)@aLpbFstbI6wrG zOXNSE054kt4mPNYTPeyM%Ag#Ci=hV;8>^WdXIp)KT+2`P@?fnFMA6eRn;LT)u2T2A@iCq`#>ek^V0I>of#I~CgeE3URjGu>SK!_b{Z!40 z*zaDUSA-z0>a`Nz{7~ICyCt6R+3GPc18_?%GP|U{&`dK!1}cxR+vSx18=tAM>p{Oq)34MbrwzveK;O~6&`7bD-c z9HO(q-7R;4PD5Tjd!w*^EChGGWUHlf!S0jGM0IF=oMnU&8~o?&2ERt-p2gwkjzodW zYDIciV>c2I4zEFZc*&(5byn481OXP?s%_o6T(~e9JBggGchWZX`S5U%K0#sUwvy-e zl^tgIBWn$eM|!~DB0_c#6+?L6KN~Ro`&V0 z9iP{6M(7jAnf$Os4FmXM5w&(2X1wE+4iHknEJ5xqgHF5lQ}G`wOdlaFJUeG2Lr*U5AGi8< z-c8P(%mxp5-L%}X53Ue~7}tfusrE`mNi!g0^}MzFac>jm#XjT3%YGMhbu3s=WV>%# zD(D~GgEr}x8~_TW(HsqY%o>FfgQcfBr|;N;x#<_hj>gqPyoD%ek{|!)GWf~;FY5ar zS9+Kf-()I1T&?t3RCHXc_Y$_5{9Ja^|I&l$A7N~P((PoX$)W)&uP?8cA2i)0Fj)^1 z4|ndSr&%oAu$8&4KKjr9QuWvezl`jGm$jqdfAUKGb?e3LPtC?7U6EMk}QQS31+TWdj zk5f#2ou2sfYp=TCr-f^+Hnv3-pjcbUC)mS6Z1F7r{G_dF5r%YdY!|pR%zFPo85F4k;5*q z`@p?BUh{G(1yFq5tq}3=8&wau8v*AJc${n={!YKv{bbTNF!WED5c?iy(9ddBNUV#|3$m-Hm4EZxX2Z1hgf@;; z91C#TeNtAk(H{<|wp1i-zW!~m|I#np#eUOf%N4isZsC|rw5PAYK!j@iGL_*3uUBhC zM;rt9mXtzCMi{cCvt#_O*_9JF)Y`qfN5;o_GxckIK>&{p6UU-m?K|9E?=5INQ8aAW$c@4 zcVPzT+)0LJzE7}i`Saj;rnpQs_mgK2AF#k)3!pRd3f;E$MdUS#2PpEx%jaK(7KFfd zBYsG)2B4@LhP$ZI$%?O##`v!4$J>nWBGp6#Y4hZbDHDddbCC0*lTv(NDu$U-4!>~q znA1CVsa0(*&tb(By^O!@ORTtXt;*eE7LgQ)i;#M;@X&F|@U6!|LnceMRMEi8Y3qc2 zp!ErC!KNqC)pqpx5pl`b#=zWLM=61fb+x%n;)c2I?hM7jSwUCKw=ZK-*b98sm`Glg zV)hY47fFmekGR%ODW{2+(-Iz;ZA#k~gg;Zyy-Rk%{ZQh^PlBW)8FOQ0xT`WgbZ&rGw8wZuEK%P8Ah21@Y>+82 zjI%91`|{hRo9*bUC8O8f2jJdnWcvC5tS51Mly4|6j?Hb7(|gau#b(>eZr<JK4(}l1a#YRe6|WyQOr<}k_cP=qJh?R)R%fF3wtlcj zdK3$|7)m$yxENO6T}ji`{#26pT0ChQvFB4!v@$wnGB+T_Ni{PiiGSxtP1ui% zWax2j%vjGk^ya%R*Gwjyi3!ssP7-tcyTN2Eq|?B!yVB)^G)TFZH}?Yr;$U4mZcMVO z%J`3>jdp@bGNy2W3uW9(p6I6yI0+ z^r#l-Uj4;eb|cR*(`jzzQz340AMO?#JT0L&TGM?<>M{35n88OR_rw8c$lB;o@}0W4 zJcguw;YUr57Vp+uzD2QbE$3?R6i{ILpGy&qyK{8v6-3x8MfB|VK1>m@yaBREgvO`I>%^v_x&Y&inh};bpS%Td|omOQ9jd>uge3Y zSwZ)n(0q9<#Qen9L3vW&;^21hAK4eYJ8%12mSiKZ%(EOx6ZabPwi4TFWZt7=!1B*q zIQbH(#Qw}j#EoVLlY?)FtuS7FZ_De zEV>|S(5J#UA-fa_Px2Y_dS+PtAQO4!?|3rJ7%Ce;tN=~xkzPELKsY4ISC+NMH{DN5 zCuWr(SmjuZzqv<~HOd~jXDHvQ8Y47T!Vu7tH^w3GaKz2jMZ|bN3*u0p^{Z=AVk^UA z;M0xeT`(ay4vt9O87a0YC@Rr=8lhM)5c8z>Oa%CLGL`v5WK}43>&8f;GhsRe6<`6ri)A*we>d@$W4cU(A%O$|hhyb5y665< z-zpb4KHkSfvD?3r^39}IEoCUro5`|@;S6_4FI(mdWoAf7zQc$9>e7@x8SXp$+4F^t zYbB9VufB;}3TdDukg)6B!N0@BO1bKc*K)RST>N0QmkQ~BFUviFUp^R^ot|OkHZzs?FtRYxKWGdk_r`*Z zUJI4;F#fs2lek9`B3cBM1YfvVqc*LAx0*s#W<5+pV-c?V8w+*bo{y2QU{ykmp552p zdJn$+idKCMb?CNL9{y0e6(fPgs-jx#k|Q`(uy-{2h?H!rFNd%v7sx}IV%A0dv(G<6 zdqr7hsDF4VO!q6Tista}iC!b2>8o5b5Gx%;@|(1GfDL>1;+Lyo#1qjlGLn434M!>u zq*!0RDf?-m{RinTiNIr>cc{SFzb@?}rSTuRthd7G)>Z^P5Ldf3uN6fg74C8T-)8}w z*-`zy3eo?y3P=8Imlqo_-IU7=;B7vSRT66G+B?6`8{^LvWNtb3#(ywa%fB{EOv>59EzctmD@k zc;?|R+{JtC&gv!E`!(T~Ph7v{_h}jw>L`GFtWnPcY^#%Q!VU9JuXO0!og?cn>_(n_ zRh#$UnS8gsV0Xspyw;>iq|$kq5ZtDPa_ESJ-+&UVuYdiw%y$-(Sj z5b7>3?Tot4QBX3)lZ|2N zmPq%*^XD8N`l1De%@WL|mA~#0R%dK3DQug*^4D?`p}{E8#-7d0%M=;I4QT3&o$E0C zXYKja)UlPSF@f)}*U}O}{J=;_C<_AIc*IO;)yHrn$o}TcWaMgYen= z2DoMpsm#0h4Iey7VU-egR&X0wrco&>avflC)nqK!&>xa|=*NbXk&)h_$0YH_SGkU2 z5`62TrJ*(r{<9D7cWVpVd`;RW`u)A`M7c8keyeNnOyov`>9GSFr(0i)Nca** zlSdzH<$d#>HU+Ca#`(}dgi>Z#_mecNGwK5`&m z#&eOnhIRYlY6LX3W4~5H9JJLz*lP#gQ=e4Y2G8RU!yF3=n(TPp>orP{@TGmcu@TC%;wN|V|dWLwv)rlSGqBU z(%NkT#_z{l#^Xe&pEwWaYks&vJ@twi=H+1AwcKx4G9n0t| z&IYrN{J0bbuQzCsrpF7IGn?(W#v{Z<*skKf z6a(DlYFImuv{{%nGRLMA`A(dr7W<6ANFHa6E#EmgGb6IT+5t=ya-rbp)OG)+m`4e! zOWLd#+ewe48yJlI(XZV(S4!P=HY~>WIwh>ea!A0%?^u}p%9=c$q`CQ#)Hy7lohH#C zvl|zm{$V6%%|&-@G;FavBiBXroa)0qYKe^7DNNS+EUlj?w}4-HjlECE0UivcO$N5y2xjuh?&yk?bt#E_x=LWGK_m zsl4G!JXEV}7`YOFP9=iZto7n*BTJ{YrZRQp*bP=_u*Cvo4SO9S-(*_sK397d4frzf z1(6Lu;hPu!1`}o`ny~K$yFv_9ywZ#PLh!P^e&NPKAImKw^_y? zonl)xC}rjBI42M#pA`4v5Qt_{G;_&N5+6q>(vm-vu0!InEos^Z_ebpH{JV>8>#L8f zRg8T04ugGf>4as|U*(~bh0O?U0&SopFqqu8m@Xx?jT>`N|;NdJ&VNAlND$XU1? zzdZiO!XuoUEKRXEW*SfYw*A%TUNj?x(~$A*b7AC^eB(Tqb|=$$OT=rm?dQooHPGVy z6}$1Xj%_>pzn!V9r{c>oy!Pt0e15%4M6OZ^x~ElQQ_p`JnH$}HB!ZeCvLRD`ePR|S z7iRgd>ZCM|=a8`)0XmUzykCWa;8G)NTOUgFzUOwr-*&j2P!TyYED)BKj7^PL%NgIH z9;ib{fn7K3Ui9G$n3`(hAJe}&GP*4i)FPnA=Iu%#p*Wq+9uP5Y7|~bh$x^DpV1lax zRLEOu)p3KrYcLlRx#d0;TMQ}vX@pJ87iI58JU$$=H?A`v78{e}{w^oS#^vr1Ug8PI z0YS!9KsB|Wc8p?E7X^GuQq)U}YBENwe}?pNtnFW6AiGY75k<&W-3<+;R;y}XY;ec7 zWFfu=#qvK7!;T%c_qblYZ=wKys>keU`E`e!w;QNe-FA>Xjz5>-y||1C)fRgh&_9wPhsU6pw_EUqVNB+A)^05 zeJ!|8_o&1=X&;!W1g-G$%PD_wEi9HSG!(LJi@l1L{03MzYY2<=#QO;~a5A_<43hKC z0;uHbUe;aSc=o{bWYuh5^fvh7F9T+SeyY21u7o8$!v=;Yi!K#Le{6kk3wDKG{><|(!w=AJ8C20j{L1k{=$6I;N>J~ zsD0Oc;gzUN<^@Z5A|PIPt-SH9g&8u%E*3M&*oj)F1=&ox3-X!AcnGk*lJ3`fV^wT0 zD!$QW>^e@~zPW6I`FZOq!YF+v=ixdl5!7|Gd-3wn{5vH5Z!8)kPT5Gck)b8w8^0c0 zZ!m42#m_(9wF-VoB7q*0)3OC2VPTS7IyQ^fQ zZ-kghH`+}=&GnQi!map&glf}PA^B!1og{%o@4-{!AU7O&cO(Aa&OJ5%gPsH+j7i;U zhV=icp|`2t!o9Gu-`7e(DC)71`8sS=vig!b6-#16YgdVflJSJKLo6(0Ci+TDQ@ z7g6ly7%eL5E(BfxIHbi|hv;f!uGrOGUiGthz`q5GWf!?Q^rtHO^Z@K!9H3mpt< zMmn-t-rWBfSKIyeEoHL^<3n66Qi!sS{i)8`X^Zk1$uzbQ0)>b}c_1}3>`LNVZ=m6a zLS55~M&AzczY@UZeO(}WiLd+;29CoXpUroBQ?TFEze>9}W^`S7)qcvQtylko?tv!r zk{%HlOD$jQGQ((emswf%@f-^q`jt>_$nbS#9Isiip^|2b7ghK~W#yHC`{5pLvaFgi z)My91Yl?9#j93e{cZU|5i!|C8Qn~^2)lB<38{}X8Y4O2I&sGhl%utiVVpP>$c*Sop zYt8S^Uv#}6;}Lz|Y&$L8CB;T4W~TS%;32$P8l&p!0|jA+D)6J5fqtz-T|jtNe}3K2qgLt?lS%LR4;HV!R`4%WrjbC%c^2Xc%w{ zMyEHoq+Om+cEZx_CKBo0YZ{!&Iiu4s{l?OE z?hM{t;3?R{+K+4v7e$dEpOg~ z&&2obp(rvmhT31T*fRzkJb0{-3t1Y}N{%xt7E~7Dim-?5GIQ774m z+szZexJHXd&j5u(mkk)hnBY@Rv|QY-eP1O_Bg?Vg$D_uA`rdOt`z}Y}6=R!No4=V; z=a8k(l^#tCf7^E*0NgoD`ulq(<*5j1k_d@c?X^FiBirkt=SyZP88X5vcT${!T7D^P z`_1*|_vD4y8~3kx11|(<_B3EGo8>{`wR_#*#lOosYj?u7kP{_d`9ej?dozSEvPw3Q zZT56#T*>h9Ryp!&O2bs$$$qS$<`PVC?xqMK^=VCGWGDTQvhj7I{bXTaU5tGV54|y6 z@+w#n{FjgIQmp){GFaOrcAG-Ww5tO*%HlNBbfXsA(Md|H*FhExz2>{+M@lDfmxC}Cva~!q>ykrb#CTbSS2|kRYqMkd_DiZh!4a%%IN9s5pk4nsjhZE{@d# znYeKn%QJ>C?2H$X>u_F=omTxhXDo>-m6szzfpWG57JF%T2M+AI_)a@to%v1YpQFj4rSI!!(Zn5{UD*^Xk#0XBq-nC4%{`7BN^wh-1LkY-a z;={uH)BJQg&(BxJSfyZx(%kxo+lg?ZzrysmlCFt$OK|t*wuI+P#I*-PUH-QWC_<=idwt8I?k$t}N1K1b7g zpMKGs_^dO9gRIL9_3;I#2#r(^O_jY1@i82N?anG7&8ThuT}{goNKdK*0{aFZaFfXEH%% zYBk85A{+lO6Is18ZgbP`F$VuI{bKp64!2M61CNHX?J8DkQke0*2bcE)2x6zk=N+dd zoK`ai-F~yJzh*AC8%LaeP!3gsQ^${#JiLajZl|_PvU{(chpRmvSCjbQG6ge`6qx^El2)# z4d|1NbX|6B3`~i9o-H(GF!AlSt+;fBmnG`#`J4C;XP}#$W$*+Nb(U>F#9Wr7O;cE6 zt=80c^Zi&{BRNm@uvIHG^)MA8_Ftx#HWmo~UnHwj^kHck#J$+Em#h25ZRYRvPCY%7 zBshu_wZ=0en#W7>RnOJ#^T!3VDQd6ip3xh;)1~E?*&$^g+XXBk z_=>)6^ctP}^JIMEYf*M@W73|BYM;QK0tlx97sjOwy0reN9jul{4U>Ifuw0bmzSum( z)4~BG_1OTPN4kg0@c{ME4%EQL0Nhm=PMhjPKoau%|KMAyl@} zoG$&bC8|MY?4JQOJ}4Ja$t~Rd0p?c0cfJJO4(o`uiW-pUe8GlE-D`&Q{esh{V7pJG zYfDoG9dl&rCYMo9@PjS(lqP(umLc zR>FBM7b!tK#HCC^dn>HC91-x*$M4enh6}&>8CEM^1nPQe#!>3nC{=BzZc*WCrz0OU z_O(=IB6)oRej+1YK0IF3O~dZ|=A+A#Y89-4=3u7tj;HZBuRGsH|L)%&mnz=lvWtAn z5;t%?TNAm|Z;WzHx&hj!k&L4^z0=92Hk3LfUYCF2;q>_vuY@eTcieg%E%)yN_Hp(H zT{st-y$x!>0`TntGxe)5L=$f2c1H0VJ%-ah^v0&f{HJ2@V|?%$4T$4-jf}XYKB~{ z%A>qcUG`$Ghx+)V7pQzd<-mon-?Hl%^9cl@s|Yxu4R?#zl(TYpl#l*}g9AoHMtIZpp~L^%K6#J&|(*Vm=WmE9njwP z2+2PY+5?s?oQSZ1Y;ahdqUR)JJQ`z4r$OgYJLotp%eA|Ftb#6KO z*~&%OD?7GBGUSdaW{wKlnS~pB-|*}|=h7;PaYoqe=b6E8vhDNnd26hdbmw`pP%EHn}&pMEr?jea(xP8Iy)L zt+iExV3C84w1AAhh|X7eQBCa;B-&Cc>3Y4p?^G`9;PJT4sn1!O`d5_eOL3CL+sgRs>(i-RGOl0*OE8R5;v^%Gy+C) z8TA16(Ks=>YsH4tiFz)nwAo!|sydASI@dzF*eY+Bsdw7a*HoO?rUT7JGpnoK$D>w7 zZSSVWS}diRi67Yuj=A&EYYpFbP*3!_n?lb~~S)WJu z2db|jfXiuJ@q4bU5-Ea3l77X87lmeWDa+NCSx5W}$3Re2)f{>cqWO=b*)Q{LbG!DXVnR` z6tC1jLd%Ot`+c=b;?v!qdc25(h^yKUJGe^N?OmsrriOk^>sc;bye*6xPT%pu4+Ej& zZ`gPVj%d0;4K*fSNw;pee_~YjZ~_n|i#SFYx(K6_^UOj_CE8Bzs>Y@=AIe*a(sM6jQ89Vs$%$wHC7(e{ntVWCylt*|xNpIZqw^uFC zjLSVLvY%^GltN$!HTDhB8zmGxE@kgjZP3a^hWPnV_uPxxJ@fC-@vAe-lL#NBdK#P& z=a}CDh#F&N)N6OHsH)7yyOj}pjhNB=VV>tsOSEwbo`)_ak*JdztTT)lfr(Z)Az<-6wa$zVU&jjSUKi>*zjcK%+V0*=BX+4^(l4$39wbFxE^=BlO$0uOvQS zOEQ+&yZtC<$jpDLdA?|eish7YOyG^g8ZLm1_Mf5;QRAXY7*VYpRy6zrwli`??v87+3*!!m|`3O6t!Paq2ci8D^iU#6psd1*qy=C zv}{&$jkX3d6u^o7gjb99m4|_75pfL(Yrx>z>yj43wL*CBGSzS*^X~cUT19x?aEbZp zb2eNGM23rqxFaS=pJ(YirSm0;%Qs<2hXSpSxehfB3#b&pn+vZaqoxd!M@G_`x4 ziImiU8=BXwguN{NtxH~`%6Q0H-0aXYQG^xJvgqQ$SSea=Ef3hSn&aJT%rR}?W&fA zQCcptYE{ScVl4cJi1o#aM=E`JiL6dwW5xYmH^dTWI;Tn}XR}L8J1x}O+VRp%R^qC= zKLG)%;ct-GP3FU;8Ae!WTa&|-@5l46xhQQmAb zIbqg2C<*l}i&}D!6I&!^ad3NzcHQ3di_{f1BhU2!x^xGBlmk%}hE&y&<(62CcNa@q zM)U)+%`vRQq5h;es;uSCYB}g0&a$nt%8`4syx$nIS>BEm;qsm=MS@RbN_%}O(p3W@ zSFiPJTg|U$EmI5n`0-Lt9$ISh3NPRUxl%&G{E$nLlj#s+yhG4UUFk4v7wP%K{i*@9 zu=ur?z?Jp!p#{giC|5`Ime?6Ft_S#@Ad4e~i=S3j=d`@>NY@<`XH7jhby5QhS>rMb z6R>`-90T|S^d&~V`58D)`rA&SG~4?o3;Sr^_`3^_ONJW>2gFG zzW^TzWJsk3z0*@)ef4clq@Mg(|^z)PoGY8O@~#i7>}QA1|%-Zc+Xb+ zS#PqIPhcMGi|{uL7!8ztA9o}X7E@#Scr#8}yx0ie$YhvN6+d{)UO+)^>H8b}>scnC z@qJZ5tcW@=n#2U5MJ*Ge&Q&X#Eg(t^@kq$0XWl>Tn$2M>bs#M8q{7ukP!@fL_D zHKm}Wm6x{^KvM1YK>QtFJfWKs9`>GGH$_ip#q4aeOiTTkqz7xOd zBD7{anO~G^v7J_Zgh+bogk3~p7`egSxMd<;8U@80EoMfR ziR_#7xCteuGszo|o&a%CH{g*;Ld^=&tBt>*hNLGKq8ed4+j_ z#P~EBx+D;C-IMbQJHt*5iyduxX7ELJW@FJQCmA|5CcgNAzv~iXGp_l{2}?=;ZTILS zY1<>0Moh2`pt@`Q$)OV|-Q4jy1zS8X`8Qr9Pzr}#)8gD_>~fntpPHl$9&<))mQx1z zz{O7vuW3nxC$GENQuidd5rq9Md^t8u3a(PV?3FGPPYwh_-U@CRrtgxxC>(sj=gPHz zOPIQxrPRW2iO}1vsR=^8_YPS?#;dyr?0_Y(75K(!MTADo9!0;2mA6SR9$D;$=TZ5Z zap?Wz+_tdc<*-=R;9T0`Ym5lK;?hscFIS1Z?5NVs+qTiK`~AD%gT7RESku+Vk6`En zKsN{JT1(dE{Ra0YebwM1H7$5dCiXdXKws%>5s-3eT?AxvziP69-#+@Iax3HIRe;5+ z?u{lpPu+@%$(ti~?rU+}?P-O@lFml9FEt?PFuTY&zVK6&9&(U!;IO(jGOP=~yf zO;4~nNXgyyG+$gV5*90KF4WKAevuP;`Kf?uC$GUgDiXMj8DE+C0ok$i-U|W=8l_*a zY0uW%=KJ?NMlX5%YG(Qt&zdmIwWB%>EFf?Nu+M&AKOf3~zV8Wamo=PjMa=DDM&`Q; zPXwMDR#+yiuo*@W=qLxLJa(GKN*mN64huhU(VQ|(rnf{O;_Qd_V@^qy5LjW6H_uG* zV-it4EO8D-<&wn_!fsj?GY;)jgFn8sR7uaU_d8}O+i2_QM{E_pZkHVXAMhCJJV}Cu zNq5vhtM?QF+Vkd(%G}g65+`BTGXCDt*)@fpsCYAR_voXTQ*I5&D|`-zsiZ-oB7A^z z|3fueJEi9CI3T`W;LVjMoG6!HajGUjsBho4N_Kn@qOyf+H@GWOy!ZT9WQ5ugEsZ2P8u?ctd1Nkv8kc?OrX<8 z)k1qq_8CONC6M&fqQPFz`1g7r@L@aPjcu6?NGyJOs+7a7l5=;)q}F7KZTXr;w6Fa4 z8D>c(TXEFq;%r0p2yYctkwx>et#k;wqsx@Xg&*5M82Q$Jdp-606WI|gRQEH#l>2X+ zcyUgXg#7;^7hG7LuN&Wn_uMoH&L6?y-nR@G#x2gM?a~&CJf|PM${Xn?9%rigW2J}h z=X4Oz5KOT1cAFRvOx?`WqttSFB8=x4{U>;i5yav`?;J!qDchmBHnf*pBj!k*=ALT> z@@%sMq#jpZvt~iZyE*U^b|Xt8{shaVmE&WrlGb$o&y$iE_xYsRR4D+;;^c&TVU2HT zUpKXGZ^c5zdwjgTFa?~q>_iG+Rb3W?(7fveY?c?J=kHzGF`rCVq7P(Gra`BSzATt& zaY*ciU5ZndF?i&nQ->q9ZTV_{e{^xyf8LsShxHy$vu^Lpa&}@uX`f?O-!MsW-AujLK`nTfV%;gdrVtw?dw6fj*>FhkC z;r`bx9!Zc8BoQRh5+z#nZj^`-B}%kG7%dUeBYKOFAT!!u8KR8djowF%C{ZVRCpx2d zh8g!K=d5+^i~HtWuPtw^_5JN3VUITRrz-YZDcTQ5(n_n3Id(-0Go?ek#hUUK zxvrg@&B*rZ^+gVU*PPlZdAOu+*a^pMe}JY^BAhhAnv=U zH6yR(aVTJyyx!$ffsaG8`2d_$rshRyYrM}8hg9qIa%?V-DUB(U+h(1|F{VoOX6k4P}-o#15Y89h6CFGx_qTRImWWEVdCz*>I1bt{3q zI_8x$h8ba1qBhN)31^)&tM9^Q=TuUVc8BVT)06D+V3~>9Sibs5Ygz`bYWN!cR;)ge zvsk5NJ&5xxwE&vPUau#(;oI0+Mmq(T+BWg*oqMfW6iZ=Li%E9}tsp|U+rXl_ikXIl z{!}6IF2H3OQzxs5uHfo;w~>y`sDc5Wi;&v?Hh!(WT_D-V0H58?L$mq08`rTq%}u~? zVRY(bdKs%R3zkf%^8m)TMO$Ine_Q}P@B7>nWE8FicPV>b^~TZhhnmI#l52c3QY^6H z#Zs-WuwTy*9k2Zxp83nl$?IQrk1t>h^Q8Mi4iT?cU2ir81AP_Ma#^DVz7MJx<+PL% z_Ikbq;v+==v0>-WCt;c7UB7^_Gq{!4Sn(rK-A=>a++&TH20O#!k+~GzymTZtsK;4; zKA|yjTzVxl$pfdQ!VJ08aw`EQDFUbk-w}>ILkp>GmN(X(+qz}kQo)GoOw^L7;h(ZF z-ZWBn35RgeBdY93Ja}5}K|_6`c9)@mpoI)d;T<=lpGFYFM@#!=ugFe`QlKxL7;(LQ zW+Y+&sDpBpAPQo&Mx#C&w|}P>+lWl9{Qg(z=>GoduP(VeBd6UVaGa+;&E}}npE4O$ zHT&y`jY;HfQNgD-j&Q(Zw1Cnf%PD4r&1Xf`JMLxUurZHP|7=r#qGF%e}Q- z0pLlX2w;l~FTzHxI(_3-szTyKotGI*Jj`OtB~FUsG@W=0I$LJhC$755K+|#6Ys|%S zwK9&$ue0XjqLtH_90he7 z^8RAad@<-vf?Lj?4v_dAaJCW6hu!GgU0YH6xmt{G{cReY7y=}Kj%RXB?T7Q4s(h(N zCNo+t4Vj5YlO?m&1r|xUaYkDA@s%l*mk%9|Mr+2{Meo3IwOD2#+qZtFi4d74a^4co zW`Z?ow4&`LWLreom{`v#TsuKRI;mz+Ih`o0>v<2Z*FOkVod5;V%*D(Hg7H9Gy^%@) z_tlWvq`# z(sS+1bTb|v$r+iM9dRW>VMXB=PUjDFn`4V9H^K~t&er)Gvs^c5^&m;Hz*z!~#cv;n zd3g9;V@6#obE9x9}j;o0?`3`KiEI(F?&bp^xc~T(a+Ap`&wfl#ZNT!I1)?Lm5mby|h zf;0Ep^p`BMNZAG_fjVo7)bk~reH!=E6tX0P5!gk|xWfcn=4pzsa&0l2$BHqKxL(do z6ArKhr5X9n^iZB6RS@aDs#ek9bK<4JmDu05ehEsu(tf(h@}u0qPDbFenvrb(9hlW_ z>pxDHC|s0)xcKmFbJ&blPq&OX(U$MkE5H0SBKD^HuIl1ApyS(fp|eNmt?l1#kRjzK z)gJBl%sWWESk+Su|1-3$E`X5Tz}|Kq4cG6mzg*4d?hT1W0>*1lrx*|=>OJXwH^s9l zW*^blbwKDJ45Dx}Tw&%BwTH~=RlpIXmwxiyPo!}61ycho$Zr2U@)i&wit2mIpxvaw zo3f`}RdvFNoF3>td8Hy|bbMRI%{#NF1gO@{^!kvwMM07~%6@TjR;QeE6`FUuuvlHB z$L-fQ5q8tq$Q>V+L_KHywEbIg#&h@B$d(M0a5zZZssC{Hda>3cGSSu>tSeyZa-%vg_2RPX&?g z)|e3RVPK%=OO}24^gnfBG(Qt|PmEMNLnwcS;`NwtVp_)$AMNwHa7}a?PRkhZ4~=c5 zHv7Zu&wGiT5?ZFeSuGDiS%v#KU3x~trUkx>zN2up!+tnZ;fofarq}0iWWlmMF+fTnd>OQ!i=nX7E6l@yx|A?{k1?3A zeCXT6T*79&QO+6pe1pIAQOyO^(e4n?CmE-+ip$Ttz{}T$E$cImiG^k+f6VGim1}22 z`IxXdcSV2-peA6Z2WJxpyH@9HA$@h7`|sGu4GAut`Ug84R7vu>!5l50Q*H&mv!-9{_5MaLli{BY{o$rREZdT% zFKBrodW2s&Uer5{(z>rSwH{I1I;)b62~7tylfUOO`lQVQmkFytSo{S+jDzd>%ARX+ zjdkwUD_ZIkx_18sf{3#LKoG3~O32(wc6|NUVH*oqV=U{4KGpDv;s10kz!qz`pX^p%%TlML9Zs@rd5KA1>rVcBHbmdn#HyRfNgtu4~o8Ji>K*?Jo zyr;jtHrwoC48Q7psjZL>{pvA6;xx1cI*o-Y(>^yB#_~`ORT#4dOkS?n-kk--(Z86DUJPV>{hS49Z zCsZ;((@ffN^2DH0>9b>_>^fTiObsz>HY6YGxKu-z5{eif$GfHX#E9(t*fw_gMR8dL z0x9mxx4Mv7vi(jBBbnzejel>sIf6+%a;(A->x@(Z?oSo?Z;)EZ-~j~I6O9_AWhP!X zoJs%L)lT5P_yas4n5!n%g&|M-=zF&`4r=d6Ho?!vQUgLeC7e1@>TF##2w0H-)NpFA zFY&Jmig9dd&SseWyDQH#@;vVbG5I)jz&XfcnbGgBs`$qI``vg7yc-u|Ny4*o!EC3l zLnwHm6>;4}y+pH{!=*`#eRQdNVg$ERW>_dn?{dU)2PbbDwj`Y&aVH!&goDGlVAxp; znkN_^XiT>S{yS3<-vXnr9S?coASee-UeFe0LFDJ&U&vSldq^y>xjSW&>o0KUoQie3|Y?%~`uZ)28MP81*7)a3nR1UPjRnICBQGjOY zf~%^Em56?g+fH6Xz1Ql1iy?krMyISV_Uj0q$1rAP_Z)8KrJ3`e(ukh%W&Q{h_K0zj zF>zLR?|WYnI5X6Joa`sHzORB#2cjW4z%qV=l!m=+T0;-+)05P#s+9iy?!DUBMh{O( zy!nQ{tfhbx|Gr4wGlaJ)4E2aEIs?5(xSeDpID*pq4qW|8=YCgPR}mC#D8gx86~5%S@3=9P6p>Xr!m#)2d+?) z+9gIho)q^KvBuDV^47TZoe=$Q_xL&~v^Hd^;RF0aN*$x>tdT{6dr2c#1O3@Gx+@E) z6r56e(&xKba8{#e^xE3|O?`;C5ee7Q2e;%eQ%#-NJ$EM4W?C(|OvUs5mnt7BlcboY zibB!Mkqe!Rz!s|ItxxKN?S7A{3$w-GWqz>bS@O30=~tQ*M1bW^k~NUDq2m%Z#6+2$ zwQO(zZV+P=(6`8H5ZlyB0&;^0u?@HGY>SS07@XN9t02697Z4_}yY{=G7Q%#-Zr>43 zyA(NYxk?I<>^~<@fPk}5{Od~I)9Wezr`=V0qq$Rh@|iuBsqw z`SdV$z3k--JdXY5mT&xkvP)v_nblSPy~Oiby~cU4|10SStIjN?PkZ7+sV!g32ZF4A zhq(7e^hu}`ek z*iNQmEA|}Ffl!`q(cKO8!PV*&d_-9sZl&4|7^>RuX~%isz6=Y}KFG0g)}AkANp?c? zg3uKzZ2b$jxfEuoHnI6Bk8=g))WXp>Tj zvKziFO|fSH8H~gj9!GS4{t9j#D_QMNm>?K&;?{4Ae|iK5NCU~e{l87uH*bRc>_Hks z-DLDHfPl^GBAU=*R(CaZMpNb@!bDYs>5i-acH$38LeQ(>f#cRHhgczTo)=w@M1!C4 z+Kh{(qGQL}LsU9qA8@(sG|O|(ltzd>G#+w22`eQsD$!5cu-#GJ^Q533W!ff>JJFmQLv;c}M3dsX)F z@yef?Enc&|Sh}$PG}z@_8R9g-4qa={395xD@?6^P@98vaTG8*OzR?mnX%5x z)>a-d*YXCpMK9}dwE3a}O|><@-*=vQSupBI5AiEsecE#G>nLPEZX0Q+H_ohe`pb1f zC_@nD8ihZvhH*5#^dKkl$3B1b?>*0m00xDf31-V+rg_)NnIk3l@?gE4jUmXWenC7B zGk53@R=<^-2jRkuU|j;{FmjFn~25 zs<2lWO^4NxOxD-dv`|fmS?l_4dCk3j=%tZe&cp2G=ROxl)7YKsQYN#zp@u$ei$*E{ zg)!ul#wJxcM*Xp)S~Hx(6U41S(~jZq64b!w7-%&ajW~&Q&pAkO+;fZ{=JI9PPJz_t zPim3P-%O|2z0`pXF5xB z6tvbGd8}sAiLQ>;3g7fi2t@~;^5zTm^^0Ve%w2v}^L1Wlpoh>pWH8H0dwO`-@-X#y zQywnfpfKR!GL=m9y{lk;60B{sam z_w7lq1T3w_3ps9kjTe$Ys=t67_9ht2l6k zk5lYq^Cl%6^1@dW{$6=| z>b2_Vfj55<_N|L*761yO=8l-2e7QC(hbyGT-Yh^nc+J4bs4QYz%JqWVT^Q;rZo5~o zv>b)&$Y$|QO6#}5@~`ay_#=srH^+`1>x~~X)gI(K_7<#ff7vw}w|)1EYjdsIG3=wp zx3)j@K|TgsERkh-R2O$9Wfylw8ANZq{G;6T;?Rv!jwjl|n=74-gSKj0(pTF4hIyOD zp2X6*#z6>2koGkQ*;L!YMq4Y*?C^c|_XtDZD zyN+9u2`Bq@&XeSq4`dQ@Ev7c*9Pr7FUK}0MN`F>4QsX^9IQkG;+)bB_g<~Zoz39bO zgr4&eq=(DJ7;zw58urn0Rqo0aVg?nkyl!!S;Hf2a>6C@dQwcR-Nntk^Yl9^l239x| zyJUM>a%=gFTyixLg|i3%gYpfHFpbU!2`^hsuhoHws)Nj+o-RE|< zIRT2*`rfY?KL8kWYG;9z_hSqp3-|vl324jZp1)N%?Q$(StlH@Lnt@I0%mi4LFqzH` zaJ1{CN=D_Ol#J3~Q-FF#oo1HvChv|p0O9(UBI(8|>@9kTG^nfpRc$)x*6NB?i1SfI zdWmE$Z$8wb9{EUXik5UHFQ9>s*wagnFzaT0q_u;{uiL}?>xSo%OGb&x;As@SrWK-3@r4eM;O>#K zb5`INP9^8nTv~AMP!YQmKd0(gz%!rg`TG)?=p)Db+uxzitHr$@ zkHv1pC|$w3dcN(F3n!p`#7g^8o5BIG$+PApfIZ>-P7GhE{xENxLX&B`PsRr~b=PYh z#@xeWtO2)jNK!on)(imZ*}7x-E;mqeCaaE#p{5Uxcn84HyhSe1hzfPJYgPwvgL06| zpGQ3R3JckYHS|fZTJSoI-LFDFA&Q+MHp+dJ8IlbiZIT)-Os_g-nu^j+EZ1i5-rDrG z_C0blC@Hvczt8{5jguRxRJ(J{8UZ)W&STA*gCBbzJPsz3f=fTWK2c!}4x~S3(Ar9( zBImqEc2)2O1I4i>W3s?Mfu6mS;_3J^s)4>1Z6FgW-bqS!1Hh#AvVRacaKsh6S5zuF z(zgI;Fww;3{*fj|`^~&@eU8^uS0ANP=dR%=3;jTmVKl>OR2S%Pv89y6*RS7|zoO~} zONC4S|4?t?WLG;FXbPXP{@2$Z{>8+6M06dvMsg+h*WLgAegyCr{;WQFwX*u2(h%6!v4yWOP4T6hEJk(*Hw&bZxCkPSePJTg*l&FfaYWo>e{(NVU>Lp9Jjtx<2LN&FV<;j6GtM{sRhUye+|SJ^aYF!hMua0<)@#H26^7kHu@g0VGM_c2+aq_MAfSNyx{-) g+=Rc4(w~cKyWS8r<&1!*SAZWCMGbJ#bF+Z|0cWkQ*8l(j literal 0 HcmV?d00001 diff --git a/crewai_tools/tools/nl2sql/images/image-5.png b/crewai_tools/tools/nl2sql/images/image-5.png new file mode 100644 index 0000000000000000000000000000000000000000..b7d6013dabb306a84987746604fced2606cc3bc3 GIT binary patch literal 66131 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


;CPR)pd0t8Q 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%0A+={fK+X-{t*uH7HS+r{TkP}eZgJ*`p@AFKz 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 afn;v};yKrKlgAFF?^jmPkS~`r_4^M?gl*~o literal 0 HcmV?d00001 diff --git a/crewai_tools/tools/nl2sql/images/image-9.png b/crewai_tools/tools/nl2sql/images/image-9.png new file mode 100644 index 0000000000000000000000000000000000000000..87f3824342c3aa28496ac5a44f06fad67f0d36fb GIT binary patch literal 56650 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 literal 0 HcmV?d00001 diff --git a/crewai_tools/tools/nl2sql/nl2sql_tool.py b/crewai_tools/tools/nl2sql/nl2sql_tool.py new file mode 100644 index 000000000..f3d892d1a --- /dev/null +++ b/crewai_tools/tools/nl2sql/nl2sql_tool.py @@ -0,0 +1,91 @@ +from typing import Any, Type, Union + +from crewai.tools import BaseTool +from pydantic import BaseModel, Field + +try: + from sqlalchemy import create_engine, text + from sqlalchemy.orm import sessionmaker + SQLALCHEMY_AVAILABLE = True +except ImportError: + SQLALCHEMY_AVAILABLE = False + + +class NL2SQLToolInput(BaseModel): + sql_query: str = Field( + title="SQL Query", + description="The SQL query to execute.", + ) + + +class NL2SQLTool(BaseTool): + name: str = "NL2SQLTool" + description: str = "Converts natural language to SQL queries and executes them." + db_uri: str = Field( + title="Database URI", + description="The URI of the database to connect to.", + ) + tables: list = [] + columns: dict = {} + args_schema: Type[BaseModel] = NL2SQLToolInput + + def model_post_init(self, __context: Any) -> None: + if not SQLALCHEMY_AVAILABLE: + raise ImportError("sqlalchemy is not installed. Please install it with `pip install crewai-tools[sqlalchemy]`") + + data = {} + tables = self._fetch_available_tables() + + for table in tables: + table_columns = self._fetch_all_available_columns(table["table_name"]) + data[f'{table["table_name"]}_columns'] = table_columns + + self.tables = tables + self.columns = data + + def _fetch_available_tables(self): + return self.execute_sql( + "SELECT table_name FROM information_schema.tables WHERE table_schema = 'public';" + ) + + def _fetch_all_available_columns(self, table_name: str): + return self.execute_sql( + f"SELECT column_name, data_type FROM information_schema.columns WHERE table_name = '{table_name}';" + ) + + def _run(self, sql_query: str): + try: + data = self.execute_sql(sql_query) + except Exception as exc: + data = ( + f"Based on these tables {self.tables} and columns {self.columns}, " + "you can create SQL queries to retrieve data from the database." + f"Get the original request {sql_query} and the error {exc} and create the correct SQL query." + ) + + return data + + def execute_sql(self, sql_query: str) -> Union[list, str]: + if not SQLALCHEMY_AVAILABLE: + raise ImportError("sqlalchemy is not installed. Please install it with `pip install crewai-tools[sqlalchemy]`") + + engine = create_engine(self.db_uri) + Session = sessionmaker(bind=engine) + session = Session() + try: + result = session.execute(text(sql_query)) + session.commit() + + if result.returns_rows: + columns = result.keys() + data = [dict(zip(columns, row)) for row in result.fetchall()] + return data + else: + return f"Query {sql_query} executed successfully" + + except Exception as e: + session.rollback() + raise e + + finally: + session.close() diff --git a/crewai_tools/tools/ocr_tool/README.md b/crewai_tools/tools/ocr_tool/README.md new file mode 100644 index 000000000..f5375ca18 --- /dev/null +++ b/crewai_tools/tools/ocr_tool/README.md @@ -0,0 +1,42 @@ +# OCR Tool + +## Description + +This tool performs Optical Character Recognition (OCR) on images using supported LLMs. It can extract text from both local image files and images available via URLs. The tool leverages the LLM's vision capabilities to provide accurate text extraction from images. + +## Installation +Install the crewai_tools package +```shell +pip install 'crewai[tools]' +``` + +## Supported LLMs + +Any LLM that supports the `vision` feature should work. It must accept image_url as a user message. +The tool has been tested with: +- OpenAI's `gpt-4o` +- Gemini's `gemini/gemini-1.5-pro` + +## Usage + +In order to use the OCRTool, make sure your LLM supports the `vision` feature and the appropriate API key is set in the environment (e.g., `OPENAI_API_KEY` for OpenAI). + +```python +from crewai_tools import OCRTool + +selected_llm = LLM(model="gpt-4o") # select your LLM, the tool has been tested with gpt-4o and gemini/gemini-1.5-pro + +ocr_tool = OCRTool(llm=selected_llm) + +@agent +def researcher(self) -> Agent: + return Agent( + config=self.agents_config["researcher"], + allow_delegation=False, + tools=[ocr_tool] + ) +``` + +The tool accepts either a local file path or a URL to the image: +- For local files, provide the absolute or relative path +- For remote images, provide the complete URL starting with 'http' or 'https' diff --git a/crewai_tools/tools/ocr_tool/ocr_tool.py b/crewai_tools/tools/ocr_tool/ocr_tool.py new file mode 100644 index 000000000..aabe0ffbd --- /dev/null +++ b/crewai_tools/tools/ocr_tool/ocr_tool.py @@ -0,0 +1,126 @@ +""" +Optical Character Recognition (OCR) Tool + +This tool provides functionality for extracting text from images using supported LLMs. Make sure your model supports the `vision` feature. +""" + +import base64 +from typing import Optional, Type + +from openai import OpenAI +from pydantic import BaseModel, PrivateAttr + +from crewai.tools.base_tool import BaseTool +from crewai import LLM + + +class OCRToolSchema(BaseModel): + """Input schema for Optical Character Recognition Tool. + + Attributes: + image_path_url (str): Path to a local image file or URL of an image. + For local files, provide the absolute or relative path. + For remote images, provide the complete URL starting with 'http' or 'https'. + """ + + image_path_url: str = "The image path or URL." + + +class OCRTool(BaseTool): + """A tool for performing Optical Character Recognition on images. + + This tool leverages LLMs to extract text from images. It can process + both local image files and images available via URLs. + + Attributes: + name (str): Name of the tool. + description (str): Description of the tool's functionality. + args_schema (Type[BaseModel]): Pydantic schema for input validation. + + Private Attributes: + _llm (Optional[LLM]): Language model instance for making API calls. + """ + + name: str = "Optical Character Recognition Tool" + description: str = ( + "This tool uses an LLM's API to extract text from an image file." + ) + _llm: Optional[LLM] = PrivateAttr(default=None) + + args_schema: Type[BaseModel] = OCRToolSchema + + def __init__(self, llm: LLM = None, **kwargs): + """Initialize the OCR tool. + + Args: + llm (LLM, optional): Language model instance to use for API calls. + If not provided, a default LLM with gpt-4o model will be used. + **kwargs: Additional arguments passed to the parent class. + """ + super().__init__(**kwargs) + + if llm is None: + # Use the default LLM + llm = LLM( + model="gpt-4o", + temperature=0.7, + ) + + self._llm = llm + + def _run(self, **kwargs) -> str: + """Execute the OCR operation on the provided image. + + Args: + **kwargs: Keyword arguments containing the image_path_url. + + Returns: + str: Extracted text from the image. + If no image path/URL is provided, returns an error message. + + Note: + The method handles both local image files and remote URLs: + - For local files: The image is read and encoded to base64 + - For URLs: The URL is passed directly to the Vision API + """ + image_path_url = kwargs.get("image_path_url") + + if not image_path_url: + return "Image Path or URL is required." + + if image_path_url.startswith("http"): + image_data = image_path_url + else: + base64_image = self._encode_image(image_path_url) + image_data = f"data:image/jpeg;base64,{base64_image}" + + messages=[ + { + "role": "system", + "content": "You are an expert OCR specialist. Extract complete text from the provided image. Provide the result as a raw text." + }, + { + "role": "user", + "content": [ + { + "type": "image_url", + "image_url": {"url": image_data}, + } + ], + } + ] + + response = self._llm.call(messages=messages) + return response + + def _encode_image(self, image_path: str): + """Encode an image file to base64 format. + + Args: + image_path (str): Path to the local image file. + + Returns: + str: Base64-encoded image data as a UTF-8 string. + """ + with open(image_path, "rb") as image_file: + return base64.b64encode(image_file.read()).decode("utf-8") diff --git a/crewai_tools/tools/oxylabs_amazon_product_scraper_tool/README.md b/crewai_tools/tools/oxylabs_amazon_product_scraper_tool/README.md new file mode 100644 index 000000000..f87c70c19 --- /dev/null +++ b/crewai_tools/tools/oxylabs_amazon_product_scraper_tool/README.md @@ -0,0 +1,55 @@ +# OxylabsAmazonProductScraperTool + +Scrape any website with `OxylabsAmazonProductScraperTool` + +## Installation + +``` +pip install 'crewai[tools]' oxylabs +``` + +## Example + +```python +from crewai_tools import OxylabsAmazonProductScraperTool + +# make sure OXYLABS_USERNAME and OXYLABS_PASSWORD variables are set +tool = OxylabsAmazonProductScraperTool() + +result = tool.run(query="AAAAABBBBCC") + +print(result) +``` + +## Arguments + +- `username`: Oxylabs username. +- `password`: Oxylabs password. + +Get the credentials by creating an Oxylabs Account [here](https://oxylabs.io). + +## Advanced example + +Check out the Oxylabs [documentation](https://developers.oxylabs.io/scraper-apis/web-scraper-api/targets/amazon/product) to get the full list of parameters. + +```python +from crewai_tools import OxylabsAmazonProductScraperTool + +# make sure OXYLABS_USERNAME and OXYLABS_PASSWORD variables are set +tool = OxylabsAmazonProductScraperTool( + config={ + "domain": "com", + "parse": True, + "context": [ + { + "key": "autoselect_variant", + "value": True + } + ] + } +) + +result = tool.run(query="AAAAABBBBCC") + +print(result) +``` diff --git a/crewai_tools/tools/oxylabs_amazon_product_scraper_tool/oxylabs_amazon_product_scraper_tool.py b/crewai_tools/tools/oxylabs_amazon_product_scraper_tool/oxylabs_amazon_product_scraper_tool.py new file mode 100644 index 000000000..1d4146fcb --- /dev/null +++ b/crewai_tools/tools/oxylabs_amazon_product_scraper_tool/oxylabs_amazon_product_scraper_tool.py @@ -0,0 +1,155 @@ +import json +import os +from importlib.metadata import version +from platform import architecture, python_version +from typing import Any, List, Type + +from crewai.tools import BaseTool, EnvVar +from pydantic import BaseModel, ConfigDict, Field + +try: + from oxylabs import RealtimeClient + from oxylabs.sources.response import Response as OxylabsResponse + + OXYLABS_AVAILABLE = True +except ImportError: + RealtimeClient = Any + OxylabsResponse = Any + + OXYLABS_AVAILABLE = False + + +__all__ = ["OxylabsAmazonProductScraperTool", "OxylabsAmazonProductScraperConfig"] + + +class OxylabsAmazonProductScraperArgs(BaseModel): + query: str = Field(description="Amazon product ASIN") + + +class OxylabsAmazonProductScraperConfig(BaseModel): + """ + Amazon Product Scraper configuration options: + https://developers.oxylabs.io/scraper-apis/web-scraper-api/targets/amazon/product + """ + + domain: str | None = Field( + None, description="The domain to limit the search results to." + ) + geo_location: str | None = Field(None, description="The Deliver to location.") + user_agent_type: str | None = Field(None, description="Device type and browser.") + render: str | None = Field(None, description="Enables JavaScript rendering.") + callback_url: str | None = Field(None, description="URL to your callback endpoint.") + context: list | None = Field( + None, + description="Additional advanced settings and controls for specialized requirements.", + ) + parse: bool | None = Field(None, description="True will return structured data.") + parsing_instructions: dict | None = Field( + None, description="Instructions for parsing the results." + ) + + +class OxylabsAmazonProductScraperTool(BaseTool): + """ + Scrape Amazon product pages with OxylabsAmazonProductScraperTool. + + Get Oxylabs account: + https://dashboard.oxylabs.io/en + + Args: + username (str): Oxylabs username. + password (str): Oxylabs password. + config: Configuration options. See ``OxylabsAmazonProductScraperConfig`` + """ + + model_config = ConfigDict( + arbitrary_types_allowed=True, + validate_assignment=True, + ) + name: str = "Oxylabs Amazon Product Scraper tool" + description: str = "Scrape Amazon product pages with Oxylabs Amazon Product Scraper" + args_schema: Type[BaseModel] = OxylabsAmazonProductScraperArgs + + oxylabs_api: RealtimeClient + config: OxylabsAmazonProductScraperConfig + package_dependencies: List[str] = ["oxylabs"] + env_vars: List[EnvVar] = [ + EnvVar(name="OXYLABS_USERNAME", description="Username for Oxylabs", required=True), + EnvVar(name="OXYLABS_PASSWORD", description="Password for Oxylabs", required=True), + ] + + def __init__( + self, + username: str | None = None, + password: str | None = None, + config: OxylabsAmazonProductScraperConfig + | dict = OxylabsAmazonProductScraperConfig(), + **kwargs, + ) -> None: + bits, _ = architecture() + sdk_type = ( + f"oxylabs-crewai-sdk-python/" + f"{version('crewai')} " + f"({python_version()}; {bits})" + ) + + if username is None or password is None: + username, password = self._get_credentials_from_env() + + if OXYLABS_AVAILABLE: + # import RealtimeClient to make it accessible for the current scope + from oxylabs import RealtimeClient + + kwargs["oxylabs_api"] = RealtimeClient( + username=username, + password=password, + sdk_type=sdk_type, + ) + else: + import click + + if click.confirm( + "You are missing the 'oxylabs' package. Would you like to install it?" + ): + import subprocess + + try: + subprocess.run(["uv", "add", "oxylabs"], check=True) + from oxylabs import RealtimeClient + + kwargs["oxylabs_api"] = RealtimeClient( + username=username, + password=password, + sdk_type=sdk_type, + ) + except subprocess.CalledProcessError: + raise ImportError("Failed to install oxylabs package") + else: + raise ImportError( + "`oxylabs` package not found, please run `uv add oxylabs`" + ) + + super().__init__(config=config, **kwargs) + + def _get_credentials_from_env(self) -> tuple[str, str]: + username = os.environ.get("OXYLABS_USERNAME") + password = os.environ.get("OXYLABS_PASSWORD") + if not username or not password: + raise ValueError( + "You must pass oxylabs username and password when instantiating the tool " + "or specify OXYLABS_USERNAME and OXYLABS_PASSWORD environment variables" + ) + return username, password + + def _run(self, query: str) -> str: + response = self.oxylabs_api.amazon.scrape_product( + query, + **self.config.model_dump(exclude_none=True), + ) + + content = response.results[0].content + + if isinstance(content, dict): + return json.dumps(content) + + return content diff --git a/crewai_tools/tools/oxylabs_amazon_search_scraper_tool/README.md b/crewai_tools/tools/oxylabs_amazon_search_scraper_tool/README.md new file mode 100644 index 000000000..b0e2ef7b0 --- /dev/null +++ b/crewai_tools/tools/oxylabs_amazon_search_scraper_tool/README.md @@ -0,0 +1,54 @@ +# OxylabsAmazonSearchScraperTool + +Scrape any website with `OxylabsAmazonSearchScraperTool` + +## Installation + +``` +pip install 'crewai[tools]' oxylabs +``` + +## Example + +```python +from crewai_tools import OxylabsAmazonSearchScraperTool + +# make sure OXYLABS_USERNAME and OXYLABS_PASSWORD variables are set +tool = OxylabsAmazonSearchScraperTool() + +result = tool.run(query="headsets") + +print(result) +``` + +## Arguments + +- `username`: Oxylabs username. +- `password`: Oxylabs password. + +Get the credentials by creating an Oxylabs Account [here](https://oxylabs.io). + +## Advanced example + +Check out the Oxylabs [documentation](https://developers.oxylabs.io/scraper-apis/web-scraper-api/targets/amazon/search) to get the full list of parameters. + +```python +from crewai_tools import OxylabsAmazonSearchScraperTool + +# make sure OXYLABS_USERNAME and OXYLABS_PASSWORD variables are set +tool = OxylabsAmazonSearchScraperTool( + config={ + "domain": 'nl', + "start_page": 2, + "pages": 2, + "parse": True, + "context": [ + {'key': 'category_id', 'value': 16391693031} + ], + } +) + +result = tool.run(query='nirvana tshirt') + +print(result) +``` diff --git a/crewai_tools/tools/oxylabs_amazon_search_scraper_tool/oxylabs_amazon_search_scraper_tool.py b/crewai_tools/tools/oxylabs_amazon_search_scraper_tool/oxylabs_amazon_search_scraper_tool.py new file mode 100644 index 000000000..e659d244f --- /dev/null +++ b/crewai_tools/tools/oxylabs_amazon_search_scraper_tool/oxylabs_amazon_search_scraper_tool.py @@ -0,0 +1,157 @@ +import json +import os +from importlib.metadata import version +from platform import architecture, python_version +from typing import Any, List, Type + +from crewai.tools import BaseTool, EnvVar +from pydantic import BaseModel, ConfigDict, Field + +try: + from oxylabs import RealtimeClient + from oxylabs.sources.response import Response as OxylabsResponse + + OXYLABS_AVAILABLE = True +except ImportError: + RealtimeClient = Any + OxylabsResponse = Any + + OXYLABS_AVAILABLE = False + + +__all__ = ["OxylabsAmazonSearchScraperTool", "OxylabsAmazonSearchScraperConfig"] + + +class OxylabsAmazonSearchScraperArgs(BaseModel): + query: str = Field(description="Amazon search term") + + +class OxylabsAmazonSearchScraperConfig(BaseModel): + """ + Amazon Search Scraper configuration options: + https://developers.oxylabs.io/scraper-apis/web-scraper-api/targets/amazon/search + """ + + domain: str | None = Field( + None, description="The domain to limit the search results to." + ) + start_page: int | None = Field(None, description="The starting page number.") + pages: int | None = Field(None, description="The number of pages to scrape.") + geo_location: str | None = Field(None, description="The Deliver to location.") + user_agent_type: str | None = Field(None, description="Device type and browser.") + render: str | None = Field(None, description="Enables JavaScript rendering.") + callback_url: str | None = Field(None, description="URL to your callback endpoint.") + context: list | None = Field( + None, + description="Additional advanced settings and controls for specialized requirements.", + ) + parse: bool | None = Field(None, description="True will return structured data.") + parsing_instructions: dict | None = Field( + None, description="Instructions for parsing the results." + ) + + +class OxylabsAmazonSearchScraperTool(BaseTool): + """ + Scrape Amazon search results with OxylabsAmazonSearchScraperTool. + + Get Oxylabs account: + https://dashboard.oxylabs.io/en + + Args: + username (str): Oxylabs username. + password (str): Oxylabs password. + config: Configuration options. See ``OxylabsAmazonSearchScraperConfig`` + """ + + model_config = ConfigDict( + arbitrary_types_allowed=True, + validate_assignment=True, + ) + name: str = "Oxylabs Amazon Search Scraper tool" + description: str = "Scrape Amazon search results with Oxylabs Amazon Search Scraper" + args_schema: Type[BaseModel] = OxylabsAmazonSearchScraperArgs + + oxylabs_api: RealtimeClient + config: OxylabsAmazonSearchScraperConfig + package_dependencies: List[str] = ["oxylabs"] + env_vars: List[EnvVar] = [ + EnvVar(name="OXYLABS_USERNAME", description="Username for Oxylabs", required=True), + EnvVar(name="OXYLABS_PASSWORD", description="Password for Oxylabs", required=True), + ] + + def __init__( + self, + username: str | None = None, + password: str | None = None, + config: OxylabsAmazonSearchScraperConfig + | dict = OxylabsAmazonSearchScraperConfig(), + **kwargs, + ): + bits, _ = architecture() + sdk_type = ( + f"oxylabs-crewai-sdk-python/" + f"{version('crewai')} " + f"({python_version()}; {bits})" + ) + + if username is None or password is None: + username, password = self._get_credentials_from_env() + + if OXYLABS_AVAILABLE: + # import RealtimeClient to make it accessible for the current scope + from oxylabs import RealtimeClient + + kwargs["oxylabs_api"] = RealtimeClient( + username=username, + password=password, + sdk_type=sdk_type, + ) + else: + import click + + if click.confirm( + "You are missing the 'oxylabs' package. Would you like to install it?" + ): + import subprocess + + try: + subprocess.run(["uv", "add", "oxylabs"], check=True) + from oxylabs import RealtimeClient + + kwargs["oxylabs_api"] = RealtimeClient( + username=username, + password=password, + sdk_type=sdk_type, + ) + except subprocess.CalledProcessError: + raise ImportError("Failed to install oxylabs package") + else: + raise ImportError( + "`oxylabs` package not found, please run `uv add oxylabs`" + ) + + super().__init__(config=config, **kwargs) + + def _get_credentials_from_env(self) -> tuple[str, str]: + username = os.environ.get("OXYLABS_USERNAME") + password = os.environ.get("OXYLABS_PASSWORD") + if not username or not password: + raise ValueError( + "You must pass oxylabs username and password when instantiating the tool " + "or specify OXYLABS_USERNAME and OXYLABS_PASSWORD environment variables" + ) + return username, password + + def _run(self, query: str) -> str: + response = self.oxylabs_api.amazon.scrape_search( + query, + **self.config.model_dump(exclude_none=True), + ) + + content = response.results[0].content + + if isinstance(content, dict): + return json.dumps(content) + + return content diff --git a/crewai_tools/tools/oxylabs_google_search_scraper_tool/README.md b/crewai_tools/tools/oxylabs_google_search_scraper_tool/README.md new file mode 100644 index 000000000..e9448d2db --- /dev/null +++ b/crewai_tools/tools/oxylabs_google_search_scraper_tool/README.md @@ -0,0 +1,50 @@ +# OxylabsGoogleSearchScraperTool + +Scrape any website with `OxylabsGoogleSearchScraperTool` + +## Installation + +``` +pip install 'crewai[tools]' oxylabs +``` + +## Example + +```python +from crewai_tools import OxylabsGoogleSearchScraperTool + +# make sure OXYLABS_USERNAME and OXYLABS_PASSWORD variables are set +tool = OxylabsGoogleSearchScraperTool() + +result = tool.run(query="iPhone 16") + +print(result) +``` + +## Arguments + +- `username`: Oxylabs username. +- `password`: Oxylabs password. + +Get the credentials by creating an Oxylabs Account [here](https://oxylabs.io). + +## Advanced example + +Check out the Oxylabs [documentation](https://developers.oxylabs.io/scraper-apis/web-scraper-api/targets/google/search/search) to get the full list of parameters. + +```python +from crewai_tools import OxylabsGoogleSearchScraperTool + +# make sure OXYLABS_USERNAME and OXYLABS_PASSWORD variables are set +tool = OxylabsGoogleSearchScraperTool( + config={ + "parse": True, + "geo_location": "Paris, France", + "user_agent_type": "tablet", + } +) + +result = tool.run(query="iPhone 16") + +print(result) +``` diff --git a/crewai_tools/tools/oxylabs_google_search_scraper_tool/oxylabs_google_search_scraper_tool.py b/crewai_tools/tools/oxylabs_google_search_scraper_tool/oxylabs_google_search_scraper_tool.py new file mode 100644 index 000000000..1096df098 --- /dev/null +++ b/crewai_tools/tools/oxylabs_google_search_scraper_tool/oxylabs_google_search_scraper_tool.py @@ -0,0 +1,160 @@ +import json +import os +from importlib.metadata import version +from platform import architecture, python_version +from typing import Any, List, Type + +from crewai.tools import BaseTool, EnvVar +from pydantic import BaseModel, ConfigDict, Field + +try: + from oxylabs import RealtimeClient + from oxylabs.sources.response import Response as OxylabsResponse + + OXYLABS_AVAILABLE = True +except ImportError: + RealtimeClient = Any + OxylabsResponse = Any + + OXYLABS_AVAILABLE = False + + +__all__ = ["OxylabsGoogleSearchScraperTool", "OxylabsGoogleSearchScraperConfig"] + + +class OxylabsGoogleSearchScraperArgs(BaseModel): + query: str = Field(description="Search query") + + +class OxylabsGoogleSearchScraperConfig(BaseModel): + """ + Google Search Scraper configuration options: + https://developers.oxylabs.io/scraper-apis/web-scraper-api/targets/google/search/search + """ + + domain: str | None = Field( + None, description="The domain to limit the search results to." + ) + start_page: int | None = Field(None, description="The starting page number.") + pages: int | None = Field(None, description="The number of pages to scrape.") + limit: int | None = Field( + None, description="Number of results to retrieve in each page." + ) + geo_location: str | None = Field(None, description="The Deliver to location.") + user_agent_type: str | None = Field(None, description="Device type and browser.") + render: str | None = Field(None, description="Enables JavaScript rendering.") + callback_url: str | None = Field(None, description="URL to your callback endpoint.") + context: list | None = Field( + None, + description="Additional advanced settings and controls for specialized requirements.", + ) + parse: bool | None = Field(None, description="True will return structured data.") + parsing_instructions: dict | None = Field( + None, description="Instructions for parsing the results." + ) + + +class OxylabsGoogleSearchScraperTool(BaseTool): + """ + Scrape Google Search results with OxylabsGoogleSearchScraperTool. + + Get Oxylabs account: + https://dashboard.oxylabs.io/en + + Args: + username (str): Oxylabs username. + password (str): Oxylabs password. + config: Configuration options. See ``OxylabsGoogleSearchScraperConfig`` + """ + + model_config = ConfigDict( + arbitrary_types_allowed=True, + validate_assignment=True, + ) + name: str = "Oxylabs Google Search Scraper tool" + description: str = "Scrape Google Search results with Oxylabs Google Search Scraper" + args_schema: Type[BaseModel] = OxylabsGoogleSearchScraperArgs + + oxylabs_api: RealtimeClient + config: OxylabsGoogleSearchScraperConfig + package_dependencies: List[str] = ["oxylabs"] + env_vars: List[EnvVar] = [ + EnvVar(name="OXYLABS_USERNAME", description="Username for Oxylabs", required=True), + EnvVar(name="OXYLABS_PASSWORD", description="Password for Oxylabs", required=True), + ] + + def __init__( + self, + username: str | None = None, + password: str | None = None, + config: OxylabsGoogleSearchScraperConfig + | dict = OxylabsGoogleSearchScraperConfig(), + **kwargs, + ): + bits, _ = architecture() + sdk_type = ( + f"oxylabs-crewai-sdk-python/" + f"{version('crewai')} " + f"({python_version()}; {bits})" + ) + + if username is None or password is None: + username, password = self._get_credentials_from_env() + + if OXYLABS_AVAILABLE: + # import RealtimeClient to make it accessible for the current scope + from oxylabs import RealtimeClient + + kwargs["oxylabs_api"] = RealtimeClient( + username=username, + password=password, + sdk_type=sdk_type, + ) + else: + import click + + if click.confirm( + "You are missing the 'oxylabs' package. Would you like to install it?" + ): + import subprocess + + try: + subprocess.run(["uv", "add", "oxylabs"], check=True) + from oxylabs import RealtimeClient + + kwargs["oxylabs_api"] = RealtimeClient( + username=username, + password=password, + sdk_type=sdk_type, + ) + except subprocess.CalledProcessError: + raise ImportError("Failed to install oxylabs package") + else: + raise ImportError( + "`oxylabs` package not found, please run `uv add oxylabs`" + ) + + super().__init__(config=config, **kwargs) + + def _get_credentials_from_env(self) -> tuple[str, str]: + username = os.environ.get("OXYLABS_USERNAME") + password = os.environ.get("OXYLABS_PASSWORD") + if not username or not password: + raise ValueError( + "You must pass oxylabs username and password when instantiating the tool " + "or specify OXYLABS_USERNAME and OXYLABS_PASSWORD environment variables" + ) + return username, password + + def _run(self, query: str, **kwargs) -> str: + response = self.oxylabs_api.google.scrape_search( + query, + **self.config.model_dump(exclude_none=True), + ) + + content = response.results[0].content + + if isinstance(content, dict): + return json.dumps(content) + + return content diff --git a/crewai_tools/tools/oxylabs_universal_scraper_tool/README.md b/crewai_tools/tools/oxylabs_universal_scraper_tool/README.md new file mode 100644 index 000000000..82f345a65 --- /dev/null +++ b/crewai_tools/tools/oxylabs_universal_scraper_tool/README.md @@ -0,0 +1,69 @@ +# OxylabsUniversalScraperTool + +Scrape any website with `OxylabsUniversalScraperTool` + +## Installation + +``` +pip install 'crewai[tools]' oxylabs +``` + +## Example + +```python +from crewai_tools import OxylabsUniversalScraperTool + +# make sure OXYLABS_USERNAME and OXYLABS_PASSWORD variables are set +tool = OxylabsUniversalScraperTool() + +result = tool.run(url="https://ip.oxylabs.io") + +print(result) +``` + +## Arguments + +- `username`: Oxylabs username. +- `password`: Oxylabs password. + +Get the credentials by creating an Oxylabs Account [here](https://oxylabs.io). + +## Advanced example + +Check out the Oxylabs [documentation](https://developers.oxylabs.io/scraper-apis/web-scraper-api/other-websites) to get the full list of parameters. + +```python +from crewai_tools import OxylabsUniversalScraperTool + +# make sure OXYLABS_USERNAME and OXYLABS_PASSWORD variables are set +tool = OxylabsUniversalScraperTool( + config={ + "render": "html", + "user_agent_type": "mobile", + "context": [ + {"key": "force_headers", "value": True}, + {"key": "force_cookies", "value": True}, + { + "key": "headers", + "value": { + "Custom-Header-Name": "custom header content", + }, + }, + { + "key": "cookies", + "value": [ + {"key": "NID", "value": "1234567890"}, + {"key": "1P JAR", "value": "0987654321"}, + ], + }, + {"key": "http_method", "value": "get"}, + {"key": "follow_redirects", "value": True}, + {"key": "successful_status_codes", "value": [808, 909]}, + ], + } +) + +result = tool.run(url="https://ip.oxylabs.io") + +print(result) +``` diff --git a/crewai_tools/tools/oxylabs_universal_scraper_tool/oxylabs_universal_scraper_tool.py b/crewai_tools/tools/oxylabs_universal_scraper_tool/oxylabs_universal_scraper_tool.py new file mode 100644 index 000000000..05b174500 --- /dev/null +++ b/crewai_tools/tools/oxylabs_universal_scraper_tool/oxylabs_universal_scraper_tool.py @@ -0,0 +1,150 @@ +import json +import os +from importlib.metadata import version +from platform import architecture, python_version +from typing import Any, List, Type + +from crewai.tools import BaseTool, EnvVar +from pydantic import BaseModel, ConfigDict, Field + +try: + from oxylabs import RealtimeClient + from oxylabs.sources.response import Response as OxylabsResponse + + OXYLABS_AVAILABLE = True +except ImportError: + RealtimeClient = Any + OxylabsResponse = Any + + OXYLABS_AVAILABLE = False + +__all__ = ["OxylabsUniversalScraperTool", "OxylabsUniversalScraperConfig"] + + +class OxylabsUniversalScraperArgs(BaseModel): + url: str = Field(description="Website URL") + + +class OxylabsUniversalScraperConfig(BaseModel): + """ + Universal Scraper configuration options: + https://developers.oxylabs.io/scraper-apis/web-scraper-api/other-websites + """ + + geo_location: str | None = Field(None, description="The Deliver to location.") + user_agent_type: str | None = Field(None, description="Device type and browser.") + render: str | None = Field(None, description="Enables JavaScript rendering.") + callback_url: str | None = Field(None, description="URL to your callback endpoint.") + context: list | None = Field( + None, + description="Additional advanced settings and controls for specialized requirements.", + ) + parse: bool | None = Field(None, description="True will return structured data.") + parsing_instructions: dict | None = Field( + None, description="Instructions for parsing the results." + ) + + +class OxylabsUniversalScraperTool(BaseTool): + """ + Scrape any website with OxylabsUniversalScraperTool. + + Get Oxylabs account: + https://dashboard.oxylabs.io/en + + Args: + username (str): Oxylabs username. + password (str): Oxylabs password. + config: Configuration options. See ``OxylabsUniversalScraperConfig`` + """ + + model_config = ConfigDict( + arbitrary_types_allowed=True, + validate_assignment=True, + ) + name: str = "Oxylabs Universal Scraper tool" + description: str = "Scrape any url with Oxylabs Universal Scraper" + args_schema: Type[BaseModel] = OxylabsUniversalScraperArgs + + oxylabs_api: RealtimeClient + config: OxylabsUniversalScraperConfig + package_dependencies: List[str] = ["oxylabs"] + env_vars: List[EnvVar] = [ + EnvVar(name="OXYLABS_USERNAME", description="Username for Oxylabs", required=True), + EnvVar(name="OXYLABS_PASSWORD", description="Password for Oxylabs", required=True), + ] + + def __init__( + self, + username: str | None = None, + password: str | None = None, + config: OxylabsUniversalScraperConfig | dict = OxylabsUniversalScraperConfig(), + **kwargs, + ): + bits, _ = architecture() + sdk_type = ( + f"oxylabs-crewai-sdk-python/" + f"{version('crewai')} " + f"({python_version()}; {bits})" + ) + + if username is None or password is None: + username, password = self._get_credentials_from_env() + + if OXYLABS_AVAILABLE: + # import RealtimeClient to make it accessible for the current scope + from oxylabs import RealtimeClient + + kwargs["oxylabs_api"] = RealtimeClient( + username=username, + password=password, + sdk_type=sdk_type, + ) + else: + import click + + if click.confirm( + "You are missing the 'oxylabs' package. Would you like to install it?" + ): + import subprocess + + try: + subprocess.run(["uv", "add", "oxylabs"], check=True) + from oxylabs import RealtimeClient + + kwargs["oxylabs_api"] = RealtimeClient( + username=username, + password=password, + sdk_type=sdk_type, + ) + except subprocess.CalledProcessError: + raise ImportError("Failed to install oxylabs package") + else: + raise ImportError( + "`oxylabs` package not found, please run `uv add oxylabs`" + ) + + super().__init__(config=config, **kwargs) + + def _get_credentials_from_env(self) -> tuple[str, str]: + username = os.environ.get("OXYLABS_USERNAME") + password = os.environ.get("OXYLABS_PASSWORD") + if not username or not password: + raise ValueError( + "You must pass oxylabs username and password when instantiating the tool " + "or specify OXYLABS_USERNAME and OXYLABS_PASSWORD environment variables" + ) + return username, password + + def _run(self, url: str) -> str: + response = self.oxylabs_api.universal.scrape_url( + url, + **self.config.model_dump(exclude_none=True), + ) + + content = response.results[0].content + + if isinstance(content, dict): + return json.dumps(content) + + return content diff --git a/crewai_tools/tools/parallel_tools/README.md b/crewai_tools/tools/parallel_tools/README.md new file mode 100644 index 000000000..37f413561 --- /dev/null +++ b/crewai_tools/tools/parallel_tools/README.md @@ -0,0 +1,153 @@ +# ParallelSearchTool + +Unified Parallel web search tool using the Parallel Search API (v1beta). Returns ranked results with compressed excerpts optimized for LLMs. + +- **Quickstart**: see the official docs: [Search API Quickstart](https://docs.parallel.ai/search-api/search-quickstart) +- **Processors**: guidance on `base` vs `pro`: [Processors](https://docs.parallel.ai/search-api/processors) + +## Why this tool + +- **Single-call pipeline**: Replaces search → scrape → extract with a single, low‑latency API call. +- **LLM‑ready**: Returns compressed excerpts that feed directly into LLM prompts (fewer tokens, less pre/post‑processing). +- **Flexible**: Control result count and excerpt length; optionally restrict sources via `source_policy`. + +## Environment + +- `PARALLEL_API_KEY` (required) + +Optional (for the agent example): +- `OPENAI_API_KEY` or other LLM provider keys supported by CrewAI + +## Parameters + +- `objective` (str, optional): Natural‑language research goal (≤ 5000 chars) +- `search_queries` (list[str], optional): Up to 5 keyword queries (each ≤ 200 chars) +- `processor` (str, default `base`): `base` (fast/low cost) or `pro` (freshness/quality) +- `max_results` (int, default 10): ≤ 40 (subject to processor limits) +- `max_chars_per_result` (int, default 6000): ≥ 100; values > 30000 not guaranteed +- `source_policy` (dict, optional): Source policy for domain inclusion/exclusion + +Notes: +- API is in beta; default rate limit is 600 RPM. Contact support for production capacity. + +## Direct usage (when published) + +```python +from crewai_tools import ParallelSearchTool + +tool = ParallelSearchTool() +resp_json = tool.run( + objective="When was the United Nations established? Prefer UN's websites.", + search_queries=["Founding year UN", "Year of founding United Nations"], + processor="base", + max_results=5, + max_chars_per_result=1500, +) +print(resp_json) # => {"search_id": ..., "results": [{"url", "title", "excerpts": [...]}, ...]} +``` + +### Parameters you can pass + +Call `run(...)` with any of the following (at least one of `objective` or `search_queries` is required): + +```python +tool.run( + objective: str | None = None, # ≤ 5000 chars + search_queries: list[str] | None = None, # up to 5 items, each ≤ 200 chars + processor: str = "base", # "base" (fast) or "pro" (freshness/quality) + max_results: int = 10, # ≤ 40 (processor limits apply) + max_chars_per_result: int = 6000, # ≥ 100 (values > 30000 not guaranteed) + source_policy: dict | None = None, # optional SourcePolicy config +) +``` + +Example with `source_policy`: + +```python +source_policy = { + "allow": {"domains": ["un.org"]}, + # "deny": {"domains": ["example.com"]}, # optional +} + +resp_json = tool.run( + objective="When was the United Nations established?", + processor="base", + max_results=5, + max_chars_per_result=1500, + source_policy=source_policy, +) +``` + +## Example with agents + +Here’s a minimal example that calls `ParallelSearchTool` to fetch sources and has an LLM produce a short, cited answer. + +```python +import os +from crewai import Agent, Task, Crew, LLM, Process +from crewai_tools import ParallelSearchTool + +# LLM +llm = LLM( + model="gemini/gemini-2.0-flash", + temperature=0.5, + api_key=os.getenv("GEMINI_API_KEY") +) + +# Parallel Search +search = ParallelSearchTool() + +# User query +query = "find all the recent concerns about AI evals? please cite the sources" + +# Researcher agent +researcher = Agent( + role="Web Researcher", + backstory="You are an expert web researcher", + goal="Find cited, high-quality sources and provide a brief answer.", + tools=[search], + llm=llm, + verbose=True, +) + +# Research task +task = Task( + description=f"Research the {query} and produce a short, cited answer.", + expected_output="A concise, sourced answer to the question. The answer should be in this format: [query]: [answer] - [source]", + agent=researcher, + output_file="answer.mdx", +) + +# Crew +crew = Crew( + agents=[researcher], + tasks=[task], + verbose=True, + process=Process.sequential, +) + +# Run the crew +result = crew.kickoff(inputs={'query': query}) +print(result) +``` + +Output from the agent above: + +```md +Recent concerns about AI evaluations include: the rise of AI-related incidents alongside a lack of standardized Responsible AI (RAI) evaluations among major industrial model developers - [https://hai.stanford.edu/ai-index/2025-ai-index-report]; flawed benchmark datasets that fail to account for critical factors, leading to unrealistic estimates of AI model abilities - [https://www.nature.com/articles/d41586-025-02462-5]; the need for multi-metric, context-aware evaluations in medical imaging AI to ensure reliability and clinical relevance - [https://www.sciencedirect.com/science/article/pii/S3050577125000283]; challenges related to data sets (insufficient, imbalanced, or poor quality), communication gaps, and misaligned expectations in AI model training - [https://www.oracle.com/artificial-intelligence/ai-model-training-challenges/]; the argument that LLM agents should be evaluated primarily on their riskiness, not just performance, due to unreliability, hallucinations, and brittleness - [https://www.technologyreview.com/2025/06/24/1119187/fix-ai-evaluation-crisis/]; the fact that the AI industry's embraced benchmarks may be close to meaningless, with top makers of AI models picking and choosing different responsible AI benchmarks, complicating efforts to systematically compare risks and limitations - [https://themarkup.org/artificial-intelligence/2024/07/17/everyone-is-judging-ai-by-these-tests-but-experts-say-theyre-close-to-meaningless]; and the difficulty of building robust and reliable model evaluations, as many existing evaluation suites are limited in their ability to serve as accurate indicators of model capabilities or safety - [https://www.anthropic.com/research/evaluating-ai-systems]. +``` + +Tips: +- Ensure your LLM provider keys are set (e.g., `GEMINI_API_KEY`) and CrewAI model config is in place. +- For longer analyses, raise `max_chars_per_result` or use `processor="pro"` (higher quality, higher latency). + +## Behavior + +- Single‑request web research; no scraping/post‑processing required. +- Returns `search_id` and ranked `results` with compressed `excerpts`. +- Clear error handling on HTTP/timeouts. + +## References + +- Search API Quickstart: https://docs.parallel.ai/search-api/search-quickstart +- Processors: https://docs.parallel.ai/search-api/processors diff --git a/crewai_tools/tools/parallel_tools/__init__.py b/crewai_tools/tools/parallel_tools/__init__.py new file mode 100644 index 000000000..579dc8941 --- /dev/null +++ b/crewai_tools/tools/parallel_tools/__init__.py @@ -0,0 +1,7 @@ +from .parallel_search_tool import ParallelSearchTool + +__all__ = [ + "ParallelSearchTool", +] + + diff --git a/crewai_tools/tools/parallel_tools/parallel_search_tool.py b/crewai_tools/tools/parallel_tools/parallel_search_tool.py new file mode 100644 index 000000000..d695bac9d --- /dev/null +++ b/crewai_tools/tools/parallel_tools/parallel_search_tool.py @@ -0,0 +1,119 @@ +import os +from typing import Any, Dict, List, Optional, Type, Annotated + +import requests +from crewai.tools import BaseTool, EnvVar +from pydantic import BaseModel, Field + + +class ParallelSearchInput(BaseModel): + """Input schema for ParallelSearchTool using the Search API (v1beta). + + At least one of objective or search_queries is required. + """ + + objective: Optional[str] = Field( + None, + description="Natural-language goal for the web research (<=5000 chars)", + max_length=5000, + ) + search_queries: Optional[List[Annotated[str, Field(max_length=200)]]] = Field( + default=None, + description="Optional list of keyword queries (<=5 items, each <=200 chars)", + min_length=1, + max_length=5, + ) + processor: str = Field( + default="base", + description="Search processor: 'base' (fast/low cost) or 'pro' (higher quality/freshness)", + pattern=r"^(base|pro)$", + ) + max_results: int = Field( + default=10, + ge=1, + le=40, + description="Maximum number of search results to return (processor limits apply)", + ) + max_chars_per_result: int = Field( + default=6000, + ge=100, + description="Maximum characters per result excerpt (values >30000 not guaranteed)", + ) + source_policy: Optional[Dict[str, Any]] = Field( + default=None, description="Optional source policy configuration" + ) + + +class ParallelSearchTool(BaseTool): + name: str = "Parallel Web Search Tool" + description: str = ( + "Search the web using Parallel's Search API (v1beta). Returns ranked results with " + "compressed excerpts optimized for LLMs." + ) + args_schema: Type[BaseModel] = ParallelSearchInput + + env_vars: List[EnvVar] = [ + EnvVar( + name="PARALLEL_API_KEY", + description="API key for Parallel", + required=True, + ), + ] + package_dependencies: List[str] = ["requests"] + + search_url: str = "https://api.parallel.ai/v1beta/search" + + def _run( + self, + objective: Optional[str] = None, + search_queries: Optional[List[str]] = None, + processor: str = "base", + max_results: int = 10, + max_chars_per_result: int = 6000, + source_policy: Optional[Dict[str, Any]] = None, + **_: Any, + ) -> str: + api_key = os.environ.get("PARALLEL_API_KEY") + if not api_key: + return "Error: PARALLEL_API_KEY environment variable is required" + + if not objective and not search_queries: + return "Error: Provide at least one of 'objective' or 'search_queries'" + + headers = { + "x-api-key": api_key, + "Content-Type": "application/json", + } + + try: + payload: Dict[str, Any] = { + "processor": processor, + "max_results": max_results, + "max_chars_per_result": max_chars_per_result, + } + if objective is not None: + payload["objective"] = objective + if search_queries is not None: + payload["search_queries"] = search_queries + if source_policy is not None: + payload["source_policy"] = source_policy + + request_timeout = 90 if processor == "pro" else 30 + resp = requests.post(self.search_url, json=payload, headers=headers, timeout=request_timeout) + if resp.status_code >= 300: + return f"Parallel Search API error: {resp.status_code} {resp.text[:200]}" + data = resp.json() + return self._format_output(data) + except requests.Timeout: + return "Parallel Search API timeout. Please try again later." + except Exception as exc: # noqa: BLE001 + return f"Unexpected error calling Parallel Search API: {exc}" + + def _format_output(self, result: Dict[str, Any]) -> str: + # Return the full JSON payload (search_id + results) as a compact JSON string + try: + import json + + return json.dumps(result or {}, ensure_ascii=False) + except Exception: + return str(result or {}) diff --git a/crewai_tools/tools/patronus_eval_tool/__init__.py b/crewai_tools/tools/patronus_eval_tool/__init__.py new file mode 100644 index 000000000..351cced92 --- /dev/null +++ b/crewai_tools/tools/patronus_eval_tool/__init__.py @@ -0,0 +1,3 @@ +from .patronus_eval_tool import PatronusEvalTool +from .patronus_local_evaluator_tool import PatronusLocalEvaluatorTool +from .patronus_predefined_criteria_eval_tool import PatronusPredefinedCriteriaEvalTool diff --git a/crewai_tools/tools/patronus_eval_tool/example.py b/crewai_tools/tools/patronus_eval_tool/example.py new file mode 100644 index 000000000..185e9f485 --- /dev/null +++ b/crewai_tools/tools/patronus_eval_tool/example.py @@ -0,0 +1,55 @@ +import random + +from crewai import Agent, Crew, Task +from patronus import Client, EvaluationResult +from patronus_local_evaluator_tool import PatronusLocalEvaluatorTool + +# Test the PatronusLocalEvaluatorTool where agent uses the local evaluator +client = Client() + + +# Example of an evaluator that returns a random pass/fail result +@client.register_local_evaluator("random_evaluator") +def random_evaluator(**kwargs): + score = random.random() + return EvaluationResult( + score_raw=score, + pass_=score >= 0.5, + explanation="example explanation", # Optional justification for LLM judges + ) + + +# 1. Uses PatronusEvalTool: agent can pick the best evaluator and criteria +# patronus_eval_tool = PatronusEvalTool() + +# 2. Uses PatronusPredefinedCriteriaEvalTool: agent uses the defined evaluator and criteria +# patronus_eval_tool = PatronusPredefinedCriteriaEvalTool( +# evaluators=[{"evaluator": "judge", "criteria": "contains-code"}] +# ) + +# 3. Uses PatronusLocalEvaluatorTool: agent uses user defined evaluator +patronus_eval_tool = PatronusLocalEvaluatorTool( + patronus_client=client, + evaluator="random_evaluator", + evaluated_model_gold_answer="example label", +) + +# Create a new agent +coding_agent = Agent( + role="Coding Agent", + goal="Generate high quality code and verify that the output is code by using Patronus AI's evaluation tool.", + backstory="You are an experienced coder who can generate high quality python code. You can follow complex instructions accurately and effectively.", + tools=[patronus_eval_tool], + verbose=True, +) + +# Define tasks +generate_code = Task( + description="Create a simple program to generate the first N numbers in the Fibonacci sequence. Select the most appropriate evaluator and criteria for evaluating your output.", + expected_output="Program that generates the first N numbers in the Fibonacci sequence.", + agent=coding_agent, +) + +crew = Crew(agents=[coding_agent], tasks=[generate_code]) + +crew.kickoff() diff --git a/crewai_tools/tools/patronus_eval_tool/patronus_eval_tool.py b/crewai_tools/tools/patronus_eval_tool/patronus_eval_tool.py new file mode 100644 index 000000000..bc9a60aae --- /dev/null +++ b/crewai_tools/tools/patronus_eval_tool/patronus_eval_tool.py @@ -0,0 +1,144 @@ +import json +import os +import warnings +from typing import Any, Dict, List, Optional + +import requests +from crewai.tools import BaseTool, EnvVar + + +class PatronusEvalTool(BaseTool): + name: str = "Patronus Evaluation Tool" + evaluate_url: str = "https://api.patronus.ai/v1/evaluate" + evaluators: List[Dict[str, str]] = [] + criteria: List[Dict[str, str]] = [] + description: str = "" + env_vars: List[EnvVar] = [ + EnvVar(name="PATRONUS_API_KEY", description="API key for Patronus evaluation services", required=True), + ] + + def __init__(self, **kwargs: Any): + super().__init__(**kwargs) + temp_evaluators, temp_criteria = self._init_run() + self.evaluators = temp_evaluators + self.criteria = temp_criteria + self.description = self._generate_description() + warnings.warn( + "You are allowing the agent to select the best evaluator and criteria when you use the `PatronusEvalTool`. If this is not intended then please use `PatronusPredefinedCriteriaEvalTool` instead." + ) + + def _init_run(self): + evaluators_set = json.loads( + requests.get( + "https://api.patronus.ai/v1/evaluators", + headers={ + "accept": "application/json", + "X-API-KEY": os.environ["PATRONUS_API_KEY"], + }, + ).text + )["evaluators"] + ids, evaluators = set(), [] + for ev in evaluators_set: + if not ev["deprecated"] and ev["id"] not in ids: + evaluators.append( + { + "id": ev["id"], + "name": ev["name"], + "description": ev["description"], + "aliases": ev["aliases"], + } + ) + ids.add(ev["id"]) + + criteria_set = json.loads( + requests.get( + "https://api.patronus.ai/v1/evaluator-criteria", + headers={ + "accept": "application/json", + "X-API-KEY": os.environ["PATRONUS_API_KEY"], + }, + ).text + )["evaluator_criteria"] + criteria = [] + for cr in criteria_set: + if cr["config"].get("pass_criteria", None): + if cr["config"].get("rubric", None): + criteria.append( + { + "evaluator": cr["evaluator_family"], + "name": cr["name"], + "pass_criteria": cr["config"]["pass_criteria"], + "rubric": cr["config"]["rubric"], + } + ) + else: + criteria.append( + { + "evaluator": cr["evaluator_family"], + "name": cr["name"], + "pass_criteria": cr["config"]["pass_criteria"], + } + ) + elif cr["description"]: + criteria.append( + { + "evaluator": cr["evaluator_family"], + "name": cr["name"], + "description": cr["description"], + } + ) + + return evaluators, criteria + + def _generate_description(self) -> str: + criteria = "\n".join([json.dumps(i) for i in self.criteria]) + return f"""This tool calls the Patronus Evaluation API that takes the following arguments: + 1. evaluated_model_input: str: The agent's task description in simple text + 2. evaluated_model_output: str: The agent's output of the task + 3. evaluated_model_retrieved_context: str: The agent's context + 4. evaluators: This is a list of dictionaries containing one of the following evaluators and the corresponding criteria. An example input for this field: [{{"evaluator": "Judge", "criteria": "patronus:is-code"}}] + + Evaluators: + {criteria} + + You must ONLY choose the most appropriate evaluator and criteria based on the "pass_criteria" or "description" fields for your evaluation task and nothing from outside of the options present.""" + + def _run( + self, + evaluated_model_input: Optional[str], + evaluated_model_output: Optional[str], + evaluated_model_retrieved_context: Optional[str], + evaluators: List[Dict[str, str]], + ) -> Any: + # Assert correct format of evaluators + evals = [] + for ev in evaluators: + evals.append( + { + "evaluator": ev["evaluator"].lower(), + "criteria": ev["name"] if "name" in ev else ev["criteria"], + } + ) + + data = { + "evaluated_model_input": evaluated_model_input, + "evaluated_model_output": evaluated_model_output, + "evaluated_model_retrieved_context": evaluated_model_retrieved_context, + "evaluators": evals, + } + + headers = { + "X-API-KEY": os.getenv("PATRONUS_API_KEY"), + "accept": "application/json", + "content-type": "application/json", + } + + response = requests.post( + self.evaluate_url, headers=headers, data=json.dumps(data) + ) + if response.status_code != 200: + raise Exception( + f"Failed to evaluate model input and output. Response status code: {response.status_code}. Reason: {response.text}" + ) + + return response.json() diff --git a/crewai_tools/tools/patronus_eval_tool/patronus_local_evaluator_tool.py b/crewai_tools/tools/patronus_eval_tool/patronus_local_evaluator_tool.py new file mode 100644 index 000000000..30b78a3c4 --- /dev/null +++ b/crewai_tools/tools/patronus_eval_tool/patronus_local_evaluator_tool.py @@ -0,0 +1,120 @@ +from typing import TYPE_CHECKING, Any, Type, List + +from crewai.tools import BaseTool +from pydantic import BaseModel, ConfigDict, Field + +if TYPE_CHECKING: + from patronus import Client, EvaluationResult + +try: + import patronus + + PYPATRONUS_AVAILABLE = True +except ImportError: + PYPATRONUS_AVAILABLE = False + + +class FixedLocalEvaluatorToolSchema(BaseModel): + evaluated_model_input: str = Field( + ..., description="The agent's task description in simple text" + ) + evaluated_model_output: str = Field( + ..., description="The agent's output of the task" + ) + evaluated_model_retrieved_context: str = Field( + ..., description="The agent's context" + ) + evaluated_model_gold_answer: str = Field( + ..., description="The agent's gold answer only if available" + ) + evaluator: str = Field(..., description="The registered local evaluator") + + +class PatronusLocalEvaluatorTool(BaseTool): + name: str = "Patronus Local Evaluator Tool" + description: str = ( + "This tool is used to evaluate the model input and output using custom function evaluators." + ) + args_schema: Type[BaseModel] = FixedLocalEvaluatorToolSchema + client: "Client" = None + evaluator: str + evaluated_model_gold_answer: str + + model_config = ConfigDict(arbitrary_types_allowed=True) + package_dependencies: List[str] = ["patronus"] + + def __init__( + self, + patronus_client: "Client" = None, + evaluator: str = "", + evaluated_model_gold_answer: str = "", + **kwargs: Any, + ): + super().__init__(**kwargs) + self.evaluator = evaluator + self.evaluated_model_gold_answer = evaluated_model_gold_answer + self._initialize_patronus(patronus_client) + + def _initialize_patronus(self, patronus_client: "Client") -> None: + try: + if PYPATRONUS_AVAILABLE: + self.client = patronus_client + self._generate_description() + print( + f"Updating evaluator and gold_answer to: {self.evaluator}, {self.evaluated_model_gold_answer}" + ) + else: + raise ImportError + except ImportError: + import click + + if click.confirm( + "You are missing the 'patronus' package. Would you like to install it?" + ): + import subprocess + + try: + subprocess.run(["uv", "add", "patronus"], check=True) + self.client = patronus_client + self._generate_description() + print( + f"Updating evaluator and gold_answer to: {self.evaluator}, {self.evaluated_model_gold_answer}" + ) + except subprocess.CalledProcessError: + raise ImportError("Failed to install 'patronus' package") + else: + raise ImportError( + "`patronus` package not found, please run `uv add patronus`" + ) + + def _run( + self, + **kwargs: Any, + ) -> Any: + evaluated_model_input = kwargs.get("evaluated_model_input") + evaluated_model_output = kwargs.get("evaluated_model_output") + evaluated_model_retrieved_context = kwargs.get( + "evaluated_model_retrieved_context" + ) + evaluated_model_gold_answer = self.evaluated_model_gold_answer + evaluator = self.evaluator + + result: "EvaluationResult" = self.client.evaluate( + evaluator=evaluator, + evaluated_model_input=evaluated_model_input, + evaluated_model_output=evaluated_model_output, + evaluated_model_retrieved_context=evaluated_model_retrieved_context, + evaluated_model_gold_answer=evaluated_model_gold_answer, + tags={}, # Optional metadata, supports arbitrary key-value pairs + ) + output = f"Evaluation result: {result.pass_}, Explanation: {result.explanation}" + return output + + +try: + # Only rebuild if the class hasn't been initialized yet + if not hasattr(PatronusLocalEvaluatorTool, "_model_rebuilt"): + PatronusLocalEvaluatorTool.model_rebuild() + PatronusLocalEvaluatorTool._model_rebuilt = True +except Exception: + pass diff --git a/crewai_tools/tools/patronus_eval_tool/patronus_predefined_criteria_eval_tool.py b/crewai_tools/tools/patronus_eval_tool/patronus_predefined_criteria_eval_tool.py new file mode 100644 index 000000000..cf906586d --- /dev/null +++ b/crewai_tools/tools/patronus_eval_tool/patronus_predefined_criteria_eval_tool.py @@ -0,0 +1,104 @@ +import json +import os +from typing import Any, Dict, List, Type + +import requests +from crewai.tools import BaseTool +from pydantic import BaseModel, Field + + +class FixedBaseToolSchema(BaseModel): + evaluated_model_input: Dict = Field( + ..., description="The agent's task description in simple text" + ) + evaluated_model_output: Dict = Field( + ..., description="The agent's output of the task" + ) + evaluated_model_retrieved_context: Dict = Field( + ..., description="The agent's context" + ) + evaluated_model_gold_answer: Dict = Field( + ..., description="The agent's gold answer only if available" + ) + evaluators: List[Dict[str, str]] = Field( + ..., + description="List of dictionaries containing the evaluator and criteria to evaluate the model input and output. An example input for this field: [{'evaluator': '[evaluator-from-user]', 'criteria': '[criteria-from-user]'}]", + ) + + +class PatronusPredefinedCriteriaEvalTool(BaseTool): + """ + PatronusEvalTool is a tool to automatically evaluate and score agent interactions. + + Results are logged to the Patronus platform at app.patronus.ai + """ + + name: str = "Call Patronus API tool for evaluation of model inputs and outputs" + description: str = """This tool calls the Patronus Evaluation API that takes the following arguments:""" + evaluate_url: str = "https://api.patronus.ai/v1/evaluate" + args_schema: Type[BaseModel] = FixedBaseToolSchema + evaluators: List[Dict[str, str]] = [] + + def __init__(self, evaluators: List[Dict[str, str]], **kwargs: Any): + super().__init__(**kwargs) + if evaluators: + self.evaluators = evaluators + self.description = f"This tool calls the Patronus Evaluation API that takes an additional argument in addition to the following new argument:\n evaluators={evaluators}" + self._generate_description() + print(f"Updating judge criteria to: {self.evaluators}") + + def _run( + self, + **kwargs: Any, + ) -> Any: + evaluated_model_input = kwargs.get("evaluated_model_input") + evaluated_model_output = kwargs.get("evaluated_model_output") + evaluated_model_retrieved_context = kwargs.get( + "evaluated_model_retrieved_context" + ) + evaluated_model_gold_answer = kwargs.get("evaluated_model_gold_answer") + evaluators = self.evaluators + + headers = { + "X-API-KEY": os.getenv("PATRONUS_API_KEY"), + "accept": "application/json", + "content-type": "application/json", + } + + data = { + "evaluated_model_input": ( + evaluated_model_input + if isinstance(evaluated_model_input, str) + else evaluated_model_input.get("description") + ), + "evaluated_model_output": ( + evaluated_model_output + if isinstance(evaluated_model_output, str) + else evaluated_model_output.get("description") + ), + "evaluated_model_retrieved_context": ( + evaluated_model_retrieved_context + if isinstance(evaluated_model_retrieved_context, str) + else evaluated_model_retrieved_context.get("description") + ), + "evaluated_model_gold_answer": ( + evaluated_model_gold_answer + if isinstance(evaluated_model_gold_answer, str) + else evaluated_model_gold_answer.get("description") + ), + "evaluators": ( + evaluators + if isinstance(evaluators, list) + else evaluators.get("description") + ), + } + + response = requests.post( + self.evaluate_url, headers=headers, data=json.dumps(data) + ) + if response.status_code != 200: + raise Exception( + f"Failed to evaluate model input and output. Status code: {response.status_code}. Reason: {response.text}" + ) + + return response.json() diff --git a/crewai_tools/tools/pdf_search_tool/README.md b/crewai_tools/tools/pdf_search_tool/README.md new file mode 100644 index 000000000..a4bf5d8ed --- /dev/null +++ b/crewai_tools/tools/pdf_search_tool/README.md @@ -0,0 +1,57 @@ +# PDFSearchTool + +## Description +The PDFSearchTool is a RAG tool designed for semantic searches within PDF content. It allows for inputting a search query and a PDF document, leveraging advanced search techniques to find relevant content efficiently. This capability makes it especially useful for extracting specific information from large PDF files quickly. + +## Installation +To get started with the PDFSearchTool, first, ensure the crewai_tools package is installed with the following command: + +```shell +pip install 'crewai[tools]' +``` + +## Example +Here's how to use the PDFSearchTool to search within a PDF document: + +```python +from crewai_tools import PDFSearchTool + +# Initialize the tool allowing for any PDF content search if the path is provided during execution +tool = PDFSearchTool() + +# OR + +# Initialize the tool with a specific PDF path for exclusive search within that document +tool = PDFSearchTool(pdf='path/to/your/document.pdf') +``` + +## Arguments +- `pdf`: **Optinal** The PDF path for the search. Can be provided at initialization or within the `run` method's arguments. If provided at initialization, the tool confines its search to the specified document. + +## Custom model and embeddings + +By default, the tool uses OpenAI for both embeddings and summarization. To customize the model, you can use a config dictionary as follows: + +```python +tool = PDFSearchTool( + config=dict( + llm=dict( + provider="ollama", # or google, openai, anthropic, llama2, ... + config=dict( + model="llama2", + # temperature=0.5, + # top_p=1, + # stream=true, + ), + ), + embedder=dict( + provider="google", + config=dict( + model="models/embedding-001", + task_type="retrieval_document", + # title="Embeddings", + ), + ), + ) +) +``` diff --git a/crewai_tools/tools/pdf_search_tool/pdf_search_tool.py b/crewai_tools/tools/pdf_search_tool/pdf_search_tool.py new file mode 100644 index 000000000..96f141c17 --- /dev/null +++ b/crewai_tools/tools/pdf_search_tool/pdf_search_tool.py @@ -0,0 +1,55 @@ +from typing import Optional, Type + +from pydantic import BaseModel, Field + +try: + from embedchain.models.data_type import DataType + EMBEDCHAIN_AVAILABLE = True +except ImportError: + EMBEDCHAIN_AVAILABLE = False + +from ..rag.rag_tool import RagTool + + +class FixedPDFSearchToolSchema(BaseModel): + """Input for PDFSearchTool.""" + + query: str = Field( + ..., description="Mandatory query you want to use to search the PDF's content" + ) + + +class PDFSearchToolSchema(FixedPDFSearchToolSchema): + """Input for PDFSearchTool.""" + + pdf: str = Field(..., description="File path or URL of a PDF file to be searched") + + +class PDFSearchTool(RagTool): + name: str = "Search a PDF's content" + description: str = ( + "A tool that can be used to semantic search a query from a PDF's content." + ) + args_schema: Type[BaseModel] = PDFSearchToolSchema + + def __init__(self, pdf: Optional[str] = None, **kwargs): + super().__init__(**kwargs) + if pdf is not None: + self.add(pdf) + self.description = f"A tool that can be used to semantic search a query the {pdf} PDF's content." + self.args_schema = FixedPDFSearchToolSchema + self._generate_description() + + def add(self, pdf: str) -> None: + if not EMBEDCHAIN_AVAILABLE: + raise ImportError("embedchain is not installed. Please install it with `pip install crewai-tools[embedchain]`") + super().add(pdf, data_type=DataType.PDF_FILE) + + def _run( + self, + query: str, + pdf: Optional[str] = None, + ) -> str: + if pdf is not None: + self.add(pdf) + return super()._run(query=query) diff --git a/crewai_tools/tools/pdf_text_writing_tool/pdf_text_writing_tool.py b/crewai_tools/tools/pdf_text_writing_tool/pdf_text_writing_tool.py new file mode 100644 index 000000000..1d8f3ffd8 --- /dev/null +++ b/crewai_tools/tools/pdf_text_writing_tool/pdf_text_writing_tool.py @@ -0,0 +1,90 @@ +from pathlib import Path +from typing import Optional, Type + +from pydantic import BaseModel, Field +from pypdf import ContentStream, Font, NameObject, PageObject, PdfReader, PdfWriter + +from crewai_tools.tools.rag.rag_tool import RagTool + + +class PDFTextWritingToolSchema(BaseModel): + """Input schema for PDFTextWritingTool.""" + + pdf_path: str = Field(..., description="Path to the PDF file to modify") + text: str = Field(..., description="Text to add to the PDF") + position: tuple = Field( + ..., description="Tuple of (x, y) coordinates for text placement" + ) + font_size: int = Field(default=12, description="Font size of the text") + font_color: str = Field( + default="0 0 0 rg", description="RGB color code for the text" + ) + font_name: Optional[str] = Field( + default="F1", description="Font name for standard fonts" + ) + font_file: Optional[str] = Field( + None, description="Path to a .ttf font file for custom font usage" + ) + page_number: int = Field(default=0, description="Page number to add text to") + + +class PDFTextWritingTool(RagTool): + """A tool to add text to specific positions in a PDF, with custom font support.""" + + name: str = "PDF Text Writing Tool" + description: str = ( + "A tool that can write text to a specific position in a PDF document, with optional custom font embedding." + ) + args_schema: Type[BaseModel] = PDFTextWritingToolSchema + + def run( + self, + pdf_path: str, + text: str, + position: tuple, + font_size: int, + font_color: str, + font_name: str = "F1", + font_file: Optional[str] = None, + page_number: int = 0, + ) -> str: + reader = PdfReader(pdf_path) + writer = PdfWriter() + + if page_number >= len(reader.pages): + return "Page number out of range." + + page: PageObject = reader.pages[page_number] + content = ContentStream(page["/Contents"].data, reader) + + if font_file: + # Check if the font file exists + if not Path(font_file).exists(): + return "Font file does not exist." + + # Embed the custom font + font_name = self.embed_font(writer, font_file) + + # Prepare text operation with the custom or standard font + x_position, y_position = position + text_operation = f"BT /{font_name} {font_size} Tf {x_position} {y_position} Td ({text}) Tj ET" + content.operations.append([font_color]) # Set color + content.operations.append([text_operation]) # Add text + + # Replace old content with new content + page[NameObject("/Contents")] = content + writer.add_page(page) + + # Save the new PDF + output_pdf_path = "modified_output.pdf" + with open(output_pdf_path, "wb") as out_file: + writer.write(out_file) + + return f"Text added to {output_pdf_path} successfully." + + def embed_font(self, writer: PdfWriter, font_file: str) -> str: + """Embeds a TTF font into the PDF and returns the font name.""" + with open(font_file, "rb") as file: + font = Font.true_type(file.read()) + font_ref = writer.add_object(font) + return font_ref diff --git a/crewai_tools/tools/pg_search_tool/README.md b/crewai_tools/tools/pg_search_tool/README.md new file mode 100644 index 000000000..e462be803 --- /dev/null +++ b/crewai_tools/tools/pg_search_tool/README.md @@ -0,0 +1,56 @@ +# PGSearchTool + +## Description +This tool is designed to facilitate semantic searches within PostgreSQL database tables. Leveraging the RAG (Retrieve and Generate) technology, the PGSearchTool provides users with an efficient means of querying database table content, specifically tailored for PostgreSQL databases. It simplifies the process of finding relevant data through semantic search queries, making it an invaluable resource for users needing to perform advanced queries on extensive datasets within a PostgreSQL database. + +## Installation +To install the `crewai_tools` package and utilize the PGSearchTool, execute the following command in your terminal: + +```shell +pip install 'crewai[tools]' +``` + +## Example +Below is an example showcasing how to use the PGSearchTool to conduct a semantic search on a table within a PostgreSQL database: + +```python +from crewai_tools import PGSearchTool + +# Initialize the tool with the database URI and the target table name +tool = PGSearchTool(db_uri='postgresql://user:password@localhost:5432/mydatabase', table_name='employees') + +``` + +## Arguments +The PGSearchTool requires the following arguments for its operation: + +- `db_uri`: A string representing the URI of the PostgreSQL database to be queried. This argument is mandatory and must include the necessary authentication details and the location of the database. +- `table_name`: A string specifying the name of the table within the database on which the semantic search will be performed. This argument is mandatory. + +## Custom model and embeddings + +By default, the tool uses OpenAI for both embeddings and summarization. To customize the model, you can use a config dictionary as follows: + +```python +tool = PGSearchTool( + config=dict( + llm=dict( + provider="ollama", # or google, openai, anthropic, llama2, ... + config=dict( + model="llama2", + # temperature=0.5, + # top_p=1, + # stream=true, + ), + ), + embedder=dict( + provider="google", + config=dict( + model="models/embedding-001", + task_type="retrieval_document", + # title="Embeddings", + ), + ), + ) +) +``` diff --git a/crewai_tools/tools/pg_search_tool/pg_search_tool.py b/crewai_tools/tools/pg_search_tool/pg_search_tool.py new file mode 100644 index 000000000..30e294944 --- /dev/null +++ b/crewai_tools/tools/pg_search_tool/pg_search_tool.py @@ -0,0 +1,51 @@ +from typing import Any, Type + +try: + from embedchain.loaders.postgres import PostgresLoader + EMBEDCHAIN_AVAILABLE = True +except ImportError: + EMBEDCHAIN_AVAILABLE = False + +from pydantic import BaseModel, Field + +from ..rag.rag_tool import RagTool + + +class PGSearchToolSchema(BaseModel): + """Input for PGSearchTool.""" + + search_query: str = Field( + ..., + description="Mandatory semantic search query you want to use to search the database's content", + ) + + +class PGSearchTool(RagTool): + name: str = "Search a database's table content" + description: str = "A tool that can be used to semantic search a query from a database table's content." + args_schema: Type[BaseModel] = PGSearchToolSchema + db_uri: str = Field(..., description="Mandatory database URI") + + def __init__(self, table_name: str, **kwargs): + if not EMBEDCHAIN_AVAILABLE: + raise ImportError("embedchain is not installed. Please install it with `pip install crewai-tools[embedchain]`") + super().__init__(**kwargs) + kwargs["data_type"] = "postgres" + kwargs["loader"] = PostgresLoader(config=dict(url=self.db_uri)) + self.add(table_name) + self.description = f"A tool that can be used to semantic search a query the {table_name} database table's content." + self._generate_description() + + def add( + self, + table_name: str, + **kwargs: Any, + ) -> None: + super().add(f"SELECT * FROM {table_name};", **kwargs) + + def _run( + self, + search_query: str, + **kwargs: Any, + ) -> Any: + return super()._run(query=search_query, **kwargs) diff --git a/crewai_tools/tools/qdrant_vector_search_tool/README.md b/crewai_tools/tools/qdrant_vector_search_tool/README.md new file mode 100644 index 000000000..26ad9a15f --- /dev/null +++ b/crewai_tools/tools/qdrant_vector_search_tool/README.md @@ -0,0 +1,49 @@ +# QdrantVectorSearchTool + +## Description + +This tool is specifically crafted for conducting semantic searches within docs within a Qdrant vector database. Use this tool to find semantically similar docs to a given query. + +Qdrant is a vector database that is used to store and query vector embeddings. You can follow their docs here: https://qdrant.tech/documentation/ + +## Installation + +Install the crewai_tools package by executing the following command in your terminal: + +```shell +uv pip install 'crewai[tools] qdrant-client openai' +``` + +## Example + +To utilize the QdrantVectorSearchTool for different use cases, follow these examples: Default model is openai. + +```python +from crewai_tools import QdrantVectorSearchTool + +# To enable the tool to search any website the agent comes across or learns about during its operation +tool = QdrantVectorSearchTool( + collection_name="example_collections", + limit=3, + qdrant_url="https://your-qdrant-cluster-url.com", + qdrant_api_key="your-qdrant-api-key", # (optional) +) + + +# Adding the tool to an agent +rag_agent = Agent( + name="rag_agent", + role="You are a helpful assistant that can answer questions with the help of the QdrantVectorSearchTool. Retrieve the most relevant docs from the Qdrant database.", + llm="gpt-4o-mini", + tools=[tool], +) +``` + +## Arguments + +- `collection_name` : The name of the collection to search within. (Required) +- `qdrant_url` : The URL of the Qdrant cluster. (Required) +- `qdrant_api_key` : The API key for the Qdrant cluster. (Optional) +- `limit` : The number of results to return. (Optional) +- `vectorizer` : The vectorizer to use. (Optional) + diff --git a/crewai_tools/tools/qdrant_vector_search_tool/qdrant_search_tool.py b/crewai_tools/tools/qdrant_vector_search_tool/qdrant_search_tool.py new file mode 100644 index 000000000..73e373ae8 --- /dev/null +++ b/crewai_tools/tools/qdrant_vector_search_tool/qdrant_search_tool.py @@ -0,0 +1,183 @@ +import json +import os +from typing import Any, Callable, Optional, Type, List + + +try: + from qdrant_client import QdrantClient + from qdrant_client.http.models import Filter, FieldCondition, MatchValue + + QDRANT_AVAILABLE = True +except ImportError: + QDRANT_AVAILABLE = False + QdrantClient = Any # type placeholder + Filter = Any + FieldCondition = Any + MatchValue = Any + +from crewai.tools import BaseTool +from pydantic import BaseModel, Field + + +class QdrantToolSchema(BaseModel): + """Input for QdrantTool.""" + + query: str = Field( + ..., + description="The query to search retrieve relevant information from the Qdrant database. Pass only the query, not the question.", + ) + filter_by: Optional[str] = Field( + default=None, + description="Filter by properties. Pass only the properties, not the question.", + ) + filter_value: Optional[str] = Field( + default=None, + description="Filter by value. Pass only the value, not the question.", + ) + + +class QdrantVectorSearchTool(BaseTool): + """Tool to query and filter results from a Qdrant database. + + This tool enables vector similarity search on internal documents stored in Qdrant, + with optional filtering capabilities. + + Attributes: + client: Configured QdrantClient instance + collection_name: Name of the Qdrant collection to search + limit: Maximum number of results to return + score_threshold: Minimum similarity score threshold + qdrant_url: Qdrant server URL + qdrant_api_key: Authentication key for Qdrant + """ + + model_config = {"arbitrary_types_allowed": True} + client: QdrantClient = None + name: str = "QdrantVectorSearchTool" + description: str = "A tool to search the Qdrant database for relevant information on internal documents." + args_schema: Type[BaseModel] = QdrantToolSchema + query: Optional[str] = None + filter_by: Optional[str] = None + filter_value: Optional[str] = None + collection_name: Optional[str] = None + limit: Optional[int] = Field(default=3) + score_threshold: float = Field(default=0.35) + qdrant_url: str = Field( + ..., + description="The URL of the Qdrant server", + ) + qdrant_api_key: Optional[str] = Field( + default=None, + description="The API key for the Qdrant server", + ) + custom_embedding_fn: Optional[Callable] = Field( + default=None, + description="A custom embedding function to use for vectorization. If not provided, the default model will be used.", + ) + package_dependencies: List[str] = ["qdrant-client"] + + def __init__(self, **kwargs): + super().__init__(**kwargs) + if QDRANT_AVAILABLE: + self.client = QdrantClient( + url=self.qdrant_url, + api_key=self.qdrant_api_key if self.qdrant_api_key else None, + ) + else: + import click + + if click.confirm( + "The 'qdrant-client' package is required to use the QdrantVectorSearchTool. " + "Would you like to install it?" + ): + import subprocess + + subprocess.run(["uv", "add", "qdrant-client"], check=True) + else: + raise ImportError( + "The 'qdrant-client' package is required to use the QdrantVectorSearchTool. " + "Please install it with: uv add qdrant-client" + ) + + def _run( + self, + query: str, + filter_by: Optional[str] = None, + filter_value: Optional[str] = None, + ) -> str: + """Execute vector similarity search on Qdrant. + + Args: + query: Search query to vectorize and match + filter_by: Optional metadata field to filter on + filter_value: Optional value to filter by + + Returns: + JSON string containing search results with metadata and scores + + Raises: + ImportError: If qdrant-client is not installed + ValueError: If Qdrant credentials are missing + """ + + if not self.qdrant_url: + raise ValueError("QDRANT_URL is not set") + + # Create filter if filter parameters are provided + search_filter = None + if filter_by and filter_value: + search_filter = Filter( + must=[ + FieldCondition(key=filter_by, match=MatchValue(value=filter_value)) + ] + ) + + # Search in Qdrant using the built-in query method + query_vector = ( + self._vectorize_query(query, embedding_model="text-embedding-3-large") + if not self.custom_embedding_fn + else self.custom_embedding_fn(query) + ) + search_results = self.client.query_points( + collection_name=self.collection_name, + query=query_vector, + query_filter=search_filter, + limit=self.limit, + score_threshold=self.score_threshold, + ) + + # Format results similar to storage implementation + results = [] + # Extract the list of ScoredPoint objects from the tuple + for point in search_results: + result = { + "metadata": point[1][0].payload.get("metadata", {}), + "context": point[1][0].payload.get("text", ""), + "distance": point[1][0].score, + } + results.append(result) + + return json.dumps(results, indent=2) + + def _vectorize_query(self, query: str, embedding_model: str) -> list[float]: + """Default vectorization function with openai. + + Args: + query (str): The query to vectorize + embedding_model (str): The embedding model to use + + Returns: + list[float]: The vectorized query + """ + import openai + + client = openai.Client(api_key=os.getenv("OPENAI_API_KEY")) + embedding = ( + client.embeddings.create( + input=[query], + model=embedding_model, + ) + .data[0] + .embedding + ) + return embedding diff --git a/crewai_tools/tools/rag/README.md b/crewai_tools/tools/rag/README.md new file mode 100644 index 000000000..b432a1a69 --- /dev/null +++ b/crewai_tools/tools/rag/README.md @@ -0,0 +1,61 @@ +# RagTool: A Dynamic Knowledge Base Tool + +RagTool is designed to answer questions by leveraging the power of RAG by leveraging (EmbedChain). It integrates seamlessly with the CrewAI ecosystem, offering a versatile and powerful solution for information retrieval. + +## **Overview** + +RagTool enables users to dynamically query a knowledge base, making it an ideal tool for applications requiring access to a vast array of information. Its flexible design allows for integration with various data sources, including files, directories, web pages, yoututbe videos and custom configurations. + +## **Usage** + +RagTool can be instantiated with data from different sources, including: + +- 📰 PDF file +- 📊 CSV file +- 📃 JSON file +- 📝 Text +- 📁 Directory/ Folder +- 🌐 HTML Web page +- 📽️ Youtube Channel +- 📺 Youtube Video +- 📚 Docs website +- 📝 MDX file +- 📄 DOCX file +- 🧾 XML file +- 📬 Gmail +- 📝 Github +- 🐘 Postgres +- 🐬 MySQL +- 🤖 Slack +- 💬 Discord +- 🗨️ Discourse +- 📝 Substack +- 🐝 Beehiiv +- 💾 Dropbox +- 🖼️ Image +- ⚙️ Custom + +#### **Creating an Instance** + +```python +from crewai_tools.tools.rag_tool import RagTool + +# Example: Loading from a file +rag_tool = RagTool().from_file('path/to/your/file.txt') + +# Example: Loading from a directory +rag_tool = RagTool().from_directory('path/to/your/directory') + +# Example: Loading from a web page +rag_tool = RagTool().from_web_page('https://example.com') +``` + +## **Contribution** + +Contributions to RagTool and the broader CrewAI tools ecosystem are welcome. To contribute, please follow the standard GitHub workflow for forking the repository, making changes, and submitting a pull request. + +## **License** + +RagTool is open-source and available under the MIT license. + +Thank you for considering RagTool for your knowledge base needs. Your contributions and feedback are invaluable to making RagTool even better. diff --git a/crewai_tools/tools/rag/__init__.py b/crewai_tools/tools/rag/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/crewai_tools/tools/rag/rag_tool.py b/crewai_tools/tools/rag/rag_tool.py new file mode 100644 index 000000000..1a9fad8b8 --- /dev/null +++ b/crewai_tools/tools/rag/rag_tool.py @@ -0,0 +1,70 @@ +import portalocker + +from abc import ABC, abstractmethod +from typing import Any +from pydantic import BaseModel, ConfigDict, Field, model_validator + +from crewai.tools import BaseTool + + +class Adapter(BaseModel, ABC): + model_config = ConfigDict(arbitrary_types_allowed=True) + + @abstractmethod + def query(self, question: str) -> str: + """Query the knowledge base with a question and return the answer.""" + + @abstractmethod + def add( + self, + *args: Any, + **kwargs: Any, + ) -> None: + """Add content to the knowledge base.""" + + +class RagTool(BaseTool): + class _AdapterPlaceholder(Adapter): + def query(self, question: str) -> str: + raise NotImplementedError + + def add(self, *args: Any, **kwargs: Any) -> None: + raise NotImplementedError + + name: str = "Knowledge base" + description: str = "A knowledge base that can be used to answer questions." + summarize: bool = False + adapter: Adapter = Field(default_factory=_AdapterPlaceholder) + config: dict[str, Any] | None = None + + @model_validator(mode="after") + def _set_default_adapter(self): + if isinstance(self.adapter, RagTool._AdapterPlaceholder): + try: + from embedchain import App + except ImportError: + raise ImportError("embedchain is not installed. Please install it with `pip install crewai-tools[embedchain]`") + + from crewai_tools.adapters.embedchain_adapter import EmbedchainAdapter + + with portalocker.Lock("crewai-rag-tool.lock", timeout=10): + app = App.from_config(config=self.config) if self.config else App() + + self.adapter = EmbedchainAdapter( + embedchain_app=app, summarize=self.summarize + ) + + return self + + def add( + self, + *args: Any, + **kwargs: Any, + ) -> None: + self.adapter.add(*args, **kwargs) + + def _run( + self, + query: str, + ) -> str: + return f"Relevant Content:\n{self.adapter.query(query)}" diff --git a/crewai_tools/tools/scrape_element_from_website/scrape_element_from_website.py b/crewai_tools/tools/scrape_element_from_website/scrape_element_from_website.py new file mode 100644 index 000000000..61f5d9c8c --- /dev/null +++ b/crewai_tools/tools/scrape_element_from_website/scrape_element_from_website.py @@ -0,0 +1,81 @@ +import os +from typing import Any, Optional, Type + +import requests +from crewai.tools import BaseTool +from pydantic import BaseModel, Field + +try: + from bs4 import BeautifulSoup + BEAUTIFULSOUP_AVAILABLE = True +except ImportError: + BEAUTIFULSOUP_AVAILABLE = False + + +class FixedScrapeElementFromWebsiteToolSchema(BaseModel): + """Input for ScrapeElementFromWebsiteTool.""" + + +class ScrapeElementFromWebsiteToolSchema(FixedScrapeElementFromWebsiteToolSchema): + """Input for ScrapeElementFromWebsiteTool.""" + + website_url: str = Field(..., description="Mandatory website url to read the file") + css_element: str = Field( + ..., + description="Mandatory css reference for element to scrape from the website", + ) + + +class ScrapeElementFromWebsiteTool(BaseTool): + name: str = "Read a website content" + description: str = "A tool that can be used to read a website content." + args_schema: Type[BaseModel] = ScrapeElementFromWebsiteToolSchema + website_url: Optional[str] = None + cookies: Optional[dict] = None + css_element: Optional[str] = None + headers: Optional[dict] = { + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.110 Safari/537.36", + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9", + "Accept-Language": "en-US,en;q=0.9", + "Referer": "https://www.google.com/", + "Connection": "keep-alive", + "Upgrade-Insecure-Requests": "1", + "Accept-Encoding": "gzip, deflate, br", + } + + def __init__( + self, + website_url: Optional[str] = None, + cookies: Optional[dict] = None, + css_element: Optional[str] = None, + **kwargs, + ): + super().__init__(**kwargs) + if website_url is not None: + self.website_url = website_url + self.css_element = css_element + self.description = ( + f"A tool that can be used to read {website_url}'s content." + ) + self.args_schema = FixedScrapeElementFromWebsiteToolSchema + self._generate_description() + if cookies is not None: + self.cookies = {cookies["name"]: os.getenv(cookies["value"])} + + def _run( + self, + **kwargs: Any, + ) -> Any: + if not BEAUTIFULSOUP_AVAILABLE: + raise ImportError("beautifulsoup4 is not installed. Please install it with `pip install crewai-tools[beautifulsoup4]`") + + website_url = kwargs.get("website_url", self.website_url) + css_element = kwargs.get("css_element", self.css_element) + page = requests.get( + website_url, + headers=self.headers, + cookies=self.cookies if self.cookies else {}, + ) + parsed = BeautifulSoup(page.content, "html.parser") + elements = parsed.select(css_element) + return "\n".join([element.get_text() for element in elements]) diff --git a/crewai_tools/tools/scrape_website_tool/README.md b/crewai_tools/tools/scrape_website_tool/README.md new file mode 100644 index 000000000..6a933c355 --- /dev/null +++ b/crewai_tools/tools/scrape_website_tool/README.md @@ -0,0 +1,24 @@ +# ScrapeWebsiteTool + +## Description +A tool designed to extract and read the content of a specified website. It is capable of handling various types of web pages by making HTTP requests and parsing the received HTML content. This tool can be particularly useful for web scraping tasks, data collection, or extracting specific information from websites. + +## Installation +Install the crewai_tools package +```shell +pip install 'crewai[tools]' +``` + +## Example +```python +from crewai_tools import ScrapeWebsiteTool + +# To enable scrapping any website it finds during it's execution +tool = ScrapeWebsiteTool() + +# Initialize the tool with the website URL, so the agent can only scrap the content of the specified website +tool = ScrapeWebsiteTool(website_url='https://www.example.com') +``` + +## Arguments +- `website_url` : Mandatory website URL to read the file. This is the primary input for the tool, specifying which website's content should be scraped and read. \ No newline at end of file diff --git a/crewai_tools/tools/scrape_website_tool/scrape_website_tool.py b/crewai_tools/tools/scrape_website_tool/scrape_website_tool.py new file mode 100644 index 000000000..262e79a69 --- /dev/null +++ b/crewai_tools/tools/scrape_website_tool/scrape_website_tool.py @@ -0,0 +1,79 @@ +import os +import re +from typing import Any, Optional, Type + +import requests +try: + from bs4 import BeautifulSoup + BEAUTIFULSOUP_AVAILABLE = True +except ImportError: + BEAUTIFULSOUP_AVAILABLE = False +from crewai.tools import BaseTool +from pydantic import BaseModel, Field + + +class FixedScrapeWebsiteToolSchema(BaseModel): + """Input for ScrapeWebsiteTool.""" + + +class ScrapeWebsiteToolSchema(FixedScrapeWebsiteToolSchema): + """Input for ScrapeWebsiteTool.""" + + website_url: str = Field(..., description="Mandatory website url to read the file") + + +class ScrapeWebsiteTool(BaseTool): + name: str = "Read website content" + description: str = "A tool that can be used to read a website content." + args_schema: Type[BaseModel] = ScrapeWebsiteToolSchema + website_url: Optional[str] = None + cookies: Optional[dict] = None + headers: Optional[dict] = { + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.110 Safari/537.36", + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9", + "Accept-Language": "en-US,en;q=0.9", + "Referer": "https://www.google.com/", + "Connection": "keep-alive", + "Upgrade-Insecure-Requests": "1", + } + + def __init__( + self, + website_url: Optional[str] = None, + cookies: Optional[dict] = None, + **kwargs, + ): + super().__init__(**kwargs) + if not BEAUTIFULSOUP_AVAILABLE: + raise ImportError("beautifulsoup4 is not installed. Please install it with `pip install crewai-tools[beautifulsoup4]`") + + if website_url is not None: + self.website_url = website_url + self.description = ( + f"A tool that can be used to read {website_url}'s content." + ) + self.args_schema = FixedScrapeWebsiteToolSchema + self._generate_description() + if cookies is not None: + self.cookies = {cookies["name"]: os.getenv(cookies["value"])} + + def _run( + self, + **kwargs: Any, + ) -> Any: + website_url = kwargs.get("website_url", self.website_url) + page = requests.get( + website_url, + timeout=15, + headers=self.headers, + cookies=self.cookies if self.cookies else {}, + ) + + page.encoding = page.apparent_encoding + parsed = BeautifulSoup(page.text, "html.parser") + + text = "The following text is scraped website content:\n\n" + text += parsed.get_text(" ") + text = re.sub("[ \t]+", " ", text) + text = re.sub("\\s+\n\\s+", "\n", text) + return text diff --git a/crewai_tools/tools/scrapegraph_scrape_tool/README.md b/crewai_tools/tools/scrapegraph_scrape_tool/README.md new file mode 100644 index 000000000..e006c0ff9 --- /dev/null +++ b/crewai_tools/tools/scrapegraph_scrape_tool/README.md @@ -0,0 +1,84 @@ +# ScrapegraphScrapeTool + +## Description +A tool that leverages Scrapegraph AI's SmartScraper API to intelligently extract content from websites. This tool provides advanced web scraping capabilities with AI-powered content extraction, making it ideal for targeted data collection and content analysis tasks. + +## Installation +Install the required packages: +```shell +pip install 'crewai[tools]' +``` + +## Example Usage + +### Basic Usage +```python +from crewai_tools import ScrapegraphScrapeTool + +# Basic usage with API key +tool = ScrapegraphScrapeTool(api_key="your_api_key") +result = tool.run( + website_url="https://www.example.com", + user_prompt="Extract the main heading and summary" +) +``` + +### Fixed Website URL +```python +# Initialize with a fixed website URL +tool = ScrapegraphScrapeTool( + website_url="https://www.example.com", + api_key="your_api_key" +) +result = tool.run() +``` + +### Custom Prompt +```python +# With custom prompt +tool = ScrapegraphScrapeTool( + api_key="your_api_key", + user_prompt="Extract all product prices and descriptions" +) +result = tool.run(website_url="https://www.example.com") +``` + +### Error Handling +```python +try: + tool = ScrapegraphScrapeTool(api_key="your_api_key") + result = tool.run( + website_url="https://www.example.com", + user_prompt="Extract the main heading" + ) +except ValueError as e: + print(f"Configuration error: {e}") # Handles invalid URLs or missing API keys +except RuntimeError as e: + print(f"Scraping error: {e}") # Handles API or network errors +``` + +## Arguments +- `website_url`: The URL of the website to scrape (required if not set during initialization) +- `user_prompt`: Custom instructions for content extraction (optional) +- `api_key`: Your Scrapegraph API key (required, can be set via SCRAPEGRAPH_API_KEY environment variable) + +## Environment Variables +- `SCRAPEGRAPH_API_KEY`: Your Scrapegraph API key, you can obtain one [here](https://scrapegraphai.com) + +## Rate Limiting +The Scrapegraph API has rate limits that vary based on your subscription plan. Consider the following best practices: +- Implement appropriate delays between requests when processing multiple URLs +- Handle rate limit errors gracefully in your application +- Check your API plan limits on the Scrapegraph dashboard + +## Error Handling +The tool may raise the following exceptions: +- `ValueError`: When API key is missing or URL format is invalid +- `RuntimeError`: When scraping operation fails (network issues, API errors) +- `RateLimitError`: When API rate limits are exceeded + +## Best Practices +1. Always validate URLs before making requests +2. Implement proper error handling as shown in examples +3. Consider caching results for frequently accessed pages +4. Monitor your API usage through the Scrapegraph dashboard diff --git a/crewai_tools/tools/scrapegraph_scrape_tool/scrapegraph_scrape_tool.py b/crewai_tools/tools/scrapegraph_scrape_tool/scrapegraph_scrape_tool.py new file mode 100644 index 000000000..34f42e52e --- /dev/null +++ b/crewai_tools/tools/scrapegraph_scrape_tool/scrapegraph_scrape_tool.py @@ -0,0 +1,183 @@ +import os +from typing import TYPE_CHECKING, Any, Optional, Type, List +from urllib.parse import urlparse + +from crewai.tools import BaseTool, EnvVar +from pydantic import BaseModel, ConfigDict, Field, field_validator + +# Type checking import +if TYPE_CHECKING: + from scrapegraph_py import Client + + +class ScrapegraphError(Exception): + """Base exception for Scrapegraph-related errors""" + + +class RateLimitError(ScrapegraphError): + """Raised when API rate limits are exceeded""" + + +class FixedScrapegraphScrapeToolSchema(BaseModel): + """Input for ScrapegraphScrapeTool when website_url is fixed.""" + + +class ScrapegraphScrapeToolSchema(FixedScrapegraphScrapeToolSchema): + """Input for ScrapegraphScrapeTool.""" + + website_url: str = Field(..., description="Mandatory website url to scrape") + user_prompt: str = Field( + default="Extract the main content of the webpage", + description="Prompt to guide the extraction of content", + ) + + @field_validator("website_url") + def validate_url(cls, v): + """Validate URL format""" + try: + result = urlparse(v) + if not all([result.scheme, result.netloc]): + raise ValueError + return v + except Exception: + raise ValueError( + "Invalid URL format. URL must include scheme (http/https) and domain" + ) + + +class ScrapegraphScrapeTool(BaseTool): + """ + A tool that uses Scrapegraph AI to intelligently scrape website content. + + Raises: + ValueError: If API key is missing or URL format is invalid + RateLimitError: If API rate limits are exceeded + RuntimeError: If scraping operation fails + """ + + model_config = ConfigDict(arbitrary_types_allowed=True) + + name: str = "Scrapegraph website scraper" + description: str = ( + "A tool that uses Scrapegraph AI to intelligently scrape website content." + ) + args_schema: Type[BaseModel] = ScrapegraphScrapeToolSchema + website_url: Optional[str] = None + user_prompt: Optional[str] = None + api_key: Optional[str] = None + enable_logging: bool = False + _client: Optional["Client"] = None + package_dependencies: List[str] = ["scrapegraph-py"] + env_vars: List[EnvVar] = [ + EnvVar(name="SCRAPEGRAPH_API_KEY", description="API key for Scrapegraph AI services", required=False), + ] + + def __init__( + self, + website_url: Optional[str] = None, + user_prompt: Optional[str] = None, + api_key: Optional[str] = None, + enable_logging: bool = False, + **kwargs, + ): + super().__init__(**kwargs) + try: + from scrapegraph_py import Client + from scrapegraph_py.logger import sgai_logger + + except ImportError: + import click + + if click.confirm( + "You are missing the 'scrapegraph-py' package. Would you like to install it?" + ): + import subprocess + + subprocess.run(["uv", "add", "scrapegraph-py"], check=True) + from scrapegraph_py import Client + from scrapegraph_py.logger import sgai_logger + + else: + raise ImportError( + "`scrapegraph-py` package not found, please run `uv add scrapegraph-py`" + ) + + self.api_key = api_key or os.getenv("SCRAPEGRAPH_API_KEY") + self._client = Client(api_key=self.api_key) + + if not self.api_key: + raise ValueError("Scrapegraph API key is required") + + if website_url is not None: + self._validate_url(website_url) + self.website_url = website_url + self.description = f"A tool that uses Scrapegraph AI to intelligently scrape {website_url}'s content." + self.args_schema = FixedScrapegraphScrapeToolSchema + + if user_prompt is not None: + self.user_prompt = user_prompt + + # Configure logging only if enabled + if self.enable_logging: + sgai_logger.set_logging(level="INFO") + + @staticmethod + def _validate_url(url: str) -> None: + """Validate URL format""" + try: + result = urlparse(url) + if not all([result.scheme, result.netloc]): + raise ValueError + except Exception: + raise ValueError( + "Invalid URL format. URL must include scheme (http/https) and domain" + ) + + def _handle_api_response(self, response: dict) -> str: + """Handle and validate API response""" + if not response: + raise RuntimeError("Empty response from Scrapegraph API") + + if "error" in response: + error_msg = response.get("error", {}).get("message", "Unknown error") + if "rate limit" in error_msg.lower(): + raise RateLimitError(f"Rate limit exceeded: {error_msg}") + raise RuntimeError(f"API error: {error_msg}") + + if "result" not in response: + raise RuntimeError("Invalid response format from Scrapegraph API") + + return response["result"] + + def _run( + self, + **kwargs: Any, + ) -> Any: + website_url = kwargs.get("website_url", self.website_url) + user_prompt = ( + kwargs.get("user_prompt", self.user_prompt) + or "Extract the main content of the webpage" + ) + + if not website_url: + raise ValueError("website_url is required") + + # Validate URL format + self._validate_url(website_url) + + try: + # Make the SmartScraper request + response = self._client.smartscraper( + website_url=website_url, + user_prompt=user_prompt, + ) + + return response + + except RateLimitError: + raise # Re-raise rate limit errors + except Exception as e: + raise RuntimeError(f"Scraping failed: {str(e)}") + finally: + # Always close the client + self._client.close() diff --git a/crewai_tools/tools/scrapfly_scrape_website_tool/README.md b/crewai_tools/tools/scrapfly_scrape_website_tool/README.md new file mode 100644 index 000000000..6ab9c9d52 --- /dev/null +++ b/crewai_tools/tools/scrapfly_scrape_website_tool/README.md @@ -0,0 +1,57 @@ +# ScrapflyScrapeWebsiteTool + +## Description +[ScrapFly](https://scrapfly.io/) is a web scraping API with headless browser capabilities, proxies, and anti-bot bypass. It allows for extracting web page data into accessible LLM markdown or text. + +## Setup and Installation +1. **Install ScrapFly Python SDK**: Install `scrapfly-sdk` Python package is installed to use the ScrapFly Web Loader. Install it via pip with the following command: + + ```bash + pip install scrapfly-sdk + ``` + +2. **API Key**: Register for free from [scrapfly.io/register](https://www.scrapfly.io/register/) to obtain your API key. + +## Example Usage + +Utilize the ScrapflyScrapeWebsiteTool as follows to retrieve a web page data as text, markdown (LLM accissible) or HTML: + +```python +from crewai_tools import ScrapflyScrapeWebsiteTool + +tool = ScrapflyScrapeWebsiteTool( + api_key="Your ScrapFly API key" +) + +result = tool._run( + url="https://web-scraping.dev/products", + scrape_format="markdown", + ignore_scrape_failures=True +) +``` + +## Additional Arguments +The ScrapflyScrapeWebsiteTool also allows passigng ScrapeConfig object for customizing the scrape request. See the [API params documentation](https://scrapfly.io/docs/scrape-api/getting-started) for the full feature details and their API params: +```python +from crewai_tools import ScrapflyScrapeWebsiteTool + +tool = ScrapflyScrapeWebsiteTool( + api_key="Your ScrapFly API key" +) + +scrapfly_scrape_config = { + "asp": True, # Bypass scraping blocking and solutions, like Cloudflare + "render_js": True, # Enable JavaScript rendering with a cloud headless browser + "proxy_pool": "public_residential_pool", # Select a proxy pool (datacenter or residnetial) + "country": "us", # Select a proxy location + "auto_scroll": True, # Auto scroll the page + "js": "" # Execute custom JavaScript code by the headless browser +} + +result = tool._run( + url="https://web-scraping.dev/products", + scrape_format="markdown", + ignore_scrape_failures=True, + scrape_config=scrapfly_scrape_config +) +``` \ No newline at end of file diff --git a/crewai_tools/tools/scrapfly_scrape_website_tool/scrapfly_scrape_website_tool.py b/crewai_tools/tools/scrapfly_scrape_website_tool/scrapfly_scrape_website_tool.py new file mode 100644 index 000000000..38bdab2a0 --- /dev/null +++ b/crewai_tools/tools/scrapfly_scrape_website_tool/scrapfly_scrape_website_tool.py @@ -0,0 +1,76 @@ +import os +import logging +from typing import Any, Dict, Literal, Optional, Type, List + +from crewai.tools import BaseTool, EnvVar +from pydantic import BaseModel, Field + +logger = logging.getLogger(__file__) + + +class ScrapflyScrapeWebsiteToolSchema(BaseModel): + url: str = Field(description="Webpage URL") + scrape_format: Optional[Literal["raw", "markdown", "text"]] = Field( + default="markdown", description="Webpage extraction format" + ) + scrape_config: Optional[Dict[str, Any]] = Field( + default=None, description="Scrapfly request scrape config" + ) + ignore_scrape_failures: Optional[bool] = Field( + default=None, description="whether to ignore failures" + ) + + +class ScrapflyScrapeWebsiteTool(BaseTool): + name: str = "Scrapfly web scraping API tool" + description: str = ( + "Scrape a webpage url using Scrapfly and return its content as markdown or text" + ) + args_schema: Type[BaseModel] = ScrapflyScrapeWebsiteToolSchema + api_key: str = None + scrapfly: Optional[Any] = None + package_dependencies: List[str] = ["scrapfly-sdk"] + env_vars: List[EnvVar] = [ + EnvVar(name="SCRAPFLY_API_KEY", description="API key for Scrapfly", required=True), + ] + + def __init__(self, api_key: str): + super().__init__() + try: + from scrapfly import ScrapflyClient + except ImportError: + import click + + if click.confirm( + "You are missing the 'scrapfly-sdk' package. Would you like to install it?" + ): + import subprocess + + subprocess.run(["uv", "add", "scrapfly-sdk"], check=True) + else: + raise ImportError( + "`scrapfly-sdk` package not found, please run `uv add scrapfly-sdk`" + ) + self.scrapfly = ScrapflyClient(key=api_key or os.getenv("SCRAPFLY_API_KEY")) + + def _run( + self, + url: str, + scrape_format: str = "markdown", + scrape_config: Optional[Dict[str, Any]] = None, + ignore_scrape_failures: Optional[bool] = None, + ): + from scrapfly import ScrapeApiResponse, ScrapeConfig + + scrape_config = scrape_config if scrape_config is not None else {} + try: + response: ScrapeApiResponse = self.scrapfly.scrape( + ScrapeConfig(url, format=scrape_format, **scrape_config) + ) + return response.scrape_result["content"] + except Exception as e: + if ignore_scrape_failures: + logger.error(f"Error fetching data from {url}, exception: {e}") + return None + else: + raise e diff --git a/crewai_tools/tools/selenium_scraping_tool/README.md b/crewai_tools/tools/selenium_scraping_tool/README.md new file mode 100644 index 000000000..2d54eb970 --- /dev/null +++ b/crewai_tools/tools/selenium_scraping_tool/README.md @@ -0,0 +1,44 @@ +# SeleniumScrapingTool + +## Description +This tool is designed for efficient web scraping, enabling users to extract content from web pages. It supports targeted scraping by allowing the specification of a CSS selector for desired elements. The flexibility of the tool enables it to be used on any website URL provided by the user, making it a versatile tool for various web scraping needs. + +## Installation +Install the crewai_tools package +``` +pip install 'crewai[tools]' +``` + +## Example +```python +from crewai_tools import SeleniumScrapingTool + +# Example 1: Scrape any website it finds during its execution +tool = SeleniumScrapingTool() + +# Example 2: Scrape the entire webpage +tool = SeleniumScrapingTool(website_url='https://example.com') + +# Example 3: Scrape a specific CSS element from the webpage +tool = SeleniumScrapingTool(website_url='https://example.com', css_element='.main-content') + +# Example 4: Scrape using optional parameters for customized scraping +tool = SeleniumScrapingTool(website_url='https://example.com', css_element='.main-content', cookie={'name': 'user', 'value': 'John Doe'}) + +# Example 5: Scrape content in HTML format +tool = SeleniumScrapingTool(website_url='https://example.com', return_html=True) +result = tool._run() +# Returns HTML content like: ['
Hello World
', ''] + +# Example 6: Scrape content in text format (default) +tool = SeleniumScrapingTool(website_url='https://example.com', return_html=False) +result = tool._run() +# Returns text content like: ['Hello World', 'Copyright 2024'] +``` + +## Arguments +- `website_url`: Mandatory. The URL of the website to scrape. +- `css_element`: Mandatory. The CSS selector for a specific element to scrape from the website. +- `cookie`: Optional. A dictionary containing cookie information. This parameter allows the tool to simulate a session with cookie information, providing access to content that may be restricted to logged-in users. +- `wait_time`: Optional. The number of seconds the tool waits after loading the website and after setting a cookie, before scraping the content. This allows for dynamic content to load properly. +- `return_html`: Optional. If True, the tool returns HTML content. If False, the tool returns text content. diff --git a/crewai_tools/tools/selenium_scraping_tool/selenium_scraping_tool.py b/crewai_tools/tools/selenium_scraping_tool/selenium_scraping_tool.py new file mode 100644 index 000000000..5f7365c8a --- /dev/null +++ b/crewai_tools/tools/selenium_scraping_tool/selenium_scraping_tool.py @@ -0,0 +1,186 @@ +import re +import time +from typing import Any, Optional, Type, List +from urllib.parse import urlparse + +from crewai.tools import BaseTool +from pydantic import BaseModel, Field, field_validator + + +class FixedSeleniumScrapingToolSchema(BaseModel): + """Input for SeleniumScrapingTool.""" + + +class SeleniumScrapingToolSchema(FixedSeleniumScrapingToolSchema): + """Input for SeleniumScrapingTool.""" + + website_url: str = Field( + ..., + description="Mandatory website url to read the file. Must start with http:// or https://", + ) + css_element: str = Field( + ..., + description="Mandatory css reference for element to scrape from the website", + ) + + @field_validator("website_url") + def validate_website_url(cls, v): + if not v: + raise ValueError("Website URL cannot be empty") + + if len(v) > 2048: # Common maximum URL length + raise ValueError("URL is too long (max 2048 characters)") + + if not re.match(r"^https?://", v): + raise ValueError("URL must start with http:// or https://") + + try: + result = urlparse(v) + if not all([result.scheme, result.netloc]): + raise ValueError("Invalid URL format") + except Exception as e: + raise ValueError(f"Invalid URL: {str(e)}") + + if re.search(r"\s", v): + raise ValueError("URL cannot contain whitespace") + + return v + + +class SeleniumScrapingTool(BaseTool): + name: str = "Read a website content" + description: str = "A tool that can be used to read a website content." + args_schema: Type[BaseModel] = SeleniumScrapingToolSchema + website_url: Optional[str] = None + driver: Optional[Any] = None + cookie: Optional[dict] = None + wait_time: Optional[int] = 3 + css_element: Optional[str] = None + return_html: Optional[bool] = False + _by: Optional[Any] = None + package_dependencies: List[str] = ["selenium", "webdriver-manager"] + + def __init__( + self, + website_url: Optional[str] = None, + cookie: Optional[dict] = None, + css_element: Optional[str] = None, + **kwargs, + ): + super().__init__(**kwargs) + try: + from selenium import webdriver + from selenium.webdriver.chrome.options import Options + from selenium.webdriver.common.by import By + except ImportError: + import click + + if click.confirm( + "You are missing the 'selenium' and 'webdriver-manager' packages. Would you like to install it?" + ): + import subprocess + + subprocess.run( + ["uv", "pip", "install", "selenium", "webdriver-manager"], + check=True, + ) + from selenium import webdriver + from selenium.webdriver.chrome.options import Options + from selenium.webdriver.common.by import By + else: + raise ImportError( + "`selenium` and `webdriver-manager` package not found, please run `uv add selenium webdriver-manager`" + ) + + if 'driver' not in kwargs: + if 'options' not in kwargs: + options: Options = Options() + options.add_argument("--headless") + else: + options = kwargs['options'] + self.driver = webdriver.Chrome(options=options) + else: + self.driver = kwargs['driver'] + + self._by = By + if cookie is not None: + self.cookie = cookie + + if css_element is not None: + self.css_element = css_element + + if website_url is not None: + self.website_url = website_url + self.description = ( + f"A tool that can be used to read {website_url}'s content." + ) + self.args_schema = FixedSeleniumScrapingToolSchema + + self._generate_description() + + def _run( + self, + **kwargs: Any, + ) -> Any: + website_url = kwargs.get("website_url", self.website_url) + css_element = kwargs.get("css_element", self.css_element) + return_html = kwargs.get("return_html", self.return_html) + try: + self._make_request(website_url, self.cookie, self.wait_time) + content = self._get_content(css_element, return_html) + return "\n".join(content) + except Exception as e: + return f"Error scraping website: {str(e)}" + finally: + self.driver.close() + + def _get_content(self, css_element, return_html): + content = [] + + if self._is_css_element_empty(css_element): + content.append(self._get_body_content(return_html)) + else: + content.extend(self._get_elements_content(css_element, return_html)) + + return content + + def _is_css_element_empty(self, css_element): + return css_element is None or css_element.strip() == "" + + def _get_body_content(self, return_html): + body_element = self.driver.find_element(self._by.TAG_NAME, "body") + + return ( + body_element.get_attribute("outerHTML") + if return_html + else body_element.text + ) + + def _get_elements_content(self, css_element, return_html): + elements_content = [] + + for element in self.driver.find_elements(self._by.CSS_SELECTOR, css_element): + elements_content.append( + element.get_attribute("outerHTML") if return_html else element.text + ) + + return elements_content + + def _make_request(self, url, cookie, wait_time): + if not url: + raise ValueError("URL cannot be empty") + + # Validate URL format + if not re.match(r"^https?://", url): + raise ValueError("URL must start with http:// or https://") + + self.driver.get(url) + time.sleep(wait_time) + if cookie: + self.driver.add_cookie(cookie) + time.sleep(wait_time) + self.driver.get(url) + time.sleep(wait_time) + + def close(self): + self.driver.close() diff --git a/crewai_tools/tools/serpapi_tool/README.md b/crewai_tools/tools/serpapi_tool/README.md new file mode 100644 index 000000000..d81b851f8 --- /dev/null +++ b/crewai_tools/tools/serpapi_tool/README.md @@ -0,0 +1,32 @@ +# SerpApi Tools + +## Description +[SerpApi](https://serpapi.com/) tools are built for searching information in the internet. It currently supports: +- Google Search +- Google Shopping + +To successfully make use of SerpApi tools, you have to have `SERPAPI_API_KEY` set in the environment. To get the API key, register a free account at [SerpApi](https://serpapi.com/). + +## Installation +To start using the SerpApi Tools, you must first install the `crewai_tools` package. This can be easily done with the following command: + +```shell +pip install 'crewai[tools]' +``` + +## Examples +The following example demonstrates how to initialize the tool + +### Google Search +```python +from crewai_tools import SerpApiGoogleSearchTool + +tool = SerpApiGoogleSearchTool() +``` + +### Google Shopping +```python +from crewai_tools import SerpApiGoogleShoppingTool + +tool = SerpApiGoogleShoppingTool() +``` diff --git a/crewai_tools/tools/serpapi_tool/serpapi_base_tool.py b/crewai_tools/tools/serpapi_tool/serpapi_base_tool.py new file mode 100644 index 000000000..aa73d63d5 --- /dev/null +++ b/crewai_tools/tools/serpapi_tool/serpapi_base_tool.py @@ -0,0 +1,54 @@ +import os +import re +from typing import Any, Optional, Union, List + +from crewai.tools import BaseTool, EnvVar + + +class SerpApiBaseTool(BaseTool): + """Base class for SerpApi functionality with shared capabilities.""" + + package_dependencies: List[str] = ["serpapi"] + env_vars: List[EnvVar] = [ + EnvVar(name="SERPAPI_API_KEY", description="API key for SerpApi searches", required=True), + ] + + client: Optional[Any] = None + + def __init__(self, **kwargs): + super().__init__(**kwargs) + + try: + from serpapi import Client # type: ignore + except ImportError: + import click + + if click.confirm( + "You are missing the 'serpapi' package. Would you like to install it?" + ): + import subprocess + + subprocess.run(["uv", "add", "serpapi"], check=True) + from serpapi import Client + else: + raise ImportError( + "`serpapi` package not found, please install with `uv add serpapi`" + ) + api_key = os.getenv("SERPAPI_API_KEY") + if not api_key: + raise ValueError( + "Missing API key, you can get the key from https://serpapi.com/manage-api-key" + ) + self.client = Client(api_key=api_key) + + def _omit_fields(self, data: Union[dict, list], omit_patterns: list[str]) -> None: + if isinstance(data, dict): + for field in list(data.keys()): + if any(re.compile(p).match(field) for p in omit_patterns): + data.pop(field, None) + else: + if isinstance(data[field], (dict, list)): + self._omit_fields(data[field], omit_patterns) + elif isinstance(data, list): + for item in data: + self._omit_fields(item, omit_patterns) diff --git a/crewai_tools/tools/serpapi_tool/serpapi_google_search_tool.py b/crewai_tools/tools/serpapi_tool/serpapi_google_search_tool.py new file mode 100644 index 000000000..9f11611ab --- /dev/null +++ b/crewai_tools/tools/serpapi_tool/serpapi_google_search_tool.py @@ -0,0 +1,60 @@ +from typing import Any, Optional, Type + +import re +from pydantic import BaseModel, Field, ConfigDict +from .serpapi_base_tool import SerpApiBaseTool + +try: + from serpapi import HTTPError +except ImportError: + HTTPError = Any + + +class SerpApiGoogleSearchToolSchema(BaseModel): + """Input for Google Search.""" + + search_query: str = Field( + ..., description="Mandatory search query you want to use to Google search." + ) + location: Optional[str] = Field( + None, description="Location you want the search to be performed in." + ) + + +class SerpApiGoogleSearchTool(SerpApiBaseTool): + model_config = ConfigDict( + arbitrary_types_allowed=True, validate_assignment=True, frozen=False + ) + name: str = "Google Search" + description: str = ( + "A tool to perform to perform a Google search with a search_query." + ) + args_schema: Type[BaseModel] = SerpApiGoogleSearchToolSchema + + def _run( + self, + **kwargs: Any, + ) -> Any: + try: + results = self.client.search( + { + "q": kwargs.get("search_query"), + "location": kwargs.get("location"), + } + ).as_dict() + + self._omit_fields( + results, + [ + r"search_metadata", + r"search_parameters", + r"serpapi_.+", + r".+_token", + r"displayed_link", + r"pagination", + ], + ) + + return results + except HTTPError as e: + return f"An error occurred: {str(e)}. Some parameters may be invalid." diff --git a/crewai_tools/tools/serpapi_tool/serpapi_google_shopping_tool.py b/crewai_tools/tools/serpapi_tool/serpapi_google_shopping_tool.py new file mode 100644 index 000000000..428bb6b52 --- /dev/null +++ b/crewai_tools/tools/serpapi_tool/serpapi_google_shopping_tool.py @@ -0,0 +1,60 @@ +from typing import Any, Optional, Type + +from pydantic import BaseModel, Field +from .serpapi_base_tool import SerpApiBaseTool +from pydantic import ConfigDict + +try: + from serpapi import HTTPError +except ImportError: + HTTPError = Any + + +class SerpApiGoogleShoppingToolSchema(BaseModel): + """Input for Google Shopping.""" + + search_query: str = Field( + ..., description="Mandatory search query you want to use to Google shopping." + ) + location: Optional[str] = Field( + None, description="Location you want the search to be performed in." + ) + + +class SerpApiGoogleShoppingTool(SerpApiBaseTool): + model_config = ConfigDict( + arbitrary_types_allowed=True, validate_assignment=True, frozen=False + ) + name: str = "Google Shopping" + description: str = ( + "A tool to perform search on Google shopping with a search_query." + ) + args_schema: Type[BaseModel] = SerpApiGoogleShoppingToolSchema + + def _run( + self, + **kwargs: Any, + ) -> Any: + try: + results = self.client.search( + { + "engine": "google_shopping", + "q": kwargs.get("search_query"), + "location": kwargs.get("location"), + } + ).as_dict() + + self._omit_fields( + results, + [ + r"search_metadata", + r"search_parameters", + r"serpapi_.+", + r"filters", + r"pagination", + ], + ) + + return results + except HTTPError as e: + return f"An error occurred: {str(e)}. Some parameters may be invalid." diff --git a/crewai_tools/tools/serper_dev_tool/README.md b/crewai_tools/tools/serper_dev_tool/README.md new file mode 100644 index 000000000..06f1abd56 --- /dev/null +++ b/crewai_tools/tools/serper_dev_tool/README.md @@ -0,0 +1,52 @@ +# SerperDevTool Documentation + +## Description +The SerperDevTool is a powerful search tool that interfaces with the `serper.dev` API to perform internet searches. It supports multiple search types including general search and news search, with features like knowledge graph integration, organic results, "People Also Ask" questions, and related searches. + +## Features +- Multiple search types: 'search' (default) and 'news' +- Knowledge graph integration for enhanced search context +- Organic search results with sitelinks +- "People Also Ask" questions and answers +- Related searches suggestions +- News search with date, source, and image information +- Configurable number of results +- Optional result saving to file + +## Installation +```shell +pip install 'crewai[tools]' +``` + +## Usage +```python +from crewai_tools import SerperDevTool + +# Initialize the tool +tool = SerperDevTool( + n_results=10, # Optional: Number of results to return (default: 10) + save_file=False, # Optional: Save results to file (default: False) + search_type="search", # Optional: Type of search - "search" or "news" (default: "search") + country="us", # Optional: Country for search (default: "") + location="New York", # Optional: Location for search (default: "") + locale="en-US" # Optional: Locale for search (default: "") +) + +# Execute a search +results = tool._run(search_query="your search query") +``` + +## Configuration +1. **API Key Setup**: + - Sign up for an account at `serper.dev` + - Obtain your API key + - Set the environment variable: `SERPER_API_KEY` + +## Response Format +The tool returns structured data including: +- Search parameters +- Knowledge graph data (for general search) +- Organic search results +- "People Also Ask" questions +- Related searches +- News results (for news search type) diff --git a/crewai_tools/tools/serper_dev_tool/serper_dev_tool.py b/crewai_tools/tools/serper_dev_tool/serper_dev_tool.py new file mode 100644 index 000000000..23f15dd92 --- /dev/null +++ b/crewai_tools/tools/serper_dev_tool/serper_dev_tool.py @@ -0,0 +1,247 @@ +import datetime +import json +import logging +import os +from typing import Any, List, Optional, Type + +import requests +from crewai.tools import BaseTool, EnvVar +from pydantic import BaseModel, Field + +logger = logging.getLogger(__name__) + + +def _save_results_to_file(content: str) -> None: + """Saves the search results to a file.""" + try: + filename = f"search_results_{datetime.datetime.now().strftime('%Y-%m-%d_%H-%M-%S')}.txt" + with open(filename, "w") as file: + file.write(content) + logger.info(f"Results saved to {filename}") + except IOError as e: + logger.error(f"Failed to save results to file: {e}") + raise + + +class SerperDevToolSchema(BaseModel): + """Input for SerperDevTool.""" + + search_query: str = Field( + ..., description="Mandatory search query you want to use to search the internet" + ) + + +class SerperDevTool(BaseTool): + name: str = "Search the internet with Serper" + description: str = ( + "A tool that can be used to search the internet with a search_query. " + "Supports different search types: 'search' (default), 'news'" + ) + args_schema: Type[BaseModel] = SerperDevToolSchema + base_url: str = "https://google.serper.dev" + n_results: int = 10 + save_file: bool = False + search_type: str = "search" + country: Optional[str] = "" + location: Optional[str] = "" + locale: Optional[str] = "" + env_vars: List[EnvVar] = [ + EnvVar(name="SERPER_API_KEY", description="API key for Serper", required=True), + ] + + def _get_search_url(self, search_type: str) -> str: + """Get the appropriate endpoint URL based on search type.""" + search_type = search_type.lower() + allowed_search_types = ["search", "news"] + if search_type not in allowed_search_types: + raise ValueError( + f"Invalid search type: {search_type}. Must be one of: {', '.join(allowed_search_types)}" + ) + return f"{self.base_url}/{search_type}" + + def _process_knowledge_graph(self, kg: dict) -> dict: + """Process knowledge graph data from search results.""" + return { + "title": kg.get("title", ""), + "type": kg.get("type", ""), + "website": kg.get("website", ""), + "imageUrl": kg.get("imageUrl", ""), + "description": kg.get("description", ""), + "descriptionSource": kg.get("descriptionSource", ""), + "descriptionLink": kg.get("descriptionLink", ""), + "attributes": kg.get("attributes", {}), + } + + def _process_organic_results(self, organic_results: list) -> list: + """Process organic search results.""" + processed_results = [] + for result in organic_results[: self.n_results]: + try: + result_data = { + "title": result["title"], + "link": result["link"], + "snippet": result.get("snippet", ""), + "position": result.get("position"), + } + + if "sitelinks" in result: + result_data["sitelinks"] = [ + { + "title": sitelink.get("title", ""), + "link": sitelink.get("link", ""), + } + for sitelink in result["sitelinks"] + ] + + processed_results.append(result_data) + except KeyError: + logger.warning(f"Skipping malformed organic result: {result}") + continue + return processed_results + + def _process_people_also_ask(self, paa_results: list) -> list: + """Process 'People Also Ask' results.""" + processed_results = [] + for result in paa_results[: self.n_results]: + try: + result_data = { + "question": result["question"], + "snippet": result.get("snippet", ""), + "title": result.get("title", ""), + "link": result.get("link", ""), + } + processed_results.append(result_data) + except KeyError: + logger.warning(f"Skipping malformed PAA result: {result}") + continue + return processed_results + + def _process_related_searches(self, related_results: list) -> list: + """Process related search results.""" + processed_results = [] + for result in related_results[: self.n_results]: + try: + processed_results.append({"query": result["query"]}) + except KeyError: + logger.warning(f"Skipping malformed related search result: {result}") + continue + return processed_results + + def _process_news_results(self, news_results: list) -> list: + """Process news search results.""" + processed_results = [] + for result in news_results[: self.n_results]: + try: + result_data = { + "title": result["title"], + "link": result["link"], + "snippet": result.get("snippet", ""), + "date": result.get("date", ""), + "source": result.get("source", ""), + "imageUrl": result.get("imageUrl", ""), + } + processed_results.append(result_data) + except KeyError: + logger.warning(f"Skipping malformed news result: {result}") + continue + return processed_results + + def _make_api_request(self, search_query: str, search_type: str) -> dict: + """Make API request to Serper.""" + search_url = self._get_search_url(search_type) + payload = {"q": search_query, "num": self.n_results} + + if self.country != "": + payload["gl"] = self.country + if self.location != "": + payload["location"] = self.location + if self.locale != "": + payload["hl"] = self.locale + + headers = { + "X-API-KEY": os.environ["SERPER_API_KEY"], + "content-type": "application/json", + } + payload = json.dumps(payload) + + response = None + try: + response = requests.post( + search_url, headers=headers, json=json.loads(payload), timeout=10 + ) + response.raise_for_status() + results = response.json() + if not results: + logger.error("Empty response from Serper API") + raise ValueError("Empty response from Serper API") + return results + except requests.exceptions.RequestException as e: + error_msg = f"Error making request to Serper API: {e}" + if response is not None and hasattr(response, "content"): + error_msg += f"\nResponse content: {response.content}" + logger.error(error_msg) + raise + except json.JSONDecodeError as e: + if response is not None and hasattr(response, "content"): + logger.error(f"Error decoding JSON response: {e}") + logger.error(f"Response content: {response.content}") + else: + logger.error( + f"Error decoding JSON response: {e} (No response content available)" + ) + raise + + def _process_search_results(self, results: dict, search_type: str) -> dict: + """Process search results based on search type.""" + formatted_results = {} + + if search_type == "search": + if "knowledgeGraph" in results: + formatted_results["knowledgeGraph"] = self._process_knowledge_graph( + results["knowledgeGraph"] + ) + + if "organic" in results: + formatted_results["organic"] = self._process_organic_results( + results["organic"] + ) + + if "peopleAlsoAsk" in results: + formatted_results["peopleAlsoAsk"] = self._process_people_also_ask( + results["peopleAlsoAsk"] + ) + + if "relatedSearches" in results: + formatted_results["relatedSearches"] = self._process_related_searches( + results["relatedSearches"] + ) + + elif search_type == "news": + if "news" in results: + formatted_results["news"] = self._process_news_results(results["news"]) + + return formatted_results + + def _run(self, **kwargs: Any) -> Any: + """Execute the search operation.""" + search_query = kwargs.get("search_query") or kwargs.get("query") + search_type = kwargs.get("search_type", self.search_type) + save_file = kwargs.get("save_file", self.save_file) + + results = self._make_api_request(search_query, search_type) + + formatted_results = { + "searchParameters": { + "q": search_query, + "type": search_type, + **results.get("searchParameters", {}), + } + } + + formatted_results.update(self._process_search_results(results, search_type)) + formatted_results["credits"] = results.get("credits", 1) + + if save_file: + _save_results_to_file(json.dumps(formatted_results, indent=2)) + + return formatted_results diff --git a/crewai_tools/tools/serper_scrape_website_tool/serper_scrape_website_tool.py b/crewai_tools/tools/serper_scrape_website_tool/serper_scrape_website_tool.py new file mode 100644 index 000000000..cefb431f4 --- /dev/null +++ b/crewai_tools/tools/serper_scrape_website_tool/serper_scrape_website_tool.py @@ -0,0 +1,80 @@ +from crewai.tools import BaseTool, EnvVar +from typing import Type, List +from pydantic import BaseModel, Field +import requests +import json +import os + + +class SerperScrapeWebsiteInput(BaseModel): + """Input schema for SerperScrapeWebsite.""" + url: str = Field(..., description="The URL of the website to scrape") + include_markdown: bool = Field( + default=True, + description="Whether to include markdown formatting in the scraped content" + ) + + +class SerperScrapeWebsiteTool(BaseTool): + name: str = "serper_scrape_website" + description: str = ( + "Scrapes website content using Serper's scraping API. " + "This tool can extract clean, readable content from any website URL, " + "optionally including markdown formatting for better structure." + ) + args_schema: Type[BaseModel] = SerperScrapeWebsiteInput + env_vars: List[EnvVar] = [ + EnvVar(name="SERPER_API_KEY", description="API key for Serper", required=True), + ] + + def _run(self, url: str, include_markdown: bool = True) -> str: + """ + Scrape website content using Serper API. + + Args: + url: The URL to scrape + include_markdown: Whether to include markdown formatting + + Returns: + Scraped website content as a string + """ + try: + # Serper API endpoint + api_url = "https://scrape.serper.dev" + + # Get API key from environment variable for security + api_key = os.getenv('SERPER_API_KEY') + + # Prepare the payload + payload = json.dumps({ + "url": url, + "includeMarkdown": include_markdown + }) + + # Set headers + headers = { + 'X-API-KEY': api_key, + 'Content-Type': 'application/json' + } + + # Make the API request + response = requests.post(api_url, headers=headers, data=payload) + + # Check if request was successful + if response.status_code == 200: + result = response.json() + + # Extract the scraped content + if 'text' in result: + return result['text'] + else: + return f"Successfully scraped {url}, but no text content found in response: {response.text}" + else: + return f"Error scraping {url}: HTTP {response.status_code} - {response.text}" + + except requests.exceptions.RequestException as e: + return f"Network error while scraping {url}: {str(e)}" + except json.JSONDecodeError as e: + return f"Error parsing JSON response while scraping {url}: {str(e)}" + except Exception as e: + return f"Unexpected error while scraping {url}: {str(e)}" diff --git a/crewai_tools/tools/serply_api_tool/README.md b/crewai_tools/tools/serply_api_tool/README.md new file mode 100644 index 000000000..5c6b9395e --- /dev/null +++ b/crewai_tools/tools/serply_api_tool/README.md @@ -0,0 +1,117 @@ +# Serply API Documentation + +## Description +This tool is designed to perform a web/news/scholar search for a specified query from a text's content across the internet. It utilizes the [Serply.io](https://serply.io) API to fetch and display the most relevant search results based on the query provided by the user. + +## Installation + +To incorporate this tool into your project, follow the installation instructions below: +```shell +pip install 'crewai[tools]' +``` + +## Examples + +## Web Search +The following example demonstrates how to initialize the tool and execute a search the web with a given query: + +```python +from crewai_tools import SerplyWebSearchTool + +# Initialize the tool for internet searching capabilities +tool = SerplyWebSearchTool() + +# increase search limits to 100 results +tool = SerplyWebSearchTool(limit=100) + + +# change results language (fr - French) +tool = SerplyWebSearchTool(hl="fr") +``` + +## News Search +The following example demonstrates how to initialize the tool and execute a search news with a given query: + +```python +from crewai_tools import SerplyNewsSearchTool + +# Initialize the tool for internet searching capabilities +tool = SerplyNewsSearchTool() + +# change country news (JP - Japan) +tool = SerplyNewsSearchTool(proxy_location="JP") +``` + +## Scholar Search +The following example demonstrates how to initialize the tool and execute a search scholar articles a given query: + +```python +from crewai_tools import SerplyScholarSearchTool + +# Initialize the tool for internet searching capabilities +tool = SerplyScholarSearchTool() + +# change country news (GB - Great Britain) +tool = SerplyScholarSearchTool(proxy_location="GB") +``` + +## Job Search +The following example demonstrates how to initialize the tool and searching for jobs in the USA: + +```python +from crewai_tools import SerplyJobSearchTool + +# Initialize the tool for internet searching capabilities +tool = SerplyJobSearchTool() +``` + + +## Web Page To Markdown +The following example demonstrates how to initialize the tool and fetch a web page and convert it to markdown: + +```python +from crewai_tools import SerplyWebpageToMarkdownTool + +# Initialize the tool for internet searching capabilities +tool = SerplyWebpageToMarkdownTool() + +# change country make request from (DE - Germany) +tool = SerplyWebpageToMarkdownTool(proxy_location="DE") +``` + +## Combining Multiple Tools + +The following example demonstrates performing a Google search to find relevant articles. Then, convert those articles to markdown format for easier extraction of key points. + +```python +from crewai import Agent +from crewai_tools import SerplyWebSearchTool, SerplyWebpageToMarkdownTool + +search_tool = SerplyWebSearchTool() +convert_to_markdown = SerplyWebpageToMarkdownTool() + +# Creating a senior researcher agent with memory and verbose mode +researcher = Agent( + role='Senior Researcher', + goal='Uncover groundbreaking technologies in {topic}', + verbose=True, + memory=True, + backstory=( + "Driven by curiosity, you're at the forefront of" + "innovation, eager to explore and share knowledge that could change" + "the world." + ), + tools=[search_tool, convert_to_markdown], + allow_delegation=True +) +``` + +## Steps to Get Started +To effectively use the `SerplyApiTool`, follow these steps: + +1. **Package Installation**: Confirm that the `crewai[tools]` package is installed in your Python environment. +2. **API Key Acquisition**: Acquire a `serper.dev` API key by registering for a free account at [Serply.io](https://serply.io). +3. **Environment Configuration**: Store your obtained API key in an environment variable named `SERPLY_API_KEY` to facilitate its use by the tool. + +## Conclusion +By integrating the `SerplyApiTool` into Python projects, users gain the ability to conduct real-time searches, relevant news across the internet directly from their applications. By adhering to the setup and usage guidelines provided, incorporating this tool into projects is streamlined and straightforward. diff --git a/crewai_tools/tools/serply_api_tool/serply_job_search_tool.py b/crewai_tools/tools/serply_api_tool/serply_job_search_tool.py new file mode 100644 index 000000000..9d99fa01b --- /dev/null +++ b/crewai_tools/tools/serply_api_tool/serply_job_search_tool.py @@ -0,0 +1,89 @@ +import os +from typing import Any, List, Optional, Type +from urllib.parse import urlencode + +import requests +from pydantic import BaseModel, Field + +from crewai.tools import EnvVar +from crewai_tools.tools.rag.rag_tool import RagTool + + +class SerplyJobSearchToolSchema(BaseModel): + """Input for Job Search.""" + + search_query: str = Field( + ..., + description="Mandatory search query you want to use to fetch jobs postings.", + ) + + +class SerplyJobSearchTool(RagTool): + name: str = "Job Search" + description: str = ( + "A tool to perform to perform a job search in the US with a search_query." + ) + args_schema: Type[BaseModel] = SerplyJobSearchToolSchema + request_url: str = "https://api.serply.io/v1/job/search/" + proxy_location: Optional[str] = "US" + """ + proxy_location: (str): Where to get jobs, specifically for a specific country results. + - Currently only supports US + """ + headers: Optional[dict] = {} + env_vars: List[EnvVar] = [ + EnvVar(name="SERPLY_API_KEY", description="API key for Serply services", required=True), + ] + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.headers = { + "X-API-KEY": os.environ["SERPLY_API_KEY"], + "User-Agent": "crew-tools", + "X-Proxy-Location": self.proxy_location, + } + + def _run( + self, + query: Optional[str] = None, + search_query: Optional[str] = None, + ) -> str: + query_payload = {} + + if query is not None: + query_payload["q"] = query + elif search_query is not None: + query_payload["q"] = search_query + + # build the url + url = f"{self.request_url}{urlencode(query_payload)}" + + response = requests.request("GET", url, headers=self.headers) + + jobs = response.json().get("jobs", "") + + if not jobs: + return "" + + string = [] + for job in jobs: + try: + string.append( + "\n".join( + [ + f"Position: {job['position']}", + f"Employer: {job['employer']}", + f"Location: {job['location']}", + f"Link: {job['link']}", + f"""Highest: {', '.join([h for h in job['highlights']])}""", + f"Is Remote: {job['is_remote']}", + f"Is Hybrid: {job['is_remote']}", + "---", + ] + ) + ) + except KeyError: + continue + + content = "\n".join(string) + return f"\nSearch results: {content}\n" diff --git a/crewai_tools/tools/serply_api_tool/serply_news_search_tool.py b/crewai_tools/tools/serply_api_tool/serply_news_search_tool.py new file mode 100644 index 000000000..5a2b27798 --- /dev/null +++ b/crewai_tools/tools/serply_api_tool/serply_news_search_tool.py @@ -0,0 +1,89 @@ +import os +from typing import Any, List, Optional, Type +from urllib.parse import urlencode + +import requests +from crewai.tools import BaseTool, EnvVar +from pydantic import BaseModel, Field + + +class SerplyNewsSearchToolSchema(BaseModel): + """Input for Serply News Search.""" + + search_query: str = Field( + ..., description="Mandatory search query you want to use to fetch news articles" + ) + + +class SerplyNewsSearchTool(BaseTool): + name: str = "News Search" + description: str = "A tool to perform News article search with a search_query." + args_schema: Type[BaseModel] = SerplyNewsSearchToolSchema + search_url: str = "https://api.serply.io/v1/news/" + proxy_location: Optional[str] = "US" + headers: Optional[dict] = {} + limit: Optional[int] = 10 + env_vars: List[EnvVar] = [ + EnvVar(name="SERPLY_API_KEY", description="API key for Serply services", required=True), + ] + + def __init__( + self, limit: Optional[int] = 10, proxy_location: Optional[str] = "US", **kwargs + ): + """ + param: limit (int): The maximum number of results to return [10-100, defaults to 10] + proxy_location: (str): Where to get news, specifically for a specific country results. + ['US', 'CA', 'IE', 'GB', 'FR', 'DE', 'SE', 'IN', 'JP', 'KR', 'SG', 'AU', 'BR'] (defaults to US) + """ + super().__init__(**kwargs) + self.limit = limit + self.proxy_location = proxy_location + self.headers = { + "X-API-KEY": os.environ["SERPLY_API_KEY"], + "User-Agent": "crew-tools", + "X-Proxy-Location": proxy_location, + } + + def _run( + self, + **kwargs: Any, + ) -> Any: + # build query parameters + query_payload = {} + + if "query" in kwargs: + query_payload["q"] = kwargs["query"] + elif "search_query" in kwargs: + query_payload["q"] = kwargs["search_query"] + + # build the url + url = f"{self.search_url}{urlencode(query_payload)}" + + response = requests.request("GET", url, headers=self.headers) + results = response.json() + if "entries" in results: + results = results["entries"] + string = [] + for result in results[: self.limit]: + try: + # follow url + r = requests.get(result["link"]) + final_link = r.history[-1].headers["Location"] + string.append( + "\n".join( + [ + f"Title: {result['title']}", + f"Link: {final_link}", + f"Source: {result['source']['title']}", + f"Published: {result['published']}", + "---", + ] + ) + ) + except KeyError: + continue + + content = "\n".join(string) + return f"\nSearch results: {content}\n" + else: + return results diff --git a/crewai_tools/tools/serply_api_tool/serply_scholar_search_tool.py b/crewai_tools/tools/serply_api_tool/serply_scholar_search_tool.py new file mode 100644 index 000000000..c49734c56 --- /dev/null +++ b/crewai_tools/tools/serply_api_tool/serply_scholar_search_tool.py @@ -0,0 +1,93 @@ +import os +from typing import Any, List, Optional, Type +from urllib.parse import urlencode + +import requests +from crewai.tools import BaseTool, EnvVar +from pydantic import BaseModel, Field + + +class SerplyScholarSearchToolSchema(BaseModel): + """Input for Serply Scholar Search.""" + + search_query: str = Field( + ..., + description="Mandatory search query you want to use to fetch scholarly literature", + ) + + +class SerplyScholarSearchTool(BaseTool): + name: str = "Scholar Search" + description: str = ( + "A tool to perform scholarly literature search with a search_query." + ) + args_schema: Type[BaseModel] = SerplyScholarSearchToolSchema + search_url: str = "https://api.serply.io/v1/scholar/" + hl: Optional[str] = "us" + proxy_location: Optional[str] = "US" + headers: Optional[dict] = {} + env_vars: List[EnvVar] = [ + EnvVar(name="SERPLY_API_KEY", description="API key for Serply services", required=True), + ] + + def __init__(self, hl: str = "us", proxy_location: Optional[str] = "US", **kwargs): + """ + param: hl (str): host Language code to display results in + (reference https://developers.google.com/custom-search/docs/xml_results?hl=en#wsInterfaceLanguages) + proxy_location: (str): Specify the proxy location for the search, specifically for a specific country results. + ['US', 'CA', 'IE', 'GB', 'FR', 'DE', 'SE', 'IN', 'JP', 'KR', 'SG', 'AU', 'BR'] (defaults to US) + """ + super().__init__(**kwargs) + self.hl = hl + self.proxy_location = proxy_location + self.headers = { + "X-API-KEY": os.environ["SERPLY_API_KEY"], + "User-Agent": "crew-tools", + "X-Proxy-Location": proxy_location, + } + + def _run( + self, + **kwargs: Any, + ) -> Any: + query_payload = {"hl": self.hl} + + if "query" in kwargs: + query_payload["q"] = kwargs["query"] + elif "search_query" in kwargs: + query_payload["q"] = kwargs["search_query"] + + # build the url + url = f"{self.search_url}{urlencode(query_payload)}" + + response = requests.request("GET", url, headers=self.headers) + articles = response.json().get("articles", "") + + if not articles: + return "" + + string = [] + for article in articles: + try: + if "doc" in article: + link = article["doc"]["link"] + else: + link = article["link"] + authors = [author["name"] for author in article["author"]["authors"]] + string.append( + "\n".join( + [ + f"Title: {article['title']}", + f"Link: {link}", + f"Description: {article['description']}", + f"Cite: {article['cite']}", + f"Authors: {', '.join(authors)}", + "---", + ] + ) + ) + except KeyError: + continue + + content = "\n".join(string) + return f"\nSearch results: {content}\n" diff --git a/crewai_tools/tools/serply_api_tool/serply_web_search_tool.py b/crewai_tools/tools/serply_api_tool/serply_web_search_tool.py new file mode 100644 index 000000000..6801f4065 --- /dev/null +++ b/crewai_tools/tools/serply_api_tool/serply_web_search_tool.py @@ -0,0 +1,104 @@ +import os +from typing import Any, List, Optional, Type +from urllib.parse import urlencode + +import requests +from crewai.tools import BaseTool, EnvVar +from pydantic import BaseModel, Field + + +class SerplyWebSearchToolSchema(BaseModel): + """Input for Serply Web Search.""" + + search_query: str = Field( + ..., description="Mandatory search query you want to use to Google search" + ) + + +class SerplyWebSearchTool(BaseTool): + name: str = "Google Search" + description: str = "A tool to perform Google search with a search_query." + args_schema: Type[BaseModel] = SerplyWebSearchToolSchema + search_url: str = "https://api.serply.io/v1/search/" + hl: Optional[str] = "us" + limit: Optional[int] = 10 + device_type: Optional[str] = "desktop" + proxy_location: Optional[str] = "US" + query_payload: Optional[dict] = {} + headers: Optional[dict] = {} + env_vars: List[EnvVar] = [ + EnvVar(name="SERPLY_API_KEY", description="API key for Serply services", required=True), + ] + + def __init__( + self, + hl: str = "us", + limit: int = 10, + device_type: str = "desktop", + proxy_location: str = "US", + **kwargs, + ): + """ + param: query (str): The query to search for + param: hl (str): host Language code to display results in + (reference https://developers.google.com/custom-search/docs/xml_results?hl=en#wsInterfaceLanguages) + param: limit (int): The maximum number of results to return [10-100, defaults to 10] + param: device_type (str): desktop/mobile results (defaults to desktop) + proxy_location: (str): Where to perform the search, specifically for local/regional results. + ['US', 'CA', 'IE', 'GB', 'FR', 'DE', 'SE', 'IN', 'JP', 'KR', 'SG', 'AU', 'BR'] (defaults to US) + """ + super().__init__(**kwargs) + + self.limit = limit + self.device_type = device_type + self.proxy_location = proxy_location + + # build query parameters + self.query_payload = { + "num": limit, + "gl": proxy_location.upper(), + "hl": hl.lower(), + } + self.headers = { + "X-API-KEY": os.environ["SERPLY_API_KEY"], + "X-User-Agent": device_type, + "User-Agent": "crew-tools", + "X-Proxy-Location": proxy_location, + } + + def _run( + self, + **kwargs: Any, + ) -> Any: + if "query" in kwargs: + self.query_payload["q"] = kwargs["query"] + elif "search_query" in kwargs: + self.query_payload["q"] = kwargs["search_query"] + + # build the url + url = f"{self.search_url}{urlencode(self.query_payload)}" + + response = requests.request("GET", url, headers=self.headers) + results = response.json() + if "results" in results: + results = results["results"] + string = [] + for result in results: + try: + string.append( + "\n".join( + [ + f"Title: {result['title']}", + f"Link: {result['link']}", + f"Description: {result['description'].strip()}", + "---", + ] + ) + ) + except KeyError: + continue + + content = "\n".join(string) + return f"\nSearch results: {content}\n" + else: + return results diff --git a/crewai_tools/tools/serply_api_tool/serply_webpage_to_markdown_tool.py b/crewai_tools/tools/serply_api_tool/serply_webpage_to_markdown_tool.py new file mode 100644 index 000000000..fa2404f75 --- /dev/null +++ b/crewai_tools/tools/serply_api_tool/serply_webpage_to_markdown_tool.py @@ -0,0 +1,54 @@ +import os +from typing import Any, List, Optional, Type + +import requests +from crewai.tools import EnvVar +from pydantic import BaseModel, Field + +from crewai_tools.tools.rag.rag_tool import RagTool + + +class SerplyWebpageToMarkdownToolSchema(BaseModel): + """Input for Serply Search.""" + + url: str = Field( + ..., + description="Mandatory url you want to use to fetch and convert to markdown", + ) + + +class SerplyWebpageToMarkdownTool(RagTool): + name: str = "Webpage to Markdown" + description: str = ( + "A tool to perform convert a webpage to markdown to make it easier for LLMs to understand" + ) + args_schema: Type[BaseModel] = SerplyWebpageToMarkdownToolSchema + request_url: str = "https://api.serply.io/v1/request" + proxy_location: Optional[str] = "US" + headers: Optional[dict] = {} + env_vars: List[EnvVar] = [ + EnvVar(name="SERPLY_API_KEY", description="API key for Serply services", required=True), + ] + + def __init__(self, proxy_location: Optional[str] = "US", **kwargs): + """ + proxy_location: (str): Where to perform the search, specifically for a specific country results. + ['US', 'CA', 'IE', 'GB', 'FR', 'DE', 'SE', 'IN', 'JP', 'KR', 'SG', 'AU', 'BR'] (defaults to US) + """ + super().__init__(**kwargs) + self.proxy_location = proxy_location + self.headers = { + "X-API-KEY": os.environ["SERPLY_API_KEY"], + "User-Agent": "crew-tools", + "X-Proxy-Location": proxy_location, + } + + def _run( + self, + url: str, + ) -> str: + data = {"url": url, "method": "GET", "response_type": "markdown"} + response = requests.request( + "POST", self.request_url, headers=self.headers, json=data + ) + return response.text diff --git a/crewai_tools/tools/singlestore_search_tool/README.md b/crewai_tools/tools/singlestore_search_tool/README.md new file mode 100644 index 000000000..954264683 --- /dev/null +++ b/crewai_tools/tools/singlestore_search_tool/README.md @@ -0,0 +1,299 @@ +# SingleStoreSearchTool + +## Description +The SingleStoreSearchTool is designed to facilitate semantic searches and SQL queries within SingleStore database tables. This tool provides a secure interface for executing SELECT and SHOW queries against SingleStore databases, with built-in connection pooling for optimal performance. It supports various connection methods and allows you to work with specific table subsets within your database. + +## Installation +To install the `crewai_tools` package with SingleStore support, execute the following command: + +```shell +pip install 'crewai[tools]' +``` + +Or install with the SingleStore extra for the latest dependencies: + +```shell +uv sync --extra singlestore +``` + +Or install the required dependencies manually: + +```shell +pip install singlestoredb>=1.12.4 SQLAlchemy>=2.0.40 +``` + +## Features + +- 🔒 **Secure Query Execution**: Only SELECT and SHOW queries are allowed for security +- 🚀 **Connection Pooling**: Built-in connection pooling for optimal performance +- 📊 **Table Subset Support**: Work with specific tables or all tables in the database +- 🔧 **Flexible Configuration**: Multiple connection methods supported +- 🛡️ **SSL/TLS Support**: Comprehensive SSL configuration options +- ⚡ **Efficient Resource Management**: Automatic connection lifecycle management + +## Basic Usage + +### Simple Connection + +```python +from crewai_tools import SingleStoreSearchTool + +# Basic connection using host/user/password +tool = SingleStoreSearchTool( + host='localhost', + user='your_username', + password='your_password', + database='your_database', + port=3306 +) + +# Execute a search query +result = tool._run("SELECT * FROM employees WHERE department = 'Engineering' LIMIT 10") +print(result) +``` + +### Working with Specific Tables + +```python +# Initialize tool for specific tables only +tool = SingleStoreSearchTool( + tables=['employees', 'departments'], # Only work with these tables + host='your_host', + user='your_username', + password='your_password', + database='your_database' +) +``` + +## Complete CrewAI Integration Example + +Here's a complete example showing how to use the SingleStoreSearchTool with CrewAI agents and tasks: + +```python +from crewai import Agent, Task, Crew +from crewai_tools import SingleStoreSearchTool + +# Initialize the SingleStore search tool +singlestore_tool = SingleStoreSearchTool( + tables=["products", "sales", "customers"], # Specify the tables you want to search + host="localhost", + port=3306, + user="root", + password="pass", + database="crewai", +) + +# Create an agent that uses this tool +data_analyst = Agent( + role="Business Analyst", + goal="Analyze and answer business questions using SQL data", + backstory="Expert in interpreting business needs and transforming them into data queries.", + tools=[singlestore_tool], + verbose=True, + embedder={ + "provider": "ollama", + "config": { + "model": "nomic-embed-text", + }, + }, +) + +# Define a task +task = Task( + description="List the top 2 customers by total sales amount.", + agent=data_analyst, + expected_output="A ranked list of top 2 customers that have the highest total sales amount, including their names and total sales figures.", +) + +# Run the crew +crew = Crew(tasks=[task], verbose=True) +result = crew.kickoff() +``` + +### Advanced CrewAI Example with Multiple Agents + +```python +from crewai import Agent, Task, Crew +from crewai_tools import SingleStoreSearchTool + +# Initialize the tool with connection URL +singlestore_tool = SingleStoreSearchTool( + host="user:password@localhost:3306/ecommerce_db", + tables=["orders", "products", "customers", "order_items"] +) + +# Data Analyst Agent +data_analyst = Agent( + role="Senior Data Analyst", + goal="Extract insights from database queries and provide data-driven recommendations", + backstory="You are an experienced data analyst with expertise in SQL and business intelligence.", + tools=[singlestore_tool], + verbose=True +) + +# Business Intelligence Agent +bi_specialist = Agent( + role="Business Intelligence Specialist", + goal="Transform data insights into actionable business recommendations", + backstory="You specialize in translating complex data analysis into clear business strategies.", + verbose=True +) + +# Define multiple tasks +data_extraction_task = Task( + description=""" + Analyze the sales data to find: + 1. Top 5 best-selling products by quantity + 2. Monthly sales trends for the last 6 months + 3. Customer segments by purchase frequency + """, + agent=data_analyst, + expected_output="Detailed SQL query results with sales analysis including product rankings, trends, and customer segments." +) + +insights_task = Task( + description=""" + Based on the sales data analysis, provide business recommendations for: + 1. Inventory management for top products + 2. Marketing strategies for different customer segments + 3. Sales forecasting insights + """, + agent=bi_specialist, + expected_output="Strategic business recommendations with actionable insights based on the data analysis.", + context=[data_extraction_task] +) + +# Create and run the crew +analytics_crew = Crew( + agents=[data_analyst, bi_specialist], + tasks=[data_extraction_task, insights_task], + verbose=True +) + +result = analytics_crew.kickoff() +``` + +## Connection Methods + +SingleStore supports multiple connection methods. Choose the one that best fits your environment: + +### 1. Standard Connection + +```python +tool = SingleStoreSearchTool( + host='your_host', + user='your_username', + password='your_password', + database='your_database', + port=3306 +) +``` + +### 2. Connection URL (Recommended) + +You can use a complete connection URL in the `host` parameter for simplified configuration: + +```python +# Using connection URL in host parameter +tool = SingleStoreSearchTool( + host='user:password@localhost:3306/database_name' +) + +# Or for SingleStore Cloud +tool = SingleStoreSearchTool( + host='user:password@your_cloud_host:3333/database_name?ssl_disabled=false' +) +``` + +### 3. Environment Variable Configuration + +Set the `SINGLESTOREDB_URL` environment variable and initialize the tool without any connection arguments: + +```bash +# Set the environment variable +export SINGLESTOREDB_URL="singlestoredb://user:password@localhost:3306/database_name" + +# Or for cloud connections +export SINGLESTOREDB_URL="singlestoredb://user:password@your_cloud_host:3333/database_name?ssl_disabled=false" +``` + +```python +# No connection arguments needed when using environment variable +tool = SingleStoreSearchTool() + +# Or specify only table subset +tool = SingleStoreSearchTool(tables=['employees', 'departments']) +``` + +### 4. Connection with SSL + +```python +tool = SingleStoreSearchTool( + host='your_host', + user='your_username', + password='your_password', + database='your_database', + ssl_ca='/path/to/ca-cert.pem', + ssl_cert='/path/to/client-cert.pem', + ssl_key='/path/to/client-key.pem' +) +``` + +### 5. Advanced Configuration + +```python +tool = SingleStoreSearchTool( + host='your_host', + user='your_username', + password='your_password', + database='your_database', + # Connection pool settings + pool_size=10, + max_overflow=20, + timeout=60, + # Advanced options + charset='utf8mb4', + autocommit=True, + connect_timeout=30, + results_format='tuple', + # Custom connection attributes + conn_attrs={ + 'program_name': 'MyApp', + 'custom_attr': 'value' + } +) +``` + +## Configuration Parameters + +### Basic Connection Parameters +- `host`: Database host address or complete connection URL +- `user`: Database username +- `password`: Database password +- `port`: Database port (default: 3306) +- `database`: Database name +- `tables`: List of specific tables to work with (optional) + +### Connection Pool Parameters +- `pool_size`: Maximum number of connections in the pool (default: 5) +- `max_overflow`: Maximum overflow connections beyond pool_size (default: 10) +- `timeout`: Connection timeout in seconds (default: 30) + +### SSL/TLS Parameters +- `ssl_key`: Path to client private key file +- `ssl_cert`: Path to client certificate file +- `ssl_ca`: Path to certificate authority file +- `ssl_disabled`: Disable SSL (default: None) +- `ssl_verify_cert`: Verify server certificate +- `ssl_verify_identity`: Verify server identity + +### Advanced Parameters +- `charset`: Character set for the connection +- `autocommit`: Enable autocommit mode +- `connect_timeout`: Connection timeout in seconds +- `results_format`: Format for query results ('tuple', 'dict', etc.) +- `vector_data_format`: Format for vector data ('binary', 'json') +- `parse_json`: Parse JSON columns automatically + + +For more detailed connection options and advanced configurations, refer to the [SingleStore Python SDK documentation](https://singlestoredb-python.labs.singlestore.com/getting-started.html). diff --git a/crewai_tools/tools/singlestore_search_tool/__init__.py b/crewai_tools/tools/singlestore_search_tool/__init__.py new file mode 100644 index 000000000..4ec997152 --- /dev/null +++ b/crewai_tools/tools/singlestore_search_tool/__init__.py @@ -0,0 +1,6 @@ +from .singlestore_search_tool import SingleStoreSearchTool, SingleStoreSearchToolSchema + +__all__ = [ + "SingleStoreSearchTool", + "SingleStoreSearchToolSchema", +] diff --git a/crewai_tools/tools/singlestore_search_tool/singlestore_search_tool.py b/crewai_tools/tools/singlestore_search_tool/singlestore_search_tool.py new file mode 100644 index 000000000..4c8d768a3 --- /dev/null +++ b/crewai_tools/tools/singlestore_search_tool/singlestore_search_tool.py @@ -0,0 +1,429 @@ +from typing import Any, Callable, Dict, List, Optional, Type + +from crewai.tools import BaseTool, EnvVar +from pydantic import BaseModel, Field + +try: + from singlestoredb import connect + from sqlalchemy.pool import QueuePool + + SINGLSTORE_AVAILABLE = True + +except ImportError: + SINGLSTORE_AVAILABLE = False + + +class SingleStoreSearchToolSchema(BaseModel): + """Input schema for SingleStoreSearchTool. + + This schema defines the expected input format for the search tool, + ensuring that only valid SELECT and SHOW queries are accepted. + """ + + search_query: str = Field( + ..., + description=( + "Mandatory semantic search query you want to use to search the database's content. " + "Only SELECT and SHOW queries are supported." + ), + ) + + +class SingleStoreSearchTool(BaseTool): + """A tool for performing semantic searches on SingleStore database tables. + + This tool provides a safe interface for executing SELECT and SHOW queries + against a SingleStore database with connection pooling for optimal performance. + """ + + name: str = "Search a database's table(s) content" + description: str = ( + "A tool that can be used to semantic search a query from a database." + ) + args_schema: Type[BaseModel] = SingleStoreSearchToolSchema + + package_dependencies: List[str] = ["singlestoredb", "SQLAlchemy"] + env_vars: List[EnvVar] = [ + EnvVar( + name="SINGLESTOREDB_URL", + description="A comprehensive URL string that can encapsulate host, port," + " username, password, and database information, often used in environments" + " like SingleStore notebooks or specific frameworks." + " For example: 'me:p455w0rd@s2-host.com/my_db'", + required=False, + default=None, + ), + EnvVar( + name="SINGLESTOREDB_HOST", + description="Specifies the hostname, IP address, or URL of" + " the SingleStoreDB workspace or cluster", + required=False, + default=None, + ), + EnvVar( + name="SINGLESTOREDB_PORT", + description="Defines the port number on which the" + " SingleStoreDB server is listening", + required=False, + default=None, + ), + EnvVar( + name="SINGLESTOREDB_USER", + description="Specifies the database user name", + required=False, + default=None, + ), + EnvVar( + name="SINGLESTOREDB_PASSWORD", + description="Specifies the database user password", + required=False, + default=None, + ), + EnvVar( + name="SINGLESTOREDB_DATABASE", + description="Name of the database to connect to", + required=False, + default=None, + ), + EnvVar( + name="SINGLESTOREDB_SSL_KEY", + description="File containing SSL key", + required=False, + default=None, + ), + EnvVar( + name="SINGLESTOREDB_SSL_CERT", + description="File containing SSL certificate", + required=False, + default=None, + ), + EnvVar( + name="SINGLESTOREDB_SSL_CA", + description="File containing SSL certificate authority", + required=False, + default=None, + ), + EnvVar( + name="SINGLESTOREDB_CONNECT_TIMEOUT", + description="The timeout for connecting to the database in seconds", + required=False, + default=None, + ), + ] + + connection_args: dict = {} + connection_pool: Optional[Any] = None + + def __init__( + self, + tables: List[str] = [], + # Basic connection parameters + host: Optional[str] = None, + user: Optional[str] = None, + password: Optional[str] = None, + port: Optional[int] = None, + database: Optional[str] = None, + driver: Optional[str] = None, + # Connection behavior options + pure_python: Optional[bool] = None, + local_infile: Optional[bool] = None, + charset: Optional[str] = None, + # SSL/TLS configuration + ssl_key: Optional[str] = None, + ssl_cert: Optional[str] = None, + ssl_ca: Optional[str] = None, + ssl_disabled: Optional[bool] = None, + ssl_cipher: Optional[str] = None, + ssl_verify_cert: Optional[bool] = None, + tls_sni_servername: Optional[str] = None, + ssl_verify_identity: Optional[bool] = None, + # Advanced connection options + conv: Optional[Dict[int, Callable[..., Any]]] = None, + credential_type: Optional[str] = None, + autocommit: Optional[bool] = None, + # Result formatting options + results_type: Optional[str] = None, + buffered: Optional[bool] = None, + results_format: Optional[str] = None, + program_name: Optional[str] = None, + conn_attrs: Optional[Dict[str, str]] = {}, + # Query execution options + multi_statements: Optional[bool] = None, + client_found_rows: Optional[bool] = None, + connect_timeout: Optional[int] = None, + # Data type handling + nan_as_null: Optional[bool] = None, + inf_as_null: Optional[bool] = None, + encoding_errors: Optional[str] = None, + track_env: Optional[bool] = None, + enable_extended_data_types: Optional[bool] = None, + vector_data_format: Optional[str] = None, + parse_json: Optional[bool] = None, + # Connection pool configuration + pool_size: Optional[int] = 5, + max_overflow: Optional[int] = 10, + timeout: Optional[float] = 30, + **kwargs, + ): + """Initialize the SingleStore search tool. + + Args: + tables: List of table names to work with. If empty, all tables will be used. + host: Database host address + user: Database username + password: Database password + port: Database port number + database: Database name + pool_size: Maximum number of connections in the pool + max_overflow: Maximum overflow connections beyond pool_size + timeout: Connection timeout in seconds + **kwargs: Additional arguments passed to the parent class + """ + + if not SINGLSTORE_AVAILABLE: + import click + + if click.confirm( + "You are missing the 'singlestore' package. Would you like to install it?" + ): + import subprocess + + try: + subprocess.run( + ["uv", "add", "crewai-tools[singlestore]"], check=True + ) + + except subprocess.CalledProcessError: + raise ImportError("Failed to install singlestore package") + else: + raise ImportError( + "`singlestore` package not found, please run `uv add crewai-tools[singlestore]`" + ) + + # Set the data type for the parent class + kwargs["data_type"] = "singlestore" + super().__init__(**kwargs) + + # Build connection arguments dictionary with sensible defaults + self.connection_args = { + # Basic connection parameters + "host": host, + "user": user, + "password": password, + "port": port, + "database": database, + "driver": driver, + # Connection behavior + "pure_python": pure_python, + "local_infile": local_infile, + "charset": charset, + # SSL/TLS settings + "ssl_key": ssl_key, + "ssl_cert": ssl_cert, + "ssl_ca": ssl_ca, + "ssl_disabled": ssl_disabled, + "ssl_cipher": ssl_cipher, + "ssl_verify_cert": ssl_verify_cert, + "tls_sni_servername": tls_sni_servername, + "ssl_verify_identity": ssl_verify_identity, + # Advanced options + "conv": conv or {}, + "credential_type": credential_type, + "autocommit": autocommit, + # Result formatting + "results_type": results_type, + "buffered": buffered, + "results_format": results_format, + "program_name": program_name, + "conn_attrs": conn_attrs or {}, + # Query execution + "multi_statements": multi_statements, + "client_found_rows": client_found_rows, + "connect_timeout": connect_timeout or 10, # Default: 10 seconds + # Data type handling with defaults + "nan_as_null": nan_as_null or False, + "inf_as_null": inf_as_null or False, + "encoding_errors": encoding_errors or "strict", + "track_env": track_env or False, + "enable_extended_data_types": enable_extended_data_types or False, + "vector_data_format": vector_data_format or "binary", + "parse_json": parse_json or True, + } + + # Ensure connection attributes are properly initialized + if "conn_attrs" not in self.connection_args or not self.connection_args.get( + "conn_attrs" + ): + self.connection_args["conn_attrs"] = dict() + + # Add tool identification to connection attributes + self.connection_args["conn_attrs"][ + "_connector_name" + ] = "crewAI SingleStore Tool" + self.connection_args["conn_attrs"]["_connector_version"] = "1.0" + + # Initialize connection pool for efficient connection management + self.connection_pool = QueuePool( + creator=self._create_connection, + pool_size=pool_size, + max_overflow=max_overflow, + timeout=timeout, + ) + + # Validate database schema and initialize table information + self._initialize_tables(tables) + + def _initialize_tables(self, tables: List[str]) -> None: + """Initialize and validate the tables that this tool will work with. + + Args: + tables: List of table names to validate and use + + Raises: + ValueError: If no tables exist or specified tables don't exist + """ + conn = self._get_connection() + try: + with conn.cursor() as cursor: + # Get all existing tables in the database + cursor.execute("SHOW TABLES") + existing_tables = {table[0] for table in cursor.fetchall()} + + # Validate that the database has tables + if not existing_tables or len(existing_tables) == 0: + raise ValueError( + "No tables found in the database. " + "Please ensure the database is initialized with the required tables." + ) + + # Use all tables if none specified + if not tables or len(tables) == 0: + tables = existing_tables + + # Build table definitions for description + table_definitions = [] + for table in tables: + if table not in existing_tables: + raise ValueError( + f"Table {table} does not exist in the database. " + f"Please ensure the table is created." + ) + + # Get column information for each table + cursor.execute(f"SHOW COLUMNS FROM {table}") + columns = cursor.fetchall() + column_info = ", ".join(f"{row[0]} {row[1]}" for row in columns) + table_definitions.append(f"{table}({column_info})") + finally: + # Ensure the connection is returned to the pool + conn.close() + + # Update the tool description with actual table information + self.description = ( + f"A tool that can be used to semantic search a query from a SingleStore " + f"database's {', '.join(table_definitions)} table(s) content." + ) + self._generate_description() + + def _get_connection(self) -> Optional[Any]: + """Get a connection from the connection pool. + + Returns: + Connection: A SingleStore database connection + + Raises: + Exception: If connection cannot be established + """ + try: + conn = self.connection_pool.connect() + return conn + except Exception: + # Re-raise the exception to be handled by the caller + raise + + def _create_connection(self) -> Optional[Any]: + """Create a new SingleStore connection. + + This method is used by the connection pool to create new connections + when needed. + + Returns: + Connection: A new SingleStore database connection + + Raises: + Exception: If connection cannot be created + """ + try: + conn = connect(**self.connection_args) + return conn + except Exception: + # Re-raise the exception to be handled by the caller + raise + + def _validate_query(self, search_query: str) -> tuple[bool, str]: + """Validate the search query to ensure it's safe to execute. + + Only SELECT and SHOW statements are allowed for security reasons. + + Args: + search_query: The SQL query to validate + + Returns: + tuple: (is_valid: bool, message: str) + """ + # Check if the input is a string + if not isinstance(search_query, str): + return False, "Search query must be a string." + + # Remove leading/trailing whitespace and convert to lowercase for checking + query_lower = search_query.strip().lower() + + # Allow only SELECT and SHOW statements + if not (query_lower.startswith("select") or query_lower.startswith("show")): + return ( + False, + "Only SELECT and SHOW queries are supported for security reasons.", + ) + + return True, "Valid query" + + def _run(self, search_query: str) -> Any: + """Execute the search query against the SingleStore database. + + Args: + search_query: The SQL query to execute + **kwargs: Additional keyword arguments (unused) + + Returns: + str: Formatted search results or error message + """ + # Validate the query before execution + valid, message = self._validate_query(search_query) + if not valid: + return f"Invalid search query: {message}" + + # Execute the query using a connection from the pool + conn = self._get_connection() + try: + with conn.cursor() as cursor: + try: + # Execute the validated search query + cursor.execute(search_query) + results = cursor.fetchall() + + # Handle empty results + if not results: + return "No results found." + + # Format the results for readable output + formatted_results = "\n".join( + [", ".join([str(item) for item in row]) for row in results] + ) + return f"Search Results:\n{formatted_results}" + + except Exception as e: + return f"Error executing search query: {e}" + + finally: + # Ensure the connection is returned to the pool + conn.close() diff --git a/crewai_tools/tools/snowflake_search_tool/README.md b/crewai_tools/tools/snowflake_search_tool/README.md new file mode 100644 index 000000000..fc0b845c3 --- /dev/null +++ b/crewai_tools/tools/snowflake_search_tool/README.md @@ -0,0 +1,155 @@ +# Snowflake Search Tool + +A tool for executing queries on Snowflake data warehouse with built-in connection pooling, retry logic, and async execution support. + +## Installation + +```bash +uv sync --extra snowflake + +OR +uv pip install snowflake-connector-python>=3.5.0 snowflake-sqlalchemy>=1.5.0 cryptography>=41.0.0 + +OR +pip install snowflake-connector-python>=3.5.0 snowflake-sqlalchemy>=1.5.0 cryptography>=41.0.0 +``` + +## Quick Start + +```python +import asyncio +from crewai_tools import SnowflakeSearchTool, SnowflakeConfig + +# Create configuration +config = SnowflakeConfig( + account="your_account", + user="your_username", + password="your_password", + warehouse="COMPUTE_WH", + database="your_database", + snowflake_schema="your_schema" # Note: Uses snowflake_schema instead of schema +) + +# Initialize tool +tool = SnowflakeSearchTool( + config=config, + pool_size=5, + max_retries=3, + enable_caching=True +) + +# Execute query +async def main(): + results = await tool._run( + query="SELECT * FROM your_table LIMIT 10", + timeout=300 + ) + print(f"Retrieved {len(results)} rows") + +if __name__ == "__main__": + asyncio.run(main()) +``` + +## Features + +- ✨ Asynchronous query execution +- 🚀 Connection pooling for better performance +- 🔄 Automatic retries for transient failures +- 💾 Query result caching (optional) +- 🔒 Support for both password and key-pair authentication +- 📝 Comprehensive error handling and logging + +## Configuration Options + +### SnowflakeConfig Parameters + +| Parameter | Required | Description | +|-----------|----------|-------------| +| account | Yes | Snowflake account identifier | +| user | Yes | Snowflake username | +| password | Yes* | Snowflake password | +| private_key_path | No* | Path to private key file (alternative to password) | +| warehouse | Yes | Snowflake warehouse name | +| database | Yes | Default database | +| snowflake_schema | Yes | Default schema | +| role | No | Snowflake role | +| session_parameters | No | Custom session parameters dict | + +\* Either password or private_key_path must be provided + +### Tool Parameters + +| Parameter | Default | Description | +|-----------|---------|-------------| +| pool_size | 5 | Number of connections in the pool | +| max_retries | 3 | Maximum retry attempts for failed queries | +| retry_delay | 1.0 | Delay between retries in seconds | +| enable_caching | True | Enable/disable query result caching | + +## Advanced Usage + +### Using Key-Pair Authentication + +```python +config = SnowflakeConfig( + account="your_account", + user="your_username", + private_key_path="/path/to/private_key.p8", + warehouse="your_warehouse", + database="your_database", + snowflake_schema="your_schema" +) +``` + +### Custom Session Parameters + +```python +config = SnowflakeConfig( + # ... other config parameters ... + session_parameters={ + "QUERY_TAG": "my_app", + "TIMEZONE": "America/Los_Angeles" + } +) +``` + +## Best Practices + +1. **Error Handling**: Always wrap query execution in try-except blocks +2. **Logging**: Enable logging to track query execution and errors +3. **Connection Management**: Use appropriate pool sizes for your workload +4. **Timeouts**: Set reasonable query timeouts to prevent hanging +5. **Security**: Use key-pair auth in production and never hardcode credentials + +## Example with Logging + +```python +import logging + +# Configure logging +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger(__name__) + +async def main(): + try: + # ... tool initialization ... + results = await tool._run(query="SELECT * FROM table LIMIT 10") + logger.info(f"Query completed successfully. Retrieved {len(results)} rows") + except Exception as e: + logger.error(f"Query failed: {str(e)}") + raise +``` + +## Error Handling + +The tool automatically handles common Snowflake errors: +- DatabaseError +- OperationalError +- ProgrammingError +- Network timeouts +- Connection issues + +Errors are logged and retried based on your retry configuration. \ No newline at end of file diff --git a/crewai_tools/tools/snowflake_search_tool/__init__.py b/crewai_tools/tools/snowflake_search_tool/__init__.py new file mode 100644 index 000000000..abc1a45f5 --- /dev/null +++ b/crewai_tools/tools/snowflake_search_tool/__init__.py @@ -0,0 +1,11 @@ +from .snowflake_search_tool import ( + SnowflakeConfig, + SnowflakeSearchTool, + SnowflakeSearchToolInput, +) + +__all__ = [ + "SnowflakeSearchTool", + "SnowflakeSearchToolInput", + "SnowflakeConfig", +] diff --git a/crewai_tools/tools/snowflake_search_tool/snowflake_search_tool.py b/crewai_tools/tools/snowflake_search_tool/snowflake_search_tool.py new file mode 100644 index 000000000..a4cd21044 --- /dev/null +++ b/crewai_tools/tools/snowflake_search_tool/snowflake_search_tool.py @@ -0,0 +1,268 @@ +import asyncio +import logging +from concurrent.futures import ThreadPoolExecutor +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Type + +from crewai.tools.base_tool import BaseTool +from pydantic import BaseModel, ConfigDict, Field, SecretStr + +if TYPE_CHECKING: + # Import types for type checking only + from snowflake.connector.connection import SnowflakeConnection + from snowflake.connector.errors import DatabaseError, OperationalError + +try: + import snowflake.connector + from cryptography.hazmat.backends import default_backend + from cryptography.hazmat.primitives import serialization + + SNOWFLAKE_AVAILABLE = True +except ImportError: + SNOWFLAKE_AVAILABLE = False + +# Configure logging +logger = logging.getLogger(__name__) + +# Cache for query results +_query_cache = {} + + +class SnowflakeConfig(BaseModel): + """Configuration for Snowflake connection.""" + + model_config = ConfigDict(protected_namespaces=()) + + account: str = Field( + ..., description="Snowflake account identifier", pattern=r"^[a-zA-Z0-9\-_]+$" + ) + user: str = Field(..., description="Snowflake username") + password: Optional[SecretStr] = Field(None, description="Snowflake password") + private_key_path: Optional[str] = Field( + None, description="Path to private key file" + ) + warehouse: Optional[str] = Field(None, description="Snowflake warehouse") + database: Optional[str] = Field(None, description="Default database") + snowflake_schema: Optional[str] = Field(None, description="Default schema") + role: Optional[str] = Field(None, description="Snowflake role") + session_parameters: Optional[Dict[str, Any]] = Field( + default_factory=dict, description="Session parameters" + ) + + @property + def has_auth(self) -> bool: + return bool(self.password or self.private_key_path) + + def model_post_init(self, *args, **kwargs): + if not self.has_auth: + raise ValueError("Either password or private_key_path must be provided") + + +class SnowflakeSearchToolInput(BaseModel): + """Input schema for SnowflakeSearchTool.""" + + model_config = ConfigDict(protected_namespaces=()) + + query: str = Field(..., description="SQL query or semantic search query to execute") + database: Optional[str] = Field(None, description="Override default database") + snowflake_schema: Optional[str] = Field(None, description="Override default schema") + timeout: Optional[int] = Field(300, description="Query timeout in seconds") + + +class SnowflakeSearchTool(BaseTool): + """Tool for executing queries and semantic search on Snowflake.""" + + name: str = "Snowflake Database Search" + description: str = ( + "Execute SQL queries or semantic search on Snowflake data warehouse. " + "Supports both raw SQL and natural language queries." + ) + args_schema: Type[BaseModel] = SnowflakeSearchToolInput + + # Define Pydantic fields + config: SnowflakeConfig = Field( + ..., description="Snowflake connection configuration" + ) + pool_size: int = Field(default=5, description="Size of connection pool") + max_retries: int = Field(default=3, description="Maximum retry attempts") + retry_delay: float = Field( + default=1.0, description="Delay between retries in seconds" + ) + enable_caching: bool = Field( + default=True, description="Enable query result caching" + ) + + model_config = ConfigDict( + arbitrary_types_allowed=True, validate_assignment=True, frozen=False + ) + + _connection_pool: Optional[List["SnowflakeConnection"]] = None + _pool_lock: Optional[asyncio.Lock] = None + _thread_pool: Optional[ThreadPoolExecutor] = None + _model_rebuilt: bool = False + package_dependencies: List[str] = ["snowflake-connector-python", "snowflake-sqlalchemy", "cryptography"] + + def __init__(self, **data): + """Initialize SnowflakeSearchTool.""" + super().__init__(**data) + self._initialize_snowflake() + + def _initialize_snowflake(self) -> None: + try: + if SNOWFLAKE_AVAILABLE: + self._connection_pool = [] + self._pool_lock = asyncio.Lock() + self._thread_pool = ThreadPoolExecutor(max_workers=self.pool_size) + else: + raise ImportError + except ImportError: + import click + + if click.confirm( + "You are missing the 'snowflake-connector-python' package. Would you like to install it?" + ): + import subprocess + + try: + subprocess.run( + [ + "uv", + "add", + "cryptography", + "snowflake-connector-python", + "snowflake-sqlalchemy", + ], + check=True, + ) + + self._connection_pool = [] + self._pool_lock = asyncio.Lock() + self._thread_pool = ThreadPoolExecutor(max_workers=self.pool_size) + except subprocess.CalledProcessError: + raise ImportError("Failed to install Snowflake dependencies") + else: + raise ImportError( + "Snowflake dependencies not found. Please install them by running " + "`uv add cryptography snowflake-connector-python snowflake-sqlalchemy`" + ) + + async def _get_connection(self) -> "SnowflakeConnection": + """Get a connection from the pool or create a new one.""" + async with self._pool_lock: + if not self._connection_pool: + conn = await asyncio.get_event_loop().run_in_executor( + self._thread_pool, self._create_connection + ) + self._connection_pool.append(conn) + return self._connection_pool.pop() + + def _create_connection(self) -> "SnowflakeConnection": + """Create a new Snowflake connection.""" + conn_params = { + "account": self.config.account, + "user": self.config.user, + "warehouse": self.config.warehouse, + "database": self.config.database, + "schema": self.config.snowflake_schema, + "role": self.config.role, + "session_parameters": self.config.session_parameters, + } + + if self.config.password: + conn_params["password"] = self.config.password.get_secret_value() + elif self.config.private_key_path and serialization: + with open(self.config.private_key_path, "rb") as key_file: + p_key = serialization.load_pem_private_key( + key_file.read(), password=None, backend=default_backend() + ) + conn_params["private_key"] = p_key + + return snowflake.connector.connect(**conn_params) + + def _get_cache_key(self, query: str, timeout: int) -> str: + """Generate a cache key for the query.""" + return f"{self.config.account}:{self.config.database}:{self.config.snowflake_schema}:{query}:{timeout}" + + async def _execute_query( + self, query: str, timeout: int = 300 + ) -> List[Dict[str, Any]]: + """Execute a query with retries and return results.""" + + if self.enable_caching: + cache_key = self._get_cache_key(query, timeout) + if cache_key in _query_cache: + logger.info("Returning cached result") + return _query_cache[cache_key] + + for attempt in range(self.max_retries): + try: + conn = await self._get_connection() + try: + cursor = conn.cursor() + cursor.execute(query, timeout=timeout) + + if not cursor.description: + return [] + + columns = [col[0] for col in cursor.description] + results = [dict(zip(columns, row)) for row in cursor.fetchall()] + + if self.enable_caching: + _query_cache[self._get_cache_key(query, timeout)] = results + + return results + finally: + cursor.close() + async with self._pool_lock: + self._connection_pool.append(conn) + except (DatabaseError, OperationalError) as e: + if attempt == self.max_retries - 1: + raise + await asyncio.sleep(self.retry_delay * (2**attempt)) + logger.warning(f"Query failed, attempt {attempt + 1}: {str(e)}") + continue + + async def _run( + self, + query: str, + database: Optional[str] = None, + snowflake_schema: Optional[str] = None, + timeout: int = 300, + **kwargs: Any, + ) -> Any: + """Execute the search query.""" + + try: + # Override database/schema if provided + if database: + await self._execute_query(f"USE DATABASE {database}") + if snowflake_schema: + await self._execute_query(f"USE SCHEMA {snowflake_schema}") + + results = await self._execute_query(query, timeout) + return results + except Exception as e: + logger.error(f"Error executing query: {str(e)}") + raise + + def __del__(self): + """Cleanup connections on deletion.""" + try: + if self._connection_pool: + for conn in self._connection_pool: + try: + conn.close() + except Exception: + pass + if self._thread_pool: + self._thread_pool.shutdown() + except Exception: + pass + + +try: + # Only rebuild if the class hasn't been initialized yet + if not hasattr(SnowflakeSearchTool, "_model_rebuilt"): + SnowflakeSearchTool.model_rebuild() + SnowflakeSearchTool._model_rebuilt = True +except Exception: + pass diff --git a/crewai_tools/tools/spider_tool/README.md b/crewai_tools/tools/spider_tool/README.md new file mode 100644 index 000000000..482c7c830 --- /dev/null +++ b/crewai_tools/tools/spider_tool/README.md @@ -0,0 +1,87 @@ +# SpiderTool + +## Description +[Spider](https://spider.cloud/?ref=crewai) is a high-performance web scraping and crawling tool that delivers optimized markdown for LLMs and AI agents. It intelligently switches between HTTP requests and JavaScript rendering based on page requirements. Perfect for both single-page scraping and website crawling—making it ideal for content extraction and data collection. + +## Installation +To use the Spider API you need to download the [Spider SDK](https://pypi.org/project/spider-client/) and the crewai[tools] SDK, too: + +```python +pip install spider-client 'crewai[tools]' +``` + +## Example +This example shows you how you can use the Spider tool to enable your agent to scrape and crawl websites. The data returned from the Spider API is LLM-ready. + +```python +from crewai_tools import SpiderTool + +# To enable scraping any website it finds during its execution +spider_tool = SpiderTool(api_key='YOUR_API_KEY') + +# Initialize the tool with the website URL, so the agent can only scrape the content of the specified website +spider_tool = SpiderTool(website_url='https://spider.cloud') + +# Pass in custom parameters, see below for more details +spider_tool = SpiderTool( + website_url='https://spider.cloud', + custom_params={"depth": 2, "anti_bot": True, "proxy_enabled": True} +) + +# Advanced usage using css query selector to extract content +css_extraction_map = { + "/": [ # pass in path (main index in this case) + { + "name": "headers", # give it a name for this element + "selectors": [ + "h1" + ] + } + ] +} + +spider_tool = SpiderTool( + website_url='https://spider.cloud', + custom_params={"anti_bot": True, "proxy_enabled": True, "metadata": True, "css_extraction_map": css_extraction_map} +) + +### Response (extracted text will be in the metadata) +"css_extracted": { + "headers": [ + "The Web Crawler for AI Agents and LLMs!" + ] +} +``` +## Agent setup +```yaml +researcher: + role: > + You're a researcher that is tasked with researching a website and it's content (use crawl mode). The website is to crawl is: {website_url}. +``` + +## Arguments + +- `api_key` (string, optional): Specifies Spider API key. If not specified, it looks for `SPIDER_API_KEY` in environment variables. +- `website_url` (string): The website URL. Will be used as a fallback if passed when the tool is initialized. +- `log_failures` (bool): Log scrape failures or fail silently. Defaults to `true`. +- `custom_params` (object, optional): Optional parameters for the request. + - `return_format` (string): The return format of the website's content. Defaults to `markdown`. + - `request` (string): The request type to perform. Possible values are `http`, `chrome`, and `smart`. Use `smart` to perform an HTTP request by default until JavaScript rendering is needed for the HTML. + - `limit` (int): The maximum number of pages allowed to crawl per website. Remove the value or set it to `0` to crawl all pages. + - `depth` (int): The crawl limit for maximum depth. If `0`, no limit will be applied. + - `locale` (string): The locale to use for request, example `en-US`. + - `cookies` (string): Add HTTP cookies to use for request. + - `stealth` (bool): Use stealth mode for headless chrome request to help prevent being blocked. The default is `true` on chrome. + - `headers` (object): Forward HTTP headers to use for all request. The object is expected to be a map of key value pairs. + - `metadata` (bool): Boolean to store metadata about the pages and content found. Defaults to `false`. + - `subdomains` (bool): Allow subdomains to be included. Default is `false`. + - `user_agent` (string): Add a custom HTTP user agent to the request. By default this is set to a random agent. + - `proxy_enabled` (bool): Enable high performance premium proxies for the request to prevent being blocked at the network level. + - `css_extraction_map` (object): Use CSS or XPath selectors to scrape contents from the web page. Set the paths and the extraction object map to perform extractions per path or page. + - `request_timeout` (int): The timeout to use for request. Timeouts can be from `5-60`. The default is `30` seconds. + - `return_headers` (bool): Return the HTTP response headers with the results. Defaults to `false`. + - `filter_output_main_only` (bool): Filter the nav, aside, and footer from the output. + - `headers` (object): Forward HTTP headers to use for all request. The object is expected to be a map of key value pairs. + +Learn other parameters that can be used: [https://spider.cloud/docs/api](https://spider.cloud/docs/api) + diff --git a/crewai_tools/tools/spider_tool/spider_tool.py b/crewai_tools/tools/spider_tool/spider_tool.py new file mode 100644 index 000000000..3aee6ef88 --- /dev/null +++ b/crewai_tools/tools/spider_tool/spider_tool.py @@ -0,0 +1,214 @@ +import logging +from typing import Any, Dict, Literal, Optional, Type, List +from urllib.parse import unquote, urlparse + +from crewai.tools import BaseTool, EnvVar +from pydantic import BaseModel, Field + +logger = logging.getLogger(__file__) + + +class SpiderToolSchema(BaseModel): + """Input schema for SpiderTool.""" + + website_url: str = Field( + ..., description="Mandatory website URL to scrape or crawl" + ) + mode: Literal["scrape", "crawl"] = Field( + default="scrape", + description="The mode of the SpiderTool. The only two allowed modes are `scrape` or `crawl`. Crawl mode will follow up to 5 links and return their content in markdown format.", + ) + + +class SpiderToolConfig(BaseModel): + """Configuration settings for SpiderTool. + + Contains all default values and constants used by SpiderTool. + Centralizes configuration management for easier maintenance. + """ + + # Crawling settings + DEFAULT_CRAWL_LIMIT: int = 5 + DEFAULT_RETURN_FORMAT: str = "markdown" + + # Request parameters + DEFAULT_REQUEST_MODE: str = "smart" + FILTER_SVG: bool = True + + +class SpiderTool(BaseTool): + """Tool for scraping and crawling websites. + This tool provides functionality to either scrape a single webpage or crawl multiple + pages, returning content in a format suitable for LLM processing. + """ + + name: str = "SpiderTool" + description: str = ( + "A tool to scrape or crawl a website and return LLM-ready content." + ) + args_schema: Type[BaseModel] = SpiderToolSchema + custom_params: Optional[Dict[str, Any]] = None + website_url: Optional[str] = None + api_key: Optional[str] = None + spider: Any = None + log_failures: bool = True + config: SpiderToolConfig = SpiderToolConfig() + package_dependencies: List[str] = ["spider-client"] + env_vars: List[EnvVar] = [ + EnvVar(name="SPIDER_API_KEY", description="API key for Spider.cloud", required=True), + ] + + def __init__( + self, + api_key: Optional[str] = None, + website_url: Optional[str] = None, + custom_params: Optional[Dict[str, Any]] = None, + log_failures: bool = True, + **kwargs, + ): + """Initialize SpiderTool for web scraping and crawling. + + Args: + api_key (Optional[str]): Spider API key for authentication. Required for production use. + website_url (Optional[str]): Default website URL to scrape/crawl. Can be overridden during execution. + custom_params (Optional[Dict[str, Any]]): Additional parameters to pass to Spider API. + These override any parameters set by the LLM. + log_failures (bool): If True, logs errors. Defaults to True. + **kwargs: Additional arguments passed to BaseTool. + + Raises: + ImportError: If spider-client package is not installed. + RuntimeError: If Spider client initialization fails. + """ + + super().__init__(**kwargs) + if website_url is not None: + self.website_url = website_url + + self.log_failures = log_failures + self.custom_params = custom_params + + try: + from spider import Spider # type: ignore + + except ImportError: + import click + + if click.confirm( + "You are missing the 'spider-client' package. Would you like to install it?" + ): + import subprocess + + subprocess.run(["uv", "pip", "install", "spider-client"], check=True) + from spider import Spider + else: + raise ImportError( + "`spider-client` package not found, please run `uv add spider-client`" + ) + self.spider = Spider(api_key=api_key) + + def _validate_url(self, url: str) -> bool: + """Validate URL format and security constraints. + + Args: + url (str): URL to validate. Must be a properly formatted HTTP(S) URL + + Returns: + bool: True if URL is valid and meets security requirements, False otherwise. + """ + try: + url = url.strip() + decoded_url = unquote(url) + + result = urlparse(decoded_url) + if not all([result.scheme, result.netloc]): + return False + + if result.scheme not in ["http", "https"]: + return False + + return True + except Exception: + return False + + def _run( + self, + website_url: str, + mode: Literal["scrape", "crawl"] = "scrape", + ) -> Optional[str]: + """Execute the spider tool to scrape or crawl the specified website. + + Args: + website_url (str): The URL to process. Must be a valid HTTP(S) URL. + mode (Literal["scrape", "crawl"]): Operation mode. + - "scrape": Extract content from single page + - "crawl": Follow links and extract content from multiple pages + + Returns: + Optional[str]: Extracted content in markdown format, or None if extraction fails + and log_failures is True. + + Raises: + ValueError: If URL is invalid or missing, or if mode is invalid. + ImportError: If spider-client package is not properly installed. + ConnectionError: If network connection fails while accessing the URL. + Exception: For other runtime errors. + """ + + try: + params = {} + url = website_url or self.website_url + + if not url: + raise ValueError( + "Website URL must be provided either during initialization or execution" + ) + + if not self._validate_url(url): + raise ValueError(f"Invalid URL format: {url}") + + if mode not in ["scrape", "crawl"]: + raise ValueError( + f"Invalid mode: {mode}. Must be either 'scrape' or 'crawl'" + ) + + params = { + "request": self.config.DEFAULT_REQUEST_MODE, + "filter_output_svg": self.config.FILTER_SVG, + "return_format": self.config.DEFAULT_RETURN_FORMAT, + } + + if mode == "crawl": + params["limit"] = self.config.DEFAULT_CRAWL_LIMIT + + if self.custom_params: + params.update(self.custom_params) + + action = ( + self.spider.scrape_url if mode == "scrape" else self.spider.crawl_url + ) + return action(url=url, params=params) + + except ValueError as ve: + if self.log_failures: + logger.error(f"Validation error for URL {url}: {str(ve)}") + return None + raise ve + + except ImportError as ie: + logger.error(f"Spider client import error: {str(ie)}") + raise ie + + except ConnectionError as ce: + if self.log_failures: + logger.error(f"Connection error while accessing {url}: {str(ce)}") + return None + raise ce + + except Exception as e: + if self.log_failures: + logger.error( + f"Unexpected error during {mode} operation on {url}: {str(e)}" + ) + return None + raise e diff --git a/crewai_tools/tools/stagehand_tool/.env.example b/crewai_tools/tools/stagehand_tool/.env.example new file mode 100644 index 000000000..7a4d2890a --- /dev/null +++ b/crewai_tools/tools/stagehand_tool/.env.example @@ -0,0 +1,5 @@ +ANTHROPIC_API_KEY="your_anthropic_api_key" +OPENAI_API_KEY="your_openai_api_key" +MODEL_API_KEY="your_model_api_key" +BROWSERBASE_API_KEY="your_browserbase_api_key" +BROWSERBASE_PROJECT_ID="your_browserbase_project_id" \ No newline at end of file diff --git a/crewai_tools/tools/stagehand_tool/README.md b/crewai_tools/tools/stagehand_tool/README.md new file mode 100644 index 000000000..707b99343 --- /dev/null +++ b/crewai_tools/tools/stagehand_tool/README.md @@ -0,0 +1,273 @@ +# Stagehand Web Automation Tool + +This tool integrates the [Stagehand](https://docs.stagehand.dev/) framework with CrewAI, allowing agents to interact with websites and automate browser tasks using natural language instructions. + +## Description + +Stagehand is a powerful browser automation framework built by Browserbase that allows AI agents to: + +- Navigate to websites +- Click buttons, links, and other elements +- Fill in forms +- Extract data from web pages +- Observe and identify elements +- Perform complex workflows + +The StagehandTool wraps the Stagehand Python SDK to provide CrewAI agents with the ability to control a real web browser and interact with websites using three core primitives: + +1. **Act**: Perform actions like clicking, typing, or navigating +2. **Extract**: Extract structured data from web pages +3. **Observe**: Identify and analyze elements on the page + +## Requirements + +Before using this tool, you will need: + +1. A [Browserbase](https://www.browserbase.com/) account with API key and project ID +2. An API key for an LLM (OpenAI or Anthropic Claude) +3. The Stagehand Python SDK installed + +Install the dependencies: + +```bash +pip install stagehand-py +``` + +## Usage + +### Basic Usage + +The StagehandTool can be used in two ways: + +1. **Using a context manager (recommended)**: +```python +from crewai import Agent, Task, Crew +from crewai_tools import StagehandTool +from stagehand.schemas import AvailableModel + +# Initialize the tool with your API keys using a context manager +with StagehandTool( + api_key="your-browserbase-api-key", + project_id="your-browserbase-project-id", + model_api_key="your-llm-api-key", # OpenAI or Anthropic API key + model_name=AvailableModel.CLAUDE_3_7_SONNET_LATEST, # Optional: specify which model to use +) as stagehand_tool: + # Create an agent with the tool + researcher = Agent( + role="Web Researcher", + goal="Find and summarize information from websites", + backstory="I'm an expert at finding information online.", + verbose=True, + tools=[stagehand_tool], + ) + + # Create a task that uses the tool + research_task = Task( + description="Go to https://www.example.com and tell me what you see on the homepage.", + agent=researcher, + ) + + # Run the crew + crew = Crew( + agents=[researcher], + tasks=[research_task], + verbose=True, + ) + + result = crew.kickoff() + print(result) + # Resources are automatically cleaned up when exiting the context +``` + +2. **Manual resource management**: +```python +from crewai import Agent, Task, Crew +from crewai_tools import StagehandTool +from stagehand.schemas import AvailableModel + +# Initialize the tool with your API keys +stagehand_tool = StagehandTool( + api_key="your-browserbase-api-key", + project_id="your-browserbase-project-id", + model_api_key="your-llm-api-key", + model_name=AvailableModel.CLAUDE_3_7_SONNET_LATEST, +) + +try: + # Create an agent with the tool + researcher = Agent( + role="Web Researcher", + goal="Find and summarize information from websites", + backstory="I'm an expert at finding information online.", + verbose=True, + tools=[stagehand_tool], + ) + + # Create a task that uses the tool + research_task = Task( + description="Go to https://www.example.com and tell me what you see on the homepage.", + agent=researcher, + ) + + # Run the crew + crew = Crew( + agents=[researcher], + tasks=[research_task], + verbose=True, + ) + + result = crew.kickoff() + print(result) +finally: + # Explicitly clean up resources + stagehand_tool.close() +``` + +The context manager approach (option 1) is recommended as it ensures proper cleanup of resources even if exceptions occur. However, both approaches are valid and will properly manage the browser session. + +## Command Types + +The StagehandTool supports three different command types, each designed for specific web automation tasks: + +### 1. Act - Perform Actions on a Page + +The `act` command type (default) allows the agent to perform actions on a webpage, such as clicking buttons, filling forms, navigating, and more. + +**When to use**: Use `act` when you need to interact with a webpage by performing actions like clicking, typing, scrolling, or navigating. + +**Example usage**: +```python +# Perform an action (default behavior) +result = stagehand_tool.run( + instruction="Click the login button", + url="https://example.com", + command_type="act" # Default, so can be omitted +) + +# Fill out a form +result = stagehand_tool.run( + instruction="Fill the contact form with name 'John Doe', email 'john@example.com', and message 'Hello world'", + url="https://example.com/contact" +) + +# Multiple actions in sequence +result = stagehand_tool.run( + instruction="Search for 'AI tools' in the search box and press Enter", + url="https://example.com" +) +``` + +### 2. Extract - Get Data from a Page + +The `extract` command type allows the agent to extract structured data from a webpage, such as product information, article text, or table data. + +**When to use**: Use `extract` when you need to retrieve specific information from a webpage in a structured format. + +**Example usage**: +```python +# Extract all product information +result = stagehand_tool.run( + instruction="Extract all product names, prices, and descriptions", + url="https://example.com/products", + command_type="extract" +) + +# Extract specific information with a selector +result = stagehand_tool.run( + instruction="Extract the main article title and content", + url="https://example.com/blog/article", + command_type="extract", + selector=".article-container" # Optional CSS selector to limit extraction scope +) + +# Extract tabular data +result = stagehand_tool.run( + instruction="Extract the data from the pricing table as a structured list of plans with their features and costs", + url="https://example.com/pricing", + command_type="extract", + selector=".pricing-table" +) +``` + +### 3. Observe - Identify Elements on a Page + +The `observe` command type allows the agent to identify and analyze specific elements on a webpage, returning information about their attributes, location, and suggested actions. + +**When to use**: Use `observe` when you need to identify UI elements, understand page structure, or determine what actions are possible. + +**Example usage**: +```python +# Find interactive elements +result = stagehand_tool.run( + instruction="Find all interactive elements in the navigation menu", + url="https://example.com", + command_type="observe" +) + +# Identify form fields +result = stagehand_tool.run( + instruction="Identify all the input fields in the registration form", + url="https://example.com/register", + command_type="observe", + selector="#registration-form" +) + +# Analyze page structure +result = stagehand_tool.run( + instruction="Find the main content sections of this page", + url="https://example.com/about", + command_type="observe" +) +``` + +## Advanced Configuration + +You can customize the behavior of the StagehandTool by specifying different parameters: + +```python +stagehand_tool = StagehandTool( + api_key="your-browserbase-api-key", + project_id="your-browserbase-project-id", + model_api_key="your-llm-api-key", + model_name=AvailableModel.CLAUDE_3_7_SONNET_LATEST, + dom_settle_timeout_ms=5000, # Wait longer for DOM to settle + headless=True, # Run browser in headless mode (no visible window) + self_heal=True, # Attempt to recover from errors + wait_for_captcha_solves=True, # Wait for CAPTCHA solving + verbose=1, # Control logging verbosity (0-3) +) +``` + +## Tips for Effective Use + +1. **Be specific in instructions**: The more specific your instructions, the better the results. For example, instead of "click the button," use "click the 'Submit' button at the bottom of the contact form." + +2. **Use the right command type**: Choose the appropriate command type based on your task: + - Use `act` for interactions and navigation + - Use `extract` for gathering information + - Use `observe` for understanding page structure + +3. **Leverage selectors**: When extracting data or observing elements, use CSS selectors to narrow the scope and improve accuracy. + +4. **Handle multi-step processes**: For complex workflows, break them down into multiple tool calls, each handling a specific step. + +5. **Error handling**: Implement appropriate error handling in your agent's logic to deal with potential issues like elements not found or pages not loading. + +## Troubleshooting + +- **Session not starting**: Ensure you have valid API keys for both Browserbase and your LLM provider. +- **Elements not found**: Try increasing the `dom_settle_timeout_ms` parameter to give the page more time to load. +- **Actions not working**: Make sure your instructions are clear and specific. You may need to use `observe` first to identify the correct elements. +- **Extract returning incomplete data**: Try refining your instruction or providing a more specific selector. + +## Resources + +- [Stagehand Documentation](https://docs.stagehand.dev/reference/introduction) - Complete reference for the Stagehand framework +- [Browserbase](https://www.browserbase.com) - Browser automation platform +- [Join Slack Community](https://stagehand.dev/slack) - Get help and connect with other users of Stagehand + +## Contact + +For more information about Stagehand, visit [the Stagehand documentation](https://docs.stagehand.dev/). + +For questions about the CrewAI integration, join our [Slack](https://stagehand.dev/slack) or open an issue in this repository. \ No newline at end of file diff --git a/crewai_tools/tools/stagehand_tool/__init__.py b/crewai_tools/tools/stagehand_tool/__init__.py new file mode 100644 index 000000000..2b3e24856 --- /dev/null +++ b/crewai_tools/tools/stagehand_tool/__init__.py @@ -0,0 +1,3 @@ +from .stagehand_tool import StagehandTool + +__all__ = ["StagehandTool"] diff --git a/crewai_tools/tools/stagehand_tool/example.py b/crewai_tools/tools/stagehand_tool/example.py new file mode 100644 index 000000000..0d9735cad --- /dev/null +++ b/crewai_tools/tools/stagehand_tool/example.py @@ -0,0 +1,116 @@ +""" +StagehandTool Example + +This example demonstrates how to use the StagehandTool in a CrewAI workflow. +It shows how to use the three main primitives: act, extract, and observe. + +Prerequisites: +1. A Browserbase account with API key and project ID +2. An LLM API key (OpenAI or Anthropic) +3. Installed dependencies: crewai, crewai-tools, stagehand-py + +Usage: +- Set your API keys in environment variables (recommended) +- Or modify the script to include your API keys directly +- Run the script: python stagehand_example.py +""" + +import os + +from crewai import Agent, Crew, Process, Task +from dotenv import load_dotenv +from stagehand.schemas import AvailableModel + +from crewai_tools import StagehandTool + +# Load environment variables from .env file +load_dotenv() + +# Get API keys from environment variables +# You can set these in your shell or in a .env file +browserbase_api_key = os.environ.get("BROWSERBASE_API_KEY") +browserbase_project_id = os.environ.get("BROWSERBASE_PROJECT_ID") +model_api_key = os.environ.get("OPENAI_API_KEY") # or OPENAI_API_KEY + +# Initialize the StagehandTool with your credentials and use context manager +with StagehandTool( + api_key=browserbase_api_key, # New parameter naming + project_id=browserbase_project_id, # New parameter naming + model_api_key=model_api_key, + model_name=AvailableModel.GPT_4O, # Using the enum from schemas +) as stagehand_tool: + # Create a web researcher agent with the StagehandTool + researcher = Agent( + role="Web Researcher", + goal="Find and extract information from websites using different Stagehand primitives", + backstory=( + "You are an expert web automation agent equipped with the StagehandTool. " + "Your primary function is to interact with websites based on natural language instructions. " + "You must carefully choose the correct command (`command_type`) for each task:\n" + "- Use 'act' (the default) for general interactions like clicking buttons ('Click the login button'), " + "filling forms ('Fill the form with username user and password pass'), scrolling, or navigating within the site.\n" + "- Use 'navigate' specifically when you need to go to a new web page; you MUST provide the target URL " + "in the `url` parameter along with the instruction (e.g., instruction='Go to Google', url='https://google.com').\n" + "- Use 'extract' when the goal is to pull structured data from the page. Provide a clear `instruction` " + "describing what data to extract (e.g., 'Extract all product names and prices').\n" + "- Use 'observe' to identify and analyze elements on the current page based on an `instruction` " + "(e.g., 'Find all images in the main content area').\n\n" + "Remember to break down complex tasks into simple, sequential steps in your `instruction`. For example, " + "instead of 'Search for OpenAI on Google and click the first result', use multiple steps with the tool:\n" + "1. Use 'navigate' with url='https://google.com'.\n" + "2. Use 'act' with instruction='Type OpenAI in the search bar'.\n" + "3. Use 'act' with instruction='Click the search button'.\n" + "4. Use 'act' with instruction='Click the first search result link for OpenAI'.\n\n" + "Always be precise in your instructions and choose the most appropriate command and parameters (`instruction`, `url`, `command_type`, `selector`) for the task at hand." + ), + llm="gpt-4o", + verbose=True, + allow_delegation=False, + tools=[stagehand_tool], + ) + + # Define a research task that demonstrates all three primitives + research_task = Task( + description=( + "Demonstrate Stagehand capabilities by performing the following steps:\n" + "1. Go to https://www.stagehand.dev\n" + "2. Extract all the text content from the page\n" + "3. Find the Docs link and click on it\n" + "4. Go to https://httpbin.org/forms/post and observe what elements are available on the page\n" + "5. Provide a summary of what you learned about using these different commands" + ), + expected_output=( + "A demonstration of all three Stagehand primitives (act, extract, observe) " + "with examples of how each was used and what information was gathered." + ), + agent=researcher, + ) + + # Alternative task: Real research using the primitives + web_research_task = Task( + description=( + "Go to google.com and search for 'Stagehand'.\n" + "Then extract the first search result." + ), + expected_output=( + "A summary report about Stagehand's capabilities and pricing, demonstrating how " + "the different primitives can be used together for effective web research." + ), + agent=researcher, + ) + + # Set up the crew + crew = Crew( + agents=[researcher], + tasks=[research_task], # You can switch this to web_research_task if you prefer + verbose=True, + process=Process.sequential, + ) + + # Run the crew and get the result + result = crew.kickoff() + + print("\n==== RESULTS ====\n") + print(result) + +# Resources are automatically cleaned up when exiting the context manager diff --git a/crewai_tools/tools/stagehand_tool/stagehand_tool.py b/crewai_tools/tools/stagehand_tool/stagehand_tool.py new file mode 100644 index 000000000..d3a61f914 --- /dev/null +++ b/crewai_tools/tools/stagehand_tool/stagehand_tool.py @@ -0,0 +1,732 @@ +import asyncio +import json +import os +import re +from typing import Any, Dict, List, Optional, Type, Union + +from pydantic import BaseModel, Field + +# Define a flag to track whether stagehand is available +_HAS_STAGEHAND = False + +try: + from stagehand import Stagehand, StagehandConfig, StagehandPage, configure_logging + from stagehand.schemas import ( + ActOptions, + AvailableModel, + ExtractOptions, + ObserveOptions, + ) + + _HAS_STAGEHAND = True +except ImportError: + # Define type stubs for when stagehand is not installed + Stagehand = Any + StagehandPage = Any + StagehandConfig = Any + ActOptions = Any + ExtractOptions = Any + ObserveOptions = Any + + # Mock configure_logging function + def configure_logging(level=None, remove_logger_name=None, quiet_dependencies=None): + pass + + # Define only what's needed for class defaults + class AvailableModel: + CLAUDE_3_7_SONNET_LATEST = "anthropic.claude-3-7-sonnet-20240607" + + +from crewai.tools import BaseTool + + +class StagehandResult(BaseModel): + """Result from a Stagehand operation. + + Attributes: + success: Whether the operation completed successfully + data: The result data from the operation + error: Optional error message if the operation failed + """ + + success: bool = Field( + ..., description="Whether the operation completed successfully" + ) + data: Union[str, Dict, List] = Field( + ..., description="The result data from the operation" + ) + error: Optional[str] = Field( + None, description="Optional error message if the operation failed" + ) + + +class StagehandToolSchema(BaseModel): + """Input for StagehandTool.""" + + instruction: Optional[str] = Field( + None, + description="Single atomic action with location context. For reliability on complex pages, use ONE specific action with location hints. Good examples: 'Click the search input field in the header', 'Type Italy in the focused field', 'Press Enter', 'Click the first link in the results area'. Avoid combining multiple actions. For 'navigate' command type, this can be omitted if only URL is provided.", + ) + url: Optional[str] = Field( + None, + description="The URL to navigate to before executing the instruction. MUST be used with 'navigate' command. ", + ) + command_type: Optional[str] = Field( + "act", + description="""The type of command to execute (choose one): + - 'act': Perform an action like clicking buttons, filling forms, etc. (default) + - 'navigate': Specifically navigate to a URL + - 'extract': Extract structured data from the page + - 'observe': Identify and analyze elements on the page + """, + ) + + +class StagehandTool(BaseTool): + """ + A tool that uses Stagehand to automate web browser interactions using natural language with atomic action handling. + + Stagehand allows AI agents to interact with websites through a browser, + performing actions like clicking buttons, filling forms, and extracting data. + + The tool supports four main command types: + 1. act - Perform actions like clicking, typing, scrolling, or navigating + 2. navigate - Specifically navigate to a URL (shorthand for act with navigation) + 3. extract - Extract structured data from web pages + 4. observe - Identify and analyze elements on a page + + Usage examples: + - Navigate to a website: instruction="Go to the homepage", url="https://example.com" + - Click a button: instruction="Click the login button" + - Fill a form: instruction="Fill the login form with username 'user' and password 'pass'" + - Extract data: instruction="Extract all product prices and names", command_type="extract" + - Observe elements: instruction="Find all navigation menu items", command_type="observe" + - Complex tasks: instruction="Step 1: Navigate to https://example.com; Step 2: Scroll down to the 'Features' section; Step 3: Click 'Learn More'", command_type="act" + + Example of breaking down "Search for OpenAI" into multiple steps: + 1. First navigation: instruction="Go to Google", url="https://google.com", command_type="navigate" + 2. Enter search term: instruction="Type 'OpenAI' in the search box", command_type="act" + 3. Submit search: instruction="Press the Enter key or click the search button", command_type="act" + 4. Click on result: instruction="Click on the OpenAI website link in the search results", command_type="act" + """ + + name: str = "Web Automation Tool" + description: str = """Use this tool to control a web browser and interact with websites using natural language. + + Capabilities: + - Navigate to websites and follow links + - Click buttons, links, and other elements + - Fill in forms and input fields + - Search within websites + - Extract information from web pages + - Identify and analyze elements on a page + + To use this tool, provide a natural language instruction describing what you want to do. + For reliability on complex pages, use specific, atomic instructions with location hints: + - Good: "Click the search box in the header" + - Good: "Type 'Italy' in the focused field" + - Bad: "Search for Italy and click the first result" + + For different types of tasks, specify the command_type: + - 'act': For performing one atomic action (default) + - 'navigate': For navigating to a URL + - 'extract': For getting data from a specific page section + - 'observe': For finding elements in a specific area + """ + args_schema: Type[BaseModel] = StagehandToolSchema + + # Stagehand configuration + api_key: Optional[str] = None + project_id: Optional[str] = None + model_api_key: Optional[str] = None + model_name: Optional[AvailableModel] = AvailableModel.CLAUDE_3_7_SONNET_LATEST + server_url: Optional[str] = "https://api.stagehand.browserbase.com/v1" + headless: bool = False + dom_settle_timeout_ms: int = 3000 + self_heal: bool = True + wait_for_captcha_solves: bool = True + verbose: int = 1 + + # Token management settings + max_retries_on_token_limit: int = 3 + use_simplified_dom: bool = True + + # Instance variables + _stagehand: Optional[Stagehand] = None + _page: Optional[StagehandPage] = None + _session_id: Optional[str] = None + _testing: bool = False + + def __init__( + self, + api_key: Optional[str] = None, + project_id: Optional[str] = None, + model_api_key: Optional[str] = None, + model_name: Optional[str] = None, + server_url: Optional[str] = None, + session_id: Optional[str] = None, + headless: Optional[bool] = None, + dom_settle_timeout_ms: Optional[int] = None, + self_heal: Optional[bool] = None, + wait_for_captcha_solves: Optional[bool] = None, + verbose: Optional[int] = None, + _testing: bool = False, + **kwargs, + ): + # Set testing flag early so that other init logic can rely on it + self._testing = _testing + super().__init__(**kwargs) + + # Set up logger + import logging + + self._logger = logging.getLogger(__name__) + + # Set configuration from parameters or environment + self.api_key = api_key or os.getenv("BROWSERBASE_API_KEY") + self.project_id = project_id or os.getenv("BROWSERBASE_PROJECT_ID") + + if model_api_key: + self.model_api_key = model_api_key + if model_name: + self.model_name = model_name + if server_url: + self.server_url = server_url + if headless is not None: + self.headless = headless + if dom_settle_timeout_ms is not None: + self.dom_settle_timeout_ms = dom_settle_timeout_ms + if self_heal is not None: + self.self_heal = self_heal + if wait_for_captcha_solves is not None: + self.wait_for_captcha_solves = wait_for_captcha_solves + if verbose is not None: + self.verbose = verbose + + self._session_id = session_id + + # Configure logging based on verbosity level + if not self._testing: + log_level = {1: "INFO", 2: "WARNING", 3: "DEBUG"}.get(self.verbose, "ERROR") + configure_logging( + level=log_level, remove_logger_name=True, quiet_dependencies=True + ) + + self._check_required_credentials() + + def _check_required_credentials(self): + """Validate that required credentials are present.""" + if not self._testing and not _HAS_STAGEHAND: + raise ImportError( + "`stagehand` package not found, please run `uv add stagehand`" + ) + + if not self.api_key: + raise ValueError("api_key is required (or set BROWSERBASE_API_KEY in env).") + if not self.project_id: + raise ValueError( + "project_id is required (or set BROWSERBASE_PROJECT_ID in env)." + ) + + def __del__(self): + """Ensure cleanup on deletion""" + try: + self.close() + except Exception: + pass + + def _get_model_api_key(self): + """Get the appropriate API key based on the model being used.""" + # Check model type and get appropriate key + model_str = str(self.model_name) + if "gpt" in model_str.lower(): + return self.model_api_key or os.getenv("OPENAI_API_KEY") + elif "claude" in model_str.lower() or "anthropic" in model_str.lower(): + return self.model_api_key or os.getenv("ANTHROPIC_API_KEY") + elif "gemini" in model_str.lower(): + return self.model_api_key or os.getenv("GOOGLE_API_KEY") + else: + # Default to trying OpenAI, then Anthropic + return ( + self.model_api_key + or os.getenv("OPENAI_API_KEY") + or os.getenv("ANTHROPIC_API_KEY") + ) + + async def _setup_stagehand(self, session_id: Optional[str] = None): + """Initialize Stagehand if not already set up.""" + + # If we're in testing mode, return mock objects + if self._testing: + if not self._stagehand: + # Create mock objects for testing + class MockPage: + async def act(self, options): + mock_result = type("MockResult", (), {})() + mock_result.model_dump = lambda: { + "message": "Action completed successfully" + } + return mock_result + + async def goto(self, url): + return None + + async def extract(self, options): + mock_result = type("MockResult", (), {})() + mock_result.model_dump = lambda: {"data": "Extracted content"} + return mock_result + + async def observe(self, options): + mock_result1 = type( + "MockResult", + (), + {"description": "Test element", "method": "click"}, + )() + return [mock_result1] + + async def wait_for_load_state(self, state): + return None + + class MockStagehand: + def __init__(self): + self.page = MockPage() + self.session_id = "test-session-id" + + async def init(self): + return None + + async def close(self): + return None + + self._stagehand = MockStagehand() + await self._stagehand.init() + self._page = self._stagehand.page + self._session_id = self._stagehand.session_id + + return self._stagehand, self._page + + # Normal initialization for non-testing mode + if not self._stagehand: + # Get the appropriate API key based on model type + model_api_key = self._get_model_api_key() + + if not model_api_key: + raise ValueError( + "No appropriate API key found for model. Please set OPENAI_API_KEY, ANTHROPIC_API_KEY, or GOOGLE_API_KEY" + ) + + # Build the StagehandConfig with proper parameter names + config = StagehandConfig( + env="BROWSERBASE", + apiKey=self.api_key, # Browserbase API key (camelCase) + projectId=self.project_id, # Browserbase project ID (camelCase) + modelApiKey=model_api_key, # LLM API key - auto-detected based on model + modelName=self.model_name, + apiUrl=self.server_url + if self.server_url + else "https://api.stagehand.browserbase.com/v1", + domSettleTimeoutMs=self.dom_settle_timeout_ms, + selfHeal=self.self_heal, + waitForCaptchaSolves=self.wait_for_captcha_solves, + verbose=self.verbose, + browserbaseSessionID=session_id or self._session_id, + ) + + # Initialize Stagehand with config + self._stagehand = Stagehand(config=config) + + # Initialize the Stagehand instance + await self._stagehand.init() + self._page = self._stagehand.page + self._session_id = self._stagehand.session_id + + return self._stagehand, self._page + + def _extract_steps(self, instruction: str) -> List[str]: + """Extract individual steps from multi-step instructions""" + # Check for numbered steps (Step 1:, Step 2:, etc.) + if re.search(r"Step \d+:", instruction, re.IGNORECASE): + steps = re.findall( + r"Step \d+:\s*([^;]+?)(?=Step \d+:|$)", + instruction, + re.IGNORECASE | re.DOTALL, + ) + return [step.strip() for step in steps if step.strip()] + # Check for semicolon-separated instructions + elif ";" in instruction: + return [step.strip() for step in instruction.split(";") if step.strip()] + else: + return [instruction] + + def _simplify_instruction(self, instruction: str) -> str: + """Simplify complex instructions to basic actions""" + # Extract the core action from complex instructions + instruction_lower = instruction.lower() + + if "search" in instruction_lower and "click" in instruction_lower: + # For search tasks, focus on the search action first + if "type" in instruction_lower or "enter" in instruction_lower: + return "click on the search input field" + else: + return "search for content on the page" + elif "click" in instruction_lower: + # Extract what to click + if "button" in instruction_lower: + return "click the button" + elif "link" in instruction_lower: + return "click the link" + elif "search" in instruction_lower: + return "click the search field" + else: + return "click on the element" + elif "type" in instruction_lower or "enter" in instruction_lower: + return "type in the input field" + else: + return instruction # Return as-is if can't simplify + + async def _async_run( + self, + instruction: Optional[str] = None, + url: Optional[str] = None, + command_type: str = "act", + ): + """Override _async_run with improved atomic action handling""" + + # Handle missing instruction based on command type + if not instruction: + if command_type == "navigate" and url: + instruction = f"Navigate to {url}" + elif command_type == "observe": + instruction = "Observe elements on the page" + elif command_type == "extract": + instruction = "Extract information from the page" + else: + instruction = "Perform the requested action" + + # For testing mode, use parent implementation + if self._testing: + return await super()._async_run(instruction, url, command_type) + + try: + _, page = await self._setup_stagehand(self._session_id) + + self._logger.info( + f"Executing {command_type} with instruction: {instruction}" + ) + + # Get the API key to pass to model operations + model_api_key = self._get_model_api_key() + model_client_options = {"apiKey": model_api_key} + + # Always navigate first if URL is provided and we're doing actions + if url and command_type.lower() == "act": + self._logger.info(f"Navigating to {url} before performing actions") + await page.goto(url) + await page.wait_for_load_state("networkidle") + # Small delay to ensure page is fully loaded + await asyncio.sleep(1) + + # Process according to command type + if command_type.lower() == "act": + # Extract steps from complex instructions + steps = self._extract_steps(instruction) + self._logger.info(f"Extracted {len(steps)} steps: {steps}") + + results = [] + for i, step in enumerate(steps): + self._logger.info(f"Executing step {i + 1}/{len(steps)}: {step}") + + try: + # Create act options with API key for each step + from stagehand.schemas import ActOptions + + act_options = ActOptions( + action=step, + modelName=self.model_name, + domSettleTimeoutMs=self.dom_settle_timeout_ms, + modelClientOptions=model_client_options, + ) + + result = await page.act(act_options) + results.append(result.model_dump()) + + # Small delay between steps to let DOM settle + if i < len(steps) - 1: # Don't delay after last step + await asyncio.sleep(0.5) + + except Exception as step_error: + error_msg = f"Step failed: {step_error}" + self._logger.warning(f"Step {i + 1} failed: {error_msg}") + + # Try with simplified instruction + try: + simplified = self._simplify_instruction(step) + if simplified != step: + self._logger.info( + f"Retrying with simplified instruction: {simplified}" + ) + + act_options = ActOptions( + action=simplified, + modelName=self.model_name, + domSettleTimeoutMs=self.dom_settle_timeout_ms, + modelClientOptions=model_client_options, + ) + + result = await page.act(act_options) + results.append(result.model_dump()) + else: + # If we can't simplify or retry fails, record the error + results.append({"error": error_msg, "step": step}) + except Exception as retry_error: + self._logger.error(f"Retry also failed: {retry_error}") + results.append({"error": str(retry_error), "step": step}) + + # Return combined results + if len(results) == 1: + # Single step, return as-is + if "error" in results[0]: + return self._format_result( + False, results[0], results[0]["error"] + ) + return self._format_result(True, results[0]) + else: + # Multiple steps, return all results + has_errors = any("error" in result for result in results) + return self._format_result(not has_errors, {"steps": results}) + + elif command_type.lower() == "navigate": + # For navigation, use the goto method directly + if not url: + error_msg = "No URL provided for navigation. Please provide a URL." + self._logger.error(error_msg) + return self._format_result(False, {}, error_msg) + + result = await page.goto(url) + self._logger.info(f"Navigate operation completed to {url}") + return self._format_result( + True, + { + "url": url, + "message": f"Successfully navigated to {url}", + }, + ) + + elif command_type.lower() == "extract": + # Create extract options with API key + from stagehand.schemas import ExtractOptions + + extract_options = ExtractOptions( + instruction=instruction, + modelName=self.model_name, + domSettleTimeoutMs=self.dom_settle_timeout_ms, + useTextExtract=True, + modelClientOptions=model_client_options, # Add API key here + ) + + result = await page.extract(extract_options) + self._logger.info(f"Extract operation completed successfully {result}") + return self._format_result(True, result.model_dump()) + + elif command_type.lower() == "observe": + # Create observe options with API key + from stagehand.schemas import ObserveOptions + + observe_options = ObserveOptions( + instruction=instruction, + modelName=self.model_name, + onlyVisible=True, + domSettleTimeoutMs=self.dom_settle_timeout_ms, + modelClientOptions=model_client_options, # Add API key here + ) + + results = await page.observe(observe_options) + + # Format the observation results + formatted_results = [] + for i, result in enumerate(results): + formatted_results.append( + { + "index": i + 1, + "description": result.description, + "method": result.method, + } + ) + + self._logger.info( + f"Observe operation completed with {len(formatted_results)} elements found" + ) + return self._format_result(True, formatted_results) + + else: + error_msg = f"Unknown command type: {command_type}" + self._logger.error(error_msg) + return self._format_result(False, {}, error_msg) + + except Exception as e: + error_msg = f"Error using Stagehand: {str(e)}" + self._logger.error(f"Operation failed: {error_msg}") + return self._format_result(False, {}, error_msg) + + def _format_result(self, success, data, error=None): + """Helper to format results consistently""" + return StagehandResult(success=success, data=data, error=error) + + def _run( + self, + instruction: Optional[str] = None, + url: Optional[str] = None, + command_type: str = "act", + ) -> str: + """ + Run the Stagehand tool with the given instruction. + + Args: + instruction: Natural language instruction for browser automation + url: Optional URL to navigate to before executing the instruction + command_type: Type of command to execute ('act', 'extract', or 'observe') + + Returns: + The result of the browser automation task + """ + # Handle missing instruction based on command type + if not instruction: + if command_type == "navigate" and url: + instruction = f"Navigate to {url}" + elif command_type == "observe": + instruction = "Observe elements on the page" + elif command_type == "extract": + instruction = "Extract information from the page" + else: + instruction = "Perform the requested action" + # Create an event loop if we're not already in one + try: + loop = asyncio.get_event_loop() + if loop.is_running(): + # We're in an existing event loop, use it + import concurrent.futures + + with concurrent.futures.ThreadPoolExecutor() as executor: + future = executor.submit( + asyncio.run, self._async_run(instruction, url, command_type) + ) + result = future.result() + else: + # We have a loop but it's not running + result = loop.run_until_complete( + self._async_run(instruction, url, command_type) + ) + + # Format the result for output + if result.success: + if command_type.lower() == "act": + if isinstance(result.data, dict) and "steps" in result.data: + # Multiple steps + step_messages = [] + for i, step in enumerate(result.data["steps"]): + if "error" in step: + step_messages.append( + f"Step {i + 1}: Failed - {step['error']}" + ) + else: + step_messages.append( + f"Step {i + 1}: {step.get('message', 'Completed')}" + ) + return "\n".join(step_messages) + else: + return ( + f"Action result: {result.data.get('message', 'Completed')}" + ) + elif command_type.lower() == "extract": + return f"Extracted data: {json.dumps(result.data, indent=2)}" + elif command_type.lower() == "observe": + formatted_results = [] + for element in result.data: + formatted_results.append( + f"Element {element['index']}: {element['description']}" + ) + if element.get("method"): + formatted_results.append( + f"Suggested action: {element['method']}" + ) + return "\n".join(formatted_results) + else: + return json.dumps(result.data, indent=2) + else: + return f"Error: {result.error}" + + except RuntimeError: + # No event loop exists, create one + result = asyncio.run(self._async_run(instruction, url, command_type)) + + if result.success: + if isinstance(result.data, dict): + return json.dumps(result.data, indent=2) + else: + return str(result.data) + else: + return f"Error: {result.error}" + + async def _async_close(self): + """Asynchronously clean up Stagehand resources.""" + # Skip for test mode + if self._testing: + self._stagehand = None + self._page = None + return + + if self._stagehand: + await self._stagehand.close() + self._stagehand = None + if self._page: + self._page = None + + def close(self): + """Clean up Stagehand resources.""" + # Skip actual closing for testing mode + if self._testing: + self._stagehand = None + self._page = None + return + + if self._stagehand: + try: + # Handle both synchronous and asynchronous cases + if hasattr(self._stagehand, "close"): + if asyncio.iscoroutinefunction(self._stagehand.close): + try: + loop = asyncio.get_event_loop() + if loop.is_running(): + import concurrent.futures + + with ( + concurrent.futures.ThreadPoolExecutor() as executor + ): + future = executor.submit( + asyncio.run, self._async_close() + ) + future.result() + else: + loop.run_until_complete(self._async_close()) + except RuntimeError: + asyncio.run(self._async_close()) + else: + # Handle non-async close method (for mocks) + self._stagehand.close() + except Exception as e: + # Log but don't raise - we're cleaning up + print(f"Error closing Stagehand: {str(e)}") + + self._stagehand = None + + if self._page: + self._page = None + + def __enter__(self): + """Enter the context manager.""" + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + """Exit the context manager and clean up resources.""" + self.close() + diff --git a/crewai_tools/tools/tavily_extractor_tool/README.md b/crewai_tools/tools/tavily_extractor_tool/README.md new file mode 100644 index 000000000..8e2794dd1 --- /dev/null +++ b/crewai_tools/tools/tavily_extractor_tool/README.md @@ -0,0 +1,99 @@ +# TavilyExtractorTool + +## Description + +The `TavilyExtractorTool` allows CrewAI agents to extract structured content from web pages using the Tavily API. It can process single URLs or lists of URLs and provides options for controlling the extraction depth and including images. + +## Installation + +To use the `TavilyExtractorTool`, you need to install the `tavily-python` library: + +```shell +pip install 'crewai[tools]' tavily-python +``` + +You also need to set your Tavily API key as an environment variable: + +```bash +export TAVILY_API_KEY='your-tavily-api-key' +``` + +## Example + +Here's how to initialize and use the `TavilyExtractorTool` within a CrewAI agent: + +```python +import os +from crewai import Agent, Task, Crew +from crewai_tools import TavilyExtractorTool + +# Ensure TAVILY_API_KEY is set in your environment +# os.environ["TAVILY_API_KEY"] = "YOUR_API_KEY" + +# Initialize the tool +tavily_tool = TavilyExtractorTool() + +# Create an agent that uses the tool +extractor_agent = Agent( + role='Web Content Extractor', + goal='Extract key information from specified web pages', + backstory='You are an expert at extracting relevant content from websites using the Tavily API.', + tools=[tavily_tool], + verbose=True +) + +# Define a task for the agent +extract_task = Task( + description='Extract the main content from the URL https://example.com using basic extraction depth.', + expected_output='A JSON string containing the extracted content from the URL.', + agent=extractor_agent, + tool_inputs={ + 'urls': 'https://example.com', + 'extract_depth': 'basic' + } +) + +# Create and run the crew +crew = Crew( + agents=[extractor_agent], + tasks=[extract_task], + verbose=2 +) + +result = crew.kickoff() +print(result) + +# Example with multiple URLs and advanced extraction +extract_multiple_task = Task( + description='Extract content from https://example.com and https://anotherexample.org using advanced extraction.', + expected_output='A JSON string containing the extracted content from both URLs.', + agent=extractor_agent, + tool_inputs={ + 'urls': ['https://example.com', 'https://anotherexample.org'], + 'extract_depth': 'advanced', + 'include_images': True + } +) + +result_multiple = crew.kickoff(inputs={'urls': ['https://example.com', 'https://anotherexample.org'], 'extract_depth': 'advanced', 'include_images': True}) # If task doesn't specify inputs directly +print(result_multiple) + +``` + +## Arguments + +The `TavilyExtractorTool` accepts the following arguments during initialization or when running the tool: + +- `api_key` (Optional[str]): Your Tavily API key. If not provided during initialization, it defaults to the `TAVILY_API_KEY` environment variable. +- `proxies` (Optional[dict[str, str]]): Proxies to use for the API requests. Defaults to `None`. + +When running the tool (`_run` or `_arun` methods, or via agent execution), it uses the `TavilyExtractorToolSchema` and expects the following inputs: + +- `urls` (Union[List[str], str]): **Required**. A single URL string or a list of URL strings to extract data from. +- `include_images` (Optional[bool]): Whether to include images in the extraction results. Defaults to `False`. +- `extract_depth` (Literal["basic", "advanced"]): The depth of extraction. Use `"basic"` for faster, surface-level extraction or `"advanced"` for more comprehensive extraction. Defaults to `"basic"`. +- `timeout` (int): The maximum time in seconds to wait for the extraction request to complete. Defaults to `60`. + +## Response Format + +The tool returns a JSON string representing the structured data extracted from the provided URL(s). The exact structure depends on the content of the pages and the `extract_depth` used. Refer to the [Tavily API documentation](https://docs.tavily.com/docs/tavily-api/python-sdk#extract) for details on the response structure. diff --git a/crewai_tools/tools/tavily_extractor_tool/tavily_extractor_tool.py b/crewai_tools/tools/tavily_extractor_tool/tavily_extractor_tool.py new file mode 100644 index 000000000..5e8a760ee --- /dev/null +++ b/crewai_tools/tools/tavily_extractor_tool/tavily_extractor_tool.py @@ -0,0 +1,170 @@ +from crewai.tools import BaseTool, EnvVar +from pydantic import BaseModel, Field +from typing import Optional, Type, Any, Union, List, Literal +from dotenv import load_dotenv +import os +import json + +load_dotenv() +try: + from tavily import TavilyClient, AsyncTavilyClient + + TAVILY_AVAILABLE = True +except ImportError: + TAVILY_AVAILABLE = False + TavilyClient = Any + AsyncTavilyClient = Any + + +class TavilyExtractorToolSchema(BaseModel): + """Input schema for TavilyExtractorTool.""" + + urls: Union[List[str], str] = Field( + ..., + description="The URL(s) to extract data from. Can be a single URL or a list of URLs.", + ) + + +class TavilyExtractorTool(BaseTool): + package_dependencies: List[str] = ["tavily-python"] + env_vars: List[EnvVar] = [ + EnvVar(name="TAVILY_API_KEY", description="API key for Tavily extraction service", required=True), + ] + """ + Tool that uses the Tavily API to extract content from web pages. + + Attributes: + client: Synchronous Tavily client. + async_client: Asynchronous Tavily client. + name: The name of the tool. + description: The description of the tool. + args_schema: The schema for the tool's arguments. + api_key: The Tavily API key. + proxies: Optional proxies for the API requests. + include_images: Whether to include images in the extraction. + extract_depth: The depth of extraction. + timeout: The timeout for the extraction request in seconds. + """ + + model_config = {"arbitrary_types_allowed": True} + client: Optional[TavilyClient] = None + async_client: Optional[AsyncTavilyClient] = None + name: str = "TavilyExtractorTool" + description: str = "Extracts content from one or more web pages using the Tavily API. Returns structured data." + args_schema: Type[BaseModel] = TavilyExtractorToolSchema + api_key: Optional[str] = Field( + default_factory=lambda: os.getenv("TAVILY_API_KEY"), + description="The Tavily API key. If not provided, it will be loaded from the environment variable TAVILY_API_KEY.", + ) + proxies: Optional[dict[str, str]] = Field( + default=None, + description="Optional proxies to use for the Tavily API requests.", + ) + include_images: bool = Field( + default=False, + description="Whether to include images in the extraction.", + ) + extract_depth: Literal["basic", "advanced"] = Field( + default="basic", + description="The depth of extraction. 'basic' for basic extraction, 'advanced' for advanced extraction.", + ) + timeout: int = Field( + default=60, + description="The timeout for the extraction request in seconds.", + ) + + def __init__(self, **kwargs: Any): + """ + Initializes the TavilyExtractorTool. + + Args: + **kwargs: Additional keyword arguments. + """ + super().__init__(**kwargs) + if TAVILY_AVAILABLE: + self.client = TavilyClient(api_key=self.api_key, proxies=self.proxies) + self.async_client = AsyncTavilyClient( + api_key=self.api_key, proxies=self.proxies + ) + else: + try: + import click + import subprocess + except ImportError: + raise ImportError( + "The 'tavily-python' package is required. 'click' and 'subprocess' are also needed to assist with installation if the package is missing. " + "Please install 'tavily-python' manually (e.g., 'uv add tavily-python') and ensure 'click' and 'subprocess' are available." + ) + + if click.confirm( + "You are missing the 'tavily-python' package, which is required for TavilyExtractorTool. Would you like to install it?" + ): + try: + subprocess.run(["pip", "install", "tavily-python"], check=True) + raise ImportError( + "'tavily-python' has been installed. Please restart your Python application to use the TavilyExtractorTool." + ) + except subprocess.CalledProcessError as e: + raise ImportError( + f"Attempted to install 'tavily-python' but failed: {e}. " + f"Please install it manually to use the TavilyExtractorTool." + ) + else: + raise ImportError( + "The 'tavily-python' package is required to use the TavilyExtractorTool. " + "Please install it with: uv add tavily-python" + ) + + def _run( + self, + urls: Union[List[str], str], + ) -> str: + """ + Synchronously extracts content from the given URL(s). + + Args: + urls: The URL(s) to extract data from. + + Returns: + A JSON string containing the extracted data. + """ + if not self.client: + raise ValueError( + "Tavily client is not initialized. Ensure 'tavily-python' is installed and API key is set." + ) + + return json.dumps( + self.client.extract( + urls=urls, + extract_depth=self.extract_depth, + include_images=self.include_images, + timeout=self.timeout, + ), + indent=2, + ) + + async def _arun( + self, + urls: Union[List[str], str], + ) -> str: + """ + Asynchronously extracts content from the given URL(s). + + Args: + urls: The URL(s) to extract data from. + + Returns: + A JSON string containing the extracted data. + """ + if not self.async_client: + raise ValueError( + "Tavily async client is not initialized. Ensure 'tavily-python' is installed and API key is set." + ) + + results = await self.async_client.extract( + urls=urls, + extract_depth=self.extract_depth, + include_images=self.include_images, + timeout=self.timeout, + ) + return json.dumps(results, indent=2) diff --git a/crewai_tools/tools/tavily_search_tool/README.md b/crewai_tools/tools/tavily_search_tool/README.md new file mode 100644 index 000000000..185b19887 --- /dev/null +++ b/crewai_tools/tools/tavily_search_tool/README.md @@ -0,0 +1,115 @@ +# Tavily Search Tool + +## Description + +The `TavilySearchTool` provides an interface to the Tavily Search API, enabling CrewAI agents to perform comprehensive web searches. It allows for specifying search depth, topics, time ranges, included/excluded domains, and whether to include direct answers, raw content, or images in the results. The tool returns the search results as a JSON string. + +## Installation + +To use the `TavilySearchTool`, you need to install the `tavily-python` library: + +```shell +pip install 'crewai[tools]' tavily-python +``` + +## Environment Variables + +Ensure your Tavily API key is set as an environment variable: + +```bash +export TAVILY_API_KEY='your_tavily_api_key' +``` + +## Example + +Here's how to initialize and use the `TavilySearchTool` within a CrewAI agent: + +```python +import os +from crewai import Agent, Task, Crew +from crewai_tools import TavilySearchTool + +# Ensure the TAVILY_API_KEY environment variable is set +# os.environ["TAVILY_API_KEY"] = "YOUR_TAVILY_API_KEY" + +# Initialize the tool +tavily_tool = TavilySearchTool() + +# Create an agent that uses the tool +researcher = Agent( + role='Market Researcher', + goal='Find information about the latest AI trends', + backstory='An expert market researcher specializing in technology.', + tools=[tavily_tool], + verbose=True +) + +# Create a task for the agent +research_task = Task( + description='Search for the top 3 AI trends in 2024.', + expected_output='A JSON report summarizing the top 3 AI trends found.', + agent=researcher +) + +# Form the crew and kick it off +crew = Crew( + agents=[researcher], + tasks=[research_task], + verbose=2 +) + +result = crew.kickoff() +print(result) + +# Example of using specific parameters +detailed_search_result = tavily_tool.run( + query="What are the recent advancements in large language models?", + search_depth="advanced", + topic="general", + max_results=5, + include_answer=True +) +print(detailed_search_result) +``` + +## Arguments + +The `TavilySearchTool` accepts the following arguments during initialization or when calling the `run` method: + +- `query` (str): **Required**. The search query string. +- `search_depth` (Literal["basic", "advanced"], optional): The depth of the search. Defaults to `"basic"`. +- `topic` (Literal["general", "news", "finance"], optional): The topic to focus the search on. Defaults to `"general"`. +- `time_range` (Literal["day", "week", "month", "year"], optional): The time range for the search. Defaults to `None`. +- `days` (int, optional): The number of days to search back. Relevant if `time_range` is not set. Defaults to `7`. +- `max_results` (int, optional): The maximum number of search results to return. Defaults to `5`. +- `include_domains` (Sequence[str], optional): A list of domains to prioritize in the search. Defaults to `None`. +- `exclude_domains` (Sequence[str], optional): A list of domains to exclude from the search. Defaults to `None`. +- `include_answer` (Union[bool, Literal["basic", "advanced"]], optional): Whether to include a direct answer synthesized from the search results. Defaults to `False`. +- `include_raw_content` (bool, optional): Whether to include the raw HTML content of the searched pages. Defaults to `False`. +- `include_images` (bool, optional): Whether to include image results. Defaults to `False`. +- `timeout` (int, optional): The request timeout in seconds. Defaults to `60`. +- `api_key` (str, optional): Your Tavily API key. If not provided, it's read from the `TAVILY_API_KEY` environment variable. +- `proxies` (dict[str, str], optional): A dictionary of proxies to use for the API request. Defaults to `None`. + +## Custom Configuration + +You can configure the tool during initialization: + +```python +# Example: Initialize with a default max_results and specific API key +custom_tavily_tool = TavilySearchTool( + api_key="YOUR_SPECIFIC_TAVILY_KEY", + config={ + 'max_results': 10, + 'search_depth': 'advanced' + } +) + +# The agent will use these defaults unless overridden in the task input +agent_with_custom_tool = Agent( + # ... agent configuration ... + tools=[custom_tavily_tool] +) +``` + +Note: The `config` dictionary allows setting default values for the arguments defined in `TavilySearchToolSchema`. These defaults can be overridden when the tool is executed if the specific parameters are provided in the agent's action input. diff --git a/crewai_tools/tools/tavily_search_tool/tavily_search_tool.py b/crewai_tools/tools/tavily_search_tool/tavily_search_tool.py new file mode 100644 index 000000000..2f9d6dcca --- /dev/null +++ b/crewai_tools/tools/tavily_search_tool/tavily_search_tool.py @@ -0,0 +1,249 @@ +from crewai.tools import BaseTool, EnvVar +from pydantic import BaseModel, Field +from typing import Optional, Type, Any, Union, Literal, Sequence, List +from dotenv import load_dotenv +import os +import json + +load_dotenv() +try: + from tavily import TavilyClient, AsyncTavilyClient + + TAVILY_AVAILABLE = True +except ImportError: + TAVILY_AVAILABLE = False + TavilyClient = Any + AsyncTavilyClient = Any + + +class TavilySearchToolSchema(BaseModel): + """Input schema for TavilySearchTool.""" + + query: str = Field(..., description="The search query string.") + + +class TavilySearchTool(BaseTool): + """ + Tool that uses the Tavily Search API to perform web searches. + + Attributes: + client: An instance of TavilyClient. + async_client: An instance of AsyncTavilyClient. + name: The name of the tool. + description: A description of the tool's purpose. + args_schema: The schema for the tool's arguments. + api_key: The Tavily API key. + proxies: Optional proxies for the API requests. + search_depth: The depth of the search. + topic: The topic to focus the search on. + time_range: The time range for the search. + days: The number of days to search back. + max_results: The maximum number of results to return. + include_domains: A list of domains to include in the search. + exclude_domains: A list of domains to exclude from the search. + include_answer: Whether to include a direct answer to the query. + include_raw_content: Whether to include the raw content of the search results. + include_images: Whether to include images in the search results. + timeout: The timeout for the search request in seconds. + max_content_length_per_result: Maximum length for the 'content' of each search result. + """ + + model_config = {"arbitrary_types_allowed": True} + client: Optional[TavilyClient] = None + async_client: Optional[AsyncTavilyClient] = None + name: str = "Tavily Search" + description: str = ( + "A tool that performs web searches using the Tavily Search API. " + "It returns a JSON object containing the search results." + ) + args_schema: Type[BaseModel] = TavilySearchToolSchema + api_key: Optional[str] = Field( + default_factory=lambda: os.getenv("TAVILY_API_KEY"), + description="The Tavily API key. If not provided, it will be loaded from the environment variable TAVILY_API_KEY.", + ) + proxies: Optional[dict[str, str]] = Field( + default=None, + description="Optional proxies to use for the Tavily API requests.", + ) + search_depth: Literal["basic", "advanced"] = Field( + default="basic", description="The depth of the search." + ) + topic: Literal["general", "news", "finance"] = Field( + default="general", description="The topic to focus the search on." + ) + time_range: Optional[Literal["day", "week", "month", "year"]] = Field( + default=None, description="The time range for the search." + ) + days: int = Field(default=7, description="The number of days to search back.") + max_results: int = Field( + default=5, description="The maximum number of results to return." + ) + include_domains: Optional[Sequence[str]] = Field( + default=None, description="A list of domains to include in the search." + ) + exclude_domains: Optional[Sequence[str]] = Field( + default=None, description="A list of domains to exclude from the search." + ) + include_answer: Union[bool, Literal["basic", "advanced"]] = Field( + default=False, description="Whether to include a direct answer to the query." + ) + include_raw_content: bool = Field( + default=False, + description="Whether to include the raw content of the search results.", + ) + include_images: bool = Field( + default=False, description="Whether to include images in the search results." + ) + timeout: int = Field( + default=60, description="The timeout for the search request in seconds." + ) + max_content_length_per_result: int = Field( + default=1000, + description="Maximum length for the 'content' of each search result to avoid context window issues.", + ) + package_dependencies: List[str] = ["tavily-python"] + env_vars: List[EnvVar] = [ + EnvVar(name="TAVILY_API_KEY", description="API key for Tavily search service", required=True), + ] + + def __init__(self, **kwargs: Any): + super().__init__(**kwargs) + if TAVILY_AVAILABLE: + self.client = TavilyClient(api_key=self.api_key, proxies=self.proxies) + self.async_client = AsyncTavilyClient( + api_key=self.api_key, proxies=self.proxies + ) + else: + try: + import click + import subprocess + except ImportError: + raise ImportError( + "The 'tavily-python' package is required. 'click' and 'subprocess' are also needed to assist with installation if the package is missing. " + "Please install 'tavily-python' manually (e.g., 'pip install tavily-python') and ensure 'click' and 'subprocess' are available." + ) + + if click.confirm( + "You are missing the 'tavily-python' package, which is required for TavilySearchTool. Would you like to install it?" + ): + try: + subprocess.run(["uv", "add", "tavily-python"], check=True) + raise ImportError( + "'tavily-python' has been installed. Please restart your Python application to use the TavilySearchTool." + ) + except subprocess.CalledProcessError as e: + raise ImportError( + f"Attempted to install 'tavily-python' but failed: {e}. " + f"Please install it manually to use the TavilySearchTool." + ) + else: + raise ImportError( + "The 'tavily-python' package is required to use the TavilySearchTool. " + "Please install it with: uv add tavily-python" + ) + + def _run( + self, + query: str, + ) -> str: + """ + Synchronously performs a search using the Tavily API. + Content of each result is truncated to `max_content_length_per_result`. + + Args: + query: The search query string. + + Returns: + A JSON string containing the search results with truncated content. + """ + if not self.client: + raise ValueError( + "Tavily client is not initialized. Ensure 'tavily-python' is installed and API key is set." + ) + + raw_results = self.client.search( + query=query, + search_depth=self.search_depth, + topic=self.topic, + time_range=self.time_range, + days=self.days, + max_results=self.max_results, + include_domains=self.include_domains, + exclude_domains=self.exclude_domains, + include_answer=self.include_answer, + include_raw_content=self.include_raw_content, + include_images=self.include_images, + timeout=self.timeout, + ) + + if ( + isinstance(raw_results, dict) + and "results" in raw_results + and isinstance(raw_results["results"], list) + ): + for item in raw_results["results"]: + if ( + isinstance(item, dict) + and "content" in item + and isinstance(item["content"], str) + ): + if len(item["content"]) > self.max_content_length_per_result: + item["content"] = ( + item["content"][: self.max_content_length_per_result] + + "..." + ) + + return json.dumps(raw_results, indent=2) + + async def _arun( + self, + query: str, + ) -> str: + """ + Asynchronously performs a search using the Tavily API. + Content of each result is truncated to `max_content_length_per_result`. + + Args: + query: The search query string. + + Returns: + A JSON string containing the search results with truncated content. + """ + if not self.async_client: + raise ValueError( + "Tavily async client is not initialized. Ensure 'tavily-python' is installed and API key is set." + ) + + raw_results = await self.async_client.search( + query=query, + search_depth=self.search_depth, + topic=self.topic, + time_range=self.time_range, + days=self.days, + max_results=self.max_results, + include_domains=self.include_domains, + exclude_domains=self.exclude_domains, + include_answer=self.include_answer, + include_raw_content=self.include_raw_content, + include_images=self.include_images, + timeout=self.timeout, + ) + + if ( + isinstance(raw_results, dict) + and "results" in raw_results + and isinstance(raw_results["results"], list) + ): + for item in raw_results["results"]: + if ( + isinstance(item, dict) + and "content" in item + and isinstance(item["content"], str) + ): + if len(item["content"]) > self.max_content_length_per_result: + item["content"] = ( + item["content"][: self.max_content_length_per_result] + + "..." + ) + + return json.dumps(raw_results, indent=2) diff --git a/crewai_tools/tools/txt_search_tool/README.md b/crewai_tools/tools/txt_search_tool/README.md new file mode 100644 index 000000000..aaf68c291 --- /dev/null +++ b/crewai_tools/tools/txt_search_tool/README.md @@ -0,0 +1,59 @@ +# TXTSearchTool + +## Description +This tool is used to perform a RAG (Retrieval-Augmented Generation) search within the content of a text file. It allows for semantic searching of a query within a specified text file's content, making it an invaluable resource for quickly extracting information or finding specific sections of text based on the query provided. + +## Installation +To use the TXTSearchTool, you first need to install the crewai_tools package. This can be done using pip, a package manager for Python. Open your terminal or command prompt and enter the following command: + +```shell +pip install 'crewai[tools]' +``` + +This command will download and install the TXTSearchTool along with any necessary dependencies. + +## Example +The following example demonstrates how to use the TXTSearchTool to search within a text file. This example shows both the initialization of the tool with a specific text file and the subsequent search within that file's content. + +```python +from crewai_tools import TXTSearchTool + +# Initialize the tool to search within any text file's content the agent learns about during its execution +tool = TXTSearchTool() + +# OR + +# Initialize the tool with a specific text file, so the agent can search within the given text file's content +tool = TXTSearchTool(txt='path/to/text/file.txt') +``` + +## Arguments +- `txt` (str): **Optinal**. The path to the text file you want to search. This argument is only required if the tool was not initialized with a specific text file; otherwise, the search will be conducted within the initially provided text file. + +## Custom model and embeddings + +By default, the tool uses OpenAI for both embeddings and summarization. To customize the model, you can use a config dictionary as follows: + +```python +tool = TXTSearchTool( + config=dict( + llm=dict( + provider="ollama", # or google, openai, anthropic, llama2, ... + config=dict( + model="llama2", + # temperature=0.5, + # top_p=1, + # stream=true, + ), + ), + embedder=dict( + provider="google", + config=dict( + model="models/embedding-001", + task_type="retrieval_document", + # title="Embeddings", + ), + ), + ) +) +``` diff --git a/crewai_tools/tools/txt_search_tool/txt_search_tool.py b/crewai_tools/tools/txt_search_tool/txt_search_tool.py new file mode 100644 index 000000000..93d696ab1 --- /dev/null +++ b/crewai_tools/tools/txt_search_tool/txt_search_tool.py @@ -0,0 +1,45 @@ +from typing import Optional, Type + +from pydantic import BaseModel, Field + +from ..rag.rag_tool import RagTool + + +class FixedTXTSearchToolSchema(BaseModel): + """Input for TXTSearchTool.""" + + search_query: str = Field( + ..., + description="Mandatory search query you want to use to search the txt's content", + ) + + +class TXTSearchToolSchema(FixedTXTSearchToolSchema): + """Input for TXTSearchTool.""" + + txt: str = Field(..., description="File path or URL of a TXT file to be searched") + + +class TXTSearchTool(RagTool): + name: str = "Search a txt's content" + description: str = ( + "A tool that can be used to semantic search a query from a txt's content." + ) + args_schema: Type[BaseModel] = TXTSearchToolSchema + + def __init__(self, txt: Optional[str] = None, **kwargs): + super().__init__(**kwargs) + if txt is not None: + self.add(txt) + self.description = f"A tool that can be used to semantic search a query the {txt} txt's content." + self.args_schema = FixedTXTSearchToolSchema + self._generate_description() + + def _run( + self, + search_query: str, + txt: Optional[str] = None, + ) -> str: + if txt is not None: + self.add(txt) + return super()._run(query=search_query) diff --git a/crewai_tools/tools/vision_tool/README.md b/crewai_tools/tools/vision_tool/README.md new file mode 100644 index 000000000..bf7ab7486 --- /dev/null +++ b/crewai_tools/tools/vision_tool/README.md @@ -0,0 +1,30 @@ +# Vision Tool + +## Description + +This tool is used to extract text from images. When passed to the agent it will extract the text from the image and then use it to generate a response, report or any other output. The URL or the PATH of the image should be passed to the Agent. + + +## Installation +Install the crewai_tools package +```shell +pip install 'crewai[tools]' +``` + +## Usage + +In order to use the VisionTool, the OpenAI API key should be set in the environment variable `OPENAI_API_KEY`. + +```python +from crewai_tools import VisionTool + +vision_tool = VisionTool() + +@agent +def researcher(self) -> Agent: + return Agent( + config=self.agents_config["researcher"], + allow_delegation=False, + tools=[vision_tool] + ) +``` diff --git a/crewai_tools/tools/vision_tool/vision_tool.py b/crewai_tools/tools/vision_tool/vision_tool.py new file mode 100644 index 000000000..6df658898 --- /dev/null +++ b/crewai_tools/tools/vision_tool/vision_tool.py @@ -0,0 +1,130 @@ +import base64 +from pathlib import Path +from typing import List, Optional, Type + +from crewai import LLM +from crewai.tools import BaseTool, EnvVar +from pydantic import BaseModel, PrivateAttr, field_validator + + +class ImagePromptSchema(BaseModel): + """Input for Vision Tool.""" + + image_path_url: str = "The image path or URL." + + @field_validator("image_path_url") + def validate_image_path_url(cls, v: str) -> str: + if v.startswith("http"): + return v + + path = Path(v) + if not path.exists(): + raise ValueError(f"Image file does not exist: {v}") + + # Validate supported formats + valid_extensions = {".jpg", ".jpeg", ".png", ".gif", ".webp"} + if path.suffix.lower() not in valid_extensions: + raise ValueError( + f"Unsupported image format. Supported formats: {valid_extensions}" + ) + + return v + + +class VisionTool(BaseTool): + """Tool for analyzing images using vision models. + + Args: + llm: Optional LLM instance to use + model: Model identifier to use if no LLM is provided + """ + + name: str = "Vision Tool" + description: str = ( + "This tool uses OpenAI's Vision API to describe the contents of an image." + ) + args_schema: Type[BaseModel] = ImagePromptSchema + env_vars: List[EnvVar] = [ + EnvVar(name="OPENAI_API_KEY", description="API key for OpenAI services", required=True), + ] + + _model: str = PrivateAttr(default="gpt-4o-mini") + _llm: Optional[LLM] = PrivateAttr(default=None) + + def __init__(self, llm: Optional[LLM] = None, model: str = "gpt-4o-mini", **kwargs): + """Initialize the vision tool. + + Args: + llm: Optional LLM instance to use + model: Model identifier to use if no LLM is provided + **kwargs: Additional arguments for the base tool + """ + super().__init__(**kwargs) + self._model = model + self._llm = llm + + @property + def model(self) -> str: + """Get the current model identifier.""" + return self._model + + @model.setter + def model(self, value: str) -> None: + """Set the model identifier and reset LLM if it was auto-created.""" + self._model = value + if self._llm is not None and self._llm._model != value: + self._llm = None + + @property + def llm(self) -> LLM: + """Get the LLM instance, creating one if needed.""" + if self._llm is None: + self._llm = LLM(model=self._model, stop=["STOP", "END"]) + return self._llm + + def _run(self, **kwargs) -> str: + try: + image_path_url = kwargs.get("image_path_url") + if not image_path_url: + return "Image Path or URL is required." + + ImagePromptSchema(image_path_url=image_path_url) + + if image_path_url.startswith("http"): + image_data = image_path_url + else: + try: + base64_image = self._encode_image(image_path_url) + image_data = f"data:image/jpeg;base64,{base64_image}" + except Exception as e: + return f"Error processing image: {str(e)}" + + response = self.llm.call( + messages=[ + { + "role": "user", + "content": [ + {"type": "text", "text": "What's in this image?"}, + { + "type": "image_url", + "image_url": {"url": image_data}, + }, + ], + }, + ], + ) + return response + except Exception as e: + return f"An error occurred: {str(e)}" + + def _encode_image(self, image_path: str) -> str: + """Encode an image file as base64. + + Args: + image_path: Path to the image file + + Returns: + Base64-encoded image data + """ + with open(image_path, "rb") as image_file: + return base64.b64encode(image_file.read()).decode("utf-8") diff --git a/crewai_tools/tools/weaviate_tool/README.md b/crewai_tools/tools/weaviate_tool/README.md new file mode 100644 index 000000000..c48f2f70a --- /dev/null +++ b/crewai_tools/tools/weaviate_tool/README.md @@ -0,0 +1,80 @@ +# WeaviateVectorSearchTool + +## Description +This tool is specifically crafted for conducting semantic searches within docs within a Weaviate vector database. Use this tool to find semantically similar docs to a given query. + +Weaviate is a vector database that is used to store and query vector embeddings. You can follow their docs here: https://weaviate.io/developers/wcs/connect + +## Installation +Install the crewai_tools package by executing the following command in your terminal: + +```shell +uv pip install 'crewai[tools]' +``` + +## Example +To utilize the WeaviateVectorSearchTool for different use cases, follow these examples: + +```python +from crewai_tools import WeaviateVectorSearchTool + +# To enable the tool to search any website the agent comes across or learns about during its operation +tool = WeaviateVectorSearchTool( + collection_name='example_collections', + limit=3, + weaviate_cluster_url="https://your-weaviate-cluster-url.com", + weaviate_api_key="your-weaviate-api-key", +) + +# or + +# Setup custom model for vectorizer and generative model +tool = WeaviateVectorSearchTool( + collection_name='example_collections', + limit=3, + vectorizer=Configure.Vectorizer.text2vec_openai(model="nomic-embed-text"), + generative_model=Configure.Generative.openai(model="gpt-4o-mini"), + weaviate_cluster_url="https://your-weaviate-cluster-url.com", + weaviate_api_key="your-weaviate-api-key", +) + +# Adding the tool to an agent +rag_agent = Agent( + name="rag_agent", + role="You are a helpful assistant that can answer questions with the help of the WeaviateVectorSearchTool.", + llm="gpt-4o-mini", + tools=[tool], +) +``` + +## Arguments +- `collection_name` : The name of the collection to search within. (Required) +- `weaviate_cluster_url` : The URL of the Weaviate cluster. (Required) +- `weaviate_api_key` : The API key for the Weaviate cluster. (Required) +- `limit` : The number of results to return. (Optional) +- `vectorizer` : The vectorizer to use. (Optional) +- `generative_model` : The generative model to use. (Optional) + +Preloading the Weaviate database with documents: + +```python +from crewai_tools import WeaviateVectorSearchTool + +# Use before hooks to generate the documents and add them to the Weaviate database. Follow the weaviate docs: https://weaviate.io/developers/wcs/connect +test_docs = client.collections.get("example_collections") + + +docs_to_load = os.listdir("knowledge") +with test_docs.batch.dynamic() as batch: + for d in docs_to_load: + with open(os.path.join("knowledge", d), "r") as f: + content = f.read() + batch.add_object( + { + "content": content, + "year": d.split("_")[0], + } + ) +tool = WeaviateVectorSearchTool(collection_name='example_collections', limit=3) + +``` diff --git a/crewai_tools/tools/weaviate_tool/vector_search.py b/crewai_tools/tools/weaviate_tool/vector_search.py new file mode 100644 index 000000000..c75dd03da --- /dev/null +++ b/crewai_tools/tools/weaviate_tool/vector_search.py @@ -0,0 +1,124 @@ +import json +import os +from typing import Any, Optional, Type, List + +try: + import weaviate + from weaviate.classes.config import Configure, Vectorizers + from weaviate.classes.init import Auth + + WEAVIATE_AVAILABLE = True +except ImportError: + WEAVIATE_AVAILABLE = False + weaviate = Any # type placeholder + Configure = Any + Vectorizers = Any + Auth = Any + +from crewai.tools import BaseTool, EnvVar +from pydantic import BaseModel, Field + + +class WeaviateToolSchema(BaseModel): + """Input for WeaviateTool.""" + + query: str = Field( + ..., + description="The query to search retrieve relevant information from the Weaviate database. Pass only the query, not the question.", + ) + + +class WeaviateVectorSearchTool(BaseTool): + """Tool to search the Weaviate database""" + + package_dependencies: List[str] = ["weaviate-client"] + name: str = "WeaviateVectorSearchTool" + description: str = "A tool to search the Weaviate database for relevant information on internal documents." + args_schema: Type[BaseModel] = WeaviateToolSchema + query: Optional[str] = None + vectorizer: Optional[Vectorizers] = None + generative_model: Optional[str] = None + collection_name: Optional[str] = None + limit: Optional[int] = Field(default=3) + headers: Optional[dict] = None + alpha: Optional[int] = Field(default=0.75) + env_vars: List[EnvVar] = [ + EnvVar(name="OPENAI_API_KEY", description="OpenAI API key for embedding generation and retrieval", required=True), + ] + weaviate_cluster_url: str = Field( + ..., + description="The URL of the Weaviate cluster", + ) + weaviate_api_key: str = Field( + ..., + description="The API key for the Weaviate cluster", + ) + package_dependencies: List[str] = ["weaviate-client"] + + def __init__(self, **kwargs): + super().__init__(**kwargs) + if WEAVIATE_AVAILABLE: + openai_api_key = os.environ.get("OPENAI_API_KEY") + if not openai_api_key: + raise ValueError( + "OPENAI_API_KEY environment variable is required for WeaviateVectorSearchTool and it is mandatory to use the tool." + ) + self.headers = {"X-OpenAI-Api-Key": openai_api_key} + self.vectorizer = self.vectorizer or Configure.Vectorizer.text2vec_openai( + model="nomic-embed-text", + ) + self.generative_model = ( + self.generative_model + or Configure.Generative.openai( + model="gpt-4o", + ) + ) + else: + import click + + if click.confirm( + "You are missing the 'weaviate-client' package. Would you like to install it?" + ): + import subprocess + + subprocess.run(["uv", "pip", "install", "weaviate-client"], check=True) + + else: + raise ImportError( + "You are missing the 'weaviate-client' package. Would you like to install it?" + ) + + def _run(self, query: str) -> str: + if not WEAVIATE_AVAILABLE: + raise ImportError( + "You are missing the 'weaviate-client' package. Would you like to install it?" + ) + + if not self.weaviate_cluster_url or not self.weaviate_api_key: + raise ValueError("WEAVIATE_URL or WEAVIATE_API_KEY is not set") + + client = weaviate.connect_to_weaviate_cloud( + cluster_url=self.weaviate_cluster_url, + auth_credentials=Auth.api_key(self.weaviate_api_key), + headers=self.headers, + ) + internal_docs = client.collections.get(self.collection_name) + + if not internal_docs: + internal_docs = client.collections.create( + name=self.collection_name, + vectorizer_config=self.vectorizer, + generative_config=self.generative_model, + ) + + response = internal_docs.query.hybrid( + query=query, + limit=self.limit, + alpha=self.alpha + ) + json_response = "" + for obj in response.objects: + json_response += json.dumps(obj.properties, indent=2) + + client.close() + return json_response diff --git a/crewai_tools/tools/website_search/README.md b/crewai_tools/tools/website_search/README.md new file mode 100644 index 000000000..a86c75b45 --- /dev/null +++ b/crewai_tools/tools/website_search/README.md @@ -0,0 +1,57 @@ +# WebsiteSearchTool + +## Description +This tool is specifically crafted for conducting semantic searches within the content of a particular website. Leveraging a Retrieval-Augmented Generation (RAG) model, it navigates through the information provided on a given URL. Users have the flexibility to either initiate a search across any website known or discovered during its usage or to concentrate the search on a predefined, specific website. + +## Installation +Install the crewai_tools package by executing the following command in your terminal: + +```shell +pip install 'crewai[tools]' +``` + +## Example +To utilize the WebsiteSearchTool for different use cases, follow these examples: + +```python +from crewai_tools import WebsiteSearchTool + +# To enable the tool to search any website the agent comes across or learns about during its operation +tool = WebsiteSearchTool() + +# OR + +# To restrict the tool to only search within the content of a specific website. +tool = WebsiteSearchTool(website='https://example.com') +``` + +## Arguments +- `website` : An optional argument that specifies the valid website URL to perform the search on. This becomes necessary if the tool is initialized without a specific website. In the `WebsiteSearchToolSchema`, this argument is mandatory. However, in the `FixedWebsiteSearchToolSchema`, it becomes optional if a website is provided during the tool's initialization, as it will then only search within the predefined website's content. + +## Custom model and embeddings + +By default, the tool uses OpenAI for both embeddings and summarization. To customize the model, you can use a config dictionary as follows: + +```python +tool = WebsiteSearchTool( + config=dict( + llm=dict( + provider="ollama", # or google, openai, anthropic, llama2, ... + config=dict( + model="llama2", + # temperature=0.5, + # top_p=1, + # stream=true, + ), + ), + embedder=dict( + provider="google", + config=dict( + model="models/embedding-001", + task_type="retrieval_document", + # title="Embeddings", + ), + ), + ) +) +``` diff --git a/crewai_tools/tools/website_search/website_search_tool.py b/crewai_tools/tools/website_search/website_search_tool.py new file mode 100644 index 000000000..9728b44db --- /dev/null +++ b/crewai_tools/tools/website_search/website_search_tool.py @@ -0,0 +1,58 @@ +from typing import Any, Optional, Type + +try: + from embedchain.models.data_type import DataType + EMBEDCHAIN_AVAILABLE = True +except ImportError: + EMBEDCHAIN_AVAILABLE = False + +from pydantic import BaseModel, Field + +from ..rag.rag_tool import RagTool + + +class FixedWebsiteSearchToolSchema(BaseModel): + """Input for WebsiteSearchTool.""" + + search_query: str = Field( + ..., + description="Mandatory search query you want to use to search a specific website", + ) + + +class WebsiteSearchToolSchema(FixedWebsiteSearchToolSchema): + """Input for WebsiteSearchTool.""" + + website: str = Field( + ..., description="Mandatory valid website URL you want to search on" + ) + + +class WebsiteSearchTool(RagTool): + name: str = "Search in a specific website" + description: str = ( + "A tool that can be used to semantic search a query from a specific URL content." + ) + args_schema: Type[BaseModel] = WebsiteSearchToolSchema + + def __init__(self, website: Optional[str] = None, **kwargs): + super().__init__(**kwargs) + if website is not None: + self.add(website) + self.description = f"A tool that can be used to semantic search a query from {website} website content." + self.args_schema = FixedWebsiteSearchToolSchema + self._generate_description() + + def add(self, website: str) -> None: + if not EMBEDCHAIN_AVAILABLE: + raise ImportError("embedchain is not installed. Please install it with `pip install crewai-tools[embedchain]`") + super().add(website, data_type=DataType.WEB_PAGE) + + def _run( + self, + search_query: str, + website: Optional[str] = None, + ) -> str: + if website is not None: + self.add(website) + return super()._run(query=search_query) diff --git a/crewai_tools/tools/xml_search_tool/README.md b/crewai_tools/tools/xml_search_tool/README.md new file mode 100644 index 000000000..a019d9e15 --- /dev/null +++ b/crewai_tools/tools/xml_search_tool/README.md @@ -0,0 +1,57 @@ +# XMLSearchTool + +## Description +The XMLSearchTool is a cutting-edge RAG tool engineered for conducting semantic searches within XML files. Ideal for users needing to parse and extract information from XML content efficiently, this tool supports inputting a search query and an optional XML file path. By specifying an XML path, users can target their search more precisely to the content of that file, thereby obtaining more relevant search outcomes. + +## Installation +To start using the XMLSearchTool, you must first install the crewai_tools package. This can be easily done with the following command: + +```shell +pip install 'crewai[tools]' +``` + +## Example +Here are two examples demonstrating how to use the XMLSearchTool. The first example shows searching within a specific XML file, while the second example illustrates initiating a search without predefining an XML path, providing flexibility in search scope. + +```python +from crewai_tools.tools.xml_search_tool import XMLSearchTool + +# Allow agents to search within any XML file's content as it learns about their paths during execution +tool = XMLSearchTool() + +# OR + +# Initialize the tool with a specific XML file path for exclusive search within that document +tool = XMLSearchTool(xml='path/to/your/xmlfile.xml') +``` + +## Arguments +- `xml`: This is the path to the XML file you wish to search. It is an optional parameter during the tool's initialization but must be provided either at initialization or as part of the `run` method's arguments to execute a search. + +## Custom model and embeddings + +By default, the tool uses OpenAI for both embeddings and summarization. To customize the model, you can use a config dictionary as follows: + +```python +tool = XMLSearchTool( + config=dict( + llm=dict( + provider="ollama", # or google, openai, anthropic, llama2, ... + config=dict( + model="llama2", + # temperature=0.5, + # top_p=1, + # stream=true, + ), + ), + embedder=dict( + provider="google", + config=dict( + model="models/embedding-001", + task_type="retrieval_document", + # title="Embeddings", + ), + ), + ) +) +``` diff --git a/crewai_tools/tools/xml_search_tool/xml_search_tool.py b/crewai_tools/tools/xml_search_tool/xml_search_tool.py new file mode 100644 index 000000000..426b0ca38 --- /dev/null +++ b/crewai_tools/tools/xml_search_tool/xml_search_tool.py @@ -0,0 +1,45 @@ +from typing import Optional, Type + +from pydantic import BaseModel, Field + +from ..rag.rag_tool import RagTool + + +class FixedXMLSearchToolSchema(BaseModel): + """Input for XMLSearchTool.""" + + search_query: str = Field( + ..., + description="Mandatory search query you want to use to search the XML's content", + ) + + +class XMLSearchToolSchema(FixedXMLSearchToolSchema): + """Input for XMLSearchTool.""" + + xml: str = Field(..., description="File path or URL of a XML file to be searched") + + +class XMLSearchTool(RagTool): + name: str = "Search a XML's content" + description: str = ( + "A tool that can be used to semantic search a query from a XML's content." + ) + args_schema: Type[BaseModel] = XMLSearchToolSchema + + def __init__(self, xml: Optional[str] = None, **kwargs): + super().__init__(**kwargs) + if xml is not None: + self.add(xml) + self.description = f"A tool that can be used to semantic search a query the {xml} XML's content." + self.args_schema = FixedXMLSearchToolSchema + self._generate_description() + + def _run( + self, + search_query: str, + xml: Optional[str] = None, + ) -> str: + if xml is not None: + self.add(xml) + return super()._run(query=search_query) diff --git a/crewai_tools/tools/youtube_channel_search_tool/README.md b/crewai_tools/tools/youtube_channel_search_tool/README.md new file mode 100644 index 000000000..090684f48 --- /dev/null +++ b/crewai_tools/tools/youtube_channel_search_tool/README.md @@ -0,0 +1,57 @@ +# YoutubeChannelSearchTool + +## Description +This tool is designed to perform semantic searches within a specific Youtube channel's content. Leveraging the RAG (Retrieval-Augmented Generation) methodology, it provides relevant search results, making it invaluable for extracting information or finding specific content without the need to manually sift through videos. It streamlines the search process within Youtube channels, catering to researchers, content creators, and viewers seeking specific information or topics. + +## Installation +To utilize the YoutubeChannelSearchTool, the `crewai_tools` package must be installed. Execute the following command in your shell to install: + +```shell +pip install 'crewai[tools]' +``` + +## Example +To begin using the YoutubeChannelSearchTool, follow the example below. This demonstrates initializing the tool with a specific Youtube channel handle and conducting a search within that channel's content. + +```python +from crewai_tools import YoutubeChannelSearchTool + +# Initialize the tool to search within any Youtube channel's content the agent learns about during its execution +tool = YoutubeChannelSearchTool() + +# OR + +# Initialize the tool with a specific Youtube channel handle to target your search +tool = YoutubeChannelSearchTool(youtube_channel_handle='@exampleChannel') +``` + +## Arguments +- `youtube_channel_handle` : A mandatory string representing the Youtube channel handle. This parameter is crucial for initializing the tool to specify the channel you want to search within. The tool is designed to only search within the content of the provided channel handle. + +## Custom model and embeddings + +By default, the tool uses OpenAI for both embeddings and summarization. To customize the model, you can use a config dictionary as follows: + +```python +tool = YoutubeChannelSearchTool( + config=dict( + llm=dict( + provider="ollama", # or google, openai, anthropic, llama2, ... + config=dict( + model="llama2", + # temperature=0.5, + # top_p=1, + # stream=true, + ), + ), + embedder=dict( + provider="google", + config=dict( + model="models/embedding-001", + task_type="retrieval_document", + # title="Embeddings", + ), + ), + ) +) +``` diff --git a/crewai_tools/tools/youtube_channel_search_tool/youtube_channel_search_tool.py b/crewai_tools/tools/youtube_channel_search_tool/youtube_channel_search_tool.py new file mode 100644 index 000000000..6d16a708d --- /dev/null +++ b/crewai_tools/tools/youtube_channel_search_tool/youtube_channel_search_tool.py @@ -0,0 +1,61 @@ +from typing import Any, Optional, Type + +try: + from embedchain.models.data_type import DataType + EMBEDCHAIN_AVAILABLE = True +except ImportError: + EMBEDCHAIN_AVAILABLE = False + +from pydantic import BaseModel, Field + +from ..rag.rag_tool import RagTool + + +class FixedYoutubeChannelSearchToolSchema(BaseModel): + """Input for YoutubeChannelSearchTool.""" + + search_query: str = Field( + ..., + description="Mandatory search query you want to use to search the Youtube Channels content", + ) + + +class YoutubeChannelSearchToolSchema(FixedYoutubeChannelSearchToolSchema): + """Input for YoutubeChannelSearchTool.""" + + youtube_channel_handle: str = Field( + ..., description="Mandatory youtube_channel_handle path you want to search" + ) + + +class YoutubeChannelSearchTool(RagTool): + name: str = "Search a Youtube Channels content" + description: str = ( + "A tool that can be used to semantic search a query from a Youtube Channels content." + ) + args_schema: Type[BaseModel] = YoutubeChannelSearchToolSchema + + def __init__(self, youtube_channel_handle: Optional[str] = None, **kwargs): + super().__init__(**kwargs) + if youtube_channel_handle is not None: + self.add(youtube_channel_handle) + self.description = f"A tool that can be used to semantic search a query the {youtube_channel_handle} Youtube Channels content." + self.args_schema = FixedYoutubeChannelSearchToolSchema + self._generate_description() + + def add( + self, + youtube_channel_handle: str, + ) -> None: + if not youtube_channel_handle.startswith("@"): + youtube_channel_handle = f"@{youtube_channel_handle}" + super().add(youtube_channel_handle, data_type=DataType.YOUTUBE_CHANNEL) + + def _run( + self, + search_query: str, + youtube_channel_handle: Optional[str] = None, + ) -> str: + if youtube_channel_handle is not None: + self.add(youtube_channel_handle) + return super()._run(query=search_query) diff --git a/crewai_tools/tools/youtube_video_search_tool/README.md b/crewai_tools/tools/youtube_video_search_tool/README.md new file mode 100644 index 000000000..8b84613b4 --- /dev/null +++ b/crewai_tools/tools/youtube_video_search_tool/README.md @@ -0,0 +1,60 @@ +# YoutubeVideoSearchTool + +## Description + +This tool is part of the `crewai_tools` package and is designed to perform semantic searches within Youtube video content, utilizing Retrieval-Augmented Generation (RAG) techniques. It is one of several "Search" tools in the package that leverage RAG for different sources. The YoutubeVideoSearchTool allows for flexibility in searches; users can search across any Youtube video content without specifying a video URL, or they can target their search to a specific Youtube video by providing its URL. + +## Installation + +To utilize the YoutubeVideoSearchTool, you must first install the `crewai_tools` package. This package contains the YoutubeVideoSearchTool among other utilities designed to enhance your data analysis and processing tasks. Install the package by executing the following command in your terminal: + +``` +pip install 'crewai[tools]' +``` + +## Example + +To integrate the YoutubeVideoSearchTool into your Python projects, follow the example below. This demonstrates how to use the tool both for general Youtube content searches and for targeted searches within a specific video's content. + +```python +from crewai_tools import YoutubeVideoSearchTool + +# General search across Youtube content without specifying a video URL, so the agent can search within any Youtube video content it learns about irs url during its operation +tool = YoutubeVideoSearchTool() + +# Targeted search within a specific Youtube video's content +tool = YoutubeVideoSearchTool(youtube_video_url='https://youtube.com/watch?v=example') +``` +## Arguments + +The YoutubeVideoSearchTool accepts the following initialization arguments: + +- `youtube_video_url`: An optional argument at initialization but required if targeting a specific Youtube video. It specifies the Youtube video URL path you want to search within. + +## Custom model and embeddings + +By default, the tool uses OpenAI for both embeddings and summarization. To customize the model, you can use a config dictionary as follows: + +```python +tool = YoutubeVideoSearchTool( + config=dict( + llm=dict( + provider="ollama", # or google, openai, anthropic, llama2, ... + config=dict( + model="llama2", + # temperature=0.5, + # top_p=1, + # stream=true, + ), + ), + embedder=dict( + provider="google", + config=dict( + model="models/embedding-001", + task_type="retrieval_document", + # title="Embeddings", + ), + ), + ) +) +``` diff --git a/crewai_tools/tools/youtube_video_search_tool/youtube_video_search_tool.py b/crewai_tools/tools/youtube_video_search_tool/youtube_video_search_tool.py new file mode 100644 index 000000000..b93cc6c29 --- /dev/null +++ b/crewai_tools/tools/youtube_video_search_tool/youtube_video_search_tool.py @@ -0,0 +1,58 @@ +from typing import Any, Optional, Type + +try: + from embedchain.models.data_type import DataType + EMBEDCHAIN_AVAILABLE = True +except ImportError: + EMBEDCHAIN_AVAILABLE = False + +from pydantic import BaseModel, Field + +from ..rag.rag_tool import RagTool + + +class FixedYoutubeVideoSearchToolSchema(BaseModel): + """Input for YoutubeVideoSearchTool.""" + + search_query: str = Field( + ..., + description="Mandatory search query you want to use to search the Youtube Video content", + ) + + +class YoutubeVideoSearchToolSchema(FixedYoutubeVideoSearchToolSchema): + """Input for YoutubeVideoSearchTool.""" + + youtube_video_url: str = Field( + ..., description="Mandatory youtube_video_url path you want to search" + ) + + +class YoutubeVideoSearchTool(RagTool): + name: str = "Search a Youtube Video content" + description: str = ( + "A tool that can be used to semantic search a query from a Youtube Video content." + ) + args_schema: Type[BaseModel] = YoutubeVideoSearchToolSchema + + def __init__(self, youtube_video_url: Optional[str] = None, **kwargs): + super().__init__(**kwargs) + if youtube_video_url is not None: + self.add(youtube_video_url) + self.description = f"A tool that can be used to semantic search a query the {youtube_video_url} Youtube Video content." + self.args_schema = FixedYoutubeVideoSearchToolSchema + self._generate_description() + + def add(self, youtube_video_url: str) -> None: + if not EMBEDCHAIN_AVAILABLE: + raise ImportError("embedchain is not installed. Please install it with `pip install crewai-tools[embedchain]`") + super().add(youtube_video_url, data_type=DataType.YOUTUBE_VIDEO) + + def _run( + self, + search_query: str, + youtube_video_url: Optional[str] = None, + ) -> str: + if youtube_video_url is not None: + self.add(youtube_video_url) + return super()._run(query=search_query) diff --git a/crewai_tools/tools/zapier_action_tool/README.md b/crewai_tools/tools/zapier_action_tool/README.md new file mode 100644 index 000000000..5a6dad43b --- /dev/null +++ b/crewai_tools/tools/zapier_action_tool/README.md @@ -0,0 +1,91 @@ +# Zapier Action Tools + +## Description + +This tool enables CrewAI agents to interact with Zapier actions, allowing them to automate workflows and integrate with hundreds of applications through Zapier's platform. The tool dynamically creates BaseTool instances for each available Zapier action, making it easy to incorporate automation into your AI workflows. + +## Installation + +Install the crewai_tools package by executing the following command in your terminal: + +```shell +uv pip install 'crewai[tools]' +``` + +## Example + +To utilize the ZapierActionTools for different use cases, follow these examples: + +```python +from crewai_tools import ZapierActionTools +from crewai import Agent + +# Get all available Zapier actions you are connected to. +tools = ZapierActionTools( + zapier_api_key="your-zapier-api-key" +) + +# Or specify only certain actions you want to use +tools = ZapierActionTools( + zapier_api_key="your-zapier-api-key", + action_list=["gmail_find_email", "slack_send_message", "google_sheets_create_row"] +) + +# Adding the tools to an agent +zapier_agent = Agent( + name="zapier_agent", + role="You are a helpful assistant that can automate tasks using Zapier integrations.", + llm="gpt-4o-mini", + tools=tools, + goal="Automate workflows and integrate with various applications", + backstory="You are a Zapier automation expert that helps users connect and automate their favorite apps.", + verbose=True, +) + +# Example usage +result = zapier_agent.kickoff( + "Find emails from john@example.com in Gmail" +) +``` + +## Arguments + +- `zapier_api_key` : Your Zapier API key for authentication. Can also be set via `ZAPIER_API_KEY` environment variable. (Required) +- `action_list` : A list of specific Zapier action names to include. If not provided, all available actions will be returned. (Optional) + +## Environment Variables + +You can set your Zapier API key as an environment variable instead of passing it directly: + +```bash +export ZAPIER_API_KEY="your-zapier-api-key" +``` + +Then use the tool without explicitly passing the API key: + +```python +from crewai_tools import ZapierActionTools + +# API key will be automatically loaded from environment +tools = ZapierActionTools( + action_list=["gmail_find_email", "slack_send_message"] +) +``` + +## Getting Your Zapier API Key + +1. Log in to your Zapier account +2. Go to https://zapier.com/app/developer/ +3. Create a new app or use an existing one +4. Navigate to the "Authentication" section +5. Copy your API key + +## Available Actions + +The tool will dynamically discover all available Zapier actions associated with your API key. Common actions include: + +- Gmail operations (find emails, send emails) +- Slack messaging +- Google Sheets operations +- Calendar events +- And hundreds more depending on your Zapier integrations diff --git a/crewai_tools/tools/zapier_action_tool/zapier_action_tool.py b/crewai_tools/tools/zapier_action_tool/zapier_action_tool.py new file mode 100644 index 000000000..190ef3fc3 --- /dev/null +++ b/crewai_tools/tools/zapier_action_tool/zapier_action_tool.py @@ -0,0 +1,33 @@ +import os +import logging +from typing import List, Optional +from crewai.tools import BaseTool +from crewai_tools.adapters.zapier_adapter import ZapierActionsAdapter + +logger = logging.getLogger(__name__) + + +def ZapierActionTools( + zapier_api_key: Optional[str] = None, action_list: Optional[List[str]] = None +) -> List[BaseTool]: + """Factory function that returns Zapier action tools. + + Args: + zapier_api_key: The API key for Zapier. + action_list: Optional list of specific tool names to include. + + Returns: + A list of Zapier action tools. + """ + if zapier_api_key is None: + zapier_api_key = os.getenv("ZAPIER_API_KEY") + if zapier_api_key is None: + logger.error("ZAPIER_API_KEY is not set") + raise ValueError("ZAPIER_API_KEY is not set") + adapter = ZapierActionsAdapter(zapier_api_key) + all_tools = adapter.tools() + + if action_list is None: + return all_tools + + return [tool for tool in all_tools if tool.name in action_list] diff --git a/generate_tool_specs.py b/generate_tool_specs.py new file mode 100644 index 000000000..1e5e8069c --- /dev/null +++ b/generate_tool_specs.py @@ -0,0 +1,158 @@ +#!/usr/bin/env python3 + +import inspect +import json +from pathlib import Path +from typing import Any, Dict, List, Optional, Type + +from pydantic import BaseModel + +from crewai_tools import tools +from crewai.tools.base_tool import BaseTool, EnvVar + +from pydantic.json_schema import GenerateJsonSchema +from pydantic_core import PydanticOmit + + +class SchemaGenerator(GenerateJsonSchema): + def handle_invalid_for_json_schema(self, schema, error_info): + raise PydanticOmit + + +class ToolSpecExtractor: + def __init__(self) -> None: + self.tools_spec: List[Dict[str, Any]] = [] + self.processed_tools: set[str] = set() + + def extract_all_tools(self) -> List[Dict[str, Any]]: + for name in dir(tools): + if name.endswith("Tool") and name not in self.processed_tools: + obj = getattr(tools, name, None) + if inspect.isclass(obj): + self.extract_tool_info(obj) + self.processed_tools.add(name) + return self.tools_spec + + def extract_tool_info(self, tool_class: BaseTool) -> None: + try: + core_schema = tool_class.__pydantic_core_schema__ + if not core_schema: + return + + schema = self._unwrap_schema(core_schema) + fields = schema.get("schema", {}).get("fields", {}) + + tool_info = { + "name": tool_class.__name__, + "humanized_name": self._extract_field_default( + fields.get("name"), fallback=tool_class.__name__ + ), + "description": self._extract_field_default( + fields.get("description") + ).strip(), + "run_params_schema": self._extract_params(fields.get("args_schema")), + "init_params_schema": self._extract_init_params(tool_class), + "env_vars": self._extract_env_vars(fields.get("env_vars")), + "package_dependencies": self._extract_field_default( + fields.get("package_dependencies"), fallback=[] + ), + } + + self.tools_spec.append(tool_info) + + except Exception as e: + print(f"Error extracting {tool_class.__name__}: {e}") + + def _unwrap_schema(self, schema: Dict) -> Dict: + while ( + schema.get("type") in {"function-after", "default"} and "schema" in schema + ): + schema = schema["schema"] + return schema + + def _extract_field_default(self, field: Optional[Dict], fallback: str = "") -> str: + if not field: + return fallback + + schema = field.get("schema", {}) + default = schema.get("default") + return default if isinstance(default, (list, str, int)) else fallback + + def _extract_params( + self, args_schema_field: Optional[Dict] + ) -> List[Dict[str, str]]: + if not args_schema_field: + return {} + + args_schema_class = args_schema_field.get("schema", {}).get("default") + if not ( + inspect.isclass(args_schema_class) + and hasattr(args_schema_class, "__pydantic_core_schema__") + ): + return {} + + try: + return args_schema_class.model_json_schema( + schema_generator=SchemaGenerator, mode="validation" + ) + except Exception as e: + print(f"Error extracting params from {args_schema_class}: {e}") + return {} + + def _extract_env_vars(self, env_vars_field: Optional[Dict]) -> List[Dict[str, str]]: + if not env_vars_field: + return [] + + env_vars = [] + for env_var in env_vars_field.get("schema", {}).get("default", []): + if isinstance(env_var, EnvVar): + env_vars.append( + { + "name": env_var.name, + "description": env_var.description, + "required": env_var.required, + "default": env_var.default, + } + ) + return env_vars + + def _extract_init_params(self, tool_class: BaseTool) -> dict: + ignored_init_params = [ + "name", + "description", + "env_vars", + "args_schema", + "description_updated", + "cache_function", + "result_as_answer", + "max_usage_count", + "current_usage_count", + "package_dependencies", + ] + + json_schema = tool_class.model_json_schema( + schema_generator=SchemaGenerator, mode="serialization" + ) + + properties = {} + for key, value in json_schema["properties"].items(): + if key not in ignored_init_params: + properties[key] = value + + json_schema["properties"] = properties + return json_schema + + def save_to_json(self, output_path: str) -> None: + with open(output_path, "w", encoding="utf-8") as f: + json.dump({"tools": self.tools_spec}, f, indent=2, sort_keys=True) + print(f"Saved tool specs to {output_path}") + + +if __name__ == "__main__": + output_file = Path(__file__).parent / "tool.specs.json" + extractor = ToolSpecExtractor() + + specs = extractor.extract_all_tools() + extractor.save_to_json(str(output_file)) + + print(f"Extracted {len(specs)} tool classes.") diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 000000000..01919491d --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,166 @@ +[project] +name = "crewai-tools" +version = "0.71.0" +description = "Set of tools for the crewAI framework" +readme = "README.md" +authors = [ + { name = "João Moura", email = "joaomdmoura@gmail.com" }, +] +requires-python = ">=3.10,<3.14" +dependencies = [ + "pydantic>=2.6.1", + "lancedb>=0.5.4", + "openai>=1.12.0", + "chromadb==0.5.23", + "pyright>=1.1.350", + "pytube>=15.0.0", + "requests>=2.31.0", + "docker>=7.1.0", + "crewai>=0.177.0", + "click>=8.1.8", + "lancedb>=0.5.4", + "tiktoken>=0.8.0", + "stagehand>=0.4.1", + "portalocker==2.7.0", +] + +[project.urls] +Homepage = "https://crewai.com" +Repository = "https://github.com/crewAIInc/crewAI-tools" +Documentation = "https://docs.crewai.com" + + +[project.scripts] + +[project.optional-dependencies] +embedchain = [ + "embedchain>=0.1.114", +] +scrapfly-sdk = [ + "scrapfly-sdk>=0.8.19", +] +sqlalchemy = [ + "sqlalchemy>=2.0.35", +] +multion = [ + "multion>=1.1.0", +] +firecrawl-py = [ + "firecrawl-py>=1.8.0", +] +composio-core = [ + "composio-core>=0.6.11.post1", +] +browserbase = [ + "browserbase>=1.0.5", +] +weaviate-client = [ + "weaviate-client>=4.10.2", +] +patronus = [ + "patronus>=0.0.16", +] +serpapi = [ + "serpapi>=0.1.5", +] +beautifulsoup4 = [ + "beautifulsoup4>=4.12.3", +] +selenium = [ + "selenium>=4.27.1", +] +spider-client = [ + "spider-client>=0.1.25", +] +scrapegraph-py = [ + "scrapegraph-py>=1.9.0", +] +linkup-sdk = [ + "linkup-sdk>=0.2.2", +] +tavily-python = [ + "tavily-python>=0.5.4", +] +hyperbrowser = [ + "hyperbrowser>=0.18.0", +] +snowflake = [ + "cryptography>=43.0.3", + "snowflake-connector-python>=3.12.4", + "snowflake-sqlalchemy>=1.7.3", +] +singlestore = [ + "singlestoredb>=1.12.4", + "SQLAlchemy>=2.0.40", +] +exa-py = [ + "exa-py>=1.8.7", +] +qdrant-client = [ + "qdrant-client>=1.12.1", +] +apify = [ + "langchain-apify>=0.1.2,<1.0.0", +] + +databricks-sdk = [ + "databricks-sdk>=0.46.0", +] +couchbase = [ + "couchbase>=4.3.5", +] +mcp = [ + "mcp>=1.6.0", + "mcpadapt>=0.1.9", +] +stagehand = [ + "stagehand>=0.4.1", +] +github = [ + "gitpython==3.1.38", + "PyGithub==1.59.1", +] +rag = [ + "python-docx>=1.1.0", + "lxml>=5.3.0,<5.4.0", # Pin to avoid etree import issues in 5.4.0 +] +xml = [ + "unstructured[local-inference, all-docs]>=0.17.2" +] +oxylabs = [ + "oxylabs==2.0.0" +] +mongodb = [ + "pymongo>=4.13" +] +bedrock = [ + "beautifulsoup4>=4.13.4", + "bedrock-agentcore>=0.1.0", + "playwright>=1.52.0", + "nest-asyncio>=1.6.0", +] +contextual = [ + "contextual-client>=0.1.0", + "nest-asyncio>=1.6.0", +] + +[tool.pytest.ini_options] +pythonpath = ["."] + +[tool.uv] +dev-dependencies = [ + "pytest>=8.3.4", +] + +[build-system] +requires = [ + "hatchling", +] +build-backend = "hatchling.build" + +[dependency-groups] +dev = [ + "pytest-asyncio>=0.25.2", + "pytest>=8.0.0", + "pytest-recording>=0.13.3", +] diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/adapters/mcp_adapter_test.py b/tests/adapters/mcp_adapter_test.py new file mode 100644 index 000000000..81c3c529b --- /dev/null +++ b/tests/adapters/mcp_adapter_test.py @@ -0,0 +1,230 @@ +from textwrap import dedent +from unittest.mock import MagicMock, patch + +import pytest +from mcp import StdioServerParameters + +from crewai_tools import MCPServerAdapter +from crewai_tools.adapters.tool_collection import ToolCollection + +@pytest.fixture +def echo_server_script(): + return dedent( + ''' + from mcp.server.fastmcp import FastMCP + + mcp = FastMCP("Echo Server") + + @mcp.tool() + def echo_tool(text: str) -> str: + """Echo the input text""" + return f"Echo: {text}" + + @mcp.tool() + def calc_tool(a: int, b: int) -> int: + """Calculate a + b""" + return a + b + + mcp.run() + ''' + ) + + +@pytest.fixture +def echo_server_sse_script(): + return dedent( + ''' + from mcp.server.fastmcp import FastMCP + + mcp = FastMCP("Echo Server", host="127.0.0.1", port=8000) + + @mcp.tool() + def echo_tool(text: str) -> str: + """Echo the input text""" + return f"Echo: {text}" + + @mcp.tool() + def calc_tool(a: int, b: int) -> int: + """Calculate a + b""" + return a + b + + mcp.run("sse") + ''' + ) + + +@pytest.fixture +def echo_sse_server(echo_server_sse_script): + import subprocess + import time + + # Start the SSE server process with its own process group + process = subprocess.Popen( + ["python", "-c", echo_server_sse_script], + ) + + # Give the server a moment to start up + time.sleep(1) + + try: + yield {"url": "http://127.0.0.1:8000/sse"} + finally: + # Clean up the process when test is done + process.kill() + process.wait() + + +def test_context_manager_syntax(echo_server_script): + serverparams = StdioServerParameters( + command="uv", args=["run", "python", "-c", echo_server_script] + ) + with MCPServerAdapter(serverparams) as tools: + assert isinstance(tools, ToolCollection) + assert len(tools) == 2 + assert tools[0].name == "echo_tool" + assert tools[1].name == "calc_tool" + assert tools[0].run(text="hello") == "Echo: hello" + assert tools[1].run(a=5, b=3) == '8' + +def test_context_manager_syntax_sse(echo_sse_server): + sse_serverparams = echo_sse_server + with MCPServerAdapter(sse_serverparams) as tools: + assert len(tools) == 2 + assert tools[0].name == "echo_tool" + assert tools[1].name == "calc_tool" + assert tools[0].run(text="hello") == "Echo: hello" + assert tools[1].run(a=5, b=3) == '8' + +def test_try_finally_syntax(echo_server_script): + serverparams = StdioServerParameters( + command="uv", args=["run", "python", "-c", echo_server_script] + ) + try: + mcp_server_adapter = MCPServerAdapter(serverparams) + tools = mcp_server_adapter.tools + assert len(tools) == 2 + assert tools[0].name == "echo_tool" + assert tools[1].name == "calc_tool" + assert tools[0].run(text="hello") == "Echo: hello" + assert tools[1].run(a=5, b=3) == '8' + finally: + mcp_server_adapter.stop() + +def test_try_finally_syntax_sse(echo_sse_server): + sse_serverparams = echo_sse_server + mcp_server_adapter = MCPServerAdapter(sse_serverparams) + try: + tools = mcp_server_adapter.tools + assert len(tools) == 2 + assert tools[0].name == "echo_tool" + assert tools[1].name == "calc_tool" + assert tools[0].run(text="hello") == "Echo: hello" + assert tools[1].run(a=5, b=3) == '8' + finally: + mcp_server_adapter.stop() + +def test_context_manager_with_filtered_tools(echo_server_script): + serverparams = StdioServerParameters( + command="uv", args=["run", "python", "-c", echo_server_script] + ) + # Only select the echo_tool + with MCPServerAdapter(serverparams, "echo_tool") as tools: + assert isinstance(tools, ToolCollection) + assert len(tools) == 1 + assert tools[0].name == "echo_tool" + assert tools[0].run(text="hello") == "Echo: hello" + # Check that calc_tool is not present + with pytest.raises(IndexError): + _ = tools[1] + with pytest.raises(KeyError): + _ = tools["calc_tool"] + +def test_context_manager_sse_with_filtered_tools(echo_sse_server): + sse_serverparams = echo_sse_server + # Only select the calc_tool + with MCPServerAdapter(sse_serverparams, "calc_tool") as tools: + assert isinstance(tools, ToolCollection) + assert len(tools) == 1 + assert tools[0].name == "calc_tool" + assert tools[0].run(a=10, b=5) == '15' + # Check that echo_tool is not present + with pytest.raises(IndexError): + _ = tools[1] + with pytest.raises(KeyError): + _ = tools["echo_tool"] + +def test_try_finally_with_filtered_tools(echo_server_script): + serverparams = StdioServerParameters( + command="uv", args=["run", "python", "-c", echo_server_script] + ) + try: + # Select both tools but in reverse order + mcp_server_adapter = MCPServerAdapter(serverparams, "calc_tool", "echo_tool") + tools = mcp_server_adapter.tools + assert len(tools) == 2 + # The order of tools is based on filter_by_names which preserves + # the original order from the collection + assert tools[0].name == "calc_tool" + assert tools[1].name == "echo_tool" + finally: + mcp_server_adapter.stop() + +def test_filter_with_nonexistent_tool(echo_server_script): + serverparams = StdioServerParameters( + command="uv", args=["run", "python", "-c", echo_server_script] + ) + # Include a tool that doesn't exist + with MCPServerAdapter(serverparams, "echo_tool", "nonexistent_tool") as tools: + # Only echo_tool should be in the result + assert len(tools) == 1 + assert tools[0].name == "echo_tool" + +def test_filter_with_only_nonexistent_tools(echo_server_script): + serverparams = StdioServerParameters( + command="uv", args=["run", "python", "-c", echo_server_script] + ) + # All requested tools don't exist + with MCPServerAdapter(serverparams, "nonexistent1", "nonexistent2") as tools: + # Should return an empty tool collection + assert isinstance(tools, ToolCollection) + assert len(tools) == 0 + +def test_connect_timeout_parameter(echo_server_script): + serverparams = StdioServerParameters( + command="uv", args=["run", "python", "-c", echo_server_script] + ) + with MCPServerAdapter(serverparams, connect_timeout=60) as tools: + assert isinstance(tools, ToolCollection) + assert len(tools) == 2 + assert tools[0].name == "echo_tool" + assert tools[1].name == "calc_tool" + assert tools[0].run(text="hello") == "Echo: hello" + +def test_connect_timeout_with_filtered_tools(echo_server_script): + serverparams = StdioServerParameters( + command="uv", args=["run", "python", "-c", echo_server_script] + ) + with MCPServerAdapter(serverparams, "echo_tool", connect_timeout=45) as tools: + assert isinstance(tools, ToolCollection) + assert len(tools) == 1 + assert tools[0].name == "echo_tool" + assert tools[0].run(text="timeout test") == "Echo: timeout test" + +@patch('crewai_tools.adapters.mcp_adapter.MCPAdapt') +def test_connect_timeout_passed_to_mcpadapt(mock_mcpadapt): + mock_adapter_instance = MagicMock() + mock_mcpadapt.return_value = mock_adapter_instance + + serverparams = StdioServerParameters( + command="uv", args=["run", "echo", "test"] + ) + + MCPServerAdapter(serverparams) + mock_mcpadapt.assert_called_once() + assert mock_mcpadapt.call_args[0][2] == 30 + + mock_mcpadapt.reset_mock() + + MCPServerAdapter(serverparams, connect_timeout=5) + mock_mcpadapt.assert_called_once() + assert mock_mcpadapt.call_args[0][2] == 5 diff --git a/tests/base_tool_test.py b/tests/base_tool_test.py new file mode 100644 index 000000000..dbb7fe20e --- /dev/null +++ b/tests/base_tool_test.py @@ -0,0 +1,104 @@ +from typing import Callable + +from crewai.tools import BaseTool, tool +from crewai.tools.base_tool import to_langchain + + +def test_creating_a_tool_using_annotation(): + @tool("Name of my tool") + def my_tool(question: str) -> str: + """Clear description for what this tool is useful for, you agent will need this information to use it.""" + return question + + # Assert all the right attributes were defined + assert my_tool.name == "Name of my tool" + assert ( + my_tool.description + == "Tool Name: Name of my tool\nTool Arguments: {'question': {'description': None, 'type': 'str'}}\nTool Description: Clear description for what this tool is useful for, you agent will need this information to use it." + ) + assert my_tool.args_schema.model_json_schema()["properties"] == { + "question": {"title": "Question", "type": "string"} + } + assert ( + my_tool.func("What is the meaning of life?") == "What is the meaning of life?" + ) + + # Assert the langchain tool conversion worked as expected + converted_tool = to_langchain([my_tool])[0] + assert converted_tool.name == "Name of my tool" + assert ( + converted_tool.description + == "Tool Name: Name of my tool\nTool Arguments: {'question': {'description': None, 'type': 'str'}}\nTool Description: Clear description for what this tool is useful for, you agent will need this information to use it." + ) + assert converted_tool.args_schema.model_json_schema()["properties"] == { + "question": {"title": "Question", "type": "string"} + } + assert ( + converted_tool.func("What is the meaning of life?") + == "What is the meaning of life?" + ) + + +def test_creating_a_tool_using_baseclass(): + class MyCustomTool(BaseTool): + name: str = "Name of my tool" + description: str = "Clear description for what this tool is useful for, you agent will need this information to use it." + + def _run(self, question: str) -> str: + return question + + my_tool = MyCustomTool() + # Assert all the right attributes were defined + assert my_tool.name == "Name of my tool" + assert ( + my_tool.description + == "Tool Name: Name of my tool\nTool Arguments: {'question': {'description': None, 'type': 'str'}}\nTool Description: Clear description for what this tool is useful for, you agent will need this information to use it." + ) + assert my_tool.args_schema.model_json_schema()["properties"] == { + "question": {"title": "Question", "type": "string"} + } + assert ( + my_tool._run("What is the meaning of life?") == "What is the meaning of life?" + ) + + # Assert the langchain tool conversion worked as expected + converted_tool = to_langchain([my_tool])[0] + assert converted_tool.name == "Name of my tool" + assert ( + converted_tool.description + == "Tool Name: Name of my tool\nTool Arguments: {'question': {'description': None, 'type': 'str'}}\nTool Description: Clear description for what this tool is useful for, you agent will need this information to use it." + ) + assert converted_tool.args_schema.model_json_schema()["properties"] == { + "question": {"title": "Question", "type": "string"} + } + assert ( + converted_tool.invoke({"question": "What is the meaning of life?"}) + == "What is the meaning of life?" + ) + + +def test_setting_cache_function(): + class MyCustomTool(BaseTool): + name: str = "Name of my tool" + description: str = "Clear description for what this tool is useful for, you agent will need this information to use it." + cache_function: Callable = lambda: False + + def _run(self, question: str) -> str: + return question + + my_tool = MyCustomTool() + # Assert all the right attributes were defined + assert my_tool.cache_function() == False + + +def test_default_cache_function_is_true(): + class MyCustomTool(BaseTool): + name: str = "Name of my tool" + description: str = "Clear description for what this tool is useful for, you agent will need this information to use it." + + def _run(self, question: str) -> str: + return question + + my_tool = MyCustomTool() + # Assert all the right attributes were defined + assert my_tool.cache_function() == True diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 000000000..cacbc0fe2 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,21 @@ +from typing import Callable + +import pytest + + +class Helpers: + @staticmethod + def get_embedding_function() -> Callable: + def _func(input): + assert input == ["What are the requirements for the task?"] + with open("tests/data/embedding.txt", "r") as file: + content = file.read() + numbers = content.split(",") + return [[float(number) for number in numbers]] + + return _func + + +@pytest.fixture +def helpers(): + return Helpers diff --git a/tests/file_read_tool_test.py b/tests/file_read_tool_test.py new file mode 100644 index 000000000..174b32229 --- /dev/null +++ b/tests/file_read_tool_test.py @@ -0,0 +1,165 @@ +import os +from unittest.mock import mock_open, patch + +from crewai_tools import FileReadTool + + +def test_file_read_tool_constructor(): + """Test FileReadTool initialization with file_path.""" + # Create a temporary test file + test_file = "/tmp/test_file.txt" + test_content = "Hello, World!" + with open(test_file, "w") as f: + f.write(test_content) + + # Test initialization with file_path + tool = FileReadTool(file_path=test_file) + assert tool.file_path == test_file + assert "test_file.txt" in tool.description + + # Clean up + os.remove(test_file) + + +def test_file_read_tool_run(): + """Test FileReadTool _run method with file_path at runtime.""" + test_file = "/tmp/test_file.txt" + test_content = "Hello, World!" + + # Use mock_open to mock file operations + with patch("builtins.open", mock_open(read_data=test_content)): + # Test reading file with runtime file_path + tool = FileReadTool() + result = tool._run(file_path=test_file) + assert result == test_content + + +def test_file_read_tool_error_handling(): + """Test FileReadTool error handling.""" + # Test missing file path + tool = FileReadTool() + result = tool._run() + assert "Error: No file path provided" in result + + # Test non-existent file + result = tool._run(file_path="/nonexistent/file.txt") + assert "Error: File not found at path:" in result + + # Test permission error + with patch("builtins.open", side_effect=PermissionError()): + result = tool._run(file_path="/tmp/no_permission.txt") + assert "Error: Permission denied" in result + + +def test_file_read_tool_constructor_and_run(): + """Test FileReadTool using both constructor and runtime file paths.""" + test_file1 = "/tmp/test1.txt" + test_file2 = "/tmp/test2.txt" + content1 = "File 1 content" + content2 = "File 2 content" + + # First test with content1 + with patch("builtins.open", mock_open(read_data=content1)): + tool = FileReadTool(file_path=test_file1) + result = tool._run() + assert result == content1 + + # Then test with content2 (should override constructor file_path) + with patch("builtins.open", mock_open(read_data=content2)): + result = tool._run(file_path=test_file2) + assert result == content2 + + +def test_file_read_tool_chunk_reading(): + """Test FileReadTool reading specific chunks of a file.""" + test_file = "/tmp/multiline_test.txt" + lines = [ + "Line 1\n", + "Line 2\n", + "Line 3\n", + "Line 4\n", + "Line 5\n", + "Line 6\n", + "Line 7\n", + "Line 8\n", + "Line 9\n", + "Line 10\n", + ] + file_content = "".join(lines) + + with patch("builtins.open", mock_open(read_data=file_content)): + tool = FileReadTool() + + # Test reading a specific chunk (lines 3-5) + result = tool._run(file_path=test_file, start_line=3, line_count=3) + expected = "".join(lines[2:5]) # Lines are 0-indexed in the array + assert result == expected + + # Test reading from a specific line to the end + result = tool._run(file_path=test_file, start_line=8) + expected = "".join(lines[7:]) + assert result == expected + + # Test with default values (should read entire file) + result = tool._run(file_path=test_file) + expected = "".join(lines) + assert result == expected + + # Test when start_line is 1 but line_count is specified + result = tool._run(file_path=test_file, start_line=1, line_count=5) + expected = "".join(lines[0:5]) + assert result == expected + + +def test_file_read_tool_chunk_error_handling(): + """Test error handling for chunk reading.""" + test_file = "/tmp/short_test.txt" + lines = ["Line 1\n", "Line 2\n", "Line 3\n"] + file_content = "".join(lines) + + with patch("builtins.open", mock_open(read_data=file_content)): + tool = FileReadTool() + + # Test start_line exceeding file length + result = tool._run(file_path=test_file, start_line=10) + assert "Error: Start line 10 exceeds the number of lines in the file" in result + + # Test reading partial chunk when line_count exceeds available lines + result = tool._run(file_path=test_file, start_line=2, line_count=10) + expected = "".join(lines[1:]) # Should return from line 2 to end + assert result == expected + + +def test_file_read_tool_zero_or_negative_start_line(): + """Test that start_line values of 0 or negative read from the start of the file.""" + test_file = "/tmp/negative_test.txt" + lines = ["Line 1\n", "Line 2\n", "Line 3\n", "Line 4\n", "Line 5\n"] + file_content = "".join(lines) + + with patch("builtins.open", mock_open(read_data=file_content)): + tool = FileReadTool() + + # Test with start_line = None + result = tool._run(file_path=test_file, start_line=None) + expected = "".join(lines) # Should read the entire file + assert result == expected + + # Test with start_line = 0 + result = tool._run(file_path=test_file, start_line=0) + expected = "".join(lines) # Should read the entire file + assert result == expected + + # Test with start_line = 0 and limited line count + result = tool._run(file_path=test_file, start_line=0, line_count=3) + expected = "".join(lines[0:3]) # Should read first 3 lines + assert result == expected + + # Test with negative start_line + result = tool._run(file_path=test_file, start_line=-5) + expected = "".join(lines) # Should read the entire file + assert result == expected + + # Test with negative start_line and limited line count + result = tool._run(file_path=test_file, start_line=-10, line_count=2) + expected = "".join(lines[0:2]) # Should read first 2 lines + assert result == expected diff --git a/tests/it/tools/__init__.py b/tests/it/tools/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/it/tools/conftest.py b/tests/it/tools/conftest.py new file mode 100644 index 000000000..a633c22c7 --- /dev/null +++ b/tests/it/tools/conftest.py @@ -0,0 +1,21 @@ +import pytest + + +def pytest_configure(config): + """Register custom markers.""" + config.addinivalue_line("markers", "integration: mark test as an integration test") + config.addinivalue_line("markers", "asyncio: mark test as an async test") + + # Set the asyncio loop scope through ini configuration + config.inicfg["asyncio_mode"] = "auto" + + +@pytest.fixture(scope="function") +def event_loop(): + """Create an instance of the default event loop for each test case.""" + import asyncio + + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + yield loop + loop.close() diff --git a/tests/rag/__init__.py b/tests/rag/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/rag/test_csv_loader.py b/tests/rag/test_csv_loader.py new file mode 100644 index 000000000..596cb4d58 --- /dev/null +++ b/tests/rag/test_csv_loader.py @@ -0,0 +1,130 @@ +import os +import tempfile +import pytest +from unittest.mock import patch, Mock + +from crewai_tools.rag.loaders.csv_loader import CSVLoader +from crewai_tools.rag.base_loader import LoaderResult +from crewai_tools.rag.source_content import SourceContent + + +@pytest.fixture +def temp_csv_file(): + created_files = [] + + def _create(content: str): + f = tempfile.NamedTemporaryFile(mode="w", suffix=".csv", delete=False) + f.write(content) + f.close() + created_files.append(f.name) + return f.name + + yield _create + + for path in created_files: + os.unlink(path) + + +class TestCSVLoader: + def test_load_csv_from_file(self, temp_csv_file): + path = temp_csv_file("name,age,city\nJohn,25,New York\nJane,30,Chicago") + loader = CSVLoader() + result = loader.load(SourceContent(path)) + + assert isinstance(result, LoaderResult) + assert "Headers: name | age | city" in result.content + assert "Row 1: name: John | age: 25 | city: New York" in result.content + assert "Row 2: name: Jane | age: 30 | city: Chicago" in result.content + assert result.metadata == { + "format": "csv", + "columns": ["name", "age", "city"], + "rows": 2, + } + assert result.source == path + assert result.doc_id + + def test_load_csv_with_empty_values(self, temp_csv_file): + path = temp_csv_file("name,age,city\nJohn,,New York\n,30,") + result = CSVLoader().load(SourceContent(path)) + + assert "Row 1: name: John | city: New York" in result.content + assert "Row 2: age: 30" in result.content + assert result.metadata["rows"] == 2 + + def test_load_csv_malformed(self, temp_csv_file): + path = temp_csv_file("invalid,csv\nunclosed quote \"missing") + result = CSVLoader().load(SourceContent(path)) + + assert "Headers: invalid | csv" in result.content + assert 'Row 1: invalid: unclosed quote "missing' in result.content + assert result.metadata["columns"] == ["invalid", "csv"] + + def test_load_csv_empty_file(self, temp_csv_file): + path = temp_csv_file("") + result = CSVLoader().load(SourceContent(path)) + + assert result.content == "" + assert result.metadata["rows"] == 0 + + def test_load_csv_text_input(self): + raw_csv = "col1,col2\nvalue1,value2\nvalue3,value4" + result = CSVLoader().load(SourceContent(raw_csv)) + + assert "Headers: col1 | col2" in result.content + assert "Row 1: col1: value1 | col2: value2" in result.content + assert "Row 2: col1: value3 | col2: value4" in result.content + assert result.metadata["columns"] == ["col1", "col2"] + assert result.metadata["rows"] == 2 + + def test_doc_id_is_deterministic(self, temp_csv_file): + path = temp_csv_file("name,value\ntest,123") + loader = CSVLoader() + + result1 = loader.load(SourceContent(path)) + result2 = loader.load(SourceContent(path)) + + assert result1.doc_id == result2.doc_id + + @patch("requests.get") + def test_load_csv_from_url(self, mock_get): + mock_get.return_value = Mock( + text="name,value\ntest,123", + raise_for_status=Mock(return_value=None) + ) + + result = CSVLoader().load(SourceContent("https://example.com/data.csv")) + + assert "Headers: name | value" in result.content + assert "Row 1: name: test | value: 123" in result.content + headers = mock_get.call_args[1]["headers"] + assert "text/csv" in headers["Accept"] + assert "crewai-tools CSVLoader" in headers["User-Agent"] + + @patch("requests.get") + def test_load_csv_with_custom_headers(self, mock_get): + mock_get.return_value = Mock( + text="data,value\ntest,456", + raise_for_status=Mock(return_value=None) + ) + headers = {"Authorization": "Bearer token", "Custom-Header": "value"} + result = CSVLoader().load(SourceContent("https://example.com/data.csv"), headers=headers) + + assert "Headers: data | value" in result.content + assert mock_get.call_args[1]["headers"] == headers + + @patch("requests.get") + def test_csv_loader_handles_network_errors(self, mock_get): + mock_get.side_effect = Exception("Network error") + loader = CSVLoader() + + with pytest.raises(ValueError, match="Error fetching CSV from URL"): + loader.load(SourceContent("https://example.com/data.csv")) + + @patch("requests.get") + def test_csv_loader_handles_http_error(self, mock_get): + mock_get.return_value = Mock() + mock_get.return_value.raise_for_status.side_effect = Exception("404 Not Found") + loader = CSVLoader() + + with pytest.raises(ValueError, match="Error fetching CSV from URL"): + loader.load(SourceContent("https://example.com/notfound.csv")) diff --git a/tests/rag/test_directory_loader.py b/tests/rag/test_directory_loader.py new file mode 100644 index 000000000..7ddb38341 --- /dev/null +++ b/tests/rag/test_directory_loader.py @@ -0,0 +1,149 @@ +import os +import tempfile +import pytest + +from crewai_tools.rag.loaders.directory_loader import DirectoryLoader +from crewai_tools.rag.base_loader import LoaderResult +from crewai_tools.rag.source_content import SourceContent + + +@pytest.fixture +def temp_directory(): + with tempfile.TemporaryDirectory() as temp_dir: + yield temp_dir + + +class TestDirectoryLoader: + def _create_file(self, directory, filename, content="test content"): + path = os.path.join(directory, filename) + with open(path, "w") as f: + f.write(content) + return path + + def test_load_non_recursive(self, temp_directory): + self._create_file(temp_directory, "file1.txt") + self._create_file(temp_directory, "file2.txt") + subdir = os.path.join(temp_directory, "subdir") + os.makedirs(subdir) + self._create_file(subdir, "file3.txt") + + loader = DirectoryLoader() + result = loader.load(SourceContent(temp_directory), recursive=False) + + assert isinstance(result, LoaderResult) + assert "file1.txt" in result.content + assert "file2.txt" in result.content + assert "file3.txt" not in result.content + assert result.metadata["total_files"] == 2 + + def test_load_recursive(self, temp_directory): + self._create_file(temp_directory, "file1.txt") + nested = os.path.join(temp_directory, "subdir", "nested") + os.makedirs(nested) + self._create_file(os.path.join(temp_directory, "subdir"), "file2.txt") + self._create_file(nested, "file3.txt") + + loader = DirectoryLoader() + result = loader.load(SourceContent(temp_directory), recursive=True) + + assert all(f"file{i}.txt" in result.content for i in range(1, 4)) + + def test_include_and_exclude_extensions(self, temp_directory): + self._create_file(temp_directory, "a.txt") + self._create_file(temp_directory, "b.py") + self._create_file(temp_directory, "c.md") + + loader = DirectoryLoader() + result = loader.load(SourceContent(temp_directory), include_extensions=[".txt", ".py"]) + assert "a.txt" in result.content + assert "b.py" in result.content + assert "c.md" not in result.content + + result2 = loader.load(SourceContent(temp_directory), exclude_extensions=[".py", ".md"]) + assert "a.txt" in result2.content + assert "b.py" not in result2.content + assert "c.md" not in result2.content + + def test_max_files_limit(self, temp_directory): + for i in range(5): + self._create_file(temp_directory, f"file{i}.txt") + + loader = DirectoryLoader() + result = loader.load(SourceContent(temp_directory), max_files=3) + + assert result.metadata["total_files"] == 3 + assert all(f"file{i}.txt" in result.content for i in range(3)) + + def test_hidden_files_and_dirs_excluded(self, temp_directory): + self._create_file(temp_directory, "visible.txt", "visible") + self._create_file(temp_directory, ".hidden.txt", "hidden") + + hidden_dir = os.path.join(temp_directory, ".hidden") + os.makedirs(hidden_dir) + self._create_file(hidden_dir, "inside_hidden.txt") + + loader = DirectoryLoader() + result = loader.load(SourceContent(temp_directory), recursive=True) + + assert "visible.txt" in result.content + assert ".hidden.txt" not in result.content + assert "inside_hidden.txt" not in result.content + + def test_directory_does_not_exist(self): + loader = DirectoryLoader() + with pytest.raises(FileNotFoundError, match="Directory does not exist"): + loader.load(SourceContent("/path/does/not/exist")) + + def test_path_is_not_a_directory(self): + with tempfile.NamedTemporaryFile() as f: + loader = DirectoryLoader() + with pytest.raises(ValueError, match="Path is not a directory"): + loader.load(SourceContent(f.name)) + + def test_url_not_supported(self): + loader = DirectoryLoader() + with pytest.raises(ValueError, match="URL directory loading is not supported"): + loader.load(SourceContent("https://example.com")) + + def test_processing_error_handling(self, temp_directory): + self._create_file(temp_directory, "valid.txt") + error_file = self._create_file(temp_directory, "error.txt") + + loader = DirectoryLoader() + original_method = loader._process_single_file + + def mock(file_path): + if "error" in file_path: + raise ValueError("Mock error") + return original_method(file_path) + + loader._process_single_file = mock + result = loader.load(SourceContent(temp_directory)) + + assert "valid.txt" in result.content + assert "error.txt (ERROR)" in result.content + assert result.metadata["errors"] == 1 + assert len(result.metadata["error_details"]) == 1 + + def test_metadata_structure(self, temp_directory): + self._create_file(temp_directory, "test.txt", "Sample") + + loader = DirectoryLoader() + result = loader.load(SourceContent(temp_directory)) + metadata = result.metadata + + expected_keys = { + "format", "directory_path", "total_files", "processed_files", + "errors", "file_details", "error_details" + } + + assert expected_keys.issubset(metadata) + assert all(k in metadata["file_details"][0] for k in ("path", "metadata", "source")) + + def test_empty_directory(self, temp_directory): + loader = DirectoryLoader() + result = loader.load(SourceContent(temp_directory)) + + assert result.content == "" + assert result.metadata["total_files"] == 0 + assert result.metadata["processed_files"] == 0 diff --git a/tests/rag/test_docx_loader.py b/tests/rag/test_docx_loader.py new file mode 100644 index 000000000..f95aa0662 --- /dev/null +++ b/tests/rag/test_docx_loader.py @@ -0,0 +1,135 @@ +import tempfile +import pytest +from unittest.mock import patch, Mock + +from crewai_tools.rag.loaders.docx_loader import DOCXLoader +from crewai_tools.rag.base_loader import LoaderResult +from crewai_tools.rag.source_content import SourceContent + + +class TestDOCXLoader: + @patch('docx.Document') + def test_load_docx_from_file(self, mock_docx_class): + mock_doc = Mock() + mock_doc.paragraphs = [ + Mock(text="First paragraph"), + Mock(text="Second paragraph"), + Mock(text=" ") # Blank paragraph + ] + mock_doc.tables = [] + mock_docx_class.return_value = mock_doc + + with tempfile.NamedTemporaryFile(suffix='.docx') as f: + loader = DOCXLoader() + result = loader.load(SourceContent(f.name)) + + assert isinstance(result, LoaderResult) + assert result.content == "First paragraph\nSecond paragraph" + assert result.metadata == {"format": "docx", "paragraphs": 3, "tables": 0} + assert result.source == f.name + + @patch('docx.Document') + def test_load_docx_with_tables(self, mock_docx_class): + mock_doc = Mock() + mock_doc.paragraphs = [Mock(text="Document with table")] + mock_doc.tables = [Mock(), Mock()] + mock_docx_class.return_value = mock_doc + + with tempfile.NamedTemporaryFile(suffix='.docx') as f: + loader = DOCXLoader() + result = loader.load(SourceContent(f.name)) + + assert result.metadata["tables"] == 2 + + @patch('requests.get') + @patch('docx.Document') + @patch('tempfile.NamedTemporaryFile') + @patch('os.unlink') + def test_load_docx_from_url(self, mock_unlink, mock_tempfile, mock_docx_class, mock_get): + mock_get.return_value = Mock(content=b"fake docx content", raise_for_status=Mock()) + + mock_temp = Mock(name="/tmp/temp_docx_file.docx") + mock_temp.__enter__ = Mock(return_value=mock_temp) + mock_temp.__exit__ = Mock(return_value=None) + mock_tempfile.return_value = mock_temp + + mock_doc = Mock() + mock_doc.paragraphs = [Mock(text="Content from URL")] + mock_doc.tables = [] + mock_docx_class.return_value = mock_doc + + loader = DOCXLoader() + result = loader.load(SourceContent("https://example.com/test.docx")) + + assert "Content from URL" in result.content + assert result.source == "https://example.com/test.docx" + + headers = mock_get.call_args[1]['headers'] + assert "application/vnd.openxmlformats-officedocument.wordprocessingml.document" in headers['Accept'] + assert "crewai-tools DOCXLoader" in headers['User-Agent'] + + mock_temp.write.assert_called_once_with(b"fake docx content") + + @patch('requests.get') + @patch('docx.Document') + def test_load_docx_from_url_with_custom_headers(self, mock_docx_class, mock_get): + mock_get.return_value = Mock(content=b"fake docx content", raise_for_status=Mock()) + mock_docx_class.return_value = Mock(paragraphs=[], tables=[]) + + loader = DOCXLoader() + custom_headers = {"Authorization": "Bearer token"} + + with patch('tempfile.NamedTemporaryFile'), patch('os.unlink'): + loader.load(SourceContent("https://example.com/test.docx"), headers=custom_headers) + + assert mock_get.call_args[1]['headers'] == custom_headers + + @patch('requests.get') + def test_load_docx_url_download_error(self, mock_get): + mock_get.side_effect = Exception("Network error") + + loader = DOCXLoader() + with pytest.raises(ValueError, match="Error fetching DOCX from URL"): + loader.load(SourceContent("https://example.com/test.docx")) + + @patch('requests.get') + def test_load_docx_url_http_error(self, mock_get): + mock_get.return_value = Mock(raise_for_status=Mock(side_effect=Exception("404 Not Found"))) + + loader = DOCXLoader() + with pytest.raises(ValueError, match="Error fetching DOCX from URL"): + loader.load(SourceContent("https://example.com/notfound.docx")) + + def test_load_docx_invalid_source(self): + loader = DOCXLoader() + with pytest.raises(ValueError, match="Source must be a valid file path or URL"): + loader.load(SourceContent("not_a_file_or_url")) + + @patch('docx.Document') + def test_load_docx_parsing_error(self, mock_docx_class): + mock_docx_class.side_effect = Exception("Invalid DOCX file") + + with tempfile.NamedTemporaryFile(suffix='.docx') as f: + loader = DOCXLoader() + with pytest.raises(ValueError, match="Error loading DOCX file"): + loader.load(SourceContent(f.name)) + + @patch('docx.Document') + def test_load_docx_empty_document(self, mock_docx_class): + mock_docx_class.return_value = Mock(paragraphs=[], tables=[]) + + with tempfile.NamedTemporaryFile(suffix='.docx') as f: + loader = DOCXLoader() + result = loader.load(SourceContent(f.name)) + + assert result.content == "" + assert result.metadata == {"paragraphs": 0, "tables": 0, "format": "docx"} + + @patch('docx.Document') + def test_docx_doc_id_generation(self, mock_docx_class): + mock_docx_class.return_value = Mock(paragraphs=[Mock(text="Consistent content")], tables=[]) + + with tempfile.NamedTemporaryFile(suffix='.docx') as f: + loader = DOCXLoader() + source = SourceContent(f.name) + assert loader.load(source).doc_id == loader.load(source).doc_id diff --git a/tests/rag/test_json_loader.py b/tests/rag/test_json_loader.py new file mode 100644 index 000000000..b57480e16 --- /dev/null +++ b/tests/rag/test_json_loader.py @@ -0,0 +1,180 @@ +import json +import os +import tempfile +import pytest +from unittest.mock import patch, Mock + +from crewai_tools.rag.loaders.json_loader import JSONLoader +from crewai_tools.rag.base_loader import LoaderResult +from crewai_tools.rag.source_content import SourceContent + + +class TestJSONLoader: + def _create_temp_json_file(self, data) -> str: + """Helper to write JSON data to a temporary file and return its path.""" + with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f: + json.dump(data, f) + return f.name + + def _create_temp_raw_file(self, content: str) -> str: + """Helper to write raw content to a temporary file and return its path.""" + with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f: + f.write(content) + return f.name + + def _load_from_path(self, path) -> LoaderResult: + loader = JSONLoader() + return loader.load(SourceContent(path)) + + def test_load_json_dict(self): + path = self._create_temp_json_file({"name": "John", "age": 30, "items": ["a", "b", "c"]}) + try: + result = self._load_from_path(path) + assert isinstance(result, LoaderResult) + assert all(k in result.content for k in ["name", "John", "age", "30"]) + assert result.metadata == { + "format": "json", "type": "dict", "size": 3 + } + assert result.source == path + finally: + os.unlink(path) + + def test_load_json_list(self): + path = self._create_temp_json_file([ + {"id": 1, "name": "Item 1"}, + {"id": 2, "name": "Item 2"}, + ]) + try: + result = self._load_from_path(path) + assert result.metadata["type"] == "list" + assert result.metadata["size"] == 2 + assert all(item in result.content for item in ["Item 1", "Item 2"]) + finally: + os.unlink(path) + + @pytest.mark.parametrize("value, expected_type", [ + ("simple string value", "str"), + (42, "int"), + ]) + def test_load_json_primitives(self, value, expected_type): + path = self._create_temp_json_file(value) + try: + result = self._load_from_path(path) + assert result.metadata["type"] == expected_type + assert result.metadata["size"] == 1 + assert str(value) in result.content + finally: + os.unlink(path) + + def test_load_malformed_json(self): + path = self._create_temp_raw_file('{"invalid": json,}') + try: + result = self._load_from_path(path) + assert result.metadata["format"] == "json" + assert "parse_error" in result.metadata + assert result.content == '{"invalid": json,}' + finally: + os.unlink(path) + + def test_load_empty_file(self): + path = self._create_temp_raw_file('') + try: + result = self._load_from_path(path) + assert "parse_error" in result.metadata + assert result.content == '' + finally: + os.unlink(path) + + def test_load_text_input(self): + json_text = '{"message": "hello", "count": 5}' + loader = JSONLoader() + result = loader.load(SourceContent(json_text)) + assert all(part in result.content for part in ["message", "hello", "count", "5"]) + assert result.metadata["type"] == "dict" + assert result.metadata["size"] == 2 + + def test_load_complex_nested_json(self): + data = { + "users": [ + {"id": 1, "profile": {"name": "Alice", "settings": {"theme": "dark"}}}, + {"id": 2, "profile": {"name": "Bob", "settings": {"theme": "light"}}} + ], + "meta": {"total": 2, "version": "1.0"} + } + path = self._create_temp_json_file(data) + try: + result = self._load_from_path(path) + for value in ["Alice", "Bob", "dark", "light"]: + assert value in result.content + assert result.metadata["size"] == 2 # top-level keys + finally: + os.unlink(path) + + def test_consistent_doc_id(self): + path = self._create_temp_json_file({"test": "data"}) + try: + result1 = self._load_from_path(path) + result2 = self._load_from_path(path) + assert result1.doc_id == result2.doc_id + finally: + os.unlink(path) + + # ------------------------------ + # URL-based tests + # ------------------------------ + + @patch('requests.get') + def test_url_response_valid_json(self, mock_get): + mock_get.return_value = Mock( + text='{"key": "value", "number": 123}', + json=Mock(return_value={"key": "value", "number": 123}), + raise_for_status=Mock() + ) + + loader = JSONLoader() + result = loader.load(SourceContent("https://api.example.com/data.json")) + + assert all(val in result.content for val in ["key", "value", "number", "123"]) + headers = mock_get.call_args[1]['headers'] + assert "application/json" in headers['Accept'] + assert "crewai-tools JSONLoader" in headers['User-Agent'] + + @patch('requests.get') + def test_url_response_not_json(self, mock_get): + mock_get.return_value = Mock( + text='{"key": "value"}', + json=Mock(side_effect=ValueError("Not JSON")), + raise_for_status=Mock() + ) + + loader = JSONLoader() + result = loader.load(SourceContent("https://example.com/data.json")) + assert all(part in result.content for part in ["key", "value"]) + + @patch('requests.get') + def test_url_with_custom_headers(self, mock_get): + mock_get.return_value = Mock( + text='{"data": "test"}', + json=Mock(return_value={"data": "test"}), + raise_for_status=Mock() + ) + headers = {"Authorization": "Bearer token", "Custom-Header": "value"} + + loader = JSONLoader() + loader.load(SourceContent("https://api.example.com/data.json"), headers=headers) + + assert mock_get.call_args[1]['headers'] == headers + + @patch('requests.get') + def test_url_network_failure(self, mock_get): + mock_get.side_effect = Exception("Network error") + loader = JSONLoader() + with pytest.raises(ValueError, match="Error fetching JSON from URL"): + loader.load(SourceContent("https://api.example.com/data.json")) + + @patch('requests.get') + def test_url_http_error(self, mock_get): + mock_get.return_value = Mock(raise_for_status=Mock(side_effect=Exception("404"))) + loader = JSONLoader() + with pytest.raises(ValueError, match="Error fetching JSON from URL"): + loader.load(SourceContent("https://api.example.com/404.json")) diff --git a/tests/rag/test_mdx_loader.py b/tests/rag/test_mdx_loader.py new file mode 100644 index 000000000..ef7944c28 --- /dev/null +++ b/tests/rag/test_mdx_loader.py @@ -0,0 +1,176 @@ +import os +import tempfile +import pytest +from unittest.mock import patch, Mock + +from crewai_tools.rag.loaders.mdx_loader import MDXLoader +from crewai_tools.rag.base_loader import LoaderResult +from crewai_tools.rag.source_content import SourceContent + + +class TestMDXLoader: + + def _write_temp_mdx(self, content): + f = tempfile.NamedTemporaryFile(mode='w', suffix='.mdx', delete=False) + f.write(content) + f.close() + return f.name + + def _load_from_file(self, content): + path = self._write_temp_mdx(content) + try: + loader = MDXLoader() + return loader.load(SourceContent(path)), path + finally: + os.unlink(path) + + def test_load_basic_mdx_file(self): + content = """ +import Component from './Component' +export const meta = { title: 'Test' } + +# Test MDX File + +This is a **markdown** file with JSX. + + + +Some more content. + +
+

Nested content

+
+""" + result, path = self._load_from_file(content) + + assert isinstance(result, LoaderResult) + assert all(tag not in result.content for tag in ["import", "export", ""]) + assert all(text in result.content for text in ["# Test MDX File", "markdown", "Some more content", "Nested content"]) + assert result.metadata["format"] == "mdx" + assert result.source == path + + def test_mdx_multiple_imports_exports(self): + content = """ +import React from 'react' +import { useState } from 'react' +import CustomComponent from './custom' + +export default function Layout() { return null } +export const config = { test: true } + +# Content + +Regular markdown content here. +""" + result, _ = self._load_from_file(content) + assert "# Content" in result.content + assert "Regular markdown content here." in result.content + assert "import" not in result.content and "export" not in result.content + + def test_complex_jsx_cleanup(self): + content = """ +# MDX with Complex JSX + +
+ Info: This is important information. +
  • Item 1
  • Item 2
+
+ +Regular paragraph text. + +Nested content inside component +""" + result, _ = self._load_from_file(content) + assert all(tag not in result.content for tag in ["", "
    ", " +

    Only JSX content

    +

    No markdown here

    +
+""" + result, _ = self._load_from_file(content) + assert all(tag not in result.content for tag in ["
", "

", "

"]) + assert "Only JSX content" in result.content + assert "No markdown here" in result.content + + @patch('requests.get') + def test_load_mdx_from_url(self, mock_get): + mock_get.return_value = Mock(text="# MDX from URL\n\nContent here.\n\n", raise_for_status=lambda: None) + loader = MDXLoader() + result = loader.load(SourceContent("https://example.com/content.mdx")) + assert "# MDX from URL" in result.content + assert "" not in result.content + + @patch('requests.get') + def test_load_mdx_with_custom_headers(self, mock_get): + mock_get.return_value = Mock(text="# Custom headers test", raise_for_status=lambda: None) + loader = MDXLoader() + loader.load(SourceContent("https://example.com"), headers={"Authorization": "Bearer token"}) + assert mock_get.call_args[1]['headers'] == {"Authorization": "Bearer token"} + + @patch('requests.get') + def test_mdx_url_fetch_error(self, mock_get): + mock_get.side_effect = Exception("Network error") + with pytest.raises(ValueError, match="Error fetching MDX from URL"): + MDXLoader().load(SourceContent("https://example.com")) + + def test_load_inline_mdx_text(self): + content = """# Inline MDX\n\nimport Something from 'somewhere'\n\nContent with .\n\nexport const meta = { title: 'Test' }""" + loader = MDXLoader() + result = loader.load(SourceContent(content)) + assert "# Inline MDX" in result.content + assert "Content with ." in result.content + + def test_empty_result_after_cleaning(self): + content = """ +import Something from 'somewhere' +export const config = {} +

+""" + result, _ = self._load_from_file(content) + assert result.content.strip() == "" + + def test_edge_case_parsing(self): + content = """ +# Title + + +Multi-line +JSX content + + +import { a, b } from 'module' + +export { x, y } + +Final text. +""" + result, _ = self._load_from_file(content) + assert "# Title" in result.content + assert "JSX content" in result.content + assert "Final text." in result.content + assert all(phrase not in result.content for phrase in ["import {", "export {", ""]) diff --git a/tests/rag/test_text_loaders.py b/tests/rag/test_text_loaders.py new file mode 100644 index 000000000..e72738778 --- /dev/null +++ b/tests/rag/test_text_loaders.py @@ -0,0 +1,160 @@ +import hashlib +import os +import tempfile +import pytest + +from crewai_tools.rag.loaders.text_loader import TextFileLoader, TextLoader +from crewai_tools.rag.base_loader import LoaderResult +from crewai_tools.rag.source_content import SourceContent + + +def write_temp_file(content, suffix=".txt", encoding="utf-8"): + with tempfile.NamedTemporaryFile(mode="w", suffix=suffix, delete=False, encoding=encoding) as f: + f.write(content) + return f.name + + +def cleanup_temp_file(path): + try: + os.unlink(path) + except FileNotFoundError: + pass + + +class TestTextFileLoader: + def test_basic_text_file(self): + content = "This is test content\nWith multiple lines\nAnd more text" + path = write_temp_file(content) + try: + result = TextFileLoader().load(SourceContent(path)) + assert isinstance(result, LoaderResult) + assert result.content == content + assert result.source == path + assert result.doc_id + assert result.metadata in (None, {}) + finally: + cleanup_temp_file(path) + + def test_empty_file(self): + path = write_temp_file("") + try: + result = TextFileLoader().load(SourceContent(path)) + assert result.content == "" + finally: + cleanup_temp_file(path) + + def test_unicode_content(self): + content = "Hello 世界 🌍 émojis 🎉 åäö" + path = write_temp_file(content) + try: + result = TextFileLoader().load(SourceContent(path)) + assert content in result.content + finally: + cleanup_temp_file(path) + + def test_large_file(self): + content = "\n".join(f"Line {i}" for i in range(100)) + path = write_temp_file(content) + try: + result = TextFileLoader().load(SourceContent(path)) + assert "Line 0" in result.content + assert "Line 99" in result.content + assert result.content.count("\n") == 99 + finally: + cleanup_temp_file(path) + + def test_missing_file(self): + with pytest.raises(FileNotFoundError): + TextFileLoader().load(SourceContent("/nonexistent/path.txt")) + + def test_permission_denied(self): + path = write_temp_file("Some content") + os.chmod(path, 0o000) + try: + with pytest.raises(PermissionError): + TextFileLoader().load(SourceContent(path)) + finally: + os.chmod(path, 0o644) + cleanup_temp_file(path) + + def test_doc_id_consistency(self): + content = "Consistent content" + path = write_temp_file(content) + try: + loader = TextFileLoader() + result1 = loader.load(SourceContent(path)) + result2 = loader.load(SourceContent(path)) + expected_id = hashlib.sha256((path + content).encode("utf-8")).hexdigest() + assert result1.doc_id == result2.doc_id == expected_id + finally: + cleanup_temp_file(path) + + def test_various_extensions(self): + content = "Same content" + for ext in [".txt", ".md", ".log", ".json"]: + path = write_temp_file(content, suffix=ext) + try: + result = TextFileLoader().load(SourceContent(path)) + assert result.content == content + finally: + cleanup_temp_file(path) + + +class TestTextLoader: + def test_basic_text(self): + content = "Raw text" + result = TextLoader().load(SourceContent(content)) + expected_hash = hashlib.sha256(content.encode("utf-8")).hexdigest() + assert result.content == content + assert result.source == expected_hash + assert result.doc_id == expected_hash + + def test_multiline_text(self): + content = "Line 1\nLine 2\nLine 3" + result = TextLoader().load(SourceContent(content)) + assert "Line 2" in result.content + + def test_empty_text(self): + result = TextLoader().load(SourceContent("")) + assert result.content == "" + assert result.source == hashlib.sha256("".encode("utf-8")).hexdigest() + + def test_unicode_text(self): + content = "世界 🌍 émojis 🎉 åäö" + result = TextLoader().load(SourceContent(content)) + assert content in result.content + + def test_special_characters(self): + content = "!@#$$%^&*()_+-=~`{}[]\\|;:'\",.<>/?" + result = TextLoader().load(SourceContent(content)) + assert result.content == content + + def test_doc_id_uniqueness(self): + result1 = TextLoader().load(SourceContent("A")) + result2 = TextLoader().load(SourceContent("B")) + assert result1.doc_id != result2.doc_id + + def test_whitespace_text(self): + content = " \n\t " + result = TextLoader().load(SourceContent(content)) + assert result.content == content + + def test_long_text(self): + content = "A" * 10000 + result = TextLoader().load(SourceContent(content)) + assert len(result.content) == 10000 + + +class TestTextLoadersIntegration: + def test_consistency_between_loaders(self): + content = "Consistent content" + text_result = TextLoader().load(SourceContent(content)) + file_path = write_temp_file(content) + try: + file_result = TextFileLoader().load(SourceContent(file_path)) + + assert text_result.content == file_result.content + assert text_result.source != file_result.source + assert text_result.doc_id != file_result.doc_id + finally: + cleanup_temp_file(file_path) diff --git a/tests/rag/test_webpage_loader.py b/tests/rag/test_webpage_loader.py new file mode 100644 index 000000000..9e02f410b --- /dev/null +++ b/tests/rag/test_webpage_loader.py @@ -0,0 +1,137 @@ +import pytest +from unittest.mock import patch, Mock +from crewai_tools.rag.loaders.webpage_loader import WebPageLoader +from crewai_tools.rag.base_loader import LoaderResult +from crewai_tools.rag.source_content import SourceContent + + +class TestWebPageLoader: + def setup_mock_response(self, text, status_code=200, content_type="text/html"): + response = Mock() + response.text = text + response.apparent_encoding = "utf-8" + response.status_code = status_code + response.headers = {"content-type": content_type} + return response + + def setup_mock_soup(self, text, title=None, script_style_elements=None): + soup = Mock() + soup.get_text.return_value = text + soup.title = Mock(string=title) if title is not None else None + soup.return_value = script_style_elements or [] + return soup + + @patch('requests.get') + @patch('crewai_tools.rag.loaders.webpage_loader.BeautifulSoup') + def test_load_basic_webpage(self, mock_bs, mock_get): + mock_get.return_value = self.setup_mock_response("Test Page

Test content

") + mock_bs.return_value = self.setup_mock_soup("Test content", title="Test Page") + + loader = WebPageLoader() + result = loader.load(SourceContent("https://example.com")) + + assert isinstance(result, LoaderResult) + assert result.content == "Test content" + assert result.metadata["title"] == "Test Page" + + @patch('requests.get') + @patch('crewai_tools.rag.loaders.webpage_loader.BeautifulSoup') + def test_load_webpage_with_scripts_and_styles(self, mock_bs, mock_get): + html = """ + Page with Scripts +

Visible content

+ """ + mock_get.return_value = self.setup_mock_response(html) + scripts = [Mock(), Mock()] + styles = [Mock()] + for el in scripts + styles: + el.decompose = Mock() + mock_bs.return_value = self.setup_mock_soup("Page with Scripts Visible content", title="Page with Scripts", script_style_elements=scripts + styles) + + loader = WebPageLoader() + result = loader.load(SourceContent("https://example.com/with-scripts")) + + assert "Visible content" in result.content + for el in scripts + styles: + el.decompose.assert_called_once() + + @patch('requests.get') + @patch('crewai_tools.rag.loaders.webpage_loader.BeautifulSoup') + def test_text_cleaning_and_title_handling(self, mock_bs, mock_get): + mock_get.return_value = self.setup_mock_response("

Messy text

") + mock_bs.return_value = self.setup_mock_soup("Text with extra spaces\n\n More\t text \n\n", title=None) + + loader = WebPageLoader() + result = loader.load(SourceContent("https://example.com/messy-text")) + assert result.content is not None + assert result.metadata["title"] == "" + + @patch('requests.get') + @patch('crewai_tools.rag.loaders.webpage_loader.BeautifulSoup') + def test_empty_or_missing_title(self, mock_bs, mock_get): + for title in [None, ""]: + mock_get.return_value = self.setup_mock_response("Content") + mock_bs.return_value = self.setup_mock_soup("Content", title=title) + + loader = WebPageLoader() + result = loader.load(SourceContent("https://example.com")) + assert result.metadata["title"] == "" + + @patch('requests.get') + def test_custom_and_default_headers(self, mock_get): + mock_get.return_value = self.setup_mock_response("Test") + custom_headers = {"User-Agent": "Bot", "Authorization": "Bearer xyz", "Accept": "text/html"} + + with patch('crewai_tools.rag.loaders.webpage_loader.BeautifulSoup') as mock_bs: + mock_bs.return_value = self.setup_mock_soup("Test") + WebPageLoader().load(SourceContent("https://example.com"), headers=custom_headers) + + assert mock_get.call_args[1]['headers'] == custom_headers + + @patch('requests.get') + def test_error_handling(self, mock_get): + for error in [Exception("Fail"), ValueError("Bad"), ImportError("Oops")]: + mock_get.side_effect = error + with pytest.raises(ValueError, match="Error loading webpage"): + WebPageLoader().load(SourceContent("https://example.com")) + + @patch('requests.get') + def test_timeout_and_http_error(self, mock_get): + import requests + mock_get.side_effect = requests.Timeout("Timeout") + with pytest.raises(ValueError): + WebPageLoader().load(SourceContent("https://example.com")) + + mock_response = Mock() + mock_response.raise_for_status.side_effect = requests.HTTPError("404") + mock_get.side_effect = None + mock_get.return_value = mock_response + with pytest.raises(ValueError): + WebPageLoader().load(SourceContent("https://example.com/404")) + + @patch('requests.get') + @patch('crewai_tools.rag.loaders.webpage_loader.BeautifulSoup') + def test_doc_id_consistency(self, mock_bs, mock_get): + mock_get.return_value = self.setup_mock_response("Doc") + mock_bs.return_value = self.setup_mock_soup("Doc") + + loader = WebPageLoader() + result1 = loader.load(SourceContent("https://example.com")) + result2 = loader.load(SourceContent("https://example.com")) + + assert result1.doc_id == result2.doc_id + + @patch('requests.get') + @patch('crewai_tools.rag.loaders.webpage_loader.BeautifulSoup') + def test_status_code_and_content_type(self, mock_bs, mock_get): + for status in [200, 201, 301]: + mock_get.return_value = self.setup_mock_response(f"Status {status}", status_code=status) + mock_bs.return_value = self.setup_mock_soup(f"Status {status}") + result = WebPageLoader().load(SourceContent(f"https://example.com/{status}")) + assert result.metadata["status_code"] == status + + for ctype in ["text/html", "text/plain", "application/xhtml+xml"]: + mock_get.return_value = self.setup_mock_response("Content", content_type=ctype) + mock_bs.return_value = self.setup_mock_soup("Content") + result = WebPageLoader().load(SourceContent("https://example.com")) + assert result.metadata["content_type"] == ctype diff --git a/tests/rag/test_xml_loader.py b/tests/rag/test_xml_loader.py new file mode 100644 index 000000000..9e02f410b --- /dev/null +++ b/tests/rag/test_xml_loader.py @@ -0,0 +1,137 @@ +import pytest +from unittest.mock import patch, Mock +from crewai_tools.rag.loaders.webpage_loader import WebPageLoader +from crewai_tools.rag.base_loader import LoaderResult +from crewai_tools.rag.source_content import SourceContent + + +class TestWebPageLoader: + def setup_mock_response(self, text, status_code=200, content_type="text/html"): + response = Mock() + response.text = text + response.apparent_encoding = "utf-8" + response.status_code = status_code + response.headers = {"content-type": content_type} + return response + + def setup_mock_soup(self, text, title=None, script_style_elements=None): + soup = Mock() + soup.get_text.return_value = text + soup.title = Mock(string=title) if title is not None else None + soup.return_value = script_style_elements or [] + return soup + + @patch('requests.get') + @patch('crewai_tools.rag.loaders.webpage_loader.BeautifulSoup') + def test_load_basic_webpage(self, mock_bs, mock_get): + mock_get.return_value = self.setup_mock_response("Test Page

Test content

") + mock_bs.return_value = self.setup_mock_soup("Test content", title="Test Page") + + loader = WebPageLoader() + result = loader.load(SourceContent("https://example.com")) + + assert isinstance(result, LoaderResult) + assert result.content == "Test content" + assert result.metadata["title"] == "Test Page" + + @patch('requests.get') + @patch('crewai_tools.rag.loaders.webpage_loader.BeautifulSoup') + def test_load_webpage_with_scripts_and_styles(self, mock_bs, mock_get): + html = """ + Page with Scripts +

Visible content

+ """ + mock_get.return_value = self.setup_mock_response(html) + scripts = [Mock(), Mock()] + styles = [Mock()] + for el in scripts + styles: + el.decompose = Mock() + mock_bs.return_value = self.setup_mock_soup("Page with Scripts Visible content", title="Page with Scripts", script_style_elements=scripts + styles) + + loader = WebPageLoader() + result = loader.load(SourceContent("https://example.com/with-scripts")) + + assert "Visible content" in result.content + for el in scripts + styles: + el.decompose.assert_called_once() + + @patch('requests.get') + @patch('crewai_tools.rag.loaders.webpage_loader.BeautifulSoup') + def test_text_cleaning_and_title_handling(self, mock_bs, mock_get): + mock_get.return_value = self.setup_mock_response("

Messy text

") + mock_bs.return_value = self.setup_mock_soup("Text with extra spaces\n\n More\t text \n\n", title=None) + + loader = WebPageLoader() + result = loader.load(SourceContent("https://example.com/messy-text")) + assert result.content is not None + assert result.metadata["title"] == "" + + @patch('requests.get') + @patch('crewai_tools.rag.loaders.webpage_loader.BeautifulSoup') + def test_empty_or_missing_title(self, mock_bs, mock_get): + for title in [None, ""]: + mock_get.return_value = self.setup_mock_response("Content") + mock_bs.return_value = self.setup_mock_soup("Content", title=title) + + loader = WebPageLoader() + result = loader.load(SourceContent("https://example.com")) + assert result.metadata["title"] == "" + + @patch('requests.get') + def test_custom_and_default_headers(self, mock_get): + mock_get.return_value = self.setup_mock_response("Test") + custom_headers = {"User-Agent": "Bot", "Authorization": "Bearer xyz", "Accept": "text/html"} + + with patch('crewai_tools.rag.loaders.webpage_loader.BeautifulSoup') as mock_bs: + mock_bs.return_value = self.setup_mock_soup("Test") + WebPageLoader().load(SourceContent("https://example.com"), headers=custom_headers) + + assert mock_get.call_args[1]['headers'] == custom_headers + + @patch('requests.get') + def test_error_handling(self, mock_get): + for error in [Exception("Fail"), ValueError("Bad"), ImportError("Oops")]: + mock_get.side_effect = error + with pytest.raises(ValueError, match="Error loading webpage"): + WebPageLoader().load(SourceContent("https://example.com")) + + @patch('requests.get') + def test_timeout_and_http_error(self, mock_get): + import requests + mock_get.side_effect = requests.Timeout("Timeout") + with pytest.raises(ValueError): + WebPageLoader().load(SourceContent("https://example.com")) + + mock_response = Mock() + mock_response.raise_for_status.side_effect = requests.HTTPError("404") + mock_get.side_effect = None + mock_get.return_value = mock_response + with pytest.raises(ValueError): + WebPageLoader().load(SourceContent("https://example.com/404")) + + @patch('requests.get') + @patch('crewai_tools.rag.loaders.webpage_loader.BeautifulSoup') + def test_doc_id_consistency(self, mock_bs, mock_get): + mock_get.return_value = self.setup_mock_response("Doc") + mock_bs.return_value = self.setup_mock_soup("Doc") + + loader = WebPageLoader() + result1 = loader.load(SourceContent("https://example.com")) + result2 = loader.load(SourceContent("https://example.com")) + + assert result1.doc_id == result2.doc_id + + @patch('requests.get') + @patch('crewai_tools.rag.loaders.webpage_loader.BeautifulSoup') + def test_status_code_and_content_type(self, mock_bs, mock_get): + for status in [200, 201, 301]: + mock_get.return_value = self.setup_mock_response(f"Status {status}", status_code=status) + mock_bs.return_value = self.setup_mock_soup(f"Status {status}") + result = WebPageLoader().load(SourceContent(f"https://example.com/{status}")) + assert result.metadata["status_code"] == status + + for ctype in ["text/html", "text/plain", "application/xhtml+xml"]: + mock_get.return_value = self.setup_mock_response("Content", content_type=ctype) + mock_bs.return_value = self.setup_mock_soup("Content") + result = WebPageLoader().load(SourceContent("https://example.com")) + assert result.metadata["content_type"] == ctype diff --git a/tests/test_generate_tool_specs.py b/tests/test_generate_tool_specs.py new file mode 100644 index 000000000..eeb407be1 --- /dev/null +++ b/tests/test_generate_tool_specs.py @@ -0,0 +1,193 @@ +import json +from typing import List, Optional, Type +from unittest import mock + +import pytest +from crewai.tools.base_tool import BaseTool, EnvVar +from pydantic import BaseModel, Field + +from generate_tool_specs import ToolSpecExtractor + + +class MockToolSchema(BaseModel): + query: str = Field(..., description="The query parameter") + count: int = Field(5, description="Number of results to return") + filters: Optional[List[str]] = Field(None, description="Optional filters to apply") + + +class MockTool(BaseTool): + name: str = "Mock Search Tool" + description: str = "A tool that mocks search functionality" + args_schema: Type[BaseModel] = MockToolSchema + + another_parameter: str = Field( + "Another way to define a default value", description="" + ) + my_parameter: str = Field("This is default value", description="What a description") + my_parameter_bool: bool = Field(False) + package_dependencies: List[str] = Field( + ["this-is-a-required-package", "another-required-package"], description="" + ) + env_vars: List[EnvVar] = [ + EnvVar( + name="SERPER_API_KEY", + description="API key for Serper", + required=True, + default=None, + ), + EnvVar( + name="API_RATE_LIMIT", + description="API rate limit", + required=False, + default="100", + ), + ] + + +@pytest.fixture +def extractor(): + ext = ToolSpecExtractor() + return ext + + +def test_unwrap_schema(extractor): + nested_schema = { + "type": "function-after", + "schema": {"type": "default", "schema": {"type": "str", "value": "test"}}, + } + result = extractor._unwrap_schema(nested_schema) + assert result["type"] == "str" + assert result["value"] == "test" + + +@pytest.fixture +def mock_tool_extractor(extractor): + with ( + mock.patch("generate_tool_specs.dir", return_value=["MockTool"]), + mock.patch("generate_tool_specs.getattr", return_value=MockTool), + ): + extractor.extract_all_tools() + assert len(extractor.tools_spec) == 1 + return extractor.tools_spec[0] + + +def test_extract_basic_tool_info(mock_tool_extractor): + tool_info = mock_tool_extractor + + assert tool_info.keys() == { + "name", + "humanized_name", + "description", + "run_params_schema", + "env_vars", + "init_params_schema", + "package_dependencies", + } + + assert tool_info["name"] == "MockTool" + assert tool_info["humanized_name"] == "Mock Search Tool" + assert tool_info["description"] == "A tool that mocks search functionality" + + +def test_extract_init_params_schema(mock_tool_extractor): + tool_info = mock_tool_extractor + init_params_schema = tool_info["init_params_schema"] + + assert init_params_schema.keys() == { + "$defs", + "properties", + "title", + "type", + } + + another_parameter = init_params_schema["properties"]["another_parameter"] + assert another_parameter["description"] == "" + assert another_parameter["default"] == "Another way to define a default value" + assert another_parameter["type"] == "string" + + my_parameter = init_params_schema["properties"]["my_parameter"] + assert my_parameter["description"] == "What a description" + assert my_parameter["default"] == "This is default value" + assert my_parameter["type"] == "string" + + my_parameter_bool = init_params_schema["properties"]["my_parameter_bool"] + assert my_parameter_bool["default"] == False + assert my_parameter_bool["type"] == "boolean" + + +def test_extract_env_vars(mock_tool_extractor): + tool_info = mock_tool_extractor + + assert len(tool_info["env_vars"]) == 2 + api_key_var, rate_limit_var = tool_info["env_vars"] + assert api_key_var["name"] == "SERPER_API_KEY" + assert api_key_var["description"] == "API key for Serper" + assert api_key_var["required"] == True + assert api_key_var["default"] == None + + assert rate_limit_var["name"] == "API_RATE_LIMIT" + assert rate_limit_var["description"] == "API rate limit" + assert rate_limit_var["required"] == False + assert rate_limit_var["default"] == "100" + + +def test_extract_run_params_schema(mock_tool_extractor): + tool_info = mock_tool_extractor + + run_params_schema = tool_info["run_params_schema"] + assert run_params_schema.keys() == { + "properties", + "required", + "title", + "type", + } + + query_param = run_params_schema["properties"]["query"] + assert query_param["description"] == "The query parameter" + assert query_param["type"] == "string" + + count_param = run_params_schema["properties"]["count"] + assert count_param["type"] == "integer" + assert count_param["default"] == 5 + + filters_param = run_params_schema["properties"]["filters"] + assert filters_param["description"] == "Optional filters to apply" + assert filters_param["default"] == None + assert filters_param["anyOf"] == [ + {"items": {"type": "string"}, "type": "array"}, + {"type": "null"}, + ] + + +def test_extract_package_dependencies(mock_tool_extractor): + tool_info = mock_tool_extractor + assert tool_info["package_dependencies"] == [ + "this-is-a-required-package", + "another-required-package", + ] + + +def test_save_to_json(extractor, tmp_path): + extractor.tools_spec = [ + { + "name": "TestTool", + "humanized_name": "Test Tool", + "description": "A test tool", + "run_params_schema": [ + {"name": "param1", "description": "Test parameter", "type": "str"} + ], + } + ] + + file_path = tmp_path / "output.json" + extractor.save_to_json(str(file_path)) + + assert file_path.exists() + + with open(file_path, "r") as f: + data = json.load(f) + + assert "tools" in data + assert len(data["tools"]) == 1 + assert data["tools"][0]["humanized_name"] == "Test Tool" + assert data["tools"][0]["run_params_schema"][0]["name"] == "param1" diff --git a/tests/test_optional_dependencies.py b/tests/test_optional_dependencies.py new file mode 100644 index 000000000..b2d691a61 --- /dev/null +++ b/tests/test_optional_dependencies.py @@ -0,0 +1,41 @@ +import subprocess +import tempfile +from pathlib import Path + +import pytest + + +@pytest.fixture +def temp_project(): + temp_dir = tempfile.TemporaryDirectory() + project_dir = Path(temp_dir.name) / "test_project" + project_dir.mkdir() + + pyproject_content = f""" + [project] + name = "test-project" + version = "0.1.0" + description = "Test project" + requires-python = ">=3.10" + """ + + (project_dir / "pyproject.toml").write_text(pyproject_content) + run_command(["uv", "add", "--editable", f"file://{Path.cwd().absolute()}"], project_dir) + run_command(["uv", "sync"], project_dir) + yield project_dir + + +def run_command(cmd, cwd): + return subprocess.run(cmd, cwd=cwd, capture_output=True, text=True) + + +def test_no_optional_dependencies_in_init(temp_project): + """ + Test that crewai-tools can be imported without optional dependencies. + + The package defines optional dependencies in pyproject.toml, but the base + package should be importable without any of these optional dependencies + being installed. + """ + result = run_command(["uv", "run", "python", "-c", "import crewai_tools"], temp_project) + assert result.returncode == 0, f"Import failed with error: {result.stderr}" \ No newline at end of file diff --git a/tests/tools/__init__.py b/tests/tools/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/tools/brave_search_tool_test.py b/tests/tools/brave_search_tool_test.py new file mode 100644 index 000000000..36300f723 --- /dev/null +++ b/tests/tools/brave_search_tool_test.py @@ -0,0 +1,50 @@ +from unittest.mock import patch + +import pytest + +from crewai_tools.tools.brave_search_tool.brave_search_tool import BraveSearchTool + + +@pytest.fixture +def brave_tool(): + return BraveSearchTool(n_results=2) + + +def test_brave_tool_initialization(): + tool = BraveSearchTool() + assert tool.n_results == 10 + assert tool.save_file is False + + +@patch("requests.get") +def test_brave_tool_search(mock_get, brave_tool): + mock_response = { + "web": { + "results": [ + { + "title": "Test Title", + "url": "http://test.com", + "description": "Test Description", + } + ] + } + } + mock_get.return_value.json.return_value = mock_response + + result = brave_tool.run(search_query="test") + assert "Test Title" in result + assert "http://test.com" in result + + +def test_brave_tool(): + tool = BraveSearchTool( + n_results=2, + ) + x = tool.run(search_query="ChatGPT") + print(x) + + +if __name__ == "__main__": + test_brave_tool() + test_brave_tool_initialization() + # test_brave_tool_search(brave_tool) diff --git a/tests/tools/brightdata_serp_tool_test.py b/tests/tools/brightdata_serp_tool_test.py new file mode 100644 index 000000000..11ca018e8 --- /dev/null +++ b/tests/tools/brightdata_serp_tool_test.py @@ -0,0 +1,54 @@ +import unittest +from unittest.mock import MagicMock, patch + +from crewai_tools.tools.brightdata_tool.brightdata_serp import BrightDataSearchTool + + +class TestBrightDataSearchTool(unittest.TestCase): + @patch.dict( + "os.environ", + {"BRIGHT_DATA_API_KEY": "test_api_key", "BRIGHT_DATA_ZONE": "test_zone"}, + ) + def setUp(self): + self.tool = BrightDataSearchTool() + + @patch("requests.post") + def test_run_successful_search(self, mock_post): + # Sample mock JSON response + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.text = "mock response text" + mock_post.return_value = mock_response + + # Define search input + input_data = { + "query": "latest AI news", + "search_engine": "google", + "country": "us", + "language": "en", + "search_type": "nws", + "device_type": "desktop", + "parse_results": True, + "save_file": False, + } + + result = self.tool._run(**input_data) + + # Assertions + self.assertIsInstance(result, str) # Your tool returns response.text (string) + mock_post.assert_called_once() + + @patch("requests.post") + def test_run_with_request_exception(self, mock_post): + mock_post.side_effect = Exception("Timeout") + + result = self.tool._run(query="AI", search_engine="google") + self.assertIn("Error", result) + + def tearDown(self): + # Clean up env vars + pass + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/tools/brightdata_webunlocker_tool_test.py b/tests/tools/brightdata_webunlocker_tool_test.py new file mode 100644 index 000000000..629cb2e87 --- /dev/null +++ b/tests/tools/brightdata_webunlocker_tool_test.py @@ -0,0 +1,64 @@ +from unittest.mock import Mock, patch + +import requests + +from crewai_tools.tools.brightdata_tool.brightdata_unlocker import ( + BrightDataWebUnlockerTool, +) + + +@patch.dict( + "os.environ", + {"BRIGHT_DATA_API_KEY": "test_api_key", "BRIGHT_DATA_ZONE": "test_zone"}, +) +@patch("crewai_tools.tools.brightdata_tool.brightdata_unlocker.requests.post") +def test_run_success_html(mock_post): + mock_response = Mock() + mock_response.status_code = 200 + mock_response.text = "Test" + mock_response.raise_for_status = Mock() + mock_post.return_value = mock_response + + tool = BrightDataWebUnlockerTool() + result = tool._run(url="https://example.com", format="html", save_file=False) + + print(result) + + +@patch.dict( + "os.environ", + {"BRIGHT_DATA_API_KEY": "test_api_key", "BRIGHT_DATA_ZONE": "test_zone"}, +) +@patch("crewai_tools.tools.brightdata_tool.brightdata_unlocker.requests.post") +def test_run_success_json(mock_post): + mock_response = Mock() + mock_response.status_code = 200 + mock_response.text = "mock response text" + mock_response.raise_for_status = Mock() + mock_post.return_value = mock_response + + tool = BrightDataWebUnlockerTool() + result = tool._run(url="https://example.com", format="json") + + assert isinstance(result, str) + + +@patch.dict( + "os.environ", + {"BRIGHT_DATA_API_KEY": "test_api_key", "BRIGHT_DATA_ZONE": "test_zone"}, +) +@patch("crewai_tools.tools.brightdata_tool.brightdata_unlocker.requests.post") +def test_run_http_error(mock_post): + mock_response = Mock() + mock_response.status_code = 403 + mock_response.text = "Forbidden" + mock_response.raise_for_status.side_effect = requests.HTTPError( + response=mock_response + ) + mock_post.return_value = mock_response + + tool = BrightDataWebUnlockerTool() + result = tool._run(url="https://example.com") + + assert "HTTP Error" in result + assert "Forbidden" in result diff --git a/tests/tools/cassettes/test_search_tools/test_csv_search_tool.yaml b/tests/tools/cassettes/test_search_tools/test_csv_search_tool.yaml new file mode 100644 index 000000000..4247ba7bb --- /dev/null +++ b/tests/tools/cassettes/test_search_tools/test_csv_search_tool.yaml @@ -0,0 +1,251 @@ +interactions: +- request: + body: '{"input": ["name: test, description: This is a test CSV file"], "model": + "text-embedding-ada-002", "encoding_format": "base64"}' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate, zstd + connection: + - keep-alive + content-length: + - '127' + content-type: + - application/json + host: + - api.openai.com + user-agent: + - OpenAI/Python 1.66.3 + x-stainless-arch: + - arm64 + x-stainless-async: + - 'false' + x-stainless-lang: + - python + x-stainless-os: + - MacOS + x-stainless-package-version: + - 1.66.3 + x-stainless-read-timeout: + - '600' + x-stainless-retry-count: + - '0' + x-stainless-runtime: + - CPython + x-stainless-runtime-version: + - 3.12.9 + method: POST + uri: https://api.openai.com/v1/embeddings + response: + content: "{\n \"object\": \"list\",\n \"data\": [\n {\n \"object\": + \"embedding\",\n \"index\": 0,\n \"embedding\": \"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\"\n + \ }\n ],\n \"model\": \"text-embedding-ada-002-v2\",\n \"usage\": {\n \"prompt_tokens\": + 12,\n \"total_tokens\": 12\n }\n}\n" + headers: + CF-RAY: + - 936f9362dc5e7e01-GRU + Connection: + - keep-alive + Content-Encoding: + - gzip + Content-Type: + - application/json + Date: + - Sun, 27 Apr 2025 16:07:57 GMT + Server: + - cloudflare + Set-Cookie: + - __cf_bm=8J2Cz0gyk5BpRSYbzjWETqMiyphlW8TAe7802MlHMe0-1745770077-1.0.1.1-qbyKIgJQJDS2wWDKC1x0RrxzJk5mcE4wDunq25j.sNSe_EMvVEIQTJR6t4Jmrknve3lSxXTsW4VL_3EYYk6ehiq6yIQDVzVkqNfU_xlwris; + path=/; expires=Sun, 27-Apr-25 16:37:57 GMT; domain=.api.openai.com; HttpOnly; + Secure; SameSite=None + - _cfuvid=r11uIiVDOWdkeX_FkCjMMnH.Z9zcvp4ptrKi3luIa9s-1745770077165-0.0.1.1-604800000; + path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None + Transfer-Encoding: + - chunked + X-Content-Type-Options: + - nosniff + access-control-allow-origin: + - '*' + access-control-expose-headers: + - X-Request-ID + alt-svc: + - h3=":443"; ma=86400 + cf-cache-status: + - DYNAMIC + openai-model: + - text-embedding-ada-002-v2 + openai-organization: + - crewai-iuxna1 + openai-processing-ms: + - '170' + openai-version: + - '2020-10-01' + strict-transport-security: + - max-age=31536000; includeSubDomains; preload + via: + - envoy-router-56c4dc8986-59htf + x-envoy-upstream-service-time: + - '96' + x-ratelimit-limit-requests: + - '10000' + x-ratelimit-limit-tokens: + - '10000000' + x-ratelimit-remaining-requests: + - '9999' + x-ratelimit-remaining-tokens: + - '9999987' + x-ratelimit-reset-requests: + - 6ms + x-ratelimit-reset-tokens: + - 0s + x-request-id: + - req_4953e9919d74fabb02f89c587d38ea30 + http_version: HTTP/1.1 + status_code: 200 +- request: + body: '{"batch": [{"properties": {"class": "App", "version": "0.1.126", "language": + "python", "pid": 35168, "$lib": "posthog-python", "$lib_version": "3.9.3", "$geoip_disable": + true}, "timestamp": "2025-04-27T16:07:52.864741+00:00", "context": {}, "distinct_id": + "5303ea6e-a423-419e-a71c-3a0f0eaaaa16", "event": "query"}], "historical_migration": + false, "sentAt": "2025-04-27T16:07:56.879642+00:00", "api_key": "phc_PHQDA5KwztijnSojsxJ2c1DuJd52QCzJzT2xnSGvjN2"}' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate, zstd + Connection: + - keep-alive + Content-Length: + - '454' + Content-Type: + - application/json + User-Agent: + - posthog-python/3.9.3 + method: POST + uri: https://us.i.posthog.com/batch/ + response: + body: + string: '{"status":"Ok"}' + headers: + Connection: + - keep-alive + Content-Length: + - '15' + Content-Type: + - application/json + Date: + - Sun, 27 Apr 2025 16:07:57 GMT + access-control-allow-credentials: + - 'true' + server: + - envoy + vary: + - origin, access-control-request-method, access-control-request-headers + x-envoy-upstream-service-time: + - '52' + status: + code: 200 + message: OK +- request: + body: '{"input": ["test CSV"], "model": "text-embedding-ada-002", "encoding_format": + "base64"}' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate, zstd + connection: + - keep-alive + content-length: + - '87' + content-type: + - application/json + cookie: + - __cf_bm=8J2Cz0gyk5BpRSYbzjWETqMiyphlW8TAe7802MlHMe0-1745770077-1.0.1.1-qbyKIgJQJDS2wWDKC1x0RrxzJk5mcE4wDunq25j.sNSe_EMvVEIQTJR6t4Jmrknve3lSxXTsW4VL_3EYYk6ehiq6yIQDVzVkqNfU_xlwris; + _cfuvid=r11uIiVDOWdkeX_FkCjMMnH.Z9zcvp4ptrKi3luIa9s-1745770077165-0.0.1.1-604800000 + host: + - api.openai.com + user-agent: + - OpenAI/Python 1.66.3 + x-stainless-arch: + - arm64 + x-stainless-async: + - 'false' + x-stainless-lang: + - python + x-stainless-os: + - MacOS + x-stainless-package-version: + - 1.66.3 + x-stainless-read-timeout: + - '600' + x-stainless-retry-count: + - '0' + x-stainless-runtime: + - CPython + x-stainless-runtime-version: + - 3.12.9 + method: POST + uri: https://api.openai.com/v1/embeddings + response: + content: "{\n \"object\": \"list\",\n \"data\": [\n {\n \"object\": + \"embedding\",\n \"index\": 0,\n \"embedding\": \"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\"\n + \ }\n ],\n \"model\": \"text-embedding-ada-002-v2\",\n \"usage\": {\n \"prompt_tokens\": + 2,\n \"total_tokens\": 2\n }\n}\n" + headers: + CF-RAY: + - 936f93666e9d7e01-GRU + Connection: + - keep-alive + Content-Encoding: + - gzip + Content-Type: + - application/json + Date: + - Sun, 27 Apr 2025 16:07:57 GMT + Server: + - cloudflare + Transfer-Encoding: + - chunked + X-Content-Type-Options: + - nosniff + access-control-allow-origin: + - '*' + access-control-expose-headers: + - X-Request-ID + alt-svc: + - h3=":443"; ma=86400 + cf-cache-status: + - DYNAMIC + openai-model: + - text-embedding-ada-002-v2 + openai-organization: + - crewai-iuxna1 + openai-processing-ms: + - '59' + openai-version: + - '2020-10-01' + strict-transport-security: + - max-age=31536000; includeSubDomains; preload + via: + - envoy-router-787f678775-4x2zc + x-envoy-upstream-service-time: + - '39' + x-ratelimit-limit-requests: + - '10000' + x-ratelimit-limit-tokens: + - '10000000' + x-ratelimit-remaining-requests: + - '9999' + x-ratelimit-remaining-tokens: + - '9999998' + x-ratelimit-reset-requests: + - 6ms + x-ratelimit-reset-tokens: + - 0s + x-request-id: + - req_9bb18ff3e7da9b6d8406f6361cbf5497 + http_version: HTTP/1.1 + status_code: 200 +version: 1 diff --git a/tests/tools/cassettes/test_search_tools/test_directory_search_tool.yaml b/tests/tools/cassettes/test_search_tools/test_directory_search_tool.yaml new file mode 100644 index 000000000..6f3fd2d58 --- /dev/null +++ b/tests/tools/cassettes/test_search_tools/test_directory_search_tool.yaml @@ -0,0 +1,544 @@ +interactions: +- request: + body: '{"input": ["This is a test file for directory search"], "model": "text-embedding-ada-002", + "encoding_format": "base64"}' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate, zstd + connection: + - keep-alive + content-length: + - '119' + content-type: + - application/json + host: + - api.openai.com + user-agent: + - OpenAI/Python 1.66.3 + x-stainless-arch: + - arm64 + x-stainless-async: + - 'false' + x-stainless-lang: + - python + x-stainless-os: + - MacOS + x-stainless-package-version: + - 1.66.3 + x-stainless-read-timeout: + - '600' + x-stainless-retry-count: + - '0' + x-stainless-runtime: + - CPython + x-stainless-runtime-version: + - 3.12.9 + method: POST + uri: https://api.openai.com/v1/embeddings + response: + content: "{\n \"object\": \"list\",\n \"data\": [\n {\n \"object\": + \"embedding\",\n \"index\": 0,\n \"embedding\": \"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\"\n + \ }\n ],\n \"model\": \"text-embedding-ada-002-v2\",\n \"usage\": {\n \"prompt_tokens\": + 8,\n \"total_tokens\": 8\n }\n}\n" + headers: + CF-RAY: + - 936f92b30c267df3-GRU + Connection: + - keep-alive + Content-Encoding: + - gzip + Content-Type: + - application/json + Date: + - Sun, 27 Apr 2025 16:07:28 GMT + Server: + - cloudflare + Set-Cookie: + - __cf_bm=nFZbdbFah.LWbzeW.glnLPLT8LbiE2gQXhAnfko3dOM-1745770048-1.0.1.1-6X7_GmSlrhT2JDG3UI.GdG197sz4YerSq59cGRFhchAip2X4Az27dMYcavJW.noLsarkBrxKgf7B5SZg7354p8ZOH9VBHq35KlZ6QavVyJ8; + path=/; expires=Sun, 27-Apr-25 16:37:28 GMT; domain=.api.openai.com; HttpOnly; + Secure; SameSite=None + - _cfuvid=.vAWcVjI11dzJOYj038IwLPbCQXQ1.tBpWmDu6Xt46k-1745770048727-0.0.1.1-604800000; + path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None + Transfer-Encoding: + - chunked + X-Content-Type-Options: + - nosniff + access-control-allow-origin: + - '*' + access-control-expose-headers: + - X-Request-ID + alt-svc: + - h3=":443"; ma=86400 + cf-cache-status: + - DYNAMIC + openai-model: + - text-embedding-ada-002-v2 + openai-organization: + - crewai-iuxna1 + openai-processing-ms: + - '78' + openai-version: + - '2020-10-01' + strict-transport-security: + - max-age=31536000; includeSubDomains; preload + via: + - envoy-router-97cfd68d4-7qqkm + x-envoy-upstream-service-time: + - '51' + x-ratelimit-limit-requests: + - '10000' + x-ratelimit-limit-tokens: + - '10000000' + x-ratelimit-remaining-requests: + - '9999' + x-ratelimit-remaining-tokens: + - '9999989' + x-ratelimit-reset-requests: + - 6ms + x-ratelimit-reset-tokens: + - 0s + x-request-id: + - req_b2ab62724f2840722a52cfed5dd64580 + http_version: HTTP/1.1 + status_code: 200 +- request: + body: '{"batch": [{"properties": {"class": "App", "version": "0.1.126", "language": + "python", "pid": 35099, "$lib": "posthog-python", "$lib_version": "3.9.3", "$geoip_disable": + true}, "timestamp": "2025-04-27T16:07:28.073953+00:00", "context": {}, "distinct_id": + "5303ea6e-a423-419e-a71c-3a0f0eaaaa16", "event": "init"}], "historical_migration": + false, "sentAt": "2025-04-27T16:07:28.576735+00:00", "api_key": "phc_PHQDA5KwztijnSojsxJ2c1DuJd52QCzJzT2xnSGvjN2"}' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate, zstd + Connection: + - keep-alive + Content-Length: + - '453' + Content-Type: + - application/json + User-Agent: + - posthog-python/3.9.3 + method: POST + uri: https://us.i.posthog.com/batch/ + response: + body: + string: '{"status":"Ok"}' + headers: + Connection: + - keep-alive + Content-Length: + - '15' + Content-Type: + - application/json + Date: + - Sun, 27 Apr 2025 16:07:29 GMT + access-control-allow-credentials: + - 'true' + server: + - envoy + vary: + - origin, access-control-request-method, access-control-request-headers + x-envoy-upstream-service-time: + - '37' + status: + code: 200 + message: OK +- request: + body: '{"batch": [{"properties": {"class": "App", "version": "0.1.126", "language": + "python", "pid": 35099, "$lib": "posthog-python", "$lib_version": "3.9.3", "$geoip_disable": + true}, "timestamp": "2025-04-27T16:07:28.073953+00:00", "context": {}, "distinct_id": + "5303ea6e-a423-419e-a71c-3a0f0eaaaa16", "event": "init"}], "historical_migration": + false, "sentAt": "2025-04-27T16:07:29.624095+00:00", "api_key": "phc_PHQDA5KwztijnSojsxJ2c1DuJd52QCzJzT2xnSGvjN2"}' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate, zstd + Connection: + - keep-alive + Content-Length: + - '453' + Content-Type: + - application/json + User-Agent: + - posthog-python/3.9.3 + method: POST + uri: https://us.i.posthog.com/batch/ + response: + body: + string: '{"status":"Ok"}' + headers: + Connection: + - keep-alive + Content-Length: + - '15' + Content-Type: + - application/json + Date: + - Sun, 27 Apr 2025 16:07:30 GMT + access-control-allow-credentials: + - 'true' + server: + - envoy + vary: + - origin, access-control-request-method, access-control-request-headers + x-envoy-upstream-service-time: + - '28' + status: + code: 200 + message: OK +- request: + body: '{"batch": [{"properties": {"class": "App", "version": "0.1.126", "language": + "python", "pid": 35099, "$lib": "posthog-python", "$lib_version": "3.9.3", "$geoip_disable": + true}, "timestamp": "2025-04-27T16:07:28.073953+00:00", "context": {}, "distinct_id": + "5303ea6e-a423-419e-a71c-3a0f0eaaaa16", "event": "init"}], "historical_migration": + false, "sentAt": "2025-04-27T16:07:30.646962+00:00", "api_key": "phc_PHQDA5KwztijnSojsxJ2c1DuJd52QCzJzT2xnSGvjN2"}' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate, zstd + Connection: + - keep-alive + Content-Length: + - '453' + Content-Type: + - application/json + User-Agent: + - posthog-python/3.9.3 + method: POST + uri: https://us.i.posthog.com/batch/ + response: + body: + string: '{"status":"Ok"}' + headers: + Connection: + - keep-alive + Content-Length: + - '15' + Content-Type: + - application/json + Date: + - Sun, 27 Apr 2025 16:07:31 GMT + access-control-allow-credentials: + - 'true' + server: + - envoy + vary: + - origin, access-control-request-method, access-control-request-headers + x-envoy-upstream-service-time: + - '28' + status: + code: 200 + message: OK +- request: + body: '{"input": ["test file"], "model": "text-embedding-ada-002", "encoding_format": + "base64"}' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate, zstd + connection: + - keep-alive + content-length: + - '88' + content-type: + - application/json + cookie: + - __cf_bm=nFZbdbFah.LWbzeW.glnLPLT8LbiE2gQXhAnfko3dOM-1745770048-1.0.1.1-6X7_GmSlrhT2JDG3UI.GdG197sz4YerSq59cGRFhchAip2X4Az27dMYcavJW.noLsarkBrxKgf7B5SZg7354p8ZOH9VBHq35KlZ6QavVyJ8; + _cfuvid=.vAWcVjI11dzJOYj038IwLPbCQXQ1.tBpWmDu6Xt46k-1745770048727-0.0.1.1-604800000 + host: + - api.openai.com + user-agent: + - OpenAI/Python 1.66.3 + x-stainless-arch: + - arm64 + x-stainless-async: + - 'false' + x-stainless-lang: + - python + x-stainless-os: + - MacOS + x-stainless-package-version: + - 1.66.3 + x-stainless-read-timeout: + - '600' + x-stainless-retry-count: + - '0' + x-stainless-runtime: + - CPython + x-stainless-runtime-version: + - 3.12.9 + method: POST + uri: https://api.openai.com/v1/embeddings + response: + content: "{\n \"object\": \"list\",\n \"data\": [\n {\n \"object\": + \"embedding\",\n \"index\": 0,\n \"embedding\": \"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\"\n + \ }\n ],\n \"model\": \"text-embedding-ada-002-v2\",\n \"usage\": {\n \"prompt_tokens\": + 2,\n \"total_tokens\": 2\n }\n}\n" + headers: + CF-RAY: + - 936f92b4cd887df3-GRU + Connection: + - keep-alive + Content-Encoding: + - gzip + Content-Type: + - application/json + Date: + - Sun, 27 Apr 2025 16:07:33 GMT + Server: + - cloudflare + Transfer-Encoding: + - chunked + X-Content-Type-Options: + - nosniff + access-control-allow-origin: + - '*' + access-control-expose-headers: + - X-Request-ID + alt-svc: + - h3=":443"; ma=86400 + cf-cache-status: + - DYNAMIC + openai-model: + - text-embedding-ada-002-v2 + openai-organization: + - crewai-iuxna1 + openai-processing-ms: + - '162' + openai-version: + - '2020-10-01' + strict-transport-security: + - max-age=31536000; includeSubDomains; preload + via: + - envoy-router-7bbfccd4b9-5rlz8 + x-envoy-upstream-service-time: + - '98' + x-ratelimit-limit-requests: + - '10000' + x-ratelimit-limit-tokens: + - '10000000' + x-ratelimit-remaining-requests: + - '9999' + x-ratelimit-remaining-tokens: + - '9999998' + x-ratelimit-reset-requests: + - 6ms + x-ratelimit-reset-tokens: + - 0s + x-request-id: + - req_5414bfd96cbd16d84a01f68e994a38f2 + http_version: HTTP/1.1 + status_code: 200 +- request: + body: '{"input": ["This is a test file for directory search"], "model": "text-embedding-ada-002", + "encoding_format": "base64"}' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate, zstd + connection: + - keep-alive + content-length: + - '119' + content-type: + - application/json + host: + - api.openai.com + user-agent: + - OpenAI/Python 1.66.3 + x-stainless-arch: + - arm64 + x-stainless-async: + - 'false' + x-stainless-lang: + - python + x-stainless-os: + - MacOS + x-stainless-package-version: + - 1.66.3 + x-stainless-read-timeout: + - '600' + x-stainless-retry-count: + - '0' + x-stainless-runtime: + - CPython + x-stainless-runtime-version: + - 3.12.9 + method: POST + uri: https://api.openai.com/v1/embeddings + response: + content: "{\n \"object\": \"list\",\n \"data\": [\n {\n \"object\": + \"embedding\",\n \"index\": 0,\n \"embedding\": \"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\"\n + \ }\n ],\n \"model\": \"text-embedding-ada-002-v2\",\n \"usage\": {\n \"prompt_tokens\": + 8,\n \"total_tokens\": 8\n }\n}\n" + headers: + CF-RAY: + - 936f9336cc417e0a-GRU + Connection: + - keep-alive + Content-Encoding: + - gzip + Content-Type: + - application/json + Date: + - Sun, 27 Apr 2025 16:07:49 GMT + Server: + - cloudflare + Set-Cookie: + - __cf_bm=EO3qaPuy2laM3xDGRwHtVhJMUVrBq0C4x5BxYYC8dT0-1745770069-1.0.1.1-kOylsOMvWlUF5owqqiIUziYDoC1f8vVA4C7C9em_s1Gdawqe_C0R5yIfCxJzf9.q9LZJQyCGp8L2rJaFzDF0Nk2pkv2v.tT.uQTRlmCgzwY; + path=/; expires=Sun, 27-Apr-25 16:37:49 GMT; domain=.api.openai.com; HttpOnly; + Secure; SameSite=None + - _cfuvid=52fi4.4bJilzZrvgAS3YttTnBjtEe8pVmM0VbBM5jis-1745770069782-0.0.1.1-604800000; + path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None + Transfer-Encoding: + - chunked + X-Content-Type-Options: + - nosniff + access-control-allow-origin: + - '*' + access-control-expose-headers: + - X-Request-ID + alt-svc: + - h3=":443"; ma=86400 + cf-cache-status: + - DYNAMIC + openai-model: + - text-embedding-ada-002-v2 + openai-organization: + - crewai-iuxna1 + openai-processing-ms: + - '39' + openai-version: + - '2020-10-01' + strict-transport-security: + - max-age=31536000; includeSubDomains; preload + via: + - envoy-router-97cfd68d4-nw6rt + x-envoy-upstream-service-time: + - '28' + x-ratelimit-limit-requests: + - '10000' + x-ratelimit-limit-tokens: + - '10000000' + x-ratelimit-remaining-requests: + - '9999' + x-ratelimit-remaining-tokens: + - '9999989' + x-ratelimit-reset-requests: + - 6ms + x-ratelimit-reset-tokens: + - 0s + x-request-id: + - req_f9ca57dbb69b376529e9c874f44dba39 + http_version: HTTP/1.1 + status_code: 200 +- request: + body: '{"input": ["test file"], "model": "text-embedding-ada-002", "encoding_format": + "base64"}' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate, zstd + connection: + - keep-alive + content-length: + - '88' + content-type: + - application/json + cookie: + - __cf_bm=EO3qaPuy2laM3xDGRwHtVhJMUVrBq0C4x5BxYYC8dT0-1745770069-1.0.1.1-kOylsOMvWlUF5owqqiIUziYDoC1f8vVA4C7C9em_s1Gdawqe_C0R5yIfCxJzf9.q9LZJQyCGp8L2rJaFzDF0Nk2pkv2v.tT.uQTRlmCgzwY; + _cfuvid=52fi4.4bJilzZrvgAS3YttTnBjtEe8pVmM0VbBM5jis-1745770069782-0.0.1.1-604800000 + host: + - api.openai.com + user-agent: + - OpenAI/Python 1.66.3 + x-stainless-arch: + - arm64 + x-stainless-async: + - 'false' + x-stainless-lang: + - python + x-stainless-os: + - MacOS + x-stainless-package-version: + - 1.66.3 + x-stainless-read-timeout: + - '600' + x-stainless-retry-count: + - '0' + x-stainless-runtime: + - CPython + x-stainless-runtime-version: + - 3.12.9 + method: POST + uri: https://api.openai.com/v1/embeddings + response: + content: "{\n \"object\": \"list\",\n \"data\": [\n {\n \"object\": + \"embedding\",\n \"index\": 0,\n \"embedding\": \"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\"\n + \ }\n ],\n \"model\": \"text-embedding-ada-002-v2\",\n \"usage\": {\n \"prompt_tokens\": + 2,\n \"total_tokens\": 2\n }\n}\n" + headers: + CF-RAY: + - 936f93388d697e0a-GRU + Connection: + - keep-alive + Content-Encoding: + - gzip + Content-Type: + - application/json + Date: + - Sun, 27 Apr 2025 16:07:50 GMT + Server: + - cloudflare + Transfer-Encoding: + - chunked + X-Content-Type-Options: + - nosniff + access-control-allow-origin: + - '*' + access-control-expose-headers: + - X-Request-ID + alt-svc: + - h3=":443"; ma=86400 + cf-cache-status: + - DYNAMIC + openai-model: + - text-embedding-ada-002-v2 + openai-organization: + - crewai-iuxna1 + openai-processing-ms: + - '132' + openai-version: + - '2020-10-01' + strict-transport-security: + - max-age=31536000; includeSubDomains; preload + via: + - envoy-router-75c844b786-xxzqk + x-envoy-upstream-service-time: + - '61' + x-ratelimit-limit-requests: + - '10000' + x-ratelimit-limit-tokens: + - '10000000' + x-ratelimit-remaining-requests: + - '9999' + x-ratelimit-remaining-tokens: + - '9999998' + x-ratelimit-reset-requests: + - 6ms + x-ratelimit-reset-tokens: + - 0s + x-request-id: + - req_5d278e154a0358a46c53ec740679883c + http_version: HTTP/1.1 + status_code: 200 +version: 1 diff --git a/tests/tools/cassettes/test_search_tools/test_json_search_tool.yaml b/tests/tools/cassettes/test_search_tools/test_json_search_tool.yaml new file mode 100644 index 000000000..2e509ef4a --- /dev/null +++ b/tests/tools/cassettes/test_search_tools/test_json_search_tool.yaml @@ -0,0 +1,300 @@ +interactions: +- request: + body: '{"input": ["\"test\": \"This is a test JSON file\""], "model": "text-embedding-ada-002", + "encoding_format": "base64"}' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate, zstd + connection: + - keep-alive + content-length: + - '117' + content-type: + - application/json + host: + - api.openai.com + user-agent: + - OpenAI/Python 1.66.3 + x-stainless-arch: + - arm64 + x-stainless-async: + - 'false' + x-stainless-lang: + - python + x-stainless-os: + - MacOS + x-stainless-package-version: + - 1.66.3 + x-stainless-read-timeout: + - '600' + x-stainless-retry-count: + - '0' + x-stainless-runtime: + - CPython + x-stainless-runtime-version: + - 3.12.9 + method: POST + uri: https://api.openai.com/v1/embeddings + response: + content: "{\n \"object\": \"list\",\n \"data\": [\n {\n \"object\": + \"embedding\",\n \"index\": 0,\n \"embedding\": \"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\"\n + \ }\n ],\n \"model\": \"text-embedding-ada-002-v2\",\n \"usage\": {\n \"prompt_tokens\": + 11,\n \"total_tokens\": 11\n }\n}\n" + headers: + CF-RAY: + - 936f93430d8e7df5-GRU + Connection: + - keep-alive + Content-Encoding: + - gzip + Content-Type: + - application/json + Date: + - Sun, 27 Apr 2025 16:07:51 GMT + Server: + - cloudflare + Set-Cookie: + - __cf_bm=ZWmwH7qy_Do2gMLxa1sSsrEo.85HbT2vglvD1Dwg1Zs-1745770071-1.0.1.1-WFNWy52G66A4oGmHOWFAlhnFBFbZJ31LnUNvi7bwKg2R2anwH7wnxAc.zA9GMIYExcRah3uIl5KRt723DyGt5EZ60XcQksxVd2co80t2i.g; + path=/; expires=Sun, 27-Apr-25 16:37:51 GMT; domain=.api.openai.com; HttpOnly; + Secure; SameSite=None + - _cfuvid=u7YNKY8LlLPo_cstP53bpHP1eV7pP._t2QByCJYNkyk-1745770071796-0.0.1.1-604800000; + path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None + Transfer-Encoding: + - chunked + X-Content-Type-Options: + - nosniff + access-control-allow-origin: + - '*' + access-control-expose-headers: + - X-Request-ID + alt-svc: + - h3=":443"; ma=86400 + cf-cache-status: + - DYNAMIC + openai-model: + - text-embedding-ada-002-v2 + openai-organization: + - crewai-iuxna1 + openai-processing-ms: + - '93' + openai-version: + - '2020-10-01' + strict-transport-security: + - max-age=31536000; includeSubDomains; preload + via: + - envoy-router-5f4895bd76-796jv + x-envoy-upstream-service-time: + - '61' + x-ratelimit-limit-requests: + - '10000' + x-ratelimit-limit-tokens: + - '10000000' + x-ratelimit-remaining-requests: + - '9999' + x-ratelimit-remaining-tokens: + - '9999991' + x-ratelimit-reset-requests: + - 6ms + x-ratelimit-reset-tokens: + - 0s + x-request-id: + - req_69bfa1db5b89ca60293896c5f37d0d8f + http_version: HTTP/1.1 + status_code: 200 +- request: + body: '{"batch": [{"properties": {"class": "App", "version": "0.1.126", "language": + "python", "pid": 35168, "$lib": "posthog-python", "$lib_version": "3.9.3", "$geoip_disable": + true}, "timestamp": "2025-04-27T16:07:50.287520+00:00", "context": {}, "distinct_id": + "5303ea6e-a423-419e-a71c-3a0f0eaaaa16", "event": "init"}], "historical_migration": + false, "sentAt": "2025-04-27T16:07:51.445161+00:00", "api_key": "phc_PHQDA5KwztijnSojsxJ2c1DuJd52QCzJzT2xnSGvjN2"}' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate, zstd + Connection: + - keep-alive + Content-Length: + - '453' + Content-Type: + - application/json + User-Agent: + - posthog-python/3.9.3 + method: POST + uri: https://us.i.posthog.com/batch/ + response: + body: + string: '{"status":"Ok"}' + headers: + Connection: + - keep-alive + Content-Length: + - '15' + Content-Type: + - application/json + Date: + - Sun, 27 Apr 2025 16:07:52 GMT + access-control-allow-credentials: + - 'true' + server: + - envoy + vary: + - origin, access-control-request-method, access-control-request-headers + x-envoy-upstream-service-time: + - '44' + status: + code: 200 + message: OK +- request: + body: '{"batch": [{"properties": {"class": "App", "version": "0.1.126", "language": + "python", "pid": 35168, "$lib": "posthog-python", "$lib_version": "3.9.3", "$geoip_disable": + true}, "timestamp": "2025-04-27T16:07:51.347055+00:00", "context": {}, "distinct_id": + "5303ea6e-a423-419e-a71c-3a0f0eaaaa16", "event": "init"}, {"properties": {"class": + "App", "version": "0.1.126", "language": "python", "pid": 35168, "$lib": "posthog-python", + "$lib_version": "3.9.3", "$geoip_disable": true, "data_type": "json", "word_count": + 7, "chunks_count": 1}, "timestamp": "2025-04-27T16:07:51.676881+00:00", "context": + {}, "distinct_id": "5303ea6e-a423-419e-a71c-3a0f0eaaaa16", "event": "add"}], + "historical_migration": false, "sentAt": "2025-04-27T16:07:51.852107+00:00", + "api_key": "phc_PHQDA5KwztijnSojsxJ2c1DuJd52QCzJzT2xnSGvjN2"}' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate, zstd + Connection: + - keep-alive + Content-Length: + - '812' + Content-Type: + - application/json + User-Agent: + - posthog-python/3.9.3 + method: POST + uri: https://us.i.posthog.com/batch/ + response: + body: + string: '{"status":"Ok"}' + headers: + Connection: + - keep-alive + Content-Length: + - '15' + Content-Type: + - application/json + Date: + - Sun, 27 Apr 2025 16:07:52 GMT + access-control-allow-credentials: + - 'true' + server: + - envoy + vary: + - origin, access-control-request-method, access-control-request-headers + x-envoy-upstream-service-time: + - '24' + status: + code: 200 + message: OK +- request: + body: '{"input": ["test JSON"], "model": "text-embedding-ada-002", "encoding_format": + "base64"}' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate, zstd + connection: + - keep-alive + content-length: + - '88' + content-type: + - application/json + cookie: + - __cf_bm=ZWmwH7qy_Do2gMLxa1sSsrEo.85HbT2vglvD1Dwg1Zs-1745770071-1.0.1.1-WFNWy52G66A4oGmHOWFAlhnFBFbZJ31LnUNvi7bwKg2R2anwH7wnxAc.zA9GMIYExcRah3uIl5KRt723DyGt5EZ60XcQksxVd2co80t2i.g; + _cfuvid=u7YNKY8LlLPo_cstP53bpHP1eV7pP._t2QByCJYNkyk-1745770071796-0.0.1.1-604800000 + host: + - api.openai.com + user-agent: + - OpenAI/Python 1.66.3 + x-stainless-arch: + - arm64 + x-stainless-async: + - 'false' + x-stainless-lang: + - python + x-stainless-os: + - MacOS + x-stainless-package-version: + - 1.66.3 + x-stainless-read-timeout: + - '600' + x-stainless-retry-count: + - '0' + x-stainless-runtime: + - CPython + x-stainless-runtime-version: + - 3.12.9 + method: POST + uri: https://api.openai.com/v1/embeddings + response: + content: "{\n \"object\": \"list\",\n \"data\": [\n {\n \"object\": + \"embedding\",\n \"index\": 0,\n \"embedding\": \"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\"\n + \ }\n ],\n \"model\": \"text-embedding-ada-002-v2\",\n \"usage\": {\n \"prompt_tokens\": + 2,\n \"total_tokens\": 2\n }\n}\n" + headers: + CF-RAY: + - 936f9344eed57df5-GRU + Connection: + - keep-alive + Content-Encoding: + - gzip + Content-Type: + - application/json + Date: + - Sun, 27 Apr 2025 16:07:52 GMT + Server: + - cloudflare + Transfer-Encoding: + - chunked + X-Content-Type-Options: + - nosniff + access-control-allow-origin: + - '*' + access-control-expose-headers: + - X-Request-ID + alt-svc: + - h3=":443"; ma=86400 + cf-cache-status: + - DYNAMIC + openai-model: + - text-embedding-ada-002-v2 + openai-organization: + - crewai-iuxna1 + openai-processing-ms: + - '196' + openai-version: + - '2020-10-01' + strict-transport-security: + - max-age=31536000; includeSubDomains; preload + via: + - envoy-router-5b456cc969-csbxc + x-envoy-upstream-service-time: + - '104' + x-ratelimit-limit-requests: + - '10000' + x-ratelimit-limit-tokens: + - '10000000' + x-ratelimit-remaining-requests: + - '9999' + x-ratelimit-remaining-tokens: + - '9999998' + x-ratelimit-reset-requests: + - 6ms + x-ratelimit-reset-tokens: + - 0s + x-request-id: + - req_e2376f4641b02bc9b096c5b33fd91f9d + http_version: HTTP/1.1 + status_code: 200 +version: 1 diff --git a/tests/tools/cassettes/test_search_tools/test_mdx_search_tool.yaml b/tests/tools/cassettes/test_search_tools/test_mdx_search_tool.yaml new file mode 100644 index 000000000..914ee947f --- /dev/null +++ b/tests/tools/cassettes/test_search_tools/test_mdx_search_tool.yaml @@ -0,0 +1,255 @@ +interactions: +- request: + body: '{"input": ["# Test MDX This is a test MDX file"], "model": "text-embedding-ada-002", + "encoding_format": "base64"}' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate, zstd + connection: + - keep-alive + content-length: + - '113' + content-type: + - application/json + host: + - api.openai.com + user-agent: + - OpenAI/Python 1.66.3 + x-stainless-arch: + - arm64 + x-stainless-async: + - 'false' + x-stainless-lang: + - python + x-stainless-os: + - MacOS + x-stainless-package-version: + - 1.66.3 + x-stainless-read-timeout: + - '600' + x-stainless-retry-count: + - '0' + x-stainless-runtime: + - CPython + x-stainless-runtime-version: + - 3.12.9 + method: POST + uri: https://api.openai.com/v1/embeddings + response: + content: "{\n \"object\": \"list\",\n \"data\": [\n {\n \"object\": + \"embedding\",\n \"index\": 0,\n \"embedding\": \"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\"\n + \ }\n ],\n \"model\": \"text-embedding-ada-002-v2\",\n \"usage\": {\n \"prompt_tokens\": + 11,\n \"total_tokens\": 11\n }\n}\n" + headers: + CF-RAY: + - 936f936a5c107e1e-GRU + Connection: + - keep-alive + Content-Encoding: + - gzip + Content-Type: + - application/json + Date: + - Sun, 27 Apr 2025 16:07:58 GMT + Server: + - cloudflare + Set-Cookie: + - __cf_bm=utmyQFzT_wcpHa9.mgJCTUKMEwjaKO1KUN4w4FESXA8-1745770078-1.0.1.1-c__HC5oqY30dc8uUgateYwWXyd5rkkLT_sv7FaglerEzNk2yyURMruWVkA12xyL7Frj5cXci33jdwdr8.yO6MRk_jssq5iAvJP3Aq.SVfyE; + path=/; expires=Sun, 27-Apr-25 16:37:58 GMT; domain=.api.openai.com; HttpOnly; + Secure; SameSite=None + - _cfuvid=mD0miBgD.AKaxJ3xiOsfjrDskazLjoHBA1QrHecqrX0-1745770078037-0.0.1.1-604800000; + path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None + Transfer-Encoding: + - chunked + X-Content-Type-Options: + - nosniff + access-control-allow-origin: + - '*' + access-control-expose-headers: + - X-Request-ID + alt-svc: + - h3=":443"; ma=86400 + cf-cache-status: + - DYNAMIC + openai-model: + - text-embedding-ada-002-v2 + openai-organization: + - crewai-iuxna1 + openai-processing-ms: + - '61' + openai-version: + - '2020-10-01' + strict-transport-security: + - max-age=31536000; includeSubDomains; preload + via: + - envoy-router-5f4895bd76-h5k2k + x-envoy-upstream-service-time: + - '41' + x-ratelimit-limit-requests: + - '10000' + x-ratelimit-limit-tokens: + - '10000000' + x-ratelimit-remaining-requests: + - '9999' + x-ratelimit-remaining-tokens: + - '9999991' + x-ratelimit-reset-requests: + - 6ms + x-ratelimit-reset-tokens: + - 0s + x-request-id: + - req_3609b88991fc31a1bcb94c34547d9451 + http_version: HTTP/1.1 + status_code: 200 +- request: + body: '{"batch": [{"properties": {"class": "App", "version": "0.1.126", "language": + "python", "pid": 35168, "$lib": "posthog-python", "$lib_version": "3.9.3", "$geoip_disable": + true, "data_type": "csv", "word_count": 9, "chunks_count": 1}, "timestamp": + "2025-04-27T16:07:57.041070+00:00", "context": {}, "distinct_id": "5303ea6e-a423-419e-a71c-3a0f0eaaaa16", + "event": "add"}, {"properties": {"class": "App", "version": "0.1.126", "language": + "python", "pid": 35168, "$lib": "posthog-python", "$lib_version": "3.9.3", "$geoip_disable": + true}, "timestamp": "2025-04-27T16:07:57.605978+00:00", "context": {}, "distinct_id": + "5303ea6e-a423-419e-a71c-3a0f0eaaaa16", "event": "query"}], "historical_migration": + false, "sentAt": "2025-04-27T16:07:57.928462+00:00", "api_key": "phc_PHQDA5KwztijnSojsxJ2c1DuJd52QCzJzT2xnSGvjN2"}' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate, zstd + Connection: + - keep-alive + Content-Length: + - '812' + Content-Type: + - application/json + User-Agent: + - posthog-python/3.9.3 + method: POST + uri: https://us.i.posthog.com/batch/ + response: + body: + string: '{"status":"Ok"}' + headers: + Connection: + - keep-alive + Content-Length: + - '15' + Content-Type: + - application/json + Date: + - Sun, 27 Apr 2025 16:07:58 GMT + access-control-allow-credentials: + - 'true' + server: + - envoy + vary: + - origin, access-control-request-method, access-control-request-headers + x-envoy-upstream-service-time: + - '39' + status: + code: 200 + message: OK +- request: + body: '{"input": ["test MDX"], "model": "text-embedding-ada-002", "encoding_format": + "base64"}' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate, zstd + connection: + - keep-alive + content-length: + - '87' + content-type: + - application/json + cookie: + - __cf_bm=utmyQFzT_wcpHa9.mgJCTUKMEwjaKO1KUN4w4FESXA8-1745770078-1.0.1.1-c__HC5oqY30dc8uUgateYwWXyd5rkkLT_sv7FaglerEzNk2yyURMruWVkA12xyL7Frj5cXci33jdwdr8.yO6MRk_jssq5iAvJP3Aq.SVfyE; + _cfuvid=mD0miBgD.AKaxJ3xiOsfjrDskazLjoHBA1QrHecqrX0-1745770078037-0.0.1.1-604800000 + host: + - api.openai.com + user-agent: + - OpenAI/Python 1.66.3 + x-stainless-arch: + - arm64 + x-stainless-async: + - 'false' + x-stainless-lang: + - python + x-stainless-os: + - MacOS + x-stainless-package-version: + - 1.66.3 + x-stainless-read-timeout: + - '600' + x-stainless-retry-count: + - '0' + x-stainless-runtime: + - CPython + x-stainless-runtime-version: + - 3.12.9 + method: POST + uri: https://api.openai.com/v1/embeddings + response: + content: "{\n \"object\": \"list\",\n \"data\": [\n {\n \"object\": + \"embedding\",\n \"index\": 0,\n \"embedding\": \"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\"\n + \ }\n ],\n \"model\": \"text-embedding-ada-002-v2\",\n \"usage\": {\n \"prompt_tokens\": + 3,\n \"total_tokens\": 3\n }\n}\n" + headers: + CF-RAY: + - 936f936becf67e1e-GRU + Connection: + - keep-alive + Content-Encoding: + - gzip + Content-Type: + - application/json + Date: + - Sun, 27 Apr 2025 16:07:59 GMT + Server: + - cloudflare + Transfer-Encoding: + - chunked + X-Content-Type-Options: + - nosniff + access-control-allow-origin: + - '*' + access-control-expose-headers: + - X-Request-ID + alt-svc: + - h3=":443"; ma=86400 + cf-cache-status: + - DYNAMIC + openai-model: + - text-embedding-ada-002-v2 + openai-organization: + - crewai-iuxna1 + openai-processing-ms: + - '191' + openai-version: + - '2020-10-01' + strict-transport-security: + - max-age=31536000; includeSubDomains; preload + via: + - envoy-router-59cfcdcc4b-wnwks + x-envoy-upstream-service-time: + - '90' + x-ratelimit-limit-requests: + - '10000' + x-ratelimit-limit-tokens: + - '10000000' + x-ratelimit-remaining-requests: + - '9999' + x-ratelimit-remaining-tokens: + - '9999998' + x-ratelimit-reset-requests: + - 6ms + x-ratelimit-reset-tokens: + - 0s + x-request-id: + - req_e29bb7e5a6b3af41f94939adce45f5f1 + http_version: HTTP/1.1 + status_code: 200 +version: 1 diff --git a/tests/tools/cassettes/test_search_tools/test_txt_search_tool.yaml b/tests/tools/cassettes/test_search_tools/test_txt_search_tool.yaml new file mode 100644 index 000000000..18d07c0b3 --- /dev/null +++ b/tests/tools/cassettes/test_search_tools/test_txt_search_tool.yaml @@ -0,0 +1,251 @@ +interactions: +- request: + body: '{"input": ["This is a test file for txt search"], "model": "text-embedding-ada-002", + "encoding_format": "base64"}' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate, zstd + connection: + - keep-alive + content-length: + - '113' + content-type: + - application/json + host: + - api.openai.com + user-agent: + - OpenAI/Python 1.66.3 + x-stainless-arch: + - arm64 + x-stainless-async: + - 'false' + x-stainless-lang: + - python + x-stainless-os: + - MacOS + x-stainless-package-version: + - 1.66.3 + x-stainless-read-timeout: + - '600' + x-stainless-retry-count: + - '0' + x-stainless-runtime: + - CPython + x-stainless-runtime-version: + - 3.12.9 + method: POST + uri: https://api.openai.com/v1/embeddings + response: + content: "{\n \"object\": \"list\",\n \"data\": [\n {\n \"object\": + \"embedding\",\n \"index\": 0,\n \"embedding\": \"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\"\n + \ }\n ],\n \"model\": \"text-embedding-ada-002-v2\",\n \"usage\": {\n \"prompt_tokens\": + 8,\n \"total_tokens\": 8\n }\n}\n" + headers: + CF-RAY: + - 936f933c6fce7e0a-GRU + Connection: + - keep-alive + Content-Encoding: + - gzip + Content-Type: + - application/json + Date: + - Sun, 27 Apr 2025 16:07:50 GMT + Server: + - cloudflare + Set-Cookie: + - __cf_bm=yAVgFNKcy2l7Cppym.kBBGNZMWvD0zYJbXBq3jJGg4A-1745770070-1.0.1.1-JvNpysiGohLJGBruqnedD94Y4r9AHPY_.gIefUGns48V4KkyaY5gC8yad0_SwaXeXArhpipuz5eQynAK2Rawe64.qrtUlri84024pQ0V8lE; + path=/; expires=Sun, 27-Apr-25 16:37:50 GMT; domain=.api.openai.com; HttpOnly; + Secure; SameSite=None + - _cfuvid=NOl2bW7B9MHsJt0XLs1fWk8BS4vWKLsCcHDInciUQBY-1745770070996-0.0.1.1-604800000; + path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None + Transfer-Encoding: + - chunked + X-Content-Type-Options: + - nosniff + access-control-allow-origin: + - '*' + access-control-expose-headers: + - X-Request-ID + alt-svc: + - h3=":443"; ma=86400 + cf-cache-status: + - DYNAMIC + openai-model: + - text-embedding-ada-002-v2 + openai-organization: + - crewai-iuxna1 + openai-processing-ms: + - '172' + openai-version: + - '2020-10-01' + strict-transport-security: + - max-age=31536000; includeSubDomains; preload + via: + - envoy-router-5f4895bd76-msnvl + x-envoy-upstream-service-time: + - '100' + x-ratelimit-limit-requests: + - '10000' + x-ratelimit-limit-tokens: + - '10000000' + x-ratelimit-remaining-requests: + - '9999' + x-ratelimit-remaining-tokens: + - '9999991' + x-ratelimit-reset-requests: + - 6ms + x-ratelimit-reset-tokens: + - 0s + x-request-id: + - req_20f7c5a3327d4060dbc7a61f4c5c4ba1 + http_version: HTTP/1.1 + status_code: 200 +- request: + body: '{"batch": [{"properties": {"class": "App", "version": "0.1.126", "language": + "python", "pid": 35168, "$lib": "posthog-python", "$lib_version": "3.9.3", "$geoip_disable": + true}, "timestamp": "2025-04-27T16:07:50.287520+00:00", "context": {}, "distinct_id": + "5303ea6e-a423-419e-a71c-3a0f0eaaaa16", "event": "init"}], "historical_migration": + false, "sentAt": "2025-04-27T16:07:50.792604+00:00", "api_key": "phc_PHQDA5KwztijnSojsxJ2c1DuJd52QCzJzT2xnSGvjN2"}' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate, zstd + Connection: + - keep-alive + Content-Length: + - '453' + Content-Type: + - application/json + User-Agent: + - posthog-python/3.9.3 + method: POST + uri: https://us.i.posthog.com/batch/ + response: + body: + string: '{"status":"Ok"}' + headers: + Connection: + - keep-alive + Content-Length: + - '15' + Content-Type: + - application/json + Date: + - Sun, 27 Apr 2025 16:07:51 GMT + access-control-allow-credentials: + - 'true' + server: + - envoy + vary: + - origin, access-control-request-method, access-control-request-headers + x-envoy-upstream-service-time: + - '46' + status: + code: 200 + message: OK +- request: + body: '{"input": ["test file"], "model": "text-embedding-ada-002", "encoding_format": + "base64"}' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate, zstd + connection: + - keep-alive + content-length: + - '88' + content-type: + - application/json + cookie: + - __cf_bm=yAVgFNKcy2l7Cppym.kBBGNZMWvD0zYJbXBq3jJGg4A-1745770070-1.0.1.1-JvNpysiGohLJGBruqnedD94Y4r9AHPY_.gIefUGns48V4KkyaY5gC8yad0_SwaXeXArhpipuz5eQynAK2Rawe64.qrtUlri84024pQ0V8lE; + _cfuvid=NOl2bW7B9MHsJt0XLs1fWk8BS4vWKLsCcHDInciUQBY-1745770070996-0.0.1.1-604800000 + host: + - api.openai.com + user-agent: + - OpenAI/Python 1.66.3 + x-stainless-arch: + - arm64 + x-stainless-async: + - 'false' + x-stainless-lang: + - python + x-stainless-os: + - MacOS + x-stainless-package-version: + - 1.66.3 + x-stainless-read-timeout: + - '600' + x-stainless-retry-count: + - '0' + x-stainless-runtime: + - CPython + x-stainless-runtime-version: + - 3.12.9 + method: POST + uri: https://api.openai.com/v1/embeddings + response: + content: "{\n \"object\": \"list\",\n \"data\": [\n {\n \"object\": + \"embedding\",\n \"index\": 0,\n \"embedding\": \"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\"\n + \ }\n ],\n \"model\": \"text-embedding-ada-002-v2\",\n \"usage\": {\n \"prompt_tokens\": + 2,\n \"total_tokens\": 2\n }\n}\n" + headers: + CF-RAY: + - 936f933fe9eb7e0a-GRU + Connection: + - keep-alive + Content-Encoding: + - gzip + Content-Type: + - application/json + Date: + - Sun, 27 Apr 2025 16:07:51 GMT + Server: + - cloudflare + Transfer-Encoding: + - chunked + X-Content-Type-Options: + - nosniff + access-control-allow-origin: + - '*' + access-control-expose-headers: + - X-Request-ID + alt-svc: + - h3=":443"; ma=86400 + cf-cache-status: + - DYNAMIC + openai-model: + - text-embedding-ada-002-v2 + openai-organization: + - crewai-iuxna1 + openai-processing-ms: + - '179' + openai-version: + - '2020-10-01' + strict-transport-security: + - max-age=31536000; includeSubDomains; preload + via: + - envoy-router-7bbfccd4b9-p6rt4 + x-envoy-upstream-service-time: + - '105' + x-ratelimit-limit-requests: + - '10000' + x-ratelimit-limit-tokens: + - '10000000' + x-ratelimit-remaining-requests: + - '9999' + x-ratelimit-remaining-tokens: + - '9999998' + x-ratelimit-reset-requests: + - 6ms + x-ratelimit-reset-tokens: + - 0s + x-request-id: + - req_b1ab10d1ad4421252a7eb1b01ad92f5b + http_version: HTTP/1.1 + status_code: 200 +version: 1 diff --git a/tests/tools/couchbase_tool_test.py b/tests/tools/couchbase_tool_test.py new file mode 100644 index 000000000..424a19025 --- /dev/null +++ b/tests/tools/couchbase_tool_test.py @@ -0,0 +1,365 @@ +import pytest +from unittest.mock import MagicMock, patch, ANY + +# Mock the couchbase library before importing the tool +# This prevents ImportErrors if couchbase isn't installed in the test environment +mock_couchbase = MagicMock() +mock_couchbase.search = MagicMock() +mock_couchbase.cluster = MagicMock() +mock_couchbase.options = MagicMock() +mock_couchbase.vector_search = MagicMock() + +# Simulate the structure needed for checks +mock_couchbase.cluster.Cluster = MagicMock() +mock_couchbase.options.SearchOptions = MagicMock() +mock_couchbase.vector_search.VectorQuery = MagicMock() +mock_couchbase.vector_search.VectorSearch = MagicMock() +mock_couchbase.search.SearchRequest = MagicMock() # Mock the class itself +mock_couchbase.search.SearchRequest.create = MagicMock() # Mock the class method + +# Add necessary exception types if needed for testing error handling +class MockCouchbaseException(Exception): + pass +mock_couchbase.exceptions = MagicMock() +mock_couchbase.exceptions.BucketNotFoundException = MockCouchbaseException +mock_couchbase.exceptions.ScopeNotFoundException = MockCouchbaseException +mock_couchbase.exceptions.CollectionNotFoundException = MockCouchbaseException +mock_couchbase.exceptions.IndexNotFoundException = MockCouchbaseException + + +import sys +sys.modules['couchbase'] = mock_couchbase +sys.modules['couchbase.search'] = mock_couchbase.search +sys.modules['couchbase.cluster'] = mock_couchbase.cluster +sys.modules['couchbase.options'] = mock_couchbase.options +sys.modules['couchbase.vector_search'] = mock_couchbase.vector_search +sys.modules['couchbase.exceptions'] = mock_couchbase.exceptions + +# Now import the tool +from crewai_tools.tools.couchbase_tool.couchbase_tool import CouchbaseFTSVectorSearchTool + +# --- Test Fixtures --- +@pytest.fixture(autouse=True) +def reset_global_mocks(): + """Reset call counts for globally defined mocks before each test.""" + # Reset the specific mock causing the issue + mock_couchbase.vector_search.VectorQuery.reset_mock() + # It's good practice to also reset other related global mocks + # that might be called in your tests to prevent similar issues: + mock_couchbase.vector_search.VectorSearch.from_vector_query.reset_mock() + mock_couchbase.search.SearchRequest.create.reset_mock() + +# Additional fixture to handle import pollution in full test suite +@pytest.fixture(autouse=True) +def ensure_couchbase_mocks(): + """Ensure that couchbase imports are properly mocked even when other tests have run first.""" + # This fixture ensures our mocks are in place regardless of import order + original_modules = {} + + # Store any existing modules + for module_name in ['couchbase', 'couchbase.search', 'couchbase.cluster', 'couchbase.options', 'couchbase.vector_search', 'couchbase.exceptions']: + if module_name in sys.modules: + original_modules[module_name] = sys.modules[module_name] + + # Ensure our mocks are active + sys.modules['couchbase'] = mock_couchbase + sys.modules['couchbase.search'] = mock_couchbase.search + sys.modules['couchbase.cluster'] = mock_couchbase.cluster + sys.modules['couchbase.options'] = mock_couchbase.options + sys.modules['couchbase.vector_search'] = mock_couchbase.vector_search + sys.modules['couchbase.exceptions'] = mock_couchbase.exceptions + + yield + + # Restore original modules if they existed + for module_name, original_module in original_modules.items(): + if original_module is not None: + sys.modules[module_name] = original_module + +@pytest.fixture +def mock_cluster(): + cluster = MagicMock() + bucket_manager = MagicMock() + search_index_manager = MagicMock() + bucket = MagicMock() + scope = MagicMock() + collection = MagicMock() + scope_search_index_manager = MagicMock() + + # Setup mock return values for checks + cluster.buckets.return_value = bucket_manager + cluster.search_indexes.return_value = search_index_manager + cluster.bucket.return_value = bucket + bucket.scope.return_value = scope + scope.collection.return_value = collection + scope.search_indexes.return_value = scope_search_index_manager + + # Mock bucket existence check + bucket_manager.get_bucket.return_value = True + + # Mock scope/collection existence check + mock_scope_spec = MagicMock() + mock_scope_spec.name = "test_scope" + mock_collection_spec = MagicMock() + mock_collection_spec.name = "test_collection" + mock_scope_spec.collections = [mock_collection_spec] + bucket.collections.return_value.get_all_scopes.return_value = [mock_scope_spec] + + # Mock index existence check + mock_index_def = MagicMock() + mock_index_def.name = "test_index" + scope_search_index_manager.get_all_indexes.return_value = [mock_index_def] + search_index_manager.get_all_indexes.return_value = [mock_index_def] + + return cluster + +@pytest.fixture +def mock_embedding_function(): + # Simple mock embedding function + # return lambda query: [0.1] * 10 # Example embedding vector + return MagicMock(return_value=[0.1] * 10) + +@pytest.fixture +def tool_config(mock_cluster, mock_embedding_function): + return { + "cluster": mock_cluster, + "bucket_name": "test_bucket", + "scope_name": "test_scope", + "collection_name": "test_collection", + "index_name": "test_index", + "embedding_function": mock_embedding_function, + "limit": 5, + "embedding_key": "test_embedding", + "scoped_index": True + } + +@pytest.fixture +def couchbase_tool(tool_config): + # Patch COUCHBASE_AVAILABLE to True for these tests + with patch('crewai_tools.tools.couchbase_tool.couchbase_tool.COUCHBASE_AVAILABLE', True): + tool = CouchbaseFTSVectorSearchTool(**tool_config) + return tool + +@pytest.fixture +def mock_search_iter(): + mock_iter = MagicMock() + # Simulate search results with a 'fields' attribute + mock_row1 = MagicMock() + mock_row1.fields = {"id": "doc1", "text": "content 1", "test_embedding": [0.1]*10} + mock_row2 = MagicMock() + mock_row2.fields = {"id": "doc2", "text": "content 2", "test_embedding": [0.2]*10} + mock_iter.rows.return_value = [mock_row1, mock_row2] + return mock_iter + +# --- Test Cases --- + +def test_initialization_success(couchbase_tool, tool_config): + """Test successful initialization with valid config.""" + assert couchbase_tool.cluster == tool_config["cluster"] + assert couchbase_tool.bucket_name == "test_bucket" + assert couchbase_tool.scope_name == "test_scope" + assert couchbase_tool.collection_name == "test_collection" + assert couchbase_tool.index_name == "test_index" + assert couchbase_tool.embedding_function is not None + assert couchbase_tool.limit == 5 + assert couchbase_tool.embedding_key == "test_embedding" + assert couchbase_tool.scoped_index == True + + # Check if helper methods were called during init (via mocks in fixture) + couchbase_tool.cluster.buckets().get_bucket.assert_called_once_with("test_bucket") + couchbase_tool.cluster.bucket().collections().get_all_scopes.assert_called_once() + couchbase_tool.cluster.bucket().scope().search_indexes().get_all_indexes.assert_called_once() + +def test_initialization_missing_required_args(mock_cluster, mock_embedding_function): + """Test initialization fails when required arguments are missing.""" + base_config = { + "cluster": mock_cluster, "bucket_name": "b", "scope_name": "s", + "collection_name": "c", "index_name": "i", "embedding_function": mock_embedding_function + } + required_keys = base_config.keys() + + with patch('crewai_tools.tools.couchbase_tool.couchbase_tool.COUCHBASE_AVAILABLE', True): + for key in required_keys: + incomplete_config = base_config.copy() + del incomplete_config[key] + with pytest.raises(ValueError): + CouchbaseFTSVectorSearchTool(**incomplete_config) + +def test_initialization_couchbase_unavailable(): + """Test behavior when couchbase library is not available.""" + with patch('crewai_tools.tools.couchbase_tool.couchbase_tool.COUCHBASE_AVAILABLE', False): + with patch('click.confirm', return_value=False) as mock_confirm: + with pytest.raises(ImportError, match="The 'couchbase' package is required"): + CouchbaseFTSVectorSearchTool(cluster=MagicMock(), bucket_name="b", scope_name="s", + collection_name="c", index_name="i", embedding_function=MagicMock()) + mock_confirm.assert_called_once() # Ensure user was prompted + +def test_run_success_scoped_index(couchbase_tool, mock_search_iter, tool_config, mock_embedding_function): + """Test successful _run execution with a scoped index.""" + query = "find relevant documents" + # expected_embedding = mock_embedding_function(query) + + # Mock the scope search method + couchbase_tool._scope.search = MagicMock(return_value=mock_search_iter) + # Mock the VectorQuery/VectorSearch/SearchRequest creation using runtime patching + with patch('crewai_tools.tools.couchbase_tool.couchbase_tool.VectorQuery') as mock_vq, \ + patch('crewai_tools.tools.couchbase_tool.couchbase_tool.VectorSearch') as mock_vs, \ + patch('crewai_tools.tools.couchbase_tool.couchbase_tool.search.SearchRequest') as mock_sr, \ + patch('crewai_tools.tools.couchbase_tool.couchbase_tool.SearchOptions') as mock_so: + + # Set up the mock objects and their return values + mock_vector_query = MagicMock() + mock_vector_search = MagicMock() + mock_search_req = MagicMock() + mock_search_options = MagicMock() + + mock_vq.return_value = mock_vector_query + mock_vs.from_vector_query.return_value = mock_vector_search + mock_sr.create.return_value = mock_search_req + mock_so.return_value = mock_search_options + + result = couchbase_tool._run(query=query) + + # Check embedding function call + tool_config['embedding_function'].assert_called_once_with(query) + + # Check VectorQuery call + mock_vq.assert_called_once_with( + tool_config['embedding_key'], mock_embedding_function.return_value, tool_config['limit'] + ) + # Check VectorSearch call + mock_vs.from_vector_query.assert_called_once_with(mock_vector_query) + # Check SearchRequest creation + mock_sr.create.assert_called_once_with(mock_vector_search) + # Check SearchOptions creation + mock_so.assert_called_once_with(limit=tool_config['limit'], fields=["*"]) + + # Check that scope search was called correctly + couchbase_tool._scope.search.assert_called_once_with( + tool_config['index_name'], + mock_search_req, + mock_search_options + ) + + # Check cluster search was NOT called + couchbase_tool.cluster.search.assert_not_called() + + # Check result format (simple check for JSON structure) + assert '"id": "doc1"' in result + assert '"id": "doc2"' in result + assert result.startswith('[') # Should be valid JSON after concatenation + +def test_run_success_global_index(tool_config, mock_search_iter, mock_embedding_function): + """Test successful _run execution with a global (non-scoped) index.""" + tool_config['scoped_index'] = False + with patch('crewai_tools.tools.couchbase_tool.couchbase_tool.COUCHBASE_AVAILABLE', True): + couchbase_tool = CouchbaseFTSVectorSearchTool(**tool_config) + + query = "find global documents" + # expected_embedding = mock_embedding_function(query) + + # Mock the cluster search method + couchbase_tool.cluster.search = MagicMock(return_value=mock_search_iter) + # Mock the VectorQuery/VectorSearch/SearchRequest creation using runtime patching + with patch('crewai_tools.tools.couchbase_tool.couchbase_tool.VectorQuery') as mock_vq, \ + patch('crewai_tools.tools.couchbase_tool.couchbase_tool.VectorSearch') as mock_vs, \ + patch('crewai_tools.tools.couchbase_tool.couchbase_tool.search.SearchRequest') as mock_sr, \ + patch('crewai_tools.tools.couchbase_tool.couchbase_tool.SearchOptions') as mock_so: + + # Set up the mock objects and their return values + mock_vector_query = MagicMock() + mock_vector_search = MagicMock() + mock_search_req = MagicMock() + mock_search_options = MagicMock() + + mock_vq.return_value = mock_vector_query + mock_vs.from_vector_query.return_value = mock_vector_search + mock_sr.create.return_value = mock_search_req + mock_so.return_value = mock_search_options + + result = couchbase_tool._run(query=query) + + # Check embedding function call + tool_config['embedding_function'].assert_called_once_with(query) + + # Check VectorQuery/Search call + mock_vq.assert_called_once_with( + tool_config['embedding_key'], mock_embedding_function.return_value, tool_config['limit'] + ) + mock_sr.create.assert_called_once_with(mock_vector_search) + # Check SearchOptions creation + mock_so.assert_called_once_with(limit=tool_config['limit'], fields=["*"]) + + # Check that cluster search was called correctly + couchbase_tool.cluster.search.assert_called_once_with( + tool_config['index_name'], + mock_search_req, + mock_search_options + ) + + # Check scope search was NOT called + couchbase_tool._scope.search.assert_not_called() + + # Check result format + assert '"id": "doc1"' in result + assert '"id": "doc2"' in result + +def test_check_bucket_exists_fail(tool_config): + """Test check for bucket non-existence.""" + mock_cluster = tool_config['cluster'] + mock_cluster.buckets().get_bucket.side_effect = mock_couchbase.exceptions.BucketNotFoundException("Bucket not found") + + with patch('crewai_tools.tools.couchbase_tool.couchbase_tool.COUCHBASE_AVAILABLE', True): + with pytest.raises(ValueError, match="Bucket test_bucket does not exist."): + CouchbaseFTSVectorSearchTool(**tool_config) + + +def test_check_scope_exists_fail(tool_config): + """Test check for scope non-existence.""" + mock_cluster = tool_config['cluster'] + # Simulate scope not being in the list returned + mock_scope_spec = MagicMock() + mock_scope_spec.name = "wrong_scope" + mock_cluster.bucket().collections().get_all_scopes.return_value = [mock_scope_spec] + + with patch('crewai_tools.tools.couchbase_tool.couchbase_tool.COUCHBASE_AVAILABLE', True): + with pytest.raises(ValueError, match="Scope test_scope not found"): + CouchbaseFTSVectorSearchTool(**tool_config) + + +def test_check_collection_exists_fail(tool_config): + """Test check for collection non-existence.""" + mock_cluster = tool_config['cluster'] + # Simulate collection not being in the scope's list + mock_scope_spec = MagicMock() + mock_scope_spec.name = "test_scope" + mock_collection_spec = MagicMock() + mock_collection_spec.name = "wrong_collection" + mock_scope_spec.collections = [mock_collection_spec] # Only has wrong collection + mock_cluster.bucket().collections().get_all_scopes.return_value = [mock_scope_spec] + + with patch('crewai_tools.tools.couchbase_tool.couchbase_tool.COUCHBASE_AVAILABLE', True): + with pytest.raises(ValueError, match="Collection test_collection not found"): + CouchbaseFTSVectorSearchTool(**tool_config) + +def test_check_index_exists_fail_scoped(tool_config): + """Test check for scoped index non-existence.""" + mock_cluster = tool_config['cluster'] + # Simulate index not being in the list returned by scope manager + mock_cluster.bucket().scope().search_indexes().get_all_indexes.return_value = [] + + with patch('crewai_tools.tools.couchbase_tool.couchbase_tool.COUCHBASE_AVAILABLE', True): + with pytest.raises(ValueError, match="Index test_index does not exist"): + CouchbaseFTSVectorSearchTool(**tool_config) + + +def test_check_index_exists_fail_global(tool_config): + """Test check for global index non-existence.""" + tool_config['scoped_index'] = False + mock_cluster = tool_config['cluster'] + # Simulate index not being in the list returned by cluster manager + mock_cluster.search_indexes().get_all_indexes.return_value = [] + + with patch('crewai_tools.tools.couchbase_tool.couchbase_tool.COUCHBASE_AVAILABLE', True): + with pytest.raises(ValueError, match="Index test_index does not exist"): + CouchbaseFTSVectorSearchTool(**tool_config) \ No newline at end of file diff --git a/tests/tools/crewai_enterprise_tools_test.py b/tests/tools/crewai_enterprise_tools_test.py new file mode 100644 index 000000000..2e4f51ca9 --- /dev/null +++ b/tests/tools/crewai_enterprise_tools_test.py @@ -0,0 +1,355 @@ +import os +import unittest +from unittest.mock import patch, MagicMock + + +from crewai.tools import BaseTool +from crewai_tools.tools import CrewaiEnterpriseTools +from crewai_tools.adapters.tool_collection import ToolCollection +from crewai_tools.adapters.enterprise_adapter import EnterpriseActionTool + + +class TestCrewaiEnterpriseTools(unittest.TestCase): + def setUp(self): + self.mock_tools = [ + self._create_mock_tool("tool1", "Tool 1 Description"), + self._create_mock_tool("tool2", "Tool 2 Description"), + self._create_mock_tool("tool3", "Tool 3 Description"), + ] + self.adapter_patcher = patch( + "crewai_tools.tools.crewai_enterprise_tools.crewai_enterprise_tools.EnterpriseActionKitToolAdapter" + ) + self.MockAdapter = self.adapter_patcher.start() + + mock_adapter_instance = self.MockAdapter.return_value + mock_adapter_instance.tools.return_value = self.mock_tools + + def tearDown(self): + self.adapter_patcher.stop() + + def _create_mock_tool(self, name, description): + mock_tool = MagicMock(spec=BaseTool) + mock_tool.name = name + mock_tool.description = description + return mock_tool + + @patch.dict(os.environ, {"CREWAI_ENTERPRISE_TOOLS_TOKEN": "env-token"}) + def test_returns_tool_collection(self): + tools = CrewaiEnterpriseTools() + self.assertIsInstance(tools, ToolCollection) + + @patch.dict(os.environ, {"CREWAI_ENTERPRISE_TOOLS_TOKEN": "env-token"}) + def test_returns_all_tools_when_no_actions_list(self): + tools = CrewaiEnterpriseTools() + self.assertEqual(len(tools), 3) + self.assertEqual(tools[0].name, "tool1") + self.assertEqual(tools[1].name, "tool2") + self.assertEqual(tools[2].name, "tool3") + + @patch.dict(os.environ, {"CREWAI_ENTERPRISE_TOOLS_TOKEN": "env-token"}) + def test_filters_tools_by_actions_list(self): + tools = CrewaiEnterpriseTools(actions_list=["ToOl1", "tool3"]) + self.assertEqual(len(tools), 2) + self.assertEqual(tools[0].name, "tool1") + self.assertEqual(tools[1].name, "tool3") + + def test_uses_provided_parameters(self): + CrewaiEnterpriseTools( + enterprise_token="test-token", + enterprise_action_kit_project_id="project-id", + enterprise_action_kit_project_url="project-url", + ) + + self.MockAdapter.assert_called_once_with( + enterprise_action_token="test-token", + enterprise_action_kit_project_id="project-id", + enterprise_action_kit_project_url="project-url", + ) + + @patch.dict(os.environ, {"CREWAI_ENTERPRISE_TOOLS_TOKEN": "env-token"}) + def test_uses_environment_token(self): + CrewaiEnterpriseTools() + self.MockAdapter.assert_called_once_with(enterprise_action_token="env-token") + + @patch.dict(os.environ, {"CREWAI_ENTERPRISE_TOOLS_TOKEN": "env-token"}) + def test_uses_environment_token_when_no_token_provided(self): + CrewaiEnterpriseTools(enterprise_token="") + self.MockAdapter.assert_called_once_with(enterprise_action_token="env-token") + + @patch.dict( + os.environ, + { + "CREWAI_ENTERPRISE_TOOLS_TOKEN": "env-token", + "CREWAI_ENTERPRISE_TOOLS_ACTIONS_LIST": '["tool1", "tool3"]', + }, + ) + def test_uses_environment_actions_list(self): + tools = CrewaiEnterpriseTools() + self.assertEqual(len(tools), 2) + self.assertEqual(tools[0].name, "tool1") + self.assertEqual(tools[1].name, "tool3") + + +class TestEnterpriseActionToolSchemaConversion(unittest.TestCase): + """Test the enterprise action tool schema conversion and validation.""" + + def setUp(self): + self.test_schema = { + "type": "function", + "function": { + "name": "TEST_COMPLEX_ACTION", + "description": "Test action with complex nested structure", + "parameters": { + "type": "object", + "properties": { + "filterCriteria": { + "type": "object", + "description": "Filter criteria object", + "properties": { + "operation": {"type": "string", "enum": ["AND", "OR"]}, + "rules": { + "type": "array", + "items": { + "type": "object", + "properties": { + "field": { + "type": "string", + "enum": ["name", "email", "status"], + }, + "operator": { + "type": "string", + "enum": ["equals", "contains"], + }, + "value": {"type": "string"}, + }, + "required": ["field", "operator", "value"], + }, + }, + }, + "required": ["operation", "rules"], + }, + "options": { + "type": "object", + "properties": { + "limit": {"type": "integer"}, + "offset": {"type": "integer"}, + }, + "required": [], + }, + }, + "required": [], + }, + }, + } + + def test_complex_schema_conversion(self): + """Test that complex nested schemas are properly converted to Pydantic models.""" + tool = EnterpriseActionTool( + name="gmail_search_for_email", + description="Test tool", + enterprise_action_token="test_token", + action_name="GMAIL_SEARCH_FOR_EMAIL", + action_schema=self.test_schema, + ) + + self.assertEqual(tool.name, "gmail_search_for_email") + self.assertEqual(tool.action_name, "GMAIL_SEARCH_FOR_EMAIL") + + schema_class = tool.args_schema + self.assertIsNotNone(schema_class) + + schema_fields = schema_class.model_fields + self.assertIn("filterCriteria", schema_fields) + self.assertIn("options", schema_fields) + + # Test valid input structure + valid_input = { + "filterCriteria": { + "operation": "AND", + "rules": [ + {"field": "name", "operator": "contains", "value": "test"}, + {"field": "status", "operator": "equals", "value": "active"}, + ], + }, + "options": {"limit": 10}, + } + + # This should not raise an exception + validated_input = schema_class(**valid_input) + self.assertIsNotNone(validated_input.filterCriteria) + self.assertIsNotNone(validated_input.options) + + def test_optional_fields_validation(self): + """Test that optional fields work correctly.""" + tool = EnterpriseActionTool( + name="gmail_search_for_email", + description="Test tool", + enterprise_action_token="test_token", + action_name="GMAIL_SEARCH_FOR_EMAIL", + action_schema=self.test_schema, + ) + + schema_class = tool.args_schema + + minimal_input = {} + validated_input = schema_class(**minimal_input) + self.assertIsNone(validated_input.filterCriteria) + self.assertIsNone(validated_input.options) + + partial_input = {"options": {"limit": 10}} + validated_input = schema_class(**partial_input) + self.assertIsNone(validated_input.filterCriteria) + self.assertIsNotNone(validated_input.options) + + def test_enum_validation(self): + """Test that enum values are properly validated.""" + tool = EnterpriseActionTool( + name="gmail_search_for_email", + description="Test tool", + enterprise_action_token="test_token", + action_name="GMAIL_SEARCH_FOR_EMAIL", + action_schema=self.test_schema, + ) + + schema_class = tool.args_schema + + invalid_input = { + "filterCriteria": { + "operation": "INVALID_OPERATOR", + "rules": [], + } + } + + with self.assertRaises(Exception): + schema_class(**invalid_input) + + def test_required_nested_fields(self): + """Test that required fields in nested objects are validated.""" + tool = EnterpriseActionTool( + name="gmail_search_for_email", + description="Test tool", + enterprise_action_token="test_token", + action_name="GMAIL_SEARCH_FOR_EMAIL", + action_schema=self.test_schema, + ) + + schema_class = tool.args_schema + + incomplete_input = { + "filterCriteria": { + "operation": "OR", + "rules": [ + { + "field": "name", + "operator": "contains", + } + ], + } + } + + with self.assertRaises(Exception): + schema_class(**incomplete_input) + + @patch("requests.post") + def test_tool_execution_with_complex_input(self, mock_post): + """Test that the tool can execute with complex validated input.""" + mock_response = MagicMock() + mock_response.ok = True + mock_response.json.return_value = {"success": True, "results": []} + mock_post.return_value = mock_response + + tool = EnterpriseActionTool( + name="gmail_search_for_email", + description="Test tool", + enterprise_action_token="test_token", + action_name="GMAIL_SEARCH_FOR_EMAIL", + action_schema=self.test_schema, + ) + + tool._run( + filterCriteria={ + "operation": "OR", + "rules": [ + {"field": "name", "operator": "contains", "value": "test"}, + {"field": "status", "operator": "equals", "value": "active"}, + ], + }, + options={"limit": 10}, + ) + + mock_post.assert_called_once() + call_args = mock_post.call_args + payload = call_args[1]["json"] + + self.assertIn("filterCriteria", payload) + self.assertIn("options", payload) + self.assertEqual(payload["filterCriteria"]["operation"], "OR") + + def test_model_naming_convention(self): + """Test that generated model names follow proper conventions.""" + tool = EnterpriseActionTool( + name="gmail_search_for_email", + description="Test tool", + enterprise_action_token="test_token", + action_name="GMAIL_SEARCH_FOR_EMAIL", + action_schema=self.test_schema, + ) + + schema_class = tool.args_schema + self.assertIsNotNone(schema_class) + + self.assertTrue(schema_class.__name__.endswith("Schema")) + self.assertTrue(schema_class.__name__[0].isupper()) + + complex_input = { + "filterCriteria": { + "operation": "OR", + "rules": [ + {"field": "name", "operator": "contains", "value": "test"}, + {"field": "status", "operator": "equals", "value": "active"}, + ], + }, + "options": {"limit": 10}, + } + + validated = schema_class(**complex_input) + self.assertIsNotNone(validated.filterCriteria) + + def test_simple_schema_with_enums(self): + """Test a simpler schema with basic enum validation.""" + simple_schema = { + "type": "function", + "function": { + "name": "SIMPLE_TEST", + "description": "Simple test function", + "parameters": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": ["active", "inactive", "pending"], + }, + "priority": {"type": "integer", "enum": [1, 2, 3]}, + }, + "required": ["status"], + }, + }, + } + + tool = EnterpriseActionTool( + name="simple_test", + description="Simple test tool", + enterprise_action_token="test_token", + action_name="SIMPLE_TEST", + action_schema=simple_schema, + ) + + schema_class = tool.args_schema + + valid_input = {"status": "active", "priority": 2} + validated = schema_class(**valid_input) + self.assertEqual(validated.status, "active") + self.assertEqual(validated.priority, 2) + + with self.assertRaises(Exception): + schema_class(status="invalid_status") diff --git a/tests/tools/crewai_platform_tools/test_crewai_platform_action_tool.py b/tests/tools/crewai_platform_tools/test_crewai_platform_action_tool.py new file mode 100644 index 000000000..c24237082 --- /dev/null +++ b/tests/tools/crewai_platform_tools/test_crewai_platform_action_tool.py @@ -0,0 +1,165 @@ + +import unittest +from unittest.mock import patch, Mock +import pytest +from crewai_tools.tools.crewai_platform_tools import CrewAIPlatformActionTool + + +class TestCrewAIPlatformActionTool(unittest.TestCase): + @pytest.fixture + def sample_action_schema(self): + return { + "function": { + "name": "test_action", + "description": "Test action for unit testing", + "parameters": { + "type": "object", + "properties": { + "message": { + "type": "string", + "description": "Message to send" + }, + "priority": { + "type": "integer", + "description": "Priority level" + } + }, + "required": ["message"] + } + } + } + + @pytest.fixture + def platform_action_tool(self, sample_action_schema): + return CrewAIPlatformActionTool( + description="Test Action Tool\nTest description", + action_name="test_action", + action_schema=sample_action_schema + ) + + + @patch.dict("os.environ", {"CREWAI_PLATFORM_INTEGRATION_TOKEN": "test_token"}) + @patch("crewai_tools.tools.crewai_platform_tools.crewai_platform_action_tool.requests.post") + def test_run_success(self, mock_post): + schema = { + "function": { + "name": "test_action", + "description": "Test action", + "parameters": { + "type": "object", + "properties": { + "message": {"type": "string", "description": "Message"} + }, + "required": ["message"] + } + } + } + + tool = CrewAIPlatformActionTool( + description="Test tool", + action_name="test_action", + action_schema=schema + ) + + mock_response = Mock() + mock_response.ok = True + mock_response.json.return_value = {"result": "success", "data": "test_data"} + mock_post.return_value = mock_response + + result = tool._run(message="test message") + + mock_post.assert_called_once() + _, kwargs = mock_post.call_args + + assert "test_action/execute" in kwargs["url"] + assert kwargs["headers"]["Authorization"] == "Bearer test_token" + assert kwargs["json"]["message"] == "test message" + assert "success" in result + + @patch.dict("os.environ", {"CREWAI_PLATFORM_INTEGRATION_TOKEN": "test_token"}) + @patch("crewai_tools.tools.crewai_platform_tools.crewai_platform_action_tool.requests.post") + def test_run_api_error(self, mock_post): + schema = { + "function": { + "name": "test_action", + "description": "Test action", + "parameters": { + "type": "object", + "properties": { + "message": {"type": "string", "description": "Message"} + }, + "required": ["message"] + } + } + } + + tool = CrewAIPlatformActionTool( + description="Test tool", + action_name="test_action", + action_schema=schema + ) + + mock_response = Mock() + mock_response.ok = False + mock_response.json.return_value = {"error": {"message": "Invalid request"}} + mock_post.return_value = mock_response + + result = tool._run(message="test message") + + assert "API request failed" in result + assert "Invalid request" in result + + @patch.dict("os.environ", {"CREWAI_PLATFORM_INTEGRATION_TOKEN": "test_token"}) + @patch("crewai_tools.tools.crewai_platform_tools.crewai_platform_action_tool.requests.post") + def test_run_exception(self, mock_post): + schema = { + "function": { + "name": "test_action", + "description": "Test action", + "parameters": { + "type": "object", + "properties": { + "message": {"type": "string", "description": "Message"} + }, + "required": ["message"] + } + } + } + + tool = CrewAIPlatformActionTool( + description="Test tool", + action_name="test_action", + action_schema=schema + ) + + mock_post.side_effect = Exception("Network error") + + result = tool._run(message="test message") + + assert "Error executing action test_action: Network error" in result + + def test_run_without_token(self): + schema = { + "function": { + "name": "test_action", + "description": "Test action", + "parameters": { + "type": "object", + "properties": { + "message": {"type": "string", "description": "Message"} + }, + "required": ["message"] + } + } + } + + tool = CrewAIPlatformActionTool( + description="Test tool", + action_name="test_action", + action_schema=schema + ) + + with patch.dict("os.environ", {}, clear=True): + result = tool._run(message="test message") + assert "Error executing action test_action:" in result + assert "No platform integration token found" in result diff --git a/tests/tools/crewai_platform_tools/test_crewai_platform_tool_builder.py b/tests/tools/crewai_platform_tools/test_crewai_platform_tool_builder.py new file mode 100644 index 000000000..e60be2e12 --- /dev/null +++ b/tests/tools/crewai_platform_tools/test_crewai_platform_tool_builder.py @@ -0,0 +1,223 @@ +import unittest +from unittest.mock import patch, Mock +import pytest +from crewai_tools.tools.crewai_platform_tools import CrewaiPlatformToolBuilder, CrewAIPlatformActionTool + + +class TestCrewaiPlatformToolBuilder(unittest.TestCase): + @pytest.fixture + def platform_tool_builder(self): + """Create a CrewaiPlatformToolBuilder instance for testing""" + return CrewaiPlatformToolBuilder(apps=["github", "slack"]) + + @pytest.fixture + def mock_api_response(self): + return { + "actions": { + "github": [ + { + "name": "create_issue", + "description": "Create a GitHub issue", + "parameters": { + "type": "object", + "properties": { + "title": {"type": "string", "description": "Issue title"}, + "body": {"type": "string", "description": "Issue body"} + }, + "required": ["title"] + } + } + ], + "slack": [ + { + "name": "send_message", + "description": "Send a Slack message", + "parameters": { + "type": "object", + "properties": { + "channel": {"type": "string", "description": "Channel name"}, + "text": {"type": "string", "description": "Message text"} + }, + "required": ["channel", "text"] + } + } + ] + } + } + + @patch.dict("os.environ", {"CREWAI_PLATFORM_INTEGRATION_TOKEN": "test_token"}) + @patch("crewai_tools.tools.crewai_platform_tools.crewai_platform_tool_builder.requests.get") + def test_fetch_actions_success(self, mock_get): + mock_api_response = { + "actions": { + "github": [ + { + "name": "create_issue", + "description": "Create a GitHub issue", + "parameters": { + "type": "object", + "properties": { + "title": {"type": "string", "description": "Issue title"} + }, + "required": ["title"] + } + } + ] + } + } + + builder = CrewaiPlatformToolBuilder(apps=["github", "slack/send_message"]) + + mock_response = Mock() + mock_response.raise_for_status.return_value = None + mock_response.json.return_value = mock_api_response + mock_get.return_value = mock_response + + builder._fetch_actions() + + mock_get.assert_called_once() + args, kwargs = mock_get.call_args + + assert "/actions" in args[0] + assert kwargs["headers"]["Authorization"] == "Bearer test_token" + assert kwargs["params"]["apps"] == "github,slack/send_message" + + assert "create_issue" in builder._actions_schema + assert builder._actions_schema["create_issue"]["function"]["name"] == "create_issue" + + def test_fetch_actions_no_token(self): + builder = CrewaiPlatformToolBuilder(apps=["github"]) + + with patch.dict("os.environ", {}, clear=True): + with self.assertRaises(ValueError) as context: + builder._fetch_actions() + assert "No platform integration token found" in str(context.exception) + + @patch.dict("os.environ", {"CREWAI_PLATFORM_INTEGRATION_TOKEN": "test_token"}) + @patch("crewai_tools.tools.crewai_platform_tools.crewai_platform_tool_builder.requests.get") + def test_create_tools(self, mock_get): + mock_api_response = { + "actions": { + "github": [ + { + "name": "create_issue", + "description": "Create a GitHub issue", + "parameters": { + "type": "object", + "properties": { + "title": {"type": "string", "description": "Issue title"} + }, + "required": ["title"] + } + } + ], + "slack": [ + { + "name": "send_message", + "description": "Send a Slack message", + "parameters": { + "type": "object", + "properties": { + "channel": {"type": "string", "description": "Channel name"} + }, + "required": ["channel"] + } + } + ] + } + } + + builder = CrewaiPlatformToolBuilder(apps=["github", "slack"]) + + mock_response = Mock() + mock_response.raise_for_status.return_value = None + mock_response.json.return_value = mock_api_response + mock_get.return_value = mock_response + + tools = builder.tools() + + assert len(tools) == 2 + assert all(isinstance(tool, CrewAIPlatformActionTool) for tool in tools) + + tool_names = [tool.action_name for tool in tools] + assert "create_issue" in tool_names + assert "send_message" in tool_names + + github_tool = next((t for t in tools if t.action_name == "create_issue"), None) + slack_tool = next((t for t in tools if t.action_name == "send_message"), None) + + assert github_tool is not None + assert slack_tool is not None + assert "Create a GitHub issue" in github_tool.description + assert "Send a Slack message" in slack_tool.description + + def test_tools_caching(self): + builder = CrewaiPlatformToolBuilder(apps=["github"]) + + cached_tools = [] + + def mock_create_tools(): + builder._tools = cached_tools + + with patch.object(builder, '_fetch_actions') as mock_fetch, \ + patch.object(builder, '_create_tools', side_effect=mock_create_tools) as mock_create: + + tools1 = builder.tools() + assert mock_fetch.call_count == 1 + assert mock_create.call_count == 1 + + tools2 = builder.tools() + assert mock_fetch.call_count == 1 + assert mock_create.call_count == 1 + + assert tools1 is tools2 + + @patch.dict("os.environ", {"CREWAI_PLATFORM_INTEGRATION_TOKEN": "test_token"}) + def test_empty_apps_list(self): + builder = CrewaiPlatformToolBuilder(apps=[]) + + with patch("crewai_tools.tools.crewai_platform_tools.crewai_platform_tool_builder.requests.get") as mock_get: + mock_response = Mock() + mock_response.raise_for_status.return_value = None + mock_response.json.return_value = {"actions": {}} + mock_get.return_value = mock_response + + tools = builder.tools() + + assert isinstance(tools, list) + assert len(tools) == 0 + + _, kwargs = mock_get.call_args + assert kwargs["params"]["apps"] == "" + + def test_detailed_description_generation(self): + builder = CrewaiPlatformToolBuilder(apps=["test"]) + + complex_schema = { + "type": "object", + "properties": { + "simple_string": {"type": "string", "description": "A simple string"}, + "nested_object": { + "type": "object", + "properties": { + "inner_prop": {"type": "integer", "description": "Inner property"} + }, + "description": "Nested object" + }, + "array_prop": { + "type": "array", + "items": {"type": "string"}, + "description": "Array of strings" + } + } + } + + descriptions = builder._generate_detailed_description(complex_schema) + + assert isinstance(descriptions, list) + assert len(descriptions) > 0 + + description_text = "\n".join(descriptions) + assert "simple_string" in description_text + assert "nested_object" in description_text + assert "array_prop" in description_text diff --git a/tests/tools/crewai_platform_tools/test_crewai_platform_tools.py b/tests/tools/crewai_platform_tools/test_crewai_platform_tools.py new file mode 100644 index 000000000..295c68745 --- /dev/null +++ b/tests/tools/crewai_platform_tools/test_crewai_platform_tools.py @@ -0,0 +1,95 @@ +import unittest +from unittest.mock import patch, Mock +from crewai_tools.tools.crewai_platform_tools import CrewaiPlatformTools + + +class TestCrewaiPlatformTools(unittest.TestCase): + + @patch.dict("os.environ", {"CREWAI_PLATFORM_INTEGRATION_TOKEN": "test_token"}) + @patch("crewai_tools.tools.crewai_platform_tools.crewai_platform_tool_builder.requests.get") + def test_crewai_platform_tools_basic(self, mock_get): + mock_response = Mock() + mock_response.raise_for_status.return_value = None + mock_response.json.return_value = {"actions": {"github": []}} + mock_get.return_value = mock_response + + tools = CrewaiPlatformTools(apps=["github"]) + assert tools is not None + assert isinstance(tools, list) + + @patch.dict("os.environ", {"CREWAI_PLATFORM_INTEGRATION_TOKEN": "test_token"}) + @patch("crewai_tools.tools.crewai_platform_tools.crewai_platform_tool_builder.requests.get") + def test_crewai_platform_tools_multiple_apps(self, mock_get): + mock_response = Mock() + mock_response.raise_for_status.return_value = None + mock_response.json.return_value = { + "actions": { + "github": [ + { + "name": "create_issue", + "description": "Create a GitHub issue", + "parameters": { + "type": "object", + "properties": { + "title": {"type": "string", "description": "Issue title"}, + "body": {"type": "string", "description": "Issue body"} + }, + "required": ["title"] + } + } + ], + "slack": [ + { + "name": "send_message", + "description": "Send a Slack message", + "parameters": { + "type": "object", + "properties": { + "channel": {"type": "string", "description": "Channel to send to"}, + "text": {"type": "string", "description": "Message text"} + }, + "required": ["channel", "text"] + } + } + ] + } + } + mock_get.return_value = mock_response + + tools = CrewaiPlatformTools(apps=["github", "slack"]) + assert tools is not None + assert isinstance(tools, list) + assert len(tools) == 2 + + mock_get.assert_called_once() + args, kwargs = mock_get.call_args + assert "apps=github,slack" in args[0] or kwargs.get("params", {}).get("apps") == "github,slack" + + @patch.dict("os.environ", {"CREWAI_PLATFORM_INTEGRATION_TOKEN": "test_token"}) + def test_crewai_platform_tools_empty_apps(self): + with patch("crewai_tools.tools.crewai_platform_tools.crewai_platform_tool_builder.requests.get") as mock_get: + mock_response = Mock() + mock_response.raise_for_status.return_value = None + mock_response.json.return_value = {"actions": {}} + mock_get.return_value = mock_response + + tools = CrewaiPlatformTools(apps=[]) + assert tools is not None + assert isinstance(tools, list) + assert len(tools) == 0 + + @patch.dict("os.environ", {"CREWAI_PLATFORM_INTEGRATION_TOKEN": "test_token"}) + @patch("crewai_tools.tools.crewai_platform_tools.crewai_platform_tool_builder.requests.get") + def test_crewai_platform_tools_api_error_handling(self, mock_get): + mock_get.side_effect = Exception("API Error") + + tools = CrewaiPlatformTools(apps=["github"]) + assert tools is not None + assert isinstance(tools, list) + assert len(tools) == 0 + + def test_crewai_platform_tools_no_token(self): + with patch.dict("os.environ", {}, clear=True): + with self.assertRaises(ValueError) as context: + CrewaiPlatformTools(apps=["github"]) + assert "No platform integration token found" in str(context.exception) diff --git a/tests/tools/exa_search_tool_test.py b/tests/tools/exa_search_tool_test.py new file mode 100644 index 000000000..17c92e2f4 --- /dev/null +++ b/tests/tools/exa_search_tool_test.py @@ -0,0 +1,32 @@ +import os +from unittest.mock import patch +from crewai_tools import EXASearchTool + +import pytest + +@pytest.fixture +def exa_search_tool(): + return EXASearchTool(api_key="test_api_key") + + +@pytest.fixture(autouse=True) +def mock_exa_api_key(): + with patch.dict(os.environ, {"EXA_API_KEY": "test_key_from_env"}): + yield + +def test_exa_search_tool_initialization(): + with patch("crewai_tools.tools.exa_tools.exa_search_tool.Exa") as mock_exa_class: + api_key = "test_api_key" + tool = EXASearchTool(api_key=api_key) + + assert tool.api_key == api_key + assert tool.content is False + assert tool.summary is False + assert tool.type == "auto" + mock_exa_class.assert_called_once_with(api_key=api_key) + + +def test_exa_search_tool_initialization_with_env(mock_exa_api_key): + with patch("crewai_tools.tools.exa_tools.exa_search_tool.Exa") as mock_exa_class: + EXASearchTool() + mock_exa_class.assert_called_once_with(api_key="test_key_from_env") diff --git a/tests/tools/generate_crewai_automation_tool_test.py b/tests/tools/generate_crewai_automation_tool_test.py new file mode 100644 index 000000000..715e00804 --- /dev/null +++ b/tests/tools/generate_crewai_automation_tool_test.py @@ -0,0 +1,187 @@ +import os +from unittest.mock import MagicMock, patch + +import pytest +import requests + +from crewai_tools.tools.generate_crewai_automation_tool.generate_crewai_automation_tool import ( + GenerateCrewaiAutomationTool, + GenerateCrewaiAutomationToolSchema, +) + + +@pytest.fixture(autouse=True) +def mock_env(): + with patch.dict(os.environ, {"CREWAI_PERSONAL_ACCESS_TOKEN": "test_token"}): + os.environ.pop("CREWAI_PLUS_URL", None) + yield + + +@pytest.fixture +def tool(): + return GenerateCrewaiAutomationTool() + + +@pytest.fixture +def custom_url_tool(): + with patch.dict(os.environ, {"CREWAI_PLUS_URL": "https://custom.crewai.com"}): + return GenerateCrewaiAutomationTool() + + +def test_default_initialization(tool): + assert tool.crewai_enterprise_url == "https://app.crewai.com" + assert tool.personal_access_token == "test_token" + assert tool.name == "Generate CrewAI Automation" + + +def test_custom_base_url_from_environment(custom_url_tool): + assert custom_url_tool.crewai_enterprise_url == "https://custom.crewai.com" + + +def test_personal_access_token_from_environment(tool): + assert tool.personal_access_token == "test_token" + + +def test_valid_prompt_only(): + schema = GenerateCrewaiAutomationToolSchema( + prompt="Create a web scraping automation" + ) + assert schema.prompt == "Create a web scraping automation" + assert schema.organization_id is None + + +def test_valid_prompt_with_organization_id(): + schema = GenerateCrewaiAutomationToolSchema( + prompt="Create automation", organization_id="org-123" + ) + assert schema.prompt == "Create automation" + assert schema.organization_id == "org-123" + + +def test_empty_prompt_validation(): + schema = GenerateCrewaiAutomationToolSchema(prompt="") + assert schema.prompt == "" + assert schema.organization_id is None + + +@patch("requests.post") +def test_successful_generation_without_org_id(mock_post, tool): + mock_response = MagicMock() + mock_response.json.return_value = { + "url": "https://app.crewai.com/studio/project-123" + } + mock_post.return_value = mock_response + + result = tool.run(prompt="Create automation") + + assert ( + result + == "Generated CrewAI Studio project URL: https://app.crewai.com/studio/project-123" + ) + mock_post.assert_called_once_with( + "https://app.crewai.com/crewai_plus/api/v1/studio", + headers={ + "Authorization": "Bearer test_token", + "Content-Type": "application/json", + "Accept": "application/json", + }, + json={"prompt": "Create automation"}, + ) + + +@patch("requests.post") +def test_successful_generation_with_org_id(mock_post, tool): + mock_response = MagicMock() + mock_response.json.return_value = { + "url": "https://app.crewai.com/studio/project-456" + } + mock_post.return_value = mock_response + + result = tool.run(prompt="Create automation", organization_id="org-456") + + assert ( + result + == "Generated CrewAI Studio project URL: https://app.crewai.com/studio/project-456" + ) + mock_post.assert_called_once_with( + "https://app.crewai.com/crewai_plus/api/v1/studio", + headers={ + "Authorization": "Bearer test_token", + "Content-Type": "application/json", + "Accept": "application/json", + "X-Crewai-Organization-Id": "org-456", + }, + json={"prompt": "Create automation"}, + ) + + +@patch("requests.post") +def test_custom_base_url_usage(mock_post, custom_url_tool): + mock_response = MagicMock() + mock_response.json.return_value = { + "url": "https://custom.crewai.com/studio/project-789" + } + mock_post.return_value = mock_response + + custom_url_tool.run(prompt="Create automation") + + mock_post.assert_called_once_with( + "https://custom.crewai.com/crewai_plus/api/v1/studio", + headers={ + "Authorization": "Bearer test_token", + "Content-Type": "application/json", + "Accept": "application/json", + }, + json={"prompt": "Create automation"}, + ) + + +@patch("requests.post") +def test_api_error_response_handling(mock_post, tool): + mock_post.return_value.raise_for_status.side_effect = requests.HTTPError( + "400 Bad Request" + ) + + with pytest.raises(requests.HTTPError): + tool.run(prompt="Create automation") + + +@patch("requests.post") +def test_network_error_handling(mock_post, tool): + mock_post.side_effect = requests.ConnectionError("Network unreachable") + + with pytest.raises(requests.ConnectionError): + tool.run(prompt="Create automation") + + +@patch("requests.post") +def test_api_response_missing_url(mock_post, tool): + mock_response = MagicMock() + mock_response.json.return_value = {"status": "success"} + mock_post.return_value = mock_response + + result = tool.run(prompt="Create automation") + + assert result == "Generated CrewAI Studio project URL: None" + + +def test_authorization_header_construction(tool): + headers = tool._get_headers() + + assert headers["Authorization"] == "Bearer test_token" + assert headers["Content-Type"] == "application/json" + assert headers["Accept"] == "application/json" + assert "X-Crewai-Organization-Id" not in headers + + +def test_authorization_header_with_org_id(tool): + headers = tool._get_headers(organization_id="org-123") + + assert headers["Authorization"] == "Bearer test_token" + assert headers["X-Crewai-Organization-Id"] == "org-123" + + +def test_missing_personal_access_token(): + with patch.dict(os.environ, {}, clear=True): + tool = GenerateCrewaiAutomationTool() + assert tool.personal_access_token is None diff --git a/tests/tools/parallel_search_tool_test.py b/tests/tools/parallel_search_tool_test.py new file mode 100644 index 000000000..0d4df60a7 --- /dev/null +++ b/tests/tools/parallel_search_tool_test.py @@ -0,0 +1,47 @@ +import os +import json +from urllib.parse import urlparse +from unittest.mock import patch + +import pytest + +from crewai_tools.tools.parallel_tools.parallel_search_tool import ( + ParallelSearchTool, +) + + +def test_requires_env_var(monkeypatch): + monkeypatch.delenv("PARALLEL_API_KEY", raising=False) + tool = ParallelSearchTool() + result = tool.run(objective="test") + assert "PARALLEL_API_KEY" in result + + +@patch("crewai_tools.tools.parallel_tools.parallel_search_tool.requests.post") +def test_happy_path(mock_post, monkeypatch): + monkeypatch.setenv("PARALLEL_API_KEY", "test") + + mock_post.return_value.status_code = 200 + mock_post.return_value.json.return_value = { + "search_id": "search_123", + "results": [ + { + "url": "https://www.un.org/en/about-us/history-of-the-un", + "title": "History of the United Nations", + "excerpts": [ + "Four months after the San Francisco Conference ended, the United Nations officially began, on 24 October 1945..." + ], + } + ], + } + + tool = ParallelSearchTool() + result = tool.run(objective="When was the UN established?", search_queries=["Founding year UN"]) + data = json.loads(result) + assert "search_id" in data + urls = [r.get("url", "") for r in data.get("results", [])] + # Validate host against allowed set instead of substring matching + allowed_hosts = {"www.un.org", "un.org"} + assert any(urlparse(u).netloc in allowed_hosts for u in urls) + + diff --git a/tests/tools/rag/rag_tool_test.py b/tests/tools/rag/rag_tool_test.py new file mode 100644 index 000000000..42baccc2c --- /dev/null +++ b/tests/tools/rag/rag_tool_test.py @@ -0,0 +1,43 @@ +import os +from tempfile import NamedTemporaryFile +from typing import cast +from unittest import mock + +from pytest import fixture + +from crewai_tools.adapters.embedchain_adapter import EmbedchainAdapter +from crewai_tools.tools.rag.rag_tool import RagTool + + +@fixture(autouse=True) +def mock_embedchain_db_uri(): + with NamedTemporaryFile() as tmp: + uri = f"sqlite:///{tmp.name}" + with mock.patch.dict(os.environ, {"EMBEDCHAIN_DB_URI": uri}): + yield + + +def test_custom_llm_and_embedder(): + class MyTool(RagTool): + pass + + tool = MyTool( + config=dict( + llm=dict( + provider="openai", + config=dict(model="gpt-3.5-custom"), + ), + embedder=dict( + provider="openai", + config=dict(model="text-embedding-3-custom"), + ), + ) + ) + assert tool.adapter is not None + assert isinstance(tool.adapter, EmbedchainAdapter) + + adapter = cast(EmbedchainAdapter, tool.adapter) + assert adapter.embedchain_app.llm.config.model == "gpt-3.5-custom" + assert ( + adapter.embedchain_app.embedding_model.config.model == "text-embedding-3-custom" + ) diff --git a/tests/tools/selenium_scraping_tool_test.py b/tests/tools/selenium_scraping_tool_test.py new file mode 100644 index 000000000..0e285d136 --- /dev/null +++ b/tests/tools/selenium_scraping_tool_test.py @@ -0,0 +1,129 @@ +import os +import tempfile +from unittest.mock import MagicMock, patch + +from bs4 import BeautifulSoup +from selenium.webdriver.chrome.options import Options +from crewai_tools.tools.selenium_scraping_tool.selenium_scraping_tool import ( + SeleniumScrapingTool, +) + + +def mock_driver_with_html(html_content): + driver = MagicMock() + mock_element = MagicMock() + mock_element.get_attribute.return_value = html_content + bs = BeautifulSoup(html_content, "html.parser") + mock_element.text = bs.get_text() + + driver.find_elements.return_value = [mock_element] + driver.find_element.return_value = mock_element + + return driver + + +def initialize_tool_with(mock_driver): + tool = SeleniumScrapingTool(driver=mock_driver) + return tool + + +@patch("selenium.webdriver.Chrome") +def test_tool_initialization(mocked_chrome): + temp_dir = tempfile.mkdtemp() + mocked_chrome.return_value = MagicMock() + + tool = SeleniumScrapingTool() + + assert tool.website_url is None + assert tool.css_element is None + assert tool.cookie is None + assert tool.wait_time == 3 + assert tool.return_html is False + + try: + os.rmdir(temp_dir) + except: + pass + +@patch("selenium.webdriver.Chrome") +def test_tool_initialization_with_options(mocked_chrome): + mocked_chrome.return_value = MagicMock() + + options = Options() + options.add_argument("--disable-gpu") + + SeleniumScrapingTool(options=options) + + mocked_chrome.assert_called_once_with(options=options) + + +@patch("selenium.webdriver.Chrome") +def test_scrape_without_css_selector(_mocked_chrome_driver): + html_content = "
test content
" + mock_driver = mock_driver_with_html(html_content) + tool = initialize_tool_with(mock_driver) + + result = tool._run(website_url="https://example.com") + + assert "test content" in result + mock_driver.get.assert_called_once_with("https://example.com") + mock_driver.find_element.assert_called_with("tag name", "body") + mock_driver.close.assert_called_once() + + +@patch("selenium.webdriver.Chrome") +def test_scrape_with_css_selector(_mocked_chrome_driver): + html_content = "
test content
test content in a specific div
" + mock_driver = mock_driver_with_html(html_content) + tool = initialize_tool_with(mock_driver) + + result = tool._run(website_url="https://example.com", css_element="div.test") + + assert "test content in a specific div" in result + mock_driver.get.assert_called_once_with("https://example.com") + mock_driver.find_elements.assert_called_with("css selector", "div.test") + mock_driver.close.assert_called_once() + + +@patch("selenium.webdriver.Chrome") +def test_scrape_with_return_html_true(_mocked_chrome_driver): + html_content = "
HTML content
" + mock_driver = mock_driver_with_html(html_content) + tool = initialize_tool_with(mock_driver) + + result = tool._run(website_url="https://example.com", return_html=True) + + assert html_content in result + mock_driver.get.assert_called_once_with("https://example.com") + mock_driver.find_element.assert_called_with("tag name", "body") + mock_driver.close.assert_called_once() + + +@patch("selenium.webdriver.Chrome") +def test_scrape_with_return_html_false(_mocked_chrome_driver): + html_content = "
HTML content
" + mock_driver = mock_driver_with_html(html_content) + tool = initialize_tool_with(mock_driver) + + result = tool._run(website_url="https://example.com", return_html=False) + + assert "HTML content" in result + mock_driver.get.assert_called_once_with("https://example.com") + mock_driver.find_element.assert_called_with("tag name", "body") + mock_driver.close.assert_called_once() + + +@patch("selenium.webdriver.Chrome") +def test_scrape_with_driver_error(_mocked_chrome_driver): + mock_driver = MagicMock() + mock_driver.find_element.side_effect = Exception("WebDriver error occurred") + tool = initialize_tool_with(mock_driver) + result = tool._run(website_url="https://example.com") + assert result == "Error scraping website: WebDriver error occurred" + mock_driver.close.assert_called_once() + +@patch("selenium.webdriver.Chrome") +def test_initialization_with_driver(_mocked_chrome_driver): + mock_driver = MagicMock() + tool = initialize_tool_with(mock_driver) + assert tool.driver == mock_driver diff --git a/tests/tools/serper_dev_tool_test.py b/tests/tools/serper_dev_tool_test.py new file mode 100644 index 000000000..d02f0606e --- /dev/null +++ b/tests/tools/serper_dev_tool_test.py @@ -0,0 +1,151 @@ +from unittest.mock import patch +import pytest +from crewai_tools.tools.serper_dev_tool.serper_dev_tool import SerperDevTool +import os + + +@pytest.fixture(autouse=True) +def mock_serper_api_key(): + with patch.dict(os.environ, {"SERPER_API_KEY": "test_key"}): + yield + + +@pytest.fixture +def serper_tool(): + return SerperDevTool(n_results=2) + + +def test_serper_tool_initialization(): + tool = SerperDevTool() + assert tool.n_results == 10 + assert tool.save_file is False + assert tool.search_type == "search" + assert tool.country == "" + assert tool.location == "" + assert tool.locale == "" + + +def test_serper_tool_custom_initialization(): + tool = SerperDevTool( + n_results=5, + save_file=True, + search_type="news", + country="US", + location="New York", + locale="en" + ) + assert tool.n_results == 5 + assert tool.save_file is True + assert tool.search_type == "news" + assert tool.country == "US" + assert tool.location == "New York" + assert tool.locale == "en" + + +@patch("requests.post") +def test_serper_tool_search(mock_post): + tool = SerperDevTool(n_results=2) + mock_response = { + "searchParameters": { + "q": "test query", + "type": "search" + }, + "organic": [ + { + "title": "Test Title 1", + "link": "http://test1.com", + "snippet": "Test Description 1", + "position": 1 + }, + { + "title": "Test Title 2", + "link": "http://test2.com", + "snippet": "Test Description 2", + "position": 2 + } + ], + "peopleAlsoAsk": [ + { + "question": "Test Question", + "snippet": "Test Answer", + "title": "Test Source", + "link": "http://test.com" + } + ] + } + mock_post.return_value.json.return_value = mock_response + mock_post.return_value.status_code = 200 + + result = tool.run(search_query="test query") + + assert "searchParameters" in result + assert result["searchParameters"]["q"] == "test query" + assert len(result["organic"]) == 2 + assert result["organic"][0]["title"] == "Test Title 1" + + +@patch("requests.post") +def test_serper_tool_news_search(mock_post): + tool = SerperDevTool(n_results=2, search_type="news") + mock_response = { + "searchParameters": { + "q": "test news", + "type": "news" + }, + "news": [ + { + "title": "News Title 1", + "link": "http://news1.com", + "snippet": "News Description 1", + "date": "2024-01-01", + "source": "News Source 1", + "imageUrl": "http://image1.com" + } + ] + } + mock_post.return_value.json.return_value = mock_response + mock_post.return_value.status_code = 200 + + result = tool.run(search_query="test news") + + assert "news" in result + assert len(result["news"]) == 1 + assert result["news"][0]["title"] == "News Title 1" + + +@patch("requests.post") +def test_serper_tool_with_location_params(mock_post): + tool = SerperDevTool( + n_results=2, + country="US", + location="New York", + locale="en" + ) + + tool.run(search_query="test") + + called_payload = mock_post.call_args.kwargs["json"] + assert called_payload["gl"] == "US" + assert called_payload["location"] == "New York" + assert called_payload["hl"] == "en" + + +def test_invalid_search_type(): + tool = SerperDevTool() + with pytest.raises(ValueError) as exc_info: + tool.run(search_query="test", search_type="invalid") + assert "Invalid search type" in str(exc_info.value) + + +@patch("requests.post") +def test_api_error_handling(mock_post): + tool = SerperDevTool() + mock_post.side_effect = Exception("API Error") + + with pytest.raises(Exception) as exc_info: + tool.run(search_query="test") + assert "API Error" in str(exc_info.value) + + +if __name__ == "__main__": + pytest.main([__file__]) diff --git a/tests/tools/singlestore_search_tool_test.py b/tests/tools/singlestore_search_tool_test.py new file mode 100644 index 000000000..fb0f22c14 --- /dev/null +++ b/tests/tools/singlestore_search_tool_test.py @@ -0,0 +1,336 @@ +import os +from typing import Generator + +import pytest +from singlestoredb import connect +from singlestoredb.server import docker + +from crewai_tools import SingleStoreSearchTool +from crewai_tools.tools.singlestore_search_tool import SingleStoreSearchToolSchema + + +@pytest.fixture(scope="session") +def docker_server_url() -> Generator[str, None, None]: + """Start a SingleStore Docker server for tests.""" + try: + sdb = docker.start(license="") + conn = sdb.connect() + curr = conn.cursor() + curr.execute("CREATE DATABASE test_crewai") + curr.close() + conn.close() + yield sdb.connection_url + sdb.stop() + except Exception as e: + pytest.skip(f"Could not start SingleStore Docker container: {e}") + + +@pytest.fixture(scope="function") +def clean_db_url(docker_server_url) -> Generator[str, None, None]: + """Provide a clean database URL and clean up tables after test.""" + yield docker_server_url + try: + conn = connect(host=docker_server_url, database="test_crewai") + curr = conn.cursor() + curr.execute("SHOW TABLES") + results = curr.fetchall() + for result in results: + curr.execute(f"DROP TABLE {result[0]}") + curr.close() + conn.close() + except Exception: + # Ignore cleanup errors + pass + + +@pytest.fixture +def sample_table_setup(clean_db_url): + """Set up sample tables for testing.""" + conn = connect(host=clean_db_url, database="test_crewai") + curr = conn.cursor() + + # Create sample tables + curr.execute( + """ + CREATE TABLE employees ( + id INT PRIMARY KEY, + name VARCHAR(100), + department VARCHAR(50), + salary DECIMAL(10,2) + ) + """ + ) + + curr.execute( + """ + CREATE TABLE departments ( + id INT PRIMARY KEY, + name VARCHAR(100), + budget DECIMAL(12,2) + ) + """ + ) + + # Insert sample data + curr.execute( + """ + INSERT INTO employees VALUES + (1, 'Alice Smith', 'Engineering', 75000.00), + (2, 'Bob Johnson', 'Marketing', 65000.00), + (3, 'Carol Davis', 'Engineering', 80000.00) + """ + ) + + curr.execute( + """ + INSERT INTO departments VALUES + (1, 'Engineering', 500000.00), + (2, 'Marketing', 300000.00) + """ + ) + + curr.close() + conn.close() + return clean_db_url + + +class TestSingleStoreSearchTool: + """Test suite for SingleStoreSearchTool.""" + + def test_tool_creation_with_connection_params(self, sample_table_setup): + """Test tool creation with individual connection parameters.""" + # Parse URL components for individual parameters + url_parts = sample_table_setup.split("@")[1].split(":") + host = url_parts[0] + port = int(url_parts[1].split("/")[0]) + user = "root" + password = sample_table_setup.split("@")[0].split(":")[2] + tool = SingleStoreSearchTool( + tables=[], + host=host, + port=port, + user=user, + password=password, + database="test_crewai", + ) + + assert tool.name == "Search a database's table(s) content" + assert "SingleStore" in tool.description + assert ( + "employees(id int(11), name varchar(100), department varchar(50), salary decimal(10,2))" + in tool.description.lower() + ) + assert ( + "departments(id int(11), name varchar(100), budget decimal(12,2))" + in tool.description.lower() + ) + assert tool.args_schema == SingleStoreSearchToolSchema + assert tool.connection_pool is not None + + def test_tool_creation_with_connection_url(self, sample_table_setup): + """Test tool creation with connection URL.""" + tool = SingleStoreSearchTool(host=f"{sample_table_setup}/test_crewai") + + assert tool.name == "Search a database's table(s) content" + assert tool.connection_pool is not None + + def test_tool_creation_with_specific_tables(self, sample_table_setup): + """Test tool creation with specific table list.""" + tool = SingleStoreSearchTool( + tables=["employees"], + host=sample_table_setup, + database="test_crewai", + ) + + # Check that description includes specific tables + assert "employees" in tool.description + assert "departments" not in tool.description + + def test_tool_creation_with_nonexistent_table(self, sample_table_setup): + """Test tool creation fails with non-existent table.""" + + with pytest.raises(ValueError, match="Table nonexistent does not exist"): + SingleStoreSearchTool( + tables=["employees", "nonexistent"], + host=sample_table_setup, + database="test_crewai", + ) + + def test_tool_creation_with_empty_database(self, clean_db_url): + """Test tool creation fails with empty database.""" + + with pytest.raises(ValueError, match="No tables found in the database"): + SingleStoreSearchTool(host=clean_db_url, database="test_crewai") + + def test_description_generation(self, sample_table_setup): + """Test that tool description is properly generated with table info.""" + + tool = SingleStoreSearchTool(host=sample_table_setup, database="test_crewai") + + # Check description contains table definitions + assert "employees(" in tool.description + assert "departments(" in tool.description + assert "id int" in tool.description.lower() + assert "name varchar" in tool.description.lower() + + def test_query_validation_select_allowed(self, sample_table_setup): + """Test that SELECT queries are allowed.""" + os.environ["SINGLESTOREDB_URL"] = sample_table_setup + tool = SingleStoreSearchTool(database="test_crewai") + + valid, message = tool._validate_query("SELECT * FROM employees") + assert valid is True + assert message == "Valid query" + + def test_query_validation_show_allowed(self, sample_table_setup): + """Test that SHOW queries are allowed.""" + tool = SingleStoreSearchTool(host=sample_table_setup, database="test_crewai") + + valid, message = tool._validate_query("SHOW TABLES") + assert valid is True + assert message == "Valid query" + + def test_query_validation_case_insensitive(self, sample_table_setup): + """Test that query validation is case insensitive.""" + tool = SingleStoreSearchTool(host=sample_table_setup, database="test_crewai") + + valid, _ = tool._validate_query("select * from employees") + assert valid is True + + valid, _ = tool._validate_query("SHOW tables") + assert valid is True + + def test_query_validation_insert_denied(self, sample_table_setup): + """Test that INSERT queries are denied.""" + tool = SingleStoreSearchTool(host=sample_table_setup, database="test_crewai") + + valid, message = tool._validate_query( + "INSERT INTO employees VALUES (4, 'Test', 'Test', 1000)" + ) + assert valid is False + assert "Only SELECT and SHOW queries are supported" in message + + def test_query_validation_update_denied(self, sample_table_setup): + """Test that UPDATE queries are denied.""" + tool = SingleStoreSearchTool(host=sample_table_setup, database="test_crewai") + + valid, message = tool._validate_query("UPDATE employees SET salary = 90000") + assert valid is False + assert "Only SELECT and SHOW queries are supported" in message + + def test_query_validation_delete_denied(self, sample_table_setup): + """Test that DELETE queries are denied.""" + tool = SingleStoreSearchTool(host=sample_table_setup, database="test_crewai") + + valid, message = tool._validate_query("DELETE FROM employees WHERE id = 1") + assert valid is False + assert "Only SELECT and SHOW queries are supported" in message + + def test_query_validation_non_string(self, sample_table_setup): + """Test that non-string queries are rejected.""" + tool = SingleStoreSearchTool(host=sample_table_setup, database="test_crewai") + + valid, message = tool._validate_query(123) + assert valid is False + assert "Search query must be a string" in message + + def test_run_select_query(self, sample_table_setup): + """Test executing a SELECT query.""" + tool = SingleStoreSearchTool(host=sample_table_setup, database="test_crewai") + + result = tool._run("SELECT * FROM employees ORDER BY id") + + assert "Search Results:" in result + assert "Alice Smith" in result + assert "Bob Johnson" in result + assert "Carol Davis" in result + + def test_run_filtered_query(self, sample_table_setup): + """Test executing a filtered SELECT query.""" + tool = SingleStoreSearchTool(host=sample_table_setup, database="test_crewai") + + result = tool._run( + "SELECT name FROM employees WHERE department = 'Engineering'" + ) + + assert "Search Results:" in result + assert "Alice Smith" in result + assert "Carol Davis" in result + assert "Bob Johnson" not in result + + def test_run_show_query(self, sample_table_setup): + """Test executing a SHOW query.""" + tool = SingleStoreSearchTool(host=sample_table_setup, database="test_crewai") + + result = tool._run("SHOW TABLES") + + assert "Search Results:" in result + assert "employees" in result + assert "departments" in result + + def test_run_empty_result(self, sample_table_setup): + """Test executing a query that returns no results.""" + tool = SingleStoreSearchTool(host=sample_table_setup, database="test_crewai") + + result = tool._run("SELECT * FROM employees WHERE department = 'NonExistent'") + + assert result == "No results found." + + def test_run_invalid_query_syntax(self, sample_table_setup): + """Test executing a query with invalid syntax.""" + tool = SingleStoreSearchTool(host=sample_table_setup, database="test_crewai") + + result = tool._run("SELECT * FORM employees") # Intentional typo + + assert "Error executing search query:" in result + + def test_run_denied_query(self, sample_table_setup): + """Test that denied queries return appropriate error message.""" + tool = SingleStoreSearchTool(host=sample_table_setup, database="test_crewai") + + result = tool._run("DELETE FROM employees") + + assert "Invalid search query:" in result + assert "Only SELECT and SHOW queries are supported" in result + + def test_connection_pool_usage(self, sample_table_setup): + """Test that connection pooling works correctly.""" + tool = SingleStoreSearchTool( + host=sample_table_setup, + database="test_crewai", + pool_size=2, + ) + + # Execute multiple queries to test pool usage + results = [] + for _ in range(5): + result = tool._run("SELECT COUNT(*) FROM employees") + results.append(result) + + # All queries should succeed + for result in results: + assert "Search Results:" in result + assert "3" in result # Count of employees + + def test_tool_schema_validation(self): + """Test that the tool schema validation works correctly.""" + # Valid input + valid_input = SingleStoreSearchToolSchema(search_query="SELECT * FROM test") + assert valid_input.search_query == "SELECT * FROM test" + + # Test that description is present + schema_dict = SingleStoreSearchToolSchema.model_json_schema() + assert "search_query" in schema_dict["properties"] + assert "description" in schema_dict["properties"]["search_query"] + + def test_connection_error_handling(self): + """Test handling of connection errors.""" + with pytest.raises(Exception): + # This should fail due to invalid connection parameters + SingleStoreSearchTool( + host="invalid_host", + port=9999, + user="invalid_user", + password="invalid_password", + database="invalid_db", + ) diff --git a/tests/tools/snowflake_search_tool_test.py b/tests/tools/snowflake_search_tool_test.py new file mode 100644 index 000000000..d4851b8ab --- /dev/null +++ b/tests/tools/snowflake_search_tool_test.py @@ -0,0 +1,103 @@ +import asyncio +from unittest.mock import MagicMock, patch + +import pytest + +from crewai_tools import SnowflakeConfig, SnowflakeSearchTool + + +# Unit Test Fixtures +@pytest.fixture +def mock_snowflake_connection(): + mock_conn = MagicMock() + mock_cursor = MagicMock() + mock_cursor.description = [("col1",), ("col2",)] + mock_cursor.fetchall.return_value = [(1, "value1"), (2, "value2")] + mock_cursor.execute.return_value = None + mock_conn.cursor.return_value = mock_cursor + return mock_conn + + +@pytest.fixture +def mock_config(): + return SnowflakeConfig( + account="test_account", + user="test_user", + password="test_password", + warehouse="test_warehouse", + database="test_db", + snowflake_schema="test_schema", + ) + + +@pytest.fixture +def snowflake_tool(mock_config): + with patch("snowflake.connector.connect") as mock_connect: + tool = SnowflakeSearchTool(config=mock_config) + yield tool + + +# Unit Tests +@pytest.mark.asyncio +async def test_successful_query_execution(snowflake_tool, mock_snowflake_connection): + with patch.object(snowflake_tool, "_create_connection") as mock_create_conn: + mock_create_conn.return_value = mock_snowflake_connection + + results = await snowflake_tool._run( + query="SELECT * FROM test_table", timeout=300 + ) + + assert len(results) == 2 + assert results[0]["col1"] == 1 + assert results[0]["col2"] == "value1" + mock_snowflake_connection.cursor.assert_called_once() + + +@pytest.mark.asyncio +async def test_connection_pooling(snowflake_tool, mock_snowflake_connection): + with patch.object(snowflake_tool, "_create_connection") as mock_create_conn: + mock_create_conn.return_value = mock_snowflake_connection + + # Execute multiple queries + await asyncio.gather( + snowflake_tool._run("SELECT 1"), + snowflake_tool._run("SELECT 2"), + snowflake_tool._run("SELECT 3"), + ) + + # Should reuse connections from pool + assert mock_create_conn.call_count <= snowflake_tool.pool_size + + +@pytest.mark.asyncio +async def test_cleanup_on_deletion(snowflake_tool, mock_snowflake_connection): + with patch.object(snowflake_tool, "_create_connection") as mock_create_conn: + mock_create_conn.return_value = mock_snowflake_connection + + # Add connection to pool + await snowflake_tool._get_connection() + + # Return connection to pool + async with snowflake_tool._pool_lock: + snowflake_tool._connection_pool.append(mock_snowflake_connection) + + # Trigger cleanup + snowflake_tool.__del__() + + mock_snowflake_connection.close.assert_called_once() + + +def test_config_validation(): + # Test missing required fields + with pytest.raises(ValueError): + SnowflakeConfig() + + # Test invalid account format + with pytest.raises(ValueError): + SnowflakeConfig( + account="invalid//account", user="test_user", password="test_pass" + ) + + # Test missing authentication + with pytest.raises(ValueError): + SnowflakeConfig(account="test_account", user="test_user") diff --git a/tests/tools/stagehand_tool_test.py b/tests/tools/stagehand_tool_test.py new file mode 100644 index 000000000..25c71c934 --- /dev/null +++ b/tests/tools/stagehand_tool_test.py @@ -0,0 +1,262 @@ +import sys +from unittest.mock import MagicMock, patch + +import pytest + +# Create mock classes that will be used by our fixture +class MockStagehandModule: + def __init__(self): + self.Stagehand = MagicMock() + self.StagehandConfig = MagicMock() + self.StagehandPage = MagicMock() + +class MockStagehandSchemas: + def __init__(self): + self.ActOptions = MagicMock() + self.ExtractOptions = MagicMock() + self.ObserveOptions = MagicMock() + self.AvailableModel = MagicMock() + +class MockStagehandUtils: + def __init__(self): + self.configure_logging = MagicMock() + +@pytest.fixture(scope="module", autouse=True) +def mock_stagehand_modules(): + """Mock stagehand modules at the start of this test module.""" + # Store original modules if they exist + original_modules = {} + for module_name in ["stagehand", "stagehand.schemas", "stagehand.utils"]: + if module_name in sys.modules: + original_modules[module_name] = sys.modules[module_name] + + # Create and inject mock modules + mock_stagehand = MockStagehandModule() + mock_stagehand_schemas = MockStagehandSchemas() + mock_stagehand_utils = MockStagehandUtils() + + sys.modules["stagehand"] = mock_stagehand + sys.modules["stagehand.schemas"] = mock_stagehand_schemas + sys.modules["stagehand.utils"] = mock_stagehand_utils + + # Import after mocking + from crewai_tools.tools.stagehand_tool.stagehand_tool import StagehandResult, StagehandTool + + # Make these available to tests in this module + sys.modules[__name__].StagehandResult = StagehandResult + sys.modules[__name__].StagehandTool = StagehandTool + + yield + + # Restore original modules + for module_name, module in original_modules.items(): + sys.modules[module_name] = module + + +class MockStagehandPage(MagicMock): + def act(self, options): + mock_result = MagicMock() + mock_result.model_dump.return_value = { + "message": "Action completed successfully" + } + return mock_result + + def goto(self, url): + return MagicMock() + + def extract(self, options): + mock_result = MagicMock() + mock_result.model_dump.return_value = { + "data": "Extracted content", + "metadata": {"source": "test"}, + } + return mock_result + + def observe(self, options): + result1 = MagicMock() + result1.description = "Button element" + result1.method = "click" + + result2 = MagicMock() + result2.description = "Input field" + result2.method = "type" + + return [result1, result2] + + +class MockStagehand(MagicMock): + def init(self): + self.session_id = "test-session-id" + self.page = MockStagehandPage() + + def close(self): + pass + + +@pytest.fixture +def mock_stagehand_instance(): + with patch( + "crewai_tools.tools.stagehand_tool.stagehand_tool.Stagehand", + return_value=MockStagehand(), + ) as mock: + yield mock + + +@pytest.fixture +def stagehand_tool(): + return StagehandTool( + api_key="test_api_key", + project_id="test_project_id", + model_api_key="test_model_api_key", + _testing=True, # Enable testing mode to bypass dependency check + ) + + +def test_stagehand_tool_initialization(): + """Test that the StagehandTool initializes with the correct default values.""" + tool = StagehandTool( + api_key="test_api_key", + project_id="test_project_id", + model_api_key="test_model_api_key", + _testing=True, # Enable testing mode + ) + + assert tool.api_key == "test_api_key" + assert tool.project_id == "test_project_id" + assert tool.model_api_key == "test_model_api_key" + assert tool.headless is False + assert tool.dom_settle_timeout_ms == 3000 + assert tool.self_heal is True + assert tool.wait_for_captcha_solves is True + + +@patch("crewai_tools.tools.stagehand_tool.stagehand_tool.StagehandTool._run", autospec=True) +def test_act_command(mock_run, stagehand_tool): + """Test the 'act' command functionality.""" + # Setup mock + mock_run.return_value = "Action result: Action completed successfully" + + # Run the tool + result = stagehand_tool._run( + instruction="Click the submit button", command_type="act" + ) + + # Assertions + assert "Action result" in result + assert "Action completed successfully" in result + + +@patch("crewai_tools.tools.stagehand_tool.stagehand_tool.StagehandTool._run", autospec=True) +def test_navigate_command(mock_run, stagehand_tool): + """Test the 'navigate' command functionality.""" + # Setup mock + mock_run.return_value = "Successfully navigated to https://example.com" + + # Run the tool + result = stagehand_tool._run( + instruction="Go to example.com", + url="https://example.com", + command_type="navigate", + ) + + # Assertions + assert "https://example.com" in result + + +@patch("crewai_tools.tools.stagehand_tool.stagehand_tool.StagehandTool._run", autospec=True) +def test_extract_command(mock_run, stagehand_tool): + """Test the 'extract' command functionality.""" + # Setup mock + mock_run.return_value = "Extracted data: {\"data\": \"Extracted content\", \"metadata\": {\"source\": \"test\"}}" + + # Run the tool + result = stagehand_tool._run( + instruction="Extract all product names and prices", command_type="extract" + ) + + # Assertions + assert "Extracted data" in result + assert "Extracted content" in result + + +@patch("crewai_tools.tools.stagehand_tool.stagehand_tool.StagehandTool._run", autospec=True) +def test_observe_command(mock_run, stagehand_tool): + """Test the 'observe' command functionality.""" + # Setup mock + mock_run.return_value = "Element 1: Button element\nSuggested action: click\nElement 2: Input field\nSuggested action: type" + + # Run the tool + result = stagehand_tool._run( + instruction="Find all interactive elements", command_type="observe" + ) + + # Assertions + assert "Element 1: Button element" in result + assert "Element 2: Input field" in result + assert "Suggested action: click" in result + assert "Suggested action: type" in result + + +@patch("crewai_tools.tools.stagehand_tool.stagehand_tool.StagehandTool._run", autospec=True) +def test_error_handling(mock_run, stagehand_tool): + """Test error handling in the tool.""" + # Setup mock + mock_run.return_value = "Error: Browser automation error" + + # Run the tool + result = stagehand_tool._run( + instruction="Click a non-existent button", command_type="act" + ) + + # Assertions + assert "Error:" in result + assert "Browser automation error" in result + + +def test_initialization_parameters(): + """Test that the StagehandTool initializes with the correct parameters.""" + # Create tool with custom parameters + tool = StagehandTool( + api_key="custom_api_key", + project_id="custom_project_id", + model_api_key="custom_model_api_key", + headless=True, + dom_settle_timeout_ms=5000, + self_heal=False, + wait_for_captcha_solves=False, + verbose=3, + _testing=True, # Enable testing mode + ) + + # Verify the tool was initialized with the correct parameters + assert tool.api_key == "custom_api_key" + assert tool.project_id == "custom_project_id" + assert tool.model_api_key == "custom_model_api_key" + assert tool.headless is True + assert tool.dom_settle_timeout_ms == 5000 + assert tool.self_heal is False + assert tool.wait_for_captcha_solves is False + assert tool.verbose == 3 + + +def test_close_method(): + """Test that the close method cleans up resources correctly.""" + # Create the tool with testing mode + tool = StagehandTool( + api_key="test_api_key", + project_id="test_project_id", + model_api_key="test_model_api_key", + _testing=True, + ) + + # Setup mock stagehand instance + tool._stagehand = MagicMock() + tool._stagehand.close = MagicMock() # Non-async mock + tool._page = MagicMock() + + # Call the close method + tool.close() + + # Verify resources were cleaned up + assert tool._stagehand is None + assert tool._page is None diff --git a/tests/tools/test_code_interpreter_tool.py b/tests/tools/test_code_interpreter_tool.py new file mode 100644 index 000000000..e46c8bde4 --- /dev/null +++ b/tests/tools/test_code_interpreter_tool.py @@ -0,0 +1,175 @@ +from unittest.mock import patch + +import pytest + +from crewai_tools.tools.code_interpreter_tool.code_interpreter_tool import ( + CodeInterpreterTool, + SandboxPython, +) + + +@pytest.fixture +def printer_mock(): + with patch("crewai_tools.printer.Printer.print") as mock: + yield mock + + +@pytest.fixture +def docker_unavailable_mock(): + with patch( + "crewai_tools.tools.code_interpreter_tool.code_interpreter_tool.CodeInterpreterTool._check_docker_available", + return_value=False, + ) as mock: + yield mock + + +@patch("crewai_tools.tools.code_interpreter_tool.code_interpreter_tool.docker_from_env") +def test_run_code_in_docker(docker_mock, printer_mock): + tool = CodeInterpreterTool() + code = "print('Hello, World!')" + libraries_used = ["numpy", "pandas"] + expected_output = "Hello, World!\n" + + docker_mock().containers.run().exec_run().exit_code = 0 + docker_mock().containers.run().exec_run().output = expected_output.encode() + + result = tool.run_code_in_docker(code, libraries_used) + assert result == expected_output + printer_mock.assert_called_with( + "Running code in Docker environment", color="bold_blue" + ) + + +@patch("crewai_tools.tools.code_interpreter_tool.code_interpreter_tool.docker_from_env") +def test_run_code_in_docker_with_error(docker_mock, printer_mock): + tool = CodeInterpreterTool() + code = "print(1/0)" + libraries_used = ["numpy", "pandas"] + expected_output = "Something went wrong while running the code: \nZeroDivisionError: division by zero\n" + + docker_mock().containers.run().exec_run().exit_code = 1 + docker_mock().containers.run().exec_run().output = ( + b"ZeroDivisionError: division by zero\n" + ) + + result = tool.run_code_in_docker(code, libraries_used) + assert result == expected_output + printer_mock.assert_called_with( + "Running code in Docker environment", color="bold_blue" + ) + + +@patch("crewai_tools.tools.code_interpreter_tool.code_interpreter_tool.docker_from_env") +def test_run_code_in_docker_with_script(docker_mock, printer_mock): + tool = CodeInterpreterTool() + code = """print("This is line 1") +print("This is line 2")""" + libraries_used = [] + expected_output = "This is line 1\nThis is line 2\n" + + docker_mock().containers.run().exec_run().exit_code = 0 + docker_mock().containers.run().exec_run().output = expected_output.encode() + + result = tool.run_code_in_docker(code, libraries_used) + assert result == expected_output + printer_mock.assert_called_with( + "Running code in Docker environment", color="bold_blue" + ) + + +def test_restricted_sandbox_basic_code_execution(printer_mock, docker_unavailable_mock): + """Test basic code execution.""" + tool = CodeInterpreterTool() + code = """ +result = 2 + 2 +print(result) +""" + result = tool.run(code=code, libraries_used=[]) + printer_mock.assert_called_with( + "Running code in restricted sandbox", color="yellow" + ) + assert result == 4 + + +def test_restricted_sandbox_running_with_blocked_modules( + printer_mock, docker_unavailable_mock +): + """Test that restricted modules cannot be imported.""" + tool = CodeInterpreterTool() + restricted_modules = SandboxPython.BLOCKED_MODULES + + for module in restricted_modules: + code = f""" +import {module} +result = "Import succeeded" +""" + result = tool.run(code=code, libraries_used=[]) + printer_mock.assert_called_with( + "Running code in restricted sandbox", color="yellow" + ) + + assert f"An error occurred: Importing '{module}' is not allowed" in result + + +def test_restricted_sandbox_running_with_blocked_builtins( + printer_mock, docker_unavailable_mock +): + """Test that restricted builtins are not available.""" + tool = CodeInterpreterTool() + restricted_builtins = SandboxPython.UNSAFE_BUILTINS + + for builtin in restricted_builtins: + code = f""" +{builtin}("test") +result = "Builtin available" +""" + result = tool.run(code=code, libraries_used=[]) + printer_mock.assert_called_with( + "Running code in restricted sandbox", color="yellow" + ) + assert f"An error occurred: name '{builtin}' is not defined" in result + + +def test_restricted_sandbox_running_with_no_result_variable( + printer_mock, docker_unavailable_mock +): + """Test behavior when no result variable is set.""" + tool = CodeInterpreterTool() + code = """ +x = 10 +""" + result = tool.run(code=code, libraries_used=[]) + printer_mock.assert_called_with( + "Running code in restricted sandbox", color="yellow" + ) + assert result == "No result variable found." + + +def test_unsafe_mode_running_with_no_result_variable( + printer_mock, docker_unavailable_mock +): + """Test behavior when no result variable is set.""" + tool = CodeInterpreterTool(unsafe_mode=True) + code = """ +x = 10 +""" + result = tool.run(code=code, libraries_used=[]) + printer_mock.assert_called_with( + "WARNING: Running code in unsafe mode", color="bold_magenta" + ) + assert result == "No result variable found." + + +def test_unsafe_mode_running_unsafe_code(printer_mock, docker_unavailable_mock): + """Test behavior when no result variable is set.""" + tool = CodeInterpreterTool(unsafe_mode=True) + code = """ +import os +os.system("ls -la") +result = eval("5/1") +""" + result = tool.run(code=code, libraries_used=[]) + printer_mock.assert_called_with( + "WARNING: Running code in unsafe mode", color="bold_magenta" + ) + assert 5.0 == result diff --git a/tests/tools/test_import_without_warnings.py b/tests/tools/test_import_without_warnings.py new file mode 100644 index 000000000..5635832ed --- /dev/null +++ b/tests/tools/test_import_without_warnings.py @@ -0,0 +1,10 @@ +import pytest +from pydantic.warnings import PydanticDeprecatedSince20 + + +@pytest.mark.filterwarnings("error", category=PydanticDeprecatedSince20) +def test_import_tools_without_pydantic_deprecation_warnings(): + # This test is to ensure that the import of crewai_tools does not raise any Pydantic deprecation warnings. + import crewai_tools + + assert crewai_tools diff --git a/tests/tools/test_mongodb_vector_search_tool.py b/tests/tools/test_mongodb_vector_search_tool.py new file mode 100644 index 000000000..b76debbde --- /dev/null +++ b/tests/tools/test_mongodb_vector_search_tool.py @@ -0,0 +1,75 @@ +import json +from unittest.mock import patch + +import pytest + +from crewai_tools import MongoDBVectorSearchConfig, MongoDBVectorSearchTool + + +# Unit Test Fixtures +@pytest.fixture +def mongodb_vector_search_tool(): + tool = MongoDBVectorSearchTool( + connection_string="foo", database_name="bar", collection_name="test" + ) + tool._embed_texts = lambda x: [[0.1]] + yield tool + + +# Unit Tests +def test_successful_query_execution(mongodb_vector_search_tool): + # Enable embedding + with patch.object(mongodb_vector_search_tool._coll, "aggregate") as mock_aggregate: + mock_aggregate.return_value = [dict(text="foo", score=0.1, _id=1)] + + results = json.loads(mongodb_vector_search_tool._run(query="sandwiches")) + + assert len(results) == 1 + assert results[0]["text"] == "foo" + assert results[0]["_id"] == 1 + + +def test_provide_config(): + query_config = MongoDBVectorSearchConfig(limit=10) + tool = MongoDBVectorSearchTool( + connection_string="foo", + database_name="bar", + collection_name="test", + query_config=query_config, + vector_index_name="foo", + embedding_model="bar", + ) + tool._embed_texts = lambda x: [[0.1]] + with patch.object(tool._coll, "aggregate") as mock_aggregate: + mock_aggregate.return_value = [dict(text="foo", score=0.1, _id=1)] + + tool._run(query="sandwiches") + assert mock_aggregate.mock_calls[-1].args[0][0]["$vectorSearch"]["limit"] == 10 + + mock_aggregate.return_value = [dict(text="foo", score=0.1, _id=1)] + + +def test_cleanup_on_deletion(mongodb_vector_search_tool): + with patch.object(mongodb_vector_search_tool, "_client") as mock_client: + # Trigger cleanup + mongodb_vector_search_tool.__del__() + + mock_client.close.assert_called_once() + + +def test_create_search_index(mongodb_vector_search_tool): + with patch( + "crewai_tools.tools.mongodb_vector_search_tool.vector_search.create_vector_search_index" + ) as mock_create_search_index: + mongodb_vector_search_tool.create_vector_search_index(dimensions=10) + kwargs = mock_create_search_index.mock_calls[0].kwargs + assert kwargs["dimensions"] == 10 + assert kwargs["similarity"] == "cosine" + + +def test_add_texts(mongodb_vector_search_tool): + with patch.object(mongodb_vector_search_tool._coll, "bulk_write") as bulk_write: + mongodb_vector_search_tool.add_texts(["foo"]) + args = bulk_write.mock_calls[0].args + assert "ReplaceOne" in str(args[0][0]) + assert "foo" in str(args[0][0]) diff --git a/tests/tools/test_oxylabs_tools.py b/tests/tools/test_oxylabs_tools.py new file mode 100644 index 000000000..3fd3feca3 --- /dev/null +++ b/tests/tools/test_oxylabs_tools.py @@ -0,0 +1,163 @@ +import json +import os +from typing import Type +from unittest.mock import MagicMock + +import pytest +from crewai.tools.base_tool import BaseTool +from oxylabs import RealtimeClient +from oxylabs.sources.response import Response as OxylabsResponse +from pydantic import BaseModel + +from crewai_tools import ( + OxylabsAmazonProductScraperTool, + OxylabsAmazonSearchScraperTool, + OxylabsGoogleSearchScraperTool, + OxylabsUniversalScraperTool, +) +from crewai_tools.tools.oxylabs_amazon_product_scraper_tool.oxylabs_amazon_product_scraper_tool import ( + OxylabsAmazonProductScraperConfig, +) +from crewai_tools.tools.oxylabs_google_search_scraper_tool.oxylabs_google_search_scraper_tool import ( + OxylabsGoogleSearchScraperConfig, +) + + +@pytest.fixture +def oxylabs_api() -> RealtimeClient: + oxylabs_api_mock = MagicMock() + + html_content = """ + + + + + Scraping Sandbox + + +
+
+
+

Amazing product

+

Price $14.99

+
+
+

Good product

+

Price $9.99

+
+
+
+ + + """ + + json_content = { + "results": { + "products": [ + {"title": "Amazing product", "price": 14.99, "currency": "USD"}, + {"title": "Good product", "price": 9.99, "currency": "USD"}, + ], + }, + } + + html_response = OxylabsResponse({"results": [{"content": html_content}]}) + json_response = OxylabsResponse({"results": [{"content": json_content}]}) + + oxylabs_api_mock.universal.scrape_url.side_effect = [json_response, html_response] + oxylabs_api_mock.amazon.scrape_search.side_effect = [json_response, html_response] + oxylabs_api_mock.amazon.scrape_product.side_effect = [json_response, html_response] + oxylabs_api_mock.google.scrape_search.side_effect = [json_response, html_response] + + return oxylabs_api_mock + + +@pytest.mark.parametrize( + ("tool_class",), + [ + (OxylabsUniversalScraperTool,), + (OxylabsAmazonSearchScraperTool,), + (OxylabsGoogleSearchScraperTool,), + (OxylabsAmazonProductScraperTool,), + ], +) +def test_tool_initialization(tool_class: Type[BaseTool]): + tool = tool_class(username="username", password="password") + assert isinstance(tool, tool_class) + + +@pytest.mark.parametrize( + ("tool_class",), + [ + (OxylabsUniversalScraperTool,), + (OxylabsAmazonSearchScraperTool,), + (OxylabsGoogleSearchScraperTool,), + (OxylabsAmazonProductScraperTool,), + ], +) +def test_tool_initialization_with_env_vars(tool_class: Type[BaseTool]): + os.environ["OXYLABS_USERNAME"] = "username" + os.environ["OXYLABS_PASSWORD"] = "password" + + tool = tool_class() + assert isinstance(tool, tool_class) + + del os.environ["OXYLABS_USERNAME"] + del os.environ["OXYLABS_PASSWORD"] + + +@pytest.mark.parametrize( + ("tool_class",), + [ + (OxylabsUniversalScraperTool,), + (OxylabsAmazonSearchScraperTool,), + (OxylabsGoogleSearchScraperTool,), + (OxylabsAmazonProductScraperTool,), + ], +) +def test_tool_initialization_failure(tool_class: Type[BaseTool]): + # making sure env vars are not set + for key in ["OXYLABS_USERNAME", "OXYLABS_PASSWORD"]: + if key in os.environ: + del os.environ[key] + + with pytest.raises(ValueError): + tool_class() + + +@pytest.mark.parametrize( + ("tool_class", "tool_config"), + [ + (OxylabsUniversalScraperTool, {"geo_location": "Paris, France"}), + ( + OxylabsAmazonSearchScraperTool, + {"domain": "co.uk"}, + ), + ( + OxylabsGoogleSearchScraperTool, + OxylabsGoogleSearchScraperConfig(render="html"), + ), + ( + OxylabsAmazonProductScraperTool, + OxylabsAmazonProductScraperConfig(parse=True), + ), + ], +) +def test_tool_invocation( + tool_class: Type[BaseTool], + tool_config: BaseModel, + oxylabs_api: RealtimeClient, +): + tool = tool_class(username="username", password="password", config=tool_config) + + # setting via __dict__ to bypass pydantic validation + tool.__dict__["oxylabs_api"] = oxylabs_api + + # verifying parsed job returns json content + result = tool.run("Scraping Query 1") + assert isinstance(result, str) + assert isinstance(json.loads(result), dict) + + # verifying raw job returns str + result = tool.run("Scraping Query 2") + assert isinstance(result, str) + assert "" in result diff --git a/tests/tools/test_search_tools.py b/tests/tools/test_search_tools.py new file mode 100644 index 000000000..eaa0c591c --- /dev/null +++ b/tests/tools/test_search_tools.py @@ -0,0 +1,309 @@ +import os +import tempfile +from pathlib import Path +from unittest.mock import ANY, MagicMock + +import pytest +from embedchain.models.data_type import DataType + +from crewai_tools.tools import ( + CodeDocsSearchTool, + CSVSearchTool, + DirectorySearchTool, + DOCXSearchTool, + GithubSearchTool, + JSONSearchTool, + MDXSearchTool, + PDFSearchTool, + TXTSearchTool, + WebsiteSearchTool, + XMLSearchTool, + YoutubeChannelSearchTool, + YoutubeVideoSearchTool, +) +from crewai_tools.tools.rag.rag_tool import Adapter + +pytestmark = [pytest.mark.vcr(filter_headers=["authorization"])] + + +@pytest.fixture +def mock_adapter(): + mock_adapter = MagicMock(spec=Adapter) + return mock_adapter + + +def test_directory_search_tool(): + with tempfile.TemporaryDirectory() as temp_dir: + test_file = Path(temp_dir) / "test.txt" + test_file.write_text("This is a test file for directory search") + + tool = DirectorySearchTool(directory=temp_dir) + result = tool._run(search_query="test file") + assert "test file" in result.lower() + + +def test_pdf_search_tool(mock_adapter): + mock_adapter.query.return_value = "this is a test" + + tool = PDFSearchTool(pdf="test.pdf", adapter=mock_adapter) + result = tool._run(query="test content") + assert "this is a test" in result.lower() + mock_adapter.add.assert_called_once_with("test.pdf", data_type=DataType.PDF_FILE) + mock_adapter.query.assert_called_once_with("test content") + + mock_adapter.query.reset_mock() + mock_adapter.add.reset_mock() + + tool = PDFSearchTool(adapter=mock_adapter) + result = tool._run(pdf="test.pdf", query="test content") + assert "this is a test" in result.lower() + mock_adapter.add.assert_called_once_with("test.pdf", data_type=DataType.PDF_FILE) + mock_adapter.query.assert_called_once_with("test content") + + +def test_txt_search_tool(): + with tempfile.NamedTemporaryFile(suffix=".txt", delete=False) as temp_file: + temp_file.write(b"This is a test file for txt search") + temp_file_path = temp_file.name + + try: + tool = TXTSearchTool() + tool.add(temp_file_path) + result = tool._run(search_query="test file") + assert "test file" in result.lower() + finally: + os.unlink(temp_file_path) + + +def test_docx_search_tool(mock_adapter): + mock_adapter.query.return_value = "this is a test" + + tool = DOCXSearchTool(docx="test.docx", adapter=mock_adapter) + result = tool._run(search_query="test content") + assert "this is a test" in result.lower() + mock_adapter.add.assert_called_once_with("test.docx", data_type=DataType.DOCX) + mock_adapter.query.assert_called_once_with("test content") + + mock_adapter.query.reset_mock() + mock_adapter.add.reset_mock() + + tool = DOCXSearchTool(adapter=mock_adapter) + result = tool._run(docx="test.docx", search_query="test content") + assert "this is a test" in result.lower() + mock_adapter.add.assert_called_once_with("test.docx", data_type=DataType.DOCX) + mock_adapter.query.assert_called_once_with("test content") + + +def test_json_search_tool(): + with tempfile.NamedTemporaryFile(suffix=".json", delete=False) as temp_file: + temp_file.write(b'{"test": "This is a test JSON file"}') + temp_file_path = temp_file.name + + try: + tool = JSONSearchTool() + result = tool._run(search_query="test JSON", json_path=temp_file_path) + assert "test json" in result.lower() + finally: + os.unlink(temp_file_path) + + +def test_xml_search_tool(mock_adapter): + mock_adapter.query.return_value = "this is a test" + + tool = XMLSearchTool(adapter=mock_adapter) + result = tool._run(search_query="test XML", xml="test.xml") + assert "this is a test" in result.lower() + mock_adapter.add.assert_called_once_with("test.xml") + mock_adapter.query.assert_called_once_with("test XML") + + +def test_csv_search_tool(): + with tempfile.NamedTemporaryFile(suffix=".csv", delete=False) as temp_file: + temp_file.write(b"name,description\ntest,This is a test CSV file") + temp_file_path = temp_file.name + + try: + tool = CSVSearchTool() + tool.add(temp_file_path) + result = tool._run(search_query="test CSV") + assert "test csv" in result.lower() + finally: + os.unlink(temp_file_path) + + +def test_mdx_search_tool(): + with tempfile.NamedTemporaryFile(suffix=".mdx", delete=False) as temp_file: + temp_file.write(b"# Test MDX\nThis is a test MDX file") + temp_file_path = temp_file.name + + try: + tool = MDXSearchTool() + tool.add(temp_file_path) + result = tool._run(search_query="test MDX") + assert "test mdx" in result.lower() + finally: + os.unlink(temp_file_path) + + +def test_website_search_tool(mock_adapter): + mock_adapter.query.return_value = "this is a test" + + website = "https://crewai.com" + search_query = "what is crewai?" + tool = WebsiteSearchTool(website=website, adapter=mock_adapter) + result = tool._run(search_query=search_query) + + mock_adapter.query.assert_called_once_with("what is crewai?") + mock_adapter.add.assert_called_once_with(website, data_type=DataType.WEB_PAGE) + + assert "this is a test" in result.lower() + + mock_adapter.query.reset_mock() + mock_adapter.add.reset_mock() + + tool = WebsiteSearchTool(adapter=mock_adapter) + result = tool._run(website=website, search_query=search_query) + + mock_adapter.query.assert_called_once_with("what is crewai?") + mock_adapter.add.assert_called_once_with(website, data_type=DataType.WEB_PAGE) + + assert "this is a test" in result.lower() + + +def test_youtube_video_search_tool(mock_adapter): + mock_adapter.query.return_value = "some video description" + + youtube_video_url = "https://www.youtube.com/watch?v=sample-video-id" + search_query = "what is the video about?" + tool = YoutubeVideoSearchTool( + youtube_video_url=youtube_video_url, + adapter=mock_adapter, + ) + result = tool._run(search_query=search_query) + assert "some video description" in result + + mock_adapter.add.assert_called_once_with( + youtube_video_url, data_type=DataType.YOUTUBE_VIDEO + ) + mock_adapter.query.assert_called_once_with(search_query) + + mock_adapter.query.reset_mock() + mock_adapter.add.reset_mock() + + tool = YoutubeVideoSearchTool(adapter=mock_adapter) + result = tool._run(youtube_video_url=youtube_video_url, search_query=search_query) + assert "some video description" in result + + mock_adapter.add.assert_called_once_with( + youtube_video_url, data_type=DataType.YOUTUBE_VIDEO + ) + mock_adapter.query.assert_called_once_with(search_query) + + +def test_youtube_channel_search_tool(mock_adapter): + mock_adapter.query.return_value = "channel description" + + youtube_channel_handle = "@crewai" + search_query = "what is the channel about?" + tool = YoutubeChannelSearchTool( + youtube_channel_handle=youtube_channel_handle, adapter=mock_adapter + ) + result = tool._run(search_query=search_query) + assert "channel description" in result + mock_adapter.add.assert_called_once_with( + youtube_channel_handle, data_type=DataType.YOUTUBE_CHANNEL + ) + mock_adapter.query.assert_called_once_with(search_query) + + mock_adapter.query.reset_mock() + mock_adapter.add.reset_mock() + + tool = YoutubeChannelSearchTool(adapter=mock_adapter) + result = tool._run( + youtube_channel_handle=youtube_channel_handle, search_query=search_query + ) + assert "channel description" in result + + mock_adapter.add.assert_called_once_with( + youtube_channel_handle, data_type=DataType.YOUTUBE_CHANNEL + ) + mock_adapter.query.assert_called_once_with(search_query) + + +def test_code_docs_search_tool(mock_adapter): + mock_adapter.query.return_value = "test documentation" + + docs_url = "https://crewai.com/any-docs-url" + search_query = "test documentation" + tool = CodeDocsSearchTool(docs_url=docs_url, adapter=mock_adapter) + result = tool._run(search_query=search_query) + assert "test documentation" in result + mock_adapter.add.assert_called_once_with(docs_url, data_type=DataType.DOCS_SITE) + mock_adapter.query.assert_called_once_with(search_query) + + mock_adapter.query.reset_mock() + mock_adapter.add.reset_mock() + + tool = CodeDocsSearchTool(adapter=mock_adapter) + result = tool._run(docs_url=docs_url, search_query=search_query) + assert "test documentation" in result + mock_adapter.add.assert_called_once_with(docs_url, data_type=DataType.DOCS_SITE) + mock_adapter.query.assert_called_once_with(search_query) + + +def test_github_search_tool(mock_adapter): + mock_adapter.query.return_value = "repo description" + + # ensure the provided repo and content types are used after initialization + tool = GithubSearchTool( + gh_token="test_token", + github_repo="crewai/crewai", + content_types=["code"], + adapter=mock_adapter, + ) + result = tool._run(search_query="tell me about crewai repo") + assert "repo description" in result + mock_adapter.add.assert_called_once_with( + "repo:crewai/crewai type:code", data_type="github", loader=ANY + ) + mock_adapter.query.assert_called_once_with("tell me about crewai repo") + + # ensure content types provided by run call is used + mock_adapter.query.reset_mock() + mock_adapter.add.reset_mock() + + tool = GithubSearchTool(gh_token="test_token", adapter=mock_adapter) + result = tool._run( + github_repo="crewai/crewai", + content_types=["code", "issue"], + search_query="tell me about crewai repo", + ) + assert "repo description" in result + mock_adapter.add.assert_called_once_with( + "repo:crewai/crewai type:code,issue", data_type="github", loader=ANY + ) + mock_adapter.query.assert_called_once_with("tell me about crewai repo") + + # ensure default content types are used if not provided + mock_adapter.query.reset_mock() + mock_adapter.add.reset_mock() + + tool = GithubSearchTool(gh_token="test_token", adapter=mock_adapter) + result = tool._run( + github_repo="crewai/crewai", + search_query="tell me about crewai repo", + ) + assert "repo description" in result + mock_adapter.add.assert_called_once_with( + "repo:crewai/crewai type:code,repo,pr,issue", data_type="github", loader=ANY + ) + mock_adapter.query.assert_called_once_with("tell me about crewai repo") + + # ensure nothing is added if no repo is provided + mock_adapter.query.reset_mock() + mock_adapter.add.reset_mock() + + tool = GithubSearchTool(gh_token="test_token", adapter=mock_adapter) + result = tool._run(search_query="tell me about crewai repo") + mock_adapter.add.assert_not_called() + mock_adapter.query.assert_called_once_with("tell me about crewai repo") diff --git a/tests/tools/tool_collection_test.py b/tests/tools/tool_collection_test.py new file mode 100644 index 000000000..e409a4e76 --- /dev/null +++ b/tests/tools/tool_collection_test.py @@ -0,0 +1,231 @@ +import unittest +from unittest.mock import MagicMock + +from crewai.tools import BaseTool +from crewai_tools.adapters.tool_collection import ToolCollection + + +class TestToolCollection(unittest.TestCase): + def setUp(self): + + self.search_tool = self._create_mock_tool("SearcH", "Search Tool") # Tool name is case sensitive + self.calculator_tool = self._create_mock_tool("calculator", "Calculator Tool") + self.translator_tool = self._create_mock_tool("translator", "Translator Tool") + + self.tools = ToolCollection([ + self.search_tool, + self.calculator_tool, + self.translator_tool + ]) + + def _create_mock_tool(self, name, description): + mock_tool = MagicMock(spec=BaseTool) + mock_tool.name = name + mock_tool.description = description + return mock_tool + + def test_initialization(self): + self.assertEqual(len(self.tools), 3) + self.assertEqual(self.tools[0].name, "SearcH") + self.assertEqual(self.tools[1].name, "calculator") + self.assertEqual(self.tools[2].name, "translator") + + def test_empty_initialization(self): + empty_collection = ToolCollection() + self.assertEqual(len(empty_collection), 0) + self.assertEqual(empty_collection._name_cache, {}) + + def test_initialization_with_none(self): + collection = ToolCollection(None) + self.assertEqual(len(collection), 0) + self.assertEqual(collection._name_cache, {}) + + def test_access_by_index(self): + self.assertEqual(self.tools[0], self.search_tool) + self.assertEqual(self.tools[1], self.calculator_tool) + self.assertEqual(self.tools[2], self.translator_tool) + + def test_access_by_name(self): + self.assertEqual(self.tools["search"], self.search_tool) + self.assertEqual(self.tools["calculator"], self.calculator_tool) + self.assertEqual(self.tools["translator"], self.translator_tool) + + def test_key_error_for_invalid_name(self): + with self.assertRaises(KeyError): + _ = self.tools["nonexistent"] + + def test_index_error_for_invalid_index(self): + with self.assertRaises(IndexError): + _ = self.tools[10] + + def test_negative_index(self): + self.assertEqual(self.tools[-1], self.translator_tool) + self.assertEqual(self.tools[-2], self.calculator_tool) + self.assertEqual(self.tools[-3], self.search_tool) + + def test_append(self): + new_tool = self._create_mock_tool("new", "New Tool") + self.tools.append(new_tool) + + self.assertEqual(len(self.tools), 4) + self.assertEqual(self.tools[3], new_tool) + self.assertEqual(self.tools["new"], new_tool) + self.assertIn("new", self.tools._name_cache) + + def test_append_duplicate_name(self): + duplicate_tool = self._create_mock_tool("search", "Duplicate Search Tool") + self.tools.append(duplicate_tool) + + self.assertEqual(len(self.tools), 4) + self.assertEqual(self.tools["search"], duplicate_tool) + + def test_extend(self): + new_tools = [ + self._create_mock_tool("tool4", "Tool 4"), + self._create_mock_tool("tool5", "Tool 5"), + ] + self.tools.extend(new_tools) + + self.assertEqual(len(self.tools), 5) + self.assertEqual(self.tools["tool4"], new_tools[0]) + self.assertEqual(self.tools["tool5"], new_tools[1]) + self.assertIn("tool4", self.tools._name_cache) + self.assertIn("tool5", self.tools._name_cache) + + def test_insert(self): + new_tool = self._create_mock_tool("inserted", "Inserted Tool") + self.tools.insert(1, new_tool) + + self.assertEqual(len(self.tools), 4) + self.assertEqual(self.tools[1], new_tool) + self.assertEqual(self.tools["inserted"], new_tool) + self.assertIn("inserted", self.tools._name_cache) + + def test_remove(self): + self.tools.remove(self.calculator_tool) + + self.assertEqual(len(self.tools), 2) + with self.assertRaises(KeyError): + _ = self.tools["calculator"] + self.assertNotIn("calculator", self.tools._name_cache) + + def test_remove_nonexistent_tool(self): + nonexistent_tool = self._create_mock_tool("nonexistent", "Nonexistent Tool") + + with self.assertRaises(ValueError): + self.tools.remove(nonexistent_tool) + + def test_pop(self): + popped = self.tools.pop(1) + + self.assertEqual(popped, self.calculator_tool) + self.assertEqual(len(self.tools), 2) + with self.assertRaises(KeyError): + _ = self.tools["calculator"] + self.assertNotIn("calculator", self.tools._name_cache) + + def test_pop_last(self): + popped = self.tools.pop() + + self.assertEqual(popped, self.translator_tool) + self.assertEqual(len(self.tools), 2) + with self.assertRaises(KeyError): + _ = self.tools["translator"] + self.assertNotIn("translator", self.tools._name_cache) + + def test_clear(self): + self.tools.clear() + + self.assertEqual(len(self.tools), 0) + self.assertEqual(self.tools._name_cache, {}) + with self.assertRaises(KeyError): + _ = self.tools["search"] + + def test_iteration(self): + tools_list = list(self.tools) + self.assertEqual(tools_list, [self.search_tool, self.calculator_tool, self.translator_tool]) + + def test_contains(self): + self.assertIn(self.search_tool, self.tools) + self.assertIn(self.calculator_tool, self.tools) + self.assertIn(self.translator_tool, self.tools) + + nonexistent_tool = self._create_mock_tool("nonexistent", "Nonexistent Tool") + self.assertNotIn(nonexistent_tool, self.tools) + + def test_slicing(self): + slice_result = self.tools[1:3] + self.assertEqual(len(slice_result), 2) + self.assertEqual(slice_result[0], self.calculator_tool) + self.assertEqual(slice_result[1], self.translator_tool) + + self.assertIsInstance(slice_result, list) + self.assertNotIsInstance(slice_result, ToolCollection) + + def test_getitem_with_tool_name_as_int(self): + numeric_name_tool = self._create_mock_tool("123", "Numeric Name Tool") + self.tools.append(numeric_name_tool) + + self.assertEqual(self.tools["123"], numeric_name_tool) + + with self.assertRaises(IndexError): + _ = self.tools[123] + + def test_filter_by_names(self): + + filtered = self.tools.filter_by_names(None) + + self.assertIsInstance(filtered, ToolCollection) + self.assertEqual(len(filtered), 3) + + filtered = self.tools.filter_by_names(["search", "translator"]) + + self.assertIsInstance(filtered, ToolCollection) + self.assertEqual(len(filtered), 2) + self.assertEqual(filtered[0], self.search_tool) + self.assertEqual(filtered[1], self.translator_tool) + self.assertEqual(filtered["search"], self.search_tool) + self.assertEqual(filtered["translator"], self.translator_tool) + + filtered = self.tools.filter_by_names(["search", "nonexistent"]) + + self.assertIsInstance(filtered, ToolCollection) + self.assertEqual(len(filtered), 1) + self.assertEqual(filtered[0], self.search_tool) + + filtered = self.tools.filter_by_names(["nonexistent1", "nonexistent2"]) + + self.assertIsInstance(filtered, ToolCollection) + self.assertEqual(len(filtered), 0) + + filtered = self.tools.filter_by_names([]) + + self.assertIsInstance(filtered, ToolCollection) + self.assertEqual(len(filtered), 0) + + def test_filter_where(self): + filtered = self.tools.filter_where(lambda tool: tool.name.startswith("S")) + + self.assertIsInstance(filtered, ToolCollection) + self.assertEqual(len(filtered), 1) + self.assertEqual(filtered[0], self.search_tool) + self.assertEqual(filtered["search"], self.search_tool) + + filtered = self.tools.filter_where(lambda tool: True) + + self.assertIsInstance(filtered, ToolCollection) + self.assertEqual(len(filtered), 3) + self.assertEqual(filtered[0], self.search_tool) + self.assertEqual(filtered[1], self.calculator_tool) + self.assertEqual(filtered[2], self.translator_tool) + + filtered = self.tools.filter_where(lambda tool: False) + + self.assertIsInstance(filtered, ToolCollection) + self.assertEqual(len(filtered), 0) + filtered = self.tools.filter_where(lambda tool: len(tool.name) > 8) + + self.assertIsInstance(filtered, ToolCollection) + self.assertEqual(len(filtered), 2) + self.assertEqual(filtered[0], self.calculator_tool) + self.assertEqual(filtered[1], self.translator_tool) diff --git a/tool.specs.json b/tool.specs.json new file mode 100644 index 000000000..c9cb7a2cd --- /dev/null +++ b/tool.specs.json @@ -0,0 +1,9711 @@ +{ + "tools": [ + { + "description": "A wrapper around [AI-Minds](https://mindsdb.com/minds). Useful for when you need answers to questions from your data, stored in data sources including PostgreSQL, MySQL, MariaDB, ClickHouse, Snowflake and Google BigQuery. Input should be a question in natural language.", + "env_vars": [ + { + "default": null, + "description": "API key for AI-Minds", + "name": "MINDS_API_KEY", + "required": true + } + ], + "humanized_name": "AIMind Tool", + "init_params_schema": { + "$defs": { + "EnvVar": { + "properties": { + "default": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Default" + }, + "description": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "required": { + "default": true, + "title": "Required", + "type": "boolean" + } + }, + "required": [ + "name", + "description" + ], + "title": "EnvVar", + "type": "object" + } + }, + "properties": { + "api_key": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Api Key" + }, + "datasources": { + "anyOf": [ + { + "items": { + "additionalProperties": true, + "type": "object" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Datasources" + }, + "mind_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Mind Name" + } + }, + "title": "AIMindTool", + "type": "object" + }, + "name": "AIMindTool", + "package_dependencies": [ + "minds-sdk" + ], + "run_params_schema": { + "description": "Input for AIMind Tool.", + "properties": { + "query": { + "description": "Question in natural language to ask the AI-Mind", + "title": "Query", + "type": "string" + } + }, + "required": [ + "query" + ], + "title": "AIMindToolInputSchema", + "type": "object" + } + }, + { + "description": "Fetches metadata from Arxiv based on a search query and optionally downloads PDFs.", + "env_vars": [], + "humanized_name": "Arxiv Paper Fetcher and Downloader", + "init_params_schema": { + "$defs": { + "EnvVar": { + "properties": { + "default": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Default" + }, + "description": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "required": { + "default": true, + "title": "Required", + "type": "boolean" + } + }, + "required": [ + "name", + "description" + ], + "title": "EnvVar", + "type": "object" + } + }, + "additionalProperties": true, + "properties": {}, + "title": "ArxivPaperTool", + "type": "object" + }, + "name": "ArxivPaperTool", + "package_dependencies": [ + "pydantic" + ], + "run_params_schema": { + "properties": { + "max_results": { + "default": 5, + "description": "Max results to fetch; must be between 1 and 100", + "maximum": 100, + "minimum": 1, + "title": "Max Results", + "type": "integer" + }, + "search_query": { + "description": "Search query for Arxiv, e.g., 'transformer neural network'", + "title": "Search Query", + "type": "string" + } + }, + "required": [ + "search_query" + ], + "title": "ArxivToolInput", + "type": "object" + } + }, + { + "description": "A tool that can be used to search the internet with a search_query.", + "env_vars": [ + { + "default": null, + "description": "API key for Brave Search", + "name": "BRAVE_API_KEY", + "required": true + } + ], + "humanized_name": "Brave Web Search the internet", + "init_params_schema": { + "$defs": { + "EnvVar": { + "properties": { + "default": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Default" + }, + "description": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "required": { + "default": true, + "title": "Required", + "type": "boolean" + } + }, + "required": [ + "name", + "description" + ], + "title": "EnvVar", + "type": "object" + } + }, + "description": "BraveSearchTool - A tool for performing web searches using the Brave Search API.\n\nThis module provides functionality to search the internet using Brave's Search API,\nsupporting customizable result counts and country-specific searches.\n\nDependencies:\n - requests\n - pydantic\n - python-dotenv (for API key management)", + "properties": { + "country": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "", + "title": "Country" + }, + "n_results": { + "default": 10, + "title": "N Results", + "type": "integer" + }, + "save_file": { + "default": false, + "title": "Save File", + "type": "boolean" + }, + "search_url": { + "default": "https://api.search.brave.com/res/v1/web/search", + "title": "Search Url", + "type": "string" + } + }, + "title": "BraveSearchTool", + "type": "object" + }, + "name": "BraveSearchTool", + "package_dependencies": [], + "run_params_schema": { + "description": "Input for BraveSearchTool.", + "properties": { + "search_query": { + "description": "Mandatory search query you want to use to search the internet", + "title": "Search Query", + "type": "string" + } + }, + "required": [ + "search_query" + ], + "title": "BraveSearchToolSchema", + "type": "object" + } + }, + { + "description": "Scrapes structured data using Bright Data Dataset API from a URL and optional input parameters", + "env_vars": [], + "humanized_name": "Bright Data Dataset Tool", + "init_params_schema": { + "$defs": { + "EnvVar": { + "properties": { + "default": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Default" + }, + "description": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "required": { + "default": true, + "title": "Required", + "type": "boolean" + } + }, + "required": [ + "name", + "description" + ], + "title": "EnvVar", + "type": "object" + } + }, + "description": "CrewAI-compatible tool for scraping structured data using Bright Data Datasets.\n\nAttributes:\n name (str): Tool name displayed in the CrewAI environment.\n description (str): Tool description shown to agents or users.\n args_schema (Type[BaseModel]): Pydantic schema for validating input arguments.", + "properties": { + "additional_params": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Additional Params" + }, + "dataset_type": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Dataset Type" + }, + "format": { + "default": "json", + "title": "Format", + "type": "string" + }, + "url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Url" + }, + "zipcode": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Zipcode" + } + }, + "title": "BrightDataDatasetTool", + "type": "object" + }, + "name": "BrightDataDatasetTool", + "package_dependencies": [], + "run_params_schema": { + "description": "Schema for validating input parameters for the BrightDataDatasetTool.\n\nAttributes:\n dataset_type (str): Required Bright Data Dataset Type used to specify which dataset to access.\n format (str): Response format (json by default). Multiple formats exist - json, ndjson, jsonl, csv\n url (str): The URL from which structured data needs to be extracted.\n zipcode (Optional[str]): An optional ZIP code to narrow down the data geographically.\n additional_params (Optional[Dict]): Extra parameters for the Bright Data API call.", + "properties": { + "additional_params": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Additional params if any", + "title": "Additional Params" + }, + "dataset_type": { + "description": "The Bright Data Dataset Type", + "title": "Dataset Type", + "type": "string" + }, + "format": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "json", + "description": "Response format (json by default)", + "title": "Format" + }, + "url": { + "description": "The URL to extract data from", + "title": "Url", + "type": "string" + }, + "zipcode": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional zipcode", + "title": "Zipcode" + } + }, + "required": [ + "dataset_type", + "url" + ], + "title": "BrightDataDatasetToolSchema", + "type": "object" + } + }, + { + "description": "Tool to perform web search using Bright Data SERP API.", + "env_vars": [], + "humanized_name": "Bright Data SERP Search", + "init_params_schema": { + "$defs": { + "EnvVar": { + "properties": { + "default": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Default" + }, + "description": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "required": { + "default": true, + "title": "Required", + "type": "boolean" + } + }, + "required": [ + "name", + "description" + ], + "title": "EnvVar", + "type": "object" + } + }, + "description": "A web search tool that utilizes Bright Data's SERP API to perform queries and return either structured results\nor raw page content from search engines like Google or Bing.\n\nAttributes:\n name (str): Tool name used by the agent.\n description (str): A brief explanation of what the tool does.\n args_schema (Type[BaseModel]): Schema class for validating tool arguments.\n base_url (str): The Bright Data API endpoint used for making the POST request.\n api_key (str): Bright Data API key loaded from the environment variable 'BRIGHT_DATA_API_KEY'.\n zone (str): Zone identifier from Bright Data, loaded from the environment variable 'BRIGHT_DATA_ZONE'.\n\nRaises:\n ValueError: If API key or zone environment variables are not set.", + "properties": { + "api_key": { + "default": "", + "title": "Api Key", + "type": "string" + }, + "base_url": { + "default": "", + "title": "Base Url", + "type": "string" + }, + "country": { + "default": "us", + "title": "Country", + "type": "string" + }, + "device_type": { + "default": "desktop", + "title": "Device Type", + "type": "string" + }, + "language": { + "default": "en", + "title": "Language", + "type": "string" + }, + "parse_results": { + "default": true, + "title": "Parse Results", + "type": "boolean" + }, + "query": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Query" + }, + "search_engine": { + "default": "google", + "title": "Search Engine", + "type": "string" + }, + "search_type": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Search Type" + }, + "zone": { + "default": "", + "title": "Zone", + "type": "string" + } + }, + "title": "BrightDataSearchTool", + "type": "object" + }, + "name": "BrightDataSearchTool", + "package_dependencies": [], + "run_params_schema": { + "description": "Schema that defines the input arguments for the BrightDataSearchToolSchema.\n\nAttributes:\n query (str): The search query to be executed (e.g., \"latest AI news\").\n search_engine (Optional[str]): The search engine to use (\"google\", \"bing\", \"yandex\"). Default is \"google\".\n country (Optional[str]): Two-letter country code for geo-targeting (e.g., \"us\", \"in\"). Default is \"us\".\n language (Optional[str]): Language code for search results (e.g., \"en\", \"es\"). Default is \"en\".\n search_type (Optional[str]): Type of search, such as \"isch\" (images), \"nws\" (news), \"jobs\", etc.\n device_type (Optional[str]): Device type to simulate (\"desktop\", \"mobile\", \"ios\", \"android\"). Default is \"desktop\".\n parse_results (Optional[bool]): If True, results will be returned in structured JSON. If False, raw HTML. Default is True.", + "properties": { + "country": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "us", + "description": "Two-letter country code for geo-targeting (e.g., 'us', 'gb')", + "title": "Country" + }, + "device_type": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "desktop", + "description": "Device type to simulate (e.g., 'mobile', 'desktop', 'ios')", + "title": "Device Type" + }, + "language": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "en", + "description": "Language code (e.g., 'en', 'es') used in the query URL", + "title": "Language" + }, + "parse_results": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": true, + "description": "Whether to parse and return JSON (True) or raw HTML/text (False)", + "title": "Parse Results" + }, + "query": { + "description": "Search query to perform", + "title": "Query", + "type": "string" + }, + "search_engine": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "google", + "description": "Search engine domain (e.g., 'google', 'bing', 'yandex')", + "title": "Search Engine" + }, + "search_type": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Type of search (e.g., 'isch' for images, 'nws' for news)", + "title": "Search Type" + } + }, + "required": [ + "query" + ], + "title": "BrightDataSearchToolSchema", + "type": "object" + } + }, + { + "description": "Tool to perform web scraping using Bright Data Web Unlocker", + "env_vars": [], + "humanized_name": "Bright Data Web Unlocker Scraping", + "init_params_schema": { + "$defs": { + "EnvVar": { + "properties": { + "default": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Default" + }, + "description": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "required": { + "default": true, + "title": "Required", + "type": "boolean" + } + }, + "required": [ + "name", + "description" + ], + "title": "EnvVar", + "type": "object" + } + }, + "description": "A tool for performing web scraping using the Bright Data Web Unlocker API.\n\nThis tool allows automated and programmatic access to web pages by routing requests\nthrough Bright Data's unlocking and proxy infrastructure, which can bypass bot\nprotection mechanisms like CAPTCHA, geo-restrictions, and anti-bot detection.\n\nAttributes:\n name (str): Name of the tool.\n description (str): Description of what the tool does.\n args_schema (Type[BaseModel]): Pydantic model schema for expected input arguments.\n base_url (str): Base URL of the Bright Data Web Unlocker API.\n api_key (str): Bright Data API key (must be set in the BRIGHT_DATA_API_KEY environment variable).\n zone (str): Bright Data zone identifier (must be set in the BRIGHT_DATA_ZONE environment variable).\n\nMethods:\n _run(**kwargs: Any) -> Any:\n Sends a scraping request to Bright Data's Web Unlocker API and returns the result.", + "properties": { + "api_key": { + "default": "", + "title": "Api Key", + "type": "string" + }, + "base_url": { + "default": "", + "title": "Base Url", + "type": "string" + }, + "data_format": { + "default": "markdown", + "title": "Data Format", + "type": "string" + }, + "format": { + "default": "raw", + "title": "Format", + "type": "string" + }, + "url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Url" + }, + "zone": { + "default": "", + "title": "Zone", + "type": "string" + } + }, + "title": "BrightDataWebUnlockerTool", + "type": "object" + }, + "name": "BrightDataWebUnlockerTool", + "package_dependencies": [], + "run_params_schema": { + "description": "Pydantic schema for input parameters used by the BrightDataWebUnlockerTool.\n\nThis schema defines the structure and validation for parameters passed when performing\na web scraping request using Bright Data's Web Unlocker.\n\nAttributes:\n url (str): The target URL to scrape.\n format (Optional[str]): Format of the response returned by Bright Data. Default 'raw' format.\n data_format (Optional[str]): Response data format (html by default). markdown is one more option.", + "properties": { + "data_format": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "markdown", + "description": "Response data format (html by default)", + "title": "Data Format" + }, + "format": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "raw", + "description": "Response format (raw is standard)", + "title": "Format" + }, + "url": { + "description": "URL to perform the web scraping", + "title": "Url", + "type": "string" + } + }, + "required": [ + "url" + ], + "title": "BrightDataUnlockerToolSchema", + "type": "object" + } + }, + { + "description": "Load webpages url in a headless browser using Browserbase and return the contents", + "env_vars": [ + { + "default": null, + "description": "API key for Browserbase services", + "name": "BROWSERBASE_API_KEY", + "required": false + }, + { + "default": null, + "description": "Project ID for Browserbase services", + "name": "BROWSERBASE_PROJECT_ID", + "required": false + } + ], + "humanized_name": "Browserbase web load tool", + "init_params_schema": { + "$defs": { + "EnvVar": { + "properties": { + "default": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Default" + }, + "description": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "required": { + "default": true, + "title": "Required", + "type": "boolean" + } + }, + "required": [ + "name", + "description" + ], + "title": "EnvVar", + "type": "object" + } + }, + "properties": { + "api_key": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Api Key" + }, + "browserbase": { + "anyOf": [ + {}, + { + "type": "null" + } + ], + "default": null, + "title": "Browserbase" + }, + "project_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Project Id" + }, + "proxy": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Proxy" + }, + "session_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Session Id" + }, + "text_content": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": false, + "title": "Text Content" + } + }, + "title": "BrowserbaseLoadTool", + "type": "object" + }, + "name": "BrowserbaseLoadTool", + "package_dependencies": [ + "browserbase" + ], + "run_params_schema": { + "properties": { + "url": { + "description": "Website URL", + "title": "Url", + "type": "string" + } + }, + "required": [ + "url" + ], + "title": "BrowserbaseLoadToolSchema", + "type": "object" + } + }, + { + "description": "A tool that can be used to semantic search a query from a CSV's content.", + "env_vars": [], + "humanized_name": "Search a CSV's content", + "init_params_schema": { + "$defs": { + "Adapter": { + "properties": {}, + "title": "Adapter", + "type": "object" + }, + "EnvVar": { + "properties": { + "default": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Default" + }, + "description": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "required": { + "default": true, + "title": "Required", + "type": "boolean" + } + }, + "required": [ + "name", + "description" + ], + "title": "EnvVar", + "type": "object" + } + }, + "properties": { + "adapter": { + "$ref": "#/$defs/Adapter" + }, + "config": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Config" + }, + "summarize": { + "default": false, + "title": "Summarize", + "type": "boolean" + } + }, + "title": "CSVSearchTool", + "type": "object" + }, + "name": "CSVSearchTool", + "package_dependencies": [], + "run_params_schema": { + "description": "Input for CSVSearchTool.", + "properties": { + "csv": { + "description": "Mandatory csv path you want to search", + "title": "Csv", + "type": "string" + }, + "search_query": { + "description": "Mandatory search query you want to use to search the CSV's content", + "title": "Search Query", + "type": "string" + } + }, + "required": [ + "search_query", + "csv" + ], + "title": "CSVSearchToolSchema", + "type": "object" + } + }, + { + "description": "A tool that can be used to semantic search a query from a Code Docs content.", + "env_vars": [], + "humanized_name": "Search a Code Docs content", + "init_params_schema": { + "$defs": { + "Adapter": { + "properties": {}, + "title": "Adapter", + "type": "object" + }, + "EnvVar": { + "properties": { + "default": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Default" + }, + "description": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "required": { + "default": true, + "title": "Required", + "type": "boolean" + } + }, + "required": [ + "name", + "description" + ], + "title": "EnvVar", + "type": "object" + } + }, + "properties": { + "adapter": { + "$ref": "#/$defs/Adapter" + }, + "config": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Config" + }, + "summarize": { + "default": false, + "title": "Summarize", + "type": "boolean" + } + }, + "title": "CodeDocsSearchTool", + "type": "object" + }, + "name": "CodeDocsSearchTool", + "package_dependencies": [], + "run_params_schema": { + "description": "Input for CodeDocsSearchTool.", + "properties": { + "docs_url": { + "description": "Mandatory docs_url path you want to search", + "title": "Docs Url", + "type": "string" + }, + "search_query": { + "description": "Mandatory search query you want to use to search the Code Docs content", + "title": "Search Query", + "type": "string" + } + }, + "required": [ + "search_query", + "docs_url" + ], + "title": "CodeDocsSearchToolSchema", + "type": "object" + } + }, + { + "description": "Interprets Python3 code strings with a final print statement.", + "env_vars": [], + "humanized_name": "Code Interpreter", + "init_params_schema": { + "$defs": { + "EnvVar": { + "properties": { + "default": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Default" + }, + "description": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "required": { + "default": true, + "title": "Required", + "type": "boolean" + } + }, + "required": [ + "name", + "description" + ], + "title": "EnvVar", + "type": "object" + } + }, + "description": "A tool for executing Python code in isolated environments.\n\nThis tool provides functionality to run Python code either in a Docker container\nfor safe isolation or directly in a restricted sandbox. It can handle installing\nPython packages and executing arbitrary Python code.", + "properties": { + "code": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Code" + }, + "default_image_tag": { + "default": "code-interpreter:latest", + "title": "Default Image Tag", + "type": "string" + }, + "unsafe_mode": { + "default": false, + "title": "Unsafe Mode", + "type": "boolean" + }, + "user_docker_base_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "User Docker Base Url" + }, + "user_dockerfile_path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "User Dockerfile Path" + } + }, + "title": "CodeInterpreterTool", + "type": "object" + }, + "name": "CodeInterpreterTool", + "package_dependencies": [], + "run_params_schema": { + "description": "Schema for defining inputs to the CodeInterpreterTool.\n\nThis schema defines the required parameters for code execution,\nincluding the code to run and any libraries that need to be installed.", + "properties": { + "code": { + "description": "Python3 code used to be interpreted in the Docker container. ALWAYS PRINT the final result and the output of the code", + "title": "Code", + "type": "string" + }, + "libraries_used": { + "description": "List of libraries used in the code with proper installing names separated by commas. Example: numpy,pandas,beautifulsoup4", + "items": { + "type": "string" + }, + "title": "Libraries Used", + "type": "array" + } + }, + "required": [ + "code", + "libraries_used" + ], + "title": "CodeInterpreterSchema", + "type": "object" + } + }, + { + "description": "", + "env_vars": [ + { + "default": null, + "description": "API key for Composio services", + "name": "COMPOSIO_API_KEY", + "required": true + } + ], + "humanized_name": "ComposioTool", + "init_params_schema": { + "$defs": { + "EnvVar": { + "properties": { + "default": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Default" + }, + "description": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "required": { + "default": true, + "title": "Required", + "type": "boolean" + } + }, + "required": [ + "name", + "description" + ], + "title": "EnvVar", + "type": "object" + } + }, + "description": "Wrapper for composio tools.", + "properties": {}, + "required": [ + "name", + "description" + ], + "title": "ComposioTool", + "type": "object" + }, + "name": "ComposioTool", + "package_dependencies": [], + "run_params_schema": {} + }, + { + "description": "Create a new Contextual AI RAG agent with documents and datastore", + "env_vars": [], + "humanized_name": "Contextual AI Create Agent Tool", + "init_params_schema": { + "$defs": { + "EnvVar": { + "properties": { + "default": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Default" + }, + "description": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "required": { + "default": true, + "title": "Required", + "type": "boolean" + } + }, + "required": [ + "name", + "description" + ], + "title": "EnvVar", + "type": "object" + } + }, + "description": "Tool to create Contextual AI RAG agents with documents.", + "properties": { + "api_key": { + "title": "Api Key", + "type": "string" + }, + "contextual_client": { + "default": null, + "title": "Contextual Client" + } + }, + "required": [ + "api_key" + ], + "title": "ContextualAICreateAgentTool", + "type": "object" + }, + "name": "ContextualAICreateAgentTool", + "package_dependencies": [ + "contextual-client" + ], + "run_params_schema": { + "description": "Schema for contextual create agent tool.", + "properties": { + "agent_description": { + "description": "Description for the new agent", + "title": "Agent Description", + "type": "string" + }, + "agent_name": { + "description": "Name for the new agent", + "title": "Agent Name", + "type": "string" + }, + "datastore_name": { + "description": "Name for the new datastore", + "title": "Datastore Name", + "type": "string" + }, + "document_paths": { + "description": "List of file paths to upload", + "items": { + "type": "string" + }, + "title": "Document Paths", + "type": "array" + } + }, + "required": [ + "agent_name", + "agent_description", + "datastore_name", + "document_paths" + ], + "title": "ContextualAICreateAgentSchema", + "type": "object" + } + }, + { + "description": "Parse documents using Contextual AI's advanced document parser", + "env_vars": [], + "humanized_name": "Contextual AI Document Parser", + "init_params_schema": { + "$defs": { + "EnvVar": { + "properties": { + "default": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Default" + }, + "description": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "required": { + "default": true, + "title": "Required", + "type": "boolean" + } + }, + "required": [ + "name", + "description" + ], + "title": "EnvVar", + "type": "object" + } + }, + "description": "Tool to parse documents using Contextual AI's parser.", + "properties": { + "api_key": { + "title": "Api Key", + "type": "string" + } + }, + "required": [ + "api_key" + ], + "title": "ContextualAIParseTool", + "type": "object" + }, + "name": "ContextualAIParseTool", + "package_dependencies": [ + "contextual-client" + ], + "run_params_schema": { + "description": "Schema for contextual parse tool.", + "properties": { + "enable_document_hierarchy": { + "default": true, + "description": "Enable document hierarchy", + "title": "Enable Document Hierarchy", + "type": "boolean" + }, + "figure_caption_mode": { + "default": "concise", + "description": "Figure caption mode", + "title": "Figure Caption Mode", + "type": "string" + }, + "file_path": { + "description": "Path to the document to parse", + "title": "File Path", + "type": "string" + }, + "output_types": { + "default": [ + "markdown-per-page" + ], + "description": "List of output types", + "items": { + "type": "string" + }, + "title": "Output Types", + "type": "array" + }, + "page_range": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Page range to parse (e.g., '0-5')", + "title": "Page Range" + }, + "parse_mode": { + "default": "standard", + "description": "Parsing mode", + "title": "Parse Mode", + "type": "string" + } + }, + "required": [ + "file_path" + ], + "title": "ContextualAIParseSchema", + "type": "object" + } + }, + { + "description": "Use this tool to query a Contextual AI RAG agent with access to your documents", + "env_vars": [], + "humanized_name": "Contextual AI Query Tool", + "init_params_schema": { + "$defs": { + "EnvVar": { + "properties": { + "default": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Default" + }, + "description": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "required": { + "default": true, + "title": "Required", + "type": "boolean" + } + }, + "required": [ + "name", + "description" + ], + "title": "EnvVar", + "type": "object" + } + }, + "description": "Tool to query Contextual AI RAG agents.", + "properties": { + "api_key": { + "title": "Api Key", + "type": "string" + }, + "contextual_client": { + "default": null, + "title": "Contextual Client" + } + }, + "required": [ + "api_key" + ], + "title": "ContextualAIQueryTool", + "type": "object" + }, + "name": "ContextualAIQueryTool", + "package_dependencies": [ + "contextual-client" + ], + "run_params_schema": { + "description": "Schema for contextual query tool.", + "properties": { + "agent_id": { + "description": "ID of the Contextual AI agent to query", + "title": "Agent Id", + "type": "string" + }, + "datastore_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional datastore ID for document readiness verification", + "title": "Datastore Id" + }, + "query": { + "description": "Query to send to the Contextual AI agent.", + "title": "Query", + "type": "string" + } + }, + "required": [ + "query", + "agent_id" + ], + "title": "ContextualAIQuerySchema", + "type": "object" + } + }, + { + "description": "Rerank documents using Contextual AI's instruction-following reranker", + "env_vars": [], + "humanized_name": "Contextual AI Document Reranker", + "init_params_schema": { + "$defs": { + "EnvVar": { + "properties": { + "default": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Default" + }, + "description": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "required": { + "default": true, + "title": "Required", + "type": "boolean" + } + }, + "required": [ + "name", + "description" + ], + "title": "EnvVar", + "type": "object" + } + }, + "description": "Tool to rerank documents using Contextual AI's instruction-following reranker.", + "properties": { + "api_key": { + "title": "Api Key", + "type": "string" + } + }, + "required": [ + "api_key" + ], + "title": "ContextualAIRerankTool", + "type": "object" + }, + "name": "ContextualAIRerankTool", + "package_dependencies": [ + "contextual-client" + ], + "run_params_schema": { + "description": "Schema for contextual rerank tool.", + "properties": { + "documents": { + "description": "List of document texts to rerank", + "items": { + "type": "string" + }, + "title": "Documents", + "type": "array" + }, + "instruction": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional instruction for reranking behavior", + "title": "Instruction" + }, + "metadata": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional metadata for each document", + "title": "Metadata" + }, + "model": { + "default": "ctxl-rerank-en-v1-instruct", + "description": "Reranker model to use", + "title": "Model", + "type": "string" + }, + "query": { + "description": "The search query to rerank documents against", + "title": "Query", + "type": "string" + } + }, + "required": [ + "query", + "documents" + ], + "title": "ContextualAIRerankSchema", + "type": "object" + } + }, + { + "description": "A tool to search the Couchbase database for relevant information on internal documents.", + "env_vars": [], + "humanized_name": "CouchbaseFTSVectorSearchTool", + "init_params_schema": { + "$defs": { + "EnvVar": { + "properties": { + "default": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Default" + }, + "description": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "required": { + "default": true, + "title": "Required", + "type": "boolean" + } + }, + "required": [ + "name", + "description" + ], + "title": "EnvVar", + "type": "object" + } + }, + "description": "Tool to search the Couchbase database", + "properties": { + "bucket_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": [ + null + ], + "title": "Bucket Name" + }, + "cluster": { + "anyOf": [ + {}, + { + "type": "null" + } + ], + "default": null, + "title": "Cluster" + }, + "collection_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": [ + null + ], + "title": "Collection Name" + }, + "embedding_key": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "embedding", + "description": "Name of the field in the search index that stores the vector", + "title": "Embedding Key" + }, + "index_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": [ + null + ], + "title": "Index Name" + }, + "limit": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": 3, + "title": "Limit" + }, + "scope_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": [ + null + ], + "title": "Scope Name" + }, + "scoped_index": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Scoped Index" + } + }, + "title": "CouchbaseFTSVectorSearchTool", + "type": "object" + }, + "name": "CouchbaseFTSVectorSearchTool", + "package_dependencies": [], + "run_params_schema": { + "description": "Input for CouchbaseTool.", + "properties": { + "query": { + "description": "The query to search retrieve relevant information from the Couchbase database. Pass only the query, not the question.", + "title": "Query", + "type": "string" + } + }, + "required": [ + "query" + ], + "title": "CouchbaseToolSchema", + "type": "object" + } + }, + { + "description": "A tool that can be used to semantic search a query from a DOCX's content.", + "env_vars": [], + "humanized_name": "Search a DOCX's content", + "init_params_schema": { + "$defs": { + "Adapter": { + "properties": {}, + "title": "Adapter", + "type": "object" + }, + "EnvVar": { + "properties": { + "default": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Default" + }, + "description": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "required": { + "default": true, + "title": "Required", + "type": "boolean" + } + }, + "required": [ + "name", + "description" + ], + "title": "EnvVar", + "type": "object" + } + }, + "properties": { + "adapter": { + "$ref": "#/$defs/Adapter" + }, + "config": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Config" + }, + "summarize": { + "default": false, + "title": "Summarize", + "type": "boolean" + } + }, + "title": "DOCXSearchTool", + "type": "object" + }, + "name": "DOCXSearchTool", + "package_dependencies": [], + "run_params_schema": { + "description": "Input for DOCXSearchTool.", + "properties": { + "docx": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Mandatory docx path you want to search", + "title": "Docx" + }, + "search_query": { + "description": "Mandatory search query you want to use to search the DOCX's content", + "title": "Search Query", + "type": "string" + } + }, + "required": [ + "docx", + "search_query" + ], + "title": "DOCXSearchToolSchema", + "type": "object" + } + }, + { + "description": "Generates images using OpenAI's Dall-E model.", + "env_vars": [ + { + "default": null, + "description": "API key for OpenAI services", + "name": "OPENAI_API_KEY", + "required": true + } + ], + "humanized_name": "Dall-E Tool", + "init_params_schema": { + "$defs": { + "EnvVar": { + "properties": { + "default": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Default" + }, + "description": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "required": { + "default": true, + "title": "Required", + "type": "boolean" + } + }, + "required": [ + "name", + "description" + ], + "title": "EnvVar", + "type": "object" + } + }, + "properties": { + "model": { + "default": "dall-e-3", + "title": "Model", + "type": "string" + }, + "n": { + "default": 1, + "title": "N", + "type": "integer" + }, + "quality": { + "default": "standard", + "title": "Quality", + "type": "string" + }, + "size": { + "default": "1024x1024", + "title": "Size", + "type": "string" + } + }, + "title": "DallETool", + "type": "object" + }, + "name": "DallETool", + "package_dependencies": [], + "run_params_schema": { + "description": "Input for Dall-E Tool.", + "properties": { + "image_description": { + "description": "Description of the image to be generated by Dall-E.", + "title": "Image Description", + "type": "string" + } + }, + "required": [ + "image_description" + ], + "title": "ImagePromptSchema", + "type": "object" + } + }, + { + "description": "Execute SQL queries against Databricks workspace tables and return the results. Provide a 'query' parameter with the SQL query to execute.", + "env_vars": [], + "humanized_name": "Databricks SQL Query", + "init_params_schema": { + "$defs": { + "EnvVar": { + "properties": { + "default": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Default" + }, + "description": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "required": { + "default": true, + "title": "Required", + "type": "boolean" + } + }, + "required": [ + "name", + "description" + ], + "title": "EnvVar", + "type": "object" + } + }, + "description": "A tool for querying Databricks workspace tables using SQL.\n\nThis tool executes SQL queries against Databricks tables and returns the results.\nIt requires Databricks authentication credentials to be set as environment variables.\n\nAuthentication can be provided via:\n- Databricks CLI profile: Set DATABRICKS_CONFIG_PROFILE environment variable\n- Direct credentials: Set DATABRICKS_HOST and DATABRICKS_TOKEN environment variables\n\nExample:\n >>> tool = DatabricksQueryTool()\n >>> results = tool.run(query=\"SELECT * FROM my_table LIMIT 10\")", + "properties": { + "default_catalog": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Default Catalog" + }, + "default_schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Default Schema" + }, + "default_warehouse_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Default Warehouse Id" + } + }, + "title": "DatabricksQueryTool", + "type": "object" + }, + "name": "DatabricksQueryTool", + "package_dependencies": [ + "databricks-sdk" + ], + "run_params_schema": { + "description": "Input schema for DatabricksQueryTool.", + "properties": { + "catalog": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Databricks catalog name (optional, defaults to configured catalog)", + "title": "Catalog" + }, + "db_schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Databricks schema name (optional, defaults to configured schema)", + "title": "Db Schema" + }, + "query": { + "description": "SQL query to execute against the Databricks workspace table", + "title": "Query", + "type": "string" + }, + "row_limit": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": 1000, + "description": "Maximum number of rows to return (default: 1000)", + "title": "Row Limit" + }, + "warehouse_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Databricks SQL warehouse ID (optional, defaults to configured warehouse)", + "title": "Warehouse Id" + } + }, + "required": [ + "query" + ], + "title": "DatabricksQueryToolSchema", + "type": "object" + } + }, + { + "description": "A tool that can be used to recursively list a directory's content.", + "env_vars": [], + "humanized_name": "List files in directory", + "init_params_schema": { + "$defs": { + "EnvVar": { + "properties": { + "default": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Default" + }, + "description": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "required": { + "default": true, + "title": "Required", + "type": "boolean" + } + }, + "required": [ + "name", + "description" + ], + "title": "EnvVar", + "type": "object" + } + }, + "properties": { + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Directory" + } + }, + "title": "DirectoryReadTool", + "type": "object" + }, + "name": "DirectoryReadTool", + "package_dependencies": [], + "run_params_schema": { + "description": "Input for DirectoryReadTool.", + "properties": { + "directory": { + "description": "Mandatory directory to list content", + "title": "Directory", + "type": "string" + } + }, + "required": [ + "directory" + ], + "title": "DirectoryReadToolSchema", + "type": "object" + } + }, + { + "description": "A tool that can be used to semantic search a query from a directory's content.", + "env_vars": [], + "humanized_name": "Search a directory's content", + "init_params_schema": { + "$defs": { + "Adapter": { + "properties": {}, + "title": "Adapter", + "type": "object" + }, + "EnvVar": { + "properties": { + "default": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Default" + }, + "description": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "required": { + "default": true, + "title": "Required", + "type": "boolean" + } + }, + "required": [ + "name", + "description" + ], + "title": "EnvVar", + "type": "object" + } + }, + "properties": { + "adapter": { + "$ref": "#/$defs/Adapter" + }, + "config": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Config" + }, + "summarize": { + "default": false, + "title": "Summarize", + "type": "boolean" + } + }, + "title": "DirectorySearchTool", + "type": "object" + }, + "name": "DirectorySearchTool", + "package_dependencies": [], + "run_params_schema": { + "description": "Input for DirectorySearchTool.", + "properties": { + "directory": { + "description": "Mandatory directory you want to search", + "title": "Directory", + "type": "string" + }, + "search_query": { + "description": "Mandatory search query you want to use to search the directory's content", + "title": "Search Query", + "type": "string" + } + }, + "required": [ + "search_query", + "directory" + ], + "title": "DirectorySearchToolSchema", + "type": "object" + } + }, + { + "description": "Search the internet using Exa", + "env_vars": [ + { + "default": null, + "description": "API key for Exa services", + "name": "EXA_API_KEY", + "required": false + } + ], + "humanized_name": "EXASearchTool", + "init_params_schema": { + "$defs": { + "EnvVar": { + "properties": { + "default": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Default" + }, + "description": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "required": { + "default": true, + "title": "Required", + "type": "boolean" + } + }, + "required": [ + "name", + "description" + ], + "title": "EnvVar", + "type": "object" + } + }, + "properties": { + "api_key": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "API key for Exa services", + "required": false, + "title": "Api Key" + }, + "client": { + "anyOf": [ + {}, + { + "type": "null" + } + ], + "default": null, + "title": "Client" + }, + "content": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": false, + "title": "Content" + }, + "summary": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": false, + "title": "Summary" + }, + "type": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "auto", + "title": "Type" + } + }, + "title": "EXASearchTool", + "type": "object" + }, + "name": "EXASearchTool", + "package_dependencies": [ + "exa_py" + ], + "run_params_schema": { + "properties": { + "end_published_date": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "End date for the search", + "title": "End Published Date" + }, + "include_domains": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "List of domains to include in the search", + "title": "Include Domains" + }, + "search_query": { + "description": "Mandatory search query you want to use to search the internet", + "title": "Search Query", + "type": "string" + }, + "start_published_date": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Start date for the search", + "title": "Start Published Date" + } + }, + "required": [ + "search_query" + ], + "title": "EXABaseToolSchema", + "type": "object" + } + }, + { + "description": "Compresses a file or directory into an archive (.zip currently supported). Useful for archiving logs, documents, or backups.", + "env_vars": [], + "humanized_name": "File Compressor Tool", + "init_params_schema": { + "$defs": { + "EnvVar": { + "properties": { + "default": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Default" + }, + "description": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "required": { + "default": true, + "title": "Required", + "type": "boolean" + } + }, + "required": [ + "name", + "description" + ], + "title": "EnvVar", + "type": "object" + } + }, + "properties": {}, + "title": "FileCompressorTool", + "type": "object" + }, + "name": "FileCompressorTool", + "package_dependencies": [], + "run_params_schema": { + "description": "Input schema for FileCompressorTool.", + "properties": { + "format": { + "default": "zip", + "description": "Compression format ('zip', 'tar', 'tar.gz', 'tar.bz2', 'tar.xz').", + "title": "Format", + "type": "string" + }, + "input_path": { + "description": "Path to the file or directory to compress.", + "title": "Input Path", + "type": "string" + }, + "output_path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional output archive filename.", + "title": "Output Path" + }, + "overwrite": { + "default": false, + "description": "Whether to overwrite the archive if it already exists.", + "title": "Overwrite", + "type": "boolean" + } + }, + "required": [ + "input_path" + ], + "title": "FileCompressorToolInput", + "type": "object" + } + }, + { + "description": "A tool that reads the content of a file. To use this tool, provide a 'file_path' parameter with the path to the file you want to read. Optionally, provide 'start_line' to start reading from a specific line and 'line_count' to limit the number of lines read.", + "env_vars": [], + "humanized_name": "Read a file's content", + "init_params_schema": { + "$defs": { + "EnvVar": { + "properties": { + "default": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Default" + }, + "description": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "required": { + "default": true, + "title": "Required", + "type": "boolean" + } + }, + "required": [ + "name", + "description" + ], + "title": "EnvVar", + "type": "object" + } + }, + "description": "A tool for reading file contents.\n\nThis tool inherits its schema handling from BaseTool to avoid recursive schema\ndefinition issues. The args_schema is set to FileReadToolSchema which defines\nthe required file_path parameter. The schema should not be overridden in the\nconstructor as it would break the inheritance chain and cause infinite loops.\n\nThe tool supports two ways of specifying the file path:\n1. At construction time via the file_path parameter\n2. At runtime via the file_path parameter in the tool's input\n\nArgs:\n file_path (Optional[str]): Path to the file to be read. If provided,\n this becomes the default file path for the tool.\n **kwargs: Additional keyword arguments passed to BaseTool.\n\nExample:\n >>> tool = FileReadTool(file_path=\"/path/to/file.txt\")\n >>> content = tool.run() # Reads /path/to/file.txt\n >>> content = tool.run(file_path=\"/path/to/other.txt\") # Reads other.txt\n >>> content = tool.run(file_path=\"/path/to/file.txt\", start_line=100, line_count=50) # Reads lines 100-149", + "properties": { + "file_path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "File Path" + } + }, + "title": "FileReadTool", + "type": "object" + }, + "name": "FileReadTool", + "package_dependencies": [], + "run_params_schema": { + "description": "Input for FileReadTool.", + "properties": { + "file_path": { + "description": "Mandatory file full path to read the file", + "title": "File Path", + "type": "string" + }, + "line_count": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Number of lines to read. If None, reads the entire file", + "title": "Line Count" + }, + "start_line": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": 1, + "description": "Line number to start reading from (1-indexed)", + "title": "Start Line" + } + }, + "required": [ + "file_path" + ], + "title": "FileReadToolSchema", + "type": "object" + } + }, + { + "description": "A tool to write content to a specified file. Accepts filename, content, and optionally a directory path and overwrite flag as input.", + "env_vars": [], + "humanized_name": "File Writer Tool", + "init_params_schema": { + "$defs": { + "EnvVar": { + "properties": { + "default": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Default" + }, + "description": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "required": { + "default": true, + "title": "Required", + "type": "boolean" + } + }, + "required": [ + "name", + "description" + ], + "title": "EnvVar", + "type": "object" + } + }, + "properties": {}, + "title": "FileWriterTool", + "type": "object" + }, + "name": "FileWriterTool", + "package_dependencies": [], + "run_params_schema": { + "properties": { + "content": { + "title": "Content", + "type": "string" + }, + "directory": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "./", + "title": "Directory" + }, + "filename": { + "title": "Filename", + "type": "string" + }, + "overwrite": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "boolean" + } + ], + "default": false, + "title": "Overwrite" + } + }, + "required": [ + "filename", + "content" + ], + "title": "FileWriterToolInput", + "type": "object" + } + }, + { + "description": "Crawl webpages using Firecrawl and return the contents", + "env_vars": [ + { + "default": null, + "description": "API key for Firecrawl services", + "name": "FIRECRAWL_API_KEY", + "required": true + } + ], + "humanized_name": "Firecrawl web crawl tool", + "init_params_schema": { + "$defs": { + "EnvVar": { + "properties": { + "default": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Default" + }, + "description": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "required": { + "default": true, + "title": "Required", + "type": "boolean" + } + }, + "required": [ + "name", + "description" + ], + "title": "EnvVar", + "type": "object" + } + }, + "description": "Tool for crawling websites using Firecrawl. To run this tool, you need to have a Firecrawl API key.\n\nArgs:\n api_key (str): Your Firecrawl API key.\n config (dict): Optional. It contains Firecrawl API parameters.\n\nDefault configuration options:\n max_depth (int): Maximum depth to crawl. Default: 2\n ignore_sitemap (bool): Whether to ignore sitemap. Default: True\n limit (int): Maximum number of pages to crawl. Default: 100\n allow_backward_links (bool): Allow crawling backward links. Default: False\n allow_external_links (bool): Allow crawling external links. Default: False\n scrape_options (ScrapeOptions): Options for scraping content\n - formats (list[str]): Content formats to return. Default: [\"markdown\", \"screenshot\", \"links\"]\n - only_main_content (bool): Only return main content. Default: True\n - timeout (int): Timeout in milliseconds. Default: 30000", + "properties": { + "api_key": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Api Key" + }, + "config": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Config" + } + }, + "title": "FirecrawlCrawlWebsiteTool", + "type": "object" + }, + "name": "FirecrawlCrawlWebsiteTool", + "package_dependencies": [ + "firecrawl-py" + ], + "run_params_schema": { + "properties": { + "url": { + "description": "Website URL", + "title": "Url", + "type": "string" + } + }, + "required": [ + "url" + ], + "title": "FirecrawlCrawlWebsiteToolSchema", + "type": "object" + } + }, + { + "description": "Scrape webpages using Firecrawl and return the contents", + "env_vars": [ + { + "default": null, + "description": "API key for Firecrawl services", + "name": "FIRECRAWL_API_KEY", + "required": true + } + ], + "humanized_name": "Firecrawl web scrape tool", + "init_params_schema": { + "$defs": { + "EnvVar": { + "properties": { + "default": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Default" + }, + "description": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "required": { + "default": true, + "title": "Required", + "type": "boolean" + } + }, + "required": [ + "name", + "description" + ], + "title": "EnvVar", + "type": "object" + } + }, + "description": "Tool for scraping webpages using Firecrawl. To run this tool, you need to have a Firecrawl API key.\n\nArgs:\n api_key (str): Your Firecrawl API key.\n config (dict): Optional. It contains Firecrawl API parameters.\n\nDefault configuration options:\n formats (list[str]): Content formats to return. Default: [\"markdown\"]\n onlyMainContent (bool): Only return main content. Default: True\n includeTags (list[str]): Tags to include. Default: []\n excludeTags (list[str]): Tags to exclude. Default: []\n headers (dict): Headers to include. Default: {}\n waitFor (int): Time to wait for page to load in ms. Default: 0\n json_options (dict): Options for JSON extraction. Default: None", + "properties": { + "api_key": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Api Key" + }, + "config": { + "additionalProperties": true, + "title": "Config", + "type": "object" + } + }, + "title": "FirecrawlScrapeWebsiteTool", + "type": "object" + }, + "name": "FirecrawlScrapeWebsiteTool", + "package_dependencies": [ + "firecrawl-py" + ], + "run_params_schema": { + "properties": { + "url": { + "description": "Website URL", + "title": "Url", + "type": "string" + } + }, + "required": [ + "url" + ], + "title": "FirecrawlScrapeWebsiteToolSchema", + "type": "object" + } + }, + { + "description": "Search webpages using Firecrawl and return the results", + "env_vars": [ + { + "default": null, + "description": "API key for Firecrawl services", + "name": "FIRECRAWL_API_KEY", + "required": true + } + ], + "humanized_name": "Firecrawl web search tool", + "init_params_schema": { + "$defs": { + "EnvVar": { + "properties": { + "default": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Default" + }, + "description": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "required": { + "default": true, + "title": "Required", + "type": "boolean" + } + }, + "required": [ + "name", + "description" + ], + "title": "EnvVar", + "type": "object" + } + }, + "description": "Tool for searching webpages using Firecrawl. To run this tool, you need to have a Firecrawl API key.\n\nArgs:\n api_key (str): Your Firecrawl API key.\n config (dict): Optional. It contains Firecrawl API parameters.\n\nDefault configuration options:\n limit (int): Maximum number of pages to crawl. Default: 5\n tbs (str): Time before search. Default: None\n lang (str): Language. Default: \"en\"\n country (str): Country. Default: \"us\"\n location (str): Location. Default: None\n timeout (int): Timeout in milliseconds. Default: 60000", + "properties": { + "api_key": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Api Key" + }, + "config": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Config" + } + }, + "title": "FirecrawlSearchTool", + "type": "object" + }, + "name": "FirecrawlSearchTool", + "package_dependencies": [ + "firecrawl-py" + ], + "run_params_schema": { + "properties": { + "query": { + "description": "Search query", + "title": "Query", + "type": "string" + } + }, + "required": [ + "query" + ], + "title": "FirecrawlSearchToolSchema", + "type": "object" + } + }, + { + "description": "A tool that leverages CrewAI Studio's capabilities to automatically generate complete CrewAI automations based on natural language descriptions. It translates high-level requirements into functional CrewAI implementations.", + "env_vars": [ + { + "default": null, + "description": "Personal Access Token for CrewAI Enterprise API", + "name": "CREWAI_PERSONAL_ACCESS_TOKEN", + "required": true + }, + { + "default": null, + "description": "Base URL for CrewAI Enterprise API", + "name": "CREWAI_PLUS_URL", + "required": false + } + ], + "humanized_name": "Generate CrewAI Automation", + "init_params_schema": { + "$defs": { + "EnvVar": { + "properties": { + "default": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Default" + }, + "description": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "required": { + "default": true, + "title": "Required", + "type": "boolean" + } + }, + "required": [ + "name", + "description" + ], + "title": "EnvVar", + "type": "object" + } + }, + "properties": { + "crewai_enterprise_url": { + "description": "The base URL of CrewAI Enterprise. If not provided, it will be loaded from the environment variable CREWAI_PLUS_URL with default https://app.crewai.com.", + "title": "Crewai Enterprise Url", + "type": "string" + }, + "personal_access_token": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "The user's Personal Access Token to access CrewAI Enterprise API. If not provided, it will be loaded from the environment variable CREWAI_PERSONAL_ACCESS_TOKEN.", + "title": "Personal Access Token" + } + }, + "title": "GenerateCrewaiAutomationTool", + "type": "object" + }, + "name": "GenerateCrewaiAutomationTool", + "package_dependencies": [], + "run_params_schema": { + "properties": { + "organization_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The identifier for the CrewAI Enterprise organization. If not specified, a default organization will be used.", + "title": "Organization Id" + }, + "prompt": { + "description": "The prompt to generate the CrewAI automation, e.g. 'Generate a CrewAI automation that will scrape the website and store the data in a database.'", + "title": "Prompt", + "type": "string" + } + }, + "required": [ + "prompt" + ], + "title": "GenerateCrewaiAutomationToolSchema", + "type": "object" + } + }, + { + "description": "A tool that can be used to semantic search a query from a github repo's content. This is not the GitHub API, but instead a tool that can provide semantic search capabilities.", + "env_vars": [], + "humanized_name": "Search a github repo's content", + "init_params_schema": { + "$defs": { + "Adapter": { + "properties": {}, + "title": "Adapter", + "type": "object" + }, + "EnvVar": { + "properties": { + "default": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Default" + }, + "description": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "required": { + "default": true, + "title": "Required", + "type": "boolean" + } + }, + "required": [ + "name", + "description" + ], + "title": "EnvVar", + "type": "object" + } + }, + "properties": { + "adapter": { + "$ref": "#/$defs/Adapter" + }, + "config": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Config" + }, + "content_types": { + "description": "Content types you want to be included search, options: [code, repo, pr, issue]", + "items": { + "type": "string" + }, + "title": "Content Types", + "type": "array" + }, + "gh_token": { + "title": "Gh Token", + "type": "string" + }, + "summarize": { + "default": false, + "title": "Summarize", + "type": "boolean" + } + }, + "required": [ + "gh_token" + ], + "title": "GithubSearchTool", + "type": "object" + }, + "name": "GithubSearchTool", + "package_dependencies": [], + "run_params_schema": { + "description": "Input for GithubSearchTool.", + "properties": { + "content_types": { + "description": "Mandatory content types you want to be included search, options: [code, repo, pr, issue]", + "items": { + "type": "string" + }, + "title": "Content Types", + "type": "array" + }, + "github_repo": { + "description": "Mandatory github you want to search", + "title": "Github Repo", + "type": "string" + }, + "search_query": { + "description": "Mandatory search query you want to use to search the github repo's content", + "title": "Search Query", + "type": "string" + } + }, + "required": [ + "search_query", + "github_repo", + "content_types" + ], + "title": "GithubSearchToolSchema", + "type": "object" + } + }, + { + "description": "Scrape or crawl a website using Hyperbrowser and return the contents in properly formatted markdown or html", + "env_vars": [ + { + "default": null, + "description": "API key for Hyperbrowser services", + "name": "HYPERBROWSER_API_KEY", + "required": false + } + ], + "humanized_name": "Hyperbrowser web load tool", + "init_params_schema": { + "$defs": { + "EnvVar": { + "properties": { + "default": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Default" + }, + "description": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "required": { + "default": true, + "title": "Required", + "type": "boolean" + } + }, + "required": [ + "name", + "description" + ], + "title": "EnvVar", + "type": "object" + } + }, + "description": "HyperbrowserLoadTool.\n\nScrape or crawl web pages and load the contents with optional parameters for configuring content extraction.\nRequires the `hyperbrowser` package.\nGet your API Key from https://app.hyperbrowser.ai/\n\nArgs:\n api_key: The Hyperbrowser API key, can be set as an environment variable `HYPERBROWSER_API_KEY` or passed directly", + "properties": { + "api_key": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Api Key" + }, + "hyperbrowser": { + "anyOf": [ + {}, + { + "type": "null" + } + ], + "default": null, + "title": "Hyperbrowser" + } + }, + "title": "HyperbrowserLoadTool", + "type": "object" + }, + "name": "HyperbrowserLoadTool", + "package_dependencies": [ + "hyperbrowser" + ], + "run_params_schema": { + "properties": { + "operation": { + "description": "Operation to perform on the website. Either 'scrape' or 'crawl'", + "enum": [ + "scrape", + "crawl" + ], + "title": "Operation", + "type": "string" + }, + "params": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "description": "Optional params for scrape or crawl. For more information on the supported params, visit https://docs.hyperbrowser.ai/reference/sdks/python/scrape#start-scrape-job-and-wait or https://docs.hyperbrowser.ai/reference/sdks/python/crawl#start-crawl-job-and-wait", + "title": "Params" + }, + "url": { + "description": "Website URL", + "title": "Url", + "type": "string" + } + }, + "required": [ + "url", + "operation", + "params" + ], + "title": "HyperbrowserLoadToolSchema", + "type": "object" + } + }, + { + "description": "Invokes an CrewAI Platform Automation using API", + "env_vars": [], + "humanized_name": "invoke_amp_automation", + "init_params_schema": { + "$defs": { + "EnvVar": { + "properties": { + "default": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Default" + }, + "description": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "required": { + "default": true, + "title": "Required", + "type": "boolean" + } + }, + "required": [ + "name", + "description" + ], + "title": "EnvVar", + "type": "object" + } + }, + "description": "A CrewAI tool for invoking external crew/flows APIs.\n\nThis tool provides CrewAI Platform API integration with external crew services, supporting:\n- Dynamic input schema configuration\n- Automatic polling for task completion\n- Bearer token authentication\n- Comprehensive error handling\n\nExample:\n Basic usage:\n >>> tool = InvokeCrewAIAutomationTool(\n ... crew_api_url=\"https://api.example.com\",\n ... crew_bearer_token=\"your_token\",\n ... crew_name=\"My Crew\",\n ... crew_description=\"Description of what the crew does\"\n ... )\n \n With custom inputs:\n >>> custom_inputs = {\n ... \"param1\": Field(..., description=\"Description of param1\"),\n ... \"param2\": Field(default=\"default_value\", description=\"Description of param2\")\n ... }\n >>> tool = InvokeCrewAIAutomationTool(\n ... crew_api_url=\"https://api.example.com\",\n ... crew_bearer_token=\"your_token\",\n ... crew_name=\"My Crew\",\n ... crew_description=\"Description of what the crew does\",\n ... crew_inputs=custom_inputs\n ... )\n \n Example:\n >>> tools=[\n ... InvokeCrewAIAutomationTool(\n ... crew_api_url=\"https://canary-crew-[...].crewai.com\",\n ... crew_bearer_token=\"[Your token: abcdef012345]\",\n ... crew_name=\"State of AI Report\",\n ... crew_description=\"Retrieves a report on state of AI for a given year.\",\n ... crew_inputs={\n ... \"year\": Field(..., description=\"Year to retrieve the report for (integer)\")\n ... }\n ... )\n ... ]", + "properties": { + "crew_api_url": { + "title": "Crew Api Url", + "type": "string" + }, + "crew_bearer_token": { + "title": "Crew Bearer Token", + "type": "string" + }, + "max_polling_time": { + "default": 600, + "title": "Max Polling Time", + "type": "integer" + } + }, + "required": [ + "crew_api_url", + "crew_bearer_token" + ], + "title": "InvokeCrewAIAutomationTool", + "type": "object" + }, + "name": "InvokeCrewAIAutomationTool", + "package_dependencies": [], + "run_params_schema": { + "description": "Input schema for InvokeCrewAIAutomationTool.", + "properties": { + "prompt": { + "description": "The prompt or query to send to the crew", + "title": "Prompt", + "type": "string" + } + }, + "required": [ + "prompt" + ], + "title": "InvokeCrewAIAutomationInput", + "type": "object" + } + }, + { + "description": "A tool that can be used to semantic search a query from a JSON's content.", + "env_vars": [], + "humanized_name": "Search a JSON's content", + "init_params_schema": { + "$defs": { + "Adapter": { + "properties": {}, + "title": "Adapter", + "type": "object" + }, + "EnvVar": { + "properties": { + "default": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Default" + }, + "description": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "required": { + "default": true, + "title": "Required", + "type": "boolean" + } + }, + "required": [ + "name", + "description" + ], + "title": "EnvVar", + "type": "object" + } + }, + "properties": { + "adapter": { + "$ref": "#/$defs/Adapter" + }, + "config": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Config" + }, + "summarize": { + "default": false, + "title": "Summarize", + "type": "boolean" + } + }, + "title": "JSONSearchTool", + "type": "object" + }, + "name": "JSONSearchTool", + "package_dependencies": [], + "run_params_schema": { + "description": "Input for JSONSearchTool.", + "properties": { + "json_path": { + "description": "Mandatory json path you want to search", + "title": "Json Path", + "type": "string" + }, + "search_query": { + "description": "Mandatory search query you want to use to search the JSON's content", + "title": "Search Query", + "type": "string" + } + }, + "required": [ + "search_query", + "json_path" + ], + "title": "JSONSearchToolSchema", + "type": "object" + } + }, + { + "description": "Performs an API call to Linkup to retrieve contextual information.", + "env_vars": [ + { + "default": null, + "description": "API key for Linkup", + "name": "LINKUP_API_KEY", + "required": true + } + ], + "humanized_name": "Linkup Search Tool", + "init_params_schema": { + "$defs": { + "EnvVar": { + "properties": { + "default": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Default" + }, + "description": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "required": { + "default": true, + "title": "Required", + "type": "boolean" + } + }, + "required": [ + "name", + "description" + ], + "title": "EnvVar", + "type": "object" + } + }, + "properties": {}, + "title": "LinkupSearchTool", + "type": "object" + }, + "name": "LinkupSearchTool", + "package_dependencies": [ + "linkup-sdk" + ], + "run_params_schema": {} + }, + { + "description": "", + "env_vars": [], + "humanized_name": "LlamaIndexTool", + "init_params_schema": { + "$defs": { + "EnvVar": { + "properties": { + "default": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Default" + }, + "description": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "required": { + "default": true, + "title": "Required", + "type": "boolean" + } + }, + "required": [ + "name", + "description" + ], + "title": "EnvVar", + "type": "object" + } + }, + "description": "Tool to wrap LlamaIndex tools/query engines.", + "properties": { + "llama_index_tool": { + "title": "Llama Index Tool" + } + }, + "required": [ + "name", + "description", + "llama_index_tool" + ], + "title": "LlamaIndexTool", + "type": "object" + }, + "name": "LlamaIndexTool", + "package_dependencies": [], + "run_params_schema": {} + }, + { + "description": "A tool that can be used to semantic search a query from a MDX's content.", + "env_vars": [], + "humanized_name": "Search a MDX's content", + "init_params_schema": { + "$defs": { + "Adapter": { + "properties": {}, + "title": "Adapter", + "type": "object" + }, + "EnvVar": { + "properties": { + "default": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Default" + }, + "description": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "required": { + "default": true, + "title": "Required", + "type": "boolean" + } + }, + "required": [ + "name", + "description" + ], + "title": "EnvVar", + "type": "object" + } + }, + "properties": { + "adapter": { + "$ref": "#/$defs/Adapter" + }, + "config": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Config" + }, + "summarize": { + "default": false, + "title": "Summarize", + "type": "boolean" + } + }, + "title": "MDXSearchTool", + "type": "object" + }, + "name": "MDXSearchTool", + "package_dependencies": [], + "run_params_schema": { + "description": "Input for MDXSearchTool.", + "properties": { + "mdx": { + "description": "Mandatory mdx path you want to search", + "title": "Mdx", + "type": "string" + }, + "search_query": { + "description": "Mandatory search query you want to use to search the MDX's content", + "title": "Search Query", + "type": "string" + } + }, + "required": [ + "search_query", + "mdx" + ], + "title": "MDXSearchToolSchema", + "type": "object" + } + }, + { + "description": "A tool to perfrom a vector search on a MongoDB database for relevant information on internal documents.", + "env_vars": [ + { + "default": null, + "description": "API key for Browserbase services", + "name": "BROWSERBASE_API_KEY", + "required": false + }, + { + "default": null, + "description": "Project ID for Browserbase services", + "name": "BROWSERBASE_PROJECT_ID", + "required": false + } + ], + "humanized_name": "MongoDBVectorSearchTool", + "init_params_schema": { + "$defs": { + "EnvVar": { + "properties": { + "default": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Default" + }, + "description": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "required": { + "default": true, + "title": "Required", + "type": "boolean" + } + }, + "required": [ + "name", + "description" + ], + "title": "EnvVar", + "type": "object" + }, + "MongoDBVectorSearchConfig": { + "description": "Configuration for MongoDB vector search queries.", + "properties": { + "include_embeddings": { + "default": false, + "description": "Whether to include the embedding vector of each result in metadata.", + "title": "Include Embeddings", + "type": "boolean" + }, + "limit": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": 4, + "description": "number of documents to return.", + "title": "Limit" + }, + "oversampling_factor": { + "default": 10, + "description": "Multiple of limit used when generating number of candidates at each step in the HNSW Vector Search", + "title": "Oversampling Factor", + "type": "integer" + }, + "post_filter_pipeline": { + "anyOf": [ + { + "items": { + "additionalProperties": true, + "type": "object" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Pipeline of MongoDB aggregation stages to filter/process results after $vectorSearch.", + "title": "Post Filter Pipeline" + }, + "pre_filter": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "description": "List of MQL match expressions comparing an indexed field", + "title": "Pre Filter" + } + }, + "title": "MongoDBVectorSearchConfig", + "type": "object" + } + }, + "description": "Tool to perfrom a vector search the MongoDB database", + "properties": { + "collection_name": { + "description": "The name of the MongoDB collection", + "title": "Collection Name", + "type": "string" + }, + "connection_string": { + "description": "The connection string of the MongoDB cluster", + "title": "Connection String", + "type": "string" + }, + "database_name": { + "description": "The name of the MongoDB database", + "title": "Database Name", + "type": "string" + }, + "dimensions": { + "default": 1536, + "description": "Number of dimensions in the embedding vector", + "title": "Dimensions", + "type": "integer" + }, + "embedding_key": { + "default": "embedding", + "description": "Field that will contain the embedding for each document", + "title": "Embedding Key", + "type": "string" + }, + "embedding_model": { + "default": "text-embedding-3-large", + "description": "Text OpenAI embedding model to use", + "title": "Embedding Model", + "type": "string" + }, + "query_config": { + "anyOf": [ + { + "$ref": "#/$defs/MongoDBVectorSearchConfig" + }, + { + "type": "null" + } + ], + "default": null, + "description": "MongoDB Vector Search query configuration" + }, + "text_key": { + "default": "text", + "description": "MongoDB field that will contain the text for each document", + "title": "Text Key", + "type": "string" + }, + "vector_index_name": { + "default": "vector_index", + "description": "Name of the Atlas Search vector index", + "title": "Vector Index Name", + "type": "string" + } + }, + "required": [ + "database_name", + "collection_name", + "connection_string" + ], + "title": "MongoDBVectorSearchTool", + "type": "object" + }, + "name": "MongoDBVectorSearchTool", + "package_dependencies": [ + "mongdb" + ], + "run_params_schema": { + "description": "Input for MongoDBTool.", + "properties": { + "query": { + "description": "The query to search retrieve relevant information from the MongoDB database. Pass only the query, not the question.", + "title": "Query", + "type": "string" + } + }, + "required": [ + "query" + ], + "title": "MongoDBToolSchema", + "type": "object" + } + }, + { + "description": "Multion gives the ability for LLMs to control web browsers using natural language instructions.\n If the status is 'CONTINUE', reissue the same instruction to continue execution", + "env_vars": [ + { + "default": null, + "description": "API key for Multion", + "name": "MULTION_API_KEY", + "required": true + } + ], + "humanized_name": "Multion Browse Tool", + "init_params_schema": { + "$defs": { + "EnvVar": { + "properties": { + "default": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Default" + }, + "description": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "required": { + "default": true, + "title": "Required", + "type": "boolean" + } + }, + "required": [ + "name", + "description" + ], + "title": "EnvVar", + "type": "object" + } + }, + "description": "Tool to wrap MultiOn Browse Capabilities.", + "properties": { + "local": { + "default": false, + "title": "Local", + "type": "boolean" + }, + "max_steps": { + "default": 3, + "title": "Max Steps", + "type": "integer" + }, + "multion": { + "anyOf": [ + {}, + { + "type": "null" + } + ], + "default": null, + "title": "Multion" + }, + "session_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Session Id" + } + }, + "title": "MultiOnTool", + "type": "object" + }, + "name": "MultiOnTool", + "package_dependencies": [ + "multion" + ], + "run_params_schema": {} + }, + { + "description": "A tool that can be used to semantic search a query from a database table's content.", + "env_vars": [], + "humanized_name": "Search a database's table content", + "init_params_schema": { + "$defs": { + "Adapter": { + "properties": {}, + "title": "Adapter", + "type": "object" + }, + "EnvVar": { + "properties": { + "default": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Default" + }, + "description": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "required": { + "default": true, + "title": "Required", + "type": "boolean" + } + }, + "required": [ + "name", + "description" + ], + "title": "EnvVar", + "type": "object" + } + }, + "properties": { + "adapter": { + "$ref": "#/$defs/Adapter" + }, + "config": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Config" + }, + "db_uri": { + "description": "Mandatory database URI", + "title": "Db Uri", + "type": "string" + }, + "summarize": { + "default": false, + "title": "Summarize", + "type": "boolean" + } + }, + "required": [ + "db_uri" + ], + "title": "MySQLSearchTool", + "type": "object" + }, + "name": "MySQLSearchTool", + "package_dependencies": [], + "run_params_schema": { + "description": "Input for MySQLSearchTool.", + "properties": { + "search_query": { + "description": "Mandatory semantic search query you want to use to search the database's content", + "title": "Search Query", + "type": "string" + } + }, + "required": [ + "search_query" + ], + "title": "MySQLSearchToolSchema", + "type": "object" + } + }, + { + "description": "Converts natural language to SQL queries and executes them.", + "env_vars": [], + "humanized_name": "NL2SQLTool", + "init_params_schema": { + "$defs": { + "EnvVar": { + "properties": { + "default": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Default" + }, + "description": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "required": { + "default": true, + "title": "Required", + "type": "boolean" + } + }, + "required": [ + "name", + "description" + ], + "title": "EnvVar", + "type": "object" + } + }, + "properties": { + "columns": { + "additionalProperties": true, + "default": {}, + "title": "Columns", + "type": "object" + }, + "db_uri": { + "description": "The URI of the database to connect to.", + "title": "Database URI", + "type": "string" + }, + "tables": { + "default": [], + "items": {}, + "title": "Tables", + "type": "array" + } + }, + "required": [ + "db_uri" + ], + "title": "NL2SQLTool", + "type": "object" + }, + "name": "NL2SQLTool", + "package_dependencies": [], + "run_params_schema": { + "properties": { + "sql_query": { + "description": "The SQL query to execute.", + "title": "SQL Query", + "type": "string" + } + }, + "required": [ + "sql_query" + ], + "title": "NL2SQLToolInput", + "type": "object" + } + }, + { + "description": "This tool uses an LLM's API to extract text from an image file.", + "env_vars": [], + "humanized_name": "Optical Character Recognition Tool", + "init_params_schema": { + "$defs": { + "EnvVar": { + "properties": { + "default": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Default" + }, + "description": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "required": { + "default": true, + "title": "Required", + "type": "boolean" + } + }, + "required": [ + "name", + "description" + ], + "title": "EnvVar", + "type": "object" + } + }, + "description": "A tool for performing Optical Character Recognition on images.\n\nThis tool leverages LLMs to extract text from images. It can process\nboth local image files and images available via URLs.\n\nAttributes:\n name (str): Name of the tool.\n description (str): Description of the tool's functionality.\n args_schema (Type[BaseModel]): Pydantic schema for input validation.\n\nPrivate Attributes:\n _llm (Optional[LLM]): Language model instance for making API calls.", + "properties": {}, + "title": "OCRTool", + "type": "object" + }, + "name": "OCRTool", + "package_dependencies": [], + "run_params_schema": { + "description": "Input schema for Optical Character Recognition Tool.\n\nAttributes:\n image_path_url (str): Path to a local image file or URL of an image.\n For local files, provide the absolute or relative path.\n For remote images, provide the complete URL starting with 'http' or 'https'.", + "properties": { + "image_path_url": { + "default": "The image path or URL.", + "title": "Image Path Url", + "type": "string" + } + }, + "title": "OCRToolSchema", + "type": "object" + } + }, + { + "description": "Scrape Amazon product pages with Oxylabs Amazon Product Scraper", + "env_vars": [ + { + "default": null, + "description": "Username for Oxylabs", + "name": "OXYLABS_USERNAME", + "required": true + }, + { + "default": null, + "description": "Password for Oxylabs", + "name": "OXYLABS_PASSWORD", + "required": true + } + ], + "humanized_name": "Oxylabs Amazon Product Scraper tool", + "init_params_schema": { + "$defs": { + "EnvVar": { + "properties": { + "default": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Default" + }, + "description": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "required": { + "default": true, + "title": "Required", + "type": "boolean" + } + }, + "required": [ + "name", + "description" + ], + "title": "EnvVar", + "type": "object" + }, + "OxylabsAmazonProductScraperConfig": { + "description": "Amazon Product Scraper configuration options:\nhttps://developers.oxylabs.io/scraper-apis/web-scraper-api/targets/amazon/product", + "properties": { + "callback_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "URL to your callback endpoint.", + "title": "Callback Url" + }, + "context": { + "anyOf": [ + { + "items": {}, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Additional advanced settings and controls for specialized requirements.", + "title": "Context" + }, + "domain": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The domain to limit the search results to.", + "title": "Domain" + }, + "geo_location": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The Deliver to location.", + "title": "Geo Location" + }, + "parse": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "description": "True will return structured data.", + "title": "Parse" + }, + "parsing_instructions": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Instructions for parsing the results.", + "title": "Parsing Instructions" + }, + "render": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Enables JavaScript rendering.", + "title": "Render" + }, + "user_agent_type": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Device type and browser.", + "title": "User Agent Type" + } + }, + "title": "OxylabsAmazonProductScraperConfig", + "type": "object" + } + }, + "description": "Scrape Amazon product pages with OxylabsAmazonProductScraperTool.\n\nGet Oxylabs account:\nhttps://dashboard.oxylabs.io/en\n\nArgs:\n username (str): Oxylabs username.\n password (str): Oxylabs password.\n config: Configuration options. See ``OxylabsAmazonProductScraperConfig``", + "properties": { + "config": { + "$ref": "#/$defs/OxylabsAmazonProductScraperConfig" + }, + "oxylabs_api": { + "title": "Oxylabs Api" + } + }, + "required": [ + "oxylabs_api", + "config" + ], + "title": "OxylabsAmazonProductScraperTool", + "type": "object" + }, + "name": "OxylabsAmazonProductScraperTool", + "package_dependencies": [ + "oxylabs" + ], + "run_params_schema": { + "properties": { + "query": { + "description": "Amazon product ASIN", + "title": "Query", + "type": "string" + } + }, + "required": [ + "query" + ], + "title": "OxylabsAmazonProductScraperArgs", + "type": "object" + } + }, + { + "description": "Scrape Amazon search results with Oxylabs Amazon Search Scraper", + "env_vars": [ + { + "default": null, + "description": "Username for Oxylabs", + "name": "OXYLABS_USERNAME", + "required": true + }, + { + "default": null, + "description": "Password for Oxylabs", + "name": "OXYLABS_PASSWORD", + "required": true + } + ], + "humanized_name": "Oxylabs Amazon Search Scraper tool", + "init_params_schema": { + "$defs": { + "EnvVar": { + "properties": { + "default": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Default" + }, + "description": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "required": { + "default": true, + "title": "Required", + "type": "boolean" + } + }, + "required": [ + "name", + "description" + ], + "title": "EnvVar", + "type": "object" + }, + "OxylabsAmazonSearchScraperConfig": { + "description": "Amazon Search Scraper configuration options:\nhttps://developers.oxylabs.io/scraper-apis/web-scraper-api/targets/amazon/search", + "properties": { + "callback_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "URL to your callback endpoint.", + "title": "Callback Url" + }, + "context": { + "anyOf": [ + { + "items": {}, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Additional advanced settings and controls for specialized requirements.", + "title": "Context" + }, + "domain": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The domain to limit the search results to.", + "title": "Domain" + }, + "geo_location": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The Deliver to location.", + "title": "Geo Location" + }, + "pages": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The number of pages to scrape.", + "title": "Pages" + }, + "parse": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "description": "True will return structured data.", + "title": "Parse" + }, + "parsing_instructions": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Instructions for parsing the results.", + "title": "Parsing Instructions" + }, + "render": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Enables JavaScript rendering.", + "title": "Render" + }, + "start_page": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The starting page number.", + "title": "Start Page" + }, + "user_agent_type": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Device type and browser.", + "title": "User Agent Type" + } + }, + "title": "OxylabsAmazonSearchScraperConfig", + "type": "object" + } + }, + "description": "Scrape Amazon search results with OxylabsAmazonSearchScraperTool.\n\nGet Oxylabs account:\nhttps://dashboard.oxylabs.io/en\n\nArgs:\n username (str): Oxylabs username.\n password (str): Oxylabs password.\n config: Configuration options. See ``OxylabsAmazonSearchScraperConfig``", + "properties": { + "config": { + "$ref": "#/$defs/OxylabsAmazonSearchScraperConfig" + }, + "oxylabs_api": { + "title": "Oxylabs Api" + } + }, + "required": [ + "oxylabs_api", + "config" + ], + "title": "OxylabsAmazonSearchScraperTool", + "type": "object" + }, + "name": "OxylabsAmazonSearchScraperTool", + "package_dependencies": [ + "oxylabs" + ], + "run_params_schema": { + "properties": { + "query": { + "description": "Amazon search term", + "title": "Query", + "type": "string" + } + }, + "required": [ + "query" + ], + "title": "OxylabsAmazonSearchScraperArgs", + "type": "object" + } + }, + { + "description": "Scrape Google Search results with Oxylabs Google Search Scraper", + "env_vars": [ + { + "default": null, + "description": "Username for Oxylabs", + "name": "OXYLABS_USERNAME", + "required": true + }, + { + "default": null, + "description": "Password for Oxylabs", + "name": "OXYLABS_PASSWORD", + "required": true + } + ], + "humanized_name": "Oxylabs Google Search Scraper tool", + "init_params_schema": { + "$defs": { + "EnvVar": { + "properties": { + "default": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Default" + }, + "description": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "required": { + "default": true, + "title": "Required", + "type": "boolean" + } + }, + "required": [ + "name", + "description" + ], + "title": "EnvVar", + "type": "object" + }, + "OxylabsGoogleSearchScraperConfig": { + "description": "Google Search Scraper configuration options:\nhttps://developers.oxylabs.io/scraper-apis/web-scraper-api/targets/google/search/search", + "properties": { + "callback_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "URL to your callback endpoint.", + "title": "Callback Url" + }, + "context": { + "anyOf": [ + { + "items": {}, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Additional advanced settings and controls for specialized requirements.", + "title": "Context" + }, + "domain": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The domain to limit the search results to.", + "title": "Domain" + }, + "geo_location": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The Deliver to location.", + "title": "Geo Location" + }, + "limit": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Number of results to retrieve in each page.", + "title": "Limit" + }, + "pages": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The number of pages to scrape.", + "title": "Pages" + }, + "parse": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "description": "True will return structured data.", + "title": "Parse" + }, + "parsing_instructions": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Instructions for parsing the results.", + "title": "Parsing Instructions" + }, + "render": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Enables JavaScript rendering.", + "title": "Render" + }, + "start_page": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The starting page number.", + "title": "Start Page" + }, + "user_agent_type": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Device type and browser.", + "title": "User Agent Type" + } + }, + "title": "OxylabsGoogleSearchScraperConfig", + "type": "object" + } + }, + "description": "Scrape Google Search results with OxylabsGoogleSearchScraperTool.\n\nGet Oxylabs account:\nhttps://dashboard.oxylabs.io/en\n\nArgs:\n username (str): Oxylabs username.\n password (str): Oxylabs password.\n config: Configuration options. See ``OxylabsGoogleSearchScraperConfig``", + "properties": { + "config": { + "$ref": "#/$defs/OxylabsGoogleSearchScraperConfig" + }, + "oxylabs_api": { + "title": "Oxylabs Api" + } + }, + "required": [ + "oxylabs_api", + "config" + ], + "title": "OxylabsGoogleSearchScraperTool", + "type": "object" + }, + "name": "OxylabsGoogleSearchScraperTool", + "package_dependencies": [ + "oxylabs" + ], + "run_params_schema": { + "properties": { + "query": { + "description": "Search query", + "title": "Query", + "type": "string" + } + }, + "required": [ + "query" + ], + "title": "OxylabsGoogleSearchScraperArgs", + "type": "object" + } + }, + { + "description": "Scrape any url with Oxylabs Universal Scraper", + "env_vars": [ + { + "default": null, + "description": "Username for Oxylabs", + "name": "OXYLABS_USERNAME", + "required": true + }, + { + "default": null, + "description": "Password for Oxylabs", + "name": "OXYLABS_PASSWORD", + "required": true + } + ], + "humanized_name": "Oxylabs Universal Scraper tool", + "init_params_schema": { + "$defs": { + "EnvVar": { + "properties": { + "default": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Default" + }, + "description": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "required": { + "default": true, + "title": "Required", + "type": "boolean" + } + }, + "required": [ + "name", + "description" + ], + "title": "EnvVar", + "type": "object" + }, + "OxylabsUniversalScraperConfig": { + "description": "Universal Scraper configuration options:\nhttps://developers.oxylabs.io/scraper-apis/web-scraper-api/other-websites", + "properties": { + "callback_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "URL to your callback endpoint.", + "title": "Callback Url" + }, + "context": { + "anyOf": [ + { + "items": {}, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Additional advanced settings and controls for specialized requirements.", + "title": "Context" + }, + "geo_location": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The Deliver to location.", + "title": "Geo Location" + }, + "parse": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "description": "True will return structured data.", + "title": "Parse" + }, + "parsing_instructions": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Instructions for parsing the results.", + "title": "Parsing Instructions" + }, + "render": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Enables JavaScript rendering.", + "title": "Render" + }, + "user_agent_type": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Device type and browser.", + "title": "User Agent Type" + } + }, + "title": "OxylabsUniversalScraperConfig", + "type": "object" + } + }, + "description": "Scrape any website with OxylabsUniversalScraperTool.\n\nGet Oxylabs account:\nhttps://dashboard.oxylabs.io/en\n\nArgs:\n username (str): Oxylabs username.\n password (str): Oxylabs password.\n config: Configuration options. See ``OxylabsUniversalScraperConfig``", + "properties": { + "config": { + "$ref": "#/$defs/OxylabsUniversalScraperConfig" + }, + "oxylabs_api": { + "title": "Oxylabs Api" + } + }, + "required": [ + "oxylabs_api", + "config" + ], + "title": "OxylabsUniversalScraperTool", + "type": "object" + }, + "name": "OxylabsUniversalScraperTool", + "package_dependencies": [ + "oxylabs" + ], + "run_params_schema": { + "properties": { + "url": { + "description": "Website URL", + "title": "Url", + "type": "string" + } + }, + "required": [ + "url" + ], + "title": "OxylabsUniversalScraperArgs", + "type": "object" + } + }, + { + "description": "A tool that can be used to semantic search a query from a PDF's content.", + "env_vars": [], + "humanized_name": "Search a PDF's content", + "init_params_schema": { + "$defs": { + "Adapter": { + "properties": {}, + "title": "Adapter", + "type": "object" + }, + "EnvVar": { + "properties": { + "default": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Default" + }, + "description": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "required": { + "default": true, + "title": "Required", + "type": "boolean" + } + }, + "required": [ + "name", + "description" + ], + "title": "EnvVar", + "type": "object" + } + }, + "properties": { + "adapter": { + "$ref": "#/$defs/Adapter" + }, + "config": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Config" + }, + "summarize": { + "default": false, + "title": "Summarize", + "type": "boolean" + } + }, + "title": "PDFSearchTool", + "type": "object" + }, + "name": "PDFSearchTool", + "package_dependencies": [], + "run_params_schema": { + "description": "Input for PDFSearchTool.", + "properties": { + "pdf": { + "description": "Mandatory pdf path you want to search", + "title": "Pdf", + "type": "string" + }, + "query": { + "description": "Mandatory query you want to use to search the PDF's content", + "title": "Query", + "type": "string" + } + }, + "required": [ + "query", + "pdf" + ], + "title": "PDFSearchToolSchema", + "type": "object" + } + }, + { + "description": "A tool that can be used to semantic search a query from a database table's content.", + "env_vars": [], + "humanized_name": "Search a database's table content", + "init_params_schema": { + "$defs": { + "Adapter": { + "properties": {}, + "title": "Adapter", + "type": "object" + }, + "EnvVar": { + "properties": { + "default": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Default" + }, + "description": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "required": { + "default": true, + "title": "Required", + "type": "boolean" + } + }, + "required": [ + "name", + "description" + ], + "title": "EnvVar", + "type": "object" + } + }, + "properties": { + "adapter": { + "$ref": "#/$defs/Adapter" + }, + "config": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Config" + }, + "db_uri": { + "description": "Mandatory database URI", + "title": "Db Uri", + "type": "string" + }, + "summarize": { + "default": false, + "title": "Summarize", + "type": "boolean" + } + }, + "required": [ + "db_uri" + ], + "title": "PGSearchTool", + "type": "object" + }, + "name": "PGSearchTool", + "package_dependencies": [], + "run_params_schema": { + "description": "Input for PGSearchTool.", + "properties": { + "search_query": { + "description": "Mandatory semantic search query you want to use to search the database's content", + "title": "Search Query", + "type": "string" + } + }, + "required": [ + "search_query" + ], + "title": "PGSearchToolSchema", + "type": "object" + } + }, + { + "description": "Search the web using Parallel's Search API (v1beta). Returns ranked results with compressed excerpts optimized for LLMs.", + "env_vars": [ + { + "default": null, + "description": "API key for Parallel", + "name": "PARALLEL_API_KEY", + "required": true + } + ], + "humanized_name": "Parallel Web Search Tool", + "init_params_schema": { + "$defs": { + "EnvVar": { + "properties": { + "default": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Default" + }, + "description": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "required": { + "default": true, + "title": "Required", + "type": "boolean" + } + }, + "required": [ + "name", + "description" + ], + "title": "EnvVar", + "type": "object" + } + }, + "properties": { + "search_url": { + "default": "https://api.parallel.ai/v1beta/search", + "title": "Search Url", + "type": "string" + } + }, + "title": "ParallelSearchTool", + "type": "object" + }, + "name": "ParallelSearchTool", + "package_dependencies": [ + "requests" + ], + "run_params_schema": { + "description": "Input schema for ParallelSearchTool using the Search API (v1beta).\n\nAt least one of objective or search_queries is required.", + "properties": { + "max_chars_per_result": { + "default": 6000, + "description": "Maximum characters per result excerpt (values >30000 not guaranteed)", + "minimum": 100, + "title": "Max Chars Per Result", + "type": "integer" + }, + "max_results": { + "default": 10, + "description": "Maximum number of search results to return (processor limits apply)", + "maximum": 40, + "minimum": 1, + "title": "Max Results", + "type": "integer" + }, + "objective": { + "anyOf": [ + { + "maxLength": 5000, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Natural-language goal for the web research (<=5000 chars)", + "title": "Objective" + }, + "processor": { + "default": "base", + "description": "Search processor: 'base' (fast/low cost) or 'pro' (higher quality/freshness)", + "pattern": "^(base|pro)$", + "title": "Processor", + "type": "string" + }, + "search_queries": { + "anyOf": [ + { + "items": { + "maxLength": 200, + "type": "string" + }, + "maxItems": 5, + "minItems": 1, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional list of keyword queries (<=5 items, each <=200 chars)", + "title": "Search Queries" + }, + "source_policy": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional source policy configuration", + "title": "Source Policy" + } + }, + "title": "ParallelSearchInput", + "type": "object" + } + }, + { + "description": "", + "env_vars": [ + { + "default": null, + "description": "API key for Patronus evaluation services", + "name": "PATRONUS_API_KEY", + "required": true + } + ], + "humanized_name": "Patronus Evaluation Tool", + "init_params_schema": { + "$defs": { + "EnvVar": { + "properties": { + "default": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Default" + }, + "description": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "required": { + "default": true, + "title": "Required", + "type": "boolean" + } + }, + "required": [ + "name", + "description" + ], + "title": "EnvVar", + "type": "object" + } + }, + "properties": { + "criteria": { + "default": [], + "items": { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + "title": "Criteria", + "type": "array" + }, + "evaluate_url": { + "default": "https://api.patronus.ai/v1/evaluate", + "title": "Evaluate Url", + "type": "string" + }, + "evaluators": { + "default": [], + "items": { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + "title": "Evaluators", + "type": "array" + } + }, + "title": "PatronusEvalTool", + "type": "object" + }, + "name": "PatronusEvalTool", + "package_dependencies": [], + "run_params_schema": {} + }, + { + "description": "This tool calls the Patronus Evaluation API that takes the following arguments:", + "env_vars": [], + "humanized_name": "Call Patronus API tool for evaluation of model inputs and outputs", + "init_params_schema": { + "$defs": { + "EnvVar": { + "properties": { + "default": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Default" + }, + "description": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "required": { + "default": true, + "title": "Required", + "type": "boolean" + } + }, + "required": [ + "name", + "description" + ], + "title": "EnvVar", + "type": "object" + } + }, + "description": "PatronusEvalTool is a tool to automatically evaluate and score agent interactions.\n\nResults are logged to the Patronus platform at app.patronus.ai", + "properties": { + "evaluate_url": { + "default": "https://api.patronus.ai/v1/evaluate", + "title": "Evaluate Url", + "type": "string" + }, + "evaluators": { + "default": [], + "items": { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + "title": "Evaluators", + "type": "array" + } + }, + "title": "PatronusPredefinedCriteriaEvalTool", + "type": "object" + }, + "name": "PatronusPredefinedCriteriaEvalTool", + "package_dependencies": [], + "run_params_schema": { + "properties": { + "evaluated_model_gold_answer": { + "additionalProperties": true, + "description": "The agent's gold answer only if available", + "title": "Evaluated Model Gold Answer", + "type": "object" + }, + "evaluated_model_input": { + "additionalProperties": true, + "description": "The agent's task description in simple text", + "title": "Evaluated Model Input", + "type": "object" + }, + "evaluated_model_output": { + "additionalProperties": true, + "description": "The agent's output of the task", + "title": "Evaluated Model Output", + "type": "object" + }, + "evaluated_model_retrieved_context": { + "additionalProperties": true, + "description": "The agent's context", + "title": "Evaluated Model Retrieved Context", + "type": "object" + }, + "evaluators": { + "description": "List of dictionaries containing the evaluator and criteria to evaluate the model input and output. An example input for this field: [{'evaluator': '[evaluator-from-user]', 'criteria': '[criteria-from-user]'}]", + "items": { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + "title": "Evaluators", + "type": "array" + } + }, + "required": [ + "evaluated_model_input", + "evaluated_model_output", + "evaluated_model_retrieved_context", + "evaluated_model_gold_answer", + "evaluators" + ], + "title": "FixedBaseToolSchema", + "type": "object" + } + }, + { + "description": "A tool to search the Qdrant database for relevant information on internal documents.", + "env_vars": [], + "humanized_name": "QdrantVectorSearchTool", + "init_params_schema": { + "$defs": { + "EnvVar": { + "properties": { + "default": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Default" + }, + "description": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "required": { + "default": true, + "title": "Required", + "type": "boolean" + } + }, + "required": [ + "name", + "description" + ], + "title": "EnvVar", + "type": "object" + } + }, + "description": "Tool to query and filter results from a Qdrant database.\n\nThis tool enables vector similarity search on internal documents stored in Qdrant,\nwith optional filtering capabilities.\n\nAttributes:\n client: Configured QdrantClient instance\n collection_name: Name of the Qdrant collection to search\n limit: Maximum number of results to return\n score_threshold: Minimum similarity score threshold\n qdrant_url: Qdrant server URL\n qdrant_api_key: Authentication key for Qdrant", + "properties": { + "collection_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Collection Name" + }, + "filter_by": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Filter By" + }, + "filter_value": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Filter Value" + }, + "limit": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": 3, + "title": "Limit" + }, + "qdrant_api_key": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The API key for the Qdrant server", + "title": "Qdrant Api Key" + }, + "qdrant_url": { + "description": "The URL of the Qdrant server", + "title": "Qdrant Url", + "type": "string" + }, + "query": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Query" + }, + "score_threshold": { + "default": 0.35, + "title": "Score Threshold", + "type": "number" + } + }, + "required": [ + "qdrant_url" + ], + "title": "QdrantVectorSearchTool", + "type": "object" + }, + "name": "QdrantVectorSearchTool", + "package_dependencies": [ + "qdrant-client" + ], + "run_params_schema": { + "description": "Input for QdrantTool.", + "properties": { + "filter_by": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Filter by properties. Pass only the properties, not the question.", + "title": "Filter By" + }, + "filter_value": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Filter by value. Pass only the value, not the question.", + "title": "Filter Value" + }, + "query": { + "description": "The query to search retrieve relevant information from the Qdrant database. Pass only the query, not the question.", + "title": "Query", + "type": "string" + } + }, + "required": [ + "query" + ], + "title": "QdrantToolSchema", + "type": "object" + } + }, + { + "description": "A knowledge base that can be used to answer questions.", + "env_vars": [], + "humanized_name": "Knowledge base", + "init_params_schema": { + "$defs": { + "Adapter": { + "properties": {}, + "title": "Adapter", + "type": "object" + }, + "EnvVar": { + "properties": { + "default": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Default" + }, + "description": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "required": { + "default": true, + "title": "Required", + "type": "boolean" + } + }, + "required": [ + "name", + "description" + ], + "title": "EnvVar", + "type": "object" + } + }, + "properties": { + "adapter": { + "$ref": "#/$defs/Adapter" + }, + "config": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Config" + }, + "summarize": { + "default": false, + "title": "Summarize", + "type": "boolean" + } + }, + "title": "RagTool", + "type": "object" + }, + "name": "RagTool", + "package_dependencies": [], + "run_params_schema": {} + }, + { + "description": "A tool that can be used to read a website content.", + "env_vars": [], + "humanized_name": "Read a website content", + "init_params_schema": { + "$defs": { + "EnvVar": { + "properties": { + "default": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Default" + }, + "description": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "required": { + "default": true, + "title": "Required", + "type": "boolean" + } + }, + "required": [ + "name", + "description" + ], + "title": "EnvVar", + "type": "object" + } + }, + "properties": { + "cookies": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Cookies" + }, + "css_element": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Css Element" + }, + "headers": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "default": { + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9", + "Accept-Encoding": "gzip, deflate, br", + "Accept-Language": "en-US,en;q=0.9", + "Connection": "keep-alive", + "Referer": "https://www.google.com/", + "Upgrade-Insecure-Requests": "1", + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.110 Safari/537.36" + }, + "title": "Headers" + }, + "website_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Website Url" + } + }, + "title": "ScrapeElementFromWebsiteTool", + "type": "object" + }, + "name": "ScrapeElementFromWebsiteTool", + "package_dependencies": [], + "run_params_schema": { + "description": "Input for ScrapeElementFromWebsiteTool.", + "properties": { + "css_element": { + "description": "Mandatory css reference for element to scrape from the website", + "title": "Css Element", + "type": "string" + }, + "website_url": { + "description": "Mandatory website url to read the file", + "title": "Website Url", + "type": "string" + } + }, + "required": [ + "website_url", + "css_element" + ], + "title": "ScrapeElementFromWebsiteToolSchema", + "type": "object" + } + }, + { + "description": "A tool that can be used to read a website content.", + "env_vars": [], + "humanized_name": "Read website content", + "init_params_schema": { + "$defs": { + "EnvVar": { + "properties": { + "default": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Default" + }, + "description": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "required": { + "default": true, + "title": "Required", + "type": "boolean" + } + }, + "required": [ + "name", + "description" + ], + "title": "EnvVar", + "type": "object" + } + }, + "properties": { + "cookies": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Cookies" + }, + "headers": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "default": { + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9", + "Accept-Language": "en-US,en;q=0.9", + "Connection": "keep-alive", + "Referer": "https://www.google.com/", + "Upgrade-Insecure-Requests": "1", + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.110 Safari/537.36" + }, + "title": "Headers" + }, + "website_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Website Url" + } + }, + "title": "ScrapeWebsiteTool", + "type": "object" + }, + "name": "ScrapeWebsiteTool", + "package_dependencies": [], + "run_params_schema": { + "description": "Input for ScrapeWebsiteTool.", + "properties": { + "website_url": { + "description": "Mandatory website url to read the file", + "title": "Website Url", + "type": "string" + } + }, + "required": [ + "website_url" + ], + "title": "ScrapeWebsiteToolSchema", + "type": "object" + } + }, + { + "description": "A tool that uses Scrapegraph AI to intelligently scrape website content.", + "env_vars": [ + { + "default": null, + "description": "API key for Scrapegraph AI services", + "name": "SCRAPEGRAPH_API_KEY", + "required": false + } + ], + "humanized_name": "Scrapegraph website scraper", + "init_params_schema": { + "$defs": { + "EnvVar": { + "properties": { + "default": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Default" + }, + "description": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "required": { + "default": true, + "title": "Required", + "type": "boolean" + } + }, + "required": [ + "name", + "description" + ], + "title": "EnvVar", + "type": "object" + } + }, + "description": "A tool that uses Scrapegraph AI to intelligently scrape website content.\n\nRaises:\n ValueError: If API key is missing or URL format is invalid\n RateLimitError: If API rate limits are exceeded\n RuntimeError: If scraping operation fails", + "properties": { + "api_key": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Api Key" + }, + "enable_logging": { + "default": false, + "title": "Enable Logging", + "type": "boolean" + }, + "user_prompt": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "User Prompt" + }, + "website_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Website Url" + } + }, + "title": "ScrapegraphScrapeTool", + "type": "object" + }, + "name": "ScrapegraphScrapeTool", + "package_dependencies": [ + "scrapegraph-py" + ], + "run_params_schema": { + "description": "Input for ScrapegraphScrapeTool.", + "properties": { + "user_prompt": { + "default": "Extract the main content of the webpage", + "description": "Prompt to guide the extraction of content", + "title": "User Prompt", + "type": "string" + }, + "website_url": { + "description": "Mandatory website url to scrape", + "title": "Website Url", + "type": "string" + } + }, + "required": [ + "website_url" + ], + "title": "ScrapegraphScrapeToolSchema", + "type": "object" + } + }, + { + "description": "Scrape a webpage url using Scrapfly and return its content as markdown or text", + "env_vars": [ + { + "default": null, + "description": "API key for Scrapfly", + "name": "SCRAPFLY_API_KEY", + "required": true + } + ], + "humanized_name": "Scrapfly web scraping API tool", + "init_params_schema": { + "$defs": { + "EnvVar": { + "properties": { + "default": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Default" + }, + "description": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "required": { + "default": true, + "title": "Required", + "type": "boolean" + } + }, + "required": [ + "name", + "description" + ], + "title": "EnvVar", + "type": "object" + } + }, + "properties": { + "api_key": { + "default": null, + "title": "Api Key", + "type": "string" + }, + "scrapfly": { + "anyOf": [ + {}, + { + "type": "null" + } + ], + "default": null, + "title": "Scrapfly" + } + }, + "title": "ScrapflyScrapeWebsiteTool", + "type": "object" + }, + "name": "ScrapflyScrapeWebsiteTool", + "package_dependencies": [ + "scrapfly-sdk" + ], + "run_params_schema": { + "properties": { + "ignore_scrape_failures": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": null, + "description": "whether to ignore failures", + "title": "Ignore Scrape Failures" + }, + "scrape_config": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Scrapfly request scrape config", + "title": "Scrape Config" + }, + "scrape_format": { + "anyOf": [ + { + "enum": [ + "raw", + "markdown", + "text" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": "markdown", + "description": "Webpage extraction format", + "title": "Scrape Format" + }, + "url": { + "description": "Webpage URL", + "title": "Url", + "type": "string" + } + }, + "required": [ + "url" + ], + "title": "ScrapflyScrapeWebsiteToolSchema", + "type": "object" + } + }, + { + "description": "A tool that can be used to read a website content.", + "env_vars": [], + "humanized_name": "Read a website content", + "init_params_schema": { + "$defs": { + "EnvVar": { + "properties": { + "default": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Default" + }, + "description": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "required": { + "default": true, + "title": "Required", + "type": "boolean" + } + }, + "required": [ + "name", + "description" + ], + "title": "EnvVar", + "type": "object" + } + }, + "properties": { + "cookie": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Cookie" + }, + "css_element": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Css Element" + }, + "driver": { + "anyOf": [ + {}, + { + "type": "null" + } + ], + "default": null, + "title": "Driver" + }, + "return_html": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": false, + "title": "Return Html" + }, + "wait_time": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": 3, + "title": "Wait Time" + }, + "website_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Website Url" + } + }, + "title": "SeleniumScrapingTool", + "type": "object" + }, + "name": "SeleniumScrapingTool", + "package_dependencies": [ + "selenium", + "webdriver-manager" + ], + "run_params_schema": { + "description": "Input for SeleniumScrapingTool.", + "properties": { + "css_element": { + "description": "Mandatory css reference for element to scrape from the website", + "title": "Css Element", + "type": "string" + }, + "website_url": { + "description": "Mandatory website url to read the file. Must start with http:// or https://", + "title": "Website Url", + "type": "string" + } + }, + "required": [ + "website_url", + "css_element" + ], + "title": "SeleniumScrapingToolSchema", + "type": "object" + } + }, + { + "description": "A tool to perform to perform a Google search with a search_query.", + "env_vars": [ + { + "default": null, + "description": "API key for SerpApi searches", + "name": "SERPAPI_API_KEY", + "required": true + } + ], + "humanized_name": "Google Search", + "init_params_schema": { + "$defs": { + "EnvVar": { + "properties": { + "default": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Default" + }, + "description": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "required": { + "default": true, + "title": "Required", + "type": "boolean" + } + }, + "required": [ + "name", + "description" + ], + "title": "EnvVar", + "type": "object" + } + }, + "properties": { + "client": { + "anyOf": [ + {}, + { + "type": "null" + } + ], + "default": null, + "title": "Client" + } + }, + "title": "SerpApiGoogleSearchTool", + "type": "object" + }, + "name": "SerpApiGoogleSearchTool", + "package_dependencies": [ + "serpapi" + ], + "run_params_schema": { + "description": "Input for Google Search.", + "properties": { + "location": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Location you want the search to be performed in.", + "title": "Location" + }, + "search_query": { + "description": "Mandatory search query you want to use to Google search.", + "title": "Search Query", + "type": "string" + } + }, + "required": [ + "search_query" + ], + "title": "SerpApiGoogleSearchToolSchema", + "type": "object" + } + }, + { + "description": "A tool to perform search on Google shopping with a search_query.", + "env_vars": [ + { + "default": null, + "description": "API key for SerpApi searches", + "name": "SERPAPI_API_KEY", + "required": true + } + ], + "humanized_name": "Google Shopping", + "init_params_schema": { + "$defs": { + "EnvVar": { + "properties": { + "default": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Default" + }, + "description": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "required": { + "default": true, + "title": "Required", + "type": "boolean" + } + }, + "required": [ + "name", + "description" + ], + "title": "EnvVar", + "type": "object" + } + }, + "properties": { + "client": { + "anyOf": [ + {}, + { + "type": "null" + } + ], + "default": null, + "title": "Client" + } + }, + "title": "SerpApiGoogleShoppingTool", + "type": "object" + }, + "name": "SerpApiGoogleShoppingTool", + "package_dependencies": [ + "serpapi" + ], + "run_params_schema": { + "description": "Input for Google Shopping.", + "properties": { + "location": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Location you want the search to be performed in.", + "title": "Location" + }, + "search_query": { + "description": "Mandatory search query you want to use to Google shopping.", + "title": "Search Query", + "type": "string" + } + }, + "required": [ + "search_query" + ], + "title": "SerpApiGoogleShoppingToolSchema", + "type": "object" + } + }, + { + "description": "A tool that can be used to search the internet with a search_query. Supports different search types: 'search' (default), 'news'", + "env_vars": [ + { + "default": null, + "description": "API key for Serper", + "name": "SERPER_API_KEY", + "required": true + } + ], + "humanized_name": "Search the internet with Serper", + "init_params_schema": { + "$defs": { + "EnvVar": { + "properties": { + "default": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Default" + }, + "description": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "required": { + "default": true, + "title": "Required", + "type": "boolean" + } + }, + "required": [ + "name", + "description" + ], + "title": "EnvVar", + "type": "object" + } + }, + "properties": { + "base_url": { + "default": "https://google.serper.dev", + "title": "Base Url", + "type": "string" + }, + "country": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "", + "title": "Country" + }, + "locale": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "", + "title": "Locale" + }, + "location": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "", + "title": "Location" + }, + "n_results": { + "default": 10, + "title": "N Results", + "type": "integer" + }, + "save_file": { + "default": false, + "title": "Save File", + "type": "boolean" + }, + "search_type": { + "default": "search", + "title": "Search Type", + "type": "string" + } + }, + "title": "SerperDevTool", + "type": "object" + }, + "name": "SerperDevTool", + "package_dependencies": [], + "run_params_schema": { + "description": "Input for SerperDevTool.", + "properties": { + "search_query": { + "description": "Mandatory search query you want to use to search the internet", + "title": "Search Query", + "type": "string" + } + }, + "required": [ + "search_query" + ], + "title": "SerperDevToolSchema", + "type": "object" + } + }, + { + "description": "Scrapes website content using Serper's scraping API. This tool can extract clean, readable content from any website URL, optionally including markdown formatting for better structure.", + "env_vars": [ + { + "default": null, + "description": "API key for Serper", + "name": "SERPER_API_KEY", + "required": true + } + ], + "humanized_name": "serper_scrape_website", + "init_params_schema": { + "$defs": { + "EnvVar": { + "properties": { + "default": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Default" + }, + "description": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "required": { + "default": true, + "title": "Required", + "type": "boolean" + } + }, + "required": [ + "name", + "description" + ], + "title": "EnvVar", + "type": "object" + } + }, + "properties": {}, + "title": "SerperScrapeWebsiteTool", + "type": "object" + }, + "name": "SerperScrapeWebsiteTool", + "package_dependencies": [], + "run_params_schema": { + "description": "Input schema for SerperScrapeWebsite.", + "properties": { + "include_markdown": { + "default": true, + "description": "Whether to include markdown formatting in the scraped content", + "title": "Include Markdown", + "type": "boolean" + }, + "url": { + "description": "The URL of the website to scrape", + "title": "Url", + "type": "string" + } + }, + "required": [ + "url" + ], + "title": "SerperScrapeWebsiteInput", + "type": "object" + } + }, + { + "description": "A tool to perform to perform a job search in the US with a search_query.", + "env_vars": [ + { + "default": null, + "description": "API key for Serply services", + "name": "SERPLY_API_KEY", + "required": true + } + ], + "humanized_name": "Job Search", + "init_params_schema": { + "$defs": { + "Adapter": { + "properties": {}, + "title": "Adapter", + "type": "object" + }, + "EnvVar": { + "properties": { + "default": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Default" + }, + "description": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "required": { + "default": true, + "title": "Required", + "type": "boolean" + } + }, + "required": [ + "name", + "description" + ], + "title": "EnvVar", + "type": "object" + } + }, + "properties": { + "adapter": { + "$ref": "#/$defs/Adapter" + }, + "config": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Config" + }, + "headers": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "default": {}, + "title": "Headers" + }, + "proxy_location": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "US", + "title": "Proxy Location" + }, + "request_url": { + "default": "https://api.serply.io/v1/job/search/", + "title": "Request Url", + "type": "string" + }, + "summarize": { + "default": false, + "title": "Summarize", + "type": "boolean" + } + }, + "title": "SerplyJobSearchTool", + "type": "object" + }, + "name": "SerplyJobSearchTool", + "package_dependencies": [], + "run_params_schema": { + "description": "Input for Job Search.", + "properties": { + "search_query": { + "description": "Mandatory search query you want to use to fetch jobs postings.", + "title": "Search Query", + "type": "string" + } + }, + "required": [ + "search_query" + ], + "title": "SerplyJobSearchToolSchema", + "type": "object" + } + }, + { + "description": "A tool to perform News article search with a search_query.", + "env_vars": [ + { + "default": null, + "description": "API key for Serply services", + "name": "SERPLY_API_KEY", + "required": true + } + ], + "humanized_name": "News Search", + "init_params_schema": { + "$defs": { + "EnvVar": { + "properties": { + "default": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Default" + }, + "description": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "required": { + "default": true, + "title": "Required", + "type": "boolean" + } + }, + "required": [ + "name", + "description" + ], + "title": "EnvVar", + "type": "object" + } + }, + "properties": { + "headers": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "default": {}, + "title": "Headers" + }, + "limit": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": 10, + "title": "Limit" + }, + "proxy_location": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "US", + "title": "Proxy Location" + }, + "search_url": { + "default": "https://api.serply.io/v1/news/", + "title": "Search Url", + "type": "string" + } + }, + "title": "SerplyNewsSearchTool", + "type": "object" + }, + "name": "SerplyNewsSearchTool", + "package_dependencies": [], + "run_params_schema": { + "description": "Input for Serply News Search.", + "properties": { + "search_query": { + "description": "Mandatory search query you want to use to fetch news articles", + "title": "Search Query", + "type": "string" + } + }, + "required": [ + "search_query" + ], + "title": "SerplyNewsSearchToolSchema", + "type": "object" + } + }, + { + "description": "A tool to perform scholarly literature search with a search_query.", + "env_vars": [ + { + "default": null, + "description": "API key for Serply services", + "name": "SERPLY_API_KEY", + "required": true + } + ], + "humanized_name": "Scholar Search", + "init_params_schema": { + "$defs": { + "EnvVar": { + "properties": { + "default": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Default" + }, + "description": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "required": { + "default": true, + "title": "Required", + "type": "boolean" + } + }, + "required": [ + "name", + "description" + ], + "title": "EnvVar", + "type": "object" + } + }, + "properties": { + "headers": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "default": {}, + "title": "Headers" + }, + "hl": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "us", + "title": "Hl" + }, + "proxy_location": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "US", + "title": "Proxy Location" + }, + "search_url": { + "default": "https://api.serply.io/v1/scholar/", + "title": "Search Url", + "type": "string" + } + }, + "title": "SerplyScholarSearchTool", + "type": "object" + }, + "name": "SerplyScholarSearchTool", + "package_dependencies": [], + "run_params_schema": { + "description": "Input for Serply Scholar Search.", + "properties": { + "search_query": { + "description": "Mandatory search query you want to use to fetch scholarly literature", + "title": "Search Query", + "type": "string" + } + }, + "required": [ + "search_query" + ], + "title": "SerplyScholarSearchToolSchema", + "type": "object" + } + }, + { + "description": "A tool to perform Google search with a search_query.", + "env_vars": [ + { + "default": null, + "description": "API key for Serply services", + "name": "SERPLY_API_KEY", + "required": true + } + ], + "humanized_name": "Google Search", + "init_params_schema": { + "$defs": { + "EnvVar": { + "properties": { + "default": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Default" + }, + "description": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "required": { + "default": true, + "title": "Required", + "type": "boolean" + } + }, + "required": [ + "name", + "description" + ], + "title": "EnvVar", + "type": "object" + } + }, + "properties": { + "device_type": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "desktop", + "title": "Device Type" + }, + "headers": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "default": {}, + "title": "Headers" + }, + "hl": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "us", + "title": "Hl" + }, + "limit": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": 10, + "title": "Limit" + }, + "proxy_location": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "US", + "title": "Proxy Location" + }, + "query_payload": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "default": {}, + "title": "Query Payload" + }, + "search_url": { + "default": "https://api.serply.io/v1/search/", + "title": "Search Url", + "type": "string" + } + }, + "title": "SerplyWebSearchTool", + "type": "object" + }, + "name": "SerplyWebSearchTool", + "package_dependencies": [], + "run_params_schema": { + "description": "Input for Serply Web Search.", + "properties": { + "search_query": { + "description": "Mandatory search query you want to use to Google search", + "title": "Search Query", + "type": "string" + } + }, + "required": [ + "search_query" + ], + "title": "SerplyWebSearchToolSchema", + "type": "object" + } + }, + { + "description": "A tool to perform convert a webpage to markdown to make it easier for LLMs to understand", + "env_vars": [ + { + "default": null, + "description": "API key for Serply services", + "name": "SERPLY_API_KEY", + "required": true + } + ], + "humanized_name": "Webpage to Markdown", + "init_params_schema": { + "$defs": { + "Adapter": { + "properties": {}, + "title": "Adapter", + "type": "object" + }, + "EnvVar": { + "properties": { + "default": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Default" + }, + "description": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "required": { + "default": true, + "title": "Required", + "type": "boolean" + } + }, + "required": [ + "name", + "description" + ], + "title": "EnvVar", + "type": "object" + } + }, + "properties": { + "adapter": { + "$ref": "#/$defs/Adapter" + }, + "config": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Config" + }, + "headers": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "default": {}, + "title": "Headers" + }, + "proxy_location": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "US", + "title": "Proxy Location" + }, + "request_url": { + "default": "https://api.serply.io/v1/request", + "title": "Request Url", + "type": "string" + }, + "summarize": { + "default": false, + "title": "Summarize", + "type": "boolean" + } + }, + "title": "SerplyWebpageToMarkdownTool", + "type": "object" + }, + "name": "SerplyWebpageToMarkdownTool", + "package_dependencies": [], + "run_params_schema": { + "description": "Input for Serply Search.", + "properties": { + "url": { + "description": "Mandatory url you want to use to fetch and convert to markdown", + "title": "Url", + "type": "string" + } + }, + "required": [ + "url" + ], + "title": "SerplyWebpageToMarkdownToolSchema", + "type": "object" + } + }, + { + "description": "A tool that can be used to semantic search a query from a database.", + "env_vars": [ + { + "default": null, + "description": "A comprehensive URL string that can encapsulate host, port, username, password, and database information, often used in environments like SingleStore notebooks or specific frameworks. For example: 'me:p455w0rd@s2-host.com/my_db'", + "name": "SINGLESTOREDB_URL", + "required": false + }, + { + "default": null, + "description": "Specifies the hostname, IP address, or URL of the SingleStoreDB workspace or cluster", + "name": "SINGLESTOREDB_HOST", + "required": false + }, + { + "default": null, + "description": "Defines the port number on which the SingleStoreDB server is listening", + "name": "SINGLESTOREDB_PORT", + "required": false + }, + { + "default": null, + "description": "Specifies the database user name", + "name": "SINGLESTOREDB_USER", + "required": false + }, + { + "default": null, + "description": "Specifies the database user password", + "name": "SINGLESTOREDB_PASSWORD", + "required": false + }, + { + "default": null, + "description": "Name of the database to connect to", + "name": "SINGLESTOREDB_DATABASE", + "required": false + }, + { + "default": null, + "description": "File containing SSL key", + "name": "SINGLESTOREDB_SSL_KEY", + "required": false + }, + { + "default": null, + "description": "File containing SSL certificate", + "name": "SINGLESTOREDB_SSL_CERT", + "required": false + }, + { + "default": null, + "description": "File containing SSL certificate authority", + "name": "SINGLESTOREDB_SSL_CA", + "required": false + }, + { + "default": null, + "description": "The timeout for connecting to the database in seconds", + "name": "SINGLESTOREDB_CONNECT_TIMEOUT", + "required": false + } + ], + "humanized_name": "Search a database's table(s) content", + "init_params_schema": { + "$defs": { + "EnvVar": { + "properties": { + "default": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Default" + }, + "description": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "required": { + "default": true, + "title": "Required", + "type": "boolean" + } + }, + "required": [ + "name", + "description" + ], + "title": "EnvVar", + "type": "object" + } + }, + "description": "A tool for performing semantic searches on SingleStore database tables.\n\nThis tool provides a safe interface for executing SELECT and SHOW queries\nagainst a SingleStore database with connection pooling for optimal performance.", + "properties": { + "connection_args": { + "additionalProperties": true, + "default": {}, + "title": "Connection Args", + "type": "object" + }, + "connection_pool": { + "anyOf": [ + {}, + { + "type": "null" + } + ], + "default": null, + "title": "Connection Pool" + } + }, + "title": "SingleStoreSearchTool", + "type": "object" + }, + "name": "SingleStoreSearchTool", + "package_dependencies": [ + "singlestoredb", + "SQLAlchemy" + ], + "run_params_schema": { + "description": "Input schema for SingleStoreSearchTool.\n\nThis schema defines the expected input format for the search tool,\nensuring that only valid SELECT and SHOW queries are accepted.", + "properties": { + "search_query": { + "description": "Mandatory semantic search query you want to use to search the database's content. Only SELECT and SHOW queries are supported.", + "title": "Search Query", + "type": "string" + } + }, + "required": [ + "search_query" + ], + "title": "SingleStoreSearchToolSchema", + "type": "object" + } + }, + { + "description": "Execute SQL queries or semantic search on Snowflake data warehouse. Supports both raw SQL and natural language queries.", + "env_vars": [], + "humanized_name": "Snowflake Database Search", + "init_params_schema": { + "$defs": { + "EnvVar": { + "properties": { + "default": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Default" + }, + "description": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "required": { + "default": true, + "title": "Required", + "type": "boolean" + } + }, + "required": [ + "name", + "description" + ], + "title": "EnvVar", + "type": "object" + }, + "SnowflakeConfig": { + "description": "Configuration for Snowflake connection.", + "properties": { + "account": { + "description": "Snowflake account identifier", + "pattern": "^[a-zA-Z0-9\\-_]+$", + "title": "Account", + "type": "string" + }, + "database": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Default database", + "title": "Database" + }, + "password": { + "anyOf": [ + { + "format": "password", + "type": "string", + "writeOnly": true + }, + { + "type": "null" + } + ], + "default": null, + "description": "Snowflake password", + "title": "Password" + }, + "private_key_path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Path to private key file", + "title": "Private Key Path" + }, + "role": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Snowflake role", + "title": "Role" + }, + "session_parameters": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "description": "Session parameters", + "title": "Session Parameters" + }, + "snowflake_schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Default schema", + "title": "Snowflake Schema" + }, + "user": { + "description": "Snowflake username", + "title": "User", + "type": "string" + }, + "warehouse": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Snowflake warehouse", + "title": "Warehouse" + } + }, + "required": [ + "account", + "user" + ], + "title": "SnowflakeConfig", + "type": "object" + } + }, + "description": "Tool for executing queries and semantic search on Snowflake.", + "properties": { + "config": { + "$ref": "#/$defs/SnowflakeConfig", + "description": "Snowflake connection configuration" + }, + "enable_caching": { + "default": true, + "description": "Enable query result caching", + "title": "Enable Caching", + "type": "boolean" + }, + "max_retries": { + "default": 3, + "description": "Maximum retry attempts", + "title": "Max Retries", + "type": "integer" + }, + "pool_size": { + "default": 5, + "description": "Size of connection pool", + "title": "Pool Size", + "type": "integer" + }, + "retry_delay": { + "default": 1.0, + "description": "Delay between retries in seconds", + "title": "Retry Delay", + "type": "number" + } + }, + "required": [ + "config" + ], + "title": "SnowflakeSearchTool", + "type": "object" + }, + "name": "SnowflakeSearchTool", + "package_dependencies": [ + "snowflake-connector-python", + "snowflake-sqlalchemy", + "cryptography" + ], + "run_params_schema": { + "description": "Input schema for SnowflakeSearchTool.", + "properties": { + "database": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Override default database", + "title": "Database" + }, + "query": { + "description": "SQL query or semantic search query to execute", + "title": "Query", + "type": "string" + }, + "snowflake_schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Override default schema", + "title": "Snowflake Schema" + }, + "timeout": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": 300, + "description": "Query timeout in seconds", + "title": "Timeout" + } + }, + "required": [ + "query" + ], + "title": "SnowflakeSearchToolInput", + "type": "object" + } + }, + { + "description": "A tool to scrape or crawl a website and return LLM-ready content.", + "env_vars": [ + { + "default": null, + "description": "API key for Spider.cloud", + "name": "SPIDER_API_KEY", + "required": true + } + ], + "humanized_name": "SpiderTool", + "init_params_schema": { + "$defs": { + "EnvVar": { + "properties": { + "default": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Default" + }, + "description": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "required": { + "default": true, + "title": "Required", + "type": "boolean" + } + }, + "required": [ + "name", + "description" + ], + "title": "EnvVar", + "type": "object" + }, + "SpiderToolConfig": { + "description": "Configuration settings for SpiderTool.\n\nContains all default values and constants used by SpiderTool.\nCentralizes configuration management for easier maintenance.", + "properties": { + "DEFAULT_CRAWL_LIMIT": { + "default": 5, + "title": "Default Crawl Limit", + "type": "integer" + }, + "DEFAULT_REQUEST_MODE": { + "default": "smart", + "title": "Default Request Mode", + "type": "string" + }, + "DEFAULT_RETURN_FORMAT": { + "default": "markdown", + "title": "Default Return Format", + "type": "string" + }, + "FILTER_SVG": { + "default": true, + "title": "Filter Svg", + "type": "boolean" + } + }, + "title": "SpiderToolConfig", + "type": "object" + } + }, + "description": "Tool for scraping and crawling websites.\nThis tool provides functionality to either scrape a single webpage or crawl multiple\npages, returning content in a format suitable for LLM processing.", + "properties": { + "api_key": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Api Key" + }, + "config": { + "$ref": "#/$defs/SpiderToolConfig", + "default": { + "DEFAULT_CRAWL_LIMIT": 5, + "DEFAULT_REQUEST_MODE": "smart", + "DEFAULT_RETURN_FORMAT": "markdown", + "FILTER_SVG": true + } + }, + "custom_params": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Custom Params" + }, + "log_failures": { + "default": true, + "title": "Log Failures", + "type": "boolean" + }, + "spider": { + "default": null, + "title": "Spider" + }, + "website_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Website Url" + } + }, + "title": "SpiderTool", + "type": "object" + }, + "name": "SpiderTool", + "package_dependencies": [ + "spider-client" + ], + "run_params_schema": { + "description": "Input schema for SpiderTool.", + "properties": { + "mode": { + "default": "scrape", + "description": "The mode of the SpiderTool. The only two allowed modes are `scrape` or `crawl`. Crawl mode will follow up to 5 links and return their content in markdown format.", + "enum": [ + "scrape", + "crawl" + ], + "title": "Mode", + "type": "string" + }, + "website_url": { + "description": "Mandatory website URL to scrape or crawl", + "title": "Website Url", + "type": "string" + } + }, + "required": [ + "website_url" + ], + "title": "SpiderToolSchema", + "type": "object" + } + }, + { + "description": "Use this tool to control a web browser and interact with websites using natural language.\n\n Capabilities:\n - Navigate to websites and follow links\n - Click buttons, links, and other elements\n - Fill in forms and input fields\n - Search within websites\n - Extract information from web pages\n - Identify and analyze elements on a page\n\n To use this tool, provide a natural language instruction describing what you want to do.\n For reliability on complex pages, use specific, atomic instructions with location hints:\n - Good: \"Click the search box in the header\"\n - Good: \"Type 'Italy' in the focused field\"\n - Bad: \"Search for Italy and click the first result\"\n\n For different types of tasks, specify the command_type:\n - 'act': For performing one atomic action (default)\n - 'navigate': For navigating to a URL\n - 'extract': For getting data from a specific page section\n - 'observe': For finding elements in a specific area", + "env_vars": [], + "humanized_name": "Web Automation Tool", + "init_params_schema": { + "$defs": { + "AvailableModel": { + "enum": [ + "gpt-4o", + "gpt-4o-mini", + "claude-3-5-sonnet-latest", + "claude-3-7-sonnet-latest", + "computer-use-preview", + "gemini-2.0-flash" + ], + "title": "AvailableModel", + "type": "string" + }, + "EnvVar": { + "properties": { + "default": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Default" + }, + "description": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "required": { + "default": true, + "title": "Required", + "type": "boolean" + } + }, + "required": [ + "name", + "description" + ], + "title": "EnvVar", + "type": "object" + } + }, + "description": "A tool that uses Stagehand to automate web browser interactions using natural language with atomic action handling.\n\nStagehand allows AI agents to interact with websites through a browser,\nperforming actions like clicking buttons, filling forms, and extracting data.\n\nThe tool supports four main command types:\n1. act - Perform actions like clicking, typing, scrolling, or navigating\n2. navigate - Specifically navigate to a URL (shorthand for act with navigation)\n3. extract - Extract structured data from web pages\n4. observe - Identify and analyze elements on a page\n\nUsage examples:\n- Navigate to a website: instruction=\"Go to the homepage\", url=\"https://example.com\"\n- Click a button: instruction=\"Click the login button\"\n- Fill a form: instruction=\"Fill the login form with username 'user' and password 'pass'\"\n- Extract data: instruction=\"Extract all product prices and names\", command_type=\"extract\"\n- Observe elements: instruction=\"Find all navigation menu items\", command_type=\"observe\"\n- Complex tasks: instruction=\"Step 1: Navigate to https://example.com; Step 2: Scroll down to the 'Features' section; Step 3: Click 'Learn More'\", command_type=\"act\"\n\nExample of breaking down \"Search for OpenAI\" into multiple steps:\n1. First navigation: instruction=\"Go to Google\", url=\"https://google.com\", command_type=\"navigate\"\n2. Enter search term: instruction=\"Type 'OpenAI' in the search box\", command_type=\"act\"\n3. Submit search: instruction=\"Press the Enter key or click the search button\", command_type=\"act\"\n4. Click on result: instruction=\"Click on the OpenAI website link in the search results\", command_type=\"act\"", + "properties": { + "api_key": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Api Key" + }, + "dom_settle_timeout_ms": { + "default": 3000, + "title": "Dom Settle Timeout Ms", + "type": "integer" + }, + "headless": { + "default": false, + "title": "Headless", + "type": "boolean" + }, + "max_retries_on_token_limit": { + "default": 3, + "title": "Max Retries On Token Limit", + "type": "integer" + }, + "model_api_key": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Model Api Key" + }, + "model_name": { + "anyOf": [ + { + "$ref": "#/$defs/AvailableModel" + }, + { + "type": "null" + } + ], + "default": "claude-3-7-sonnet-latest" + }, + "project_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Project Id" + }, + "self_heal": { + "default": true, + "title": "Self Heal", + "type": "boolean" + }, + "server_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "https://api.stagehand.browserbase.com/v1", + "title": "Server Url" + }, + "use_simplified_dom": { + "default": true, + "title": "Use Simplified Dom", + "type": "boolean" + }, + "verbose": { + "default": 1, + "title": "Verbose", + "type": "integer" + }, + "wait_for_captcha_solves": { + "default": true, + "title": "Wait For Captcha Solves", + "type": "boolean" + } + }, + "title": "StagehandTool", + "type": "object" + }, + "name": "StagehandTool", + "package_dependencies": [], + "run_params_schema": { + "description": "Input for StagehandTool.", + "properties": { + "command_type": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "act", + "description": "The type of command to execute (choose one):\n - 'act': Perform an action like clicking buttons, filling forms, etc. (default)\n - 'navigate': Specifically navigate to a URL\n - 'extract': Extract structured data from the page\n - 'observe': Identify and analyze elements on the page\n ", + "title": "Command Type" + }, + "instruction": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Single atomic action with location context. For reliability on complex pages, use ONE specific action with location hints. Good examples: 'Click the search input field in the header', 'Type Italy in the focused field', 'Press Enter', 'Click the first link in the results area'. Avoid combining multiple actions. For 'navigate' command type, this can be omitted if only URL is provided.", + "title": "Instruction" + }, + "url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The URL to navigate to before executing the instruction. MUST be used with 'navigate' command. ", + "title": "Url" + } + }, + "title": "StagehandToolSchema", + "type": "object" + } + }, + { + "description": "A tool that can be used to semantic search a query from a txt's content.", + "env_vars": [], + "humanized_name": "Search a txt's content", + "init_params_schema": { + "$defs": { + "Adapter": { + "properties": {}, + "title": "Adapter", + "type": "object" + }, + "EnvVar": { + "properties": { + "default": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Default" + }, + "description": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "required": { + "default": true, + "title": "Required", + "type": "boolean" + } + }, + "required": [ + "name", + "description" + ], + "title": "EnvVar", + "type": "object" + } + }, + "properties": { + "adapter": { + "$ref": "#/$defs/Adapter" + }, + "config": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Config" + }, + "summarize": { + "default": false, + "title": "Summarize", + "type": "boolean" + } + }, + "title": "TXTSearchTool", + "type": "object" + }, + "name": "TXTSearchTool", + "package_dependencies": [], + "run_params_schema": { + "description": "Input for TXTSearchTool.", + "properties": { + "search_query": { + "description": "Mandatory search query you want to use to search the txt's content", + "title": "Search Query", + "type": "string" + }, + "txt": { + "description": "Mandatory txt path you want to search", + "title": "Txt", + "type": "string" + } + }, + "required": [ + "search_query", + "txt" + ], + "title": "TXTSearchToolSchema", + "type": "object" + } + }, + { + "description": "Extracts content from one or more web pages using the Tavily API. Returns structured data.", + "env_vars": [ + { + "default": null, + "description": "API key for Tavily extraction service", + "name": "TAVILY_API_KEY", + "required": true + } + ], + "humanized_name": "TavilyExtractorTool", + "init_params_schema": { + "$defs": { + "EnvVar": { + "properties": { + "default": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Default" + }, + "description": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "required": { + "default": true, + "title": "Required", + "type": "boolean" + } + }, + "required": [ + "name", + "description" + ], + "title": "EnvVar", + "type": "object" + } + }, + "properties": { + "api_key": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "The Tavily API key. If not provided, it will be loaded from the environment variable TAVILY_API_KEY.", + "title": "Api Key" + }, + "async_client": { + "anyOf": [ + {}, + { + "type": "null" + } + ], + "default": null, + "title": "Async Client" + }, + "client": { + "anyOf": [ + {}, + { + "type": "null" + } + ], + "default": null, + "title": "Client" + }, + "extract_depth": { + "default": "basic", + "description": "The depth of extraction. 'basic' for basic extraction, 'advanced' for advanced extraction.", + "enum": [ + "basic", + "advanced" + ], + "title": "Extract Depth", + "type": "string" + }, + "include_images": { + "default": false, + "description": "Whether to include images in the extraction.", + "title": "Include Images", + "type": "boolean" + }, + "proxies": { + "anyOf": [ + { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional proxies to use for the Tavily API requests.", + "title": "Proxies" + }, + "timeout": { + "default": 60, + "description": "The timeout for the extraction request in seconds.", + "title": "Timeout", + "type": "integer" + } + }, + "title": "TavilyExtractorTool", + "type": "object" + }, + "name": "TavilyExtractorTool", + "package_dependencies": [ + "tavily-python" + ], + "run_params_schema": { + "description": "Input schema for TavilyExtractorTool.", + "properties": { + "urls": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "string" + } + ], + "description": "The URL(s) to extract data from. Can be a single URL or a list of URLs.", + "title": "Urls" + } + }, + "required": [ + "urls" + ], + "title": "TavilyExtractorToolSchema", + "type": "object" + } + }, + { + "description": "A tool that performs web searches using the Tavily Search API. It returns a JSON object containing the search results.", + "env_vars": [ + { + "default": null, + "description": "API key for Tavily search service", + "name": "TAVILY_API_KEY", + "required": true + } + ], + "humanized_name": "Tavily Search", + "init_params_schema": { + "$defs": { + "EnvVar": { + "properties": { + "default": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Default" + }, + "description": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "required": { + "default": true, + "title": "Required", + "type": "boolean" + } + }, + "required": [ + "name", + "description" + ], + "title": "EnvVar", + "type": "object" + } + }, + "description": "Tool that uses the Tavily Search API to perform web searches.\n\nAttributes:\n client: An instance of TavilyClient.\n async_client: An instance of AsyncTavilyClient.\n name: The name of the tool.\n description: A description of the tool's purpose.\n args_schema: The schema for the tool's arguments.\n api_key: The Tavily API key.\n proxies: Optional proxies for the API requests.\n search_depth: The depth of the search.\n topic: The topic to focus the search on.\n time_range: The time range for the search.\n days: The number of days to search back.\n max_results: The maximum number of results to return.\n include_domains: A list of domains to include in the search.\n exclude_domains: A list of domains to exclude from the search.\n include_answer: Whether to include a direct answer to the query.\n include_raw_content: Whether to include the raw content of the search results.\n include_images: Whether to include images in the search results.\n timeout: The timeout for the search request in seconds.\n max_content_length_per_result: Maximum length for the 'content' of each search result.", + "properties": { + "api_key": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "The Tavily API key. If not provided, it will be loaded from the environment variable TAVILY_API_KEY.", + "title": "Api Key" + }, + "async_client": { + "anyOf": [ + {}, + { + "type": "null" + } + ], + "default": null, + "title": "Async Client" + }, + "client": { + "anyOf": [ + {}, + { + "type": "null" + } + ], + "default": null, + "title": "Client" + }, + "days": { + "default": 7, + "description": "The number of days to search back.", + "title": "Days", + "type": "integer" + }, + "exclude_domains": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "A list of domains to exclude from the search.", + "title": "Exclude Domains" + }, + "include_answer": { + "anyOf": [ + { + "type": "boolean" + }, + { + "enum": [ + "basic", + "advanced" + ], + "type": "string" + } + ], + "default": false, + "description": "Whether to include a direct answer to the query.", + "title": "Include Answer" + }, + "include_domains": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "description": "A list of domains to include in the search.", + "title": "Include Domains" + }, + "include_images": { + "default": false, + "description": "Whether to include images in the search results.", + "title": "Include Images", + "type": "boolean" + }, + "include_raw_content": { + "default": false, + "description": "Whether to include the raw content of the search results.", + "title": "Include Raw Content", + "type": "boolean" + }, + "max_content_length_per_result": { + "default": 1000, + "description": "Maximum length for the 'content' of each search result to avoid context window issues.", + "title": "Max Content Length Per Result", + "type": "integer" + }, + "max_results": { + "default": 5, + "description": "The maximum number of results to return.", + "title": "Max Results", + "type": "integer" + }, + "proxies": { + "anyOf": [ + { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Optional proxies to use for the Tavily API requests.", + "title": "Proxies" + }, + "search_depth": { + "default": "basic", + "description": "The depth of the search.", + "enum": [ + "basic", + "advanced" + ], + "title": "Search Depth", + "type": "string" + }, + "time_range": { + "anyOf": [ + { + "enum": [ + "day", + "week", + "month", + "year" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The time range for the search.", + "title": "Time Range" + }, + "timeout": { + "default": 60, + "description": "The timeout for the search request in seconds.", + "title": "Timeout", + "type": "integer" + }, + "topic": { + "default": "general", + "description": "The topic to focus the search on.", + "enum": [ + "general", + "news", + "finance" + ], + "title": "Topic", + "type": "string" + } + }, + "title": "TavilySearchTool", + "type": "object" + }, + "name": "TavilySearchTool", + "package_dependencies": [ + "tavily-python" + ], + "run_params_schema": { + "description": "Input schema for TavilySearchTool.", + "properties": { + "query": { + "description": "The search query string.", + "title": "Query", + "type": "string" + } + }, + "required": [ + "query" + ], + "title": "TavilySearchToolSchema", + "type": "object" + } + }, + { + "description": "This tool uses OpenAI's Vision API to describe the contents of an image.", + "env_vars": [ + { + "default": null, + "description": "API key for OpenAI services", + "name": "OPENAI_API_KEY", + "required": true + } + ], + "humanized_name": "Vision Tool", + "init_params_schema": { + "$defs": { + "EnvVar": { + "properties": { + "default": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Default" + }, + "description": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "required": { + "default": true, + "title": "Required", + "type": "boolean" + } + }, + "required": [ + "name", + "description" + ], + "title": "EnvVar", + "type": "object" + } + }, + "description": "Tool for analyzing images using vision models.\n\nArgs:\n llm: Optional LLM instance to use\n model: Model identifier to use if no LLM is provided", + "properties": {}, + "title": "VisionTool", + "type": "object" + }, + "name": "VisionTool", + "package_dependencies": [], + "run_params_schema": { + "description": "Input for Vision Tool.", + "properties": { + "image_path_url": { + "default": "The image path or URL.", + "title": "Image Path Url", + "type": "string" + } + }, + "title": "ImagePromptSchema", + "type": "object" + } + }, + { + "description": "A tool to search the Weaviate database for relevant information on internal documents.", + "env_vars": [ + { + "default": null, + "description": "OpenAI API key for embedding generation and retrieval", + "name": "OPENAI_API_KEY", + "required": true + } + ], + "humanized_name": "WeaviateVectorSearchTool", + "init_params_schema": { + "$defs": { + "EnvVar": { + "properties": { + "default": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Default" + }, + "description": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "required": { + "default": true, + "title": "Required", + "type": "boolean" + } + }, + "required": [ + "name", + "description" + ], + "title": "EnvVar", + "type": "object" + } + }, + "description": "Tool to search the Weaviate database", + "properties": { + "alpha": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": 0.75, + "title": "Alpha" + }, + "collection_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Collection Name" + }, + "generative_model": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Generative Model" + }, + "headers": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Headers" + }, + "limit": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": 3, + "title": "Limit" + }, + "query": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Query" + }, + "vectorizer": { + "anyOf": [ + {}, + { + "type": "null" + } + ], + "default": null, + "title": "Vectorizer" + }, + "weaviate_api_key": { + "description": "The API key for the Weaviate cluster", + "title": "Weaviate Api Key", + "type": "string" + }, + "weaviate_cluster_url": { + "description": "The URL of the Weaviate cluster", + "title": "Weaviate Cluster Url", + "type": "string" + } + }, + "required": [ + "weaviate_cluster_url", + "weaviate_api_key" + ], + "title": "WeaviateVectorSearchTool", + "type": "object" + }, + "name": "WeaviateVectorSearchTool", + "package_dependencies": [ + "weaviate-client" + ], + "run_params_schema": { + "description": "Input for WeaviateTool.", + "properties": { + "query": { + "description": "The query to search retrieve relevant information from the Weaviate database. Pass only the query, not the question.", + "title": "Query", + "type": "string" + } + }, + "required": [ + "query" + ], + "title": "WeaviateToolSchema", + "type": "object" + } + }, + { + "description": "A tool that can be used to semantic search a query from a specific URL content.", + "env_vars": [], + "humanized_name": "Search in a specific website", + "init_params_schema": { + "$defs": { + "Adapter": { + "properties": {}, + "title": "Adapter", + "type": "object" + }, + "EnvVar": { + "properties": { + "default": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Default" + }, + "description": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "required": { + "default": true, + "title": "Required", + "type": "boolean" + } + }, + "required": [ + "name", + "description" + ], + "title": "EnvVar", + "type": "object" + } + }, + "properties": { + "adapter": { + "$ref": "#/$defs/Adapter" + }, + "config": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Config" + }, + "summarize": { + "default": false, + "title": "Summarize", + "type": "boolean" + } + }, + "title": "WebsiteSearchTool", + "type": "object" + }, + "name": "WebsiteSearchTool", + "package_dependencies": [], + "run_params_schema": { + "description": "Input for WebsiteSearchTool.", + "properties": { + "search_query": { + "description": "Mandatory search query you want to use to search a specific website", + "title": "Search Query", + "type": "string" + }, + "website": { + "description": "Mandatory valid website URL you want to search on", + "title": "Website", + "type": "string" + } + }, + "required": [ + "search_query", + "website" + ], + "title": "WebsiteSearchToolSchema", + "type": "object" + } + }, + { + "description": "A tool that can be used to semantic search a query from a XML's content.", + "env_vars": [], + "humanized_name": "Search a XML's content", + "init_params_schema": { + "$defs": { + "Adapter": { + "properties": {}, + "title": "Adapter", + "type": "object" + }, + "EnvVar": { + "properties": { + "default": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Default" + }, + "description": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "required": { + "default": true, + "title": "Required", + "type": "boolean" + } + }, + "required": [ + "name", + "description" + ], + "title": "EnvVar", + "type": "object" + } + }, + "properties": { + "adapter": { + "$ref": "#/$defs/Adapter" + }, + "config": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Config" + }, + "summarize": { + "default": false, + "title": "Summarize", + "type": "boolean" + } + }, + "title": "XMLSearchTool", + "type": "object" + }, + "name": "XMLSearchTool", + "package_dependencies": [], + "run_params_schema": { + "description": "Input for XMLSearchTool.", + "properties": { + "search_query": { + "description": "Mandatory search query you want to use to search the XML's content", + "title": "Search Query", + "type": "string" + }, + "xml": { + "description": "Mandatory xml path you want to search", + "title": "Xml", + "type": "string" + } + }, + "required": [ + "search_query", + "xml" + ], + "title": "XMLSearchToolSchema", + "type": "object" + } + }, + { + "description": "A tool that can be used to semantic search a query from a Youtube Channels content.", + "env_vars": [], + "humanized_name": "Search a Youtube Channels content", + "init_params_schema": { + "$defs": { + "Adapter": { + "properties": {}, + "title": "Adapter", + "type": "object" + }, + "EnvVar": { + "properties": { + "default": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Default" + }, + "description": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "required": { + "default": true, + "title": "Required", + "type": "boolean" + } + }, + "required": [ + "name", + "description" + ], + "title": "EnvVar", + "type": "object" + } + }, + "properties": { + "adapter": { + "$ref": "#/$defs/Adapter" + }, + "config": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Config" + }, + "summarize": { + "default": false, + "title": "Summarize", + "type": "boolean" + } + }, + "title": "YoutubeChannelSearchTool", + "type": "object" + }, + "name": "YoutubeChannelSearchTool", + "package_dependencies": [], + "run_params_schema": { + "description": "Input for YoutubeChannelSearchTool.", + "properties": { + "search_query": { + "description": "Mandatory search query you want to use to search the Youtube Channels content", + "title": "Search Query", + "type": "string" + }, + "youtube_channel_handle": { + "description": "Mandatory youtube_channel_handle path you want to search", + "title": "Youtube Channel Handle", + "type": "string" + } + }, + "required": [ + "search_query", + "youtube_channel_handle" + ], + "title": "YoutubeChannelSearchToolSchema", + "type": "object" + } + }, + { + "description": "A tool that can be used to semantic search a query from a Youtube Video content.", + "env_vars": [], + "humanized_name": "Search a Youtube Video content", + "init_params_schema": { + "$defs": { + "Adapter": { + "properties": {}, + "title": "Adapter", + "type": "object" + }, + "EnvVar": { + "properties": { + "default": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Default" + }, + "description": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "required": { + "default": true, + "title": "Required", + "type": "boolean" + } + }, + "required": [ + "name", + "description" + ], + "title": "EnvVar", + "type": "object" + } + }, + "properties": { + "adapter": { + "$ref": "#/$defs/Adapter" + }, + "config": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Config" + }, + "summarize": { + "default": false, + "title": "Summarize", + "type": "boolean" + } + }, + "title": "YoutubeVideoSearchTool", + "type": "object" + }, + "name": "YoutubeVideoSearchTool", + "package_dependencies": [], + "run_params_schema": { + "description": "Input for YoutubeVideoSearchTool.", + "properties": { + "search_query": { + "description": "Mandatory search query you want to use to search the Youtube Video content", + "title": "Search Query", + "type": "string" + }, + "youtube_video_url": { + "description": "Mandatory youtube_video_url path you want to search", + "title": "Youtube Video Url", + "type": "string" + } + }, + "required": [ + "search_query", + "youtube_video_url" + ], + "title": "YoutubeVideoSearchToolSchema", + "type": "object" + } + } + ] +} \ No newline at end of file diff --git a/uv.lock b/uv.lock new file mode 100644 index 000000000..b73af8ee7 --- /dev/null +++ b/uv.lock @@ -0,0 +1,7800 @@ +version = 1 +revision = 1 +requires-python = ">=3.10, <3.14" +resolution-markers = [ + "python_full_version >= '3.13' and platform_python_implementation == 'PyPy' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and platform_python_implementation == 'PyPy' and sys_platform == 'linux'", + "(python_full_version >= '3.13' and platform_machine != 'aarch64' and platform_python_implementation == 'PyPy' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_python_implementation == 'PyPy' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version >= '3.13' and platform_python_implementation != 'PyPy' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and platform_python_implementation != 'PyPy' and sys_platform == 'linux'", + "(python_full_version >= '3.13' and platform_machine != 'aarch64' and platform_python_implementation != 'PyPy' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_python_implementation != 'PyPy' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version >= '3.12.4' and python_full_version < '3.13' and platform_python_implementation == 'PyPy' and sys_platform == 'darwin'", + "python_full_version >= '3.12.4' and python_full_version < '3.13' and platform_machine == 'aarch64' and platform_python_implementation == 'PyPy' and sys_platform == 'linux'", + "(python_full_version >= '3.12.4' and python_full_version < '3.13' and platform_machine != 'aarch64' and platform_python_implementation == 'PyPy' and sys_platform == 'linux') or (python_full_version >= '3.12.4' and python_full_version < '3.13' and platform_python_implementation == 'PyPy' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version >= '3.12.4' and python_full_version < '3.13' and platform_python_implementation != 'PyPy' and sys_platform == 'darwin'", + "python_full_version >= '3.12.4' and python_full_version < '3.13' and platform_machine == 'aarch64' and platform_python_implementation != 'PyPy' and sys_platform == 'linux'", + "(python_full_version >= '3.12.4' and python_full_version < '3.13' and platform_machine != 'aarch64' and platform_python_implementation != 'PyPy' and sys_platform == 'linux') or (python_full_version >= '3.12.4' and python_full_version < '3.13' and platform_python_implementation != 'PyPy' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version >= '3.12' and python_full_version < '3.12.4' and platform_python_implementation == 'PyPy' and sys_platform == 'darwin'", + "python_full_version >= '3.12' and python_full_version < '3.12.4' and platform_machine == 'aarch64' and platform_python_implementation == 'PyPy' and sys_platform == 'linux'", + "(python_full_version >= '3.12' and python_full_version < '3.12.4' and platform_machine != 'aarch64' and platform_python_implementation == 'PyPy' and sys_platform == 'linux') or (python_full_version >= '3.12' and python_full_version < '3.12.4' and platform_python_implementation == 'PyPy' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version >= '3.12' and python_full_version < '3.12.4' and platform_python_implementation != 'PyPy' and sys_platform == 'darwin'", + "python_full_version >= '3.12' and python_full_version < '3.12.4' and platform_machine == 'aarch64' and platform_python_implementation != 'PyPy' and sys_platform == 'linux'", + "(python_full_version >= '3.12' and python_full_version < '3.12.4' and platform_machine != 'aarch64' and platform_python_implementation != 'PyPy' and sys_platform == 'linux') or (python_full_version >= '3.12' and python_full_version < '3.12.4' and platform_python_implementation != 'PyPy' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version == '3.11.*' and platform_python_implementation == 'PyPy' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and platform_python_implementation == 'PyPy' and sys_platform == 'linux'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and platform_python_implementation == 'PyPy' and sys_platform == 'linux') or (python_full_version == '3.11.*' and platform_python_implementation == 'PyPy' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version == '3.11.*' and platform_python_implementation != 'PyPy' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and platform_python_implementation != 'PyPy' and sys_platform == 'linux'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and platform_python_implementation != 'PyPy' and sys_platform == 'linux') or (python_full_version == '3.11.*' and platform_python_implementation != 'PyPy' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version < '3.11' and platform_python_implementation == 'PyPy' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and platform_python_implementation == 'PyPy' and sys_platform == 'linux'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and platform_python_implementation == 'PyPy' and sys_platform == 'linux') or (python_full_version < '3.11' and platform_python_implementation == 'PyPy' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version < '3.11' and platform_python_implementation != 'PyPy' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and platform_python_implementation != 'PyPy' and sys_platform == 'linux'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and platform_python_implementation != 'PyPy' and sys_platform == 'linux') or (python_full_version < '3.11' and platform_python_implementation != 'PyPy' and sys_platform != 'darwin' and sys_platform != 'linux')", +] + +[[package]] +name = "accelerate" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "packaging" }, + { name = "psutil" }, + { name = "pyyaml" }, + { name = "safetensors" }, + { name = "torch" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c4/25/969456a95a90ed38f73f68d0f0915bdf1d76145d05054c59ad587b171150/accelerate-1.9.0.tar.gz", hash = "sha256:0e8c61f81af7bf37195b6175a545ed292617dd90563c88f49020aea5b6a0b47f", size = 383234 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/1c/a17fb513aeb684fb83bef5f395910f53103ab30308bbdd77fd66d6698c46/accelerate-1.9.0-py3-none-any.whl", hash = "sha256:c24739a97ade1d54af4549a65f8b6b046adc87e2b3e4d6c66516e32c53d5a8f1", size = 367073 }, +] + +[[package]] +name = "aiofiles" +version = "24.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/03/a88171e277e8caa88a4c77808c20ebb04ba74cc4681bf1e9416c862de237/aiofiles-24.1.0.tar.gz", hash = "sha256:22a075c9e5a3810f0c2e48f3008c94d68c65d763b9b03857924c99e57355166c", size = 30247 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/45/30bb92d442636f570cb5651bc661f52b610e2eec3f891a5dc3a4c3667db0/aiofiles-24.1.0-py3-none-any.whl", hash = "sha256:b4ec55f4195e3eb5d7abd1bf7e061763e864dd4954231fb8539a0ef8bb8260e5", size = 15896 }, +] + +[[package]] +name = "aiohappyeyeballs" +version = "2.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/26/30/f84a107a9c4331c14b2b586036f40965c128aa4fee4dda5d3d51cb14ad54/aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558", size = 22760 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", size = 15265 }, +] + +[[package]] +name = "aiohttp" +version = "3.12.15" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohappyeyeballs" }, + { name = "aiosignal" }, + { name = "async-timeout", marker = "python_full_version < '3.11'" }, + { name = "attrs" }, + { name = "frozenlist" }, + { name = "multidict" }, + { name = "propcache" }, + { name = "yarl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9b/e7/d92a237d8802ca88483906c388f7c201bbe96cd80a165ffd0ac2f6a8d59f/aiohttp-3.12.15.tar.gz", hash = "sha256:4fc61385e9c98d72fcdf47e6dd81833f47b2f77c114c29cd64a361be57a763a2", size = 7823716 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/dc/ef9394bde9080128ad401ac7ede185267ed637df03b51f05d14d1c99ad67/aiohttp-3.12.15-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b6fc902bff74d9b1879ad55f5404153e2b33a82e72a95c89cec5eb6cc9e92fbc", size = 703921 }, + { url = "https://files.pythonhosted.org/packages/8f/42/63fccfc3a7ed97eb6e1a71722396f409c46b60a0552d8a56d7aad74e0df5/aiohttp-3.12.15-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:098e92835b8119b54c693f2f88a1dec690e20798ca5f5fe5f0520245253ee0af", size = 480288 }, + { url = "https://files.pythonhosted.org/packages/9c/a2/7b8a020549f66ea2a68129db6960a762d2393248f1994499f8ba9728bbed/aiohttp-3.12.15-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:40b3fee496a47c3b4a39a731954c06f0bd9bd3e8258c059a4beb76ac23f8e421", size = 468063 }, + { url = "https://files.pythonhosted.org/packages/8f/f5/d11e088da9176e2ad8220338ae0000ed5429a15f3c9dfd983f39105399cd/aiohttp-3.12.15-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ce13fcfb0bb2f259fb42106cdc63fa5515fb85b7e87177267d89a771a660b79", size = 1650122 }, + { url = "https://files.pythonhosted.org/packages/b0/6b/b60ce2757e2faed3d70ed45dafee48cee7bfb878785a9423f7e883f0639c/aiohttp-3.12.15-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3beb14f053222b391bf9cf92ae82e0171067cc9c8f52453a0f1ec7c37df12a77", size = 1624176 }, + { url = "https://files.pythonhosted.org/packages/dd/de/8c9fde2072a1b72c4fadecf4f7d4be7a85b1d9a4ab333d8245694057b4c6/aiohttp-3.12.15-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4c39e87afe48aa3e814cac5f535bc6199180a53e38d3f51c5e2530f5aa4ec58c", size = 1696583 }, + { url = "https://files.pythonhosted.org/packages/0c/ad/07f863ca3d895a1ad958a54006c6dafb4f9310f8c2fdb5f961b8529029d3/aiohttp-3.12.15-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5f1b4ce5bc528a6ee38dbf5f39bbf11dd127048726323b72b8e85769319ffc4", size = 1738896 }, + { url = "https://files.pythonhosted.org/packages/20/43/2bd482ebe2b126533e8755a49b128ec4e58f1a3af56879a3abdb7b42c54f/aiohttp-3.12.15-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1004e67962efabbaf3f03b11b4c43b834081c9e3f9b32b16a7d97d4708a9abe6", size = 1643561 }, + { url = "https://files.pythonhosted.org/packages/23/40/2fa9f514c4cf4cbae8d7911927f81a1901838baf5e09a8b2c299de1acfe5/aiohttp-3.12.15-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8faa08fcc2e411f7ab91d1541d9d597d3a90e9004180edb2072238c085eac8c2", size = 1583685 }, + { url = "https://files.pythonhosted.org/packages/b8/c3/94dc7357bc421f4fb978ca72a201a6c604ee90148f1181790c129396ceeb/aiohttp-3.12.15-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:fe086edf38b2222328cdf89af0dde2439ee173b8ad7cb659b4e4c6f385b2be3d", size = 1627533 }, + { url = "https://files.pythonhosted.org/packages/bf/3f/1f8911fe1844a07001e26593b5c255a685318943864b27b4e0267e840f95/aiohttp-3.12.15-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:79b26fe467219add81d5e47b4a4ba0f2394e8b7c7c3198ed36609f9ba161aecb", size = 1638319 }, + { url = "https://files.pythonhosted.org/packages/4e/46/27bf57a99168c4e145ffee6b63d0458b9c66e58bb70687c23ad3d2f0bd17/aiohttp-3.12.15-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b761bac1192ef24e16706d761aefcb581438b34b13a2f069a6d343ec8fb693a5", size = 1613776 }, + { url = "https://files.pythonhosted.org/packages/0f/7e/1d2d9061a574584bb4ad3dbdba0da90a27fdc795bc227def3a46186a8bc1/aiohttp-3.12.15-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:e153e8adacfe2af562861b72f8bc47f8a5c08e010ac94eebbe33dc21d677cd5b", size = 1693359 }, + { url = "https://files.pythonhosted.org/packages/08/98/bee429b52233c4a391980a5b3b196b060872a13eadd41c3a34be9b1469ed/aiohttp-3.12.15-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:fc49c4de44977aa8601a00edbf157e9a421f227aa7eb477d9e3df48343311065", size = 1716598 }, + { url = "https://files.pythonhosted.org/packages/57/39/b0314c1ea774df3392751b686104a3938c63ece2b7ce0ba1ed7c0b4a934f/aiohttp-3.12.15-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2776c7ec89c54a47029940177e75c8c07c29c66f73464784971d6a81904ce9d1", size = 1644940 }, + { url = "https://files.pythonhosted.org/packages/1b/83/3dacb8d3f8f512c8ca43e3fa8a68b20583bd25636ffa4e56ee841ffd79ae/aiohttp-3.12.15-cp310-cp310-win32.whl", hash = "sha256:2c7d81a277fa78b2203ab626ced1487420e8c11a8e373707ab72d189fcdad20a", size = 429239 }, + { url = "https://files.pythonhosted.org/packages/eb/f9/470b5daba04d558c9673ca2034f28d067f3202a40e17804425f0c331c89f/aiohttp-3.12.15-cp310-cp310-win_amd64.whl", hash = "sha256:83603f881e11f0f710f8e2327817c82e79431ec976448839f3cd05d7afe8f830", size = 452297 }, + { url = "https://files.pythonhosted.org/packages/20/19/9e86722ec8e835959bd97ce8c1efa78cf361fa4531fca372551abcc9cdd6/aiohttp-3.12.15-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d3ce17ce0220383a0f9ea07175eeaa6aa13ae5a41f30bc61d84df17f0e9b1117", size = 711246 }, + { url = "https://files.pythonhosted.org/packages/71/f9/0a31fcb1a7d4629ac9d8f01f1cb9242e2f9943f47f5d03215af91c3c1a26/aiohttp-3.12.15-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:010cc9bbd06db80fe234d9003f67e97a10fe003bfbedb40da7d71c1008eda0fe", size = 483515 }, + { url = "https://files.pythonhosted.org/packages/62/6c/94846f576f1d11df0c2e41d3001000527c0fdf63fce7e69b3927a731325d/aiohttp-3.12.15-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3f9d7c55b41ed687b9d7165b17672340187f87a773c98236c987f08c858145a9", size = 471776 }, + { url = "https://files.pythonhosted.org/packages/f8/6c/f766d0aaafcee0447fad0328da780d344489c042e25cd58fde566bf40aed/aiohttp-3.12.15-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bc4fbc61bb3548d3b482f9ac7ddd0f18c67e4225aaa4e8552b9f1ac7e6bda9e5", size = 1741977 }, + { url = "https://files.pythonhosted.org/packages/17/e5/fb779a05ba6ff44d7bc1e9d24c644e876bfff5abe5454f7b854cace1b9cc/aiohttp-3.12.15-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7fbc8a7c410bb3ad5d595bb7118147dfbb6449d862cc1125cf8867cb337e8728", size = 1690645 }, + { url = "https://files.pythonhosted.org/packages/37/4e/a22e799c2035f5d6a4ad2cf8e7c1d1bd0923192871dd6e367dafb158b14c/aiohttp-3.12.15-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:74dad41b3458dbb0511e760fb355bb0b6689e0630de8a22b1b62a98777136e16", size = 1789437 }, + { url = "https://files.pythonhosted.org/packages/28/e5/55a33b991f6433569babb56018b2fb8fb9146424f8b3a0c8ecca80556762/aiohttp-3.12.15-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b6f0af863cf17e6222b1735a756d664159e58855da99cfe965134a3ff63b0b0", size = 1828482 }, + { url = "https://files.pythonhosted.org/packages/c6/82/1ddf0ea4f2f3afe79dffed5e8a246737cff6cbe781887a6a170299e33204/aiohttp-3.12.15-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b5b7fe4972d48a4da367043b8e023fb70a04d1490aa7d68800e465d1b97e493b", size = 1730944 }, + { url = "https://files.pythonhosted.org/packages/1b/96/784c785674117b4cb3877522a177ba1b5e4db9ce0fd519430b5de76eec90/aiohttp-3.12.15-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6443cca89553b7a5485331bc9bedb2342b08d073fa10b8c7d1c60579c4a7b9bd", size = 1668020 }, + { url = "https://files.pythonhosted.org/packages/12/8a/8b75f203ea7e5c21c0920d84dd24a5c0e971fe1e9b9ebbf29ae7e8e39790/aiohttp-3.12.15-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6c5f40ec615e5264f44b4282ee27628cea221fcad52f27405b80abb346d9f3f8", size = 1716292 }, + { url = "https://files.pythonhosted.org/packages/47/0b/a1451543475bb6b86a5cfc27861e52b14085ae232896a2654ff1231c0992/aiohttp-3.12.15-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:2abbb216a1d3a2fe86dbd2edce20cdc5e9ad0be6378455b05ec7f77361b3ab50", size = 1711451 }, + { url = "https://files.pythonhosted.org/packages/55/fd/793a23a197cc2f0d29188805cfc93aa613407f07e5f9da5cd1366afd9d7c/aiohttp-3.12.15-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:db71ce547012a5420a39c1b744d485cfb823564d01d5d20805977f5ea1345676", size = 1691634 }, + { url = "https://files.pythonhosted.org/packages/ca/bf/23a335a6670b5f5dfc6d268328e55a22651b440fca341a64fccf1eada0c6/aiohttp-3.12.15-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:ced339d7c9b5030abad5854aa5413a77565e5b6e6248ff927d3e174baf3badf7", size = 1785238 }, + { url = "https://files.pythonhosted.org/packages/57/4f/ed60a591839a9d85d40694aba5cef86dde9ee51ce6cca0bb30d6eb1581e7/aiohttp-3.12.15-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:7c7dd29c7b5bda137464dc9bfc738d7ceea46ff70309859ffde8c022e9b08ba7", size = 1805701 }, + { url = "https://files.pythonhosted.org/packages/85/e0/444747a9455c5de188c0f4a0173ee701e2e325d4b2550e9af84abb20cdba/aiohttp-3.12.15-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:421da6fd326460517873274875c6c5a18ff225b40da2616083c5a34a7570b685", size = 1718758 }, + { url = "https://files.pythonhosted.org/packages/36/ab/1006278d1ffd13a698e5dd4bfa01e5878f6bddefc296c8b62649753ff249/aiohttp-3.12.15-cp311-cp311-win32.whl", hash = "sha256:4420cf9d179ec8dfe4be10e7d0fe47d6d606485512ea2265b0d8c5113372771b", size = 428868 }, + { url = "https://files.pythonhosted.org/packages/10/97/ad2b18700708452400278039272032170246a1bf8ec5d832772372c71f1a/aiohttp-3.12.15-cp311-cp311-win_amd64.whl", hash = "sha256:edd533a07da85baa4b423ee8839e3e91681c7bfa19b04260a469ee94b778bf6d", size = 453273 }, + { url = "https://files.pythonhosted.org/packages/63/97/77cb2450d9b35f517d6cf506256bf4f5bda3f93a66b4ad64ba7fc917899c/aiohttp-3.12.15-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:802d3868f5776e28f7bf69d349c26fc0efadb81676d0afa88ed00d98a26340b7", size = 702333 }, + { url = "https://files.pythonhosted.org/packages/83/6d/0544e6b08b748682c30b9f65640d006e51f90763b41d7c546693bc22900d/aiohttp-3.12.15-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f2800614cd560287be05e33a679638e586a2d7401f4ddf99e304d98878c29444", size = 476948 }, + { url = "https://files.pythonhosted.org/packages/3a/1d/c8c40e611e5094330284b1aea8a4b02ca0858f8458614fa35754cab42b9c/aiohttp-3.12.15-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8466151554b593909d30a0a125d638b4e5f3836e5aecde85b66b80ded1cb5b0d", size = 469787 }, + { url = "https://files.pythonhosted.org/packages/38/7d/b76438e70319796bfff717f325d97ce2e9310f752a267bfdf5192ac6082b/aiohttp-3.12.15-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e5a495cb1be69dae4b08f35a6c4579c539e9b5706f606632102c0f855bcba7c", size = 1716590 }, + { url = "https://files.pythonhosted.org/packages/79/b1/60370d70cdf8b269ee1444b390cbd72ce514f0d1cd1a715821c784d272c9/aiohttp-3.12.15-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6404dfc8cdde35c69aaa489bb3542fb86ef215fc70277c892be8af540e5e21c0", size = 1699241 }, + { url = "https://files.pythonhosted.org/packages/a3/2b/4968a7b8792437ebc12186db31523f541943e99bda8f30335c482bea6879/aiohttp-3.12.15-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3ead1c00f8521a5c9070fcb88f02967b1d8a0544e6d85c253f6968b785e1a2ab", size = 1754335 }, + { url = "https://files.pythonhosted.org/packages/fb/c1/49524ed553f9a0bec1a11fac09e790f49ff669bcd14164f9fab608831c4d/aiohttp-3.12.15-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6990ef617f14450bc6b34941dba4f12d5613cbf4e33805932f853fbd1cf18bfb", size = 1800491 }, + { url = "https://files.pythonhosted.org/packages/de/5e/3bf5acea47a96a28c121b167f5ef659cf71208b19e52a88cdfa5c37f1fcc/aiohttp-3.12.15-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd736ed420f4db2b8148b52b46b88ed038d0354255f9a73196b7bbce3ea97545", size = 1719929 }, + { url = "https://files.pythonhosted.org/packages/39/94/8ae30b806835bcd1cba799ba35347dee6961a11bd507db634516210e91d8/aiohttp-3.12.15-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c5092ce14361a73086b90c6efb3948ffa5be2f5b6fbcf52e8d8c8b8848bb97c", size = 1635733 }, + { url = "https://files.pythonhosted.org/packages/7a/46/06cdef71dd03acd9da7f51ab3a9107318aee12ad38d273f654e4f981583a/aiohttp-3.12.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:aaa2234bb60c4dbf82893e934d8ee8dea30446f0647e024074237a56a08c01bd", size = 1696790 }, + { url = "https://files.pythonhosted.org/packages/02/90/6b4cfaaf92ed98d0ec4d173e78b99b4b1a7551250be8937d9d67ecb356b4/aiohttp-3.12.15-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:6d86a2fbdd14192e2f234a92d3b494dd4457e683ba07e5905a0b3ee25389ac9f", size = 1718245 }, + { url = "https://files.pythonhosted.org/packages/2e/e6/2593751670fa06f080a846f37f112cbe6f873ba510d070136a6ed46117c6/aiohttp-3.12.15-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a041e7e2612041a6ddf1c6a33b883be6a421247c7afd47e885969ee4cc58bd8d", size = 1658899 }, + { url = "https://files.pythonhosted.org/packages/8f/28/c15bacbdb8b8eb5bf39b10680d129ea7410b859e379b03190f02fa104ffd/aiohttp-3.12.15-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5015082477abeafad7203757ae44299a610e89ee82a1503e3d4184e6bafdd519", size = 1738459 }, + { url = "https://files.pythonhosted.org/packages/00/de/c269cbc4faa01fb10f143b1670633a8ddd5b2e1ffd0548f7aa49cb5c70e2/aiohttp-3.12.15-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:56822ff5ddfd1b745534e658faba944012346184fbfe732e0d6134b744516eea", size = 1766434 }, + { url = "https://files.pythonhosted.org/packages/52/b0/4ff3abd81aa7d929b27d2e1403722a65fc87b763e3a97b3a2a494bfc63bc/aiohttp-3.12.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b2acbbfff69019d9014508c4ba0401822e8bae5a5fdc3b6814285b71231b60f3", size = 1726045 }, + { url = "https://files.pythonhosted.org/packages/71/16/949225a6a2dd6efcbd855fbd90cf476052e648fb011aa538e3b15b89a57a/aiohttp-3.12.15-cp312-cp312-win32.whl", hash = "sha256:d849b0901b50f2185874b9a232f38e26b9b3d4810095a7572eacea939132d4e1", size = 423591 }, + { url = "https://files.pythonhosted.org/packages/2b/d8/fa65d2a349fe938b76d309db1a56a75c4fb8cc7b17a398b698488a939903/aiohttp-3.12.15-cp312-cp312-win_amd64.whl", hash = "sha256:b390ef5f62bb508a9d67cb3bba9b8356e23b3996da7062f1a57ce1a79d2b3d34", size = 450266 }, + { url = "https://files.pythonhosted.org/packages/f2/33/918091abcf102e39d15aba2476ad9e7bd35ddb190dcdd43a854000d3da0d/aiohttp-3.12.15-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9f922ffd05034d439dde1c77a20461cf4a1b0831e6caa26151fe7aa8aaebc315", size = 696741 }, + { url = "https://files.pythonhosted.org/packages/b5/2a/7495a81e39a998e400f3ecdd44a62107254803d1681d9189be5c2e4530cd/aiohttp-3.12.15-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2ee8a8ac39ce45f3e55663891d4b1d15598c157b4d494a4613e704c8b43112cd", size = 474407 }, + { url = "https://files.pythonhosted.org/packages/49/fc/a9576ab4be2dcbd0f73ee8675d16c707cfc12d5ee80ccf4015ba543480c9/aiohttp-3.12.15-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3eae49032c29d356b94eee45a3f39fdf4b0814b397638c2f718e96cfadf4c4e4", size = 466703 }, + { url = "https://files.pythonhosted.org/packages/09/2f/d4bcc8448cf536b2b54eed48f19682031ad182faa3a3fee54ebe5b156387/aiohttp-3.12.15-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b97752ff12cc12f46a9b20327104448042fce5c33a624f88c18f66f9368091c7", size = 1705532 }, + { url = "https://files.pythonhosted.org/packages/f1/f3/59406396083f8b489261e3c011aa8aee9df360a96ac8fa5c2e7e1b8f0466/aiohttp-3.12.15-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:894261472691d6fe76ebb7fcf2e5870a2ac284c7406ddc95823c8598a1390f0d", size = 1686794 }, + { url = "https://files.pythonhosted.org/packages/dc/71/164d194993a8d114ee5656c3b7ae9c12ceee7040d076bf7b32fb98a8c5c6/aiohttp-3.12.15-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5fa5d9eb82ce98959fc1031c28198b431b4d9396894f385cb63f1e2f3f20ca6b", size = 1738865 }, + { url = "https://files.pythonhosted.org/packages/1c/00/d198461b699188a93ead39cb458554d9f0f69879b95078dce416d3209b54/aiohttp-3.12.15-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f0fa751efb11a541f57db59c1dd821bec09031e01452b2b6217319b3a1f34f3d", size = 1788238 }, + { url = "https://files.pythonhosted.org/packages/85/b8/9e7175e1fa0ac8e56baa83bf3c214823ce250d0028955dfb23f43d5e61fd/aiohttp-3.12.15-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5346b93e62ab51ee2a9d68e8f73c7cf96ffb73568a23e683f931e52450e4148d", size = 1710566 }, + { url = "https://files.pythonhosted.org/packages/59/e4/16a8eac9df39b48ae102ec030fa9f726d3570732e46ba0c592aeeb507b93/aiohttp-3.12.15-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:049ec0360f939cd164ecbfd2873eaa432613d5e77d6b04535e3d1fbae5a9e645", size = 1624270 }, + { url = "https://files.pythonhosted.org/packages/1f/f8/cd84dee7b6ace0740908fd0af170f9fab50c2a41ccbc3806aabcb1050141/aiohttp-3.12.15-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b52dcf013b57464b6d1e51b627adfd69a8053e84b7103a7cd49c030f9ca44461", size = 1677294 }, + { url = "https://files.pythonhosted.org/packages/ce/42/d0f1f85e50d401eccd12bf85c46ba84f947a84839c8a1c2c5f6e8ab1eb50/aiohttp-3.12.15-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:9b2af240143dd2765e0fb661fd0361a1b469cab235039ea57663cda087250ea9", size = 1708958 }, + { url = "https://files.pythonhosted.org/packages/d5/6b/f6fa6c5790fb602538483aa5a1b86fcbad66244997e5230d88f9412ef24c/aiohttp-3.12.15-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ac77f709a2cde2cc71257ab2d8c74dd157c67a0558a0d2799d5d571b4c63d44d", size = 1651553 }, + { url = "https://files.pythonhosted.org/packages/04/36/a6d36ad545fa12e61d11d1932eef273928b0495e6a576eb2af04297fdd3c/aiohttp-3.12.15-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:47f6b962246f0a774fbd3b6b7be25d59b06fdb2f164cf2513097998fc6a29693", size = 1727688 }, + { url = "https://files.pythonhosted.org/packages/aa/c8/f195e5e06608a97a4e52c5d41c7927301bf757a8e8bb5bbf8cef6c314961/aiohttp-3.12.15-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:760fb7db442f284996e39cf9915a94492e1896baac44f06ae551974907922b64", size = 1761157 }, + { url = "https://files.pythonhosted.org/packages/05/6a/ea199e61b67f25ba688d3ce93f63b49b0a4e3b3d380f03971b4646412fc6/aiohttp-3.12.15-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ad702e57dc385cae679c39d318def49aef754455f237499d5b99bea4ef582e51", size = 1710050 }, + { url = "https://files.pythonhosted.org/packages/b4/2e/ffeb7f6256b33635c29dbed29a22a723ff2dd7401fff42ea60cf2060abfb/aiohttp-3.12.15-cp313-cp313-win32.whl", hash = "sha256:f813c3e9032331024de2eb2e32a88d86afb69291fbc37a3a3ae81cc9917fb3d0", size = 422647 }, + { url = "https://files.pythonhosted.org/packages/1b/8e/78ee35774201f38d5e1ba079c9958f7629b1fd079459aea9467441dbfbf5/aiohttp-3.12.15-cp313-cp313-win_amd64.whl", hash = "sha256:1a649001580bdb37c6fdb1bebbd7e3bc688e8ec2b5c6f52edbb664662b17dc84", size = 449067 }, +] + +[[package]] +name = "aiosignal" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "frozenlist" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490 }, +] + +[[package]] +name = "alembic" +version = "1.16.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mako" }, + { name = "sqlalchemy" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/83/52/72e791b75c6b1efa803e491f7cbab78e963695e76d4ada05385252927e76/alembic-1.16.4.tar.gz", hash = "sha256:efab6ada0dd0fae2c92060800e0bf5c1dc26af15a10e02fb4babff164b4725e2", size = 1968161 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/62/96b5217b742805236614f05904541000f55422a6060a90d7fd4ce26c172d/alembic-1.16.4-py3-none-any.whl", hash = "sha256:b05e51e8e82efc1abd14ba2af6392897e145930c3e0a2faf2b0da2f7f7fd660d", size = 247026 }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 }, +] + +[[package]] +name = "anthropic" +version = "0.61.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "httpx" }, + { name = "jiter" }, + { name = "pydantic" }, + { name = "sniffio" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7a/9a/b384758ef93b8f931a523efc8782f7191b175714b3952ff11002899f638b/anthropic-0.61.0.tar.gz", hash = "sha256:af4b3b8f3bc4626cca6af2d412e301974da1747179341ad9e271bdf5cbd2f008", size = 426606 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6e/07/c7907eee22f5c27a53118dd2576267052ae01f52811dbb06a2848012639e/anthropic-0.61.0-py3-none-any.whl", hash = "sha256:798c8e6cc61e6315143c3f5847d2f220c45f1e69f433436872a237413ca58803", size = 294935 }, +] + +[[package]] +name = "antlr4-python3-runtime" +version = "4.9.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3e/38/7859ff46355f76f8d19459005ca000b6e7012f2f1ca597746cbcd1fbfe5e/antlr4-python3-runtime-4.9.3.tar.gz", hash = "sha256:f224469b4168294902bb1efa80a8bf7855f24c99aef99cbefc1bcd3cce77881b", size = 117034 } + +[[package]] +name = "anyio" +version = "4.10.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "idna" }, + { name = "sniffio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f1/b4/636b3b65173d3ce9a38ef5f0522789614e590dab6a8d505340a4efe4c567/anyio-4.10.0.tar.gz", hash = "sha256:3f3fae35c96039744587aa5b8371e7e8e603c0702999535961dd336026973ba6", size = 213252 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6f/12/e5e0282d673bb9746bacfb6e2dba8719989d3660cdb2ea79aee9a9651afb/anyio-4.10.0-py3-none-any.whl", hash = "sha256:60e474ac86736bbfd6f210f7a61218939c318f43f9972497381f1c5e930ed3d1", size = 107213 }, +] + +[[package]] +name = "apify-client" +version = "1.12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "apify-shared" }, + { name = "colorama" }, + { name = "httpx" }, + { name = "more-itertools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ce/a6/621fdca657a7fb44c3ee4369e5b337224137e4d464459cd66f9c219a6906/apify_client-1.12.1.tar.gz", hash = "sha256:395e99fec7679fe66462dccddba3c0e146836e8298501bb73ac96fe979dc0695", size = 352663 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6e/c2/c7a1568aec801aa84bbaf93ab390b6bd57e850be30443365370ca3a9ccdc/apify_client-1.12.1-py3-none-any.whl", hash = "sha256:0b331677697dfa1038d17154284fc0bad1b18ba52ab792beb53711af81eac30a", size = 83218 }, +] + +[[package]] +name = "apify-shared" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/63/3e/96de53973fa0704d9b99339fad1838b53d9340870bafc7a9a9f41a7d266f/apify_shared-1.5.0.tar.gz", hash = "sha256:1cba58f0144127f7b52cced426a6527e9722620e9fd1c4ddb6f9c8ce16db0ef1", size = 14639 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/87/fe6b3e7eec76e083ce54bb1b4a19b7dd8f6d3441a3a05e053af6607fcda4/apify_shared-1.5.0-py3-none-any.whl", hash = "sha256:46409a75140d25f3487da87adbf446390214e08cda79c2938aaee085e8f7f9dd", size = 13467 }, +] + +[[package]] +name = "appdirs" +version = "1.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/d8/05696357e0311f5b5c316d7b95f46c669dd9c15aaeecbb48c7d0aeb88c40/appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41", size = 13470 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/00/2344469e2084fb287c2e0b57b72910309874c3245463acd6cf5e3db69324/appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128", size = 9566 }, +] + +[[package]] +name = "asgiref" +version = "3.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/90/61/0aa957eec22ff70b830b22ff91f825e70e1ef732c06666a805730f28b36b/asgiref-3.9.1.tar.gz", hash = "sha256:a5ab6582236218e5ef1648f242fd9f10626cfd4de8dc377db215d5d5098e3142", size = 36870 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/3c/0464dcada90d5da0e71018c04a140ad6349558afb30b3051b4264cc5b965/asgiref-3.9.1-py3-none-any.whl", hash = "sha256:f3bba7092a48005b5f5bacd747d36ee4a5a61f4a269a6df590b43144355ebd2c", size = 23790 }, +] + +[[package]] +name = "asn1crypto" +version = "1.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/de/cf/d547feed25b5244fcb9392e288ff9fdc3280b10260362fc45d37a798a6ee/asn1crypto-1.5.1.tar.gz", hash = "sha256:13ae38502be632115abf8a24cbe5f4da52e3b5231990aff31123c805306ccb9c", size = 121080 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c9/7f/09065fd9e27da0eda08b4d6897f1c13535066174cc023af248fc2a8d5e5a/asn1crypto-1.5.1-py2.py3-none-any.whl", hash = "sha256:db4e40728b728508912cbb3d44f19ce188f218e9eba635821bb4b68564f8fd67", size = 105045 }, +] + +[[package]] +name = "asttokens" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4a/e7/82da0a03e7ba5141f05cce0d302e6eed121ae055e0456ca228bf693984bc/asttokens-3.0.0.tar.gz", hash = "sha256:0dcd8baa8d62b0c1d118b399b2ddba3c4aff271d0d7a9e0d4c1681c79035bbc7", size = 61978 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/25/8a/c46dcc25341b5bce5472c718902eb3d38600a903b14fa6aeecef3f21a46f/asttokens-3.0.0-py3-none-any.whl", hash = "sha256:e3078351a059199dd5138cb1c706e6430c05eff2ff136af5eb4790f9d28932e2", size = 26918 }, +] + +[[package]] +name = "async-timeout" +version = "4.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/87/d6/21b30a550dafea84b1b8eee21b5e23fa16d010ae006011221f33dcd8d7f8/async-timeout-4.0.3.tar.gz", hash = "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f", size = 8345 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/fa/e01228c2938de91d47b307831c62ab9e4001e747789d0b05baf779a6488c/async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028", size = 5721 }, +] + +[[package]] +name = "attrs" +version = "25.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/1367933a8532ee6ff8d63537de4f1177af4bff9f3e829baf7331f595bb24/attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b", size = 812032 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815 }, +] + +[[package]] +name = "authlib" +version = "1.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8e/a1/d8d1c6f8bc922c0b87ae0d933a8ed57be1bef6970894ed79c2852a153cd3/authlib-1.6.1.tar.gz", hash = "sha256:4dffdbb1460ba6ec8c17981a4c67af7d8af131231b5a36a88a1e8c80c111cdfd", size = 159988 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/58/cc6a08053f822f98f334d38a27687b69c6655fb05cd74a7a5e70a2aeed95/authlib-1.6.1-py2.py3-none-any.whl", hash = "sha256:e9d2031c34c6309373ab845afc24168fe9e93dc52d252631f52642f21f5ed06e", size = 239299 }, +] + +[[package]] +name = "backoff" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/47/d7/5bbeb12c44d7c4f2fb5b56abce497eb5ed9f34d85701de869acedd602619/backoff-2.2.1.tar.gz", hash = "sha256:03f829f5bb1923180821643f8753b0502c3b682293992485b0eef2807afa5cba", size = 17001 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/73/b6e24bd22e6720ca8ee9a85a0c4a2971af8497d8f3193fa05390cbd46e09/backoff-2.2.1-py3-none-any.whl", hash = "sha256:63579f9a0628e06278f7e47b7d7d5b6ce20dc65c5e96a6f3ca99a6adca0396e8", size = 15148 }, +] + +[[package]] +name = "backports-asyncio-runner" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8e/ff/70dca7d7cb1cbc0edb2c6cc0c38b65cba36cccc491eca64cabd5fe7f8670/backports_asyncio_runner-1.2.0.tar.gz", hash = "sha256:a5aa7b2b7d8f8bfcaa2b57313f70792df84e32a2a746f585213373f900b42162", size = 69893 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/59/76ab57e3fe74484f48a53f8e337171b4a2349e506eabe136d7e01d059086/backports_asyncio_runner-1.2.0-py3-none-any.whl", hash = "sha256:0da0a936a8aeb554eccb426dc55af3ba63bcdc69fa1a600b5bb305413a4477b5", size = 12313 }, +] + +[[package]] +name = "bcrypt" +version = "4.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bb/5d/6d7433e0f3cd46ce0b43cd65e1db465ea024dbb8216fb2404e919c2ad77b/bcrypt-4.3.0.tar.gz", hash = "sha256:3a3fd2204178b6d2adcf09cb4f6426ffef54762577a7c9b54c159008cb288c18", size = 25697 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/2c/3d44e853d1fe969d229bd58d39ae6902b3d924af0e2b5a60d17d4b809ded/bcrypt-4.3.0-cp313-cp313t-macosx_10_12_universal2.whl", hash = "sha256:f01e060f14b6b57bbb72fc5b4a83ac21c443c9a2ee708e04a10e9192f90a6281", size = 483719 }, + { url = "https://files.pythonhosted.org/packages/a1/e2/58ff6e2a22eca2e2cff5370ae56dba29d70b1ea6fc08ee9115c3ae367795/bcrypt-4.3.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5eeac541cefd0bb887a371ef73c62c3cd78535e4887b310626036a7c0a817bb", size = 272001 }, + { url = "https://files.pythonhosted.org/packages/37/1f/c55ed8dbe994b1d088309e366749633c9eb90d139af3c0a50c102ba68a1a/bcrypt-4.3.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59e1aa0e2cd871b08ca146ed08445038f42ff75968c7ae50d2fdd7860ade2180", size = 277451 }, + { url = "https://files.pythonhosted.org/packages/d7/1c/794feb2ecf22fe73dcfb697ea7057f632061faceb7dcf0f155f3443b4d79/bcrypt-4.3.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:0042b2e342e9ae3d2ed22727c1262f76cc4f345683b5c1715f0250cf4277294f", size = 272792 }, + { url = "https://files.pythonhosted.org/packages/13/b7/0b289506a3f3598c2ae2bdfa0ea66969812ed200264e3f61df77753eee6d/bcrypt-4.3.0-cp313-cp313t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74a8d21a09f5e025a9a23e7c0fd2c7fe8e7503e4d356c0a2c1486ba010619f09", size = 289752 }, + { url = "https://files.pythonhosted.org/packages/dc/24/d0fb023788afe9e83cc118895a9f6c57e1044e7e1672f045e46733421fe6/bcrypt-4.3.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:0142b2cb84a009f8452c8c5a33ace5e3dfec4159e7735f5afe9a4d50a8ea722d", size = 277762 }, + { url = "https://files.pythonhosted.org/packages/e4/38/cde58089492e55ac4ef6c49fea7027600c84fd23f7520c62118c03b4625e/bcrypt-4.3.0-cp313-cp313t-manylinux_2_34_aarch64.whl", hash = "sha256:12fa6ce40cde3f0b899729dbd7d5e8811cb892d31b6f7d0334a1f37748b789fd", size = 272384 }, + { url = "https://files.pythonhosted.org/packages/de/6a/d5026520843490cfc8135d03012a413e4532a400e471e6188b01b2de853f/bcrypt-4.3.0-cp313-cp313t-manylinux_2_34_x86_64.whl", hash = "sha256:5bd3cca1f2aa5dbcf39e2aa13dd094ea181f48959e1071265de49cc2b82525af", size = 277329 }, + { url = "https://files.pythonhosted.org/packages/b3/a3/4fc5255e60486466c389e28c12579d2829b28a527360e9430b4041df4cf9/bcrypt-4.3.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:335a420cfd63fc5bc27308e929bee231c15c85cc4c496610ffb17923abf7f231", size = 305241 }, + { url = "https://files.pythonhosted.org/packages/c7/15/2b37bc07d6ce27cc94e5b10fd5058900eb8fb11642300e932c8c82e25c4a/bcrypt-4.3.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:0e30e5e67aed0187a1764911af023043b4542e70a7461ad20e837e94d23e1d6c", size = 309617 }, + { url = "https://files.pythonhosted.org/packages/5f/1f/99f65edb09e6c935232ba0430c8c13bb98cb3194b6d636e61d93fe60ac59/bcrypt-4.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:3b8d62290ebefd49ee0b3ce7500f5dbdcf13b81402c05f6dafab9a1e1b27212f", size = 335751 }, + { url = "https://files.pythonhosted.org/packages/00/1b/b324030c706711c99769988fcb694b3cb23f247ad39a7823a78e361bdbb8/bcrypt-4.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2ef6630e0ec01376f59a006dc72918b1bf436c3b571b80fa1968d775fa02fe7d", size = 355965 }, + { url = "https://files.pythonhosted.org/packages/aa/dd/20372a0579dd915dfc3b1cd4943b3bca431866fcb1dfdfd7518c3caddea6/bcrypt-4.3.0-cp313-cp313t-win32.whl", hash = "sha256:7a4be4cbf241afee43f1c3969b9103a41b40bcb3a3f467ab19f891d9bc4642e4", size = 155316 }, + { url = "https://files.pythonhosted.org/packages/6d/52/45d969fcff6b5577c2bf17098dc36269b4c02197d551371c023130c0f890/bcrypt-4.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5c1949bf259a388863ced887c7861da1df681cb2388645766c89fdfd9004c669", size = 147752 }, + { url = "https://files.pythonhosted.org/packages/11/22/5ada0b9af72b60cbc4c9a399fdde4af0feaa609d27eb0adc61607997a3fa/bcrypt-4.3.0-cp38-abi3-macosx_10_12_universal2.whl", hash = "sha256:f81b0ed2639568bf14749112298f9e4e2b28853dab50a8b357e31798686a036d", size = 498019 }, + { url = "https://files.pythonhosted.org/packages/b8/8c/252a1edc598dc1ce57905be173328eda073083826955ee3c97c7ff5ba584/bcrypt-4.3.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:864f8f19adbe13b7de11ba15d85d4a428c7e2f344bac110f667676a0ff84924b", size = 279174 }, + { url = "https://files.pythonhosted.org/packages/29/5b/4547d5c49b85f0337c13929f2ccbe08b7283069eea3550a457914fc078aa/bcrypt-4.3.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e36506d001e93bffe59754397572f21bb5dc7c83f54454c990c74a468cd589e", size = 283870 }, + { url = "https://files.pythonhosted.org/packages/be/21/7dbaf3fa1745cb63f776bb046e481fbababd7d344c5324eab47f5ca92dd2/bcrypt-4.3.0-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:842d08d75d9fe9fb94b18b071090220697f9f184d4547179b60734846461ed59", size = 279601 }, + { url = "https://files.pythonhosted.org/packages/6d/64/e042fc8262e971347d9230d9abbe70d68b0a549acd8611c83cebd3eaec67/bcrypt-4.3.0-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7c03296b85cb87db865d91da79bf63d5609284fc0cab9472fdd8367bbd830753", size = 297660 }, + { url = "https://files.pythonhosted.org/packages/50/b8/6294eb84a3fef3b67c69b4470fcdd5326676806bf2519cda79331ab3c3a9/bcrypt-4.3.0-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:62f26585e8b219cdc909b6a0069efc5e4267e25d4a3770a364ac58024f62a761", size = 284083 }, + { url = "https://files.pythonhosted.org/packages/62/e6/baff635a4f2c42e8788fe1b1633911c38551ecca9a749d1052d296329da6/bcrypt-4.3.0-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:beeefe437218a65322fbd0069eb437e7c98137e08f22c4660ac2dc795c31f8bb", size = 279237 }, + { url = "https://files.pythonhosted.org/packages/39/48/46f623f1b0c7dc2e5de0b8af5e6f5ac4cc26408ac33f3d424e5ad8da4a90/bcrypt-4.3.0-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:97eea7408db3a5bcce4a55d13245ab3fa566e23b4c67cd227062bb49e26c585d", size = 283737 }, + { url = "https://files.pythonhosted.org/packages/49/8b/70671c3ce9c0fca4a6cc3cc6ccbaa7e948875a2e62cbd146e04a4011899c/bcrypt-4.3.0-cp38-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:191354ebfe305e84f344c5964c7cd5f924a3bfc5d405c75ad07f232b6dffb49f", size = 312741 }, + { url = "https://files.pythonhosted.org/packages/27/fb/910d3a1caa2d249b6040a5caf9f9866c52114d51523ac2fb47578a27faee/bcrypt-4.3.0-cp38-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:41261d64150858eeb5ff43c753c4b216991e0ae16614a308a15d909503617732", size = 316472 }, + { url = "https://files.pythonhosted.org/packages/dc/cf/7cf3a05b66ce466cfb575dbbda39718d45a609daa78500f57fa9f36fa3c0/bcrypt-4.3.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:33752b1ba962ee793fa2b6321404bf20011fe45b9afd2a842139de3011898fef", size = 343606 }, + { url = "https://files.pythonhosted.org/packages/e3/b8/e970ecc6d7e355c0d892b7f733480f4aa8509f99b33e71550242cf0b7e63/bcrypt-4.3.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:50e6e80a4bfd23a25f5c05b90167c19030cf9f87930f7cb2eacb99f45d1c3304", size = 362867 }, + { url = "https://files.pythonhosted.org/packages/a9/97/8d3118efd8354c555a3422d544163f40d9f236be5b96c714086463f11699/bcrypt-4.3.0-cp38-abi3-win32.whl", hash = "sha256:67a561c4d9fb9465ec866177e7aebcad08fe23aaf6fbd692a6fab69088abfc51", size = 160589 }, + { url = "https://files.pythonhosted.org/packages/29/07/416f0b99f7f3997c69815365babbc2e8754181a4b1899d921b3c7d5b6f12/bcrypt-4.3.0-cp38-abi3-win_amd64.whl", hash = "sha256:584027857bc2843772114717a7490a37f68da563b3620f78a849bcb54dc11e62", size = 152794 }, + { url = "https://files.pythonhosted.org/packages/6e/c1/3fa0e9e4e0bfd3fd77eb8b52ec198fd6e1fd7e9402052e43f23483f956dd/bcrypt-4.3.0-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:0d3efb1157edebfd9128e4e46e2ac1a64e0c1fe46fb023158a407c7892b0f8c3", size = 498969 }, + { url = "https://files.pythonhosted.org/packages/ce/d4/755ce19b6743394787fbd7dff6bf271b27ee9b5912a97242e3caf125885b/bcrypt-4.3.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08bacc884fd302b611226c01014eca277d48f0a05187666bca23aac0dad6fe24", size = 279158 }, + { url = "https://files.pythonhosted.org/packages/9b/5d/805ef1a749c965c46b28285dfb5cd272a7ed9fa971f970435a5133250182/bcrypt-4.3.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6746e6fec103fcd509b96bacdfdaa2fbde9a553245dbada284435173a6f1aef", size = 284285 }, + { url = "https://files.pythonhosted.org/packages/ab/2b/698580547a4a4988e415721b71eb45e80c879f0fb04a62da131f45987b96/bcrypt-4.3.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:afe327968aaf13fc143a56a3360cb27d4ad0345e34da12c7290f1b00b8fe9a8b", size = 279583 }, + { url = "https://files.pythonhosted.org/packages/f2/87/62e1e426418204db520f955ffd06f1efd389feca893dad7095bf35612eec/bcrypt-4.3.0-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d9af79d322e735b1fc33404b5765108ae0ff232d4b54666d46730f8ac1a43676", size = 297896 }, + { url = "https://files.pythonhosted.org/packages/cb/c6/8fedca4c2ada1b6e889c52d2943b2f968d3427e5d65f595620ec4c06fa2f/bcrypt-4.3.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f1e3ffa1365e8702dc48c8b360fef8d7afeca482809c5e45e653af82ccd088c1", size = 284492 }, + { url = "https://files.pythonhosted.org/packages/4d/4d/c43332dcaaddb7710a8ff5269fcccba97ed3c85987ddaa808db084267b9a/bcrypt-4.3.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:3004df1b323d10021fda07a813fd33e0fd57bef0e9a480bb143877f6cba996fe", size = 279213 }, + { url = "https://files.pythonhosted.org/packages/dc/7f/1e36379e169a7df3a14a1c160a49b7b918600a6008de43ff20d479e6f4b5/bcrypt-4.3.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:531457e5c839d8caea9b589a1bcfe3756b0547d7814e9ce3d437f17da75c32b0", size = 284162 }, + { url = "https://files.pythonhosted.org/packages/1c/0a/644b2731194b0d7646f3210dc4d80c7fee3ecb3a1f791a6e0ae6bb8684e3/bcrypt-4.3.0-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:17a854d9a7a476a89dcef6c8bd119ad23e0f82557afbd2c442777a16408e614f", size = 312856 }, + { url = "https://files.pythonhosted.org/packages/dc/62/2a871837c0bb6ab0c9a88bf54de0fc021a6a08832d4ea313ed92a669d437/bcrypt-4.3.0-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:6fb1fd3ab08c0cbc6826a2e0447610c6f09e983a281b919ed721ad32236b8b23", size = 316726 }, + { url = "https://files.pythonhosted.org/packages/0c/a1/9898ea3faac0b156d457fd73a3cb9c2855c6fd063e44b8522925cdd8ce46/bcrypt-4.3.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e965a9c1e9a393b8005031ff52583cedc15b7884fce7deb8b0346388837d6cfe", size = 343664 }, + { url = "https://files.pythonhosted.org/packages/40/f2/71b4ed65ce38982ecdda0ff20c3ad1b15e71949c78b2c053df53629ce940/bcrypt-4.3.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:79e70b8342a33b52b55d93b3a59223a844962bef479f6a0ea318ebbcadf71505", size = 363128 }, + { url = "https://files.pythonhosted.org/packages/11/99/12f6a58eca6dea4be992d6c681b7ec9410a1d9f5cf368c61437e31daa879/bcrypt-4.3.0-cp39-abi3-win32.whl", hash = "sha256:b4d4e57f0a63fd0b358eb765063ff661328f69a04494427265950c71b992a39a", size = 160598 }, + { url = "https://files.pythonhosted.org/packages/a9/cf/45fb5261ece3e6b9817d3d82b2f343a505fd58674a92577923bc500bd1aa/bcrypt-4.3.0-cp39-abi3-win_amd64.whl", hash = "sha256:e53e074b120f2877a35cc6c736b8eb161377caae8925c17688bd46ba56daaa5b", size = 152799 }, + { url = "https://files.pythonhosted.org/packages/55/2d/0c7e5ab0524bf1a443e34cdd3926ec6f5879889b2f3c32b2f5074e99ed53/bcrypt-4.3.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c950d682f0952bafcceaf709761da0a32a942272fad381081b51096ffa46cea1", size = 275367 }, + { url = "https://files.pythonhosted.org/packages/10/4f/f77509f08bdff8806ecc4dc472b6e187c946c730565a7470db772d25df70/bcrypt-4.3.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:107d53b5c67e0bbc3f03ebf5b030e0403d24dda980f8e244795335ba7b4a027d", size = 280644 }, + { url = "https://files.pythonhosted.org/packages/35/18/7d9dc16a3a4d530d0a9b845160e9e5d8eb4f00483e05d44bb4116a1861da/bcrypt-4.3.0-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:b693dbb82b3c27a1604a3dff5bfc5418a7e6a781bb795288141e5f80cf3a3492", size = 274881 }, + { url = "https://files.pythonhosted.org/packages/df/c4/ae6921088adf1e37f2a3a6a688e72e7d9e45fdd3ae5e0bc931870c1ebbda/bcrypt-4.3.0-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:b6354d3760fcd31994a14c89659dee887f1351a06e5dac3c1142307172a79f90", size = 280203 }, + { url = "https://files.pythonhosted.org/packages/4c/b1/1289e21d710496b88340369137cc4c5f6ee036401190ea116a7b4ae6d32a/bcrypt-4.3.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:a839320bf27d474e52ef8cb16449bb2ce0ba03ca9f44daba6d93fa1d8828e48a", size = 275103 }, + { url = "https://files.pythonhosted.org/packages/94/41/19be9fe17e4ffc5d10b7b67f10e459fc4eee6ffe9056a88de511920cfd8d/bcrypt-4.3.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:bdc6a24e754a555d7316fa4774e64c6c3997d27ed2d1964d55920c7c227bc4ce", size = 280513 }, + { url = "https://files.pythonhosted.org/packages/aa/73/05687a9ef89edebdd8ad7474c16d8af685eb4591c3c38300bb6aad4f0076/bcrypt-4.3.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:55a935b8e9a1d2def0626c4269db3fcd26728cbff1e84f0341465c31c4ee56d8", size = 274685 }, + { url = "https://files.pythonhosted.org/packages/63/13/47bba97924ebe86a62ef83dc75b7c8a881d53c535f83e2c54c4bd701e05c/bcrypt-4.3.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:57967b7a28d855313a963aaea51bf6df89f833db4320da458e5b3c5ab6d4c938", size = 280110 }, +] + +[[package]] +name = "beautifulsoup4" +version = "4.13.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "soupsieve" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d8/e4/0c4c39e18fd76d6a628d4dd8da40543d136ce2d1752bd6eeeab0791f4d6b/beautifulsoup4-4.13.4.tar.gz", hash = "sha256:dbb3c4e1ceae6aefebdaf2423247260cd062430a410e38c66f2baa50a8437195", size = 621067 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/50/cd/30110dc0ffcf3b131156077b90e9f60ed75711223f306da4db08eff8403b/beautifulsoup4-4.13.4-py3-none-any.whl", hash = "sha256:9bbbb14bfde9d79f38b8cd5f8c7c85f4b8f2523190ebed90e950a8dea4cb1c4b", size = 187285 }, +] + +[[package]] +name = "bedrock-agentcore" +version = "0.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "boto3" }, + { name = "botocore" }, + { name = "pydantic" }, + { name = "starlette" }, + { name = "typing-extensions" }, + { name = "urllib3" }, + { name = "uvicorn" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b6/68/78ff9be5b18260f657291772e1aaa3856ef78da5a5465b9f0356c818f415/bedrock_agentcore-0.1.1.tar.gz", hash = "sha256:cade2a39ae1bbad5f37842f0bb60758d4ff25fc56e9271404972f0ed71c8b074", size = 170185 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8c/3c/81ed595a5077fe190361c1409955ee0837c402a3f0355e41ab006588d749/bedrock_agentcore-0.1.1-py3-none-any.whl", hash = "sha256:cec53a9c15bcf922f943540d9b14b75e6c69bae5483b9c32c8cd0babbc541804", size = 48910 }, +] + +[[package]] +name = "blinker" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/28/9b3f50ce0e048515135495f198351908d99540d69bfdc8c1d15b73dc55ce/blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", size = 22460 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458 }, +] + +[[package]] +name = "boto3" +version = "1.40.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore" }, + { name = "jmespath" }, + { name = "s3transfer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/a5/c859040c5d3466db6532b0d94bd81ab490093194387621b3fefd14b1f9db/boto3-1.40.3.tar.gz", hash = "sha256:8cdda3a3fbaa0229aa32fdf2f6f59b5c96e5cd5916ed45be378c06fae09cef19", size = 111805 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/89/12/d4977c85fbac3dff809558f61f486fdb3e674a87db455e321a53785d11b4/boto3-1.40.3-py3-none-any.whl", hash = "sha256:6e8ace4439b5a03ce1b07532a86a3e56fc0adc268bcdeef55624d64f99e90e2a", size = 139882 }, +] + +[[package]] +name = "botocore" +version = "1.40.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jmespath" }, + { name = "python-dateutil" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e0/0a/162669b946a4f0f44494347c407e3f7d268634a99a6f623c7b1b0fe9a959/botocore-1.40.3.tar.gz", hash = "sha256:bba6b642fff19e32bee52edbbb8dd3f45e37ba7b8e54addc9ae3b105c4eaf2a4", size = 14309624 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ad/e7/c27a2cad80dd0a47e9a5c942b5734bd05a95db6b4d6cd778393183d78c6a/botocore-1.40.3-py3-none-any.whl", hash = "sha256:0c6d00b4412babb5e3d0944b5e057d31f763bf54429d5667f367e7b46e5c1c22", size = 13970985 }, +] + +[[package]] +name = "browserbase" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "httpx" }, + { name = "pydantic" }, + { name = "sniffio" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/df/17ac5e1116ab8f1264c6a9718f935358d20bdcd8ae0e3d1f18fd580cd871/browserbase-1.4.0.tar.gz", hash = "sha256:e2ed36f513c8630b94b826042c4bb9f497c333f3bd28e5b76cb708c65b4318a0", size = 122103 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/13/4191423982a2ec69dc8c10a1c4b94a50a0861f49be81ffc19621b75841bc/browserbase-1.4.0-py3-none-any.whl", hash = "sha256:ea9f1fb4a88921975b8b9606835c441a59d8ce82ce00313a6d48bbe8e30f79fb", size = 98044 }, +] + +[[package]] +name = "build" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "(os_name == 'nt' and platform_machine != 'aarch64' and sys_platform == 'linux') or (os_name == 'nt' and sys_platform != 'darwin' and sys_platform != 'linux')" }, + { name = "importlib-metadata", marker = "python_full_version < '3.10.2'" }, + { name = "packaging" }, + { name = "pyproject-hooks" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/25/1c/23e33405a7c9eac261dff640926b8b5adaed6a6eb3e1767d441ed611d0c0/build-1.3.0.tar.gz", hash = "sha256:698edd0ea270bde950f53aed21f3a0135672206f3911e0176261a31e0e07b397", size = 48544 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/8c/2b30c12155ad8de0cf641d76a8b396a16d2c36bc6d50b621a62b7c4567c1/build-1.3.0-py3-none-any.whl", hash = "sha256:7145f0b5061ba90a1500d60bd1b13ca0a8a4cebdd0cc16ed8adf1c0e739f43b4", size = 23382 }, +] + +[[package]] +name = "cachetools" +version = "5.5.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6c/81/3747dad6b14fa2cf53fcf10548cf5aea6913e96fab41a3c198676f8948a5/cachetools-5.5.2.tar.gz", hash = "sha256:1a661caa9175d26759571b2e19580f9d6393969e5dfca11fdb1f947a23e640d4", size = 28380 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/72/76/20fa66124dbe6be5cafeb312ece67de6b61dd91a0247d1ea13db4ebb33c2/cachetools-5.5.2-py3-none-any.whl", hash = "sha256:d26a22bcc62eb95c3beabd9f1ee5e820d3d2704fe2967cbe350e20c8ffcd3f0a", size = 10080 }, +] + +[[package]] +name = "certifi" +version = "2025.8.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/67/960ebe6bf230a96cda2e0abcf73af550ec4f090005363542f0765df162e0/certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407", size = 162386 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5", size = 161216 }, +] + +[[package]] +name = "cffi" +version = "1.17.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/07/f44ca684db4e4f08a3fdc6eeb9a0d15dc6883efc7b8c90357fdbf74e186c/cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14", size = 182191 }, + { url = "https://files.pythonhosted.org/packages/08/fd/cc2fedbd887223f9f5d170c96e57cbf655df9831a6546c1727ae13fa977a/cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67", size = 178592 }, + { url = "https://files.pythonhosted.org/packages/de/cc/4635c320081c78d6ffc2cab0a76025b691a91204f4aa317d568ff9280a2d/cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382", size = 426024 }, + { url = "https://files.pythonhosted.org/packages/b6/7b/3b2b250f3aab91abe5f8a51ada1b717935fdaec53f790ad4100fe2ec64d1/cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702", size = 448188 }, + { url = "https://files.pythonhosted.org/packages/d3/48/1b9283ebbf0ec065148d8de05d647a986c5f22586b18120020452fff8f5d/cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3", size = 455571 }, + { url = "https://files.pythonhosted.org/packages/40/87/3b8452525437b40f39ca7ff70276679772ee7e8b394934ff60e63b7b090c/cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6", size = 436687 }, + { url = "https://files.pythonhosted.org/packages/8d/fb/4da72871d177d63649ac449aec2e8a29efe0274035880c7af59101ca2232/cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17", size = 446211 }, + { url = "https://files.pythonhosted.org/packages/ab/a0/62f00bcb411332106c02b663b26f3545a9ef136f80d5df746c05878f8c4b/cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8", size = 461325 }, + { url = "https://files.pythonhosted.org/packages/36/83/76127035ed2e7e27b0787604d99da630ac3123bfb02d8e80c633f218a11d/cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e", size = 438784 }, + { url = "https://files.pythonhosted.org/packages/21/81/a6cd025db2f08ac88b901b745c163d884641909641f9b826e8cb87645942/cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be", size = 461564 }, + { url = "https://files.pythonhosted.org/packages/f8/fe/4d41c2f200c4a457933dbd98d3cf4e911870877bd94d9656cc0fcb390681/cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c", size = 171804 }, + { url = "https://files.pythonhosted.org/packages/d1/b6/0b0f5ab93b0df4acc49cae758c81fe4e5ef26c3ae2e10cc69249dfd8b3ab/cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15", size = 181299 }, + { url = "https://files.pythonhosted.org/packages/6b/f4/927e3a8899e52a27fa57a48607ff7dc91a9ebe97399b357b85a0c7892e00/cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401", size = 182264 }, + { url = "https://files.pythonhosted.org/packages/6c/f5/6c3a8efe5f503175aaddcbea6ad0d2c96dad6f5abb205750d1b3df44ef29/cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf", size = 178651 }, + { url = "https://files.pythonhosted.org/packages/94/dd/a3f0118e688d1b1a57553da23b16bdade96d2f9bcda4d32e7d2838047ff7/cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4", size = 445259 }, + { url = "https://files.pythonhosted.org/packages/2e/ea/70ce63780f096e16ce8588efe039d3c4f91deb1dc01e9c73a287939c79a6/cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41", size = 469200 }, + { url = "https://files.pythonhosted.org/packages/1c/a0/a4fa9f4f781bda074c3ddd57a572b060fa0df7655d2a4247bbe277200146/cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1", size = 477235 }, + { url = "https://files.pythonhosted.org/packages/62/12/ce8710b5b8affbcdd5c6e367217c242524ad17a02fe5beec3ee339f69f85/cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6", size = 459721 }, + { url = "https://files.pythonhosted.org/packages/ff/6b/d45873c5e0242196f042d555526f92aa9e0c32355a1be1ff8c27f077fd37/cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d", size = 467242 }, + { url = "https://files.pythonhosted.org/packages/1a/52/d9a0e523a572fbccf2955f5abe883cfa8bcc570d7faeee06336fbd50c9fc/cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6", size = 477999 }, + { url = "https://files.pythonhosted.org/packages/44/74/f2a2460684a1a2d00ca799ad880d54652841a780c4c97b87754f660c7603/cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f", size = 454242 }, + { url = "https://files.pythonhosted.org/packages/f8/4a/34599cac7dfcd888ff54e801afe06a19c17787dfd94495ab0c8d35fe99fb/cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b", size = 478604 }, + { url = "https://files.pythonhosted.org/packages/34/33/e1b8a1ba29025adbdcda5fb3a36f94c03d771c1b7b12f726ff7fef2ebe36/cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655", size = 171727 }, + { url = "https://files.pythonhosted.org/packages/3d/97/50228be003bb2802627d28ec0627837ac0bf35c90cf769812056f235b2d1/cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0", size = 181400 }, + { url = "https://files.pythonhosted.org/packages/5a/84/e94227139ee5fb4d600a7a4927f322e1d4aea6fdc50bd3fca8493caba23f/cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", size = 183178 }, + { url = "https://files.pythonhosted.org/packages/da/ee/fb72c2b48656111c4ef27f0f91da355e130a923473bf5ee75c5643d00cca/cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", size = 178840 }, + { url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803 }, + { url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850 }, + { url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729 }, + { url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256 }, + { url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424 }, + { url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568 }, + { url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736 }, + { url = "https://files.pythonhosted.org/packages/86/c5/28b2d6f799ec0bdecf44dced2ec5ed43e0eb63097b0f58c293583b406582/cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", size = 172448 }, + { url = "https://files.pythonhosted.org/packages/50/b9/db34c4755a7bd1cb2d1603ac3863f22bcecbd1ba29e5ee841a4bc510b294/cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", size = 181976 }, + { url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989 }, + { url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802 }, + { url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792 }, + { url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893 }, + { url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810 }, + { url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200 }, + { url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447 }, + { url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358 }, + { url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469 }, + { url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475 }, + { url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009 }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", size = 126367 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/28/9901804da60055b406e1a1c5ba7aac1276fb77f1dde635aabfc7fd84b8ab/charset_normalizer-3.4.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c48ed483eb946e6c04ccbe02c6b4d1d48e51944b6db70f697e089c193404941", size = 201818 }, + { url = "https://files.pythonhosted.org/packages/d9/9b/892a8c8af9110935e5adcbb06d9c6fe741b6bb02608c6513983048ba1a18/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2d318c11350e10662026ad0eb71bb51c7812fc8590825304ae0bdd4ac283acd", size = 144649 }, + { url = "https://files.pythonhosted.org/packages/7b/a5/4179abd063ff6414223575e008593861d62abfc22455b5d1a44995b7c101/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9cbfacf36cb0ec2897ce0ebc5d08ca44213af24265bd56eca54bee7923c48fd6", size = 155045 }, + { url = "https://files.pythonhosted.org/packages/3b/95/bc08c7dfeddd26b4be8c8287b9bb055716f31077c8b0ea1cd09553794665/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18dd2e350387c87dabe711b86f83c9c78af772c748904d372ade190b5c7c9d4d", size = 147356 }, + { url = "https://files.pythonhosted.org/packages/a8/2d/7a5b635aa65284bf3eab7653e8b4151ab420ecbae918d3e359d1947b4d61/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8075c35cd58273fee266c58c0c9b670947c19df5fb98e7b66710e04ad4e9ff86", size = 149471 }, + { url = "https://files.pythonhosted.org/packages/ae/38/51fc6ac74251fd331a8cfdb7ec57beba8c23fd5493f1050f71c87ef77ed0/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5bf4545e3b962767e5c06fe1738f951f77d27967cb2caa64c28be7c4563e162c", size = 151317 }, + { url = "https://files.pythonhosted.org/packages/b7/17/edee1e32215ee6e9e46c3e482645b46575a44a2d72c7dfd49e49f60ce6bf/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7a6ab32f7210554a96cd9e33abe3ddd86732beeafc7a28e9955cdf22ffadbab0", size = 146368 }, + { url = "https://files.pythonhosted.org/packages/26/2c/ea3e66f2b5f21fd00b2825c94cafb8c326ea6240cd80a91eb09e4a285830/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b33de11b92e9f75a2b545d6e9b6f37e398d86c3e9e9653c4864eb7e89c5773ef", size = 154491 }, + { url = "https://files.pythonhosted.org/packages/52/47/7be7fa972422ad062e909fd62460d45c3ef4c141805b7078dbab15904ff7/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8755483f3c00d6c9a77f490c17e6ab0c8729e39e6390328e42521ef175380ae6", size = 157695 }, + { url = "https://files.pythonhosted.org/packages/2f/42/9f02c194da282b2b340f28e5fb60762de1151387a36842a92b533685c61e/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:68a328e5f55ec37c57f19ebb1fdc56a248db2e3e9ad769919a58672958e8f366", size = 154849 }, + { url = "https://files.pythonhosted.org/packages/67/44/89cacd6628f31fb0b63201a618049be4be2a7435a31b55b5eb1c3674547a/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:21b2899062867b0e1fde9b724f8aecb1af14f2778d69aacd1a5a1853a597a5db", size = 150091 }, + { url = "https://files.pythonhosted.org/packages/1f/79/4b8da9f712bc079c0f16b6d67b099b0b8d808c2292c937f267d816ec5ecc/charset_normalizer-3.4.2-cp310-cp310-win32.whl", hash = "sha256:e8082b26888e2f8b36a042a58307d5b917ef2b1cacab921ad3323ef91901c71a", size = 98445 }, + { url = "https://files.pythonhosted.org/packages/7d/d7/96970afb4fb66497a40761cdf7bd4f6fca0fc7bafde3a84f836c1f57a926/charset_normalizer-3.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:f69a27e45c43520f5487f27627059b64aaf160415589230992cec34c5e18a509", size = 105782 }, + { url = "https://files.pythonhosted.org/packages/05/85/4c40d00dcc6284a1c1ad5de5e0996b06f39d8232f1031cd23c2f5c07ee86/charset_normalizer-3.4.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2", size = 198794 }, + { url = "https://files.pythonhosted.org/packages/41/d9/7a6c0b9db952598e97e93cbdfcb91bacd89b9b88c7c983250a77c008703c/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645", size = 142846 }, + { url = "https://files.pythonhosted.org/packages/66/82/a37989cda2ace7e37f36c1a8ed16c58cf48965a79c2142713244bf945c89/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd", size = 153350 }, + { url = "https://files.pythonhosted.org/packages/df/68/a576b31b694d07b53807269d05ec3f6f1093e9545e8607121995ba7a8313/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8", size = 145657 }, + { url = "https://files.pythonhosted.org/packages/92/9b/ad67f03d74554bed3aefd56fe836e1623a50780f7c998d00ca128924a499/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f", size = 147260 }, + { url = "https://files.pythonhosted.org/packages/a6/e6/8aebae25e328160b20e31a7e9929b1578bbdc7f42e66f46595a432f8539e/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7", size = 149164 }, + { url = "https://files.pythonhosted.org/packages/8b/f2/b3c2f07dbcc248805f10e67a0262c93308cfa149a4cd3d1fe01f593e5fd2/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9", size = 144571 }, + { url = "https://files.pythonhosted.org/packages/60/5b/c3f3a94bc345bc211622ea59b4bed9ae63c00920e2e8f11824aa5708e8b7/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544", size = 151952 }, + { url = "https://files.pythonhosted.org/packages/e2/4d/ff460c8b474122334c2fa394a3f99a04cf11c646da895f81402ae54f5c42/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82", size = 155959 }, + { url = "https://files.pythonhosted.org/packages/a2/2b/b964c6a2fda88611a1fe3d4c400d39c66a42d6c169c924818c848f922415/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0", size = 153030 }, + { url = "https://files.pythonhosted.org/packages/59/2e/d3b9811db26a5ebf444bc0fa4f4be5aa6d76fc6e1c0fd537b16c14e849b6/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5", size = 148015 }, + { url = "https://files.pythonhosted.org/packages/90/07/c5fd7c11eafd561bb51220d600a788f1c8d77c5eef37ee49454cc5c35575/charset_normalizer-3.4.2-cp311-cp311-win32.whl", hash = "sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a", size = 98106 }, + { url = "https://files.pythonhosted.org/packages/a8/05/5e33dbef7e2f773d672b6d79f10ec633d4a71cd96db6673625838a4fd532/charset_normalizer-3.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28", size = 105402 }, + { url = "https://files.pythonhosted.org/packages/d7/a4/37f4d6035c89cac7930395a35cc0f1b872e652eaafb76a6075943754f095/charset_normalizer-3.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7", size = 199936 }, + { url = "https://files.pythonhosted.org/packages/ee/8a/1a5e33b73e0d9287274f899d967907cd0bf9c343e651755d9307e0dbf2b3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3", size = 143790 }, + { url = "https://files.pythonhosted.org/packages/66/52/59521f1d8e6ab1482164fa21409c5ef44da3e9f653c13ba71becdd98dec3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a", size = 153924 }, + { url = "https://files.pythonhosted.org/packages/86/2d/fb55fdf41964ec782febbf33cb64be480a6b8f16ded2dbe8db27a405c09f/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214", size = 146626 }, + { url = "https://files.pythonhosted.org/packages/8c/73/6ede2ec59bce19b3edf4209d70004253ec5f4e319f9a2e3f2f15601ed5f7/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a", size = 148567 }, + { url = "https://files.pythonhosted.org/packages/09/14/957d03c6dc343c04904530b6bef4e5efae5ec7d7990a7cbb868e4595ee30/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd", size = 150957 }, + { url = "https://files.pythonhosted.org/packages/0d/c8/8174d0e5c10ccebdcb1b53cc959591c4c722a3ad92461a273e86b9f5a302/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981", size = 145408 }, + { url = "https://files.pythonhosted.org/packages/58/aa/8904b84bc8084ac19dc52feb4f5952c6df03ffb460a887b42615ee1382e8/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c", size = 153399 }, + { url = "https://files.pythonhosted.org/packages/c2/26/89ee1f0e264d201cb65cf054aca6038c03b1a0c6b4ae998070392a3ce605/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b", size = 156815 }, + { url = "https://files.pythonhosted.org/packages/fd/07/68e95b4b345bad3dbbd3a8681737b4338ff2c9df29856a6d6d23ac4c73cb/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d", size = 154537 }, + { url = "https://files.pythonhosted.org/packages/77/1a/5eefc0ce04affb98af07bc05f3bac9094513c0e23b0562d64af46a06aae4/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f", size = 149565 }, + { url = "https://files.pythonhosted.org/packages/37/a0/2410e5e6032a174c95e0806b1a6585eb21e12f445ebe239fac441995226a/charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c", size = 98357 }, + { url = "https://files.pythonhosted.org/packages/6c/4f/c02d5c493967af3eda9c771ad4d2bbc8df6f99ddbeb37ceea6e8716a32bc/charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e", size = 105776 }, + { url = "https://files.pythonhosted.org/packages/ea/12/a93df3366ed32db1d907d7593a94f1fe6293903e3e92967bebd6950ed12c/charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0", size = 199622 }, + { url = "https://files.pythonhosted.org/packages/04/93/bf204e6f344c39d9937d3c13c8cd5bbfc266472e51fc8c07cb7f64fcd2de/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf", size = 143435 }, + { url = "https://files.pythonhosted.org/packages/22/2a/ea8a2095b0bafa6c5b5a55ffdc2f924455233ee7b91c69b7edfcc9e02284/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e", size = 153653 }, + { url = "https://files.pythonhosted.org/packages/b6/57/1b090ff183d13cef485dfbe272e2fe57622a76694061353c59da52c9a659/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1", size = 146231 }, + { url = "https://files.pythonhosted.org/packages/e2/28/ffc026b26f441fc67bd21ab7f03b313ab3fe46714a14b516f931abe1a2d8/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c", size = 148243 }, + { url = "https://files.pythonhosted.org/packages/c0/0f/9abe9bd191629c33e69e47c6ef45ef99773320e9ad8e9cb08b8ab4a8d4cb/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691", size = 150442 }, + { url = "https://files.pythonhosted.org/packages/67/7c/a123bbcedca91d5916c056407f89a7f5e8fdfce12ba825d7d6b9954a1a3c/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0", size = 145147 }, + { url = "https://files.pythonhosted.org/packages/ec/fe/1ac556fa4899d967b83e9893788e86b6af4d83e4726511eaaad035e36595/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b", size = 153057 }, + { url = "https://files.pythonhosted.org/packages/2b/ff/acfc0b0a70b19e3e54febdd5301a98b72fa07635e56f24f60502e954c461/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff", size = 156454 }, + { url = "https://files.pythonhosted.org/packages/92/08/95b458ce9c740d0645feb0e96cea1f5ec946ea9c580a94adfe0b617f3573/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b", size = 154174 }, + { url = "https://files.pythonhosted.org/packages/78/be/8392efc43487ac051eee6c36d5fbd63032d78f7728cb37aebcc98191f1ff/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148", size = 149166 }, + { url = "https://files.pythonhosted.org/packages/44/96/392abd49b094d30b91d9fbda6a69519e95802250b777841cf3bda8fe136c/charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7", size = 98064 }, + { url = "https://files.pythonhosted.org/packages/e9/b0/0200da600134e001d91851ddc797809e2fe0ea72de90e09bec5a2fbdaccb/charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980", size = 105641 }, + { url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626 }, +] + +[[package]] +name = "chroma-hnswlib" +version = "0.7.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/73/09/10d57569e399ce9cbc5eee2134996581c957f63a9addfa6ca657daf006b8/chroma_hnswlib-0.7.6.tar.gz", hash = "sha256:4dce282543039681160259d29fcde6151cc9106c6461e0485f57cdccd83059b7", size = 32256 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/74/b9dde05ea8685d2f8c4681b517e61c7887e974f6272bb24ebc8f2105875b/chroma_hnswlib-0.7.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f35192fbbeadc8c0633f0a69c3d3e9f1a4eab3a46b65458bbcbcabdd9e895c36", size = 195821 }, + { url = "https://files.pythonhosted.org/packages/fd/58/101bfa6bc41bc6cc55fbb5103c75462a7bf882e1704256eb4934df85b6a8/chroma_hnswlib-0.7.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6f007b608c96362b8f0c8b6b2ac94f67f83fcbabd857c378ae82007ec92f4d82", size = 183854 }, + { url = "https://files.pythonhosted.org/packages/17/ff/95d49bb5ce134f10d6aa08d5f3bec624eaff945f0b17d8c3fce888b9a54a/chroma_hnswlib-0.7.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:456fd88fa0d14e6b385358515aef69fc89b3c2191706fd9aee62087b62aad09c", size = 2358774 }, + { url = "https://files.pythonhosted.org/packages/3a/6d/27826180a54df80dbba8a4f338b022ba21c0c8af96fd08ff8510626dee8f/chroma_hnswlib-0.7.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5dfaae825499c2beaa3b75a12d7ec713b64226df72a5c4097203e3ed532680da", size = 2392739 }, + { url = "https://files.pythonhosted.org/packages/d6/63/ee3e8b7a8f931918755faacf783093b61f32f59042769d9db615999c3de0/chroma_hnswlib-0.7.6-cp310-cp310-win_amd64.whl", hash = "sha256:2487201982241fb1581be26524145092c95902cb09fc2646ccfbc407de3328ec", size = 150955 }, + { url = "https://files.pythonhosted.org/packages/f5/af/d15fdfed2a204c0f9467ad35084fbac894c755820b203e62f5dcba2d41f1/chroma_hnswlib-0.7.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:81181d54a2b1e4727369486a631f977ffc53c5533d26e3d366dda243fb0998ca", size = 196911 }, + { url = "https://files.pythonhosted.org/packages/0d/19/aa6f2139f1ff7ad23a690ebf2a511b2594ab359915d7979f76f3213e46c4/chroma_hnswlib-0.7.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4b4ab4e11f1083dd0a11ee4f0e0b183ca9f0f2ed63ededba1935b13ce2b3606f", size = 185000 }, + { url = "https://files.pythonhosted.org/packages/79/b1/1b269c750e985ec7d40b9bbe7d66d0a890e420525187786718e7f6b07913/chroma_hnswlib-0.7.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:53db45cd9173d95b4b0bdccb4dbff4c54a42b51420599c32267f3abbeb795170", size = 2377289 }, + { url = "https://files.pythonhosted.org/packages/c7/2d/d5663e134436e5933bc63516a20b5edc08b4c1b1588b9680908a5f1afd04/chroma_hnswlib-0.7.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c093f07a010b499c00a15bc9376036ee4800d335360570b14f7fe92badcdcf9", size = 2411755 }, + { url = "https://files.pythonhosted.org/packages/3e/79/1bce519cf186112d6d5ce2985392a89528c6e1e9332d680bf752694a4cdf/chroma_hnswlib-0.7.6-cp311-cp311-win_amd64.whl", hash = "sha256:0540b0ac96e47d0aa39e88ea4714358ae05d64bbe6bf33c52f316c664190a6a3", size = 151888 }, + { url = "https://files.pythonhosted.org/packages/93/ac/782b8d72de1c57b64fdf5cb94711540db99a92768d93d973174c62d45eb8/chroma_hnswlib-0.7.6-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e87e9b616c281bfbe748d01705817c71211613c3b063021f7ed5e47173556cb7", size = 197804 }, + { url = "https://files.pythonhosted.org/packages/32/4e/fd9ce0764228e9a98f6ff46af05e92804090b5557035968c5b4198bc7af9/chroma_hnswlib-0.7.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ec5ca25bc7b66d2ecbf14502b5729cde25f70945d22f2aaf523c2d747ea68912", size = 185421 }, + { url = "https://files.pythonhosted.org/packages/d9/3d/b59a8dedebd82545d873235ef2d06f95be244dfece7ee4a1a6044f080b18/chroma_hnswlib-0.7.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:305ae491de9d5f3c51e8bd52d84fdf2545a4a2bc7af49765cda286b7bb30b1d4", size = 2389672 }, + { url = "https://files.pythonhosted.org/packages/74/1e/80a033ea4466338824974a34f418e7b034a7748bf906f56466f5caa434b0/chroma_hnswlib-0.7.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:822ede968d25a2c88823ca078a58f92c9b5c4142e38c7c8b4c48178894a0a3c5", size = 2436986 }, +] + +[[package]] +name = "chromadb" +version = "0.5.23" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "bcrypt" }, + { name = "build" }, + { name = "chroma-hnswlib" }, + { name = "fastapi" }, + { name = "grpcio" }, + { name = "httpx" }, + { name = "importlib-resources" }, + { name = "kubernetes" }, + { name = "mmh3" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "onnxruntime" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp-proto-grpc" }, + { name = "opentelemetry-instrumentation-fastapi" }, + { name = "opentelemetry-sdk" }, + { name = "orjson" }, + { name = "overrides" }, + { name = "posthog" }, + { name = "pydantic" }, + { name = "pypika" }, + { name = "pyyaml" }, + { name = "rich" }, + { name = "tenacity" }, + { name = "tokenizers" }, + { name = "tqdm" }, + { name = "typer" }, + { name = "typing-extensions" }, + { name = "uvicorn", extra = ["standard"] }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/64/28daa773f784bcd18de944fe26ed301de844d6ee17188e26a9d6b4baf122/chromadb-0.5.23.tar.gz", hash = "sha256:360a12b9795c5a33cb1f839d14410ccbde662ef1accd36153b0ae22312edabd1", size = 33700455 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/8c/a9eb95a28e6c35a0122417976a9d435eeaceb53f596a8973e33b3dd4cfac/chromadb-0.5.23-py3-none-any.whl", hash = "sha256:ffe5bdd7276d12cb682df0d38a13aa37573e6a3678e71889ac45f539ae05ad7e", size = 628347 }, +] + +[[package]] +name = "click" +version = "8.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215 }, +] + +[[package]] +name = "cohere" +version = "5.16.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "fastavro" }, + { name = "httpx" }, + { name = "httpx-sse" }, + { name = "pydantic" }, + { name = "pydantic-core" }, + { name = "requests" }, + { name = "tokenizers" }, + { name = "types-requests" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ed/c0/dcbbef24aea47b7fa58887dd3e002fa378a04cef19ad0207a90c2eadfee8/cohere-5.16.2.tar.gz", hash = "sha256:30febd58168983647b4125831a6ac2a8db4643d222cf04373e53b9959c8d05f9", size = 163976 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/8a/3fbe81a41673d320acbc721eb4a050bef15c8bafa251b10eeddfe0cf9f61/cohere-5.16.2-py3-none-any.whl", hash = "sha256:c2c877dd6fd0bdbc8686b390322a340ad736e1cc65e3e0b6b0cbdc339bfeadbc", size = 294027 }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, +] + +[[package]] +name = "coloredlogs" +version = "15.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "humanfriendly" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cc/c7/eed8f27100517e8c0e6b923d5f0845d0cb99763da6fdee00478f91db7325/coloredlogs-15.0.1.tar.gz", hash = "sha256:7c991aa71a4577af2f82600d8f8f3a89f936baeaf9b50a9c197da014e5bf16b0", size = 278520 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/06/3d6badcf13db419e25b07041d9c7b4a2c331d3f4e7134445ec5df57714cd/coloredlogs-15.0.1-py2.py3-none-any.whl", hash = "sha256:612ee75c546f53e92e70049c9dbfcc18c935a2b9a53b66085ce9ef6a6e5c0934", size = 46018 }, +] + +[[package]] +name = "composio-core" +version = "0.7.20" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, + { name = "click" }, + { name = "fastapi" }, + { name = "importlib-metadata" }, + { name = "inflection" }, + { name = "jsonref" }, + { name = "jsonschema" }, + { name = "paramiko" }, + { name = "pillow" }, + { name = "pydantic" }, + { name = "pyperclip" }, + { name = "pysher" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "rich" }, + { name = "semver" }, + { name = "sentry-sdk" }, + { name = "uvicorn" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bc/d3/00262ca35be5e6a171104a917cd962179c62d709f0a419030899534be689/composio_core-0.7.20.tar.gz", hash = "sha256:1dc29dbf73eb72d2df1c5b0d4d2f21459d15029322cf74df8fdecc44dcaeb1f4", size = 334637 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/56/b4e2ccdda8bc7732c5616bdb3bb4cea6019fdbdbbb2ee435ca784055cb8e/composio_core-0.7.20-py3-none-any.whl", hash = "sha256:e1cfb9cfc68a4622bc15827143ddf726f429d281e8f9de5d4c0965e75d039f14", size = 501152 }, +] + +[[package]] +name = "contextual-client" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "httpx" }, + { name = "pydantic" }, + { name = "sniffio" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/02/4d/1219b84a73551c1f70be465c8e4b496ebf788152f7b124a84cc3895d2390/contextual_client-0.8.0.tar.gz", hash = "sha256:e97c3e7c5d9b5a97f23fb7b4adfe34d8d9a42817415335b1b48f6d6774bc2747", size = 148896 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/80/f1/336d9fe785004b38f3850367833be8c7d91a4a8f2ceefae5e1cfa5d08a05/contextual_client-0.8.0-py3-none-any.whl", hash = "sha256:41b6fba00e7bddd1ca06bbd3ddc7269c400e049f7c82b2bcc5302746c704dda3", size = 154607 }, +] + +[[package]] +name = "contourpy" +version = "1.3.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11' and platform_python_implementation == 'PyPy' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and platform_python_implementation == 'PyPy' and sys_platform == 'linux'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and platform_python_implementation == 'PyPy' and sys_platform == 'linux') or (python_full_version < '3.11' and platform_python_implementation == 'PyPy' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version < '3.11' and platform_python_implementation != 'PyPy' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and platform_python_implementation != 'PyPy' and sys_platform == 'linux'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and platform_python_implementation != 'PyPy' and sys_platform == 'linux') or (python_full_version < '3.11' and platform_python_implementation != 'PyPy' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/54/eb9bfc647b19f2009dd5c7f5ec51c4e6ca831725f1aea7a993034f483147/contourpy-1.3.2.tar.gz", hash = "sha256:b6945942715a034c671b7fc54f9588126b0b8bf23db2696e3ca8328f3ff0ab54", size = 13466130 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/a3/da4153ec8fe25d263aa48c1a4cbde7f49b59af86f0b6f7862788c60da737/contourpy-1.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ba38e3f9f330af820c4b27ceb4b9c7feee5fe0493ea53a8720f4792667465934", size = 268551 }, + { url = "https://files.pythonhosted.org/packages/2f/6c/330de89ae1087eb622bfca0177d32a7ece50c3ef07b28002de4757d9d875/contourpy-1.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dc41ba0714aa2968d1f8674ec97504a8f7e334f48eeacebcaa6256213acb0989", size = 253399 }, + { url = "https://files.pythonhosted.org/packages/c1/bd/20c6726b1b7f81a8bee5271bed5c165f0a8e1f572578a9d27e2ccb763cb2/contourpy-1.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9be002b31c558d1ddf1b9b415b162c603405414bacd6932d031c5b5a8b757f0d", size = 312061 }, + { url = "https://files.pythonhosted.org/packages/22/fc/a9665c88f8a2473f823cf1ec601de9e5375050f1958cbb356cdf06ef1ab6/contourpy-1.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8d2e74acbcba3bfdb6d9d8384cdc4f9260cae86ed9beee8bd5f54fee49a430b9", size = 351956 }, + { url = "https://files.pythonhosted.org/packages/25/eb/9f0a0238f305ad8fb7ef42481020d6e20cf15e46be99a1fcf939546a177e/contourpy-1.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e259bced5549ac64410162adc973c5e2fb77f04df4a439d00b478e57a0e65512", size = 320872 }, + { url = "https://files.pythonhosted.org/packages/32/5c/1ee32d1c7956923202f00cf8d2a14a62ed7517bdc0ee1e55301227fc273c/contourpy-1.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad687a04bc802cbe8b9c399c07162a3c35e227e2daccf1668eb1f278cb698631", size = 325027 }, + { url = "https://files.pythonhosted.org/packages/83/bf/9baed89785ba743ef329c2b07fd0611d12bfecbedbdd3eeecf929d8d3b52/contourpy-1.3.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cdd22595308f53ef2f891040ab2b93d79192513ffccbd7fe19be7aa773a5e09f", size = 1306641 }, + { url = "https://files.pythonhosted.org/packages/d4/cc/74e5e83d1e35de2d28bd97033426b450bc4fd96e092a1f7a63dc7369b55d/contourpy-1.3.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b4f54d6a2defe9f257327b0f243612dd051cc43825587520b1bf74a31e2f6ef2", size = 1374075 }, + { url = "https://files.pythonhosted.org/packages/0c/42/17f3b798fd5e033b46a16f8d9fcb39f1aba051307f5ebf441bad1ecf78f8/contourpy-1.3.2-cp310-cp310-win32.whl", hash = "sha256:f939a054192ddc596e031e50bb13b657ce318cf13d264f095ce9db7dc6ae81c0", size = 177534 }, + { url = "https://files.pythonhosted.org/packages/54/ec/5162b8582f2c994721018d0c9ece9dc6ff769d298a8ac6b6a652c307e7df/contourpy-1.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:c440093bbc8fc21c637c03bafcbef95ccd963bc6e0514ad887932c18ca2a759a", size = 221188 }, + { url = "https://files.pythonhosted.org/packages/b3/b9/ede788a0b56fc5b071639d06c33cb893f68b1178938f3425debebe2dab78/contourpy-1.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6a37a2fb93d4df3fc4c0e363ea4d16f83195fc09c891bc8ce072b9d084853445", size = 269636 }, + { url = "https://files.pythonhosted.org/packages/e6/75/3469f011d64b8bbfa04f709bfc23e1dd71be54d05b1b083be9f5b22750d1/contourpy-1.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b7cd50c38f500bbcc9b6a46643a40e0913673f869315d8e70de0438817cb7773", size = 254636 }, + { url = "https://files.pythonhosted.org/packages/8d/2f/95adb8dae08ce0ebca4fd8e7ad653159565d9739128b2d5977806656fcd2/contourpy-1.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6658ccc7251a4433eebd89ed2672c2ed96fba367fd25ca9512aa92a4b46c4f1", size = 313053 }, + { url = "https://files.pythonhosted.org/packages/c3/a6/8ccf97a50f31adfa36917707fe39c9a0cbc24b3bbb58185577f119736cc9/contourpy-1.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:70771a461aaeb335df14deb6c97439973d253ae70660ca085eec25241137ef43", size = 352985 }, + { url = "https://files.pythonhosted.org/packages/1d/b6/7925ab9b77386143f39d9c3243fdd101621b4532eb126743201160ffa7e6/contourpy-1.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65a887a6e8c4cd0897507d814b14c54a8c2e2aa4ac9f7686292f9769fcf9a6ab", size = 323750 }, + { url = "https://files.pythonhosted.org/packages/c2/f3/20c5d1ef4f4748e52d60771b8560cf00b69d5c6368b5c2e9311bcfa2a08b/contourpy-1.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3859783aefa2b8355697f16642695a5b9792e7a46ab86da1118a4a23a51a33d7", size = 326246 }, + { url = "https://files.pythonhosted.org/packages/8c/e5/9dae809e7e0b2d9d70c52b3d24cba134dd3dad979eb3e5e71f5df22ed1f5/contourpy-1.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:eab0f6db315fa4d70f1d8ab514e527f0366ec021ff853d7ed6a2d33605cf4b83", size = 1308728 }, + { url = "https://files.pythonhosted.org/packages/e2/4a/0058ba34aeea35c0b442ae61a4f4d4ca84d6df8f91309bc2d43bb8dd248f/contourpy-1.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d91a3ccc7fea94ca0acab82ceb77f396d50a1f67412efe4c526f5d20264e6ecd", size = 1375762 }, + { url = "https://files.pythonhosted.org/packages/09/33/7174bdfc8b7767ef2c08ed81244762d93d5c579336fc0b51ca57b33d1b80/contourpy-1.3.2-cp311-cp311-win32.whl", hash = "sha256:1c48188778d4d2f3d48e4643fb15d8608b1d01e4b4d6b0548d9b336c28fc9b6f", size = 178196 }, + { url = "https://files.pythonhosted.org/packages/5e/fe/4029038b4e1c4485cef18e480b0e2cd2d755448bb071eb9977caac80b77b/contourpy-1.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:5ebac872ba09cb8f2131c46b8739a7ff71de28a24c869bcad554477eb089a878", size = 222017 }, + { url = "https://files.pythonhosted.org/packages/34/f7/44785876384eff370c251d58fd65f6ad7f39adce4a093c934d4a67a7c6b6/contourpy-1.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4caf2bcd2969402bf77edc4cb6034c7dd7c0803213b3523f111eb7460a51b8d2", size = 271580 }, + { url = "https://files.pythonhosted.org/packages/93/3b/0004767622a9826ea3d95f0e9d98cd8729015768075d61f9fea8eeca42a8/contourpy-1.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:82199cb78276249796419fe36b7386bd8d2cc3f28b3bc19fe2454fe2e26c4c15", size = 255530 }, + { url = "https://files.pythonhosted.org/packages/e7/bb/7bd49e1f4fa805772d9fd130e0d375554ebc771ed7172f48dfcd4ca61549/contourpy-1.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:106fab697af11456fcba3e352ad50effe493a90f893fca6c2ca5c033820cea92", size = 307688 }, + { url = "https://files.pythonhosted.org/packages/fc/97/e1d5dbbfa170725ef78357a9a0edc996b09ae4af170927ba8ce977e60a5f/contourpy-1.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d14f12932a8d620e307f715857107b1d1845cc44fdb5da2bc8e850f5ceba9f87", size = 347331 }, + { url = "https://files.pythonhosted.org/packages/6f/66/e69e6e904f5ecf6901be3dd16e7e54d41b6ec6ae3405a535286d4418ffb4/contourpy-1.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:532fd26e715560721bb0d5fc7610fce279b3699b018600ab999d1be895b09415", size = 318963 }, + { url = "https://files.pythonhosted.org/packages/a8/32/b8a1c8965e4f72482ff2d1ac2cd670ce0b542f203c8e1d34e7c3e6925da7/contourpy-1.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f26b383144cf2d2c29f01a1e8170f50dacf0eac02d64139dcd709a8ac4eb3cfe", size = 323681 }, + { url = "https://files.pythonhosted.org/packages/30/c6/12a7e6811d08757c7162a541ca4c5c6a34c0f4e98ef2b338791093518e40/contourpy-1.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c49f73e61f1f774650a55d221803b101d966ca0c5a2d6d5e4320ec3997489441", size = 1308674 }, + { url = "https://files.pythonhosted.org/packages/2a/8a/bebe5a3f68b484d3a2b8ffaf84704b3e343ef1addea528132ef148e22b3b/contourpy-1.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3d80b2c0300583228ac98d0a927a1ba6a2ba6b8a742463c564f1d419ee5b211e", size = 1380480 }, + { url = "https://files.pythonhosted.org/packages/34/db/fcd325f19b5978fb509a7d55e06d99f5f856294c1991097534360b307cf1/contourpy-1.3.2-cp312-cp312-win32.whl", hash = "sha256:90df94c89a91b7362e1142cbee7568f86514412ab8a2c0d0fca72d7e91b62912", size = 178489 }, + { url = "https://files.pythonhosted.org/packages/01/c8/fadd0b92ffa7b5eb5949bf340a63a4a496a6930a6c37a7ba0f12acb076d6/contourpy-1.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:8c942a01d9163e2e5cfb05cb66110121b8d07ad438a17f9e766317bcb62abf73", size = 223042 }, + { url = "https://files.pythonhosted.org/packages/2e/61/5673f7e364b31e4e7ef6f61a4b5121c5f170f941895912f773d95270f3a2/contourpy-1.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:de39db2604ae755316cb5967728f4bea92685884b1e767b7c24e983ef5f771cb", size = 271630 }, + { url = "https://files.pythonhosted.org/packages/ff/66/a40badddd1223822c95798c55292844b7e871e50f6bfd9f158cb25e0bd39/contourpy-1.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3f9e896f447c5c8618f1edb2bafa9a4030f22a575ec418ad70611450720b5b08", size = 255670 }, + { url = "https://files.pythonhosted.org/packages/1e/c7/cf9fdee8200805c9bc3b148f49cb9482a4e3ea2719e772602a425c9b09f8/contourpy-1.3.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71e2bd4a1c4188f5c2b8d274da78faab884b59df20df63c34f74aa1813c4427c", size = 306694 }, + { url = "https://files.pythonhosted.org/packages/dd/e7/ccb9bec80e1ba121efbffad7f38021021cda5be87532ec16fd96533bb2e0/contourpy-1.3.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de425af81b6cea33101ae95ece1f696af39446db9682a0b56daaa48cfc29f38f", size = 345986 }, + { url = "https://files.pythonhosted.org/packages/dc/49/ca13bb2da90391fa4219fdb23b078d6065ada886658ac7818e5441448b78/contourpy-1.3.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:977e98a0e0480d3fe292246417239d2d45435904afd6d7332d8455981c408b85", size = 318060 }, + { url = "https://files.pythonhosted.org/packages/c8/65/5245ce8c548a8422236c13ffcdcdada6a2a812c361e9e0c70548bb40b661/contourpy-1.3.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:434f0adf84911c924519d2b08fc10491dd282b20bdd3fa8f60fd816ea0b48841", size = 322747 }, + { url = "https://files.pythonhosted.org/packages/72/30/669b8eb48e0a01c660ead3752a25b44fdb2e5ebc13a55782f639170772f9/contourpy-1.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c66c4906cdbc50e9cba65978823e6e00b45682eb09adbb78c9775b74eb222422", size = 1308895 }, + { url = "https://files.pythonhosted.org/packages/05/5a/b569f4250decee6e8d54498be7bdf29021a4c256e77fe8138c8319ef8eb3/contourpy-1.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8b7fc0cd78ba2f4695fd0a6ad81a19e7e3ab825c31b577f384aa9d7817dc3bef", size = 1379098 }, + { url = "https://files.pythonhosted.org/packages/19/ba/b227c3886d120e60e41b28740ac3617b2f2b971b9f601c835661194579f1/contourpy-1.3.2-cp313-cp313-win32.whl", hash = "sha256:15ce6ab60957ca74cff444fe66d9045c1fd3e92c8936894ebd1f3eef2fff075f", size = 178535 }, + { url = "https://files.pythonhosted.org/packages/12/6e/2fed56cd47ca739b43e892707ae9a13790a486a3173be063681ca67d2262/contourpy-1.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:e1578f7eafce927b168752ed7e22646dad6cd9bca673c60bff55889fa236ebf9", size = 223096 }, + { url = "https://files.pythonhosted.org/packages/54/4c/e76fe2a03014a7c767d79ea35c86a747e9325537a8b7627e0e5b3ba266b4/contourpy-1.3.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0475b1f6604896bc7c53bb070e355e9321e1bc0d381735421a2d2068ec56531f", size = 285090 }, + { url = "https://files.pythonhosted.org/packages/7b/e2/5aba47debd55d668e00baf9651b721e7733975dc9fc27264a62b0dd26eb8/contourpy-1.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c85bb486e9be652314bb5b9e2e3b0d1b2e643d5eec4992c0fbe8ac71775da739", size = 268643 }, + { url = "https://files.pythonhosted.org/packages/a1/37/cd45f1f051fe6230f751cc5cdd2728bb3a203f5619510ef11e732109593c/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:745b57db7758f3ffc05a10254edd3182a2a83402a89c00957a8e8a22f5582823", size = 310443 }, + { url = "https://files.pythonhosted.org/packages/8b/a2/36ea6140c306c9ff6dd38e3bcec80b3b018474ef4d17eb68ceecd26675f4/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:970e9173dbd7eba9b4e01aab19215a48ee5dd3f43cef736eebde064a171f89a5", size = 349865 }, + { url = "https://files.pythonhosted.org/packages/95/b7/2fc76bc539693180488f7b6cc518da7acbbb9e3b931fd9280504128bf956/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6c4639a9c22230276b7bffb6a850dfc8258a2521305e1faefe804d006b2e532", size = 321162 }, + { url = "https://files.pythonhosted.org/packages/f4/10/76d4f778458b0aa83f96e59d65ece72a060bacb20cfbee46cf6cd5ceba41/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc829960f34ba36aad4302e78eabf3ef16a3a100863f0d4eeddf30e8a485a03b", size = 327355 }, + { url = "https://files.pythonhosted.org/packages/43/a3/10cf483ea683f9f8ab096c24bad3cce20e0d1dd9a4baa0e2093c1c962d9d/contourpy-1.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d32530b534e986374fc19eaa77fcb87e8a99e5431499949b828312bdcd20ac52", size = 1307935 }, + { url = "https://files.pythonhosted.org/packages/78/73/69dd9a024444489e22d86108e7b913f3528f56cfc312b5c5727a44188471/contourpy-1.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e298e7e70cf4eb179cc1077be1c725b5fd131ebc81181bf0c03525c8abc297fd", size = 1372168 }, + { url = "https://files.pythonhosted.org/packages/0f/1b/96d586ccf1b1a9d2004dd519b25fbf104a11589abfd05484ff12199cca21/contourpy-1.3.2-cp313-cp313t-win32.whl", hash = "sha256:d0e589ae0d55204991450bb5c23f571c64fe43adaa53f93fc902a84c96f52fe1", size = 189550 }, + { url = "https://files.pythonhosted.org/packages/b0/e6/6000d0094e8a5e32ad62591c8609e269febb6e4db83a1c75ff8868b42731/contourpy-1.3.2-cp313-cp313t-win_amd64.whl", hash = "sha256:78e9253c3de756b3f6a5174d024c4835acd59eb3f8e2ca13e775dbffe1558f69", size = 238214 }, + { url = "https://files.pythonhosted.org/packages/33/05/b26e3c6ecc05f349ee0013f0bb850a761016d89cec528a98193a48c34033/contourpy-1.3.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:fd93cc7f3139b6dd7aab2f26a90dde0aa9fc264dbf70f6740d498a70b860b82c", size = 265681 }, + { url = "https://files.pythonhosted.org/packages/2b/25/ac07d6ad12affa7d1ffed11b77417d0a6308170f44ff20fa1d5aa6333f03/contourpy-1.3.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:107ba8a6a7eec58bb475329e6d3b95deba9440667c4d62b9b6063942b61d7f16", size = 315101 }, + { url = "https://files.pythonhosted.org/packages/8f/4d/5bb3192bbe9d3f27e3061a6a8e7733c9120e203cb8515767d30973f71030/contourpy-1.3.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ded1706ed0c1049224531b81128efbd5084598f18d8a2d9efae833edbd2b40ad", size = 220599 }, + { url = "https://files.pythonhosted.org/packages/ff/c0/91f1215d0d9f9f343e4773ba6c9b89e8c0cc7a64a6263f21139da639d848/contourpy-1.3.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5f5964cdad279256c084b69c3f412b7801e15356b16efa9d78aa974041903da0", size = 266807 }, + { url = "https://files.pythonhosted.org/packages/d4/79/6be7e90c955c0487e7712660d6cead01fa17bff98e0ea275737cc2bc8e71/contourpy-1.3.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49b65a95d642d4efa8f64ba12558fcb83407e58a2dfba9d796d77b63ccfcaff5", size = 318729 }, + { url = "https://files.pythonhosted.org/packages/87/68/7f46fb537958e87427d98a4074bcde4b67a70b04900cfc5ce29bc2f556c1/contourpy-1.3.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:8c5acb8dddb0752bf252e01a3035b21443158910ac16a3b0d20e7fed7d534ce5", size = 221791 }, +] + +[[package]] +name = "contourpy" +version = "1.3.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and platform_python_implementation == 'PyPy' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and platform_python_implementation == 'PyPy' and sys_platform == 'linux'", + "(python_full_version >= '3.13' and platform_machine != 'aarch64' and platform_python_implementation == 'PyPy' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_python_implementation == 'PyPy' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version >= '3.13' and platform_python_implementation != 'PyPy' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and platform_python_implementation != 'PyPy' and sys_platform == 'linux'", + "(python_full_version >= '3.13' and platform_machine != 'aarch64' and platform_python_implementation != 'PyPy' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_python_implementation != 'PyPy' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version >= '3.12.4' and python_full_version < '3.13' and platform_python_implementation == 'PyPy' and sys_platform == 'darwin'", + "python_full_version >= '3.12.4' and python_full_version < '3.13' and platform_machine == 'aarch64' and platform_python_implementation == 'PyPy' and sys_platform == 'linux'", + "(python_full_version >= '3.12.4' and python_full_version < '3.13' and platform_machine != 'aarch64' and platform_python_implementation == 'PyPy' and sys_platform == 'linux') or (python_full_version >= '3.12.4' and python_full_version < '3.13' and platform_python_implementation == 'PyPy' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version >= '3.12.4' and python_full_version < '3.13' and platform_python_implementation != 'PyPy' and sys_platform == 'darwin'", + "python_full_version >= '3.12.4' and python_full_version < '3.13' and platform_machine == 'aarch64' and platform_python_implementation != 'PyPy' and sys_platform == 'linux'", + "(python_full_version >= '3.12.4' and python_full_version < '3.13' and platform_machine != 'aarch64' and platform_python_implementation != 'PyPy' and sys_platform == 'linux') or (python_full_version >= '3.12.4' and python_full_version < '3.13' and platform_python_implementation != 'PyPy' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version >= '3.12' and python_full_version < '3.12.4' and platform_python_implementation == 'PyPy' and sys_platform == 'darwin'", + "python_full_version >= '3.12' and python_full_version < '3.12.4' and platform_machine == 'aarch64' and platform_python_implementation == 'PyPy' and sys_platform == 'linux'", + "(python_full_version >= '3.12' and python_full_version < '3.12.4' and platform_machine != 'aarch64' and platform_python_implementation == 'PyPy' and sys_platform == 'linux') or (python_full_version >= '3.12' and python_full_version < '3.12.4' and platform_python_implementation == 'PyPy' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version >= '3.12' and python_full_version < '3.12.4' and platform_python_implementation != 'PyPy' and sys_platform == 'darwin'", + "python_full_version >= '3.12' and python_full_version < '3.12.4' and platform_machine == 'aarch64' and platform_python_implementation != 'PyPy' and sys_platform == 'linux'", + "(python_full_version >= '3.12' and python_full_version < '3.12.4' and platform_machine != 'aarch64' and platform_python_implementation != 'PyPy' and sys_platform == 'linux') or (python_full_version >= '3.12' and python_full_version < '3.12.4' and platform_python_implementation != 'PyPy' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version == '3.11.*' and platform_python_implementation == 'PyPy' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and platform_python_implementation == 'PyPy' and sys_platform == 'linux'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and platform_python_implementation == 'PyPy' and sys_platform == 'linux') or (python_full_version == '3.11.*' and platform_python_implementation == 'PyPy' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version == '3.11.*' and platform_python_implementation != 'PyPy' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and platform_python_implementation != 'PyPy' and sys_platform == 'linux'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and platform_python_implementation != 'PyPy' and sys_platform == 'linux') or (python_full_version == '3.11.*' and platform_python_implementation != 'PyPy' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "numpy", version = "2.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/2e/c4390a31919d8a78b90e8ecf87cd4b4c4f05a5b48d05ec17db8e5404c6f4/contourpy-1.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:709a48ef9a690e1343202916450bc48b9e51c049b089c7f79a267b46cffcdaa1", size = 288773 }, + { url = "https://files.pythonhosted.org/packages/0d/44/c4b0b6095fef4dc9c420e041799591e3b63e9619e3044f7f4f6c21c0ab24/contourpy-1.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:23416f38bfd74d5d28ab8429cc4d63fa67d5068bd711a85edb1c3fb0c3e2f381", size = 270149 }, + { url = "https://files.pythonhosted.org/packages/30/2e/dd4ced42fefac8470661d7cb7e264808425e6c5d56d175291e93890cce09/contourpy-1.3.3-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:929ddf8c4c7f348e4c0a5a3a714b5c8542ffaa8c22954862a46ca1813b667ee7", size = 329222 }, + { url = "https://files.pythonhosted.org/packages/f2/74/cc6ec2548e3d276c71389ea4802a774b7aa3558223b7bade3f25787fafc2/contourpy-1.3.3-cp311-cp311-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9e999574eddae35f1312c2b4b717b7885d4edd6cb46700e04f7f02db454e67c1", size = 377234 }, + { url = "https://files.pythonhosted.org/packages/03/b3/64ef723029f917410f75c09da54254c5f9ea90ef89b143ccadb09df14c15/contourpy-1.3.3-cp311-cp311-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf67e0e3f482cb69779dd3061b534eb35ac9b17f163d851e2a547d56dba0a3a", size = 380555 }, + { url = "https://files.pythonhosted.org/packages/5f/4b/6157f24ca425b89fe2eb7e7be642375711ab671135be21e6faa100f7448c/contourpy-1.3.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51e79c1f7470158e838808d4a996fa9bac72c498e93d8ebe5119bc1e6becb0db", size = 355238 }, + { url = "https://files.pythonhosted.org/packages/98/56/f914f0dd678480708a04cfd2206e7c382533249bc5001eb9f58aa693e200/contourpy-1.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:598c3aaece21c503615fd59c92a3598b428b2f01bfb4b8ca9c4edeecc2438620", size = 1326218 }, + { url = "https://files.pythonhosted.org/packages/fb/d7/4a972334a0c971acd5172389671113ae82aa7527073980c38d5868ff1161/contourpy-1.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:322ab1c99b008dad206d406bb61d014cf0174df491ae9d9d0fac6a6fda4f977f", size = 1392867 }, + { url = "https://files.pythonhosted.org/packages/75/3e/f2cc6cd56dc8cff46b1a56232eabc6feea52720083ea71ab15523daab796/contourpy-1.3.3-cp311-cp311-win32.whl", hash = "sha256:fd907ae12cd483cd83e414b12941c632a969171bf90fc937d0c9f268a31cafff", size = 183677 }, + { url = "https://files.pythonhosted.org/packages/98/4b/9bd370b004b5c9d8045c6c33cf65bae018b27aca550a3f657cdc99acdbd8/contourpy-1.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:3519428f6be58431c56581f1694ba8e50626f2dd550af225f82fb5f5814d2a42", size = 225234 }, + { url = "https://files.pythonhosted.org/packages/d9/b6/71771e02c2e004450c12b1120a5f488cad2e4d5b590b1af8bad060360fe4/contourpy-1.3.3-cp311-cp311-win_arm64.whl", hash = "sha256:15ff10bfada4bf92ec8b31c62bf7c1834c244019b4a33095a68000d7075df470", size = 193123 }, + { url = "https://files.pythonhosted.org/packages/be/45/adfee365d9ea3d853550b2e735f9d66366701c65db7855cd07621732ccfc/contourpy-1.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b08a32ea2f8e42cf1d4be3169a98dd4be32bafe4f22b6c4cb4ba810fa9e5d2cb", size = 293419 }, + { url = "https://files.pythonhosted.org/packages/53/3e/405b59cfa13021a56bba395a6b3aca8cec012b45bf177b0eaf7a202cde2c/contourpy-1.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:556dba8fb6f5d8742f2923fe9457dbdd51e1049c4a43fd3986a0b14a1d815fc6", size = 273979 }, + { url = "https://files.pythonhosted.org/packages/d4/1c/a12359b9b2ca3a845e8f7f9ac08bdf776114eb931392fcad91743e2ea17b/contourpy-1.3.3-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92d9abc807cf7d0e047b95ca5d957cf4792fcd04e920ca70d48add15c1a90ea7", size = 332653 }, + { url = "https://files.pythonhosted.org/packages/63/12/897aeebfb475b7748ea67b61e045accdfcf0d971f8a588b67108ed7f5512/contourpy-1.3.3-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2e8faa0ed68cb29af51edd8e24798bb661eac3bd9f65420c1887b6ca89987c8", size = 379536 }, + { url = "https://files.pythonhosted.org/packages/43/8a/a8c584b82deb248930ce069e71576fc09bd7174bbd35183b7943fb1064fd/contourpy-1.3.3-cp312-cp312-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:626d60935cf668e70a5ce6ff184fd713e9683fb458898e4249b63be9e28286ea", size = 384397 }, + { url = "https://files.pythonhosted.org/packages/cc/8f/ec6289987824b29529d0dfda0d74a07cec60e54b9c92f3c9da4c0ac732de/contourpy-1.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d00e655fcef08aba35ec9610536bfe90267d7ab5ba944f7032549c55a146da1", size = 362601 }, + { url = "https://files.pythonhosted.org/packages/05/0a/a3fe3be3ee2dceb3e615ebb4df97ae6f3828aa915d3e10549ce016302bd1/contourpy-1.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:451e71b5a7d597379ef572de31eeb909a87246974d960049a9848c3bc6c41bf7", size = 1331288 }, + { url = "https://files.pythonhosted.org/packages/33/1d/acad9bd4e97f13f3e2b18a3977fe1b4a37ecf3d38d815333980c6c72e963/contourpy-1.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:459c1f020cd59fcfe6650180678a9993932d80d44ccde1fa1868977438f0b411", size = 1403386 }, + { url = "https://files.pythonhosted.org/packages/cf/8f/5847f44a7fddf859704217a99a23a4f6417b10e5ab1256a179264561540e/contourpy-1.3.3-cp312-cp312-win32.whl", hash = "sha256:023b44101dfe49d7d53932be418477dba359649246075c996866106da069af69", size = 185018 }, + { url = "https://files.pythonhosted.org/packages/19/e8/6026ed58a64563186a9ee3f29f41261fd1828f527dd93d33b60feca63352/contourpy-1.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:8153b8bfc11e1e4d75bcb0bff1db232f9e10b274e0929de9d608027e0d34ff8b", size = 226567 }, + { url = "https://files.pythonhosted.org/packages/d1/e2/f05240d2c39a1ed228d8328a78b6f44cd695f7ef47beb3e684cf93604f86/contourpy-1.3.3-cp312-cp312-win_arm64.whl", hash = "sha256:07ce5ed73ecdc4a03ffe3e1b3e3c1166db35ae7584be76f65dbbe28a7791b0cc", size = 193655 }, + { url = "https://files.pythonhosted.org/packages/68/35/0167aad910bbdb9599272bd96d01a9ec6852f36b9455cf2ca67bd4cc2d23/contourpy-1.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:177fb367556747a686509d6fef71d221a4b198a3905fe824430e5ea0fda54eb5", size = 293257 }, + { url = "https://files.pythonhosted.org/packages/96/e4/7adcd9c8362745b2210728f209bfbcf7d91ba868a2c5f40d8b58f54c509b/contourpy-1.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d002b6f00d73d69333dac9d0b8d5e84d9724ff9ef044fd63c5986e62b7c9e1b1", size = 274034 }, + { url = "https://files.pythonhosted.org/packages/73/23/90e31ceeed1de63058a02cb04b12f2de4b40e3bef5e082a7c18d9c8ae281/contourpy-1.3.3-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:348ac1f5d4f1d66d3322420f01d42e43122f43616e0f194fc1c9f5d830c5b286", size = 334672 }, + { url = "https://files.pythonhosted.org/packages/ed/93/b43d8acbe67392e659e1d984700e79eb67e2acb2bd7f62012b583a7f1b55/contourpy-1.3.3-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:655456777ff65c2c548b7c454af9c6f33f16c8884f11083244b5819cc214f1b5", size = 381234 }, + { url = "https://files.pythonhosted.org/packages/46/3b/bec82a3ea06f66711520f75a40c8fc0b113b2a75edb36aa633eb11c4f50f/contourpy-1.3.3-cp313-cp313-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:644a6853d15b2512d67881586bd03f462c7ab755db95f16f14d7e238f2852c67", size = 385169 }, + { url = "https://files.pythonhosted.org/packages/4b/32/e0f13a1c5b0f8572d0ec6ae2f6c677b7991fafd95da523159c19eff0696a/contourpy-1.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4debd64f124ca62069f313a9cb86656ff087786016d76927ae2cf37846b006c9", size = 362859 }, + { url = "https://files.pythonhosted.org/packages/33/71/e2a7945b7de4e58af42d708a219f3b2f4cff7386e6b6ab0a0fa0033c49a9/contourpy-1.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a15459b0f4615b00bbd1e91f1b9e19b7e63aea7483d03d804186f278c0af2659", size = 1332062 }, + { url = "https://files.pythonhosted.org/packages/12/fc/4e87ac754220ccc0e807284f88e943d6d43b43843614f0a8afa469801db0/contourpy-1.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ca0fdcd73925568ca027e0b17ab07aad764be4706d0a925b89227e447d9737b7", size = 1403932 }, + { url = "https://files.pythonhosted.org/packages/a6/2e/adc197a37443f934594112222ac1aa7dc9a98faf9c3842884df9a9d8751d/contourpy-1.3.3-cp313-cp313-win32.whl", hash = "sha256:b20c7c9a3bf701366556e1b1984ed2d0cedf999903c51311417cf5f591d8c78d", size = 185024 }, + { url = "https://files.pythonhosted.org/packages/18/0b/0098c214843213759692cc638fce7de5c289200a830e5035d1791d7a2338/contourpy-1.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:1cadd8b8969f060ba45ed7c1b714fe69185812ab43bd6b86a9123fe8f99c3263", size = 226578 }, + { url = "https://files.pythonhosted.org/packages/8a/9a/2f6024a0c5995243cd63afdeb3651c984f0d2bc727fd98066d40e141ad73/contourpy-1.3.3-cp313-cp313-win_arm64.whl", hash = "sha256:fd914713266421b7536de2bfa8181aa8c699432b6763a0ea64195ebe28bff6a9", size = 193524 }, + { url = "https://files.pythonhosted.org/packages/c0/b3/f8a1a86bd3298513f500e5b1f5fd92b69896449f6cab6a146a5d52715479/contourpy-1.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:88df9880d507169449d434c293467418b9f6cbe82edd19284aa0409e7fdb933d", size = 306730 }, + { url = "https://files.pythonhosted.org/packages/3f/11/4780db94ae62fc0c2053909b65dc3246bd7cecfc4f8a20d957ad43aa4ad8/contourpy-1.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d06bb1f751ba5d417047db62bca3c8fde202b8c11fb50742ab3ab962c81e8216", size = 287897 }, + { url = "https://files.pythonhosted.org/packages/ae/15/e59f5f3ffdd6f3d4daa3e47114c53daabcb18574a26c21f03dc9e4e42ff0/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e4e6b05a45525357e382909a4c1600444e2a45b4795163d3b22669285591c1ae", size = 326751 }, + { url = "https://files.pythonhosted.org/packages/0f/81/03b45cfad088e4770b1dcf72ea78d3802d04200009fb364d18a493857210/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ab3074b48c4e2cf1a960e6bbeb7f04566bf36b1861d5c9d4d8ac04b82e38ba20", size = 375486 }, + { url = "https://files.pythonhosted.org/packages/0c/ba/49923366492ffbdd4486e970d421b289a670ae8cf539c1ea9a09822b371a/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c3d53c796f8647d6deb1abe867daeb66dcc8a97e8455efa729516b997b8ed99", size = 388106 }, + { url = "https://files.pythonhosted.org/packages/9f/52/5b00ea89525f8f143651f9f03a0df371d3cbd2fccd21ca9b768c7a6500c2/contourpy-1.3.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50ed930df7289ff2a8d7afeb9603f8289e5704755c7e5c3bbd929c90c817164b", size = 352548 }, + { url = "https://files.pythonhosted.org/packages/32/1d/a209ec1a3a3452d490f6b14dd92e72280c99ae3d1e73da74f8277d4ee08f/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4feffb6537d64b84877da813a5c30f1422ea5739566abf0bd18065ac040e120a", size = 1322297 }, + { url = "https://files.pythonhosted.org/packages/bc/9e/46f0e8ebdd884ca0e8877e46a3f4e633f6c9c8c4f3f6e72be3fe075994aa/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2b7e9480ffe2b0cd2e787e4df64270e3a0440d9db8dc823312e2c940c167df7e", size = 1391023 }, + { url = "https://files.pythonhosted.org/packages/b9/70/f308384a3ae9cd2209e0849f33c913f658d3326900d0ff5d378d6a1422d2/contourpy-1.3.3-cp313-cp313t-win32.whl", hash = "sha256:283edd842a01e3dcd435b1c5116798d661378d83d36d337b8dde1d16a5fc9ba3", size = 196157 }, + { url = "https://files.pythonhosted.org/packages/b2/dd/880f890a6663b84d9e34a6f88cded89d78f0091e0045a284427cb6b18521/contourpy-1.3.3-cp313-cp313t-win_amd64.whl", hash = "sha256:87acf5963fc2b34825e5b6b048f40e3635dd547f590b04d2ab317c2619ef7ae8", size = 240570 }, + { url = "https://files.pythonhosted.org/packages/80/99/2adc7d8ffead633234817ef8e9a87115c8a11927a94478f6bb3d3f4d4f7d/contourpy-1.3.3-cp313-cp313t-win_arm64.whl", hash = "sha256:3c30273eb2a55024ff31ba7d052dde990d7d8e5450f4bbb6e913558b3d6c2301", size = 199713 }, + { url = "https://files.pythonhosted.org/packages/a5/29/8dcfe16f0107943fa92388c23f6e05cff0ba58058c4c95b00280d4c75a14/contourpy-1.3.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:cd5dfcaeb10f7b7f9dc8941717c6c2ade08f587be2226222c12b25f0483ed497", size = 278809 }, + { url = "https://files.pythonhosted.org/packages/85/a9/8b37ef4f7dafeb335daee3c8254645ef5725be4d9c6aa70b50ec46ef2f7e/contourpy-1.3.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:0c1fc238306b35f246d61a1d416a627348b5cf0648648a031e14bb8705fcdfe8", size = 261593 }, + { url = "https://files.pythonhosted.org/packages/0a/59/ebfb8c677c75605cc27f7122c90313fd2f375ff3c8d19a1694bda74aaa63/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70f9aad7de812d6541d29d2bbf8feb22ff7e1c299523db288004e3157ff4674e", size = 302202 }, + { url = "https://files.pythonhosted.org/packages/3c/37/21972a15834d90bfbfb009b9d004779bd5a07a0ec0234e5ba8f64d5736f4/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ed3657edf08512fc3fe81b510e35c2012fbd3081d2e26160f27ca28affec989", size = 329207 }, + { url = "https://files.pythonhosted.org/packages/0c/58/bd257695f39d05594ca4ad60df5bcb7e32247f9951fd09a9b8edb82d1daa/contourpy-1.3.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:3d1a3799d62d45c18bafd41c5fa05120b96a28079f2393af559b843d1a966a77", size = 225315 }, +] + +[[package]] +name = "couchbase" +version = "4.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/fb/41552fceef4ee5e4f06bd05fe571560d150c78923083722988e441b8bfa3/couchbase-4.4.0.tar.gz", hash = "sha256:5234dfa0a500ec1dd9b89318b8ca6303f587cc2d2b4772341f937f1473bbaa96", size = 6557625 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/37/77/2569fc3f189ebee3c6b969f80031449975e424d4e826f9e046c1cfae3af0/couchbase-4.4.0-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:b287bf9a6780545d0c0f68b27f2957d17d7672a2f11b8c27a210fb70538e061a", size = 5031554 }, + { url = "https://files.pythonhosted.org/packages/46/4f/91698faa4fde2d404e4c873a01af99562c7f100e418b41e66d80a71db4e9/couchbase-4.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a16813b6329ca0c8e4aad68e801bf8c8ef3c60383dfb7db88a8d9e193a8d0924", size = 4238842 }, + { url = "https://files.pythonhosted.org/packages/41/97/f58b5d7458932b3709fab532558d80129b5fc5754cc40377655398a32195/couchbase-4.4.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3c4327e9c2ac4f968aef0d63ebfdf4fec667163b0560d340b3b50b27a44186cf", size = 5045392 }, + { url = "https://files.pythonhosted.org/packages/4c/d5/64e2252cedb5ca9697ba785390fde3454bda62f4bff67fc0e684ef02af18/couchbase-4.4.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:82cde7cca3039d84c30242ce739042afcc6e713e9dbe6e29f2cedb8fd09ff29b", size = 5285374 }, + { url = "https://files.pythonhosted.org/packages/60/07/f6422c563f1540d17949253dfbaaf4815dc99c0f5911b73c915186233c51/couchbase-4.4.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d4314fe9bfad09002c8359fcb11811b152760ec812db689104154e329ed0f19d", size = 5962498 }, + { url = "https://files.pythonhosted.org/packages/55/e7/a4a8ab32d3eb2422b546c9fe1fd66757ace4652e7b27d0dd77ba071fc83b/couchbase-4.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:5a099e46584240b9c73c47e33928333a7ec2d60ad5286cffb211290ac74407c1", size = 4210628 }, + { url = "https://files.pythonhosted.org/packages/05/60/05875d771c19abde06cac8158c9db30d164fab2a0f1488c6a5d7b12daee8/couchbase-4.4.0-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:8b068c5e0fe25f51fb5e4d7411a8d0df5d571f858e5a1df25f8ef6dda52acf78", size = 5031545 }, + { url = "https://files.pythonhosted.org/packages/bf/9d/1dd1ae6278c07ade8b89d598d25b63f4131261744c571111b237ec2b6b01/couchbase-4.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ad5d45d74473951b1c7c7a580279dec390750d760dfd9f2b709fc51a88fc7644", size = 4238839 }, + { url = "https://files.pythonhosted.org/packages/d8/0e/09269d1af3d8d6c0694c03fac05ec60997a52ab2169ffc6f14d1fbbea3d4/couchbase-4.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5782f50d8612b04a5f03875003a50344b47df5357a046db044ee04d0d8bdf66f", size = 5045527 }, + { url = "https://files.pythonhosted.org/packages/54/19/ed6a88e66bf63bd97a9c7507bccd14df8260cf93327153b6885d7649ef67/couchbase-4.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:95906185b789d98d345210a318eccdb8a1f8f810f90a2d61be0ca210708cfe19", size = 5285528 }, + { url = "https://files.pythonhosted.org/packages/a8/29/5bc1f0a8fac6e8177ab5201d8783e97f65ad5f286a4ddf11396dc728e7b2/couchbase-4.4.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bd9d5130f27621557df7e80c54d3ef98b86d86a58f4daa61939c0b5e584e726", size = 5962533 }, + { url = "https://files.pythonhosted.org/packages/f0/d2/b7048fc510aff91b53a1084bb41a662b4db6d3f84c73eab5a1dc8023f4b6/couchbase-4.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:b867e4071c94193af0e19fe08c23d06d894e2fb7920c1d732681ac63ca15c46a", size = 4210697 }, + { url = "https://files.pythonhosted.org/packages/3d/02/a70d69efb904186b788149986873848eedb902417804e7258291b77c9a69/couchbase-4.4.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:f30555553ef45ac86dbf26e4d52eaf23545e9e0ea88420a6fccfc5a1f05fd035", size = 4939697 }, + { url = "https://files.pythonhosted.org/packages/3b/e0/83736b992a0756ab4345b10b82108137c1769a188333d0a51816679ab182/couchbase-4.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ff4d1a963440e8a3095368b8c9a6a06a6c009ae9bcbea25b4f43b9c0cbecf867", size = 4240692 }, + { url = "https://files.pythonhosted.org/packages/09/41/41f5d2c3dd9f92307d6442898ae87d84c4b8a4b78e5428ead3edd15536ce/couchbase-4.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f5fba44d95092018dc9e716cc2c38591931d4329136729a0d8dd59a709335305", size = 5049397 }, + { url = "https://files.pythonhosted.org/packages/26/36/32a16b5b9f95b4501a957a0463ec0907eebdc2191c1315fb78ce0ed04ecf/couchbase-4.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:40b6896dac903185668953597cebc4c4cf825393da76611d67e1b7173433406b", size = 5290540 }, + { url = "https://files.pythonhosted.org/packages/f6/b3/1a8993bd822e7635d972dabc44825e62029e5772db1f384f3afe1a37a6ad/couchbase-4.4.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2b178cce7e3ea0f97596c4368bdc7a5ed2a491d5cea2dc12be788535a30ddc5a", size = 5959844 }, + { url = "https://files.pythonhosted.org/packages/ec/ab/be7725830331e930267c27c82f478890c85421d90832cb76d0692cfb4926/couchbase-4.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:54d949da4dd8afe30458dc30b2a85a12e6a1bdc5c4d1c97d04907e37db642b67", size = 4213200 }, + { url = "https://files.pythonhosted.org/packages/50/2c/af3a653f4bd8b28e5a641ab5943eb64ed36afa961f10ebc5e03ad522f07f/couchbase-4.4.0-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:40d38e482b8250614e5ae3631d5e7c7738597053598f73ccb2426da1d4cdb196", size = 4939668 }, + { url = "https://files.pythonhosted.org/packages/d3/66/9748ee7c46032e3d09c8db8193d24f338f61a3728087f641913db9003156/couchbase-4.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9e1ae9df770f9248a85148683306fad80126ad5c34cc591b346577a08517ed78", size = 4240619 }, + { url = "https://files.pythonhosted.org/packages/32/f4/3233ca701277862175742e5eb74cc6890caaa658ce5f6a43f49e3efeee28/couchbase-4.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5c0661cd5e95b03525077ce97b40ee9aa0bfc6cf3d1a71ea29fc39636030dbd0", size = 5049336 }, + { url = "https://files.pythonhosted.org/packages/f1/da/3e2366fb6cd1d3a605355c98e7fe39b28db00b59f50634ea0f25ccbe5f2d/couchbase-4.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aadfdd7c920ddcfb0b6491eeadc63dceeda2f74360fea4971c011b2c7b4d18f2", size = 5290569 }, + { url = "https://files.pythonhosted.org/packages/76/79/5f0b13aea1a0513d7f226469a3643a183c28ff6c2849effd42e2e14e2391/couchbase-4.4.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:43884a5cb0ccaadb6d484e0a0d8adc11bdd6b617ca0cd66854d63eaebda3bf73", size = 5959836 }, + { url = "https://files.pythonhosted.org/packages/7d/0e/fdf57ab5cf40edf1bb0dd70d23b8937581610073e90b8176b6bcab8462d5/couchbase-4.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:671bdfc74383f76890744bef12b06721faccc3206c689058a2c5c72eaccfb0bc", size = 4213137 }, +] + +[[package]] +name = "crewai" +version = "0.177.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "appdirs" }, + { name = "blinker" }, + { name = "chromadb" }, + { name = "click" }, + { name = "instructor" }, + { name = "json-repair" }, + { name = "json5" }, + { name = "jsonref" }, + { name = "litellm" }, + { name = "onnxruntime" }, + { name = "openai" }, + { name = "openpyxl" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp-proto-http" }, + { name = "opentelemetry-sdk" }, + { name = "pdfplumber" }, + { name = "portalocker" }, + { name = "pydantic" }, + { name = "pyjwt" }, + { name = "python-dotenv" }, + { name = "pyvis" }, + { name = "regex" }, + { name = "tokenizers" }, + { name = "tomli" }, + { name = "tomli-w" }, + { name = "uv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8a/fa/fb7a7f78e71704da4870d54ff33bfdf4bb92f1fca9b6a20311431d4baa60/crewai-0.177.0.tar.gz", hash = "sha256:cd34f024881afa163894793e51875a45b02a06f82b342785515455a2e48bb2c0", size = 6552439 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cd/65/7f9d2ee674fd517126668f240d02656bbae2c5e307ec2ed5fd9a32f1bb89/crewai-0.177.0-py3-none-any.whl", hash = "sha256:4718335f0c8236ef42740166fe86f9636a9cf628eeea87205d85599ed1b771b4", size = 418731 }, +] + +[[package]] +name = "crewai-tools" +version = "0.71.0" +source = { editable = "." } +dependencies = [ + { name = "chromadb" }, + { name = "click" }, + { name = "crewai" }, + { name = "docker" }, + { name = "lancedb" }, + { name = "openai" }, + { name = "portalocker" }, + { name = "pydantic" }, + { name = "pyright" }, + { name = "pytube" }, + { name = "requests" }, + { name = "stagehand" }, + { name = "tiktoken" }, +] + +[package.optional-dependencies] +apify = [ + { name = "langchain-apify" }, +] +beautifulsoup4 = [ + { name = "beautifulsoup4" }, +] +bedrock = [ + { name = "beautifulsoup4" }, + { name = "bedrock-agentcore" }, + { name = "nest-asyncio" }, + { name = "playwright" }, +] +browserbase = [ + { name = "browserbase" }, +] +composio-core = [ + { name = "composio-core" }, +] +contextual = [ + { name = "contextual-client" }, + { name = "nest-asyncio" }, +] +couchbase = [ + { name = "couchbase" }, +] +databricks-sdk = [ + { name = "databricks-sdk" }, +] +embedchain = [ + { name = "embedchain" }, +] +exa-py = [ + { name = "exa-py" }, +] +firecrawl-py = [ + { name = "firecrawl-py" }, +] +github = [ + { name = "gitpython" }, + { name = "pygithub" }, +] +hyperbrowser = [ + { name = "hyperbrowser" }, +] +linkup-sdk = [ + { name = "linkup-sdk" }, +] +mcp = [ + { name = "mcp" }, + { name = "mcpadapt" }, +] +mongodb = [ + { name = "pymongo" }, +] +multion = [ + { name = "multion" }, +] +oxylabs = [ + { name = "oxylabs" }, +] +patronus = [ + { name = "patronus" }, +] +qdrant-client = [ + { name = "qdrant-client" }, +] +rag = [ + { name = "lxml" }, + { name = "python-docx" }, +] +scrapegraph-py = [ + { name = "scrapegraph-py" }, +] +scrapfly-sdk = [ + { name = "scrapfly-sdk" }, +] +selenium = [ + { name = "selenium" }, +] +serpapi = [ + { name = "serpapi" }, +] +singlestore = [ + { name = "singlestoredb", version = "1.12.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "singlestoredb", version = "1.14.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "singlestoredb", version = "1.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "sqlalchemy" }, +] +snowflake = [ + { name = "cryptography" }, + { name = "snowflake-connector-python" }, + { name = "snowflake-sqlalchemy" }, +] +spider-client = [ + { name = "spider-client" }, +] +sqlalchemy = [ + { name = "sqlalchemy" }, +] +stagehand = [ + { name = "stagehand" }, +] +tavily-python = [ + { name = "tavily-python" }, +] +weaviate-client = [ + { name = "weaviate-client" }, +] +xml = [ + { name = "unstructured", extra = ["all-docs", "local-inference"] }, +] + +[package.dev-dependencies] +dev = [ + { name = "pytest" }, + { name = "pytest-asyncio" }, + { name = "pytest-recording" }, +] + +[package.metadata] +requires-dist = [ + { name = "beautifulsoup4", marker = "extra == 'beautifulsoup4'", specifier = ">=4.12.3" }, + { name = "beautifulsoup4", marker = "extra == 'bedrock'", specifier = ">=4.13.4" }, + { name = "bedrock-agentcore", marker = "extra == 'bedrock'", specifier = ">=0.1.0" }, + { name = "browserbase", marker = "extra == 'browserbase'", specifier = ">=1.0.5" }, + { name = "chromadb", specifier = "==0.5.23" }, + { name = "click", specifier = ">=8.1.8" }, + { name = "composio-core", marker = "extra == 'composio-core'", specifier = ">=0.6.11.post1" }, + { name = "contextual-client", marker = "extra == 'contextual'", specifier = ">=0.1.0" }, + { name = "couchbase", marker = "extra == 'couchbase'", specifier = ">=4.3.5" }, + { name = "crewai", specifier = ">=0.177.0" }, + { name = "cryptography", marker = "extra == 'snowflake'", specifier = ">=43.0.3" }, + { name = "databricks-sdk", marker = "extra == 'databricks-sdk'", specifier = ">=0.46.0" }, + { name = "docker", specifier = ">=7.1.0" }, + { name = "embedchain", marker = "extra == 'embedchain'", specifier = ">=0.1.114" }, + { name = "exa-py", marker = "extra == 'exa-py'", specifier = ">=1.8.7" }, + { name = "firecrawl-py", marker = "extra == 'firecrawl-py'", specifier = ">=1.8.0" }, + { name = "gitpython", marker = "extra == 'github'", specifier = "==3.1.38" }, + { name = "hyperbrowser", marker = "extra == 'hyperbrowser'", specifier = ">=0.18.0" }, + { name = "lancedb", specifier = ">=0.5.4" }, + { name = "langchain-apify", marker = "extra == 'apify'", specifier = ">=0.1.2,<1.0.0" }, + { name = "linkup-sdk", marker = "extra == 'linkup-sdk'", specifier = ">=0.2.2" }, + { name = "lxml", marker = "extra == 'rag'", specifier = ">=5.3.0,<5.4.0" }, + { name = "mcp", marker = "extra == 'mcp'", specifier = ">=1.6.0" }, + { name = "mcpadapt", marker = "extra == 'mcp'", specifier = ">=0.1.9" }, + { name = "multion", marker = "extra == 'multion'", specifier = ">=1.1.0" }, + { name = "nest-asyncio", marker = "extra == 'bedrock'", specifier = ">=1.6.0" }, + { name = "nest-asyncio", marker = "extra == 'contextual'", specifier = ">=1.6.0" }, + { name = "openai", specifier = ">=1.12.0" }, + { name = "oxylabs", marker = "extra == 'oxylabs'", specifier = "==2.0.0" }, + { name = "patronus", marker = "extra == 'patronus'", specifier = ">=0.0.16" }, + { name = "playwright", marker = "extra == 'bedrock'", specifier = ">=1.52.0" }, + { name = "portalocker", specifier = "==2.7.0" }, + { name = "pydantic", specifier = ">=2.6.1" }, + { name = "pygithub", marker = "extra == 'github'", specifier = "==1.59.1" }, + { name = "pymongo", marker = "extra == 'mongodb'", specifier = ">=4.13" }, + { name = "pyright", specifier = ">=1.1.350" }, + { name = "python-docx", marker = "extra == 'rag'", specifier = ">=1.1.0" }, + { name = "pytube", specifier = ">=15.0.0" }, + { name = "qdrant-client", marker = "extra == 'qdrant-client'", specifier = ">=1.12.1" }, + { name = "requests", specifier = ">=2.31.0" }, + { name = "scrapegraph-py", marker = "extra == 'scrapegraph-py'", specifier = ">=1.9.0" }, + { name = "scrapfly-sdk", marker = "extra == 'scrapfly-sdk'", specifier = ">=0.8.19" }, + { name = "selenium", marker = "extra == 'selenium'", specifier = ">=4.27.1" }, + { name = "serpapi", marker = "extra == 'serpapi'", specifier = ">=0.1.5" }, + { name = "singlestoredb", marker = "extra == 'singlestore'", specifier = ">=1.12.4" }, + { name = "snowflake-connector-python", marker = "extra == 'snowflake'", specifier = ">=3.12.4" }, + { name = "snowflake-sqlalchemy", marker = "extra == 'snowflake'", specifier = ">=1.7.3" }, + { name = "spider-client", marker = "extra == 'spider-client'", specifier = ">=0.1.25" }, + { name = "sqlalchemy", marker = "extra == 'singlestore'", specifier = ">=2.0.40" }, + { name = "sqlalchemy", marker = "extra == 'sqlalchemy'", specifier = ">=2.0.35" }, + { name = "stagehand", specifier = ">=0.4.1" }, + { name = "stagehand", marker = "extra == 'stagehand'", specifier = ">=0.4.1" }, + { name = "tavily-python", marker = "extra == 'tavily-python'", specifier = ">=0.5.4" }, + { name = "tiktoken", specifier = ">=0.8.0" }, + { name = "unstructured", extras = ["local-inference", "all-docs"], marker = "extra == 'xml'", specifier = ">=0.17.2" }, + { name = "weaviate-client", marker = "extra == 'weaviate-client'", specifier = ">=4.10.2" }, +] +provides-extras = ["embedchain", "scrapfly-sdk", "sqlalchemy", "multion", "firecrawl-py", "composio-core", "browserbase", "weaviate-client", "patronus", "serpapi", "beautifulsoup4", "selenium", "spider-client", "scrapegraph-py", "linkup-sdk", "tavily-python", "hyperbrowser", "snowflake", "singlestore", "exa-py", "qdrant-client", "apify", "databricks-sdk", "couchbase", "mcp", "stagehand", "github", "rag", "xml", "oxylabs", "mongodb", "bedrock", "contextual"] + +[package.metadata.requires-dev] +dev = [ + { name = "pytest", specifier = ">=8.0.0" }, + { name = "pytest", specifier = ">=8.3.4" }, + { name = "pytest-asyncio", specifier = ">=0.25.2" }, + { name = "pytest-recording", specifier = ">=0.13.3" }, +] + +[[package]] +name = "cryptography" +version = "45.0.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d6/0d/d13399c94234ee8f3df384819dc67e0c5ce215fb751d567a55a1f4b028c7/cryptography-45.0.6.tar.gz", hash = "sha256:5c966c732cf6e4a276ce83b6e4c729edda2df6929083a952cc7da973c539c719", size = 744949 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8c/29/2793d178d0eda1ca4a09a7c4e09a5185e75738cc6d526433e8663b460ea6/cryptography-45.0.6-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:048e7ad9e08cf4c0ab07ff7f36cc3115924e22e2266e034450a890d9e312dd74", size = 7042702 }, + { url = "https://files.pythonhosted.org/packages/b3/b6/cabd07410f222f32c8d55486c464f432808abaa1f12af9afcbe8f2f19030/cryptography-45.0.6-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:44647c5d796f5fc042bbc6d61307d04bf29bccb74d188f18051b635f20a9c75f", size = 4206483 }, + { url = "https://files.pythonhosted.org/packages/8b/9e/f9c7d36a38b1cfeb1cc74849aabe9bf817990f7603ff6eb485e0d70e0b27/cryptography-45.0.6-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e40b80ecf35ec265c452eea0ba94c9587ca763e739b8e559c128d23bff7ebbbf", size = 4429679 }, + { url = "https://files.pythonhosted.org/packages/9c/2a/4434c17eb32ef30b254b9e8b9830cee4e516f08b47fdd291c5b1255b8101/cryptography-45.0.6-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:00e8724bdad672d75e6f069b27970883179bd472cd24a63f6e620ca7e41cc0c5", size = 4210553 }, + { url = "https://files.pythonhosted.org/packages/ef/1d/09a5df8e0c4b7970f5d1f3aff1b640df6d4be28a64cae970d56c6cf1c772/cryptography-45.0.6-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7a3085d1b319d35296176af31c90338eeb2ddac8104661df79f80e1d9787b8b2", size = 3894499 }, + { url = "https://files.pythonhosted.org/packages/79/62/120842ab20d9150a9d3a6bdc07fe2870384e82f5266d41c53b08a3a96b34/cryptography-45.0.6-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1b7fa6a1c1188c7ee32e47590d16a5a0646270921f8020efc9a511648e1b2e08", size = 4458484 }, + { url = "https://files.pythonhosted.org/packages/fd/80/1bc3634d45ddfed0871bfba52cf8f1ad724761662a0c792b97a951fb1b30/cryptography-45.0.6-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:275ba5cc0d9e320cd70f8e7b96d9e59903c815ca579ab96c1e37278d231fc402", size = 4210281 }, + { url = "https://files.pythonhosted.org/packages/7d/fe/ffb12c2d83d0ee625f124880a1f023b5878f79da92e64c37962bbbe35f3f/cryptography-45.0.6-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:f4028f29a9f38a2025abedb2e409973709c660d44319c61762202206ed577c42", size = 4456890 }, + { url = "https://files.pythonhosted.org/packages/8c/8e/b3f3fe0dc82c77a0deb5f493b23311e09193f2268b77196ec0f7a36e3f3e/cryptography-45.0.6-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ee411a1b977f40bd075392c80c10b58025ee5c6b47a822a33c1198598a7a5f05", size = 4333247 }, + { url = "https://files.pythonhosted.org/packages/b3/a6/c3ef2ab9e334da27a1d7b56af4a2417d77e7806b2e0f90d6267ce120d2e4/cryptography-45.0.6-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e2a21a8eda2d86bb604934b6b37691585bd095c1f788530c1fcefc53a82b3453", size = 4565045 }, + { url = "https://files.pythonhosted.org/packages/31/c3/77722446b13fa71dddd820a5faab4ce6db49e7e0bf8312ef4192a3f78e2f/cryptography-45.0.6-cp311-abi3-win32.whl", hash = "sha256:d063341378d7ee9c91f9d23b431a3502fc8bfacd54ef0a27baa72a0843b29159", size = 2928923 }, + { url = "https://files.pythonhosted.org/packages/38/63/a025c3225188a811b82932a4dcc8457a26c3729d81578ccecbcce2cb784e/cryptography-45.0.6-cp311-abi3-win_amd64.whl", hash = "sha256:833dc32dfc1e39b7376a87b9a6a4288a10aae234631268486558920029b086ec", size = 3403805 }, + { url = "https://files.pythonhosted.org/packages/5b/af/bcfbea93a30809f126d51c074ee0fac5bd9d57d068edf56c2a73abedbea4/cryptography-45.0.6-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:3436128a60a5e5490603ab2adbabc8763613f638513ffa7d311c900a8349a2a0", size = 7020111 }, + { url = "https://files.pythonhosted.org/packages/98/c6/ea5173689e014f1a8470899cd5beeb358e22bb3cf5a876060f9d1ca78af4/cryptography-45.0.6-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0d9ef57b6768d9fa58e92f4947cea96ade1233c0e236db22ba44748ffedca394", size = 4198169 }, + { url = "https://files.pythonhosted.org/packages/ba/73/b12995edc0c7e2311ffb57ebd3b351f6b268fed37d93bfc6f9856e01c473/cryptography-45.0.6-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ea3c42f2016a5bbf71825537c2ad753f2870191134933196bee408aac397b3d9", size = 4421273 }, + { url = "https://files.pythonhosted.org/packages/f7/6e/286894f6f71926bc0da67408c853dd9ba953f662dcb70993a59fd499f111/cryptography-45.0.6-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:20ae4906a13716139d6d762ceb3e0e7e110f7955f3bc3876e3a07f5daadec5f3", size = 4199211 }, + { url = "https://files.pythonhosted.org/packages/de/34/a7f55e39b9623c5cb571d77a6a90387fe557908ffc44f6872f26ca8ae270/cryptography-45.0.6-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dac5ec199038b8e131365e2324c03d20e97fe214af051d20c49db129844e8b3", size = 3883732 }, + { url = "https://files.pythonhosted.org/packages/f9/b9/c6d32edbcba0cd9f5df90f29ed46a65c4631c4fbe11187feb9169c6ff506/cryptography-45.0.6-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:18f878a34b90d688982e43f4b700408b478102dd58b3e39de21b5ebf6509c301", size = 4450655 }, + { url = "https://files.pythonhosted.org/packages/77/2d/09b097adfdee0227cfd4c699b3375a842080f065bab9014248933497c3f9/cryptography-45.0.6-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:5bd6020c80c5b2b2242d6c48487d7b85700f5e0038e67b29d706f98440d66eb5", size = 4198956 }, + { url = "https://files.pythonhosted.org/packages/55/66/061ec6689207d54effdff535bbdf85cc380d32dd5377173085812565cf38/cryptography-45.0.6-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:eccddbd986e43014263eda489abbddfbc287af5cddfd690477993dbb31e31016", size = 4449859 }, + { url = "https://files.pythonhosted.org/packages/41/ff/e7d5a2ad2d035e5a2af116e1a3adb4d8fcd0be92a18032917a089c6e5028/cryptography-45.0.6-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:550ae02148206beb722cfe4ef0933f9352bab26b087af00e48fdfb9ade35c5b3", size = 4320254 }, + { url = "https://files.pythonhosted.org/packages/82/27/092d311af22095d288f4db89fcaebadfb2f28944f3d790a4cf51fe5ddaeb/cryptography-45.0.6-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5b64e668fc3528e77efa51ca70fadcd6610e8ab231e3e06ae2bab3b31c2b8ed9", size = 4554815 }, + { url = "https://files.pythonhosted.org/packages/7e/01/aa2f4940262d588a8fdf4edabe4cda45854d00ebc6eaac12568b3a491a16/cryptography-45.0.6-cp37-abi3-win32.whl", hash = "sha256:780c40fb751c7d2b0c6786ceee6b6f871e86e8718a8ff4bc35073ac353c7cd02", size = 2912147 }, + { url = "https://files.pythonhosted.org/packages/0a/bc/16e0276078c2de3ceef6b5a34b965f4436215efac45313df90d55f0ba2d2/cryptography-45.0.6-cp37-abi3-win_amd64.whl", hash = "sha256:20d15aed3ee522faac1a39fbfdfee25d17b1284bafd808e1640a74846d7c4d1b", size = 3390459 }, + { url = "https://files.pythonhosted.org/packages/56/d2/4482d97c948c029be08cb29854a91bd2ae8da7eb9c4152461f1244dcea70/cryptography-45.0.6-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:705bb7c7ecc3d79a50f236adda12ca331c8e7ecfbea51edd931ce5a7a7c4f012", size = 3576812 }, + { url = "https://files.pythonhosted.org/packages/ec/24/55fc238fcaa122855442604b8badb2d442367dfbd5a7ca4bb0bd346e263a/cryptography-45.0.6-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:826b46dae41a1155a0c0e66fafba43d0ede1dc16570b95e40c4d83bfcf0a451d", size = 4141694 }, + { url = "https://files.pythonhosted.org/packages/f9/7e/3ea4fa6fbe51baf3903806a0241c666b04c73d2358a3ecce09ebee8b9622/cryptography-45.0.6-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:cc4d66f5dc4dc37b89cfef1bd5044387f7a1f6f0abb490815628501909332d5d", size = 4375010 }, + { url = "https://files.pythonhosted.org/packages/50/42/ec5a892d82d2a2c29f80fc19ced4ba669bca29f032faf6989609cff1f8dc/cryptography-45.0.6-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:f68f833a9d445cc49f01097d95c83a850795921b3f7cc6488731e69bde3288da", size = 4141377 }, + { url = "https://files.pythonhosted.org/packages/e7/d7/246c4c973a22b9c2931999da953a2c19cae7c66b9154c2d62ffed811225e/cryptography-45.0.6-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:3b5bf5267e98661b9b888a9250d05b063220dfa917a8203744454573c7eb79db", size = 4374609 }, + { url = "https://files.pythonhosted.org/packages/78/6d/c49ccf243f0a1b0781c2a8de8123ee552f0c8a417c6367a24d2ecb7c11b3/cryptography-45.0.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:2384f2ab18d9be88a6e4f8972923405e2dbb8d3e16c6b43f15ca491d7831bd18", size = 3322156 }, + { url = "https://files.pythonhosted.org/packages/61/69/c252de4ec047ba2f567ecb53149410219577d408c2aea9c989acae7eafce/cryptography-45.0.6-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:fc022c1fa5acff6def2fc6d7819bbbd31ccddfe67d075331a65d9cfb28a20983", size = 3584669 }, + { url = "https://files.pythonhosted.org/packages/e3/fe/deea71e9f310a31fe0a6bfee670955152128d309ea2d1c79e2a5ae0f0401/cryptography-45.0.6-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:3de77e4df42ac8d4e4d6cdb342d989803ad37707cf8f3fbf7b088c9cbdd46427", size = 4153022 }, + { url = "https://files.pythonhosted.org/packages/60/45/a77452f5e49cb580feedba6606d66ae7b82c128947aa754533b3d1bd44b0/cryptography-45.0.6-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:599c8d7df950aa68baa7e98f7b73f4f414c9f02d0e8104a30c0182a07732638b", size = 4386802 }, + { url = "https://files.pythonhosted.org/packages/a3/b9/a2f747d2acd5e3075fdf5c145c7c3568895daaa38b3b0c960ef830db6cdc/cryptography-45.0.6-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:31a2b9a10530a1cb04ffd6aa1cd4d3be9ed49f7d77a4dafe198f3b382f41545c", size = 4152706 }, + { url = "https://files.pythonhosted.org/packages/81/ec/381b3e8d0685a3f3f304a382aa3dfce36af2d76467da0fd4bb21ddccc7b2/cryptography-45.0.6-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:e5b3dda1b00fb41da3af4c5ef3f922a200e33ee5ba0f0bc9ecf0b0c173958385", size = 4386740 }, + { url = "https://files.pythonhosted.org/packages/0a/76/cf8d69da8d0b5ecb0db406f24a63a3f69ba5e791a11b782aeeefef27ccbb/cryptography-45.0.6-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:629127cfdcdc6806dfe234734d7cb8ac54edaf572148274fa377a7d3405b0043", size = 3331874 }, +] + +[[package]] +name = "cycler" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/95/a3dbbb5028f35eafb79008e7522a75244477d2838f38cbb722248dabc2a8/cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c", size = 7615 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321 }, +] + +[[package]] +name = "databricks-sdk" +version = "0.61.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-auth" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1f/81/df133b3276b66bbaf1083a3f59184d8c80157a5a3de1d2ea79ddf5f7391b/databricks_sdk-0.61.0.tar.gz", hash = "sha256:06e50663c2c87e94f5e505390b74bc5c7c5330f4b4be35616b7aed06cf940af0", size = 724055 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/70/24e4ed08cb8d5bd7ca947326065b44be875f798100576591b7a6c935a6d6/databricks_sdk-0.61.0-py3-none-any.whl", hash = "sha256:709ac7c709f843567b04fba6cea8a53ee644b79314e9a9ac4db0c1b3c1d2d5fe", size = 680551 }, +] + +[[package]] +name = "dataclasses-json" +version = "0.6.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "marshmallow" }, + { name = "typing-inspect" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/64/a4/f71d9cf3a5ac257c993b5ca3f93df5f7fb395c725e7f1e6479d2514173c3/dataclasses_json-0.6.7.tar.gz", hash = "sha256:b6b3e528266ea45b9535223bc53ca645f5208833c29229e847b3f26a1cc55fc0", size = 32227 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/be/d0d44e092656fe7a06b55e6103cbce807cdbdee17884a5367c68c9860853/dataclasses_json-0.6.7-py3-none-any.whl", hash = "sha256:0dbf33f26c8d5305befd61b39d2b3414e8a407bedc2834dea9b8d642666fb40a", size = 28686 }, +] + +[[package]] +name = "decorator" +version = "5.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/fa/6d96a0978d19e17b68d634497769987b16c8f4cd0a7a05048bec693caa6b/decorator-5.2.1.tar.gz", hash = "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360", size = 56711 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a", size = 9190 }, +] + +[[package]] +name = "deprecated" +version = "1.2.18" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/98/97/06afe62762c9a8a86af0cfb7bfdab22a43ad17138b07af5b1a58442690a2/deprecated-1.2.18.tar.gz", hash = "sha256:422b6f6d859da6f2ef57857761bfb392480502a64c3028ca9bbe86085d72115d", size = 2928744 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6e/c6/ac0b6c1e2d138f1002bcf799d330bd6d85084fece321e662a14223794041/Deprecated-1.2.18-py2.py3-none-any.whl", hash = "sha256:bd5011788200372a32418f888e326a09ff80d0214bd961147cfed01b5c018eec", size = 9998 }, +] + +[[package]] +name = "deprecation" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5a/d3/8ae2869247df154b64c1884d7346d412fed0c49df84db635aab2d1c40e62/deprecation-2.1.0.tar.gz", hash = "sha256:72b3bde64e5d778694b0cf68178aed03d15e15477116add3fb773e581f9518ff", size = 173788 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/c3/253a89ee03fc9b9682f1541728eb66db7db22148cd94f89ab22528cd1e1b/deprecation-2.1.0-py2.py3-none-any.whl", hash = "sha256:a10811591210e1fb0e768a8c25517cabeabcba6f0bf96564f8ff45189f90b14a", size = 11178 }, +] + +[[package]] +name = "diskcache" +version = "5.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3f/21/1c1ffc1a039ddcc459db43cc108658f32c57d271d7289a2794e401d0fdb6/diskcache-5.6.3.tar.gz", hash = "sha256:2c3a3fa2743d8535d832ec61c2054a1641f41775aa7c556758a109941e33e4fc", size = 67916 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/27/4570e78fc0bf5ea0ca45eb1de3818a23787af9b390c0b0a0033a1b8236f9/diskcache-5.6.3-py3-none-any.whl", hash = "sha256:5e31b2d5fbad117cc363ebaf6b689474db18a1f6438bc82358b024abd4c2ca19", size = 45550 }, +] + +[[package]] +name = "distro" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277 }, +] + +[[package]] +name = "dnspython" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/4a/263763cb2ba3816dd94b08ad3a33d5fdae34ecb856678773cc40a3605829/dnspython-2.7.0.tar.gz", hash = "sha256:ce9c432eda0dc91cf618a5cedf1a4e142651196bbcd2c80e89ed5a907e5cfaf1", size = 345197 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/1b/e0a87d256e40e8c888847551b20a017a6b98139178505dc7ffb96f04e954/dnspython-2.7.0-py3-none-any.whl", hash = "sha256:b4c34b7d10b51bcc3a5071e7b8dee77939f1e878477eeecc965e9835f63c6c86", size = 313632 }, +] + +[[package]] +name = "docker" +version = "7.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pywin32", marker = "sys_platform == 'win32'" }, + { name = "requests" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/91/9b/4a2ea29aeba62471211598dac5d96825bb49348fa07e906ea930394a83ce/docker-7.1.0.tar.gz", hash = "sha256:ad8c70e6e3f8926cb8a92619b832b4ea5299e2831c14284663184e200546fa6c", size = 117834 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e3/26/57c6fb270950d476074c087527a558ccb6f4436657314bfb6cdf484114c4/docker-7.1.0-py3-none-any.whl", hash = "sha256:c96b93b7f0a746f9e77d325bcfb87422a3d8bd4f03136ae8a85b37f1898d5fc0", size = 147774 }, +] + +[[package]] +name = "docstring-parser" +version = "0.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/9d/c3b43da9515bd270df0f80548d9944e389870713cc1fe2b8fb35fe2bcefd/docstring_parser-0.17.0.tar.gz", hash = "sha256:583de4a309722b3315439bb31d64ba3eebada841f2e2cee23b99df001434c912", size = 27442 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/55/e2/2537ebcff11c1ee1ff17d8d0b6f4db75873e3b0fb32c2d4a2ee31ecb310a/docstring_parser-0.17.0-py3-none-any.whl", hash = "sha256:cf2569abd23dce8099b300f9b4fa8191e9582dda731fd533daf54c4551658708", size = 36896 }, +] + +[[package]] +name = "durationpy" +version = "0.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/a4/e44218c2b394e31a6dd0d6b095c4e1f32d0be54c2a4b250032d717647bab/durationpy-0.10.tar.gz", hash = "sha256:1fa6893409a6e739c9c72334fc65cca1f355dbdd93405d30f726deb5bde42fba", size = 3335 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b0/0d/9feae160378a3553fa9a339b0e9c1a048e147a4127210e286ef18b730f03/durationpy-0.10-py3-none-any.whl", hash = "sha256:3b41e1b601234296b4fb368338fdcd3e13e0b4fb5b67345948f4f2bf9868b286", size = 3922 }, +] + +[[package]] +name = "effdet" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "omegaconf" }, + { name = "pycocotools" }, + { name = "timm" }, + { name = "torch" }, + { name = "torchvision" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0e/c3/12d45167ec36f7f9a5ed80bc2128392b3f6207f760d437287d32a0e43f41/effdet-0.4.1.tar.gz", hash = "sha256:ac5589fd304a5650c201986b2ef5f8e10c111093a71b1c49fa6b8817710812b5", size = 110134 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/13/563119fe0af82aca5a3b89399c435953072c39515c2e818eb82793955c3b/effdet-0.4.1-py3-none-any.whl", hash = "sha256:10889a226228d515c948e3fcf811e64c0d78d7aa94823a300045653b9c284cb7", size = 112513 }, +] + +[[package]] +name = "embedchain" +version = "0.1.128" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "alembic" }, + { name = "beautifulsoup4" }, + { name = "chromadb" }, + { name = "gptcache" }, + { name = "langchain" }, + { name = "langchain-cohere" }, + { name = "langchain-community" }, + { name = "langchain-openai" }, + { name = "langsmith" }, + { name = "mem0ai" }, + { name = "openai" }, + { name = "posthog" }, + { name = "pypdf" }, + { name = "pysbd" }, + { name = "python-dotenv" }, + { name = "rich" }, + { name = "schema" }, + { name = "sqlalchemy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4f/18/63cdd649ff26dcc532f78ccfc565ee9b25fad2abcf82f04456f5ae7580d9/embedchain-0.1.128.tar.gz", hash = "sha256:641cef036d4c2f4b2b2d26019156647dcaa1b6d98f5d16e3798a12f46499bb8f", size = 118754 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/e2/6806c26fa402e47f2924bac64d11a1af3c426b1d967588067f23325fd417/embedchain-0.1.128-py3-none-any.whl", hash = "sha256:380e848c053a335b06d535efcbfdc6b98a5d0b2a6a1f553aae94cb1c85676183", size = 211343 }, +] + +[[package]] +name = "emoji" +version = "2.14.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cb/7d/01cddcbb6f5cc0ba72e00ddf9b1fa206c802d557fd0a20b18e130edf1336/emoji-2.14.1.tar.gz", hash = "sha256:f8c50043d79a2c1410ebfae833ae1868d5941a67a6cd4d18377e2eb0bd79346b", size = 597182 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/db/a0335710caaa6d0aebdaa65ad4df789c15d89b7babd9a30277838a7d9aac/emoji-2.14.1-py3-none-any.whl", hash = "sha256:35a8a486c1460addb1499e3bf7929d3889b2e2841a57401903699fef595e942b", size = 590617 }, +] + +[[package]] +name = "et-xmlfile" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d3/38/af70d7ab1ae9d4da450eeec1fa3918940a5fafb9055e934af8d6eb0c2313/et_xmlfile-2.0.0.tar.gz", hash = "sha256:dab3f4764309081ce75662649be815c4c9081e88f0837825f90fd28317d4da54", size = 17234 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/8b/5fe2cc11fee489817272089c4203e679c63b570a5aaeb18d852ae3cbba6a/et_xmlfile-2.0.0-py3-none-any.whl", hash = "sha256:7a91720bc756843502c3b7504c77b8fe44217c85c537d85037f0f536151b2caa", size = 18059 }, +] + +[[package]] +name = "eval-type-backport" +version = "0.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/30/ea/8b0ac4469d4c347c6a385ff09dc3c048c2d021696664e26c7ee6791631b5/eval_type_backport-0.2.2.tar.gz", hash = "sha256:f0576b4cf01ebb5bd358d02314d31846af5e07678387486e2c798af0e7d849c1", size = 9079 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/31/55cd413eaccd39125368be33c46de24a1f639f2e12349b0361b4678f3915/eval_type_backport-0.2.2-py3-none-any.whl", hash = "sha256:cb6ad7c393517f476f96d456d0412ea80f0a8cf96f6892834cd9340149111b0a", size = 5830 }, +] + +[[package]] +name = "exa-py" +version = "1.14.20" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, + { name = "openai" }, + { name = "pydantic" }, + { name = "requests" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bd/00/b7efa5458c92ac415a334c9b27b0cfd6f0327ee545bd7f4c8639129a0ee0/exa_py-1.14.20.tar.gz", hash = "sha256:423789a0635b7a4ecd5f56d6b4a0dfb01126fa45ce1e04106c0bb96b7d551ebf", size = 35483 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/29/36/d574fd02741fa7706def78fd81f5fe405a84dca3d3cb94f80f27469d7d38/exa_py-1.14.20-py3-none-any.whl", hash = "sha256:e0ed9d99c3c494a0e6903e11a0f6fb773b3b23d0cd802380cf58efc97d9d332d", size = 45156 }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674 }, +] + +[[package]] +name = "executing" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/91/50/a9d80c47ff289c611ff12e63f7c5d13942c65d68125160cefd768c73e6e4/executing-2.2.0.tar.gz", hash = "sha256:5d108c028108fe2551d1a7b2e8b713341e2cb4fc0aa7dcf966fa4327a5226755", size = 978693 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/8f/c4d9bafc34ad7ad5d8dc16dd1347ee0e507a52c3adb6bfa8887e1c6a26ba/executing-2.2.0-py2.py3-none-any.whl", hash = "sha256:11387150cad388d62750327a53d3339fad4888b39a6fe233c3afbb54ecffd3aa", size = 26702 }, +] + +[[package]] +name = "fastapi" +version = "0.116.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "starlette" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/78/d7/6c8b3bfe33eeffa208183ec037fee0cce9f7f024089ab1c5d12ef04bd27c/fastapi-0.116.1.tar.gz", hash = "sha256:ed52cbf946abfd70c5a0dccb24673f0670deeb517a88b3544d03c2a6bf283143", size = 296485 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/47/d63c60f59a59467fda0f93f46335c9d18526d7071f025cb5b89d5353ea42/fastapi-0.116.1-py3-none-any.whl", hash = "sha256:c46ac7c312df840f0c9e220f7964bada936781bc4e2e6eb71f1c4d7553786565", size = 95631 }, +] + +[[package]] +name = "fastavro" +version = "1.12.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cc/ec/762dcf213e5b97ea1733b27d5a2798599a1fa51565b70a93690246029f84/fastavro-1.12.0.tar.gz", hash = "sha256:a67a87be149825d74006b57e52be068dfa24f3bfc6382543ec92cd72327fe152", size = 1025604 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ae/22/60eff8fb290dc6cea71448b97839e8e8f44d3dcae95366f34deed74f9fc3/fastavro-1.12.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e38497bd24136aad2c47376ee958be4f5b775d6f03c11893fc636eea8c1c3b40", size = 948880 }, + { url = "https://files.pythonhosted.org/packages/30/b1/e0653699d2a085be8b7ddeeff84e9e110ea776555052f99e85a5f9f39bd3/fastavro-1.12.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e8d8401b021f4b3dfc05e6f82365f14de8d170a041fbe3345f992c9c13d4f0ff", size = 3226993 }, + { url = "https://files.pythonhosted.org/packages/7d/0c/9d27972025a54e424e1c449f015251a65b658b23b0a4715e8cf96bd4005a/fastavro-1.12.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:531b89117422db967d4e1547b34089454e942341e50331fa71920e9d5e326330", size = 3240363 }, + { url = "https://files.pythonhosted.org/packages/23/c8/41d0bc7dbd5de93a75b277a4cc378cb84740a083b3b33de5ec51e7a69d5e/fastavro-1.12.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ae541edbc6091b890532d3e50d7bcdd324219730598cf9cb4522d1decabde37e", size = 3165740 }, + { url = "https://files.pythonhosted.org/packages/52/81/b317b33b838dd4db8753349fd3ac4a92f7a2c4217ce55e6db397fff22481/fastavro-1.12.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:585a11f612eaadb0dcb1d3d348b90bd0d0d3ee4cf9abafd8b319663e8a0e1dcc", size = 3245059 }, + { url = "https://files.pythonhosted.org/packages/62/f3/9df53cc1dad3873279246bb9e3996130d8dd2affbc0537a5554a01a28f84/fastavro-1.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:425fb96fbfbc06a0cc828946dd2ae9d85a5f9ff836af033d8cb963876ecb158e", size = 450639 }, + { url = "https://files.pythonhosted.org/packages/6f/51/6bd93f2c9f3bb98f84ee0ddb436eb46a308ec53e884d606b70ca9d6b132d/fastavro-1.12.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:56f78d1d527bea4833945c3a8c716969ebd133c5762e2e34f64c795bd5a10b3e", size = 962215 }, + { url = "https://files.pythonhosted.org/packages/32/37/3e2e429cefe03d1fa98cc4c4edae1d133dc895db64dabe84c17b4dc0921c/fastavro-1.12.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a7ce0d117642bb4265ef6e1619ec2d93e942a98f60636e3c0fbf1eb438c49026", size = 3412716 }, + { url = "https://files.pythonhosted.org/packages/33/28/eb37d9738ea3649bdcab1b6d4fd0facf9c36261623ea368554734d5d6821/fastavro-1.12.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:52e9d9648aad4cca5751bcbe2d3f98e85afb0ec6c6565707f4e2f647ba83ba85", size = 3439283 }, + { url = "https://files.pythonhosted.org/packages/57/6f/7aba4efbf73fd80ca20aa1db560936c222dd1b4e5cadbf9304361b9065e3/fastavro-1.12.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6183875381ec1cf85a1891bf46696fd1ec2ad732980e7bccc1e52e9904e7664d", size = 3354728 }, + { url = "https://files.pythonhosted.org/packages/bf/2d/b0d8539f4622ebf5355b7898ac7930b1ff638de85b6c3acdd0718e05d09e/fastavro-1.12.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5ad00a2b94d3c8bf9239acf92d56e3e457e1d188687a8d80f31e858ccf91a6d6", size = 3442598 }, + { url = "https://files.pythonhosted.org/packages/fe/33/882154b17e0fd468f1a5ae8cc903805531e1fcb699140315366c5f8ec20d/fastavro-1.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:6c4d1c276ff1410f3830648bb43312894ad65709ca0cb54361e28954387a46ac", size = 451836 }, + { url = "https://files.pythonhosted.org/packages/4a/f0/df076a541144d2f351820f3d9e20afa0e4250e6e63cb5a26f94688ed508c/fastavro-1.12.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e849c70198e5bdf6f08df54a68db36ff72bd73e8f14b1fd664323df073c496d8", size = 944288 }, + { url = "https://files.pythonhosted.org/packages/52/1d/5c1ea0f6e98a441953de822c7455c9ce8c3afdc7b359dd23c5a5e5039249/fastavro-1.12.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b260e1cdc9a77853a2586b32208302c08dddfb5c20720b5179ac5330e06ce698", size = 3404895 }, + { url = "https://files.pythonhosted.org/packages/36/8b/115a3ffe67fb48de0de704284fa5e793afa70932b8b2e915cc7545752f05/fastavro-1.12.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:181779688d8b80957953031f0d82ec0761be667a78e03dac642511ff996c771a", size = 3469935 }, + { url = "https://files.pythonhosted.org/packages/14/f8/bf3b7370687ab21205e07b37acdd2455ca69f5d25c72d2b315faf357b1cd/fastavro-1.12.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6881caf914b36a57d1f90810f04a89bd9c837dd4a48e1b66a8b92136e85c415d", size = 3306148 }, + { url = "https://files.pythonhosted.org/packages/97/55/fba2726b59a984c7aa2fc19c6e8ef1865eca6a3f66e78810d602ca22af59/fastavro-1.12.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8bf638248499eb78c422f12fedc08f9b90b5646c3368415e388691db60e7defb", size = 3442851 }, + { url = "https://files.pythonhosted.org/packages/a6/3e/25059b8fe0b8084fd858dca77caf0815d73e0ca4731485f34402e8d40c43/fastavro-1.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:ed4f18b7c2f651a5ee2233676f62aac332995086768301aa2c1741859d70b53e", size = 445449 }, + { url = "https://files.pythonhosted.org/packages/db/c7/f18b73b39860d54eb724f881b8932882ba10c1d4905e491cd25d159a7e49/fastavro-1.12.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:dbe2b690d9caba7d888126cc1dd980a8fcf5ee73de41a104e3f15bb5e08c19c8", size = 936220 }, + { url = "https://files.pythonhosted.org/packages/20/22/61ec800fda2a0f051a21b067e4005fd272070132d0a0566c5094e09b666c/fastavro-1.12.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:07ff9e6c6e8739203ccced3205646fdac6141c2efc83f4dffabf5f7d0176646d", size = 3348450 }, + { url = "https://files.pythonhosted.org/packages/ca/79/1f34618fb643b99e08853e8a204441ec11a24d3e1fce050e804e6ff5c5ae/fastavro-1.12.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6a172655add31882cab4e1a96b7d49f419906b465b4c2165081db7b1db79852f", size = 3417238 }, + { url = "https://files.pythonhosted.org/packages/ea/0b/79611769eb15cc17992dc3699141feb0f75afd37b0cb964b4a08be45214e/fastavro-1.12.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:be20ce0331b70b35dca1a4c7808afeedf348dc517bd41602ed8fc9a1ac2247a9", size = 3252425 }, + { url = "https://files.pythonhosted.org/packages/86/1a/65e0999bcc4bbb38df32706b6ae6ce626d528228667a5e0af059a8b25bb2/fastavro-1.12.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a52906681384a18b99b47e5f9eab64b4744d6e6bc91056b7e28641c7b3c59d2b", size = 3385322 }, + { url = "https://files.pythonhosted.org/packages/e9/49/c06ebc9e5144f7463c2bfcb900ca01f87db934caf131bccbffc5d0aaf7ec/fastavro-1.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:cf153531191bcfc445c21e05dd97232a634463aa717cf99fb2214a51b9886bff", size = 445586 }, + { url = "https://files.pythonhosted.org/packages/dd/c8/46ab37076dc0f86bb255791baf9b3c3a20f77603a86a40687edacff8c03d/fastavro-1.12.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:1928e88a760688e490118e1bedf0643b1f3727e5ba59c07ac64638dab81ae2a1", size = 1025933 }, + { url = "https://files.pythonhosted.org/packages/a9/7f/cb3e069dcc903034a6fe82182d92c75d981d86aee94bd028200a083696b3/fastavro-1.12.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cd51b706a3ab3fe4af84a0b37f60d1bcd79295df18932494fc9f49db4ba2bab2", size = 3560435 }, + { url = "https://files.pythonhosted.org/packages/d0/12/9478c28a2ac4fcc10ad9488dd3dcd5fac1ef550c3022c57840330e7cec4b/fastavro-1.12.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1148263931f6965e1942cf670f146148ca95b021ae7b7e1f98bf179f1c26cc58", size = 3453000 }, + { url = "https://files.pythonhosted.org/packages/00/32/a5c8b3af9561c308c8c27da0be998b6237a47dbbdd8d5499f02731bd4073/fastavro-1.12.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4099e0f6fb8a55f59891c0aed6bfa90c4d20a774737e5282c74181b4703ea0cb", size = 3383233 }, + { url = "https://files.pythonhosted.org/packages/42/a0/f6290f3f8059543faf3ef30efbbe9bf3e4389df881891136cd5fb1066b64/fastavro-1.12.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:10c586e9e3bab34307f8e3227a2988b6e8ac49bff8f7b56635cf4928a153f464", size = 3402032 }, +] + +[[package]] +name = "filelock" +version = "3.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0a/10/c23352565a6544bdc5353e0b15fc1c563352101f30e24bf500207a54df9a/filelock-3.18.0.tar.gz", hash = "sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2", size = 18075 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/36/2a115987e2d8c300a974597416d9de88f2444426de9571f4b59b2cca3acc/filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de", size = 16215 }, +] + +[[package]] +name = "filetype" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bb/29/745f7d30d47fe0f251d3ad3dc2978a23141917661998763bebb6da007eb1/filetype-1.2.0.tar.gz", hash = "sha256:66b56cd6474bf41d8c54660347d37afcc3f7d1970648de365c102ef77548aadb", size = 998020 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/79/1b8fa1bb3568781e84c9200f951c735f3f157429f44be0495da55894d620/filetype-1.2.0-py2.py3-none-any.whl", hash = "sha256:7ce71b6880181241cf7ac8697a2f1eb6a8bd9b429f7ad6d27b8db9ba5f1c2d25", size = 19970 }, +] + +[[package]] +name = "firecrawl-py" +version = "2.16.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, + { name = "nest-asyncio" }, + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "requests" }, + { name = "websockets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d3/42/d3745b0385a7a239d6f7e4622474328d259a4ccb262a356e06ce31785e90/firecrawl_py-2.16.3.tar.gz", hash = "sha256:5fd063ef4acc4c4be62648f1e11467336bc127780b3afc28d39078a012e6a14c", size = 39916 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/1c/7a0dbb920b52f38dd84c3f1aab814cecc4e41e535726b284e77942e2e1d8/firecrawl_py-2.16.3-py3-none-any.whl", hash = "sha256:94bb46af5e0df6c8ec414ac999a5355c0f5a46f15fd1cf5a02a3b31062db0aa8", size = 75606 }, +] + +[[package]] +name = "flatbuffers" +version = "25.2.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/30/eb5dce7994fc71a2f685d98ec33cc660c0a5887db5610137e60d8cbc4489/flatbuffers-25.2.10.tar.gz", hash = "sha256:97e451377a41262f8d9bd4295cc836133415cc03d8cb966410a4af92eb00d26e", size = 22170 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b8/25/155f9f080d5e4bc0082edfda032ea2bc2b8fab3f4d25d46c1e9dd22a1a89/flatbuffers-25.2.10-py2.py3-none-any.whl", hash = "sha256:ebba5f4d5ea615af3f7fd70fc310636fbb2bbd1f566ac0a23d98dd412de50051", size = 30953 }, +] + +[[package]] +name = "fonttools" +version = "4.59.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8a/27/ec3c723bfdf86f34c5c82bf6305df3e0f0d8ea798d2d3a7cb0c0a866d286/fonttools-4.59.0.tar.gz", hash = "sha256:be392ec3529e2f57faa28709d60723a763904f71a2b63aabe14fee6648fe3b14", size = 3532521 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1c/1f/3dcae710b7c4b56e79442b03db64f6c9f10c3348f7af40339dffcefb581e/fonttools-4.59.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:524133c1be38445c5c0575eacea42dbd44374b310b1ffc4b60ff01d881fabb96", size = 2761846 }, + { url = "https://files.pythonhosted.org/packages/eb/0e/ae3a1884fa1549acac1191cc9ec039142f6ac0e9cbc139c2e6a3dab967da/fonttools-4.59.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:21e606b2d38fed938dde871c5736822dd6bda7a4631b92e509a1f5cd1b90c5df", size = 2332060 }, + { url = "https://files.pythonhosted.org/packages/75/46/58bff92a7216829159ac7bdb1d05a48ad1b8ab8c539555f12d29fdecfdd4/fonttools-4.59.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e93df708c69a193fc7987192f94df250f83f3851fda49413f02ba5dded639482", size = 4852354 }, + { url = "https://files.pythonhosted.org/packages/05/57/767e31e48861045d89691128bd81fd4c62b62150f9a17a666f731ce4f197/fonttools-4.59.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:62224a9bb85b4b66d1b46d45cbe43d71cbf8f527d332b177e3b96191ffbc1e64", size = 4781132 }, + { url = "https://files.pythonhosted.org/packages/d7/78/adb5e9b0af5c6ce469e8b0e112f144eaa84b30dd72a486e9c778a9b03b31/fonttools-4.59.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b8974b2a266b54c96709bd5e239979cddfd2dbceed331aa567ea1d7c4a2202db", size = 4832901 }, + { url = "https://files.pythonhosted.org/packages/ac/92/bc3881097fbf3d56d112bec308c863c058e5d4c9c65f534e8ae58450ab8a/fonttools-4.59.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:209b75943d158f610b78320eacb5539aa9e920bee2c775445b2846c65d20e19d", size = 4940140 }, + { url = "https://files.pythonhosted.org/packages/4a/54/39cdb23f0eeda2e07ae9cb189f2b6f41da89aabc682d3a387b3ff4a4ed29/fonttools-4.59.0-cp310-cp310-win32.whl", hash = "sha256:4c908a7036f0f3677f8afa577bcd973e3e20ddd2f7c42a33208d18bee95cdb6f", size = 2215890 }, + { url = "https://files.pythonhosted.org/packages/d8/eb/f8388d9e19f95d8df2449febe9b1a38ddd758cfdb7d6de3a05198d785d61/fonttools-4.59.0-cp310-cp310-win_amd64.whl", hash = "sha256:8b4309a2775e4feee7356e63b163969a215d663399cce1b3d3b65e7ec2d9680e", size = 2260191 }, + { url = "https://files.pythonhosted.org/packages/06/96/520733d9602fa1bf6592e5354c6721ac6fc9ea72bc98d112d0c38b967199/fonttools-4.59.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:841b2186adce48903c0fef235421ae21549020eca942c1da773ac380b056ab3c", size = 2782387 }, + { url = "https://files.pythonhosted.org/packages/87/6a/170fce30b9bce69077d8eec9bea2cfd9f7995e8911c71be905e2eba6368b/fonttools-4.59.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9bcc1e77fbd1609198966ded6b2a9897bd6c6bcbd2287a2fc7d75f1a254179c5", size = 2342194 }, + { url = "https://files.pythonhosted.org/packages/b0/b6/7c8166c0066856f1408092f7968ac744060cf72ca53aec9036106f57eeca/fonttools-4.59.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:37c377f7cb2ab2eca8a0b319c68146d34a339792f9420fca6cd49cf28d370705", size = 5032333 }, + { url = "https://files.pythonhosted.org/packages/eb/0c/707c5a19598eafcafd489b73c4cb1c142102d6197e872f531512d084aa76/fonttools-4.59.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fa39475eaccb98f9199eccfda4298abaf35ae0caec676ffc25b3a5e224044464", size = 4974422 }, + { url = "https://files.pythonhosted.org/packages/f6/e7/6d33737d9fe632a0f59289b6f9743a86d2a9d0673de2a0c38c0f54729822/fonttools-4.59.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d3972b13148c1d1fbc092b27678a33b3080d1ac0ca305742b0119b75f9e87e38", size = 5010631 }, + { url = "https://files.pythonhosted.org/packages/63/e1/a4c3d089ab034a578820c8f2dff21ef60daf9668034a1e4fb38bb1cc3398/fonttools-4.59.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a408c3c51358c89b29cfa5317cf11518b7ce5de1717abb55c5ae2d2921027de6", size = 5122198 }, + { url = "https://files.pythonhosted.org/packages/09/77/ca82b9c12fa4de3c520b7760ee61787640cf3fde55ef1b0bfe1de38c8153/fonttools-4.59.0-cp311-cp311-win32.whl", hash = "sha256:6770d7da00f358183d8fd5c4615436189e4f683bdb6affb02cad3d221d7bb757", size = 2214216 }, + { url = "https://files.pythonhosted.org/packages/ab/25/5aa7ca24b560b2f00f260acf32c4cf29d7aaf8656e159a336111c18bc345/fonttools-4.59.0-cp311-cp311-win_amd64.whl", hash = "sha256:84fc186980231a287b28560d3123bd255d3c6b6659828c642b4cf961e2b923d0", size = 2261879 }, + { url = "https://files.pythonhosted.org/packages/e2/77/b1c8af22f4265e951cd2e5535dbef8859efcef4fb8dee742d368c967cddb/fonttools-4.59.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:f9b3a78f69dcbd803cf2fb3f972779875b244c1115481dfbdd567b2c22b31f6b", size = 2767562 }, + { url = "https://files.pythonhosted.org/packages/ff/5a/aeb975699588176bb357e8b398dfd27e5d3a2230d92b81ab8cbb6187358d/fonttools-4.59.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:57bb7e26928573ee7c6504f54c05860d867fd35e675769f3ce01b52af38d48e2", size = 2335168 }, + { url = "https://files.pythonhosted.org/packages/54/97/c6101a7e60ae138c4ef75b22434373a0da50a707dad523dd19a4889315bf/fonttools-4.59.0-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4536f2695fe5c1ffb528d84a35a7d3967e5558d2af58b4775e7ab1449d65767b", size = 4909850 }, + { url = "https://files.pythonhosted.org/packages/bd/6c/fa4d18d641054f7bff878cbea14aa9433f292b9057cb1700d8e91a4d5f4f/fonttools-4.59.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:885bde7d26e5b40e15c47bd5def48b38cbd50830a65f98122a8fb90962af7cd1", size = 4955131 }, + { url = "https://files.pythonhosted.org/packages/20/5c/331947fc1377deb928a69bde49f9003364f5115e5cbe351eea99e39412a2/fonttools-4.59.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6801aeddb6acb2c42eafa45bc1cb98ba236871ae6f33f31e984670b749a8e58e", size = 4899667 }, + { url = "https://files.pythonhosted.org/packages/8a/46/b66469dfa26b8ff0baa7654b2cc7851206c6d57fe3abdabbaab22079a119/fonttools-4.59.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:31003b6a10f70742a63126b80863ab48175fb8272a18ca0846c0482968f0588e", size = 5051349 }, + { url = "https://files.pythonhosted.org/packages/2e/05/ebfb6b1f3a4328ab69787d106a7d92ccde77ce66e98659df0f9e3f28d93d/fonttools-4.59.0-cp312-cp312-win32.whl", hash = "sha256:fbce6dae41b692a5973d0f2158f782b9ad05babc2c2019a970a1094a23909b1b", size = 2201315 }, + { url = "https://files.pythonhosted.org/packages/09/45/d2bdc9ea20bbadec1016fd0db45696d573d7a26d95ab5174ffcb6d74340b/fonttools-4.59.0-cp312-cp312-win_amd64.whl", hash = "sha256:332bfe685d1ac58ca8d62b8d6c71c2e52a6c64bc218dc8f7825c9ea51385aa01", size = 2249408 }, + { url = "https://files.pythonhosted.org/packages/f3/bb/390990e7c457d377b00890d9f96a3ca13ae2517efafb6609c1756e213ba4/fonttools-4.59.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:78813b49d749e1bb4db1c57f2d4d7e6db22c253cb0a86ad819f5dc197710d4b2", size = 2758704 }, + { url = "https://files.pythonhosted.org/packages/df/6f/d730d9fcc9b410a11597092bd2eb9ca53e5438c6cb90e4b3047ce1b723e9/fonttools-4.59.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:401b1941ce37e78b8fd119b419b617277c65ae9417742a63282257434fd68ea2", size = 2330764 }, + { url = "https://files.pythonhosted.org/packages/75/b4/b96bb66f6f8cc4669de44a158099b249c8159231d254ab6b092909388be5/fonttools-4.59.0-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:efd7e6660674e234e29937bc1481dceb7e0336bfae75b856b4fb272b5093c5d4", size = 4890699 }, + { url = "https://files.pythonhosted.org/packages/b5/57/7969af50b26408be12baa317c6147588db5b38af2759e6df94554dbc5fdb/fonttools-4.59.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51ab1ff33c19e336c02dee1e9fd1abd974a4ca3d8f7eef2a104d0816a241ce97", size = 4952934 }, + { url = "https://files.pythonhosted.org/packages/d6/e2/dd968053b6cf1f46c904f5bd409b22341477c017d8201619a265e50762d3/fonttools-4.59.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a9bf8adc9e1f3012edc8f09b08336272aec0c55bc677422273e21280db748f7c", size = 4892319 }, + { url = "https://files.pythonhosted.org/packages/6b/95/a59810d8eda09129f83467a4e58f84205dc6994ebaeb9815406363e07250/fonttools-4.59.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:37e01c6ec0c98599778c2e688350d624fa4770fbd6144551bd5e032f1199171c", size = 5034753 }, + { url = "https://files.pythonhosted.org/packages/a5/84/51a69ee89ff8d1fea0c6997e946657e25a3f08513de8435fe124929f3eef/fonttools-4.59.0-cp313-cp313-win32.whl", hash = "sha256:70d6b3ceaa9cc5a6ac52884f3b3d9544e8e231e95b23f138bdb78e6d4dc0eae3", size = 2199688 }, + { url = "https://files.pythonhosted.org/packages/a0/ee/f626cd372932d828508137a79b85167fdcf3adab2e3bed433f295c596c6a/fonttools-4.59.0-cp313-cp313-win_amd64.whl", hash = "sha256:26731739daa23b872643f0e4072d5939960237d540c35c14e6a06d47d71ca8fe", size = 2248560 }, + { url = "https://files.pythonhosted.org/packages/d0/9c/df0ef2c51845a13043e5088f7bb988ca6cd5bb82d5d4203d6a158aa58cf2/fonttools-4.59.0-py3-none-any.whl", hash = "sha256:241313683afd3baacb32a6bd124d0bce7404bc5280e12e291bae1b9bba28711d", size = 1128050 }, +] + +[[package]] +name = "frozenlist" +version = "1.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/79/b1/b64018016eeb087db503b038296fd782586432b9c077fc5c7839e9cb6ef6/frozenlist-1.7.0.tar.gz", hash = "sha256:2e310d81923c2437ea8670467121cc3e9b0f76d3043cc1d2331d56c7fb7a3a8f", size = 45078 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/af/36/0da0a49409f6b47cc2d060dc8c9040b897b5902a8a4e37d9bc1deb11f680/frozenlist-1.7.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cc4df77d638aa2ed703b878dd093725b72a824c3c546c076e8fdf276f78ee84a", size = 81304 }, + { url = "https://files.pythonhosted.org/packages/77/f0/77c11d13d39513b298e267b22eb6cb559c103d56f155aa9a49097221f0b6/frozenlist-1.7.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:716a9973a2cc963160394f701964fe25012600f3d311f60c790400b00e568b61", size = 47735 }, + { url = "https://files.pythonhosted.org/packages/37/12/9d07fa18971a44150593de56b2f2947c46604819976784bcf6ea0d5db43b/frozenlist-1.7.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a0fd1bad056a3600047fb9462cff4c5322cebc59ebf5d0a3725e0ee78955001d", size = 46775 }, + { url = "https://files.pythonhosted.org/packages/70/34/f73539227e06288fcd1f8a76853e755b2b48bca6747e99e283111c18bcd4/frozenlist-1.7.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3789ebc19cb811163e70fe2bd354cea097254ce6e707ae42e56f45e31e96cb8e", size = 224644 }, + { url = "https://files.pythonhosted.org/packages/fb/68/c1d9c2f4a6e438e14613bad0f2973567586610cc22dcb1e1241da71de9d3/frozenlist-1.7.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:af369aa35ee34f132fcfad5be45fbfcde0e3a5f6a1ec0712857f286b7d20cca9", size = 222125 }, + { url = "https://files.pythonhosted.org/packages/b9/d0/98e8f9a515228d708344d7c6986752be3e3192d1795f748c24bcf154ad99/frozenlist-1.7.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac64b6478722eeb7a3313d494f8342ef3478dff539d17002f849101b212ef97c", size = 233455 }, + { url = "https://files.pythonhosted.org/packages/79/df/8a11bcec5600557f40338407d3e5bea80376ed1c01a6c0910fcfdc4b8993/frozenlist-1.7.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f89f65d85774f1797239693cef07ad4c97fdd0639544bad9ac4b869782eb1981", size = 227339 }, + { url = "https://files.pythonhosted.org/packages/50/82/41cb97d9c9a5ff94438c63cc343eb7980dac4187eb625a51bdfdb7707314/frozenlist-1.7.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1073557c941395fdfcfac13eb2456cb8aad89f9de27bae29fabca8e563b12615", size = 212969 }, + { url = "https://files.pythonhosted.org/packages/13/47/f9179ee5ee4f55629e4f28c660b3fdf2775c8bfde8f9c53f2de2d93f52a9/frozenlist-1.7.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ed8d2fa095aae4bdc7fdd80351009a48d286635edffee66bf865e37a9125c50", size = 222862 }, + { url = "https://files.pythonhosted.org/packages/1a/52/df81e41ec6b953902c8b7e3a83bee48b195cb0e5ec2eabae5d8330c78038/frozenlist-1.7.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:24c34bea555fe42d9f928ba0a740c553088500377448febecaa82cc3e88aa1fa", size = 222492 }, + { url = "https://files.pythonhosted.org/packages/84/17/30d6ea87fa95a9408245a948604b82c1a4b8b3e153cea596421a2aef2754/frozenlist-1.7.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:69cac419ac6a6baad202c85aaf467b65ac860ac2e7f2ac1686dc40dbb52f6577", size = 238250 }, + { url = "https://files.pythonhosted.org/packages/8f/00/ecbeb51669e3c3df76cf2ddd66ae3e48345ec213a55e3887d216eb4fbab3/frozenlist-1.7.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:960d67d0611f4c87da7e2ae2eacf7ea81a5be967861e0c63cf205215afbfac59", size = 218720 }, + { url = "https://files.pythonhosted.org/packages/1a/c0/c224ce0e0eb31cc57f67742071bb470ba8246623c1823a7530be0e76164c/frozenlist-1.7.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:41be2964bd4b15bf575e5daee5a5ce7ed3115320fb3c2b71fca05582ffa4dc9e", size = 232585 }, + { url = "https://files.pythonhosted.org/packages/55/3c/34cb694abf532f31f365106deebdeac9e45c19304d83cf7d51ebbb4ca4d1/frozenlist-1.7.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:46d84d49e00c9429238a7ce02dc0be8f6d7cd0cd405abd1bebdc991bf27c15bd", size = 234248 }, + { url = "https://files.pythonhosted.org/packages/98/c0/2052d8b6cecda2e70bd81299e3512fa332abb6dcd2969b9c80dfcdddbf75/frozenlist-1.7.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:15900082e886edb37480335d9d518cec978afc69ccbc30bd18610b7c1b22a718", size = 221621 }, + { url = "https://files.pythonhosted.org/packages/c5/bf/7dcebae315436903b1d98ffb791a09d674c88480c158aa171958a3ac07f0/frozenlist-1.7.0-cp310-cp310-win32.whl", hash = "sha256:400ddd24ab4e55014bba442d917203c73b2846391dd42ca5e38ff52bb18c3c5e", size = 39578 }, + { url = "https://files.pythonhosted.org/packages/8f/5f/f69818f017fa9a3d24d1ae39763e29b7f60a59e46d5f91b9c6b21622f4cd/frozenlist-1.7.0-cp310-cp310-win_amd64.whl", hash = "sha256:6eb93efb8101ef39d32d50bce242c84bcbddb4f7e9febfa7b524532a239b4464", size = 43830 }, + { url = "https://files.pythonhosted.org/packages/34/7e/803dde33760128acd393a27eb002f2020ddb8d99d30a44bfbaab31c5f08a/frozenlist-1.7.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:aa51e147a66b2d74de1e6e2cf5921890de6b0f4820b257465101d7f37b49fb5a", size = 82251 }, + { url = "https://files.pythonhosted.org/packages/75/a9/9c2c5760b6ba45eae11334db454c189d43d34a4c0b489feb2175e5e64277/frozenlist-1.7.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9b35db7ce1cd71d36ba24f80f0c9e7cff73a28d7a74e91fe83e23d27c7828750", size = 48183 }, + { url = "https://files.pythonhosted.org/packages/47/be/4038e2d869f8a2da165f35a6befb9158c259819be22eeaf9c9a8f6a87771/frozenlist-1.7.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:34a69a85e34ff37791e94542065c8416c1afbf820b68f720452f636d5fb990cd", size = 47107 }, + { url = "https://files.pythonhosted.org/packages/79/26/85314b8a83187c76a37183ceed886381a5f992975786f883472fcb6dc5f2/frozenlist-1.7.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a646531fa8d82c87fe4bb2e596f23173caec9185bfbca5d583b4ccfb95183e2", size = 237333 }, + { url = "https://files.pythonhosted.org/packages/1f/fd/e5b64f7d2c92a41639ffb2ad44a6a82f347787abc0c7df5f49057cf11770/frozenlist-1.7.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:79b2ffbba483f4ed36a0f236ccb85fbb16e670c9238313709638167670ba235f", size = 231724 }, + { url = "https://files.pythonhosted.org/packages/20/fb/03395c0a43a5976af4bf7534759d214405fbbb4c114683f434dfdd3128ef/frozenlist-1.7.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a26f205c9ca5829cbf82bb2a84b5c36f7184c4316617d7ef1b271a56720d6b30", size = 245842 }, + { url = "https://files.pythonhosted.org/packages/d0/15/c01c8e1dffdac5d9803507d824f27aed2ba76b6ed0026fab4d9866e82f1f/frozenlist-1.7.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bcacfad3185a623fa11ea0e0634aac7b691aa925d50a440f39b458e41c561d98", size = 239767 }, + { url = "https://files.pythonhosted.org/packages/14/99/3f4c6fe882c1f5514b6848aa0a69b20cb5e5d8e8f51a339d48c0e9305ed0/frozenlist-1.7.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:72c1b0fe8fe451b34f12dce46445ddf14bd2a5bcad7e324987194dc8e3a74c86", size = 224130 }, + { url = "https://files.pythonhosted.org/packages/4d/83/220a374bd7b2aeba9d0725130665afe11de347d95c3620b9b82cc2fcab97/frozenlist-1.7.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61d1a5baeaac6c0798ff6edfaeaa00e0e412d49946c53fae8d4b8e8b3566c4ae", size = 235301 }, + { url = "https://files.pythonhosted.org/packages/03/3c/3e3390d75334a063181625343e8daab61b77e1b8214802cc4e8a1bb678fc/frozenlist-1.7.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7edf5c043c062462f09b6820de9854bf28cc6cc5b6714b383149745e287181a8", size = 234606 }, + { url = "https://files.pythonhosted.org/packages/23/1e/58232c19608b7a549d72d9903005e2d82488f12554a32de2d5fb59b9b1ba/frozenlist-1.7.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:d50ac7627b3a1bd2dcef6f9da89a772694ec04d9a61b66cf87f7d9446b4a0c31", size = 248372 }, + { url = "https://files.pythonhosted.org/packages/c0/a4/e4a567e01702a88a74ce8a324691e62a629bf47d4f8607f24bf1c7216e7f/frozenlist-1.7.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ce48b2fece5aeb45265bb7a58259f45027db0abff478e3077e12b05b17fb9da7", size = 229860 }, + { url = "https://files.pythonhosted.org/packages/73/a6/63b3374f7d22268b41a9db73d68a8233afa30ed164c46107b33c4d18ecdd/frozenlist-1.7.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:fe2365ae915a1fafd982c146754e1de6ab3478def8a59c86e1f7242d794f97d5", size = 245893 }, + { url = "https://files.pythonhosted.org/packages/6d/eb/d18b3f6e64799a79673c4ba0b45e4cfbe49c240edfd03a68be20002eaeaa/frozenlist-1.7.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:45a6f2fdbd10e074e8814eb98b05292f27bad7d1883afbe009d96abdcf3bc898", size = 246323 }, + { url = "https://files.pythonhosted.org/packages/5a/f5/720f3812e3d06cd89a1d5db9ff6450088b8f5c449dae8ffb2971a44da506/frozenlist-1.7.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:21884e23cffabb157a9dd7e353779077bf5b8f9a58e9b262c6caad2ef5f80a56", size = 233149 }, + { url = "https://files.pythonhosted.org/packages/69/68/03efbf545e217d5db8446acfd4c447c15b7c8cf4dbd4a58403111df9322d/frozenlist-1.7.0-cp311-cp311-win32.whl", hash = "sha256:284d233a8953d7b24f9159b8a3496fc1ddc00f4db99c324bd5fb5f22d8698ea7", size = 39565 }, + { url = "https://files.pythonhosted.org/packages/58/17/fe61124c5c333ae87f09bb67186d65038834a47d974fc10a5fadb4cc5ae1/frozenlist-1.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:387cbfdcde2f2353f19c2f66bbb52406d06ed77519ac7ee21be0232147c2592d", size = 44019 }, + { url = "https://files.pythonhosted.org/packages/ef/a2/c8131383f1e66adad5f6ecfcce383d584ca94055a34d683bbb24ac5f2f1c/frozenlist-1.7.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3dbf9952c4bb0e90e98aec1bd992b3318685005702656bc6f67c1a32b76787f2", size = 81424 }, + { url = "https://files.pythonhosted.org/packages/4c/9d/02754159955088cb52567337d1113f945b9e444c4960771ea90eb73de8db/frozenlist-1.7.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:1f5906d3359300b8a9bb194239491122e6cf1444c2efb88865426f170c262cdb", size = 47952 }, + { url = "https://files.pythonhosted.org/packages/01/7a/0046ef1bd6699b40acd2067ed6d6670b4db2f425c56980fa21c982c2a9db/frozenlist-1.7.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3dabd5a8f84573c8d10d8859a50ea2dec01eea372031929871368c09fa103478", size = 46688 }, + { url = "https://files.pythonhosted.org/packages/d6/a2/a910bafe29c86997363fb4c02069df4ff0b5bc39d33c5198b4e9dd42d8f8/frozenlist-1.7.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa57daa5917f1738064f302bf2626281a1cb01920c32f711fbc7bc36111058a8", size = 243084 }, + { url = "https://files.pythonhosted.org/packages/64/3e/5036af9d5031374c64c387469bfcc3af537fc0f5b1187d83a1cf6fab1639/frozenlist-1.7.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c193dda2b6d49f4c4398962810fa7d7c78f032bf45572b3e04dd5249dff27e08", size = 233524 }, + { url = "https://files.pythonhosted.org/packages/06/39/6a17b7c107a2887e781a48ecf20ad20f1c39d94b2a548c83615b5b879f28/frozenlist-1.7.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfe2b675cf0aaa6d61bf8fbffd3c274b3c9b7b1623beb3809df8a81399a4a9c4", size = 248493 }, + { url = "https://files.pythonhosted.org/packages/be/00/711d1337c7327d88c44d91dd0f556a1c47fb99afc060ae0ef66b4d24793d/frozenlist-1.7.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8fc5d5cda37f62b262405cf9652cf0856839c4be8ee41be0afe8858f17f4c94b", size = 244116 }, + { url = "https://files.pythonhosted.org/packages/24/fe/74e6ec0639c115df13d5850e75722750adabdc7de24e37e05a40527ca539/frozenlist-1.7.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b0d5ce521d1dd7d620198829b87ea002956e4319002ef0bc8d3e6d045cb4646e", size = 224557 }, + { url = "https://files.pythonhosted.org/packages/8d/db/48421f62a6f77c553575201e89048e97198046b793f4a089c79a6e3268bd/frozenlist-1.7.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:488d0a7d6a0008ca0db273c542098a0fa9e7dfaa7e57f70acef43f32b3f69dca", size = 241820 }, + { url = "https://files.pythonhosted.org/packages/1d/fa/cb4a76bea23047c8462976ea7b7a2bf53997a0ca171302deae9d6dd12096/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:15a7eaba63983d22c54d255b854e8108e7e5f3e89f647fc854bd77a237e767df", size = 236542 }, + { url = "https://files.pythonhosted.org/packages/5d/32/476a4b5cfaa0ec94d3f808f193301debff2ea42288a099afe60757ef6282/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:1eaa7e9c6d15df825bf255649e05bd8a74b04a4d2baa1ae46d9c2d00b2ca2cb5", size = 249350 }, + { url = "https://files.pythonhosted.org/packages/8d/ba/9a28042f84a6bf8ea5dbc81cfff8eaef18d78b2a1ad9d51c7bc5b029ad16/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e4389e06714cfa9d47ab87f784a7c5be91d3934cd6e9a7b85beef808297cc025", size = 225093 }, + { url = "https://files.pythonhosted.org/packages/bc/29/3a32959e68f9cf000b04e79ba574527c17e8842e38c91d68214a37455786/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:73bd45e1488c40b63fe5a7df892baf9e2a4d4bb6409a2b3b78ac1c6236178e01", size = 245482 }, + { url = "https://files.pythonhosted.org/packages/80/e8/edf2f9e00da553f07f5fa165325cfc302dead715cab6ac8336a5f3d0adc2/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:99886d98e1643269760e5fe0df31e5ae7050788dd288947f7f007209b8c33f08", size = 249590 }, + { url = "https://files.pythonhosted.org/packages/1c/80/9a0eb48b944050f94cc51ee1c413eb14a39543cc4f760ed12657a5a3c45a/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:290a172aae5a4c278c6da8a96222e6337744cd9c77313efe33d5670b9f65fc43", size = 237785 }, + { url = "https://files.pythonhosted.org/packages/f3/74/87601e0fb0369b7a2baf404ea921769c53b7ae00dee7dcfe5162c8c6dbf0/frozenlist-1.7.0-cp312-cp312-win32.whl", hash = "sha256:426c7bc70e07cfebc178bc4c2bf2d861d720c4fff172181eeb4a4c41d4ca2ad3", size = 39487 }, + { url = "https://files.pythonhosted.org/packages/0b/15/c026e9a9fc17585a9d461f65d8593d281fedf55fbf7eb53f16c6df2392f9/frozenlist-1.7.0-cp312-cp312-win_amd64.whl", hash = "sha256:563b72efe5da92e02eb68c59cb37205457c977aa7a449ed1b37e6939e5c47c6a", size = 43874 }, + { url = "https://files.pythonhosted.org/packages/24/90/6b2cebdabdbd50367273c20ff6b57a3dfa89bd0762de02c3a1eb42cb6462/frozenlist-1.7.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee80eeda5e2a4e660651370ebffd1286542b67e268aa1ac8d6dbe973120ef7ee", size = 79791 }, + { url = "https://files.pythonhosted.org/packages/83/2e/5b70b6a3325363293fe5fc3ae74cdcbc3e996c2a11dde2fd9f1fb0776d19/frozenlist-1.7.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d1a81c85417b914139e3a9b995d4a1c84559afc839a93cf2cb7f15e6e5f6ed2d", size = 47165 }, + { url = "https://files.pythonhosted.org/packages/f4/25/a0895c99270ca6966110f4ad98e87e5662eab416a17e7fd53c364bf8b954/frozenlist-1.7.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cbb65198a9132ebc334f237d7b0df163e4de83fb4f2bdfe46c1e654bdb0c5d43", size = 45881 }, + { url = "https://files.pythonhosted.org/packages/19/7c/71bb0bbe0832793c601fff68cd0cf6143753d0c667f9aec93d3c323f4b55/frozenlist-1.7.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dab46c723eeb2c255a64f9dc05b8dd601fde66d6b19cdb82b2e09cc6ff8d8b5d", size = 232409 }, + { url = "https://files.pythonhosted.org/packages/c0/45/ed2798718910fe6eb3ba574082aaceff4528e6323f9a8570be0f7028d8e9/frozenlist-1.7.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6aeac207a759d0dedd2e40745575ae32ab30926ff4fa49b1635def65806fddee", size = 225132 }, + { url = "https://files.pythonhosted.org/packages/ba/e2/8417ae0f8eacb1d071d4950f32f229aa6bf68ab69aab797b72a07ea68d4f/frozenlist-1.7.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bd8c4e58ad14b4fa7802b8be49d47993182fdd4023393899632c88fd8cd994eb", size = 237638 }, + { url = "https://files.pythonhosted.org/packages/f8/b7/2ace5450ce85f2af05a871b8c8719b341294775a0a6c5585d5e6170f2ce7/frozenlist-1.7.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04fb24d104f425da3540ed83cbfc31388a586a7696142004c577fa61c6298c3f", size = 233539 }, + { url = "https://files.pythonhosted.org/packages/46/b9/6989292c5539553dba63f3c83dc4598186ab2888f67c0dc1d917e6887db6/frozenlist-1.7.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6a5c505156368e4ea6b53b5ac23c92d7edc864537ff911d2fb24c140bb175e60", size = 215646 }, + { url = "https://files.pythonhosted.org/packages/72/31/bc8c5c99c7818293458fe745dab4fd5730ff49697ccc82b554eb69f16a24/frozenlist-1.7.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8bd7eb96a675f18aa5c553eb7ddc24a43c8c18f22e1f9925528128c052cdbe00", size = 232233 }, + { url = "https://files.pythonhosted.org/packages/59/52/460db4d7ba0811b9ccb85af996019f5d70831f2f5f255f7cc61f86199795/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:05579bf020096fe05a764f1f84cd104a12f78eaab68842d036772dc6d4870b4b", size = 227996 }, + { url = "https://files.pythonhosted.org/packages/ba/c9/f4b39e904c03927b7ecf891804fd3b4df3db29b9e487c6418e37988d6e9d/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:376b6222d114e97eeec13d46c486facd41d4f43bab626b7c3f6a8b4e81a5192c", size = 242280 }, + { url = "https://files.pythonhosted.org/packages/b8/33/3f8d6ced42f162d743e3517781566b8481322be321b486d9d262adf70bfb/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:0aa7e176ebe115379b5b1c95b4096fb1c17cce0847402e227e712c27bdb5a949", size = 217717 }, + { url = "https://files.pythonhosted.org/packages/3e/e8/ad683e75da6ccef50d0ab0c2b2324b32f84fc88ceee778ed79b8e2d2fe2e/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3fbba20e662b9c2130dc771e332a99eff5da078b2b2648153a40669a6d0e36ca", size = 236644 }, + { url = "https://files.pythonhosted.org/packages/b2/14/8d19ccdd3799310722195a72ac94ddc677541fb4bef4091d8e7775752360/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:f3f4410a0a601d349dd406b5713fec59b4cee7e71678d5b17edda7f4655a940b", size = 238879 }, + { url = "https://files.pythonhosted.org/packages/ce/13/c12bf657494c2fd1079a48b2db49fa4196325909249a52d8f09bc9123fd7/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e2cdfaaec6a2f9327bf43c933c0319a7c429058e8537c508964a133dffee412e", size = 232502 }, + { url = "https://files.pythonhosted.org/packages/d7/8b/e7f9dfde869825489382bc0d512c15e96d3964180c9499efcec72e85db7e/frozenlist-1.7.0-cp313-cp313-win32.whl", hash = "sha256:5fc4df05a6591c7768459caba1b342d9ec23fa16195e744939ba5914596ae3e1", size = 39169 }, + { url = "https://files.pythonhosted.org/packages/35/89/a487a98d94205d85745080a37860ff5744b9820a2c9acbcdd9440bfddf98/frozenlist-1.7.0-cp313-cp313-win_amd64.whl", hash = "sha256:52109052b9791a3e6b5d1b65f4b909703984b770694d3eb64fad124c835d7cba", size = 43219 }, + { url = "https://files.pythonhosted.org/packages/56/d5/5c4cf2319a49eddd9dd7145e66c4866bdc6f3dbc67ca3d59685149c11e0d/frozenlist-1.7.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a6f86e4193bb0e235ef6ce3dde5cbabed887e0b11f516ce8a0f4d3b33078ec2d", size = 84345 }, + { url = "https://files.pythonhosted.org/packages/a4/7d/ec2c1e1dc16b85bc9d526009961953df9cec8481b6886debb36ec9107799/frozenlist-1.7.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:82d664628865abeb32d90ae497fb93df398a69bb3434463d172b80fc25b0dd7d", size = 48880 }, + { url = "https://files.pythonhosted.org/packages/69/86/f9596807b03de126e11e7d42ac91e3d0b19a6599c714a1989a4e85eeefc4/frozenlist-1.7.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:912a7e8375a1c9a68325a902f3953191b7b292aa3c3fb0d71a216221deca460b", size = 48498 }, + { url = "https://files.pythonhosted.org/packages/5e/cb/df6de220f5036001005f2d726b789b2c0b65f2363b104bbc16f5be8084f8/frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9537c2777167488d539bc5de2ad262efc44388230e5118868e172dd4a552b146", size = 292296 }, + { url = "https://files.pythonhosted.org/packages/83/1f/de84c642f17c8f851a2905cee2dae401e5e0daca9b5ef121e120e19aa825/frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:f34560fb1b4c3e30ba35fa9a13894ba39e5acfc5f60f57d8accde65f46cc5e74", size = 273103 }, + { url = "https://files.pythonhosted.org/packages/88/3c/c840bfa474ba3fa13c772b93070893c6e9d5c0350885760376cbe3b6c1b3/frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:acd03d224b0175f5a850edc104ac19040d35419eddad04e7cf2d5986d98427f1", size = 292869 }, + { url = "https://files.pythonhosted.org/packages/a6/1c/3efa6e7d5a39a1d5ef0abeb51c48fb657765794a46cf124e5aca2c7a592c/frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f2038310bc582f3d6a09b3816ab01737d60bf7b1ec70f5356b09e84fb7408ab1", size = 291467 }, + { url = "https://files.pythonhosted.org/packages/4f/00/d5c5e09d4922c395e2f2f6b79b9a20dab4b67daaf78ab92e7729341f61f6/frozenlist-1.7.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b8c05e4c8e5f36e5e088caa1bf78a687528f83c043706640a92cb76cd6999384", size = 266028 }, + { url = "https://files.pythonhosted.org/packages/4e/27/72765be905619dfde25a7f33813ac0341eb6b076abede17a2e3fbfade0cb/frozenlist-1.7.0-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:765bb588c86e47d0b68f23c1bee323d4b703218037765dcf3f25c838c6fecceb", size = 284294 }, + { url = "https://files.pythonhosted.org/packages/88/67/c94103a23001b17808eb7dd1200c156bb69fb68e63fcf0693dde4cd6228c/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:32dc2e08c67d86d0969714dd484fd60ff08ff81d1a1e40a77dd34a387e6ebc0c", size = 281898 }, + { url = "https://files.pythonhosted.org/packages/42/34/a3e2c00c00f9e2a9db5653bca3fec306349e71aff14ae45ecc6d0951dd24/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:c0303e597eb5a5321b4de9c68e9845ac8f290d2ab3f3e2c864437d3c5a30cd65", size = 290465 }, + { url = "https://files.pythonhosted.org/packages/bb/73/f89b7fbce8b0b0c095d82b008afd0590f71ccb3dee6eee41791cf8cd25fd/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:a47f2abb4e29b3a8d0b530f7c3598badc6b134562b1a5caee867f7c62fee51e3", size = 266385 }, + { url = "https://files.pythonhosted.org/packages/cd/45/e365fdb554159462ca12df54bc59bfa7a9a273ecc21e99e72e597564d1ae/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:3d688126c242a6fabbd92e02633414d40f50bb6002fa4cf995a1d18051525657", size = 288771 }, + { url = "https://files.pythonhosted.org/packages/00/11/47b6117002a0e904f004d70ec5194fe9144f117c33c851e3d51c765962d0/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:4e7e9652b3d367c7bd449a727dc79d5043f48b88d0cbfd4f9f1060cf2b414104", size = 288206 }, + { url = "https://files.pythonhosted.org/packages/40/37/5f9f3c3fd7f7746082ec67bcdc204db72dad081f4f83a503d33220a92973/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:1a85e345b4c43db8b842cab1feb41be5cc0b10a1830e6295b69d7310f99becaf", size = 282620 }, + { url = "https://files.pythonhosted.org/packages/0b/31/8fbc5af2d183bff20f21aa743b4088eac4445d2bb1cdece449ae80e4e2d1/frozenlist-1.7.0-cp313-cp313t-win32.whl", hash = "sha256:3a14027124ddb70dfcee5148979998066897e79f89f64b13328595c4bdf77c81", size = 43059 }, + { url = "https://files.pythonhosted.org/packages/bb/ed/41956f52105b8dbc26e457c5705340c67c8cc2b79f394b79bffc09d0e938/frozenlist-1.7.0-cp313-cp313t-win_amd64.whl", hash = "sha256:3bf8010d71d4507775f658e9823210b7427be36625b387221642725b515dcf3e", size = 47516 }, + { url = "https://files.pythonhosted.org/packages/ee/45/b82e3c16be2182bff01179db177fe144d58b5dc787a7d4492c6ed8b9317f/frozenlist-1.7.0-py3-none-any.whl", hash = "sha256:9a5af342e34f7e97caf8c995864c7a396418ae2859cc6fdf1b1073020d516a7e", size = 13106 }, +] + +[[package]] +name = "fsspec" +version = "2025.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8b/02/0835e6ab9cfc03916fe3f78c0956cfcdb6ff2669ffa6651065d5ebf7fc98/fsspec-2025.7.0.tar.gz", hash = "sha256:786120687ffa54b8283d942929540d8bc5ccfa820deb555a2b5d0ed2b737bf58", size = 304432 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/e0/014d5d9d7a4564cf1c40b5039bc882db69fd881111e03ab3657ac0b218e2/fsspec-2025.7.0-py3-none-any.whl", hash = "sha256:8b012e39f63c7d5f10474de957f3ab793b47b45ae7d39f2fb735f8bbe25c0e21", size = 199597 }, +] + +[[package]] +name = "gitdb" +version = "4.0.12" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "smmap" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/94/63b0fc47eb32792c7ba1fe1b694daec9a63620db1e313033d18140c2320a/gitdb-4.0.12.tar.gz", hash = "sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571", size = 394684 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl", hash = "sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf", size = 62794 }, +] + +[[package]] +name = "gitpython" +version = "3.1.38" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "gitdb" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/45/cee7af549b6fa33f04531e402693a772b776cd9f845a2cbeca99cfac3331/GitPython-3.1.38.tar.gz", hash = "sha256:4d683e8957c8998b58ddb937e3e6cd167215a180e1ffd4da769ab81c620a89fe", size = 200632 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/ae/044453eacd5a526d3f242ccd77e38ee8219c65e0b132562b551bd67c61a4/GitPython-3.1.38-py3-none-any.whl", hash = "sha256:9e98b672ffcb081c2c8d5aa630d4251544fb040fb158863054242f24a2a2ba30", size = 190573 }, +] + +[[package]] +name = "google-api-core" +version = "2.25.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-auth" }, + { name = "googleapis-common-protos" }, + { name = "proto-plus" }, + { name = "protobuf" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dc/21/e9d043e88222317afdbdb567165fdbc3b0aad90064c7e0c9eb0ad9955ad8/google_api_core-2.25.1.tar.gz", hash = "sha256:d2aaa0b13c78c61cb3f4282c464c046e45fbd75755683c9c525e6e8f7ed0a5e8", size = 165443 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/4b/ead00905132820b623732b175d66354e9d3e69fcf2a5dcdab780664e7896/google_api_core-2.25.1-py3-none-any.whl", hash = "sha256:8a2a56c1fef82987a524371f99f3bd0143702fecc670c72e600c1cda6bf8dbb7", size = 160807 }, +] + +[package.optional-dependencies] +grpc = [ + { name = "grpcio" }, + { name = "grpcio-status" }, +] + +[[package]] +name = "google-auth" +version = "2.40.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cachetools" }, + { name = "pyasn1-modules" }, + { name = "rsa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9e/9b/e92ef23b84fa10a64ce4831390b7a4c2e53c0132568d99d4ae61d04c8855/google_auth-2.40.3.tar.gz", hash = "sha256:500c3a29adedeb36ea9cf24b8d10858e152f2412e3ca37829b3fa18e33d63b77", size = 281029 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/63/b19553b658a1692443c62bd07e5868adaa0ad746a0751ba62c59568cd45b/google_auth-2.40.3-py2.py3-none-any.whl", hash = "sha256:1370d4593e86213563547f97a92752fc658456fe4514c809544f330fed45a7ca", size = 216137 }, +] + +[[package]] +name = "google-cloud-vision" +version = "3.10.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-api-core", extra = ["grpc"] }, + { name = "google-auth" }, + { name = "proto-plus" }, + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/14/7e/6bf616c5bf22a0d7943082318a99f5cb09046605e4077dc5366a80326a12/google_cloud_vision-3.10.2.tar.gz", hash = "sha256:649380faab8933440b632bf88072c0c382a08d49ab02bc0b4fba821882ae1765", size = 570339 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/aa/db9febba7b5bd9c9d772e935a5c495fb2b4ee05299e46c6c4b1e7c0b66b2/google_cloud_vision-3.10.2-py3-none-any.whl", hash = "sha256:42a17fbc2219b0a88e325e2c1df6664a8dafcbae66363fb37ebcb511b018fc87", size = 527877 }, +] + +[[package]] +name = "googleapis-common-protos" +version = "1.70.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/39/24/33db22342cf4a2ea27c9955e6713140fedd51e8b141b5ce5260897020f1a/googleapis_common_protos-1.70.0.tar.gz", hash = "sha256:0e1b44e0ea153e6594f9f394fef15193a68aaaea2d843f83e2742717ca753257", size = 145903 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/86/f1/62a193f0227cf15a920390abe675f386dec35f7ae3ffe6da582d3ade42c7/googleapis_common_protos-1.70.0-py3-none-any.whl", hash = "sha256:b8bfcca8c25a2bb253e0e0b0adaf8c00773e5e6af6fd92397576680b807e0fd8", size = 294530 }, +] + +[[package]] +name = "gptcache" +version = "0.1.44" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cachetools" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/27/73/5cc20749e06017044106837550384f5d8ed00b8e9570689f17e7292e2d23/gptcache-0.1.44.tar.gz", hash = "sha256:d3d5e6a75c57594dc58212c2d6c53a7999c23ede30e0be66d213d885c0ad0be9", size = 95969 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/87/8dde0a3757bc207805f751b47878888b09db4a464ae48a55f386f091b488/gptcache-0.1.44-py3-none-any.whl", hash = "sha256:11ddd63b173dc3822b8c2eb7588ea947c825845ed0737b043038a238286bfec4", size = 131634 }, +] + +[[package]] +name = "greenlet" +version = "3.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c9/92/bb85bd6e80148a4d2e0c59f7c0c2891029f8fd510183afc7d8d2feeed9b6/greenlet-3.2.3.tar.gz", hash = "sha256:8b0dd8ae4c0d6f5e54ee55ba935eeb3d735a9b58a8a1e5b5cbab64e01a39f365", size = 185752 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/db/b4c12cff13ebac2786f4f217f06588bccd8b53d260453404ef22b121fc3a/greenlet-3.2.3-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:1afd685acd5597349ee6d7a88a8bec83ce13c106ac78c196ee9dde7c04fe87be", size = 268977 }, + { url = "https://files.pythonhosted.org/packages/52/61/75b4abd8147f13f70986df2801bf93735c1bd87ea780d70e3b3ecda8c165/greenlet-3.2.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:761917cac215c61e9dc7324b2606107b3b292a8349bdebb31503ab4de3f559ac", size = 627351 }, + { url = "https://files.pythonhosted.org/packages/35/aa/6894ae299d059d26254779a5088632874b80ee8cf89a88bca00b0709d22f/greenlet-3.2.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:a433dbc54e4a37e4fff90ef34f25a8c00aed99b06856f0119dcf09fbafa16392", size = 638599 }, + { url = "https://files.pythonhosted.org/packages/30/64/e01a8261d13c47f3c082519a5e9dbf9e143cc0498ed20c911d04e54d526c/greenlet-3.2.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:72e77ed69312bab0434d7292316d5afd6896192ac4327d44f3d613ecb85b037c", size = 634482 }, + { url = "https://files.pythonhosted.org/packages/47/48/ff9ca8ba9772d083a4f5221f7b4f0ebe8978131a9ae0909cf202f94cd879/greenlet-3.2.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:68671180e3849b963649254a882cd544a3c75bfcd2c527346ad8bb53494444db", size = 633284 }, + { url = "https://files.pythonhosted.org/packages/e9/45/626e974948713bc15775b696adb3eb0bd708bec267d6d2d5c47bb47a6119/greenlet-3.2.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:49c8cfb18fb419b3d08e011228ef8a25882397f3a859b9fe1436946140b6756b", size = 582206 }, + { url = "https://files.pythonhosted.org/packages/b1/8e/8b6f42c67d5df7db35b8c55c9a850ea045219741bb14416255616808c690/greenlet-3.2.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:efc6dc8a792243c31f2f5674b670b3a95d46fa1c6a912b8e310d6f542e7b0712", size = 1111412 }, + { url = "https://files.pythonhosted.org/packages/05/46/ab58828217349500a7ebb81159d52ca357da747ff1797c29c6023d79d798/greenlet-3.2.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:731e154aba8e757aedd0781d4b240f1225b075b4409f1bb83b05ff410582cf00", size = 1135054 }, + { url = "https://files.pythonhosted.org/packages/68/7f/d1b537be5080721c0f0089a8447d4ef72839039cdb743bdd8ffd23046e9a/greenlet-3.2.3-cp310-cp310-win_amd64.whl", hash = "sha256:96c20252c2f792defe9a115d3287e14811036d51e78b3aaddbee23b69b216302", size = 296573 }, + { url = "https://files.pythonhosted.org/packages/fc/2e/d4fcb2978f826358b673f779f78fa8a32ee37df11920dc2bb5589cbeecef/greenlet-3.2.3-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:784ae58bba89fa1fa5733d170d42486580cab9decda3484779f4759345b29822", size = 270219 }, + { url = "https://files.pythonhosted.org/packages/16/24/929f853e0202130e4fe163bc1d05a671ce8dcd604f790e14896adac43a52/greenlet-3.2.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0921ac4ea42a5315d3446120ad48f90c3a6b9bb93dd9b3cf4e4d84a66e42de83", size = 630383 }, + { url = "https://files.pythonhosted.org/packages/d1/b2/0320715eb61ae70c25ceca2f1d5ae620477d246692d9cc284c13242ec31c/greenlet-3.2.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:d2971d93bb99e05f8c2c0c2f4aa9484a18d98c4c3bd3c62b65b7e6ae33dfcfaf", size = 642422 }, + { url = "https://files.pythonhosted.org/packages/bd/49/445fd1a210f4747fedf77615d941444349c6a3a4a1135bba9701337cd966/greenlet-3.2.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:c667c0bf9d406b77a15c924ef3285e1e05250948001220368e039b6aa5b5034b", size = 638375 }, + { url = "https://files.pythonhosted.org/packages/7e/c8/ca19760cf6eae75fa8dc32b487e963d863b3ee04a7637da77b616703bc37/greenlet-3.2.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:592c12fb1165be74592f5de0d70f82bc5ba552ac44800d632214b76089945147", size = 637627 }, + { url = "https://files.pythonhosted.org/packages/65/89/77acf9e3da38e9bcfca881e43b02ed467c1dedc387021fc4d9bd9928afb8/greenlet-3.2.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29e184536ba333003540790ba29829ac14bb645514fbd7e32af331e8202a62a5", size = 585502 }, + { url = "https://files.pythonhosted.org/packages/97/c6/ae244d7c95b23b7130136e07a9cc5aadd60d59b5951180dc7dc7e8edaba7/greenlet-3.2.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:93c0bb79844a367782ec4f429d07589417052e621aa39a5ac1fb99c5aa308edc", size = 1114498 }, + { url = "https://files.pythonhosted.org/packages/89/5f/b16dec0cbfd3070658e0d744487919740c6d45eb90946f6787689a7efbce/greenlet-3.2.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:751261fc5ad7b6705f5f76726567375bb2104a059454e0226e1eef6c756748ba", size = 1139977 }, + { url = "https://files.pythonhosted.org/packages/66/77/d48fb441b5a71125bcac042fc5b1494c806ccb9a1432ecaa421e72157f77/greenlet-3.2.3-cp311-cp311-win_amd64.whl", hash = "sha256:83a8761c75312361aa2b5b903b79da97f13f556164a7dd2d5448655425bd4c34", size = 297017 }, + { url = "https://files.pythonhosted.org/packages/f3/94/ad0d435f7c48debe960c53b8f60fb41c2026b1d0fa4a99a1cb17c3461e09/greenlet-3.2.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:25ad29caed5783d4bd7a85c9251c651696164622494c00802a139c00d639242d", size = 271992 }, + { url = "https://files.pythonhosted.org/packages/93/5d/7c27cf4d003d6e77749d299c7c8f5fd50b4f251647b5c2e97e1f20da0ab5/greenlet-3.2.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88cd97bf37fe24a6710ec6a3a7799f3f81d9cd33317dcf565ff9950c83f55e0b", size = 638820 }, + { url = "https://files.pythonhosted.org/packages/c6/7e/807e1e9be07a125bb4c169144937910bf59b9d2f6d931578e57f0bce0ae2/greenlet-3.2.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:baeedccca94880d2f5666b4fa16fc20ef50ba1ee353ee2d7092b383a243b0b0d", size = 653046 }, + { url = "https://files.pythonhosted.org/packages/9d/ab/158c1a4ea1068bdbc78dba5a3de57e4c7aeb4e7fa034320ea94c688bfb61/greenlet-3.2.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:be52af4b6292baecfa0f397f3edb3c6092ce071b499dd6fe292c9ac9f2c8f264", size = 647701 }, + { url = "https://files.pythonhosted.org/packages/cc/0d/93729068259b550d6a0288da4ff72b86ed05626eaf1eb7c0d3466a2571de/greenlet-3.2.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0cc73378150b8b78b0c9fe2ce56e166695e67478550769536a6742dca3651688", size = 649747 }, + { url = "https://files.pythonhosted.org/packages/f6/f6/c82ac1851c60851302d8581680573245c8fc300253fc1ff741ae74a6c24d/greenlet-3.2.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:706d016a03e78df129f68c4c9b4c4f963f7d73534e48a24f5f5a7101ed13dbbb", size = 605461 }, + { url = "https://files.pythonhosted.org/packages/98/82/d022cf25ca39cf1200650fc58c52af32c90f80479c25d1cbf57980ec3065/greenlet-3.2.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:419e60f80709510c343c57b4bb5a339d8767bf9aef9b8ce43f4f143240f88b7c", size = 1121190 }, + { url = "https://files.pythonhosted.org/packages/f5/e1/25297f70717abe8104c20ecf7af0a5b82d2f5a980eb1ac79f65654799f9f/greenlet-3.2.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:93d48533fade144203816783373f27a97e4193177ebaaf0fc396db19e5d61163", size = 1149055 }, + { url = "https://files.pythonhosted.org/packages/1f/8f/8f9e56c5e82eb2c26e8cde787962e66494312dc8cb261c460e1f3a9c88bc/greenlet-3.2.3-cp312-cp312-win_amd64.whl", hash = "sha256:7454d37c740bb27bdeddfc3f358f26956a07d5220818ceb467a483197d84f849", size = 297817 }, + { url = "https://files.pythonhosted.org/packages/b1/cf/f5c0b23309070ae93de75c90d29300751a5aacefc0a3ed1b1d8edb28f08b/greenlet-3.2.3-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:500b8689aa9dd1ab26872a34084503aeddefcb438e2e7317b89b11eaea1901ad", size = 270732 }, + { url = "https://files.pythonhosted.org/packages/48/ae/91a957ba60482d3fecf9be49bc3948f341d706b52ddb9d83a70d42abd498/greenlet-3.2.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a07d3472c2a93117af3b0136f246b2833fdc0b542d4a9799ae5f41c28323faef", size = 639033 }, + { url = "https://files.pythonhosted.org/packages/6f/df/20ffa66dd5a7a7beffa6451bdb7400d66251374ab40b99981478c69a67a8/greenlet-3.2.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:8704b3768d2f51150626962f4b9a9e4a17d2e37c8a8d9867bbd9fa4eb938d3b3", size = 652999 }, + { url = "https://files.pythonhosted.org/packages/51/b4/ebb2c8cb41e521f1d72bf0465f2f9a2fd803f674a88db228887e6847077e/greenlet-3.2.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:5035d77a27b7c62db6cf41cf786cfe2242644a7a337a0e155c80960598baab95", size = 647368 }, + { url = "https://files.pythonhosted.org/packages/8e/6a/1e1b5aa10dced4ae876a322155705257748108b7fd2e4fae3f2a091fe81a/greenlet-3.2.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2d8aa5423cd4a396792f6d4580f88bdc6efcb9205891c9d40d20f6e670992efb", size = 650037 }, + { url = "https://files.pythonhosted.org/packages/26/f2/ad51331a157c7015c675702e2d5230c243695c788f8f75feba1af32b3617/greenlet-3.2.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2c724620a101f8170065d7dded3f962a2aea7a7dae133a009cada42847e04a7b", size = 608402 }, + { url = "https://files.pythonhosted.org/packages/26/bc/862bd2083e6b3aff23300900a956f4ea9a4059de337f5c8734346b9b34fc/greenlet-3.2.3-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:873abe55f134c48e1f2a6f53f7d1419192a3d1a4e873bace00499a4e45ea6af0", size = 1119577 }, + { url = "https://files.pythonhosted.org/packages/86/94/1fc0cc068cfde885170e01de40a619b00eaa8f2916bf3541744730ffb4c3/greenlet-3.2.3-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:024571bbce5f2c1cfff08bf3fbaa43bbc7444f580ae13b0099e95d0e6e67ed36", size = 1147121 }, + { url = "https://files.pythonhosted.org/packages/27/1a/199f9587e8cb08a0658f9c30f3799244307614148ffe8b1e3aa22f324dea/greenlet-3.2.3-cp313-cp313-win_amd64.whl", hash = "sha256:5195fb1e75e592dd04ce79881c8a22becdfa3e6f500e7feb059b1e6fdd54d3e3", size = 297603 }, +] + +[[package]] +name = "grpcio" +version = "1.74.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/38/b4/35feb8f7cab7239c5b94bd2db71abb3d6adb5f335ad8f131abb6060840b6/grpcio-1.74.0.tar.gz", hash = "sha256:80d1f4fbb35b0742d3e3d3bb654b7381cd5f015f8497279a1e9c21ba623e01b1", size = 12756048 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/66/54/68e51a90797ad7afc5b0a7881426c337f6a9168ebab73c3210b76aa7c90d/grpcio-1.74.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:85bd5cdf4ed7b2d6438871adf6afff9af7096486fcf51818a81b77ef4dd30907", size = 5481935 }, + { url = "https://files.pythonhosted.org/packages/32/2a/af817c7e9843929e93e54d09c9aee2555c2e8d81b93102a9426b36e91833/grpcio-1.74.0-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:68c8ebcca945efff9d86d8d6d7bfb0841cf0071024417e2d7f45c5e46b5b08eb", size = 10986796 }, + { url = "https://files.pythonhosted.org/packages/d5/94/d67756638d7bb07750b07d0826c68e414124574b53840ba1ff777abcd388/grpcio-1.74.0-cp310-cp310-manylinux_2_17_aarch64.whl", hash = "sha256:e154d230dc1bbbd78ad2fdc3039fa50ad7ffcf438e4eb2fa30bce223a70c7486", size = 5983663 }, + { url = "https://files.pythonhosted.org/packages/35/f5/c5e4853bf42148fea8532d49e919426585b73eafcf379a712934652a8de9/grpcio-1.74.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e8978003816c7b9eabe217f88c78bc26adc8f9304bf6a594b02e5a49b2ef9c11", size = 6653765 }, + { url = "https://files.pythonhosted.org/packages/fd/75/a1991dd64b331d199935e096cc9daa3415ee5ccbe9f909aa48eded7bba34/grpcio-1.74.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c3d7bd6e3929fd2ea7fbc3f562e4987229ead70c9ae5f01501a46701e08f1ad9", size = 6215172 }, + { url = "https://files.pythonhosted.org/packages/01/a4/7cef3dbb3b073d0ce34fd507efc44ac4c9442a0ef9fba4fb3f5c551efef5/grpcio-1.74.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:136b53c91ac1d02c8c24201bfdeb56f8b3ac3278668cbb8e0ba49c88069e1bdc", size = 6329142 }, + { url = "https://files.pythonhosted.org/packages/bf/d3/587920f882b46e835ad96014087054655312400e2f1f1446419e5179a383/grpcio-1.74.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:fe0f540750a13fd8e5da4b3eaba91a785eea8dca5ccd2bc2ffe978caa403090e", size = 7018632 }, + { url = "https://files.pythonhosted.org/packages/1f/95/c70a3b15a0bc83334b507e3d2ae20ee8fa38d419b8758a4d838f5c2a7d32/grpcio-1.74.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4e4181bfc24413d1e3a37a0b7889bea68d973d4b45dd2bc68bb766c140718f82", size = 6509641 }, + { url = "https://files.pythonhosted.org/packages/4b/06/2e7042d06247d668ae69ea6998eca33f475fd4e2855f94dcb2aa5daef334/grpcio-1.74.0-cp310-cp310-win32.whl", hash = "sha256:1733969040989f7acc3d94c22f55b4a9501a30f6aaacdbccfaba0a3ffb255ab7", size = 3817478 }, + { url = "https://files.pythonhosted.org/packages/93/20/e02b9dcca3ee91124060b65bbf5b8e1af80b3b76a30f694b44b964ab4d71/grpcio-1.74.0-cp310-cp310-win_amd64.whl", hash = "sha256:9e912d3c993a29df6c627459af58975b2e5c897d93287939b9d5065f000249b5", size = 4493971 }, + { url = "https://files.pythonhosted.org/packages/e7/77/b2f06db9f240a5abeddd23a0e49eae2b6ac54d85f0e5267784ce02269c3b/grpcio-1.74.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:69e1a8180868a2576f02356565f16635b99088da7df3d45aaa7e24e73a054e31", size = 5487368 }, + { url = "https://files.pythonhosted.org/packages/48/99/0ac8678a819c28d9a370a663007581744a9f2a844e32f0fa95e1ddda5b9e/grpcio-1.74.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:8efe72fde5500f47aca1ef59495cb59c885afe04ac89dd11d810f2de87d935d4", size = 10999804 }, + { url = "https://files.pythonhosted.org/packages/45/c6/a2d586300d9e14ad72e8dc211c7aecb45fe9846a51e558c5bca0c9102c7f/grpcio-1.74.0-cp311-cp311-manylinux_2_17_aarch64.whl", hash = "sha256:a8f0302f9ac4e9923f98d8e243939a6fb627cd048f5cd38595c97e38020dffce", size = 5987667 }, + { url = "https://files.pythonhosted.org/packages/c9/57/5f338bf56a7f22584e68d669632e521f0de460bb3749d54533fc3d0fca4f/grpcio-1.74.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2f609a39f62a6f6f05c7512746798282546358a37ea93c1fcbadf8b2fed162e3", size = 6655612 }, + { url = "https://files.pythonhosted.org/packages/82/ea/a4820c4c44c8b35b1903a6c72a5bdccec92d0840cf5c858c498c66786ba5/grpcio-1.74.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c98e0b7434a7fa4e3e63f250456eaef52499fba5ae661c58cc5b5477d11e7182", size = 6219544 }, + { url = "https://files.pythonhosted.org/packages/a4/17/0537630a921365928f5abb6d14c79ba4dcb3e662e0dbeede8af4138d9dcf/grpcio-1.74.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:662456c4513e298db6d7bd9c3b8df6f75f8752f0ba01fb653e252ed4a59b5a5d", size = 6334863 }, + { url = "https://files.pythonhosted.org/packages/e2/a6/85ca6cb9af3f13e1320d0a806658dca432ff88149d5972df1f7b51e87127/grpcio-1.74.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:3d14e3c4d65e19d8430a4e28ceb71ace4728776fd6c3ce34016947474479683f", size = 7019320 }, + { url = "https://files.pythonhosted.org/packages/4f/a7/fe2beab970a1e25d2eff108b3cf4f7d9a53c185106377a3d1989216eba45/grpcio-1.74.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1bf949792cee20d2078323a9b02bacbbae002b9e3b9e2433f2741c15bdeba1c4", size = 6514228 }, + { url = "https://files.pythonhosted.org/packages/6a/c2/2f9c945c8a248cebc3ccda1b7a1bf1775b9d7d59e444dbb18c0014e23da6/grpcio-1.74.0-cp311-cp311-win32.whl", hash = "sha256:55b453812fa7c7ce2f5c88be3018fb4a490519b6ce80788d5913f3f9d7da8c7b", size = 3817216 }, + { url = "https://files.pythonhosted.org/packages/ff/d1/a9cf9c94b55becda2199299a12b9feef0c79946b0d9d34c989de6d12d05d/grpcio-1.74.0-cp311-cp311-win_amd64.whl", hash = "sha256:86ad489db097141a907c559988c29718719aa3e13370d40e20506f11b4de0d11", size = 4495380 }, + { url = "https://files.pythonhosted.org/packages/4c/5d/e504d5d5c4469823504f65687d6c8fb97b7f7bf0b34873b7598f1df24630/grpcio-1.74.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:8533e6e9c5bd630ca98062e3a1326249e6ada07d05acf191a77bc33f8948f3d8", size = 5445551 }, + { url = "https://files.pythonhosted.org/packages/43/01/730e37056f96f2f6ce9f17999af1556df62ee8dab7fa48bceeaab5fd3008/grpcio-1.74.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:2918948864fec2a11721d91568effffbe0a02b23ecd57f281391d986847982f6", size = 10979810 }, + { url = "https://files.pythonhosted.org/packages/79/3d/09fd100473ea5c47083889ca47ffd356576173ec134312f6aa0e13111dee/grpcio-1.74.0-cp312-cp312-manylinux_2_17_aarch64.whl", hash = "sha256:60d2d48b0580e70d2e1954d0d19fa3c2e60dd7cbed826aca104fff518310d1c5", size = 5941946 }, + { url = "https://files.pythonhosted.org/packages/8a/99/12d2cca0a63c874c6d3d195629dcd85cdf5d6f98a30d8db44271f8a97b93/grpcio-1.74.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3601274bc0523f6dc07666c0e01682c94472402ac2fd1226fd96e079863bfa49", size = 6621763 }, + { url = "https://files.pythonhosted.org/packages/9d/2c/930b0e7a2f1029bbc193443c7bc4dc2a46fedb0203c8793dcd97081f1520/grpcio-1.74.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:176d60a5168d7948539def20b2a3adcce67d72454d9ae05969a2e73f3a0feee7", size = 6180664 }, + { url = "https://files.pythonhosted.org/packages/db/d5/ff8a2442180ad0867717e670f5ec42bfd8d38b92158ad6bcd864e6d4b1ed/grpcio-1.74.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:e759f9e8bc908aaae0412642afe5416c9f983a80499448fcc7fab8692ae044c3", size = 6301083 }, + { url = "https://files.pythonhosted.org/packages/b0/ba/b361d390451a37ca118e4ec7dccec690422e05bc85fba2ec72b06cefec9f/grpcio-1.74.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:9e7c4389771855a92934b2846bd807fc25a3dfa820fd912fe6bd8136026b2707", size = 6994132 }, + { url = "https://files.pythonhosted.org/packages/3b/0c/3a5fa47d2437a44ced74141795ac0251bbddeae74bf81df3447edd767d27/grpcio-1.74.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:cce634b10aeab37010449124814b05a62fb5f18928ca878f1bf4750d1f0c815b", size = 6489616 }, + { url = "https://files.pythonhosted.org/packages/ae/95/ab64703b436d99dc5217228babc76047d60e9ad14df129e307b5fec81fd0/grpcio-1.74.0-cp312-cp312-win32.whl", hash = "sha256:885912559974df35d92219e2dc98f51a16a48395f37b92865ad45186f294096c", size = 3807083 }, + { url = "https://files.pythonhosted.org/packages/84/59/900aa2445891fc47a33f7d2f76e00ca5d6ae6584b20d19af9c06fa09bf9a/grpcio-1.74.0-cp312-cp312-win_amd64.whl", hash = "sha256:42f8fee287427b94be63d916c90399ed310ed10aadbf9e2e5538b3e497d269bc", size = 4490123 }, + { url = "https://files.pythonhosted.org/packages/d4/d8/1004a5f468715221450e66b051c839c2ce9a985aa3ee427422061fcbb6aa/grpcio-1.74.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:2bc2d7d8d184e2362b53905cb1708c84cb16354771c04b490485fa07ce3a1d89", size = 5449488 }, + { url = "https://files.pythonhosted.org/packages/94/0e/33731a03f63740d7743dced423846c831d8e6da808fcd02821a4416df7fa/grpcio-1.74.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:c14e803037e572c177ba54a3e090d6eb12efd795d49327c5ee2b3bddb836bf01", size = 10974059 }, + { url = "https://files.pythonhosted.org/packages/0d/c6/3d2c14d87771a421205bdca991467cfe473ee4c6a1231c1ede5248c62ab8/grpcio-1.74.0-cp313-cp313-manylinux_2_17_aarch64.whl", hash = "sha256:f6ec94f0e50eb8fa1744a731088b966427575e40c2944a980049798b127a687e", size = 5945647 }, + { url = "https://files.pythonhosted.org/packages/c5/83/5a354c8aaff58594eef7fffebae41a0f8995a6258bbc6809b800c33d4c13/grpcio-1.74.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:566b9395b90cc3d0d0c6404bc8572c7c18786ede549cdb540ae27b58afe0fb91", size = 6626101 }, + { url = "https://files.pythonhosted.org/packages/3f/ca/4fdc7bf59bf6994aa45cbd4ef1055cd65e2884de6113dbd49f75498ddb08/grpcio-1.74.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e1ea6176d7dfd5b941ea01c2ec34de9531ba494d541fe2057c904e601879f249", size = 6182562 }, + { url = "https://files.pythonhosted.org/packages/fd/48/2869e5b2c1922583686f7ae674937986807c2f676d08be70d0a541316270/grpcio-1.74.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:64229c1e9cea079420527fa8ac45d80fc1e8d3f94deaa35643c381fa8d98f362", size = 6303425 }, + { url = "https://files.pythonhosted.org/packages/a6/0e/bac93147b9a164f759497bc6913e74af1cb632c733c7af62c0336782bd38/grpcio-1.74.0-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:0f87bddd6e27fc776aacf7ebfec367b6d49cad0455123951e4488ea99d9b9b8f", size = 6996533 }, + { url = "https://files.pythonhosted.org/packages/84/35/9f6b2503c1fd86d068b46818bbd7329db26a87cdd8c01e0d1a9abea1104c/grpcio-1.74.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:3b03d8f2a07f0fea8c8f74deb59f8352b770e3900d143b3d1475effcb08eec20", size = 6491489 }, + { url = "https://files.pythonhosted.org/packages/75/33/a04e99be2a82c4cbc4039eb3a76f6c3632932b9d5d295221389d10ac9ca7/grpcio-1.74.0-cp313-cp313-win32.whl", hash = "sha256:b6a73b2ba83e663b2480a90b82fdae6a7aa6427f62bf43b29912c0cfd1aa2bfa", size = 3805811 }, + { url = "https://files.pythonhosted.org/packages/34/80/de3eb55eb581815342d097214bed4c59e806b05f1b3110df03b2280d6dfd/grpcio-1.74.0-cp313-cp313-win_amd64.whl", hash = "sha256:fd3c71aeee838299c5887230b8a1822795325ddfea635edd82954c1eaa831e24", size = 4489214 }, +] + +[[package]] +name = "grpcio-health-checking" +version = "1.74.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "grpcio" }, + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/82/69/87ad1cd826a9f06567bccb66f0c3228a0758ea47a1d35eef15db36b2f492/grpcio_health_checking-1.74.0.tar.gz", hash = "sha256:d6749451d4cef543c3f6260ae9a86c84b9ab02a92421cecae73a632e7fe920bf", size = 16770 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/62/2fd797be7514855a0b28f946f57b9a134bba25a9873c269fa730d8fd49d8/grpcio_health_checking-1.74.0-py3-none-any.whl", hash = "sha256:9a6e7bcdf16395105425753d6e3fe31f57cb6ab3f232282254b50f02cc63d9c7", size = 18921 }, +] + +[[package]] +name = "grpcio-status" +version = "1.74.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "googleapis-common-protos" }, + { name = "grpcio" }, + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/93/22/238c5f01e6837df54494deb08d5c772bc3f5bf5fb80a15dce254892d1a81/grpcio_status-1.74.0.tar.gz", hash = "sha256:c58c1b24aa454e30f1fc6a7e0dbbc194c54a408143971a94b5f4e40bb5831432", size = 13662 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/28/aa/1b1fe7d8ab699e1ec26d3a36b91d3df9f83a30abc07d4c881d0296b17b67/grpcio_status-1.74.0-py3-none-any.whl", hash = "sha256:52cdbd759a6760fc8f668098a03f208f493dd5c76bf8e02598bbbaf1f6fc2876", size = 14425 }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515 }, +] + +[[package]] +name = "h2" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "hpack" }, + { name = "hyperframe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1b/38/d7f80fd13e6582fb8e0df8c9a653dcc02b03ca34f4d72f34869298c5baf8/h2-4.2.0.tar.gz", hash = "sha256:c8a52129695e88b1a0578d8d2cc6842bbd79128ac685463b887ee278126ad01f", size = 2150682 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/9e/984486f2d0a0bd2b024bf4bc1c62688fcafa9e61991f041fb0e2def4a982/h2-4.2.0-py3-none-any.whl", hash = "sha256:479a53ad425bb29af087f3458a61d30780bc818e4ebcf01f0b536ba916462ed0", size = 60957 }, +] + +[[package]] +name = "hf-xet" +version = "1.1.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/0a/a0f56735940fde6dd627602fec9ab3bad23f66a272397560abd65aba416e/hf_xet-1.1.7.tar.gz", hash = "sha256:20cec8db4561338824a3b5f8c19774055b04a8df7fff0cb1ff2cb1a0c1607b80", size = 477719 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/7c/8d7803995caf14e7d19a392a486a040f923e2cfeff824e9b800b92072f76/hf_xet-1.1.7-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:60dae4b44d520819e54e216a2505685248ec0adbdb2dd4848b17aa85a0375cde", size = 2761743 }, + { url = "https://files.pythonhosted.org/packages/51/a3/fa5897099454aa287022a34a30e68dbff0e617760f774f8bd1db17f06bd4/hf_xet-1.1.7-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:b109f4c11e01c057fc82004c9e51e6cdfe2cb230637644ade40c599739067b2e", size = 2624331 }, + { url = "https://files.pythonhosted.org/packages/86/50/2446a132267e60b8a48b2e5835d6e24fd988000d0f5b9b15ebd6d64ef769/hf_xet-1.1.7-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6efaaf1a5a9fc3a501d3e71e88a6bfebc69ee3a716d0e713a931c8b8d920038f", size = 3183844 }, + { url = "https://files.pythonhosted.org/packages/20/8f/ccc670616bb9beee867c6bb7139f7eab2b1370fe426503c25f5cbb27b148/hf_xet-1.1.7-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:751571540f9c1fbad9afcf222a5fb96daf2384bf821317b8bfb0c59d86078513", size = 3074209 }, + { url = "https://files.pythonhosted.org/packages/21/0a/4c30e1eb77205565b854f5e4a82cf1f056214e4dc87f2918ebf83d47ae14/hf_xet-1.1.7-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:18b61bbae92d56ae731b92087c44efcac216071182c603fc535f8e29ec4b09b8", size = 3239602 }, + { url = "https://files.pythonhosted.org/packages/f5/1e/fc7e9baf14152662ef0b35fa52a6e889f770a7ed14ac239de3c829ecb47e/hf_xet-1.1.7-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:713f2bff61b252f8523739969f247aa354ad8e6d869b8281e174e2ea1bb8d604", size = 3348184 }, + { url = "https://files.pythonhosted.org/packages/a3/73/e354eae84ceff117ec3560141224724794828927fcc013c5b449bf0b8745/hf_xet-1.1.7-cp37-abi3-win_amd64.whl", hash = "sha256:2e356da7d284479ae0f1dea3cf5a2f74fdf925d6dca84ac4341930d892c7cb34", size = 2820008 }, +] + +[[package]] +name = "hpack" +version = "4.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2c/48/71de9ed269fdae9c8057e5a4c0aa7402e8bb16f2c6e90b3aa53327b113f8/hpack-4.1.0.tar.gz", hash = "sha256:ec5eca154f7056aa06f196a557655c5b009b382873ac8d1e66e79e87535f1dca", size = 51276 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/c6/80c95b1b2b94682a72cbdbfb85b81ae2daffa4291fbfa1b1464502ede10d/hpack-4.1.0-py3-none-any.whl", hash = "sha256:157ac792668d995c657d93111f46b4535ed114f0c9c8d672271bbec7eae1b496", size = 34357 }, +] + +[[package]] +name = "html5lib" +version = "1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, + { name = "webencodings" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/b6/b55c3f49042f1df3dcd422b7f224f939892ee94f22abcf503a9b7339eaf2/html5lib-1.1.tar.gz", hash = "sha256:b2e5b40261e20f354d198eae92afc10d750afb487ed5e50f9c4eaf07c184146f", size = 272215 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6c/dd/a834df6482147d48e225a49515aabc28974ad5a4ca3215c18a882565b028/html5lib-1.1-py2.py3-none-any.whl", hash = "sha256:0d78f8fde1c230e99fe37986a60526d7049ed4bf8a9fadbad5f00e22e58e041d", size = 112173 }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784 }, +] + +[[package]] +name = "httptools" +version = "0.6.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a7/9a/ce5e1f7e131522e6d3426e8e7a490b3a01f39a6696602e1c4f33f9e94277/httptools-0.6.4.tar.gz", hash = "sha256:4e93eee4add6493b59a5c514da98c939b244fce4a0d8879cd3f466562f4b7d5c", size = 240639 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/6f/972f8eb0ea7d98a1c6be436e2142d51ad2a64ee18e02b0e7ff1f62171ab1/httptools-0.6.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3c73ce323711a6ffb0d247dcd5a550b8babf0f757e86a52558fe5b86d6fefcc0", size = 198780 }, + { url = "https://files.pythonhosted.org/packages/6a/b0/17c672b4bc5c7ba7f201eada4e96c71d0a59fbc185e60e42580093a86f21/httptools-0.6.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:345c288418f0944a6fe67be8e6afa9262b18c7626c3ef3c28adc5eabc06a68da", size = 103297 }, + { url = "https://files.pythonhosted.org/packages/92/5e/b4a826fe91971a0b68e8c2bd4e7db3e7519882f5a8ccdb1194be2b3ab98f/httptools-0.6.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:deee0e3343f98ee8047e9f4c5bc7cedbf69f5734454a94c38ee829fb2d5fa3c1", size = 443130 }, + { url = "https://files.pythonhosted.org/packages/b0/51/ce61e531e40289a681a463e1258fa1e05e0be54540e40d91d065a264cd8f/httptools-0.6.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca80b7485c76f768a3bc83ea58373f8db7b015551117375e4918e2aa77ea9b50", size = 442148 }, + { url = "https://files.pythonhosted.org/packages/ea/9e/270b7d767849b0c96f275c695d27ca76c30671f8eb8cc1bab6ced5c5e1d0/httptools-0.6.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:90d96a385fa941283ebd231464045187a31ad932ebfa541be8edf5b3c2328959", size = 415949 }, + { url = "https://files.pythonhosted.org/packages/81/86/ced96e3179c48c6f656354e106934e65c8963d48b69be78f355797f0e1b3/httptools-0.6.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:59e724f8b332319e2875efd360e61ac07f33b492889284a3e05e6d13746876f4", size = 417591 }, + { url = "https://files.pythonhosted.org/packages/75/73/187a3f620ed3175364ddb56847d7a608a6fc42d551e133197098c0143eca/httptools-0.6.4-cp310-cp310-win_amd64.whl", hash = "sha256:c26f313951f6e26147833fc923f78f95604bbec812a43e5ee37f26dc9e5a686c", size = 88344 }, + { url = "https://files.pythonhosted.org/packages/7b/26/bb526d4d14c2774fe07113ca1db7255737ffbb119315839af2065abfdac3/httptools-0.6.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f47f8ed67cc0ff862b84a1189831d1d33c963fb3ce1ee0c65d3b0cbe7b711069", size = 199029 }, + { url = "https://files.pythonhosted.org/packages/a6/17/3e0d3e9b901c732987a45f4f94d4e2c62b89a041d93db89eafb262afd8d5/httptools-0.6.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0614154d5454c21b6410fdf5262b4a3ddb0f53f1e1721cfd59d55f32138c578a", size = 103492 }, + { url = "https://files.pythonhosted.org/packages/b7/24/0fe235d7b69c42423c7698d086d4db96475f9b50b6ad26a718ef27a0bce6/httptools-0.6.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8787367fbdfccae38e35abf7641dafc5310310a5987b689f4c32cc8cc3ee975", size = 462891 }, + { url = "https://files.pythonhosted.org/packages/b1/2f/205d1f2a190b72da6ffb5f41a3736c26d6fa7871101212b15e9b5cd8f61d/httptools-0.6.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40b0f7fe4fd38e6a507bdb751db0379df1e99120c65fbdc8ee6c1d044897a636", size = 459788 }, + { url = "https://files.pythonhosted.org/packages/6e/4c/d09ce0eff09057a206a74575ae8f1e1e2f0364d20e2442224f9e6612c8b9/httptools-0.6.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:40a5ec98d3f49904b9fe36827dcf1aadfef3b89e2bd05b0e35e94f97c2b14721", size = 433214 }, + { url = "https://files.pythonhosted.org/packages/3e/d2/84c9e23edbccc4a4c6f96a1b8d99dfd2350289e94f00e9ccc7aadde26fb5/httptools-0.6.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:dacdd3d10ea1b4ca9df97a0a303cbacafc04b5cd375fa98732678151643d4988", size = 434120 }, + { url = "https://files.pythonhosted.org/packages/d0/46/4d8e7ba9581416de1c425b8264e2cadd201eb709ec1584c381f3e98f51c1/httptools-0.6.4-cp311-cp311-win_amd64.whl", hash = "sha256:288cd628406cc53f9a541cfaf06041b4c71d751856bab45e3702191f931ccd17", size = 88565 }, + { url = "https://files.pythonhosted.org/packages/bb/0e/d0b71465c66b9185f90a091ab36389a7352985fe857e352801c39d6127c8/httptools-0.6.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:df017d6c780287d5c80601dafa31f17bddb170232d85c066604d8558683711a2", size = 200683 }, + { url = "https://files.pythonhosted.org/packages/e2/b8/412a9bb28d0a8988de3296e01efa0bd62068b33856cdda47fe1b5e890954/httptools-0.6.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:85071a1e8c2d051b507161f6c3e26155b5c790e4e28d7f236422dbacc2a9cc44", size = 104337 }, + { url = "https://files.pythonhosted.org/packages/9b/01/6fb20be3196ffdc8eeec4e653bc2a275eca7f36634c86302242c4fbb2760/httptools-0.6.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69422b7f458c5af875922cdb5bd586cc1f1033295aa9ff63ee196a87519ac8e1", size = 508796 }, + { url = "https://files.pythonhosted.org/packages/f7/d8/b644c44acc1368938317d76ac991c9bba1166311880bcc0ac297cb9d6bd7/httptools-0.6.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:16e603a3bff50db08cd578d54f07032ca1631450ceb972c2f834c2b860c28ea2", size = 510837 }, + { url = "https://files.pythonhosted.org/packages/52/d8/254d16a31d543073a0e57f1c329ca7378d8924e7e292eda72d0064987486/httptools-0.6.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ec4f178901fa1834d4a060320d2f3abc5c9e39766953d038f1458cb885f47e81", size = 485289 }, + { url = "https://files.pythonhosted.org/packages/5f/3c/4aee161b4b7a971660b8be71a92c24d6c64372c1ab3ae7f366b3680df20f/httptools-0.6.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f9eb89ecf8b290f2e293325c646a211ff1c2493222798bb80a530c5e7502494f", size = 489779 }, + { url = "https://files.pythonhosted.org/packages/12/b7/5cae71a8868e555f3f67a50ee7f673ce36eac970f029c0c5e9d584352961/httptools-0.6.4-cp312-cp312-win_amd64.whl", hash = "sha256:db78cb9ca56b59b016e64b6031eda5653be0589dba2b1b43453f6e8b405a0970", size = 88634 }, + { url = "https://files.pythonhosted.org/packages/94/a3/9fe9ad23fd35f7de6b91eeb60848986058bd8b5a5c1e256f5860a160cc3e/httptools-0.6.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ade273d7e767d5fae13fa637f4d53b6e961fb7fd93c7797562663f0171c26660", size = 197214 }, + { url = "https://files.pythonhosted.org/packages/ea/d9/82d5e68bab783b632023f2fa31db20bebb4e89dfc4d2293945fd68484ee4/httptools-0.6.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:856f4bc0478ae143bad54a4242fccb1f3f86a6e1be5548fecfd4102061b3a083", size = 102431 }, + { url = "https://files.pythonhosted.org/packages/96/c1/cb499655cbdbfb57b577734fde02f6fa0bbc3fe9fb4d87b742b512908dff/httptools-0.6.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:322d20ea9cdd1fa98bd6a74b77e2ec5b818abdc3d36695ab402a0de8ef2865a3", size = 473121 }, + { url = "https://files.pythonhosted.org/packages/af/71/ee32fd358f8a3bb199b03261f10921716990808a675d8160b5383487a317/httptools-0.6.4-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4d87b29bd4486c0093fc64dea80231f7c7f7eb4dc70ae394d70a495ab8436071", size = 473805 }, + { url = "https://files.pythonhosted.org/packages/8a/0a/0d4df132bfca1507114198b766f1737d57580c9ad1cf93c1ff673e3387be/httptools-0.6.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:342dd6946aa6bda4b8f18c734576106b8a31f2fe31492881a9a160ec84ff4bd5", size = 448858 }, + { url = "https://files.pythonhosted.org/packages/1e/6a/787004fdef2cabea27bad1073bf6a33f2437b4dbd3b6fb4a9d71172b1c7c/httptools-0.6.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b36913ba52008249223042dca46e69967985fb4051951f94357ea681e1f5dc0", size = 452042 }, + { url = "https://files.pythonhosted.org/packages/4d/dc/7decab5c404d1d2cdc1bb330b1bf70e83d6af0396fd4fc76fc60c0d522bf/httptools-0.6.4-cp313-cp313-win_amd64.whl", hash = "sha256:28908df1b9bb8187393d5b5db91435ccc9c8e891657f9cbb42a2541b44c82fc8", size = 87682 }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517 }, +] + +[package.optional-dependencies] +http2 = [ + { name = "h2" }, +] + +[[package]] +name = "httpx-sse" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4c/60/8f4281fa9bbf3c8034fd54c0e7412e66edbab6bc74c4996bd616f8d0406e/httpx-sse-0.4.0.tar.gz", hash = "sha256:1e81a3a3070ce322add1d3529ed42eb5f70817f45ed6ec915ab753f961139721", size = 12624 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/9b/a181f281f65d776426002f330c31849b86b31fc9d848db62e16f03ff739f/httpx_sse-0.4.0-py3-none-any.whl", hash = "sha256:f329af6eae57eaa2bdfd962b42524764af68075ea87370a2de920af5341e318f", size = 7819 }, +] + +[[package]] +name = "huggingface-hub" +version = "0.34.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "fsspec" }, + { name = "hf-xet", marker = "platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "tqdm" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/91/b4/e6b465eca5386b52cf23cb6df8644ad318a6b0e12b4b96a7e0be09cbfbcc/huggingface_hub-0.34.3.tar.gz", hash = "sha256:d58130fd5aa7408480681475491c0abd7e835442082fbc3ef4d45b6c39f83853", size = 456800 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/59/a8/4677014e771ed1591a87b63a2392ce6923baf807193deef302dcfde17542/huggingface_hub-0.34.3-py3-none-any.whl", hash = "sha256:5444550099e2d86e68b2898b09e85878fbd788fc2957b506c6a79ce060e39492", size = 558847 }, +] + +[[package]] +name = "humanfriendly" +version = "10.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyreadline3", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cc/3f/2c29224acb2e2df4d2046e4c73ee2662023c58ff5b113c4c1adac0886c43/humanfriendly-10.0.tar.gz", hash = "sha256:6b0b831ce8f15f7300721aa49829fc4e83921a9a301cc7f606be6686a2288ddc", size = 360702 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f0/0f/310fb31e39e2d734ccaa2c0fb981ee41f7bd5056ce9bc29b2248bd569169/humanfriendly-10.0-py2.py3-none-any.whl", hash = "sha256:1697e1a8a8f550fd43c2865cd84542fc175a61dcb779b6fee18cf6b6ccba1477", size = 86794 }, +] + +[[package]] +name = "hyperbrowser" +version = "0.53.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, + { name = "jsonref" }, + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/51/51/91f14a40593f97301470b669fefe3c4a5896553aa9d801854a6132e57cb8/hyperbrowser-0.53.0.tar.gz", hash = "sha256:d8b719f58901471ab1172afa8812151b3c0b120802ad50c1b3342fe3266d6f25", size = 25778 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/b4/d1296ba69279754ac95ec121d87631d3d2726947bdaed76b1d44546c2523/hyperbrowser-0.53.0-py3-none-any.whl", hash = "sha256:ae31ab4e54b1443aaf1899482bd4e0a38e69c368e982d29af20a1f49e67c53c8", size = 52468 }, +] + +[[package]] +name = "hyperframe" +version = "6.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/02/e7/94f8232d4a74cc99514c13a9f995811485a6903d48e5d952771ef6322e30/hyperframe-6.1.0.tar.gz", hash = "sha256:f630908a00854a7adeabd6382b43923a4c4cd4b821fcb527e6ab9e15382a3b08", size = 26566 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/30/47d0bf6072f7252e6521f3447ccfa40b421b6824517f82854703d0f5a98b/hyperframe-6.1.0-py3-none-any.whl", hash = "sha256:b03380493a519fce58ea5af42e4a42317bf9bd425596f7a0835ffce80f1a42e5", size = 13007 }, +] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, +] + +[[package]] +name = "ijson" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/4f/1cfeada63f5fce87536651268ddf5cca79b8b4bbb457aee4e45777964a0a/ijson-3.4.0.tar.gz", hash = "sha256:5f74dcbad9d592c428d3ca3957f7115a42689ee7ee941458860900236ae9bb13", size = 65782 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/6b/a247ba44004154aaa71f9e6bd9f05ba412f490cc4043618efb29314f035e/ijson-3.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e27e50f6dcdee648f704abc5d31b976cd2f90b4642ed447cf03296d138433d09", size = 87609 }, + { url = "https://files.pythonhosted.org/packages/3c/1d/8d2009d74373b7dec2a49b1167e396debb896501396c70a674bb9ccc41ff/ijson-3.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2a753be681ac930740a4af9c93cfb4edc49a167faed48061ea650dc5b0f406f1", size = 59243 }, + { url = "https://files.pythonhosted.org/packages/a7/b2/a85a21ebaba81f64a326c303a94625fb94b84890c52d9efdd8acb38b6312/ijson-3.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a07c47aed534e0ec198e6a2d4360b259d32ac654af59c015afc517ad7973b7fb", size = 59309 }, + { url = "https://files.pythonhosted.org/packages/b1/35/273dfa1f27c38eeaba105496ecb54532199f76c0120177b28315daf5aec3/ijson-3.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c55f48181e11c597cd7146fb31edc8058391201ead69f8f40d2ecbb0b3e4fc6", size = 131213 }, + { url = "https://files.pythonhosted.org/packages/4d/37/9d3bb0e200a103ca9f8e9315c4d96ecaca43a3c1957c1ac069ea9dc9c6ba/ijson-3.4.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abd5669f96f79d8a2dd5ae81cbd06770a4d42c435fd4a75c74ef28d9913b697d", size = 125456 }, + { url = "https://files.pythonhosted.org/packages/00/54/8f015c4df30200fd14435dec9c67bf675dff0fee44a16c084a8ec0f82922/ijson-3.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e3ddd46d16b8542c63b1b8af7006c758d4e21cc1b86122c15f8530fae773461", size = 130192 }, + { url = "https://files.pythonhosted.org/packages/88/01/46a0540ad3461332edcc689a8874fa13f0a4c00f60f02d155b70e36f5e0b/ijson-3.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:1504cec7fe04be2bb0cc33b50c9dd3f83f98c0540ad4991d4017373b7853cfe6", size = 132217 }, + { url = "https://files.pythonhosted.org/packages/d7/da/8f8df42f3fd7ef279e20eae294738eed62d41ed5b6a4baca5121abc7cf0f/ijson-3.4.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:2f2ff456adeb216603e25d7915f10584c1b958b6eafa60038d76d08fc8a5fb06", size = 127118 }, + { url = "https://files.pythonhosted.org/packages/82/0a/a410d9d3b082cc2ec9738d54935a589974cbe54c0f358e4d17465594d660/ijson-3.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0ab00d75d61613a125fbbb524551658b1ad6919a52271ca16563ca5bc2737bb1", size = 129808 }, + { url = "https://files.pythonhosted.org/packages/2e/c6/a3e2a446b8bd2cf91cb4ca7439f128d2b379b5a79794d0ea25e379b0f4f3/ijson-3.4.0-cp310-cp310-win32.whl", hash = "sha256:ada421fd59fe2bfa4cfa64ba39aeba3f0753696cdcd4d50396a85f38b1d12b01", size = 51160 }, + { url = "https://files.pythonhosted.org/packages/18/7c/e6620603df42d2ef8a92076eaa5cd2b905366e86e113adf49e7b79970bd3/ijson-3.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:8c75e82cec05d00ed3a4af5f4edf08f59d536ed1a86ac7e84044870872d82a33", size = 53710 }, + { url = "https://files.pythonhosted.org/packages/1a/0d/3e2998f4d7b7d2db2d511e4f0cf9127b6e2140c325c3cb77be46ae46ff1d/ijson-3.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9e369bf5a173ca51846c243002ad8025d32032532523b06510881ecc8723ee54", size = 87643 }, + { url = "https://files.pythonhosted.org/packages/e9/7b/afef2b08af2fee5ead65fcd972fadc3e31f9ae2b517fe2c378d50a9bf79b/ijson-3.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:26e7da0a3cd2a56a1fde1b34231867693f21c528b683856f6691e95f9f39caec", size = 59260 }, + { url = "https://files.pythonhosted.org/packages/da/4a/39f583a2a13096f5063028bb767622f09cafc9ec254c193deee6c80af59f/ijson-3.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1c28c7f604729be22aa453e604e9617b665fa0c24cd25f9f47a970e8130c571a", size = 59311 }, + { url = "https://files.pythonhosted.org/packages/3c/58/5b80efd54b093e479c98d14b31d7794267281f6a8729f2c94fbfab661029/ijson-3.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0bed8bcb84d3468940f97869da323ba09ae3e6b950df11dea9b62e2b231ca1e3", size = 136125 }, + { url = "https://files.pythonhosted.org/packages/e5/f5/f37659b1647ecc3992216277cd8a45e2194e84e8818178f77c99e1d18463/ijson-3.4.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:296bc824f4088f2af814aaf973b0435bc887ce3d9f517b1577cc4e7d1afb1cb7", size = 130699 }, + { url = "https://files.pythonhosted.org/packages/ee/2f/4c580ac4bb5eda059b672ad0a05e4bafdae5182a6ec6ab43546763dafa91/ijson-3.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8145f8f40617b6a8aa24e28559d0adc8b889e56a203725226a8a60fa3501073f", size = 134963 }, + { url = "https://files.pythonhosted.org/packages/6d/9e/64ec39718609faab6ed6e1ceb44f9c35d71210ad9c87fff477c03503e8f8/ijson-3.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b674a97bd503ea21bc85103e06b6493b1b2a12da3372950f53e1c664566a33a4", size = 137405 }, + { url = "https://files.pythonhosted.org/packages/71/b2/f0bf0e4a0962845597996de6de59c0078bc03a1f899e03908220039f4cf6/ijson-3.4.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8bc731cf1c3282b021d3407a601a5a327613da9ad3c4cecb1123232623ae1826", size = 131861 }, + { url = "https://files.pythonhosted.org/packages/17/83/4a2e3611e2b4842b413ec84d2e54adea55ab52e4408ea0f1b1b927e19536/ijson-3.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:42ace5e940e0cf58c9de72f688d6829ddd815096d07927ee7e77df2648006365", size = 134297 }, + { url = "https://files.pythonhosted.org/packages/38/75/2d332911ac765b44cd7da0cb2b06143521ad5e31dfcc8d8587e6e6168bc8/ijson-3.4.0-cp311-cp311-win32.whl", hash = "sha256:5be39a0df4cd3f02b304382ea8885391900ac62e95888af47525a287c50005e9", size = 51161 }, + { url = "https://files.pythonhosted.org/packages/7d/ba/4ad571f9f7fcf5906b26e757b130c1713c5f0198a1e59568f05d53a0816c/ijson-3.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:0b1be1781792291e70d2e177acf564ec672a7907ba74f313583bdf39fe81f9b7", size = 53710 }, + { url = "https://files.pythonhosted.org/packages/f8/ec/317ee5b2d13e50448833ead3aa906659a32b376191f6abc2a7c6112d2b27/ijson-3.4.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:956b148f88259a80a9027ffbe2d91705fae0c004fbfba3e5a24028fbe72311a9", size = 87212 }, + { url = "https://files.pythonhosted.org/packages/f8/43/b06c96ced30cacecc5d518f89b0fd1c98c294a30ff88848b70ed7b7f72a1/ijson-3.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:06b89960f5c721106394c7fba5760b3f67c515b8eb7d80f612388f5eca2f4621", size = 59175 }, + { url = "https://files.pythonhosted.org/packages/e9/df/b4aeafb7ecde463130840ee9be36130823ec94a00525049bf700883378b8/ijson-3.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9a0bb591cf250dd7e9dfab69d634745a7f3272d31cfe879f9156e0a081fd97ee", size = 59011 }, + { url = "https://files.pythonhosted.org/packages/e3/7c/a80b8e361641609507f62022089626d4b8067f0826f51e1c09e4ba86eba8/ijson-3.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:72e92de999977f4c6b660ffcf2b8d59604ccd531edcbfde05b642baf283e0de8", size = 146094 }, + { url = "https://files.pythonhosted.org/packages/01/44/fa416347b9a802e3646c6ff377fc3278bd7d6106e17beb339514b6a3184e/ijson-3.4.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9e9602157a5b869d44b6896e64f502c712a312fcde044c2e586fccb85d3e316e", size = 137903 }, + { url = "https://files.pythonhosted.org/packages/24/c6/41a9ad4d42df50ff6e70fdce79b034f09b914802737ebbdc141153d8d791/ijson-3.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1e83660edb931a425b7ff662eb49db1f10d30ca6d4d350e5630edbed098bc01", size = 148339 }, + { url = "https://files.pythonhosted.org/packages/5f/6f/7d01efda415b8502dce67e067ed9e8a124f53e763002c02207e542e1a2f1/ijson-3.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:49bf8eac1c7b7913073865a859c215488461f7591b4fa6a33c14b51cb73659d0", size = 149383 }, + { url = "https://files.pythonhosted.org/packages/95/6c/0d67024b9ecb57916c5e5ab0350251c9fe2f86dc9c8ca2b605c194bdad6a/ijson-3.4.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:160b09273cb42019f1811469508b0a057d19f26434d44752bde6f281da6d3f32", size = 141580 }, + { url = "https://files.pythonhosted.org/packages/06/43/e10edcc1c6a3b619294de835e7678bfb3a1b8a75955f3689fd66a1e9e7b4/ijson-3.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2019ff4e6f354aa00c76c8591bd450899111c61f2354ad55cc127e2ce2492c44", size = 150280 }, + { url = "https://files.pythonhosted.org/packages/07/84/1cbeee8e8190a1ebe6926569a92cf1fa80ddb380c129beb6f86559e1bb24/ijson-3.4.0-cp312-cp312-win32.whl", hash = "sha256:931c007bf6bb8330705429989b2deed6838c22b63358a330bf362b6e458ba0bf", size = 51512 }, + { url = "https://files.pythonhosted.org/packages/66/13/530802bc391c95be6fe9f96e9aa427d94067e7c0b7da7a9092344dc44c4b/ijson-3.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:71523f2b64cb856a820223e94d23e88369f193017ecc789bb4de198cc9d349eb", size = 54081 }, + { url = "https://files.pythonhosted.org/packages/77/b3/b1d2eb2745e5204ec7a25365a6deb7868576214feb5e109bce368fb692c9/ijson-3.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e8d96f88d75196a61c9d9443de2b72c2d4a7ba9456ff117b57ae3bba23a54256", size = 87216 }, + { url = "https://files.pythonhosted.org/packages/b1/cd/cd6d340087617f8cc9bedbb21d974542fe2f160ed0126b8288d3499a469b/ijson-3.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c45906ce2c1d3b62f15645476fc3a6ca279549127f01662a39ca5ed334a00cf9", size = 59170 }, + { url = "https://files.pythonhosted.org/packages/3e/4d/32d3a9903b488d3306e3c8288f6ee4217d2eea82728261db03a1045eb5d1/ijson-3.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4ab4bc2119b35c4363ea49f29563612237cae9413d2fbe54b223be098b97bc9e", size = 59013 }, + { url = "https://files.pythonhosted.org/packages/d5/c8/db15465ab4b0b477cee5964c8bfc94bf8c45af8e27a23e1ad78d1926e587/ijson-3.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:97b0a9b5a15e61dfb1f14921ea4e0dba39f3a650df6d8f444ddbc2b19b479ff1", size = 146564 }, + { url = "https://files.pythonhosted.org/packages/c4/d8/0755545bc122473a9a434ab90e0f378780e603d75495b1ca3872de757873/ijson-3.4.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e3047bb994dabedf11de11076ed1147a307924b6e5e2df6784fb2599c4ad8c60", size = 137917 }, + { url = "https://files.pythonhosted.org/packages/d0/c6/aeb89c8939ebe3f534af26c8c88000c5e870dbb6ae33644c21a4531f87d2/ijson-3.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68c83161b052e9f5dc8191acbc862bb1e63f8a35344cb5cd0db1afd3afd487a6", size = 148897 }, + { url = "https://files.pythonhosted.org/packages/be/0e/7ef6e9b372106f2682a4a32b3c65bf86bb471a1670e4dac242faee4a7d3f/ijson-3.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1eebd9b6c20eb1dffde0ae1f0fbb4aeacec2eb7b89adb5c7c0449fc9fd742760", size = 149711 }, + { url = "https://files.pythonhosted.org/packages/d1/5d/9841c3ed75bcdabf19b3202de5f862a9c9c86ce5c7c9d95fa32347fdbf5f/ijson-3.4.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:13fb6d5c35192c541421f3ee81239d91fc15a8d8f26c869250f941f4b346a86c", size = 141691 }, + { url = "https://files.pythonhosted.org/packages/d5/d2/ce74e17218dba292e9be10a44ed0c75439f7958cdd263adb0b5b92d012d5/ijson-3.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:28b7196ff7b37c4897c547a28fa4876919696739fc91c1f347651c9736877c69", size = 150738 }, + { url = "https://files.pythonhosted.org/packages/4e/43/dcc480f94453b1075c9911d4755b823f3ace275761bb37b40139f22109ca/ijson-3.4.0-cp313-cp313-win32.whl", hash = "sha256:3c2691d2da42629522140f77b99587d6f5010440d58d36616f33bc7bdc830cc3", size = 51512 }, + { url = "https://files.pythonhosted.org/packages/35/dd/d8c5f15efd85ba51e6e11451ebe23d779361a9ec0d192064c2a8c3cdfcb8/ijson-3.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:c4554718c275a044c47eb3874f78f2c939f300215d9031e785a6711cc51b83fc", size = 54074 }, + { url = "https://files.pythonhosted.org/packages/79/73/24ad8cd106203419c4d22bed627e02e281d66b83e91bc206a371893d0486/ijson-3.4.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:915a65e3f3c0eee2ea937bc62aaedb6c14cc1e8f0bb9f3f4fb5a9e2bbfa4b480", size = 91694 }, + { url = "https://files.pythonhosted.org/packages/17/2d/f7f680984bcb7324a46a4c2df3bd73cf70faef0acfeb85a3f811abdfd590/ijson-3.4.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:afbe9748707684b6c5adc295c4fdcf27765b300aec4d484e14a13dca4e5c0afa", size = 61390 }, + { url = "https://files.pythonhosted.org/packages/09/a1/f3ca7bab86f95bdb82494739e71d271410dfefce4590785d511669127145/ijson-3.4.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d823f8f321b4d8d5fa020d0a84f089fec5d52b7c0762430476d9f8bf95bbc1a9", size = 61140 }, + { url = "https://files.pythonhosted.org/packages/51/79/dd340df3d4fc7771c95df29997956b92ed0570fe7b616d1792fea9ad93f2/ijson-3.4.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b8a0a2c54f3becf76881188beefd98b484b1d3bd005769a740d5b433b089fa23", size = 214739 }, + { url = "https://files.pythonhosted.org/packages/59/f0/85380b7f51d1f5fb7065d76a7b623e02feca920cc678d329b2eccc0011e0/ijson-3.4.0-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ced19a83ab09afa16257a0b15bc1aa888dbc555cb754be09d375c7f8d41051f2", size = 198338 }, + { url = "https://files.pythonhosted.org/packages/a5/cd/313264cf2ec42e0f01d198c49deb7b6fadeb793b3685e20e738eb6b3fa13/ijson-3.4.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8100f9885eff1f38d35cef80ef759a1bbf5fc946349afa681bd7d0e681b7f1a0", size = 207515 }, + { url = "https://files.pythonhosted.org/packages/12/94/bf14457aa87ea32641f2db577c9188ef4e4ae373478afef422b31fc7f309/ijson-3.4.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d7bcc3f7f21b0f703031ecd15209b1284ea51b2a329d66074b5261de3916c1eb", size = 210081 }, + { url = "https://files.pythonhosted.org/packages/7d/b4/eaee39e290e40e52d665db9bd1492cfdce86bd1e47948e0440db209c6023/ijson-3.4.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2dcb190227b09dd171bdcbfe4720fddd574933c66314818dfb3960c8a6246a77", size = 199253 }, + { url = "https://files.pythonhosted.org/packages/c5/9c/e09c7b9ac720a703ab115b221b819f149ed54c974edfff623c1e925e57da/ijson-3.4.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:eda4cfb1d49c6073a901735aaa62e39cb7ab47f3ad7bb184862562f776f1fa8a", size = 203816 }, + { url = "https://files.pythonhosted.org/packages/7c/14/acd304f412e32d16a2c12182b9d78206bb0ae35354d35664f45db05c1b3b/ijson-3.4.0-cp313-cp313t-win32.whl", hash = "sha256:0772638efa1f3b72b51736833404f1cbd2f5beeb9c1a3d392e7d385b9160cba7", size = 53760 }, + { url = "https://files.pythonhosted.org/packages/2f/24/93dd0a467191590a5ed1fc2b35842bca9d09900d001e00b0b497c0208ef6/ijson-3.4.0-cp313-cp313t-win_amd64.whl", hash = "sha256:3d8a0d67f36e4fb97c61a724456ef0791504b16ce6f74917a31c2e92309bbeb9", size = 56948 }, + { url = "https://files.pythonhosted.org/packages/a7/22/da919f16ca9254f8a9ea0ba482d2c1d012ce6e4c712dcafd8adb16b16c63/ijson-3.4.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:54e989c35dba9cf163d532c14bcf0c260897d5f465643f0cd1fba9c908bed7ef", size = 56480 }, + { url = "https://files.pythonhosted.org/packages/6d/54/c2afd289e034d11c4909f4ea90c9dae55053bed358064f310c3dd5033657/ijson-3.4.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:494eeb8e87afef22fbb969a4cb81ac2c535f30406f334fb6136e9117b0bb5380", size = 55956 }, + { url = "https://files.pythonhosted.org/packages/43/d6/18799b0fca9ecb8a47e22527eedcea3267e95d4567b564ef21d0299e2d12/ijson-3.4.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:81603de95de1688958af65cd2294881a4790edae7de540b70c65c8253c5dc44a", size = 69394 }, + { url = "https://files.pythonhosted.org/packages/c2/d6/c58032c69e9e977bf6d954f22cad0cd52092db89c454ea98926744523665/ijson-3.4.0-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8524be12c1773e1be466034cc49c1ecbe3d5b47bb86217bd2a57f73f970a6c19", size = 70378 }, + { url = "https://files.pythonhosted.org/packages/da/03/07c6840454d5d228bb5b4509c9a7ac5b9c0b8258e2b317a53f97372be1eb/ijson-3.4.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17994696ec895d05e0cfa21b11c68c920c82634b4a3d8b8a1455d6fe9fdee8f7", size = 67770 }, + { url = "https://files.pythonhosted.org/packages/32/c7/da58a9840380308df574dfdb0276c9d802b12f6125f999e92bcef36db552/ijson-3.4.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:0b67727aaee55d43b2e82b6a866c3cbcb2b66a5e9894212190cbd8773d0d9857", size = 53858 }, + { url = "https://files.pythonhosted.org/packages/a3/9b/0bc0594d357600c03c3b5a3a34043d764fc3ad3f0757d2f3aae5b28f6c1c/ijson-3.4.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:cdc8c5ca0eec789ed99db29c68012dda05027af0860bb360afd28d825238d69d", size = 56483 }, + { url = "https://files.pythonhosted.org/packages/00/1f/506cf2574673da1adcc8a794ebb85bf857cabe6294523978637e646814de/ijson-3.4.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:8e6b44b6ec45d5b1a0ee9d97e0e65ab7f62258727004cbbe202bf5f198bc21f7", size = 55957 }, + { url = "https://files.pythonhosted.org/packages/dc/3d/a7cd8d8a6de0f3084fe4d457a8f76176e11b013867d1cad16c67d25e8bec/ijson-3.4.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b51e239e4cb537929796e840d349fc731fdc0d58b1a0683ce5465ad725321e0f", size = 69394 }, + { url = "https://files.pythonhosted.org/packages/32/51/aa30abc02aabfc41c95887acf5f1f88da569642d7197fbe5aa105545226d/ijson-3.4.0-pp311-pypy311_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed05d43ec02be8ddb1ab59579761f6656b25d241a77fd74f4f0f7ec09074318a", size = 70377 }, + { url = "https://files.pythonhosted.org/packages/c7/37/7773659b8d8d98b34234e1237352f6b446a3c12941619686c7d4a8a5c69c/ijson-3.4.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cfeca1aaa59d93fd0a3718cbe5f7ef0effff85cf837e0bceb71831a47f39cc14", size = 67767 }, + { url = "https://files.pythonhosted.org/packages/cd/1f/dd52a84ed140e31a5d226cd47d98d21aa559aead35ef7bae479eab4c494c/ijson-3.4.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:7ca72ca12e9a1dd4252c97d952be34282907f263f7e28fcdff3a01b83981e837", size = 53864 }, +] + +[[package]] +name = "importlib-metadata" +version = "8.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/66/650a33bd90f786193e4de4b3ad86ea60b53c89b669a5c7be931fac31cdb0/importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000", size = 56641 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/b0/36bd937216ec521246249be3bf9855081de4c5e06a0c9b4219dbeda50373/importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd", size = 27656 }, +] + +[[package]] +name = "importlib-resources" +version = "6.5.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cf/8c/f834fbf984f691b4f7ff60f50b514cc3de5cc08abfc3295564dd89c5e2e7/importlib_resources-6.5.2.tar.gz", hash = "sha256:185f87adef5bcc288449d98fb4fba07cea78bc036455dd44c5fc4a2fe78fed2c", size = 44693 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/ed/1f1afb2e9e7f38a545d628f864d562a5ae64fe6f7a10e28ffb9b185b4e89/importlib_resources-6.5.2-py3-none-any.whl", hash = "sha256:789cfdc3ed28c78b67a06acb8126751ced69a3d5f79c095a98298cd8a760ccec", size = 37461 }, +] + +[[package]] +name = "inflection" +version = "0.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e1/7e/691d061b7329bc8d54edbf0ec22fbfb2afe61facb681f9aaa9bff7a27d04/inflection-0.5.1.tar.gz", hash = "sha256:1a29730d366e996aaacffb2f1f1cb9593dc38e2ddd30c91250c6dde09ea9b417", size = 15091 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/59/91/aa6bde563e0085a02a435aa99b49ef75b0a4b062635e606dab23ce18d720/inflection-0.5.1-py2.py3-none-any.whl", hash = "sha256:f38b2b640938a4f35ade69ac3d053042959b62a0f1076a5bbaa1b9526605a8a2", size = 9454 }, +] + +[[package]] +name = "iniconfig" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050 }, +] + +[[package]] +name = "instructor" +version = "1.10.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, + { name = "diskcache" }, + { name = "docstring-parser" }, + { name = "jinja2" }, + { name = "jiter" }, + { name = "openai" }, + { name = "pydantic" }, + { name = "pydantic-core" }, + { name = "requests" }, + { name = "rich" }, + { name = "tenacity" }, + { name = "typer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a5/67/63c4b4d2cc3c7b4238920ad3388a6f5d67265ab7c09ee34012d6b591130e/instructor-1.10.0.tar.gz", hash = "sha256:887d33e058b913290dbf526b0096b1bb8d7ea1a07d75afecbf716161f959697b", size = 69388981 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/fb/ffc1ade9779795a8dc8e2379b1bfb522161ee7df8df12722f50d348fb4ea/instructor-1.10.0-py3-none-any.whl", hash = "sha256:9c789f0fce915d5498059afb5314530c8a5b22b0283302679148ddae98f732b0", size = 119455 }, +] + +[[package]] +name = "invoke" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/42/127e6d792884ab860defc3f4d80a8f9812e48ace584ffc5a346de58cdc6c/invoke-2.2.0.tar.gz", hash = "sha256:ee6cbb101af1a859c7fe84f2a264c059020b0cb7fe3535f9424300ab568f6bd5", size = 299835 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/66/7f8c48009c72d73bc6bbe6eb87ac838d6a526146f7dab14af671121eb379/invoke-2.2.0-py3-none-any.whl", hash = "sha256:6ea924cc53d4f78e3d98bc436b08069a03077e6f85ad1ddaa8a116d7dad15820", size = 160274 }, +] + +[[package]] +name = "ipython" +version = "8.37.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11' and platform_python_implementation == 'PyPy' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and platform_python_implementation == 'PyPy' and sys_platform == 'linux'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and platform_python_implementation == 'PyPy' and sys_platform == 'linux') or (python_full_version < '3.11' and platform_python_implementation == 'PyPy' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version < '3.11' and platform_python_implementation != 'PyPy' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and platform_python_implementation != 'PyPy' and sys_platform == 'linux'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and platform_python_implementation != 'PyPy' and sys_platform == 'linux') or (python_full_version < '3.11' and platform_python_implementation != 'PyPy' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "colorama", marker = "python_full_version < '3.11' and sys_platform == 'win32'" }, + { name = "decorator", marker = "python_full_version < '3.11'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "jedi", marker = "python_full_version < '3.11'" }, + { name = "matplotlib-inline", marker = "python_full_version < '3.11'" }, + { name = "pexpect", marker = "python_full_version < '3.11' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "prompt-toolkit", marker = "python_full_version < '3.11'" }, + { name = "pygments", marker = "python_full_version < '3.11'" }, + { name = "stack-data", marker = "python_full_version < '3.11'" }, + { name = "traitlets", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/85/31/10ac88f3357fc276dc8a64e8880c82e80e7459326ae1d0a211b40abf6665/ipython-8.37.0.tar.gz", hash = "sha256:ca815841e1a41a1e6b73a0b08f3038af9b2252564d01fc405356d34033012216", size = 5606088 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/d0/274fbf7b0b12643cbbc001ce13e6a5b1607ac4929d1b11c72460152c9fc3/ipython-8.37.0-py3-none-any.whl", hash = "sha256:ed87326596b878932dbcb171e3e698845434d8c61b8d8cd474bf663041a9dcf2", size = 831864 }, +] + +[[package]] +name = "ipython" +version = "9.4.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and platform_python_implementation == 'PyPy' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and platform_python_implementation == 'PyPy' and sys_platform == 'linux'", + "(python_full_version >= '3.13' and platform_machine != 'aarch64' and platform_python_implementation == 'PyPy' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_python_implementation == 'PyPy' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version >= '3.13' and platform_python_implementation != 'PyPy' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and platform_python_implementation != 'PyPy' and sys_platform == 'linux'", + "(python_full_version >= '3.13' and platform_machine != 'aarch64' and platform_python_implementation != 'PyPy' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_python_implementation != 'PyPy' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version >= '3.12.4' and python_full_version < '3.13' and platform_python_implementation == 'PyPy' and sys_platform == 'darwin'", + "python_full_version >= '3.12.4' and python_full_version < '3.13' and platform_machine == 'aarch64' and platform_python_implementation == 'PyPy' and sys_platform == 'linux'", + "(python_full_version >= '3.12.4' and python_full_version < '3.13' and platform_machine != 'aarch64' and platform_python_implementation == 'PyPy' and sys_platform == 'linux') or (python_full_version >= '3.12.4' and python_full_version < '3.13' and platform_python_implementation == 'PyPy' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version >= '3.12.4' and python_full_version < '3.13' and platform_python_implementation != 'PyPy' and sys_platform == 'darwin'", + "python_full_version >= '3.12.4' and python_full_version < '3.13' and platform_machine == 'aarch64' and platform_python_implementation != 'PyPy' and sys_platform == 'linux'", + "(python_full_version >= '3.12.4' and python_full_version < '3.13' and platform_machine != 'aarch64' and platform_python_implementation != 'PyPy' and sys_platform == 'linux') or (python_full_version >= '3.12.4' and python_full_version < '3.13' and platform_python_implementation != 'PyPy' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version >= '3.12' and python_full_version < '3.12.4' and platform_python_implementation == 'PyPy' and sys_platform == 'darwin'", + "python_full_version >= '3.12' and python_full_version < '3.12.4' and platform_machine == 'aarch64' and platform_python_implementation == 'PyPy' and sys_platform == 'linux'", + "(python_full_version >= '3.12' and python_full_version < '3.12.4' and platform_machine != 'aarch64' and platform_python_implementation == 'PyPy' and sys_platform == 'linux') or (python_full_version >= '3.12' and python_full_version < '3.12.4' and platform_python_implementation == 'PyPy' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version >= '3.12' and python_full_version < '3.12.4' and platform_python_implementation != 'PyPy' and sys_platform == 'darwin'", + "python_full_version >= '3.12' and python_full_version < '3.12.4' and platform_machine == 'aarch64' and platform_python_implementation != 'PyPy' and sys_platform == 'linux'", + "(python_full_version >= '3.12' and python_full_version < '3.12.4' and platform_machine != 'aarch64' and platform_python_implementation != 'PyPy' and sys_platform == 'linux') or (python_full_version >= '3.12' and python_full_version < '3.12.4' and platform_python_implementation != 'PyPy' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version == '3.11.*' and platform_python_implementation == 'PyPy' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and platform_python_implementation == 'PyPy' and sys_platform == 'linux'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and platform_python_implementation == 'PyPy' and sys_platform == 'linux') or (python_full_version == '3.11.*' and platform_python_implementation == 'PyPy' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version == '3.11.*' and platform_python_implementation != 'PyPy' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and platform_python_implementation != 'PyPy' and sys_platform == 'linux'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and platform_python_implementation != 'PyPy' and sys_platform == 'linux') or (python_full_version == '3.11.*' and platform_python_implementation != 'PyPy' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "colorama", marker = "python_full_version >= '3.11' and sys_platform == 'win32'" }, + { name = "decorator", marker = "python_full_version >= '3.11'" }, + { name = "ipython-pygments-lexers", marker = "python_full_version >= '3.11'" }, + { name = "jedi", marker = "python_full_version >= '3.11'" }, + { name = "matplotlib-inline", marker = "python_full_version >= '3.11'" }, + { name = "pexpect", marker = "python_full_version >= '3.11' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "prompt-toolkit", marker = "python_full_version >= '3.11'" }, + { name = "pygments", marker = "python_full_version >= '3.11'" }, + { name = "stack-data", marker = "python_full_version >= '3.11'" }, + { name = "traitlets", marker = "python_full_version >= '3.11'" }, + { name = "typing-extensions", marker = "python_full_version == '3.11.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/54/80/406f9e3bde1c1fd9bf5a0be9d090f8ae623e401b7670d8f6fdf2ab679891/ipython-9.4.0.tar.gz", hash = "sha256:c033c6d4e7914c3d9768aabe76bbe87ba1dc66a92a05db6bfa1125d81f2ee270", size = 4385338 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/f8/0031ee2b906a15a33d6bfc12dd09c3dfa966b3cb5b284ecfb7549e6ac3c4/ipython-9.4.0-py3-none-any.whl", hash = "sha256:25850f025a446d9b359e8d296ba175a36aedd32e83ca9b5060430fe16801f066", size = 611021 }, +] + +[[package]] +name = "ipython-pygments-lexers" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pygments", marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ef/4c/5dd1d8af08107f88c7f741ead7a40854b8ac24ddf9ae850afbcf698aa552/ipython_pygments_lexers-1.1.1.tar.gz", hash = "sha256:09c0138009e56b6854f9535736f4171d855c8c08a563a0dcd8022f78355c7e81", size = 8393 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl", hash = "sha256:a9462224a505ade19a605f71f8fa63c2048833ce50abc86768a0d81d876dc81c", size = 8074 }, +] + +[[package]] +name = "jedi" +version = "0.19.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "parso" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/3a/79a912fbd4d8dd6fbb02bf69afd3bb72cf0c729bb3063c6f4498603db17a/jedi-0.19.2.tar.gz", hash = "sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0", size = 1231287 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9", size = 1572278 }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899 }, +] + +[[package]] +name = "jiter" +version = "0.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/9d/ae7ddb4b8ab3fb1b51faf4deb36cb48a4fbbd7cb36bad6a5fca4741306f7/jiter-0.10.0.tar.gz", hash = "sha256:07a7142c38aacc85194391108dc91b5b57093c978a9932bd86a36862759d9500", size = 162759 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/7e/4011b5c77bec97cb2b572f566220364e3e21b51c48c5bd9c4a9c26b41b67/jiter-0.10.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:cd2fb72b02478f06a900a5782de2ef47e0396b3e1f7d5aba30daeb1fce66f303", size = 317215 }, + { url = "https://files.pythonhosted.org/packages/8a/4f/144c1b57c39692efc7ea7d8e247acf28e47d0912800b34d0ad815f6b2824/jiter-0.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:32bb468e3af278f095d3fa5b90314728a6916d89ba3d0ffb726dd9bf7367285e", size = 322814 }, + { url = "https://files.pythonhosted.org/packages/63/1f/db977336d332a9406c0b1f0b82be6f71f72526a806cbb2281baf201d38e3/jiter-0.10.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa8b3e0068c26ddedc7abc6fac37da2d0af16b921e288a5a613f4b86f050354f", size = 345237 }, + { url = "https://files.pythonhosted.org/packages/d7/1c/aa30a4a775e8a672ad7f21532bdbfb269f0706b39c6ff14e1f86bdd9e5ff/jiter-0.10.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:286299b74cc49e25cd42eea19b72aa82c515d2f2ee12d11392c56d8701f52224", size = 370999 }, + { url = "https://files.pythonhosted.org/packages/35/df/f8257abc4207830cb18880781b5f5b716bad5b2a22fb4330cfd357407c5b/jiter-0.10.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6ed5649ceeaeffc28d87fb012d25a4cd356dcd53eff5acff1f0466b831dda2a7", size = 491109 }, + { url = "https://files.pythonhosted.org/packages/06/76/9e1516fd7b4278aa13a2cc7f159e56befbea9aa65c71586305e7afa8b0b3/jiter-0.10.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2ab0051160cb758a70716448908ef14ad476c3774bd03ddce075f3c1f90a3d6", size = 388608 }, + { url = "https://files.pythonhosted.org/packages/6d/64/67750672b4354ca20ca18d3d1ccf2c62a072e8a2d452ac3cf8ced73571ef/jiter-0.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:03997d2f37f6b67d2f5c475da4412be584e1cec273c1cfc03d642c46db43f8cf", size = 352454 }, + { url = "https://files.pythonhosted.org/packages/96/4d/5c4e36d48f169a54b53a305114be3efa2bbffd33b648cd1478a688f639c1/jiter-0.10.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c404a99352d839fed80d6afd6c1d66071f3bacaaa5c4268983fc10f769112e90", size = 391833 }, + { url = "https://files.pythonhosted.org/packages/0b/de/ce4a6166a78810bd83763d2fa13f85f73cbd3743a325469a4a9289af6dae/jiter-0.10.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:66e989410b6666d3ddb27a74c7e50d0829704ede652fd4c858e91f8d64b403d0", size = 523646 }, + { url = "https://files.pythonhosted.org/packages/a2/a6/3bc9acce53466972964cf4ad85efecb94f9244539ab6da1107f7aed82934/jiter-0.10.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b532d3af9ef4f6374609a3bcb5e05a1951d3bf6190dc6b176fdb277c9bbf15ee", size = 514735 }, + { url = "https://files.pythonhosted.org/packages/b4/d8/243c2ab8426a2a4dea85ba2a2ba43df379ccece2145320dfd4799b9633c5/jiter-0.10.0-cp310-cp310-win32.whl", hash = "sha256:da9be20b333970e28b72edc4dff63d4fec3398e05770fb3205f7fb460eb48dd4", size = 210747 }, + { url = "https://files.pythonhosted.org/packages/37/7a/8021bd615ef7788b98fc76ff533eaac846322c170e93cbffa01979197a45/jiter-0.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:f59e533afed0c5b0ac3eba20d2548c4a550336d8282ee69eb07b37ea526ee4e5", size = 207484 }, + { url = "https://files.pythonhosted.org/packages/1b/dd/6cefc6bd68b1c3c979cecfa7029ab582b57690a31cd2f346c4d0ce7951b6/jiter-0.10.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:3bebe0c558e19902c96e99217e0b8e8b17d570906e72ed8a87170bc290b1e978", size = 317473 }, + { url = "https://files.pythonhosted.org/packages/be/cf/fc33f5159ce132be1d8dd57251a1ec7a631c7df4bd11e1cd198308c6ae32/jiter-0.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:558cc7e44fd8e507a236bee6a02fa17199ba752874400a0ca6cd6e2196cdb7dc", size = 321971 }, + { url = "https://files.pythonhosted.org/packages/68/a4/da3f150cf1d51f6c472616fb7650429c7ce053e0c962b41b68557fdf6379/jiter-0.10.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4d613e4b379a07d7c8453c5712ce7014e86c6ac93d990a0b8e7377e18505e98d", size = 345574 }, + { url = "https://files.pythonhosted.org/packages/84/34/6e8d412e60ff06b186040e77da5f83bc158e9735759fcae65b37d681f28b/jiter-0.10.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f62cf8ba0618eda841b9bf61797f21c5ebd15a7a1e19daab76e4e4b498d515b2", size = 371028 }, + { url = "https://files.pythonhosted.org/packages/fb/d9/9ee86173aae4576c35a2f50ae930d2ccb4c4c236f6cb9353267aa1d626b7/jiter-0.10.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:919d139cdfa8ae8945112398511cb7fca58a77382617d279556b344867a37e61", size = 491083 }, + { url = "https://files.pythonhosted.org/packages/d9/2c/f955de55e74771493ac9e188b0f731524c6a995dffdcb8c255b89c6fb74b/jiter-0.10.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:13ddbc6ae311175a3b03bd8994881bc4635c923754932918e18da841632349db", size = 388821 }, + { url = "https://files.pythonhosted.org/packages/81/5a/0e73541b6edd3f4aada586c24e50626c7815c561a7ba337d6a7eb0a915b4/jiter-0.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c440ea003ad10927a30521a9062ce10b5479592e8a70da27f21eeb457b4a9c5", size = 352174 }, + { url = "https://files.pythonhosted.org/packages/1c/c0/61eeec33b8c75b31cae42be14d44f9e6fe3ac15a4e58010256ac3abf3638/jiter-0.10.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:dc347c87944983481e138dea467c0551080c86b9d21de6ea9306efb12ca8f606", size = 391869 }, + { url = "https://files.pythonhosted.org/packages/41/22/5beb5ee4ad4ef7d86f5ea5b4509f680a20706c4a7659e74344777efb7739/jiter-0.10.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:13252b58c1f4d8c5b63ab103c03d909e8e1e7842d302473f482915d95fefd605", size = 523741 }, + { url = "https://files.pythonhosted.org/packages/ea/10/768e8818538e5817c637b0df52e54366ec4cebc3346108a4457ea7a98f32/jiter-0.10.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7d1bbf3c465de4a24ab12fb7766a0003f6f9bce48b8b6a886158c4d569452dc5", size = 514527 }, + { url = "https://files.pythonhosted.org/packages/73/6d/29b7c2dc76ce93cbedabfd842fc9096d01a0550c52692dfc33d3cc889815/jiter-0.10.0-cp311-cp311-win32.whl", hash = "sha256:db16e4848b7e826edca4ccdd5b145939758dadf0dc06e7007ad0e9cfb5928ae7", size = 210765 }, + { url = "https://files.pythonhosted.org/packages/c2/c9/d394706deb4c660137caf13e33d05a031d734eb99c051142e039d8ceb794/jiter-0.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:9c9c1d5f10e18909e993f9641f12fe1c77b3e9b533ee94ffa970acc14ded3812", size = 209234 }, + { url = "https://files.pythonhosted.org/packages/6d/b5/348b3313c58f5fbfb2194eb4d07e46a35748ba6e5b3b3046143f3040bafa/jiter-0.10.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:1e274728e4a5345a6dde2d343c8da018b9d4bd4350f5a472fa91f66fda44911b", size = 312262 }, + { url = "https://files.pythonhosted.org/packages/9c/4a/6a2397096162b21645162825f058d1709a02965606e537e3304b02742e9b/jiter-0.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7202ae396446c988cb2a5feb33a543ab2165b786ac97f53b59aafb803fef0744", size = 320124 }, + { url = "https://files.pythonhosted.org/packages/2a/85/1ce02cade7516b726dd88f59a4ee46914bf79d1676d1228ef2002ed2f1c9/jiter-0.10.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23ba7722d6748b6920ed02a8f1726fb4b33e0fd2f3f621816a8b486c66410ab2", size = 345330 }, + { url = "https://files.pythonhosted.org/packages/75/d0/bb6b4f209a77190ce10ea8d7e50bf3725fc16d3372d0a9f11985a2b23eff/jiter-0.10.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:371eab43c0a288537d30e1f0b193bc4eca90439fc08a022dd83e5e07500ed026", size = 369670 }, + { url = "https://files.pythonhosted.org/packages/a0/f5/a61787da9b8847a601e6827fbc42ecb12be2c925ced3252c8ffcb56afcaf/jiter-0.10.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6c675736059020365cebc845a820214765162728b51ab1e03a1b7b3abb70f74c", size = 489057 }, + { url = "https://files.pythonhosted.org/packages/12/e4/6f906272810a7b21406c760a53aadbe52e99ee070fc5c0cb191e316de30b/jiter-0.10.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0c5867d40ab716e4684858e4887489685968a47e3ba222e44cde6e4a2154f959", size = 389372 }, + { url = "https://files.pythonhosted.org/packages/e2/ba/77013b0b8ba904bf3762f11e0129b8928bff7f978a81838dfcc958ad5728/jiter-0.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:395bb9a26111b60141757d874d27fdea01b17e8fac958b91c20128ba8f4acc8a", size = 352038 }, + { url = "https://files.pythonhosted.org/packages/67/27/c62568e3ccb03368dbcc44a1ef3a423cb86778a4389e995125d3d1aaa0a4/jiter-0.10.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6842184aed5cdb07e0c7e20e5bdcfafe33515ee1741a6835353bb45fe5d1bd95", size = 391538 }, + { url = "https://files.pythonhosted.org/packages/c0/72/0d6b7e31fc17a8fdce76164884edef0698ba556b8eb0af9546ae1a06b91d/jiter-0.10.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:62755d1bcea9876770d4df713d82606c8c1a3dca88ff39046b85a048566d56ea", size = 523557 }, + { url = "https://files.pythonhosted.org/packages/2f/09/bc1661fbbcbeb6244bd2904ff3a06f340aa77a2b94e5a7373fd165960ea3/jiter-0.10.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:533efbce2cacec78d5ba73a41756beff8431dfa1694b6346ce7af3a12c42202b", size = 514202 }, + { url = "https://files.pythonhosted.org/packages/1b/84/5a5d5400e9d4d54b8004c9673bbe4403928a00d28529ff35b19e9d176b19/jiter-0.10.0-cp312-cp312-win32.whl", hash = "sha256:8be921f0cadd245e981b964dfbcd6fd4bc4e254cdc069490416dd7a2632ecc01", size = 211781 }, + { url = "https://files.pythonhosted.org/packages/9b/52/7ec47455e26f2d6e5f2ea4951a0652c06e5b995c291f723973ae9e724a65/jiter-0.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:a7c7d785ae9dda68c2678532a5a1581347e9c15362ae9f6e68f3fdbfb64f2e49", size = 206176 }, + { url = "https://files.pythonhosted.org/packages/2e/b0/279597e7a270e8d22623fea6c5d4eeac328e7d95c236ed51a2b884c54f70/jiter-0.10.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:e0588107ec8e11b6f5ef0e0d656fb2803ac6cf94a96b2b9fc675c0e3ab5e8644", size = 311617 }, + { url = "https://files.pythonhosted.org/packages/91/e3/0916334936f356d605f54cc164af4060e3e7094364add445a3bc79335d46/jiter-0.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cafc4628b616dc32530c20ee53d71589816cf385dd9449633e910d596b1f5c8a", size = 318947 }, + { url = "https://files.pythonhosted.org/packages/6a/8e/fd94e8c02d0e94539b7d669a7ebbd2776e51f329bb2c84d4385e8063a2ad/jiter-0.10.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:520ef6d981172693786a49ff5b09eda72a42e539f14788124a07530f785c3ad6", size = 344618 }, + { url = "https://files.pythonhosted.org/packages/6f/b0/f9f0a2ec42c6e9c2e61c327824687f1e2415b767e1089c1d9135f43816bd/jiter-0.10.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:554dedfd05937f8fc45d17ebdf298fe7e0c77458232bcb73d9fbbf4c6455f5b3", size = 368829 }, + { url = "https://files.pythonhosted.org/packages/e8/57/5bbcd5331910595ad53b9fd0c610392ac68692176f05ae48d6ce5c852967/jiter-0.10.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5bc299da7789deacf95f64052d97f75c16d4fc8c4c214a22bf8d859a4288a1c2", size = 491034 }, + { url = "https://files.pythonhosted.org/packages/9b/be/c393df00e6e6e9e623a73551774449f2f23b6ec6a502a3297aeeece2c65a/jiter-0.10.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5161e201172de298a8a1baad95eb85db4fb90e902353b1f6a41d64ea64644e25", size = 388529 }, + { url = "https://files.pythonhosted.org/packages/42/3e/df2235c54d365434c7f150b986a6e35f41ebdc2f95acea3036d99613025d/jiter-0.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2e2227db6ba93cb3e2bf67c87e594adde0609f146344e8207e8730364db27041", size = 350671 }, + { url = "https://files.pythonhosted.org/packages/c6/77/71b0b24cbcc28f55ab4dbfe029f9a5b73aeadaba677843fc6dc9ed2b1d0a/jiter-0.10.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:15acb267ea5e2c64515574b06a8bf393fbfee6a50eb1673614aa45f4613c0cca", size = 390864 }, + { url = "https://files.pythonhosted.org/packages/6a/d3/ef774b6969b9b6178e1d1e7a89a3bd37d241f3d3ec5f8deb37bbd203714a/jiter-0.10.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:901b92f2e2947dc6dfcb52fd624453862e16665ea909a08398dde19c0731b7f4", size = 522989 }, + { url = "https://files.pythonhosted.org/packages/0c/41/9becdb1d8dd5d854142f45a9d71949ed7e87a8e312b0bede2de849388cb9/jiter-0.10.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:d0cb9a125d5a3ec971a094a845eadde2db0de85b33c9f13eb94a0c63d463879e", size = 513495 }, + { url = "https://files.pythonhosted.org/packages/9c/36/3468e5a18238bdedae7c4d19461265b5e9b8e288d3f86cd89d00cbb48686/jiter-0.10.0-cp313-cp313-win32.whl", hash = "sha256:48a403277ad1ee208fb930bdf91745e4d2d6e47253eedc96e2559d1e6527006d", size = 211289 }, + { url = "https://files.pythonhosted.org/packages/7e/07/1c96b623128bcb913706e294adb5f768fb7baf8db5e1338ce7b4ee8c78ef/jiter-0.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:75f9eb72ecb640619c29bf714e78c9c46c9c4eaafd644bf78577ede459f330d4", size = 205074 }, + { url = "https://files.pythonhosted.org/packages/54/46/caa2c1342655f57d8f0f2519774c6d67132205909c65e9aa8255e1d7b4f4/jiter-0.10.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:28ed2a4c05a1f32ef0e1d24c2611330219fed727dae01789f4a335617634b1ca", size = 318225 }, + { url = "https://files.pythonhosted.org/packages/43/84/c7d44c75767e18946219ba2d703a5a32ab37b0bc21886a97bc6062e4da42/jiter-0.10.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14a4c418b1ec86a195f1ca69da8b23e8926c752b685af665ce30777233dfe070", size = 350235 }, + { url = "https://files.pythonhosted.org/packages/01/16/f5a0135ccd968b480daad0e6ab34b0c7c5ba3bc447e5088152696140dcb3/jiter-0.10.0-cp313-cp313t-win_amd64.whl", hash = "sha256:d7bfed2fe1fe0e4dda6ef682cee888ba444b21e7a6553e03252e4feb6cf0adca", size = 207278 }, +] + +[[package]] +name = "jmespath" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/00/2a/e867e8531cf3e36b41201936b7fa7ba7b5702dbef42922193f05c8976cd6/jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe", size = 25843 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/31/b4/b9b800c45527aadd64d5b442f9b932b00648617eb5d63d2c7a6587b7cafc/jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980", size = 20256 }, +] + +[[package]] +name = "joblib" +version = "1.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/fe/0f5a938c54105553436dbff7a61dc4fed4b1b2c98852f8833beaf4d5968f/joblib-1.5.1.tar.gz", hash = "sha256:f4f86e351f39fe3d0d32a9f2c3d8af1ee4cec285aafcb27003dda5205576b444", size = 330475 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7d/4f/1195bbac8e0c2acc5f740661631d8d750dc38d4a32b23ee5df3cde6f4e0d/joblib-1.5.1-py3-none-any.whl", hash = "sha256:4719a31f054c7d766948dcd83e9613686b27114f190f717cec7eaa2084f8a74a", size = 307746 }, +] + +[[package]] +name = "json-repair" +version = "0.25.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/66/cb/50b0bbc3e504ef875aea0062cdc108077e4923fb8c1209c70c80dc043933/json_repair-0.25.2.tar.gz", hash = "sha256:161a56d7e6bbfd4cad3a614087e3e0dbd0e10d402dd20dc7db418432428cb32b", size = 20458 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/aa/43/ac6691c7b5aa7191c964a04ae926d2bb06d9297dba1f2287df5b85cb3715/json_repair-0.25.2-py3-none-any.whl", hash = "sha256:51d67295c3184b6c41a3572689661c6128cef6cfc9fb04db63130709adfc5bf0", size = 12740 }, +] + +[[package]] +name = "json5" +version = "0.12.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/12/be/c6c745ec4c4539b25a278b70e29793f10382947df0d9efba2fa09120895d/json5-0.12.0.tar.gz", hash = "sha256:0b4b6ff56801a1c7dc817b0241bca4ce474a0e6a163bfef3fc594d3fd263ff3a", size = 51907 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/9f/3500910d5a98549e3098807493851eeef2b89cdd3032227558a104dfe926/json5-0.12.0-py3-none-any.whl", hash = "sha256:6d37aa6c08b0609f16e1ec5ff94697e2cbbfbad5ac112afa05794da9ab7810db", size = 36079 }, +] + +[[package]] +name = "jsonpatch" +version = "1.33" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jsonpointer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/78/18813351fe5d63acad16aec57f94ec2b70a09e53ca98145589e185423873/jsonpatch-1.33.tar.gz", hash = "sha256:9fcd4009c41e6d12348b4a0ff2563ba56a2923a7dfee731d004e212e1ee5030c", size = 21699 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/07/02e16ed01e04a374e644b575638ec7987ae846d25ad97bcc9945a3ee4b0e/jsonpatch-1.33-py2.py3-none-any.whl", hash = "sha256:0ae28c0cd062bbd8b8ecc26d7d164fbbea9652a1a3693f3b956c1eae5145dade", size = 12898 }, +] + +[[package]] +name = "jsonpickle" +version = "4.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/a6/d07afcfdef402900229bcca795f80506b207af13a838d4d99ad45abf530c/jsonpickle-4.1.1.tar.gz", hash = "sha256:f86e18f13e2b96c1c1eede0b7b90095bbb61d99fedc14813c44dc2f361dbbae1", size = 316885 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/73/04df8a6fa66d43a9fd45c30f283cc4afff17da671886e451d52af60bdc7e/jsonpickle-4.1.1-py3-none-any.whl", hash = "sha256:bb141da6057898aa2438ff268362b126826c812a1721e31cf08a6e142910dc91", size = 47125 }, +] + +[[package]] +name = "jsonpointer" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6a/0a/eebeb1fa92507ea94016a2a790b93c2ae41a7e18778f85471dc54475ed25/jsonpointer-3.0.0.tar.gz", hash = "sha256:2b2d729f2091522d61c3b31f82e11870f60b68f43fbc705cb76bf4b832af59ef", size = 9114 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/92/5e77f98553e9e75130c78900d000368476aed74276eb8ae8796f65f00918/jsonpointer-3.0.0-py2.py3-none-any.whl", hash = "sha256:13e088adc14fca8b6aa8177c044e12701e6ad4b28ff10e65f2267a90109c9942", size = 7595 }, +] + +[[package]] +name = "jsonref" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/0d/c1f3277e90ccdb50d33ed5ba1ec5b3f0a242ed8c1b1a85d3afeb68464dca/jsonref-1.1.0.tar.gz", hash = "sha256:32fe8e1d85af0fdefbebce950af85590b22b60f9e95443176adbde4e1ecea552", size = 8814 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/ec/e1db9922bceb168197a558a2b8c03a7963f1afe93517ddd3cf99f202f996/jsonref-1.1.0-py3-none-any.whl", hash = "sha256:590dc7773df6c21cbf948b5dac07a72a251db28b0238ceecce0a2abfa8ec30a9", size = 9425 }, +] + +[[package]] +name = "jsonschema" +version = "4.25.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d5/00/a297a868e9d0784450faa7365c2172a7d6110c763e30ba861867c32ae6a9/jsonschema-4.25.0.tar.gz", hash = "sha256:e63acf5c11762c0e6672ffb61482bdf57f0876684d8d249c0fe2d730d48bc55f", size = 356830 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/54/c86cd8e011fe98803d7e382fd67c0df5ceab8d2b7ad8c5a81524f791551c/jsonschema-4.25.0-py3-none-any.whl", hash = "sha256:24c2e8da302de79c8b9382fee3e76b355e44d2a4364bb207159ce10b517bd716", size = 89184 }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2025.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bf/ce/46fbd9c8119cfc3581ee5643ea49464d168028cfb5caff5fc0596d0cf914/jsonschema_specifications-2025.4.1.tar.gz", hash = "sha256:630159c9f4dbea161a6a2205c3011cc4f18ff381b189fff48bb39b9bf26ae608", size = 15513 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/01/0e/b27cdbaccf30b890c40ed1da9fd4a3593a5cf94dae54fb34f8a4b74fcd3f/jsonschema_specifications-2025.4.1-py3-none-any.whl", hash = "sha256:4653bffbd6584f7de83a67e0d620ef16900b390ddc7939d56684d6c81e33f1af", size = 18437 }, +] + +[[package]] +name = "kiwisolver" +version = "1.4.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/59/7c91426a8ac292e1cdd53a63b6d9439abd573c875c3f92c146767dd33faf/kiwisolver-1.4.8.tar.gz", hash = "sha256:23d5f023bdc8c7e54eb65f03ca5d5bb25b601eac4d7f1a042888a1f45237987e", size = 97538 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/5f/4d8e9e852d98ecd26cdf8eaf7ed8bc33174033bba5e07001b289f07308fd/kiwisolver-1.4.8-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88c6f252f6816a73b1f8c904f7bbe02fd67c09a69f7cb8a0eecdbf5ce78e63db", size = 124623 }, + { url = "https://files.pythonhosted.org/packages/1d/70/7f5af2a18a76fe92ea14675f8bd88ce53ee79e37900fa5f1a1d8e0b42998/kiwisolver-1.4.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c72941acb7b67138f35b879bbe85be0f6c6a70cab78fe3ef6db9c024d9223e5b", size = 66720 }, + { url = "https://files.pythonhosted.org/packages/c6/13/e15f804a142353aefd089fadc8f1d985561a15358c97aca27b0979cb0785/kiwisolver-1.4.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ce2cf1e5688edcb727fdf7cd1bbd0b6416758996826a8be1d958f91880d0809d", size = 65413 }, + { url = "https://files.pythonhosted.org/packages/ce/6d/67d36c4d2054e83fb875c6b59d0809d5c530de8148846b1370475eeeece9/kiwisolver-1.4.8-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c8bf637892dc6e6aad2bc6d4d69d08764166e5e3f69d469e55427b6ac001b19d", size = 1650826 }, + { url = "https://files.pythonhosted.org/packages/de/c6/7b9bb8044e150d4d1558423a1568e4f227193662a02231064e3824f37e0a/kiwisolver-1.4.8-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:034d2c891f76bd3edbdb3ea11140d8510dca675443da7304205a2eaa45d8334c", size = 1628231 }, + { url = "https://files.pythonhosted.org/packages/b6/38/ad10d437563063eaaedbe2c3540a71101fc7fb07a7e71f855e93ea4de605/kiwisolver-1.4.8-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d47b28d1dfe0793d5e96bce90835e17edf9a499b53969b03c6c47ea5985844c3", size = 1408938 }, + { url = "https://files.pythonhosted.org/packages/52/ce/c0106b3bd7f9e665c5f5bc1e07cc95b5dabd4e08e3dad42dbe2faad467e7/kiwisolver-1.4.8-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eb158fe28ca0c29f2260cca8c43005329ad58452c36f0edf298204de32a9a3ed", size = 1422799 }, + { url = "https://files.pythonhosted.org/packages/d0/87/efb704b1d75dc9758087ba374c0f23d3254505edaedd09cf9d247f7878b9/kiwisolver-1.4.8-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5536185fce131780ebd809f8e623bf4030ce1b161353166c49a3c74c287897f", size = 1354362 }, + { url = "https://files.pythonhosted.org/packages/eb/b3/fd760dc214ec9a8f208b99e42e8f0130ff4b384eca8b29dd0efc62052176/kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:369b75d40abedc1da2c1f4de13f3482cb99e3237b38726710f4a793432b1c5ff", size = 2222695 }, + { url = "https://files.pythonhosted.org/packages/a2/09/a27fb36cca3fc01700687cc45dae7a6a5f8eeb5f657b9f710f788748e10d/kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:641f2ddf9358c80faa22e22eb4c9f54bd3f0e442e038728f500e3b978d00aa7d", size = 2370802 }, + { url = "https://files.pythonhosted.org/packages/3d/c3/ba0a0346db35fe4dc1f2f2cf8b99362fbb922d7562e5f911f7ce7a7b60fa/kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d561d2d8883e0819445cfe58d7ddd673e4015c3c57261d7bdcd3710d0d14005c", size = 2334646 }, + { url = "https://files.pythonhosted.org/packages/41/52/942cf69e562f5ed253ac67d5c92a693745f0bed3c81f49fc0cbebe4d6b00/kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:1732e065704b47c9afca7ffa272f845300a4eb959276bf6970dc07265e73b605", size = 2467260 }, + { url = "https://files.pythonhosted.org/packages/32/26/2d9668f30d8a494b0411d4d7d4ea1345ba12deb6a75274d58dd6ea01e951/kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:bcb1ebc3547619c3b58a39e2448af089ea2ef44b37988caf432447374941574e", size = 2288633 }, + { url = "https://files.pythonhosted.org/packages/98/99/0dd05071654aa44fe5d5e350729961e7bb535372935a45ac89a8924316e6/kiwisolver-1.4.8-cp310-cp310-win_amd64.whl", hash = "sha256:89c107041f7b27844179ea9c85d6da275aa55ecf28413e87624d033cf1f6b751", size = 71885 }, + { url = "https://files.pythonhosted.org/packages/6c/fc/822e532262a97442989335394d441cd1d0448c2e46d26d3e04efca84df22/kiwisolver-1.4.8-cp310-cp310-win_arm64.whl", hash = "sha256:b5773efa2be9eb9fcf5415ea3ab70fc785d598729fd6057bea38d539ead28271", size = 65175 }, + { url = "https://files.pythonhosted.org/packages/da/ed/c913ee28936c371418cb167b128066ffb20bbf37771eecc2c97edf8a6e4c/kiwisolver-1.4.8-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a4d3601908c560bdf880f07d94f31d734afd1bb71e96585cace0e38ef44c6d84", size = 124635 }, + { url = "https://files.pythonhosted.org/packages/4c/45/4a7f896f7467aaf5f56ef093d1f329346f3b594e77c6a3c327b2d415f521/kiwisolver-1.4.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:856b269c4d28a5c0d5e6c1955ec36ebfd1651ac00e1ce0afa3e28da95293b561", size = 66717 }, + { url = "https://files.pythonhosted.org/packages/5f/b4/c12b3ac0852a3a68f94598d4c8d569f55361beef6159dce4e7b624160da2/kiwisolver-1.4.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c2b9a96e0f326205af81a15718a9073328df1173a2619a68553decb7097fd5d7", size = 65413 }, + { url = "https://files.pythonhosted.org/packages/a9/98/1df4089b1ed23d83d410adfdc5947245c753bddfbe06541c4aae330e9e70/kiwisolver-1.4.8-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c5020c83e8553f770cb3b5fc13faac40f17e0b205bd237aebd21d53d733adb03", size = 1343994 }, + { url = "https://files.pythonhosted.org/packages/8d/bf/b4b169b050c8421a7c53ea1ea74e4ef9c335ee9013216c558a047f162d20/kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dace81d28c787956bfbfbbfd72fdcef014f37d9b48830829e488fdb32b49d954", size = 1434804 }, + { url = "https://files.pythonhosted.org/packages/66/5a/e13bd341fbcf73325ea60fdc8af752addf75c5079867af2e04cc41f34434/kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:11e1022b524bd48ae56c9b4f9296bce77e15a2e42a502cceba602f804b32bb79", size = 1450690 }, + { url = "https://files.pythonhosted.org/packages/9b/4f/5955dcb376ba4a830384cc6fab7d7547bd6759fe75a09564910e9e3bb8ea/kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b9b4d2892fefc886f30301cdd80debd8bb01ecdf165a449eb6e78f79f0fabd6", size = 1376839 }, + { url = "https://files.pythonhosted.org/packages/3a/97/5edbed69a9d0caa2e4aa616ae7df8127e10f6586940aa683a496c2c280b9/kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a96c0e790ee875d65e340ab383700e2b4891677b7fcd30a699146f9384a2bb0", size = 1435109 }, + { url = "https://files.pythonhosted.org/packages/13/fc/e756382cb64e556af6c1809a1bbb22c141bbc2445049f2da06b420fe52bf/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:23454ff084b07ac54ca8be535f4174170c1094a4cff78fbae4f73a4bcc0d4dab", size = 2245269 }, + { url = "https://files.pythonhosted.org/packages/76/15/e59e45829d7f41c776d138245cabae6515cb4eb44b418f6d4109c478b481/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:87b287251ad6488e95b4f0b4a79a6d04d3ea35fde6340eb38fbd1ca9cd35bbbc", size = 2393468 }, + { url = "https://files.pythonhosted.org/packages/e9/39/483558c2a913ab8384d6e4b66a932406f87c95a6080112433da5ed668559/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b21dbe165081142b1232a240fc6383fd32cdd877ca6cc89eab93e5f5883e1c25", size = 2355394 }, + { url = "https://files.pythonhosted.org/packages/01/aa/efad1fbca6570a161d29224f14b082960c7e08268a133fe5dc0f6906820e/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:768cade2c2df13db52475bd28d3a3fac8c9eff04b0e9e2fda0f3760f20b3f7fc", size = 2490901 }, + { url = "https://files.pythonhosted.org/packages/c9/4f/15988966ba46bcd5ab9d0c8296914436720dd67fca689ae1a75b4ec1c72f/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d47cfb2650f0e103d4bf68b0b5804c68da97272c84bb12850d877a95c056bd67", size = 2312306 }, + { url = "https://files.pythonhosted.org/packages/2d/27/bdf1c769c83f74d98cbc34483a972f221440703054894a37d174fba8aa68/kiwisolver-1.4.8-cp311-cp311-win_amd64.whl", hash = "sha256:ed33ca2002a779a2e20eeb06aea7721b6e47f2d4b8a8ece979d8ba9e2a167e34", size = 71966 }, + { url = "https://files.pythonhosted.org/packages/4a/c9/9642ea855604aeb2968a8e145fc662edf61db7632ad2e4fb92424be6b6c0/kiwisolver-1.4.8-cp311-cp311-win_arm64.whl", hash = "sha256:16523b40aab60426ffdebe33ac374457cf62863e330a90a0383639ce14bf44b2", size = 65311 }, + { url = "https://files.pythonhosted.org/packages/fc/aa/cea685c4ab647f349c3bc92d2daf7ae34c8e8cf405a6dcd3a497f58a2ac3/kiwisolver-1.4.8-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d6af5e8815fd02997cb6ad9bbed0ee1e60014438ee1a5c2444c96f87b8843502", size = 124152 }, + { url = "https://files.pythonhosted.org/packages/c5/0b/8db6d2e2452d60d5ebc4ce4b204feeb16176a851fd42462f66ade6808084/kiwisolver-1.4.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bade438f86e21d91e0cf5dd7c0ed00cda0f77c8c1616bd83f9fc157fa6760d31", size = 66555 }, + { url = "https://files.pythonhosted.org/packages/60/26/d6a0db6785dd35d3ba5bf2b2df0aedc5af089962c6eb2cbf67a15b81369e/kiwisolver-1.4.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b83dc6769ddbc57613280118fb4ce3cd08899cc3369f7d0e0fab518a7cf37fdb", size = 65067 }, + { url = "https://files.pythonhosted.org/packages/c9/ed/1d97f7e3561e09757a196231edccc1bcf59d55ddccefa2afc9c615abd8e0/kiwisolver-1.4.8-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:111793b232842991be367ed828076b03d96202c19221b5ebab421ce8bcad016f", size = 1378443 }, + { url = "https://files.pythonhosted.org/packages/29/61/39d30b99954e6b46f760e6289c12fede2ab96a254c443639052d1b573fbc/kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:257af1622860e51b1a9d0ce387bf5c2c4f36a90594cb9514f55b074bcc787cfc", size = 1472728 }, + { url = "https://files.pythonhosted.org/packages/0c/3e/804163b932f7603ef256e4a715e5843a9600802bb23a68b4e08c8c0ff61d/kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:69b5637c3f316cab1ec1c9a12b8c5f4750a4c4b71af9157645bf32830e39c03a", size = 1478388 }, + { url = "https://files.pythonhosted.org/packages/8a/9e/60eaa75169a154700be74f875a4d9961b11ba048bef315fbe89cb6999056/kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:782bb86f245ec18009890e7cb8d13a5ef54dcf2ebe18ed65f795e635a96a1c6a", size = 1413849 }, + { url = "https://files.pythonhosted.org/packages/bc/b3/9458adb9472e61a998c8c4d95cfdfec91c73c53a375b30b1428310f923e4/kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc978a80a0db3a66d25767b03688f1147a69e6237175c0f4ffffaaedf744055a", size = 1475533 }, + { url = "https://files.pythonhosted.org/packages/e4/7a/0a42d9571e35798de80aef4bb43a9b672aa7f8e58643d7bd1950398ffb0a/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:36dbbfd34838500a31f52c9786990d00150860e46cd5041386f217101350f0d3", size = 2268898 }, + { url = "https://files.pythonhosted.org/packages/d9/07/1255dc8d80271400126ed8db35a1795b1a2c098ac3a72645075d06fe5c5d/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:eaa973f1e05131de5ff3569bbba7f5fd07ea0595d3870ed4a526d486fe57fa1b", size = 2425605 }, + { url = "https://files.pythonhosted.org/packages/84/df/5a3b4cf13780ef6f6942df67b138b03b7e79e9f1f08f57c49957d5867f6e/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a66f60f8d0c87ab7f59b6fb80e642ebb29fec354a4dfad687ca4092ae69d04f4", size = 2375801 }, + { url = "https://files.pythonhosted.org/packages/8f/10/2348d068e8b0f635c8c86892788dac7a6b5c0cb12356620ab575775aad89/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:858416b7fb777a53f0c59ca08190ce24e9abbd3cffa18886a5781b8e3e26f65d", size = 2520077 }, + { url = "https://files.pythonhosted.org/packages/32/d8/014b89fee5d4dce157d814303b0fce4d31385a2af4c41fed194b173b81ac/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:085940635c62697391baafaaeabdf3dd7a6c3643577dde337f4d66eba021b2b8", size = 2338410 }, + { url = "https://files.pythonhosted.org/packages/bd/72/dfff0cc97f2a0776e1c9eb5bef1ddfd45f46246c6533b0191887a427bca5/kiwisolver-1.4.8-cp312-cp312-win_amd64.whl", hash = "sha256:01c3d31902c7db5fb6182832713d3b4122ad9317c2c5877d0539227d96bb2e50", size = 71853 }, + { url = "https://files.pythonhosted.org/packages/dc/85/220d13d914485c0948a00f0b9eb419efaf6da81b7d72e88ce2391f7aed8d/kiwisolver-1.4.8-cp312-cp312-win_arm64.whl", hash = "sha256:a3c44cb68861de93f0c4a8175fbaa691f0aa22550c331fefef02b618a9dcb476", size = 65424 }, + { url = "https://files.pythonhosted.org/packages/79/b3/e62464a652f4f8cd9006e13d07abad844a47df1e6537f73ddfbf1bc997ec/kiwisolver-1.4.8-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:1c8ceb754339793c24aee1c9fb2485b5b1f5bb1c2c214ff13368431e51fc9a09", size = 124156 }, + { url = "https://files.pythonhosted.org/packages/8d/2d/f13d06998b546a2ad4f48607a146e045bbe48030774de29f90bdc573df15/kiwisolver-1.4.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:54a62808ac74b5e55a04a408cda6156f986cefbcf0ada13572696b507cc92fa1", size = 66555 }, + { url = "https://files.pythonhosted.org/packages/59/e3/b8bd14b0a54998a9fd1e8da591c60998dc003618cb19a3f94cb233ec1511/kiwisolver-1.4.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:68269e60ee4929893aad82666821aaacbd455284124817af45c11e50a4b42e3c", size = 65071 }, + { url = "https://files.pythonhosted.org/packages/f0/1c/6c86f6d85ffe4d0ce04228d976f00674f1df5dc893bf2dd4f1928748f187/kiwisolver-1.4.8-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:34d142fba9c464bc3bbfeff15c96eab0e7310343d6aefb62a79d51421fcc5f1b", size = 1378053 }, + { url = "https://files.pythonhosted.org/packages/4e/b9/1c6e9f6dcb103ac5cf87cb695845f5fa71379021500153566d8a8a9fc291/kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ddc373e0eef45b59197de815b1b28ef89ae3955e7722cc9710fb91cd77b7f47", size = 1472278 }, + { url = "https://files.pythonhosted.org/packages/ee/81/aca1eb176de671f8bda479b11acdc42c132b61a2ac861c883907dde6debb/kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:77e6f57a20b9bd4e1e2cedda4d0b986ebd0216236f0106e55c28aea3d3d69b16", size = 1478139 }, + { url = "https://files.pythonhosted.org/packages/49/f4/e081522473671c97b2687d380e9e4c26f748a86363ce5af48b4a28e48d06/kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:08e77738ed7538f036cd1170cbed942ef749137b1311fa2bbe2a7fda2f6bf3cc", size = 1413517 }, + { url = "https://files.pythonhosted.org/packages/8f/e9/6a7d025d8da8c4931522922cd706105aa32b3291d1add8c5427cdcd66e63/kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a5ce1e481a74b44dd5e92ff03ea0cb371ae7a0268318e202be06c8f04f4f1246", size = 1474952 }, + { url = "https://files.pythonhosted.org/packages/82/13/13fa685ae167bee5d94b415991c4fc7bb0a1b6ebea6e753a87044b209678/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:fc2ace710ba7c1dfd1a3b42530b62b9ceed115f19a1656adefce7b1782a37794", size = 2269132 }, + { url = "https://files.pythonhosted.org/packages/ef/92/bb7c9395489b99a6cb41d502d3686bac692586db2045adc19e45ee64ed23/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:3452046c37c7692bd52b0e752b87954ef86ee2224e624ef7ce6cb21e8c41cc1b", size = 2425997 }, + { url = "https://files.pythonhosted.org/packages/ed/12/87f0e9271e2b63d35d0d8524954145837dd1a6c15b62a2d8c1ebe0f182b4/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7e9a60b50fe8b2ec6f448fe8d81b07e40141bfced7f896309df271a0b92f80f3", size = 2376060 }, + { url = "https://files.pythonhosted.org/packages/02/6e/c8af39288edbce8bf0fa35dee427b082758a4b71e9c91ef18fa667782138/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:918139571133f366e8362fa4a297aeba86c7816b7ecf0bc79168080e2bd79957", size = 2520471 }, + { url = "https://files.pythonhosted.org/packages/13/78/df381bc7b26e535c91469f77f16adcd073beb3e2dd25042efd064af82323/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e063ef9f89885a1d68dd8b2e18f5ead48653176d10a0e324e3b0030e3a69adeb", size = 2338793 }, + { url = "https://files.pythonhosted.org/packages/d0/dc/c1abe38c37c071d0fc71c9a474fd0b9ede05d42f5a458d584619cfd2371a/kiwisolver-1.4.8-cp313-cp313-win_amd64.whl", hash = "sha256:a17b7c4f5b2c51bb68ed379defd608a03954a1845dfed7cc0117f1cc8a9b7fd2", size = 71855 }, + { url = "https://files.pythonhosted.org/packages/a0/b6/21529d595b126ac298fdd90b705d87d4c5693de60023e0efcb4f387ed99e/kiwisolver-1.4.8-cp313-cp313-win_arm64.whl", hash = "sha256:3cd3bc628b25f74aedc6d374d5babf0166a92ff1317f46267f12d2ed54bc1d30", size = 65430 }, + { url = "https://files.pythonhosted.org/packages/34/bd/b89380b7298e3af9b39f49334e3e2a4af0e04819789f04b43d560516c0c8/kiwisolver-1.4.8-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:370fd2df41660ed4e26b8c9d6bbcad668fbe2560462cba151a721d49e5b6628c", size = 126294 }, + { url = "https://files.pythonhosted.org/packages/83/41/5857dc72e5e4148eaac5aa76e0703e594e4465f8ab7ec0fc60e3a9bb8fea/kiwisolver-1.4.8-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:84a2f830d42707de1d191b9490ac186bf7997a9495d4e9072210a1296345f7dc", size = 67736 }, + { url = "https://files.pythonhosted.org/packages/e1/d1/be059b8db56ac270489fb0b3297fd1e53d195ba76e9bbb30e5401fa6b759/kiwisolver-1.4.8-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7a3ad337add5148cf51ce0b55642dc551c0b9d6248458a757f98796ca7348712", size = 66194 }, + { url = "https://files.pythonhosted.org/packages/e1/83/4b73975f149819eb7dcf9299ed467eba068ecb16439a98990dcb12e63fdd/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7506488470f41169b86d8c9aeff587293f530a23a23a49d6bc64dab66bedc71e", size = 1465942 }, + { url = "https://files.pythonhosted.org/packages/c7/2c/30a5cdde5102958e602c07466bce058b9d7cb48734aa7a4327261ac8e002/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f0121b07b356a22fb0414cec4666bbe36fd6d0d759db3d37228f496ed67c880", size = 1595341 }, + { url = "https://files.pythonhosted.org/packages/ff/9b/1e71db1c000385aa069704f5990574b8244cce854ecd83119c19e83c9586/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d6d6bd87df62c27d4185de7c511c6248040afae67028a8a22012b010bc7ad062", size = 1598455 }, + { url = "https://files.pythonhosted.org/packages/85/92/c8fec52ddf06231b31cbb779af77e99b8253cd96bd135250b9498144c78b/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:291331973c64bb9cce50bbe871fb2e675c4331dab4f31abe89f175ad7679a4d7", size = 1522138 }, + { url = "https://files.pythonhosted.org/packages/0b/51/9eb7e2cd07a15d8bdd976f6190c0164f92ce1904e5c0c79198c4972926b7/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:893f5525bb92d3d735878ec00f781b2de998333659507d29ea4466208df37bed", size = 1582857 }, + { url = "https://files.pythonhosted.org/packages/0f/95/c5a00387a5405e68ba32cc64af65ce881a39b98d73cc394b24143bebc5b8/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b47a465040146981dc9db8647981b8cb96366fbc8d452b031e4f8fdffec3f26d", size = 2293129 }, + { url = "https://files.pythonhosted.org/packages/44/83/eeb7af7d706b8347548313fa3a3a15931f404533cc54fe01f39e830dd231/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:99cea8b9dd34ff80c521aef46a1dddb0dcc0283cf18bde6d756f1e6f31772165", size = 2421538 }, + { url = "https://files.pythonhosted.org/packages/05/f9/27e94c1b3eb29e6933b6986ffc5fa1177d2cd1f0c8efc5f02c91c9ac61de/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:151dffc4865e5fe6dafce5480fab84f950d14566c480c08a53c663a0020504b6", size = 2390661 }, + { url = "https://files.pythonhosted.org/packages/d9/d4/3c9735faa36ac591a4afcc2980d2691000506050b7a7e80bcfe44048daa7/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:577facaa411c10421314598b50413aa1ebcf5126f704f1e5d72d7e4e9f020d90", size = 2546710 }, + { url = "https://files.pythonhosted.org/packages/4c/fa/be89a49c640930180657482a74970cdcf6f7072c8d2471e1babe17a222dc/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:be4816dc51c8a471749d664161b434912eee82f2ea66bd7628bd14583a833e85", size = 2349213 }, + { url = "https://files.pythonhosted.org/packages/1f/f9/ae81c47a43e33b93b0a9819cac6723257f5da2a5a60daf46aa5c7226ea85/kiwisolver-1.4.8-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:e7a019419b7b510f0f7c9dceff8c5eae2392037eae483a7f9162625233802b0a", size = 60403 }, + { url = "https://files.pythonhosted.org/packages/58/ca/f92b5cb6f4ce0c1ebfcfe3e2e42b96917e16f7090e45b21102941924f18f/kiwisolver-1.4.8-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:286b18e86682fd2217a48fc6be6b0f20c1d0ed10958d8dc53453ad58d7be0bf8", size = 58657 }, + { url = "https://files.pythonhosted.org/packages/80/28/ae0240f732f0484d3a4dc885d055653c47144bdf59b670aae0ec3c65a7c8/kiwisolver-1.4.8-pp310-pypy310_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4191ee8dfd0be1c3666ccbac178c5a05d5f8d689bbe3fc92f3c4abec817f8fe0", size = 84948 }, + { url = "https://files.pythonhosted.org/packages/5d/eb/78d50346c51db22c7203c1611f9b513075f35c4e0e4877c5dde378d66043/kiwisolver-1.4.8-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7cd2785b9391f2873ad46088ed7599a6a71e762e1ea33e87514b1a441ed1da1c", size = 81186 }, + { url = "https://files.pythonhosted.org/packages/43/f8/7259f18c77adca88d5f64f9a522792e178b2691f3748817a8750c2d216ef/kiwisolver-1.4.8-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c07b29089b7ba090b6f1a669f1411f27221c3662b3a1b7010e67b59bb5a6f10b", size = 80279 }, + { url = "https://files.pythonhosted.org/packages/3a/1d/50ad811d1c5dae091e4cf046beba925bcae0a610e79ae4c538f996f63ed5/kiwisolver-1.4.8-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:65ea09a5a3faadd59c2ce96dc7bf0f364986a315949dc6374f04396b0d60e09b", size = 71762 }, +] + +[[package]] +name = "kubernetes" +version = "33.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "durationpy" }, + { name = "google-auth" }, + { name = "oauthlib" }, + { name = "python-dateutil" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "requests-oauthlib" }, + { name = "six" }, + { name = "urllib3" }, + { name = "websocket-client" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/52/19ebe8004c243fdfa78268a96727c71e08f00ff6fe69a301d0b7fcbce3c2/kubernetes-33.1.0.tar.gz", hash = "sha256:f64d829843a54c251061a8e7a14523b521f2dc5c896cf6d65ccf348648a88993", size = 1036779 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/89/43/d9bebfc3db7dea6ec80df5cb2aad8d274dd18ec2edd6c4f21f32c237cbbb/kubernetes-33.1.0-py2.py3-none-any.whl", hash = "sha256:544de42b24b64287f7e0aa9513c93cb503f7f40eea39b20f66810011a86eabc5", size = 1941335 }, +] + +[[package]] +name = "lancedb" +version = "0.24.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "deprecation" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "overrides" }, + { name = "packaging" }, + { name = "pyarrow" }, + { name = "pydantic" }, + { name = "tqdm" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/9b/2613660f837b1e2490b786bb1c96e522f1a493292fc04fe58c92a0d6a895/lancedb-0.24.2-cp39-abi3-macosx_10_15_x86_64.whl", hash = "sha256:925a71f8f865ebd8d3792bbe1d5743bc43ae28263ef33ec4781bee4cc6f18b6a", size = 33273298 }, + { url = "https://files.pythonhosted.org/packages/7c/06/98fc4a166721cea4d9dd9c55ed7bcf59772b701c60267cec9d7692f8414e/lancedb-0.24.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:5894c09b03b833e8480c8921178ebcf917599e05000d4a74990e5f1b9ddd1b6a", size = 30711981 }, + { url = "https://files.pythonhosted.org/packages/20/42/420a9bd2349c1c1607f52b353acf9b0af48b2860e15ad333d86c7b68054f/lancedb-0.24.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:482f8e0f09ac68f50a511baea420501908adbf749d09bf04af0b27a018260831", size = 31595926 }, + { url = "https://files.pythonhosted.org/packages/d9/6c/f315ce4161c38085cce929c1c4e9900ada617ab9c8a95166126cdd67184b/lancedb-0.24.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d4a33fb4327880eecc71b9bdc5b4bd722b8fa0f0cbe1cf454b3b0f8521147cd8", size = 34792190 }, + { url = "https://files.pythonhosted.org/packages/06/e8/5cb49026bd47947ab8fa78beff241a48bbf7d70d858c825f30f56bfd6803/lancedb-0.24.2-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:cf8473303853fc798f023495d4a4ace3ba95ce816b9da85aad691ec189e7be45", size = 31598038 }, + { url = "https://files.pythonhosted.org/packages/42/61/9e19fffa106c32b7cb29600ee5ad8530b536dc6b836b695d807795605fae/lancedb-0.24.2-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:693ce4dfcdbf9d1f0d6b79bac12e089091e2576cd5f1e2a5ba08cdbf635179f2", size = 34829210 }, + { url = "https://files.pythonhosted.org/packages/2f/51/242c53238f1a345203ed85910c0b5777aeb0437f3d66f230ba97ede74fe3/lancedb-0.24.2-cp39-abi3-win_amd64.whl", hash = "sha256:46c211f1a0a3cfc385e9031d8ebe30a98d7dfb60751aed0f9626a4f61795ea57", size = 36791580 }, +] + +[[package]] +name = "langchain" +version = "0.3.27" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "async-timeout", marker = "python_full_version < '3.11'" }, + { name = "langchain-core" }, + { name = "langchain-text-splitters" }, + { name = "langsmith" }, + { name = "pydantic" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "sqlalchemy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/83/f6/f4f7f3a56626fe07e2bb330feb61254dbdf06c506e6b59a536a337da51cf/langchain-0.3.27.tar.gz", hash = "sha256:aa6f1e6274ff055d0fd36254176770f356ed0a8994297d1df47df341953cec62", size = 10233809 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f6/d5/4861816a95b2f6993f1360cfb605aacb015506ee2090433a71de9cca8477/langchain-0.3.27-py3-none-any.whl", hash = "sha256:7b20c4f338826acb148d885b20a73a16e410ede9ee4f19bb02011852d5f98798", size = 1018194 }, +] + +[[package]] +name = "langchain-apify" +version = "0.1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "apify-client" }, + { name = "eval-type-backport" }, + { name = "langchain-core" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/a0/7afc7dec9f2f2f26b01816909e9b4d066d81d8a0664a38d044ade66f5822/langchain_apify-0.1.3.tar.gz", hash = "sha256:5631e6610e940633ff7a2cbadb165a0c2cc3232ae1b10b01f6b48752a1f5840a", size = 15074 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/ae/4f8310c11b0e8607189b60ccb91c4c67dbb8bfc15f6b150b91b39b3f4c44/langchain_apify-0.1.3-py3-none-any.whl", hash = "sha256:b3374f2698a372c1b2c3b29efc009b5555244b3f3bd2244270ef795dad9e4e2c", size = 16425 }, +] + +[[package]] +name = "langchain-cohere" +version = "0.3.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cohere" }, + { name = "langchain-core" }, + { name = "langchain-experimental" }, + { name = "pandas" }, + { name = "pydantic" }, + { name = "tabulate" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8a/24/d0ab0875b32a540898b74005860adea5c68779e445ab438b1f3222af15e3/langchain_cohere-0.3.5.tar.gz", hash = "sha256:1b397921c23696b2a11121dfbc4298bbf9a27690052cf72b2675c91594747534", size = 37570 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/64/85c21febec4ee102a9f561a1b0d6298a9d64b1a032f352510a02af0ec7c4/langchain_cohere-0.3.5-py3-none-any.whl", hash = "sha256:ff71e6a19b99f8c08b185e16408259dda55c078258cbe99acc222085ce0223bc", size = 45091 }, +] + +[[package]] +name = "langchain-community" +version = "0.3.27" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, + { name = "dataclasses-json" }, + { name = "httpx-sse" }, + { name = "langchain" }, + { name = "langchain-core" }, + { name = "langsmith" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "pydantic-settings" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "sqlalchemy" }, + { name = "tenacity" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5c/76/200494f6de488217a196c4369e665d26b94c8c3642d46e2fd62f9daf0a3a/langchain_community-0.3.27.tar.gz", hash = "sha256:e1037c3b9da0c6d10bf06e838b034eb741e016515c79ef8f3f16e53ead33d882", size = 33237737 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/bc/f8c7dae8321d37ed39ac9d7896617c4203248240a4835b136e3724b3bb62/langchain_community-0.3.27-py3-none-any.whl", hash = "sha256:581f97b795f9633da738ea95da9cb78f8879b538090c9b7a68c0aed49c828f0d", size = 2530442 }, +] + +[[package]] +name = "langchain-core" +version = "0.3.72" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jsonpatch" }, + { name = "langsmith" }, + { name = "packaging" }, + { name = "pydantic" }, + { name = "pyyaml" }, + { name = "tenacity" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8b/49/7568baeb96a57d3218cb5f1f113b142063679088fd3a0d0cae1feb0b3d36/langchain_core-0.3.72.tar.gz", hash = "sha256:4de3828909b3d7910c313242ab07b241294650f5cb6eac17738dd3638b1cd7de", size = 567227 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6e/7d/9f75023c478e3b854d67da31d721e39f0eb30ae969ec6e755430cb1c0fb5/langchain_core-0.3.72-py3-none-any.whl", hash = "sha256:9fa15d390600eb6b6544397a7aa84be9564939b6adf7a2b091179ea30405b240", size = 442806 }, +] + +[[package]] +name = "langchain-experimental" +version = "0.3.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "langchain-community" }, + { name = "langchain-core" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/27/56/a8acbb08a03383c28875b3b151e4cefea5612266917fbd6fc3c14c21e172/langchain_experimental-0.3.4.tar.gz", hash = "sha256:937c4259ee4a639c618d19acf0e2c5c2898ef127050346edc5655259aa281a21", size = 140532 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/27/fe8caa4884611286b1f7d6c5cfd76e1fef188faaa946db4fde6daa1cd2cd/langchain_experimental-0.3.4-py3-none-any.whl", hash = "sha256:2e587306aea36b60fa5e5fc05dc7281bee9f60a806f0bf9d30916e0ee096af80", size = 209154 }, +] + +[[package]] +name = "langchain-openai" +version = "0.2.14" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "langchain-core" }, + { name = "openai" }, + { name = "tiktoken" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e5/fd/8256eba9a159f95a13c5bf7f1f49683de93b3876585b768e6be5dc3a5765/langchain_openai-0.2.14.tar.gz", hash = "sha256:7a514f309e356b182a337c0ed36ab3fbe34d9834a235a3b85cb7f91ae775d978", size = 43647 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/54/63c8264d7dbc3bf31ba61bf97740fdd76386b2d4f9a58f58afd3961ce7d7/langchain_openai-0.2.14-py3-none-any.whl", hash = "sha256:d232496662f79ece9a11caf7d798ba863e559c771bc366814f7688e0fe664fe8", size = 50876 }, +] + +[[package]] +name = "langchain-text-splitters" +version = "0.3.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "langchain-core" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/91/52/d43ad77acae169210cc476cbc1e4ab37a701017c950211a11ab500fe7d7e/langchain_text_splitters-0.3.9.tar.gz", hash = "sha256:7cd1e5a3aaf609979583eeca2eb34177622570b8fa8f586a605c6b1c34e7ebdb", size = 45260 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e2/52/7638394b88bc15083fd2c3752a843784d9d2d110d68fed6437c8607fb749/langchain_text_splitters-0.3.9-py3-none-any.whl", hash = "sha256:cee0bb816211584ea79cc79927317c358543f40404bcfdd69e69ba3ccde54401", size = 33314 }, +] + +[[package]] +name = "langdetect" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0e/72/a3add0e4eec4eb9e2569554f7c70f4a3c27712f40e3284d483e88094cc0e/langdetect-1.0.9.tar.gz", hash = "sha256:cbc1fef89f8d062739774bd51eda3da3274006b3661d199c2655f6b3f6d605a0", size = 981474 } + +[[package]] +name = "langsmith" +version = "0.3.45" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, + { name = "orjson", marker = "platform_python_implementation != 'PyPy'" }, + { name = "packaging" }, + { name = "pydantic" }, + { name = "requests" }, + { name = "requests-toolbelt" }, + { name = "zstandard" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/be/86/b941012013260f95af2e90a3d9415af4a76a003a28412033fc4b09f35731/langsmith-0.3.45.tar.gz", hash = "sha256:1df3c6820c73ed210b2c7bc5cdb7bfa19ddc9126cd03fdf0da54e2e171e6094d", size = 348201 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/f4/c206c0888f8a506404cb4f16ad89593bdc2f70cf00de26a1a0a7a76ad7a3/langsmith-0.3.45-py3-none-any.whl", hash = "sha256:5b55f0518601fa65f3bb6b1a3100379a96aa7b3ed5e9380581615ba9c65ed8ed", size = 363002 }, +] + +[[package]] +name = "linkup-sdk" +version = "0.2.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9e/15/17f441d73d1dea76398980497bb8a3cce8a2b63dab42779610c04b6800d6/linkup_sdk-0.2.8.tar.gz", hash = "sha256:7ed45b76bb585ebf88d7bafa248e4907d116b19661880978b862c5f8b3b79686", size = 9905 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6c/a2/be93a8d93959e4e3756afa59c9312979ea641b90b7233acb4464a824f3c5/linkup_sdk-0.2.8-py3-none-any.whl", hash = "sha256:96acaf2c773963c21133b31d1ef18f5322ba07a34ef79c375fe75e44b9501cb6", size = 8729 }, +] + +[[package]] +name = "litellm" +version = "1.74.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, + { name = "click" }, + { name = "httpx" }, + { name = "importlib-metadata" }, + { name = "jinja2" }, + { name = "jsonschema" }, + { name = "openai" }, + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "tiktoken" }, + { name = "tokenizers" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6d/5d/646bebdb4769d77e6a018b9152c9ccf17afe15d0f88974f338d3f2ee7c15/litellm-1.74.9.tar.gz", hash = "sha256:4a32eff70342e1aee4d1cbf2de2a6ed64a7c39d86345c58d4401036af018b7de", size = 9660510 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/e4/f1546746049c99c6b8b247e2f34485b9eae36faa9322b84e2a17262e6712/litellm-1.74.9-py3-none-any.whl", hash = "sha256:ab8f8a6e4d8689d3c7c4f9c3bbc7e46212cc3ebc74ddd0f3c0c921bb459c9874", size = 8740449 }, +] + +[[package]] +name = "loguru" +version = "0.7.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "win32-setctime", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3a/05/a1dae3dffd1116099471c643b8924f5aa6524411dc6c63fdae648c4f1aca/loguru-0.7.3.tar.gz", hash = "sha256:19480589e77d47b8d85b2c827ad95d49bf31b0dcde16593892eb51dd18706eb6", size = 63559 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/29/0348de65b8cc732daa3e33e67806420b2ae89bdce2b04af740289c5c6c8c/loguru-0.7.3-py3-none-any.whl", hash = "sha256:31a33c10c8e1e10422bfd431aeb5d351c7cf7fa671e3c4df004162264b28220c", size = 61595 }, +] + +[[package]] +name = "lxml" +version = "5.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/80/61/d3dc048cd6c7be6fe45b80cedcbdd4326ba4d550375f266d9f4246d0f4bc/lxml-5.3.2.tar.gz", hash = "sha256:773947d0ed809ddad824b7b14467e1a481b8976e87278ac4a730c2f7c7fcddc1", size = 3679948 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/9c/b015de0277a13d1d51924810b248b8a685a4e3dcd02d2ffb9b4e65cc37f4/lxml-5.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c4b84d6b580a9625dfa47269bf1fd7fbba7ad69e08b16366a46acb005959c395", size = 8144077 }, + { url = "https://files.pythonhosted.org/packages/a7/6a/30467f6b66ae666d20b52dffa98c00f0f15e0567d1333d70db7c44a6939e/lxml-5.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b4c08ecb26e4270a62f81f81899dfff91623d349e433b126931c9c4577169666", size = 4423433 }, + { url = "https://files.pythonhosted.org/packages/12/85/5a50121c0b57c8aba1beec30d324dc9272a193ecd6c24ad1efb5e223a035/lxml-5.3.2-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef926e9f11e307b5a7c97b17c5c609a93fb59ffa8337afac8f89e6fe54eb0b37", size = 5230753 }, + { url = "https://files.pythonhosted.org/packages/81/07/a62896efbb74ff23e9d19a14713fb9c808dfd89d79eecb8a583d1ca722b1/lxml-5.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:017ceeabe739100379fe6ed38b033cd244ce2da4e7f6f07903421f57da3a19a2", size = 4945993 }, + { url = "https://files.pythonhosted.org/packages/74/ca/c47bffbafcd98c53c2ccd26dcb29b2de8fa0585d5afae76e5c5a9dce5f96/lxml-5.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dae97d9435dc90590f119d056d233c33006b2fd235dd990d5564992261ee7ae8", size = 5562292 }, + { url = "https://files.pythonhosted.org/packages/8f/79/f4ad46c00b72eb465be2032dad7922a14c929ae983e40cd9a179f1e727db/lxml-5.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:910f39425c6798ce63c93976ae5af5fff6949e2cb446acbd44d6d892103eaea8", size = 5000296 }, + { url = "https://files.pythonhosted.org/packages/44/cb/c974078e015990f83d13ef00dac347d74b1d62c2e6ec6e8eeb40ec9a1f1a/lxml-5.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9780de781a0d62a7c3680d07963db3048b919fc9e3726d9cfd97296a65ffce1", size = 5114822 }, + { url = "https://files.pythonhosted.org/packages/1b/c4/dde5d197d176f232c018e7dfd1acadf3aeb8e9f3effa73d13b62f9540061/lxml-5.3.2-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:1a06b0c6ba2e3ca45a009a78a4eb4d6b63831830c0a83dcdc495c13b9ca97d3e", size = 4941338 }, + { url = "https://files.pythonhosted.org/packages/eb/8b/72f8df23f6955bb0f6aca635f72ec52799104907d6b11317099e79e1c752/lxml-5.3.2-cp310-cp310-manylinux_2_28_ppc64le.whl", hash = "sha256:4c62d0a34d1110769a1bbaf77871a4b711a6f59c4846064ccb78bc9735978644", size = 5586914 }, + { url = "https://files.pythonhosted.org/packages/0f/93/7b5ff2971cc5cf017de8ef0e9fdfca6afd249b1e187cb8195e27ed40bb9a/lxml-5.3.2-cp310-cp310-manylinux_2_28_s390x.whl", hash = "sha256:8f961a4e82f411b14538fe5efc3e6b953e17f5e809c463f0756a0d0e8039b700", size = 5082388 }, + { url = "https://files.pythonhosted.org/packages/a3/3e/f81d28bceb4e978a3d450098bdc5364d9c58473ad2f4ded04f679dc76e7e/lxml-5.3.2-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:3dfc78f5f9251b6b8ad37c47d4d0bfe63ceb073a916e5b50a3bf5fd67a703335", size = 5161925 }, + { url = "https://files.pythonhosted.org/packages/4d/4b/1218fcfa0dfc8917ce29c66150cc8f6962d35579f412080aec480cc1a990/lxml-5.3.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:10e690bc03214d3537270c88e492b8612d5e41b884f232df2b069b25b09e6711", size = 5022096 }, + { url = "https://files.pythonhosted.org/packages/8c/de/8eb6fffecd9c5f129461edcdd7e1ac944f9de15783e3d89c84ed6e0374bc/lxml-5.3.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:aa837e6ee9534de8d63bc4c1249e83882a7ac22bd24523f83fad68e6ffdf41ae", size = 5652903 }, + { url = "https://files.pythonhosted.org/packages/95/79/80f4102a08495c100014593680f3f0f7bd7c1333b13520aed855fc993326/lxml-5.3.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:da4c9223319400b97a2acdfb10926b807e51b69eb7eb80aad4942c0516934858", size = 5491813 }, + { url = "https://files.pythonhosted.org/packages/15/f5/9b1f7edf6565ee31e4300edb1bcc61eaebe50a3cff4053c0206d8dc772f2/lxml-5.3.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:dc0e9bdb3aa4d1de703a437576007d366b54f52c9897cae1a3716bb44fc1fc85", size = 5227837 }, + { url = "https://files.pythonhosted.org/packages/dd/53/a187c4ccfcd5fbfca01e6c96da39499d8b801ab5dcf57717db95d7a968a8/lxml-5.3.2-cp310-cp310-win32.win32.whl", hash = "sha256:dd755a0a78dd0b2c43f972e7b51a43be518ebc130c9f1a7c4480cf08b4385486", size = 3477533 }, + { url = "https://files.pythonhosted.org/packages/f2/2c/397c5a9d76a7a0faf9e5b13143ae1a7e223e71d2197a45da71c21aacb3d4/lxml-5.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:d64ea1686474074b38da13ae218d9fde0d1dc6525266976808f41ac98d9d7980", size = 3805160 }, + { url = "https://files.pythonhosted.org/packages/84/b8/2b727f5a90902f7cc5548349f563b60911ca05f3b92e35dfa751349f265f/lxml-5.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9d61a7d0d208ace43986a92b111e035881c4ed45b1f5b7a270070acae8b0bfb4", size = 8163457 }, + { url = "https://files.pythonhosted.org/packages/91/84/23135b2dc72b3440d68c8f39ace2bb00fe78e3a2255f7c74f7e76f22498e/lxml-5.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:856dfd7eda0b75c29ac80a31a6411ca12209183e866c33faf46e77ace3ce8a79", size = 4433445 }, + { url = "https://files.pythonhosted.org/packages/c9/1c/6900ade2294488f80598af7b3229669562166384bb10bf4c915342a2f288/lxml-5.3.2-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7a01679e4aad0727bedd4c9407d4d65978e920f0200107ceeffd4b019bd48529", size = 5029603 }, + { url = "https://files.pythonhosted.org/packages/2f/e9/31dbe5deaccf0d33ec279cf400306ad4b32dfd1a0fee1fca40c5e90678fe/lxml-5.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b6b37b4c3acb8472d191816d4582379f64d81cecbdce1a668601745c963ca5cc", size = 4771236 }, + { url = "https://files.pythonhosted.org/packages/68/41/c3412392884130af3415af2e89a2007e00b2a782be6fb848a95b598a114c/lxml-5.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3df5a54e7b7c31755383f126d3a84e12a4e0333db4679462ef1165d702517477", size = 5369815 }, + { url = "https://files.pythonhosted.org/packages/34/0a/ba0309fd5f990ea0cc05aba2bea225ef1bcb07ecbf6c323c6b119fc46e7f/lxml-5.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c09a40f28dcded933dc16217d6a092be0cc49ae25811d3b8e937c8060647c353", size = 4843663 }, + { url = "https://files.pythonhosted.org/packages/b6/c6/663b5d87d51d00d4386a2d52742a62daa486c5dc6872a443409d9aeafece/lxml-5.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a1ef20f1851ccfbe6c5a04c67ec1ce49da16ba993fdbabdce87a92926e505412", size = 4918028 }, + { url = "https://files.pythonhosted.org/packages/75/5f/f6a72ccbe05cf83341d4b6ad162ed9e1f1ffbd12f1c4b8bc8ae413392282/lxml-5.3.2-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:f79a63289dbaba964eb29ed3c103b7911f2dce28c36fe87c36a114e6bd21d7ad", size = 4792005 }, + { url = "https://files.pythonhosted.org/packages/37/7b/8abd5b332252239ffd28df5842ee4e5bf56e1c613c323586c21ccf5af634/lxml-5.3.2-cp311-cp311-manylinux_2_28_ppc64le.whl", hash = "sha256:75a72697d95f27ae00e75086aed629f117e816387b74a2f2da6ef382b460b710", size = 5405363 }, + { url = "https://files.pythonhosted.org/packages/5a/79/549b7ec92b8d9feb13869c1b385a0749d7ccfe5590d1e60f11add9cdd580/lxml-5.3.2-cp311-cp311-manylinux_2_28_s390x.whl", hash = "sha256:b9b00c9ee1cc3a76f1f16e94a23c344e0b6e5c10bec7f94cf2d820ce303b8c01", size = 4932915 }, + { url = "https://files.pythonhosted.org/packages/57/eb/4fa626d0bac8b4f2aa1d0e6a86232db030fd0f462386daf339e4a0ee352b/lxml-5.3.2-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:77cbcab50cbe8c857c6ba5f37f9a3976499c60eada1bf6d38f88311373d7b4bc", size = 4983473 }, + { url = "https://files.pythonhosted.org/packages/1b/c8/79d61d13cbb361c2c45fbe7c8bd00ea6a23b3e64bc506264d2856c60d702/lxml-5.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:29424058f072a24622a0a15357bca63d796954758248a72da6d512f9bd9a4493", size = 4855284 }, + { url = "https://files.pythonhosted.org/packages/80/16/9f84e1ef03a13136ab4f9482c9adaaad425c68b47556b9d3192a782e5d37/lxml-5.3.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:7d82737a8afe69a7c80ef31d7626075cc7d6e2267f16bf68af2c764b45ed68ab", size = 5458355 }, + { url = "https://files.pythonhosted.org/packages/aa/6d/f62860451bb4683e87636e49effb76d499773337928e53356c1712ccec24/lxml-5.3.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:95473d1d50a5d9fcdb9321fdc0ca6e1edc164dce4c7da13616247d27f3d21e31", size = 5300051 }, + { url = "https://files.pythonhosted.org/packages/3f/5f/3b6c4acec17f9a57ea8bb89a658a70621db3fb86ea588e7703b6819d9b03/lxml-5.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2162068f6da83613f8b2a32ca105e37a564afd0d7009b0b25834d47693ce3538", size = 5033481 }, + { url = "https://files.pythonhosted.org/packages/79/bd/3c4dd7d903bb9981f4876c61ef2ff5d5473e409ef61dc7337ac207b91920/lxml-5.3.2-cp311-cp311-win32.whl", hash = "sha256:f8695752cf5d639b4e981afe6c99e060621362c416058effd5c704bede9cb5d1", size = 3474266 }, + { url = "https://files.pythonhosted.org/packages/1f/ea/9311fa1ef75b7d601c89600fc612838ee77ad3d426184941cba9cf62641f/lxml-5.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:d1a94cbb4ee64af3ab386c2d63d6d9e9cf2e256ac0fd30f33ef0a3c88f575174", size = 3815230 }, + { url = "https://files.pythonhosted.org/packages/0d/7e/c749257a7fabc712c4df57927b0f703507f316e9f2c7e3219f8f76d36145/lxml-5.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:16b3897691ec0316a1aa3c6585f61c8b7978475587c5b16fc1d2c28d283dc1b0", size = 8193212 }, + { url = "https://files.pythonhosted.org/packages/a8/50/17e985ba162c9f1ca119f4445004b58f9e5ef559ded599b16755e9bfa260/lxml-5.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:a8d4b34a0eeaf6e73169dcfd653c8d47f25f09d806c010daf074fba2db5e2d3f", size = 4451439 }, + { url = "https://files.pythonhosted.org/packages/c2/b5/4960ba0fcca6ce394ed4a2f89ee13083e7fcbe9641a91166e8e9792fedb1/lxml-5.3.2-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9cd7a959396da425022e1e4214895b5cfe7de7035a043bcc2d11303792b67554", size = 5052146 }, + { url = "https://files.pythonhosted.org/packages/5f/d1/184b04481a5d1f5758916de087430752a7b229bddbd6c1d23405078c72bd/lxml-5.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cac5eaeec3549c5df7f8f97a5a6db6963b91639389cdd735d5a806370847732b", size = 4789082 }, + { url = "https://files.pythonhosted.org/packages/7d/75/1a19749d373e9a3d08861addccdf50c92b628c67074b22b8f3c61997cf5a/lxml-5.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:29b5f7d77334877c2146e7bb8b94e4df980325fab0a8af4d524e5d43cd6f789d", size = 5312300 }, + { url = "https://files.pythonhosted.org/packages/fb/00/9d165d4060d3f347e63b219fcea5c6a3f9193e9e2868c6801e18e5379725/lxml-5.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:13f3495cfec24e3d63fffd342cc8141355d1d26ee766ad388775f5c8c5ec3932", size = 4836655 }, + { url = "https://files.pythonhosted.org/packages/b8/e9/06720a33cc155966448a19677f079100517b6629a872382d22ebd25e48aa/lxml-5.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e70ad4c9658beeff99856926fd3ee5fde8b519b92c693f856007177c36eb2e30", size = 4961795 }, + { url = "https://files.pythonhosted.org/packages/2d/57/4540efab2673de2904746b37ef7f74385329afd4643ed92abcc9ec6e00ca/lxml-5.3.2-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:507085365783abd7879fa0a6fa55eddf4bdd06591b17a2418403bb3aff8a267d", size = 4779791 }, + { url = "https://files.pythonhosted.org/packages/99/ad/6056edf6c9f4fa1d41e6fbdae52c733a4a257fd0d7feccfa26ae051bb46f/lxml-5.3.2-cp312-cp312-manylinux_2_28_ppc64le.whl", hash = "sha256:5bb304f67cbf5dfa07edad904732782cbf693286b9cd85af27059c5779131050", size = 5346807 }, + { url = "https://files.pythonhosted.org/packages/a1/fa/5be91fc91a18f3f705ea5533bc2210b25d738c6b615bf1c91e71a9b2f26b/lxml-5.3.2-cp312-cp312-manylinux_2_28_s390x.whl", hash = "sha256:3d84f5c093645c21c29a4e972b84cb7cf682f707f8706484a5a0c7ff13d7a988", size = 4909213 }, + { url = "https://files.pythonhosted.org/packages/f3/74/71bb96a3b5ae36b74e0402f4fa319df5559a8538577f8c57c50f1b57dc15/lxml-5.3.2-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:bdc13911db524bd63f37b0103af014b7161427ada41f1b0b3c9b5b5a9c1ca927", size = 4987694 }, + { url = "https://files.pythonhosted.org/packages/08/c2/3953a68b0861b2f97234b1838769269478ccf872d8ea7a26e911238220ad/lxml-5.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1ec944539543f66ebc060ae180d47e86aca0188bda9cbfadff47d86b0dc057dc", size = 4862865 }, + { url = "https://files.pythonhosted.org/packages/e0/9a/52e48f7cfd5a5e61f44a77e679880580dfb4f077af52d6ed5dd97e3356fe/lxml-5.3.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:59d437cc8a7f838282df5a199cf26f97ef08f1c0fbec6e84bd6f5cc2b7913f6e", size = 5423383 }, + { url = "https://files.pythonhosted.org/packages/17/67/42fe1d489e4dcc0b264bef361aef0b929fbb2b5378702471a3043bc6982c/lxml-5.3.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:0e275961adbd32e15672e14e0cc976a982075208224ce06d149c92cb43db5b93", size = 5286864 }, + { url = "https://files.pythonhosted.org/packages/29/e4/03b1d040ee3aaf2bd4e1c2061de2eae1178fe9a460d3efc1ea7ef66f6011/lxml-5.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:038aeb6937aa404480c2966b7f26f1440a14005cb0702078c173c028eca72c31", size = 5056819 }, + { url = "https://files.pythonhosted.org/packages/83/b3/e2ec8a6378e4d87da3af9de7c862bcea7ca624fc1a74b794180c82e30123/lxml-5.3.2-cp312-cp312-win32.whl", hash = "sha256:3c2c8d0fa3277147bff180e3590be67597e17d365ce94beb2efa3138a2131f71", size = 3486177 }, + { url = "https://files.pythonhosted.org/packages/d5/8a/6a08254b0bab2da9573735725caab8302a2a1c9b3818533b41568ca489be/lxml-5.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:77809fcd97dfda3f399102db1794f7280737b69830cd5c961ac87b3c5c05662d", size = 3817134 }, + { url = "https://files.pythonhosted.org/packages/19/fe/904fd1b0ba4f42ed5a144fcfff7b8913181892a6aa7aeb361ee783d441f8/lxml-5.3.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:77626571fb5270ceb36134765f25b665b896243529eefe840974269b083e090d", size = 8173598 }, + { url = "https://files.pythonhosted.org/packages/97/e8/5e332877b3ce4e2840507b35d6dbe1cc33b17678ece945ba48d2962f8c06/lxml-5.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:78a533375dc7aa16d0da44af3cf6e96035e484c8c6b2b2445541a5d4d3d289ee", size = 4441586 }, + { url = "https://files.pythonhosted.org/packages/de/f4/8fe2e6d8721803182fbce2325712e98f22dbc478126070e62731ec6d54a0/lxml-5.3.2-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a6f62b2404b3f3f0744bbcabb0381c5fe186fa2a9a67ecca3603480f4846c585", size = 5038447 }, + { url = "https://files.pythonhosted.org/packages/a6/ac/fa63f86a1a4b1ba8b03599ad9e2f5212fa813223ac60bfe1155390d1cc0c/lxml-5.3.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ea918da00091194526d40c30c4996971f09dacab032607581f8d8872db34fbf", size = 4783583 }, + { url = "https://files.pythonhosted.org/packages/1a/7a/08898541296a02c868d4acc11f31a5839d80f5b21d4a96f11d4c0fbed15e/lxml-5.3.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c35326f94702a7264aa0eea826a79547d3396a41ae87a70511b9f6e9667ad31c", size = 5305684 }, + { url = "https://files.pythonhosted.org/packages/0b/be/9a6d80b467771b90be762b968985d3de09e0d5886092238da65dac9c1f75/lxml-5.3.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e3bef90af21d31c4544bc917f51e04f94ae11b43156356aff243cdd84802cbf2", size = 4830797 }, + { url = "https://files.pythonhosted.org/packages/8d/1c/493632959f83519802637f7db3be0113b6e8a4e501b31411fbf410735a75/lxml-5.3.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:52fa7ba11a495b7cbce51573c73f638f1dcff7b3ee23697467dc063f75352a69", size = 4950302 }, + { url = "https://files.pythonhosted.org/packages/c7/13/01aa3b92a6b93253b90c061c7527261b792f5ae7724b420cded733bfd5d6/lxml-5.3.2-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:ad131e2c4d2c3803e736bb69063382334e03648de2a6b8f56a878d700d4b557d", size = 4775247 }, + { url = "https://files.pythonhosted.org/packages/60/4a/baeb09fbf5c84809e119c9cf8e2e94acec326a9b45563bf5ae45a234973b/lxml-5.3.2-cp313-cp313-manylinux_2_28_ppc64le.whl", hash = "sha256:00a4463ca409ceacd20490a893a7e08deec7870840eff33dc3093067b559ce3e", size = 5338824 }, + { url = "https://files.pythonhosted.org/packages/69/c7/a05850f169ad783ed09740ac895e158b06d25fce4b13887a8ac92a84d61c/lxml-5.3.2-cp313-cp313-manylinux_2_28_s390x.whl", hash = "sha256:87e8d78205331cace2b73ac8249294c24ae3cba98220687b5b8ec5971a2267f1", size = 4899079 }, + { url = "https://files.pythonhosted.org/packages/de/48/18ca583aba5235582db0e933ed1af6540226ee9ca16c2ee2d6f504fcc34a/lxml-5.3.2-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:bf6389133bb255e530a4f2f553f41c4dd795b1fbb6f797aea1eff308f1e11606", size = 4978041 }, + { url = "https://files.pythonhosted.org/packages/b6/55/6968ddc88554209d1dba0dca196360c629b3dfe083bc32a3370f9523a0c4/lxml-5.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b3709fc752b42fb6b6ffa2ba0a5b9871646d97d011d8f08f4d5b3ee61c7f3b2b", size = 4859761 }, + { url = "https://files.pythonhosted.org/packages/2e/52/d2d3baa1e0b7d04a729613160f1562f466fb1a0e45085a33acb0d6981a2b/lxml-5.3.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:abc795703d0de5d83943a4badd770fbe3d1ca16ee4ff3783d7caffc252f309ae", size = 5418209 }, + { url = "https://files.pythonhosted.org/packages/d3/50/6005b297ba5f858a113d6e81ccdb3a558b95a615772e7412d1f1cbdf22d7/lxml-5.3.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:98050830bb6510159f65d9ad1b8aca27f07c01bb3884ba95f17319ccedc4bcf9", size = 5274231 }, + { url = "https://files.pythonhosted.org/packages/fb/33/6f40c09a5f7d7e7fcb85ef75072e53eba3fbadbf23e4991ca069ab2b1abb/lxml-5.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6ba465a91acc419c5682f8b06bcc84a424a7aa5c91c220241c6fd31de2a72bc6", size = 5051899 }, + { url = "https://files.pythonhosted.org/packages/8b/3a/673bc5c0d5fb6596ee2963dd016fdaefaed2c57ede82c7634c08cbda86c1/lxml-5.3.2-cp313-cp313-win32.whl", hash = "sha256:56a1d56d60ea1ec940f949d7a309e0bff05243f9bd337f585721605670abb1c1", size = 3485315 }, + { url = "https://files.pythonhosted.org/packages/8c/be/cab8dd33b0dbe3af5b5d4d24137218f79ea75d540f74eb7d8581195639e0/lxml-5.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:1a580dc232c33d2ad87d02c8a3069d47abbcdce974b9c9cc82a79ff603065dbe", size = 3814639 }, + { url = "https://files.pythonhosted.org/packages/3d/1a/480682ac974e0f8778503300a61d96c3b4d992d2ae024f9db18d5fd895d1/lxml-5.3.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:521ab9c80b98c30b2d987001c3ede2e647e92eeb2ca02e8cb66ef5122d792b24", size = 3937182 }, + { url = "https://files.pythonhosted.org/packages/74/e6/ac87269713e372b58c4334913601a65d7a6f3b7df9ac15a4a4014afea7ae/lxml-5.3.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f1231b0f9810289d41df1eacc4ebb859c63e4ceee29908a0217403cddce38d0", size = 4235148 }, + { url = "https://files.pythonhosted.org/packages/75/ec/7d7af58047862fb59fcdec6e3abcffc7a98f7f7560e580485169ce28b706/lxml-5.3.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:271f1a4d5d2b383c36ad8b9b489da5ea9c04eca795a215bae61ed6a57cf083cd", size = 4349974 }, + { url = "https://files.pythonhosted.org/packages/ff/de/021ef34a57a372778f44182d2043fa3cae0b0407ac05fc35834f842586f2/lxml-5.3.2-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:6fca8a5a13906ba2677a5252752832beb0f483a22f6c86c71a2bb320fba04f61", size = 4238656 }, + { url = "https://files.pythonhosted.org/packages/0a/96/00874cb83ebb2cf649f2a8cad191d8da64fe1cf15e6580d5a7967755d6a3/lxml-5.3.2-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:ea0c3b7922209160faef194a5b6995bfe7fa05ff7dda6c423ba17646b7b9de10", size = 4373836 }, + { url = "https://files.pythonhosted.org/packages/6b/40/7d49ff503cc90b03253eba0768feec909b47ce92a90591b025c774a29a95/lxml-5.3.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:0a006390834603e5952a2ff74b9a31a6007c7cc74282a087aa6467afb4eea987", size = 3487898 }, +] + +[[package]] +name = "mako" +version = "1.3.10" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9e/38/bd5b78a920a64d708fe6bc8e0a2c075e1389d53bef8413725c63ba041535/mako-1.3.10.tar.gz", hash = "sha256:99579a6f39583fa7e5630a28c3c1f440e4e97a414b80372649c0ce338da2ea28", size = 392474 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/fb/99f81ac72ae23375f22b7afdb7642aba97c00a713c217124420147681a2f/mako-1.3.10-py3-none-any.whl", hash = "sha256:baef24a52fc4fc514a0887ac600f9f1cff3d82c61d4d700a1fa84d597b88db59", size = 78509 }, +] + +[[package]] +name = "markdown" +version = "3.8.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/c2/4ab49206c17f75cb08d6311171f2d65798988db4360c4d1485bd0eedd67c/markdown-3.8.2.tar.gz", hash = "sha256:247b9a70dd12e27f67431ce62523e675b866d254f900c4fe75ce3dda62237c45", size = 362071 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/2b/34cc11786bc00d0f04d0f5fdc3a2b1ae0b6239eef72d3d345805f9ad92a1/markdown-3.8.2-py3-none-any.whl", hash = "sha256:5c83764dbd4e00bdd94d85a19b8d55ccca20fe35b2e678a1422b380324dd5f24", size = 106827 }, +] + +[[package]] +name = "markdown-it-py" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528 }, +] + +[[package]] +name = "markupsafe" +version = "3.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/90/d08277ce111dd22f77149fd1a5d4653eeb3b3eaacbdfcbae5afb2600eebd/MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8", size = 14357 }, + { url = "https://files.pythonhosted.org/packages/04/e1/6e2194baeae0bca1fae6629dc0cbbb968d4d941469cbab11a3872edff374/MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158", size = 12393 }, + { url = "https://files.pythonhosted.org/packages/1d/69/35fa85a8ece0a437493dc61ce0bb6d459dcba482c34197e3efc829aa357f/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579", size = 21732 }, + { url = "https://files.pythonhosted.org/packages/22/35/137da042dfb4720b638d2937c38a9c2df83fe32d20e8c8f3185dbfef05f7/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d", size = 20866 }, + { url = "https://files.pythonhosted.org/packages/29/28/6d029a903727a1b62edb51863232152fd335d602def598dade38996887f0/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb", size = 20964 }, + { url = "https://files.pythonhosted.org/packages/cc/cd/07438f95f83e8bc028279909d9c9bd39e24149b0d60053a97b2bc4f8aa51/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b", size = 21977 }, + { url = "https://files.pythonhosted.org/packages/29/01/84b57395b4cc062f9c4c55ce0df7d3108ca32397299d9df00fedd9117d3d/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c", size = 21366 }, + { url = "https://files.pythonhosted.org/packages/bd/6e/61ebf08d8940553afff20d1fb1ba7294b6f8d279df9fd0c0db911b4bbcfd/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171", size = 21091 }, + { url = "https://files.pythonhosted.org/packages/11/23/ffbf53694e8c94ebd1e7e491de185124277964344733c45481f32ede2499/MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50", size = 15065 }, + { url = "https://files.pythonhosted.org/packages/44/06/e7175d06dd6e9172d4a69a72592cb3f7a996a9c396eee29082826449bbc3/MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a", size = 15514 }, + { url = "https://files.pythonhosted.org/packages/6b/28/bbf83e3f76936960b850435576dd5e67034e200469571be53f69174a2dfd/MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", size = 14353 }, + { url = "https://files.pythonhosted.org/packages/6c/30/316d194b093cde57d448a4c3209f22e3046c5bb2fb0820b118292b334be7/MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", size = 12392 }, + { url = "https://files.pythonhosted.org/packages/f2/96/9cdafba8445d3a53cae530aaf83c38ec64c4d5427d975c974084af5bc5d2/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", size = 23984 }, + { url = "https://files.pythonhosted.org/packages/f1/a4/aefb044a2cd8d7334c8a47d3fb2c9f328ac48cb349468cc31c20b539305f/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", size = 23120 }, + { url = "https://files.pythonhosted.org/packages/8d/21/5e4851379f88f3fad1de30361db501300d4f07bcad047d3cb0449fc51f8c/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", size = 23032 }, + { url = "https://files.pythonhosted.org/packages/00/7b/e92c64e079b2d0d7ddf69899c98842f3f9a60a1ae72657c89ce2655c999d/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", size = 24057 }, + { url = "https://files.pythonhosted.org/packages/f9/ac/46f960ca323037caa0a10662ef97d0a4728e890334fc156b9f9e52bcc4ca/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", size = 23359 }, + { url = "https://files.pythonhosted.org/packages/69/84/83439e16197337b8b14b6a5b9c2105fff81d42c2a7c5b58ac7b62ee2c3b1/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", size = 23306 }, + { url = "https://files.pythonhosted.org/packages/9a/34/a15aa69f01e2181ed8d2b685c0d2f6655d5cca2c4db0ddea775e631918cd/MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", size = 15094 }, + { url = "https://files.pythonhosted.org/packages/da/b8/3a3bd761922d416f3dc5d00bfbed11f66b1ab89a0c2b6e887240a30b0f6b/MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", size = 15521 }, + { url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", size = 14274 }, + { url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", size = 12348 }, + { url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", size = 24149 }, + { url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", size = 23118 }, + { url = "https://files.pythonhosted.org/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", size = 22993 }, + { url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", size = 24178 }, + { url = "https://files.pythonhosted.org/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", size = 23319 }, + { url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352 }, + { url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097 }, + { url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601 }, + { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274 }, + { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352 }, + { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122 }, + { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085 }, + { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978 }, + { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208 }, + { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357 }, + { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344 }, + { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101 }, + { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603 }, + { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510 }, + { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486 }, + { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480 }, + { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914 }, + { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796 }, + { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473 }, + { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114 }, + { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098 }, + { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208 }, + { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739 }, +] + +[[package]] +name = "marshmallow" +version = "3.26.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ab/5e/5e53d26b42ab75491cda89b871dab9e97c840bf12c63ec58a1919710cd06/marshmallow-3.26.1.tar.gz", hash = "sha256:e6d8affb6cb61d39d26402096dc0aee12d5a26d490a121f118d2e81dc0719dc6", size = 221825 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/34/75/51952c7b2d3873b44a0028b1bd26a25078c18f92f256608e8d1dc61b39fd/marshmallow-3.26.1-py3-none-any.whl", hash = "sha256:3350409f20a70a7e4e11a27661187b77cdcaeb20abca41c1454fe33636bea09c", size = 50878 }, +] + +[[package]] +name = "matplotlib" +version = "3.10.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "contourpy", version = "1.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "contourpy", version = "1.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "cycler" }, + { name = "fonttools" }, + { name = "kiwisolver" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "packaging" }, + { name = "pillow" }, + { name = "pyparsing" }, + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/43/91/f2939bb60b7ebf12478b030e0d7f340247390f402b3b189616aad790c366/matplotlib-3.10.5.tar.gz", hash = "sha256:352ed6ccfb7998a00881692f38b4ca083c691d3e275b4145423704c34c909076", size = 34804044 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/89/5355cdfe43242cb4d1a64a67cb6831398b665ad90e9702c16247cbd8d5ab/matplotlib-3.10.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:5d4773a6d1c106ca05cb5a5515d277a6bb96ed09e5c8fab6b7741b8fcaa62c8f", size = 8229094 }, + { url = "https://files.pythonhosted.org/packages/34/bc/ba802650e1c69650faed261a9df004af4c6f21759d7a1ec67fe972f093b3/matplotlib-3.10.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dc88af74e7ba27de6cbe6faee916024ea35d895ed3d61ef6f58c4ce97da7185a", size = 8091464 }, + { url = "https://files.pythonhosted.org/packages/ac/64/8d0c8937dee86c286625bddb1902efacc3e22f2b619f5b5a8df29fe5217b/matplotlib-3.10.5-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:64c4535419d5617f7363dad171a5a59963308e0f3f813c4bed6c9e6e2c131512", size = 8653163 }, + { url = "https://files.pythonhosted.org/packages/11/dc/8dfc0acfbdc2fc2336c72561b7935cfa73db9ca70b875d8d3e1b3a6f371a/matplotlib-3.10.5-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a277033048ab22d34f88a3c5243938cef776493f6201a8742ed5f8b553201343", size = 9490635 }, + { url = "https://files.pythonhosted.org/packages/54/02/e3fdfe0f2e9fb05f3a691d63876639dbf684170fdcf93231e973104153b4/matplotlib-3.10.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e4a6470a118a2e93022ecc7d3bd16b3114b2004ea2bf014fff875b3bc99b70c6", size = 9539036 }, + { url = "https://files.pythonhosted.org/packages/c1/29/82bf486ff7f4dbedfb11ccc207d0575cbe3be6ea26f75be514252bde3d70/matplotlib-3.10.5-cp310-cp310-win_amd64.whl", hash = "sha256:7e44cada61bec8833c106547786814dd4a266c1b2964fd25daa3804f1b8d4467", size = 8093529 }, + { url = "https://files.pythonhosted.org/packages/aa/c7/1f2db90a1d43710478bb1e9b57b162852f79234d28e4f48a28cc415aa583/matplotlib-3.10.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:dcfc39c452c6a9f9028d3e44d2d721484f665304857188124b505b2c95e1eecf", size = 8239216 }, + { url = "https://files.pythonhosted.org/packages/82/6d/ca6844c77a4f89b1c9e4d481c412e1d1dbabf2aae2cbc5aa2da4a1d6683e/matplotlib-3.10.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:903352681b59f3efbf4546985142a9686ea1d616bb054b09a537a06e4b892ccf", size = 8102130 }, + { url = "https://files.pythonhosted.org/packages/1d/1e/5e187a30cc673a3e384f3723e5f3c416033c1d8d5da414f82e4e731128ea/matplotlib-3.10.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:080c3676a56b8ee1c762bcf8fca3fe709daa1ee23e6ef06ad9f3fc17332f2d2a", size = 8666471 }, + { url = "https://files.pythonhosted.org/packages/03/c0/95540d584d7d645324db99a845ac194e915ef75011a0d5e19e1b5cee7e69/matplotlib-3.10.5-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4b4984d5064a35b6f66d2c11d668565f4389b1119cc64db7a4c1725bc11adffc", size = 9500518 }, + { url = "https://files.pythonhosted.org/packages/ba/2e/e019352099ea58b4169adb9c6e1a2ad0c568c6377c2b677ee1f06de2adc7/matplotlib-3.10.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3967424121d3a46705c9fa9bdb0931de3228f13f73d7bb03c999c88343a89d89", size = 9552372 }, + { url = "https://files.pythonhosted.org/packages/b7/81/3200b792a5e8b354f31f4101ad7834743ad07b6d620259f2059317b25e4d/matplotlib-3.10.5-cp311-cp311-win_amd64.whl", hash = "sha256:33775bbeb75528555a15ac29396940128ef5613cf9a2d31fb1bfd18b3c0c0903", size = 8100634 }, + { url = "https://files.pythonhosted.org/packages/52/46/a944f6f0c1f5476a0adfa501969d229ce5ae60cf9a663be0e70361381f89/matplotlib-3.10.5-cp311-cp311-win_arm64.whl", hash = "sha256:c61333a8e5e6240e73769d5826b9a31d8b22df76c0778f8480baf1b4b01c9420", size = 7978880 }, + { url = "https://files.pythonhosted.org/packages/66/1e/c6f6bcd882d589410b475ca1fc22e34e34c82adff519caf18f3e6dd9d682/matplotlib-3.10.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:00b6feadc28a08bd3c65b2894f56cf3c94fc8f7adcbc6ab4516ae1e8ed8f62e2", size = 8253056 }, + { url = "https://files.pythonhosted.org/packages/53/e6/d6f7d1b59413f233793dda14419776f5f443bcccb2dfc84b09f09fe05dbe/matplotlib-3.10.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ee98a5c5344dc7f48dc261b6ba5d9900c008fc12beb3fa6ebda81273602cc389", size = 8110131 }, + { url = "https://files.pythonhosted.org/packages/66/2b/bed8a45e74957549197a2ac2e1259671cd80b55ed9e1fe2b5c94d88a9202/matplotlib-3.10.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a17e57e33de901d221a07af32c08870ed4528db0b6059dce7d7e65c1122d4bea", size = 8669603 }, + { url = "https://files.pythonhosted.org/packages/7e/a7/315e9435b10d057f5e52dfc603cd353167ae28bb1a4e033d41540c0067a4/matplotlib-3.10.5-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97b9d6443419085950ee4a5b1ee08c363e5c43d7176e55513479e53669e88468", size = 9508127 }, + { url = "https://files.pythonhosted.org/packages/7f/d9/edcbb1f02ca99165365d2768d517898c22c6040187e2ae2ce7294437c413/matplotlib-3.10.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ceefe5d40807d29a66ae916c6a3915d60ef9f028ce1927b84e727be91d884369", size = 9566926 }, + { url = "https://files.pythonhosted.org/packages/3b/d9/6dd924ad5616c97b7308e6320cf392c466237a82a2040381163b7500510a/matplotlib-3.10.5-cp312-cp312-win_amd64.whl", hash = "sha256:c04cba0f93d40e45b3c187c6c52c17f24535b27d545f757a2fffebc06c12b98b", size = 8107599 }, + { url = "https://files.pythonhosted.org/packages/0e/f3/522dc319a50f7b0279fbe74f86f7a3506ce414bc23172098e8d2bdf21894/matplotlib-3.10.5-cp312-cp312-win_arm64.whl", hash = "sha256:a41bcb6e2c8e79dc99c5511ae6f7787d2fb52efd3d805fff06d5d4f667db16b2", size = 7978173 }, + { url = "https://files.pythonhosted.org/packages/8d/05/4f3c1f396075f108515e45cb8d334aff011a922350e502a7472e24c52d77/matplotlib-3.10.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:354204db3f7d5caaa10e5de74549ef6a05a4550fdd1c8f831ab9bca81efd39ed", size = 8253586 }, + { url = "https://files.pythonhosted.org/packages/2f/2c/e084415775aac7016c3719fe7006cdb462582c6c99ac142f27303c56e243/matplotlib-3.10.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b072aac0c3ad563a2b3318124756cb6112157017f7431626600ecbe890df57a1", size = 8110715 }, + { url = "https://files.pythonhosted.org/packages/52/1b/233e3094b749df16e3e6cd5a44849fd33852e692ad009cf7de00cf58ddf6/matplotlib-3.10.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d52fd5b684d541b5a51fb276b2b97b010c75bee9aa392f96b4a07aeb491e33c7", size = 8669397 }, + { url = "https://files.pythonhosted.org/packages/e8/ec/03f9e003a798f907d9f772eed9b7c6a9775d5bd00648b643ebfb88e25414/matplotlib-3.10.5-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee7a09ae2f4676276f5a65bd9f2bd91b4f9fbaedf49f40267ce3f9b448de501f", size = 9508646 }, + { url = "https://files.pythonhosted.org/packages/91/e7/c051a7a386680c28487bca27d23b02d84f63e3d2a9b4d2fc478e6a42e37e/matplotlib-3.10.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ba6c3c9c067b83481d647af88b4e441d532acdb5ef22178a14935b0b881188f4", size = 9567424 }, + { url = "https://files.pythonhosted.org/packages/36/c2/24302e93ff431b8f4173ee1dd88976c8d80483cadbc5d3d777cef47b3a1c/matplotlib-3.10.5-cp313-cp313-win_amd64.whl", hash = "sha256:07442d2692c9bd1cceaa4afb4bbe5b57b98a7599de4dabfcca92d3eea70f9ebe", size = 8107809 }, + { url = "https://files.pythonhosted.org/packages/0b/33/423ec6a668d375dad825197557ed8fbdb74d62b432c1ed8235465945475f/matplotlib-3.10.5-cp313-cp313-win_arm64.whl", hash = "sha256:48fe6d47380b68a37ccfcc94f009530e84d41f71f5dae7eda7c4a5a84aa0a674", size = 7978078 }, + { url = "https://files.pythonhosted.org/packages/51/17/521fc16ec766455c7bb52cc046550cf7652f6765ca8650ff120aa2d197b6/matplotlib-3.10.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3b80eb8621331449fc519541a7461987f10afa4f9cfd91afcd2276ebe19bd56c", size = 8295590 }, + { url = "https://files.pythonhosted.org/packages/f8/12/23c28b2c21114c63999bae129fce7fd34515641c517ae48ce7b7dcd33458/matplotlib-3.10.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:47a388908e469d6ca2a6015858fa924e0e8a2345a37125948d8e93a91c47933e", size = 8158518 }, + { url = "https://files.pythonhosted.org/packages/81/f8/aae4eb25e8e7190759f3cb91cbeaa344128159ac92bb6b409e24f8711f78/matplotlib-3.10.5-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8b6b49167d208358983ce26e43aa4196073b4702858670f2eb111f9a10652b4b", size = 8691815 }, + { url = "https://files.pythonhosted.org/packages/d0/ba/450c39ebdd486bd33a359fc17365ade46c6a96bf637bbb0df7824de2886c/matplotlib-3.10.5-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a8da0453a7fd8e3da114234ba70c5ba9ef0e98f190309ddfde0f089accd46ea", size = 9522814 }, + { url = "https://files.pythonhosted.org/packages/89/11/9c66f6a990e27bb9aa023f7988d2d5809cb98aa39c09cbf20fba75a542ef/matplotlib-3.10.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:52c6573dfcb7726a9907b482cd5b92e6b5499b284ffacb04ffbfe06b3e568124", size = 9573917 }, + { url = "https://files.pythonhosted.org/packages/b3/69/8b49394de92569419e5e05e82e83df9b749a0ff550d07631ea96ed2eb35a/matplotlib-3.10.5-cp313-cp313t-win_amd64.whl", hash = "sha256:a23193db2e9d64ece69cac0c8231849db7dd77ce59c7b89948cf9d0ce655a3ce", size = 8181034 }, + { url = "https://files.pythonhosted.org/packages/47/23/82dc435bb98a2fc5c20dffcac8f0b083935ac28286413ed8835df40d0baa/matplotlib-3.10.5-cp313-cp313t-win_arm64.whl", hash = "sha256:56da3b102cf6da2776fef3e71cd96fcf22103a13594a18ac9a9b31314e0be154", size = 8023337 }, + { url = "https://files.pythonhosted.org/packages/e4/eb/7d4c5de49eb78294e1a8e2be8a6ecff8b433e921b731412a56cd1abd3567/matplotlib-3.10.5-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b5fa2e941f77eb579005fb804026f9d0a1082276118d01cc6051d0d9626eaa7f", size = 8222360 }, + { url = "https://files.pythonhosted.org/packages/16/8a/e435db90927b66b16d69f8f009498775f4469f8de4d14b87856965e58eba/matplotlib-3.10.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1fc0d2a3241cdcb9daaca279204a3351ce9df3c0e7e621c7e04ec28aaacaca30", size = 8087462 }, + { url = "https://files.pythonhosted.org/packages/0b/dd/06c0e00064362f5647f318e00b435be2ff76a1bdced97c5eaf8347311fbe/matplotlib-3.10.5-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8dee65cb1424b7dc982fe87895b5613d4e691cc57117e8af840da0148ca6c1d7", size = 8659802 }, + { url = "https://files.pythonhosted.org/packages/dc/d6/e921be4e1a5f7aca5194e1f016cb67ec294548e530013251f630713e456d/matplotlib-3.10.5-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:160e125da27a749481eaddc0627962990f6029811dbeae23881833a011a0907f", size = 8233224 }, + { url = "https://files.pythonhosted.org/packages/ec/74/a2b9b04824b9c349c8f1b2d21d5af43fa7010039427f2b133a034cb09e59/matplotlib-3.10.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ac3d50760394d78a3c9be6b28318fe22b494c4fcf6407e8fd4794b538251899b", size = 8098539 }, + { url = "https://files.pythonhosted.org/packages/fc/66/cd29ebc7f6c0d2a15d216fb572573e8fc38bd5d6dec3bd9d7d904c0949f7/matplotlib-3.10.5-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6c49465bf689c4d59d174d0c7795fb42a21d4244d11d70e52b8011987367ac61", size = 8672192 }, +] + +[[package]] +name = "matplotlib-inline" +version = "0.1.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/99/5b/a36a337438a14116b16480db471ad061c36c3694df7c2084a0da7ba538b7/matplotlib_inline-0.1.7.tar.gz", hash = "sha256:8423b23ec666be3d16e16b60bdd8ac4e86e840ebd1dd11a30b9f117f2fa0ab90", size = 8159 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/8e/9ad090d3553c280a8060fbf6e24dc1c0c29704ee7d1c372f0c174aa59285/matplotlib_inline-0.1.7-py3-none-any.whl", hash = "sha256:df192d39a4ff8f21b1895d72e6a13f5fcc5099f00fa84384e0ea28c2cc0653ca", size = 9899 }, +] + +[[package]] +name = "mcp" +version = "1.12.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "httpx" }, + { name = "httpx-sse" }, + { name = "jsonschema" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "python-multipart" }, + { name = "pywin32", marker = "sys_platform == 'win32'" }, + { name = "sse-starlette" }, + { name = "starlette" }, + { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4d/19/9955e2df5384ff5dd25d38f8e88aaf89d2d3d9d39f27e7383eaf0b293836/mcp-1.12.3.tar.gz", hash = "sha256:ab2e05f5e5c13e1dc90a4a9ef23ac500a6121362a564447855ef0ab643a99fed", size = 427203 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/8b/0be74e3308a486f1d127f3f6767de5f9f76454c9b4183210c61cc50999b6/mcp-1.12.3-py3-none-any.whl", hash = "sha256:5483345bf39033b858920a5b6348a303acacf45b23936972160ff152107b850e", size = 158810 }, +] + +[[package]] +name = "mcpadapt" +version = "0.1.12" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jsonref" }, + { name = "mcp" }, + { name = "pydantic" }, + { name = "python-dotenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f4/f6/006dd6f7d527adc0f99df6aee89c6a9ecc651b315c99052a8e51146f7c1f/mcpadapt-0.1.12.tar.gz", hash = "sha256:e0b39a73c79d310b00d70ef5a10afbdebead6b7965cf158217e9b9517db8cc2e", size = 4224935 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/17/244566a37828772a33eca964401f82259a1b593566f7ac2281c5de600ff6/mcpadapt-0.1.12-py3-none-any.whl", hash = "sha256:f477a3f931a70e26d66f9cebde89f53404d8be298a5714e6c57948422402d1ed", size = 18449 }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 }, +] + +[[package]] +name = "mem0ai" +version = "0.1.115" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "openai" }, + { name = "posthog" }, + { name = "pydantic" }, + { name = "pytz" }, + { name = "qdrant-client" }, + { name = "sqlalchemy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/2a/ccf4a8b79a4e67d3a2475599ff8276d3bdccce5c5da2e14deb449e7dfb1a/mem0ai-0.1.115.tar.gz", hash = "sha256:147a6593604188acd30281c40171112aed9f16e196fa528627430c15e00f1e32", size = 115605 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/d5/55a5504077c175f4ff18259672df6fd0eae863236d730c87677d6de43f9a/mem0ai-0.1.115-py3-none-any.whl", hash = "sha256:29310bd5bcab644f7a4dbf87bd1afd878eb68458a2fb36cfcbf20bdff46fbdaf", size = 178065 }, +] + +[[package]] +name = "mmh3" +version = "5.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a7/af/f28c2c2f51f31abb4725f9a64bc7863d5f491f6539bd26aee2a1d21a649e/mmh3-5.2.0.tar.gz", hash = "sha256:1efc8fec8478e9243a78bb993422cf79f8ff85cb4cf6b79647480a31e0d950a8", size = 33582 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/2b/870f0ff5ecf312c58500f45950751f214b7068665e66e9bfd8bc2595587c/mmh3-5.2.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:81c504ad11c588c8629536b032940f2a359dda3b6cbfd4ad8f74cb24dcd1b0bc", size = 56119 }, + { url = "https://files.pythonhosted.org/packages/3b/88/eb9a55b3f3cf43a74d6bfa8db0e2e209f966007777a1dc897c52c008314c/mmh3-5.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0b898cecff57442724a0f52bf42c2de42de63083a91008fb452887e372f9c328", size = 40634 }, + { url = "https://files.pythonhosted.org/packages/d1/4c/8e4b3878bf8435c697d7ce99940a3784eb864521768069feaccaff884a17/mmh3-5.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:be1374df449465c9f2500e62eee73a39db62152a8bdfbe12ec5b5c1cd451344d", size = 40080 }, + { url = "https://files.pythonhosted.org/packages/45/ac/0a254402c8c5ca424a0a9ebfe870f5665922f932830f0a11a517b6390a09/mmh3-5.2.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b0d753ad566c721faa33db7e2e0eddd74b224cdd3eaf8481d76c926603c7a00e", size = 95321 }, + { url = "https://files.pythonhosted.org/packages/39/8e/29306d5eca6dfda4b899d22c95b5420db4e0ffb7e0b6389b17379654ece5/mmh3-5.2.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:dfbead5575f6470c17e955b94f92d62a03dfc3d07f2e6f817d9b93dc211a1515", size = 101220 }, + { url = "https://files.pythonhosted.org/packages/49/f7/0dd1368e531e52a17b5b8dd2f379cce813bff2d0978a7748a506f1231152/mmh3-5.2.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7434a27754049144539d2099a6d2da5d88b8bdeedf935180bf42ad59b3607aa3", size = 103991 }, + { url = "https://files.pythonhosted.org/packages/35/06/abc7122c40f4abbfcef01d2dac6ec0b77ede9757e5be8b8a40a6265b1274/mmh3-5.2.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cadc16e8ea64b5d9a47363013e2bea469e121e6e7cb416a7593aeb24f2ad122e", size = 110894 }, + { url = "https://files.pythonhosted.org/packages/f4/2f/837885759afa4baccb8e40456e1cf76a4f3eac835b878c727ae1286c5f82/mmh3-5.2.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d765058da196f68dc721116cab335e696e87e76720e6ef8ee5a24801af65e63d", size = 118327 }, + { url = "https://files.pythonhosted.org/packages/40/cc/5683ba20a21bcfb3f1605b1c474f46d30354f728a7412201f59f453d405a/mmh3-5.2.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8b0c53fe0994beade1ad7c0f13bd6fec980a0664bfbe5a6a7d64500b9ab76772", size = 101701 }, + { url = "https://files.pythonhosted.org/packages/0e/24/99ab3fb940150aec8a26dbdfc39b200b5592f6aeb293ec268df93e054c30/mmh3-5.2.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:49037d417419863b222ae47ee562b2de9c3416add0a45c8d7f4e864be8dc4f89", size = 96712 }, + { url = "https://files.pythonhosted.org/packages/61/04/d7c4cb18f1f001ede2e8aed0f9dbbfad03d161c9eea4fffb03f14f4523e5/mmh3-5.2.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:6ecb4e750d712abde046858ee6992b65c93f1f71b397fce7975c3860c07365d2", size = 110302 }, + { url = "https://files.pythonhosted.org/packages/d8/bf/4dac37580cfda74425a4547500c36fa13ef581c8a756727c37af45e11e9a/mmh3-5.2.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:382a6bb3f8c6532ea084e7acc5be6ae0c6effa529240836d59352398f002e3fc", size = 111929 }, + { url = "https://files.pythonhosted.org/packages/eb/b1/49f0a582c7a942fb71ddd1ec52b7d21d2544b37d2b2d994551346a15b4f6/mmh3-5.2.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7733ec52296fc1ba22e9b90a245c821adbb943e98c91d8a330a2254612726106", size = 100111 }, + { url = "https://files.pythonhosted.org/packages/dc/94/ccec09f438caeb2506f4c63bb3b99aa08a9e09880f8fc047295154756210/mmh3-5.2.0-cp310-cp310-win32.whl", hash = "sha256:127c95336f2a98c51e7682341ab7cb0be3adb9df0819ab8505a726ed1801876d", size = 40783 }, + { url = "https://files.pythonhosted.org/packages/ea/f4/8d39a32c8203c1cdae88fdb04d1ea4aa178c20f159df97f4c5a2eaec702c/mmh3-5.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:419005f84ba1cab47a77465a2a843562dadadd6671b8758bf179d82a15ca63eb", size = 41549 }, + { url = "https://files.pythonhosted.org/packages/cc/a1/30efb1cd945e193f62574144dd92a0c9ee6463435e4e8ffce9b9e9f032f0/mmh3-5.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:d22c9dcafed659fadc605538946c041722b6d1104fe619dbf5cc73b3c8a0ded8", size = 39335 }, + { url = "https://files.pythonhosted.org/packages/f7/87/399567b3796e134352e11a8b973cd470c06b2ecfad5468fe580833be442b/mmh3-5.2.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7901c893e704ee3c65f92d39b951f8f34ccf8e8566768c58103fb10e55afb8c1", size = 56107 }, + { url = "https://files.pythonhosted.org/packages/c3/09/830af30adf8678955b247d97d3d9543dd2fd95684f3cd41c0cd9d291da9f/mmh3-5.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4a5f5536b1cbfa72318ab3bfc8a8188b949260baed186b75f0abc75b95d8c051", size = 40635 }, + { url = "https://files.pythonhosted.org/packages/07/14/eaba79eef55b40d653321765ac5e8f6c9ac38780b8a7c2a2f8df8ee0fb72/mmh3-5.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cedac4f4054b8f7859e5aed41aaa31ad03fce6851901a7fdc2af0275ac533c10", size = 40078 }, + { url = "https://files.pythonhosted.org/packages/bb/26/83a0f852e763f81b2265d446b13ed6d49ee49e1fc0c47b9655977e6f3d81/mmh3-5.2.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:eb756caf8975882630ce4e9fbbeb9d3401242a72528230422c9ab3a0d278e60c", size = 97262 }, + { url = "https://files.pythonhosted.org/packages/00/7d/b7133b10d12239aeaebf6878d7eaf0bf7d3738c44b4aba3c564588f6d802/mmh3-5.2.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:097e13c8b8a66c5753c6968b7640faefe85d8e38992703c1f666eda6ef4c3762", size = 103118 }, + { url = "https://files.pythonhosted.org/packages/7b/3e/62f0b5dce2e22fd5b7d092aba285abd7959ea2b17148641e029f2eab1ffa/mmh3-5.2.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a7c0c7845566b9686480e6a7e9044db4afb60038d5fabd19227443f0104eeee4", size = 106072 }, + { url = "https://files.pythonhosted.org/packages/66/84/ea88bb816edfe65052c757a1c3408d65c4201ddbd769d4a287b0f1a628b2/mmh3-5.2.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:61ac226af521a572700f863d6ecddc6ece97220ce7174e311948ff8c8919a363", size = 112925 }, + { url = "https://files.pythonhosted.org/packages/2e/13/c9b1c022807db575fe4db806f442d5b5784547e2e82cff36133e58ea31c7/mmh3-5.2.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:582f9dbeefe15c32a5fa528b79b088b599a1dfe290a4436351c6090f90ddebb8", size = 120583 }, + { url = "https://files.pythonhosted.org/packages/8a/5f/0e2dfe1a38f6a78788b7eb2b23432cee24623aeabbc907fed07fc17d6935/mmh3-5.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2ebfc46b39168ab1cd44670a32ea5489bcbc74a25795c61b6d888c5c2cf654ed", size = 99127 }, + { url = "https://files.pythonhosted.org/packages/77/27/aefb7d663b67e6a0c4d61a513c83e39ba2237e8e4557fa7122a742a23de5/mmh3-5.2.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1556e31e4bd0ac0c17eaf220be17a09c171d7396919c3794274cb3415a9d3646", size = 98544 }, + { url = "https://files.pythonhosted.org/packages/ab/97/a21cc9b1a7c6e92205a1b5fa030cdf62277d177570c06a239eca7bd6dd32/mmh3-5.2.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:81df0dae22cd0da87f1c978602750f33d17fb3d21fb0f326c89dc89834fea79b", size = 106262 }, + { url = "https://files.pythonhosted.org/packages/43/18/db19ae82ea63c8922a880e1498a75342311f8aa0c581c4dd07711473b5f7/mmh3-5.2.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:eba01ec3bd4a49b9ac5ca2bc6a73ff5f3af53374b8556fcc2966dd2af9eb7779", size = 109824 }, + { url = "https://files.pythonhosted.org/packages/9f/f5/41dcf0d1969125fc6f61d8618b107c79130b5af50b18a4651210ea52ab40/mmh3-5.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e9a011469b47b752e7d20de296bb34591cdfcbe76c99c2e863ceaa2aa61113d2", size = 97255 }, + { url = "https://files.pythonhosted.org/packages/32/b3/cce9eaa0efac1f0e735bb178ef9d1d2887b4927fe0ec16609d5acd492dda/mmh3-5.2.0-cp311-cp311-win32.whl", hash = "sha256:bc44fc2b886243d7c0d8daeb37864e16f232e5b56aaec27cc781d848264cfd28", size = 40779 }, + { url = "https://files.pythonhosted.org/packages/7c/e9/3fa0290122e6d5a7041b50ae500b8a9f4932478a51e48f209a3879fe0b9b/mmh3-5.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:8ebf241072cf2777a492d0e09252f8cc2b3edd07dfdb9404b9757bffeb4f2cee", size = 41549 }, + { url = "https://files.pythonhosted.org/packages/3a/54/c277475b4102588e6f06b2e9095ee758dfe31a149312cdbf62d39a9f5c30/mmh3-5.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:b5f317a727bba0e633a12e71228bc6a4acb4f471a98b1c003163b917311ea9a9", size = 39336 }, + { url = "https://files.pythonhosted.org/packages/bf/6a/d5aa7edb5c08e0bd24286c7d08341a0446f9a2fbbb97d96a8a6dd81935ee/mmh3-5.2.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:384eda9361a7bf83a85e09447e1feafe081034af9dd428893701b959230d84be", size = 56141 }, + { url = "https://files.pythonhosted.org/packages/08/49/131d0fae6447bc4a7299ebdb1a6fb9d08c9f8dcf97d75ea93e8152ddf7ab/mmh3-5.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2c9da0d568569cc87315cb063486d761e38458b8ad513fedd3dc9263e1b81bcd", size = 40681 }, + { url = "https://files.pythonhosted.org/packages/8f/6f/9221445a6bcc962b7f5ff3ba18ad55bba624bacdc7aa3fc0a518db7da8ec/mmh3-5.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:86d1be5d63232e6eb93c50881aea55ff06eb86d8e08f9b5417c8c9b10db9db96", size = 40062 }, + { url = "https://files.pythonhosted.org/packages/1e/d4/6bb2d0fef81401e0bb4c297d1eb568b767de4ce6fc00890bc14d7b51ecc4/mmh3-5.2.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bf7bee43e17e81671c447e9c83499f53d99bf440bc6d9dc26a841e21acfbe094", size = 97333 }, + { url = "https://files.pythonhosted.org/packages/44/e0/ccf0daff8134efbb4fbc10a945ab53302e358c4b016ada9bf97a6bdd50c1/mmh3-5.2.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7aa18cdb58983ee660c9c400b46272e14fa253c675ed963d3812487f8ca42037", size = 103310 }, + { url = "https://files.pythonhosted.org/packages/02/63/1965cb08a46533faca0e420e06aff8bbaf9690a6f0ac6ae6e5b2e4544687/mmh3-5.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ae9d032488fcec32d22be6542d1a836f00247f40f320844dbb361393b5b22773", size = 106178 }, + { url = "https://files.pythonhosted.org/packages/c2/41/c883ad8e2c234013f27f92061200afc11554ea55edd1bcf5e1accd803a85/mmh3-5.2.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1861fb6b1d0453ed7293200139c0a9011eeb1376632e048e3766945b13313c5", size = 113035 }, + { url = "https://files.pythonhosted.org/packages/df/b5/1ccade8b1fa625d634a18bab7bf08a87457e09d5ec8cf83ca07cbea9d400/mmh3-5.2.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:99bb6a4d809aa4e528ddfe2c85dd5239b78b9dd14be62cca0329db78505e7b50", size = 120784 }, + { url = "https://files.pythonhosted.org/packages/77/1c/919d9171fcbdcdab242e06394464ccf546f7d0f3b31e0d1e3a630398782e/mmh3-5.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1f8d8b627799f4e2fcc7c034fed8f5f24dc7724ff52f69838a3d6d15f1ad4765", size = 99137 }, + { url = "https://files.pythonhosted.org/packages/66/8a/1eebef5bd6633d36281d9fc83cf2e9ba1ba0e1a77dff92aacab83001cee4/mmh3-5.2.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b5995088dd7023d2d9f310a0c67de5a2b2e06a570ecfd00f9ff4ab94a67cde43", size = 98664 }, + { url = "https://files.pythonhosted.org/packages/13/41/a5d981563e2ee682b21fb65e29cc0f517a6734a02b581359edd67f9d0360/mmh3-5.2.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1a5f4d2e59d6bba8ef01b013c472741835ad961e7c28f50c82b27c57748744a4", size = 106459 }, + { url = "https://files.pythonhosted.org/packages/24/31/342494cd6ab792d81e083680875a2c50fa0c5df475ebf0b67784f13e4647/mmh3-5.2.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fd6e6c3d90660d085f7e73710eab6f5545d4854b81b0135a3526e797009dbda3", size = 110038 }, + { url = "https://files.pythonhosted.org/packages/28/44/efda282170a46bb4f19c3e2b90536513b1d821c414c28469a227ca5a1789/mmh3-5.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c4a2f3d83879e3de2eb8cbf562e71563a8ed15ee9b9c2e77ca5d9f73072ac15c", size = 97545 }, + { url = "https://files.pythonhosted.org/packages/68/8f/534ae319c6e05d714f437e7206f78c17e66daca88164dff70286b0e8ea0c/mmh3-5.2.0-cp312-cp312-win32.whl", hash = "sha256:2421b9d665a0b1ad724ec7332fb5a98d075f50bc51a6ff854f3a1882bd650d49", size = 40805 }, + { url = "https://files.pythonhosted.org/packages/b8/f6/f6abdcfefcedab3c964868048cfe472764ed358c2bf6819a70dd4ed4ed3a/mmh3-5.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:72d80005b7634a3a2220f81fbeb94775ebd12794623bb2e1451701ea732b4aa3", size = 41597 }, + { url = "https://files.pythonhosted.org/packages/15/fd/f7420e8cbce45c259c770cac5718badf907b302d3a99ec587ba5ce030237/mmh3-5.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:3d6bfd9662a20c054bc216f861fa330c2dac7c81e7fb8307b5e32ab5b9b4d2e0", size = 39350 }, + { url = "https://files.pythonhosted.org/packages/d8/fa/27f6ab93995ef6ad9f940e96593c5dd24744d61a7389532b0fec03745607/mmh3-5.2.0-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:e79c00eba78f7258e5b354eccd4d7907d60317ced924ea4a5f2e9d83f5453065", size = 40874 }, + { url = "https://files.pythonhosted.org/packages/11/9c/03d13bcb6a03438bc8cac3d2e50f80908d159b31a4367c2e1a7a077ded32/mmh3-5.2.0-cp313-cp313-android_21_x86_64.whl", hash = "sha256:956127e663d05edbeec54df38885d943dfa27406594c411139690485128525de", size = 42012 }, + { url = "https://files.pythonhosted.org/packages/4e/78/0865d9765408a7d504f1789944e678f74e0888b96a766d578cb80b040999/mmh3-5.2.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:c3dca4cb5b946ee91b3d6bb700d137b1cd85c20827f89fdf9c16258253489044", size = 39197 }, + { url = "https://files.pythonhosted.org/packages/3e/12/76c3207bd186f98b908b6706c2317abb73756d23a4e68ea2bc94825b9015/mmh3-5.2.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:e651e17bfde5840e9e4174b01e9e080ce49277b70d424308b36a7969d0d1af73", size = 39840 }, + { url = "https://files.pythonhosted.org/packages/5d/0d/574b6cce5555c9f2b31ea189ad44986755eb14e8862db28c8b834b8b64dc/mmh3-5.2.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:9f64bf06f4bf623325fda3a6d02d36cd69199b9ace99b04bb2d7fd9f89688504", size = 40644 }, + { url = "https://files.pythonhosted.org/packages/52/82/3731f8640b79c46707f53ed72034a58baad400be908c87b0088f1f89f986/mmh3-5.2.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ddc63328889bcaee77b743309e5c7d2d52cee0d7d577837c91b6e7cc9e755e0b", size = 56153 }, + { url = "https://files.pythonhosted.org/packages/4f/34/e02dca1d4727fd9fdeaff9e2ad6983e1552804ce1d92cc796e5b052159bb/mmh3-5.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:bb0fdc451fb6d86d81ab8f23d881b8d6e37fc373a2deae1c02d27002d2ad7a05", size = 40684 }, + { url = "https://files.pythonhosted.org/packages/8f/36/3dee40767356e104967e6ed6d102ba47b0b1ce2a89432239b95a94de1b89/mmh3-5.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b29044e1ffdb84fe164d0a7ea05c7316afea93c00f8ed9449cf357c36fc4f814", size = 40057 }, + { url = "https://files.pythonhosted.org/packages/31/58/228c402fccf76eb39a0a01b8fc470fecf21965584e66453b477050ee0e99/mmh3-5.2.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:58981d6ea9646dbbf9e59a30890cbf9f610df0e4a57dbfe09215116fd90b0093", size = 97344 }, + { url = "https://files.pythonhosted.org/packages/34/82/fc5ce89006389a6426ef28e326fc065b0fbaaed230373b62d14c889f47ea/mmh3-5.2.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7e5634565367b6d98dc4aa2983703526ef556b3688ba3065edb4b9b90ede1c54", size = 103325 }, + { url = "https://files.pythonhosted.org/packages/09/8c/261e85777c6aee1ebd53f2f17e210e7481d5b0846cd0b4a5c45f1e3761b8/mmh3-5.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0271ac12415afd3171ab9a3c7cbfc71dee2c68760a7dc9d05bf8ed6ddfa3a7a", size = 106240 }, + { url = "https://files.pythonhosted.org/packages/70/73/2f76b3ad8a3d431824e9934403df36c0ddacc7831acf82114bce3c4309c8/mmh3-5.2.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:45b590e31bc552c6f8e2150ff1ad0c28dd151e9f87589e7eaf508fbdd8e8e908", size = 113060 }, + { url = "https://files.pythonhosted.org/packages/9f/b9/7ea61a34e90e50a79a9d87aa1c0b8139a7eaf4125782b34b7d7383472633/mmh3-5.2.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bdde97310d59604f2a9119322f61b31546748499a21b44f6715e8ced9308a6c5", size = 120781 }, + { url = "https://files.pythonhosted.org/packages/0f/5b/ae1a717db98c7894a37aeedbd94b3f99e6472a836488f36b6849d003485b/mmh3-5.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:fc9c5f280438cf1c1a8f9abb87dc8ce9630a964120cfb5dd50d1e7ce79690c7a", size = 99174 }, + { url = "https://files.pythonhosted.org/packages/e3/de/000cce1d799fceebb6d4487ae29175dd8e81b48e314cba7b4da90bcf55d7/mmh3-5.2.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:c903e71fd8debb35ad2a4184c1316b3cb22f64ce517b4e6747f25b0a34e41266", size = 98734 }, + { url = "https://files.pythonhosted.org/packages/79/19/0dc364391a792b72fbb22becfdeacc5add85cc043cd16986e82152141883/mmh3-5.2.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:eed4bba7ff8a0d37106ba931ab03bdd3915fbb025bcf4e1f0aa02bc8114960c5", size = 106493 }, + { url = "https://files.pythonhosted.org/packages/3c/b1/bc8c28e4d6e807bbb051fefe78e1156d7f104b89948742ad310612ce240d/mmh3-5.2.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:1fdb36b940e9261aff0b5177c5b74a36936b902f473180f6c15bde26143681a9", size = 110089 }, + { url = "https://files.pythonhosted.org/packages/3b/a2/d20f3f5c95e9c511806686c70d0a15479cc3941c5f322061697af1c1ff70/mmh3-5.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7303aab41e97adcf010a09efd8f1403e719e59b7705d5e3cfed3dd7571589290", size = 97571 }, + { url = "https://files.pythonhosted.org/packages/7b/23/665296fce4f33488deec39a750ffd245cfc07aafb0e3ef37835f91775d14/mmh3-5.2.0-cp313-cp313-win32.whl", hash = "sha256:03e08c6ebaf666ec1e3d6ea657a2d363bb01effd1a9acfe41f9197decaef0051", size = 40806 }, + { url = "https://files.pythonhosted.org/packages/59/b0/92e7103f3b20646e255b699e2d0327ce53a3f250e44367a99dc8be0b7c7a/mmh3-5.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:7fddccd4113e7b736706e17a239a696332360cbaddf25ae75b57ba1acce65081", size = 41600 }, + { url = "https://files.pythonhosted.org/packages/99/22/0b2bd679a84574647de538c5b07ccaa435dbccc37815067fe15b90fe8dad/mmh3-5.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:fa0c966ee727aad5406d516375593c5f058c766b21236ab8985693934bb5085b", size = 39349 }, +] + +[[package]] +name = "monotonic" +version = "1.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ea/ca/8e91948b782ddfbd194f323e7e7d9ba12e5877addf04fb2bf8fca38e86ac/monotonic-1.6.tar.gz", hash = "sha256:3a55207bcfed53ddd5c5bae174524062935efed17792e9de2ad0205ce9ad63f7", size = 7615 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/67/7e8406a29b6c45be7af7740456f7f37025f0506ae2e05fb9009a53946860/monotonic-1.6-py2.py3-none-any.whl", hash = "sha256:68687e19a14f11f26d140dd5c86f3dba4bf5df58003000ed467e0e2a69bca96c", size = 8154 }, +] + +[[package]] +name = "more-itertools" +version = "10.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ce/a0/834b0cebabbfc7e311f30b46c8188790a37f89fc8d756660346fe5abfd09/more_itertools-10.7.0.tar.gz", hash = "sha256:9fddd5403be01a94b204faadcff459ec3568cf110265d3c54323e1e866ad29d3", size = 127671 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2b/9f/7ba6f94fc1e9ac3d2b853fdff3035fb2fa5afbed898c4a72b8a020610594/more_itertools-10.7.0-py3-none-any.whl", hash = "sha256:d43980384673cb07d2f7d2d918c616b30c659c089ee23953f601d6609c67510e", size = 65278 }, +] + +[[package]] +name = "mpmath" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", size = 508106 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198 }, +] + +[[package]] +name = "msoffcrypto-tool" +version = "5.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "olefile" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d2/b7/0fd6573157e0ec60c0c470e732ab3322fba4d2834fd24e1088d670522a01/msoffcrypto_tool-5.4.2.tar.gz", hash = "sha256:44b545adba0407564a0cc3d6dde6ca36b7c0fdf352b85bca51618fa1d4817370", size = 41183 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/54/7f6d3d9acad083dae8c22d9ab483b657359a1bf56fee1d7af88794677707/msoffcrypto_tool-5.4.2-py3-none-any.whl", hash = "sha256:274fe2181702d1e5a107ec1b68a4c9fea997a44972ae1cc9ae0cb4f6a50fef0e", size = 48713 }, +] + +[[package]] +name = "multidict" +version = "6.6.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/2c/5dad12e82fbdf7470f29bff2171484bf07cb3b16ada60a6589af8f376440/multidict-6.6.3.tar.gz", hash = "sha256:798a9eb12dab0a6c2e29c1de6f3468af5cb2da6053a20dfa3344907eed0937cc", size = 101006 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/67/414933982bce2efce7cbcb3169eaaf901e0f25baec69432b4874dfb1f297/multidict-6.6.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a2be5b7b35271f7fff1397204ba6708365e3d773579fe2a30625e16c4b4ce817", size = 77017 }, + { url = "https://files.pythonhosted.org/packages/8a/fe/d8a3ee1fad37dc2ef4f75488b0d9d4f25bf204aad8306cbab63d97bff64a/multidict-6.6.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:12f4581d2930840295c461764b9a65732ec01250b46c6b2c510d7ee68872b140", size = 44897 }, + { url = "https://files.pythonhosted.org/packages/1f/e0/265d89af8c98240265d82b8cbcf35897f83b76cd59ee3ab3879050fd8c45/multidict-6.6.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dd7793bab517e706c9ed9d7310b06c8672fd0aeee5781bfad612f56b8e0f7d14", size = 44574 }, + { url = "https://files.pythonhosted.org/packages/e6/05/6b759379f7e8e04ccc97cfb2a5dcc5cdbd44a97f072b2272dc51281e6a40/multidict-6.6.3-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:72d8815f2cd3cf3df0f83cac3f3ef801d908b2d90409ae28102e0553af85545a", size = 225729 }, + { url = "https://files.pythonhosted.org/packages/4e/f5/8d5a15488edd9a91fa4aad97228d785df208ed6298580883aa3d9def1959/multidict-6.6.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:531e331a2ee53543ab32b16334e2deb26f4e6b9b28e41f8e0c87e99a6c8e2d69", size = 242515 }, + { url = "https://files.pythonhosted.org/packages/6e/b5/a8f317d47d0ac5bb746d6d8325885c8967c2a8ce0bb57be5399e3642cccb/multidict-6.6.3-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:42ca5aa9329a63be8dc49040f63817d1ac980e02eeddba763a9ae5b4027b9c9c", size = 222224 }, + { url = "https://files.pythonhosted.org/packages/76/88/18b2a0d5e80515fa22716556061189c2853ecf2aa2133081ebbe85ebea38/multidict-6.6.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:208b9b9757060b9faa6f11ab4bc52846e4f3c2fb8b14d5680c8aac80af3dc751", size = 253124 }, + { url = "https://files.pythonhosted.org/packages/62/bf/ebfcfd6b55a1b05ef16d0775ae34c0fe15e8dab570d69ca9941073b969e7/multidict-6.6.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:acf6b97bd0884891af6a8b43d0f586ab2fcf8e717cbd47ab4bdddc09e20652d8", size = 251529 }, + { url = "https://files.pythonhosted.org/packages/44/11/780615a98fd3775fc309d0234d563941af69ade2df0bb82c91dda6ddaea1/multidict-6.6.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:68e9e12ed00e2089725669bdc88602b0b6f8d23c0c95e52b95f0bc69f7fe9b55", size = 241627 }, + { url = "https://files.pythonhosted.org/packages/28/3d/35f33045e21034b388686213752cabc3a1b9d03e20969e6fa8f1b1d82db1/multidict-6.6.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:05db2f66c9addb10cfa226e1acb363450fab2ff8a6df73c622fefe2f5af6d4e7", size = 239351 }, + { url = "https://files.pythonhosted.org/packages/6e/cc/ff84c03b95b430015d2166d9aae775a3985d757b94f6635010d0038d9241/multidict-6.6.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:0db58da8eafb514db832a1b44f8fa7906fdd102f7d982025f816a93ba45e3dcb", size = 233429 }, + { url = "https://files.pythonhosted.org/packages/2e/f0/8cd49a0b37bdea673a4b793c2093f2f4ba8e7c9d6d7c9bd672fd6d38cd11/multidict-6.6.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:14117a41c8fdb3ee19c743b1c027da0736fdb79584d61a766da53d399b71176c", size = 243094 }, + { url = "https://files.pythonhosted.org/packages/96/19/5d9a0cfdafe65d82b616a45ae950975820289069f885328e8185e64283c2/multidict-6.6.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:877443eaaabcd0b74ff32ebeed6f6176c71850feb7d6a1d2db65945256ea535c", size = 248957 }, + { url = "https://files.pythonhosted.org/packages/e6/dc/c90066151da87d1e489f147b9b4327927241e65f1876702fafec6729c014/multidict-6.6.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:70b72e749a4f6e7ed8fb334fa8d8496384840319512746a5f42fa0aec79f4d61", size = 243590 }, + { url = "https://files.pythonhosted.org/packages/ec/39/458afb0cccbb0ee9164365273be3e039efddcfcb94ef35924b7dbdb05db0/multidict-6.6.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:43571f785b86afd02b3855c5ac8e86ec921b760298d6f82ff2a61daf5a35330b", size = 237487 }, + { url = "https://files.pythonhosted.org/packages/35/38/0016adac3990426610a081787011177e661875546b434f50a26319dc8372/multidict-6.6.3-cp310-cp310-win32.whl", hash = "sha256:20c5a0c3c13a15fd5ea86c42311859f970070e4e24de5a550e99d7c271d76318", size = 41390 }, + { url = "https://files.pythonhosted.org/packages/f3/d2/17897a8f3f2c5363d969b4c635aa40375fe1f09168dc09a7826780bfb2a4/multidict-6.6.3-cp310-cp310-win_amd64.whl", hash = "sha256:ab0a34a007704c625e25a9116c6770b4d3617a071c8a7c30cd338dfbadfe6485", size = 45954 }, + { url = "https://files.pythonhosted.org/packages/2d/5f/d4a717c1e457fe44072e33fa400d2b93eb0f2819c4d669381f925b7cba1f/multidict-6.6.3-cp310-cp310-win_arm64.whl", hash = "sha256:769841d70ca8bdd140a715746199fc6473414bd02efd678d75681d2d6a8986c5", size = 42981 }, + { url = "https://files.pythonhosted.org/packages/08/f0/1a39863ced51f639c81a5463fbfa9eb4df59c20d1a8769ab9ef4ca57ae04/multidict-6.6.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:18f4eba0cbac3546b8ae31e0bbc55b02c801ae3cbaf80c247fcdd89b456ff58c", size = 76445 }, + { url = "https://files.pythonhosted.org/packages/c9/0e/a7cfa451c7b0365cd844e90b41e21fab32edaa1e42fc0c9f68461ce44ed7/multidict-6.6.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef43b5dd842382329e4797c46f10748d8c2b6e0614f46b4afe4aee9ac33159df", size = 44610 }, + { url = "https://files.pythonhosted.org/packages/c6/bb/a14a4efc5ee748cc1904b0748be278c31b9295ce5f4d2ef66526f410b94d/multidict-6.6.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bf9bd1fd5eec01494e0f2e8e446a74a85d5e49afb63d75a9934e4a5423dba21d", size = 44267 }, + { url = "https://files.pythonhosted.org/packages/c2/f8/410677d563c2d55e063ef74fe578f9d53fe6b0a51649597a5861f83ffa15/multidict-6.6.3-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:5bd8d6f793a787153956cd35e24f60485bf0651c238e207b9a54f7458b16d539", size = 230004 }, + { url = "https://files.pythonhosted.org/packages/fd/df/2b787f80059314a98e1ec6a4cc7576244986df3e56b3c755e6fc7c99e038/multidict-6.6.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1bf99b4daf908c73856bd87ee0a2499c3c9a3d19bb04b9c6025e66af3fd07462", size = 247196 }, + { url = "https://files.pythonhosted.org/packages/05/f2/f9117089151b9a8ab39f9019620d10d9718eec2ac89e7ca9d30f3ec78e96/multidict-6.6.3-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b9e59946b49dafaf990fd9c17ceafa62976e8471a14952163d10a7a630413a9", size = 225337 }, + { url = "https://files.pythonhosted.org/packages/93/2d/7115300ec5b699faa152c56799b089a53ed69e399c3c2d528251f0aeda1a/multidict-6.6.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e2db616467070d0533832d204c54eea6836a5e628f2cb1e6dfd8cd6ba7277cb7", size = 257079 }, + { url = "https://files.pythonhosted.org/packages/15/ea/ff4bab367623e39c20d3b07637225c7688d79e4f3cc1f3b9f89867677f9a/multidict-6.6.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7394888236621f61dcdd25189b2768ae5cc280f041029a5bcf1122ac63df79f9", size = 255461 }, + { url = "https://files.pythonhosted.org/packages/74/07/2c9246cda322dfe08be85f1b8739646f2c4c5113a1422d7a407763422ec4/multidict-6.6.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f114d8478733ca7388e7c7e0ab34b72547476b97009d643644ac33d4d3fe1821", size = 246611 }, + { url = "https://files.pythonhosted.org/packages/a8/62/279c13d584207d5697a752a66ffc9bb19355a95f7659140cb1b3cf82180e/multidict-6.6.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cdf22e4db76d323bcdc733514bf732e9fb349707c98d341d40ebcc6e9318ef3d", size = 243102 }, + { url = "https://files.pythonhosted.org/packages/69/cc/e06636f48c6d51e724a8bc8d9e1db5f136fe1df066d7cafe37ef4000f86a/multidict-6.6.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:e995a34c3d44ab511bfc11aa26869b9d66c2d8c799fa0e74b28a473a692532d6", size = 238693 }, + { url = "https://files.pythonhosted.org/packages/89/a4/66c9d8fb9acf3b226cdd468ed009537ac65b520aebdc1703dd6908b19d33/multidict-6.6.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:766a4a5996f54361d8d5a9050140aa5362fe48ce51c755a50c0bc3706460c430", size = 246582 }, + { url = "https://files.pythonhosted.org/packages/cf/01/c69e0317be556e46257826d5449feb4e6aa0d18573e567a48a2c14156f1f/multidict-6.6.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:3893a0d7d28a7fe6ca7a1f760593bc13038d1d35daf52199d431b61d2660602b", size = 253355 }, + { url = "https://files.pythonhosted.org/packages/c0/da/9cc1da0299762d20e626fe0042e71b5694f9f72d7d3f9678397cbaa71b2b/multidict-6.6.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:934796c81ea996e61914ba58064920d6cad5d99140ac3167901eb932150e2e56", size = 247774 }, + { url = "https://files.pythonhosted.org/packages/e6/91/b22756afec99cc31105ddd4a52f95ab32b1a4a58f4d417979c570c4a922e/multidict-6.6.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9ed948328aec2072bc00f05d961ceadfd3e9bfc2966c1319aeaf7b7c21219183", size = 242275 }, + { url = "https://files.pythonhosted.org/packages/be/f1/adcc185b878036a20399d5be5228f3cbe7f823d78985d101d425af35c800/multidict-6.6.3-cp311-cp311-win32.whl", hash = "sha256:9f5b28c074c76afc3e4c610c488e3493976fe0e596dd3db6c8ddfbb0134dcac5", size = 41290 }, + { url = "https://files.pythonhosted.org/packages/e0/d4/27652c1c6526ea6b4f5ddd397e93f4232ff5de42bea71d339bc6a6cc497f/multidict-6.6.3-cp311-cp311-win_amd64.whl", hash = "sha256:bc7f6fbc61b1c16050a389c630da0b32fc6d4a3d191394ab78972bf5edc568c2", size = 45942 }, + { url = "https://files.pythonhosted.org/packages/16/18/23f4932019804e56d3c2413e237f866444b774b0263bcb81df2fdecaf593/multidict-6.6.3-cp311-cp311-win_arm64.whl", hash = "sha256:d4e47d8faffaae822fb5cba20937c048d4f734f43572e7079298a6c39fb172cb", size = 42880 }, + { url = "https://files.pythonhosted.org/packages/0e/a0/6b57988ea102da0623ea814160ed78d45a2645e4bbb499c2896d12833a70/multidict-6.6.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:056bebbeda16b2e38642d75e9e5310c484b7c24e3841dc0fb943206a72ec89d6", size = 76514 }, + { url = "https://files.pythonhosted.org/packages/07/7a/d1e92665b0850c6c0508f101f9cf0410c1afa24973e1115fe9c6a185ebf7/multidict-6.6.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e5f481cccb3c5c5e5de5d00b5141dc589c1047e60d07e85bbd7dea3d4580d63f", size = 45394 }, + { url = "https://files.pythonhosted.org/packages/52/6f/dd104490e01be6ef8bf9573705d8572f8c2d2c561f06e3826b081d9e6591/multidict-6.6.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:10bea2ee839a759ee368b5a6e47787f399b41e70cf0c20d90dfaf4158dfb4e55", size = 43590 }, + { url = "https://files.pythonhosted.org/packages/44/fe/06e0e01b1b0611e6581b7fd5a85b43dacc08b6cea3034f902f383b0873e5/multidict-6.6.3-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:2334cfb0fa9549d6ce2c21af2bfbcd3ac4ec3646b1b1581c88e3e2b1779ec92b", size = 237292 }, + { url = "https://files.pythonhosted.org/packages/ce/71/4f0e558fb77696b89c233c1ee2d92f3e1d5459070a0e89153c9e9e804186/multidict-6.6.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8fee016722550a2276ca2cb5bb624480e0ed2bd49125b2b73b7010b9090e888", size = 258385 }, + { url = "https://files.pythonhosted.org/packages/e3/25/cca0e68228addad24903801ed1ab42e21307a1b4b6dd2cf63da5d3ae082a/multidict-6.6.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5511cb35f5c50a2db21047c875eb42f308c5583edf96bd8ebf7d770a9d68f6d", size = 242328 }, + { url = "https://files.pythonhosted.org/packages/6e/a3/46f2d420d86bbcb8fe660b26a10a219871a0fbf4d43cb846a4031533f3e0/multidict-6.6.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:712b348f7f449948e0a6c4564a21c7db965af900973a67db432d724619b3c680", size = 268057 }, + { url = "https://files.pythonhosted.org/packages/9e/73/1c743542fe00794a2ec7466abd3f312ccb8fad8dff9f36d42e18fb1ec33e/multidict-6.6.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e4e15d2138ee2694e038e33b7c3da70e6b0ad8868b9f8094a72e1414aeda9c1a", size = 269341 }, + { url = "https://files.pythonhosted.org/packages/a4/11/6ec9dcbe2264b92778eeb85407d1df18812248bf3506a5a1754bc035db0c/multidict-6.6.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8df25594989aebff8a130f7899fa03cbfcc5d2b5f4a461cf2518236fe6f15961", size = 256081 }, + { url = "https://files.pythonhosted.org/packages/9b/2b/631b1e2afeb5f1696846d747d36cda075bfdc0bc7245d6ba5c319278d6c4/multidict-6.6.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:159ca68bfd284a8860f8d8112cf0521113bffd9c17568579e4d13d1f1dc76b65", size = 253581 }, + { url = "https://files.pythonhosted.org/packages/bf/0e/7e3b93f79efeb6111d3bf9a1a69e555ba1d07ad1c11bceb56b7310d0d7ee/multidict-6.6.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:e098c17856a8c9ade81b4810888c5ad1914099657226283cab3062c0540b0643", size = 250750 }, + { url = "https://files.pythonhosted.org/packages/ad/9e/086846c1d6601948e7de556ee464a2d4c85e33883e749f46b9547d7b0704/multidict-6.6.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:67c92ed673049dec52d7ed39f8cf9ebbadf5032c774058b4406d18c8f8fe7063", size = 251548 }, + { url = "https://files.pythonhosted.org/packages/8c/7b/86ec260118e522f1a31550e87b23542294880c97cfbf6fb18cc67b044c66/multidict-6.6.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:bd0578596e3a835ef451784053cfd327d607fc39ea1a14812139339a18a0dbc3", size = 262718 }, + { url = "https://files.pythonhosted.org/packages/8c/bd/22ce8f47abb0be04692c9fc4638508b8340987b18691aa7775d927b73f72/multidict-6.6.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:346055630a2df2115cd23ae271910b4cae40f4e336773550dca4889b12916e75", size = 259603 }, + { url = "https://files.pythonhosted.org/packages/07/9c/91b7ac1691be95cd1f4a26e36a74b97cda6aa9820632d31aab4410f46ebd/multidict-6.6.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:555ff55a359302b79de97e0468e9ee80637b0de1fce77721639f7cd9440b3a10", size = 251351 }, + { url = "https://files.pythonhosted.org/packages/6f/5c/4d7adc739884f7a9fbe00d1eac8c034023ef8bad71f2ebe12823ca2e3649/multidict-6.6.3-cp312-cp312-win32.whl", hash = "sha256:73ab034fb8d58ff85c2bcbadc470efc3fafeea8affcf8722855fb94557f14cc5", size = 41860 }, + { url = "https://files.pythonhosted.org/packages/6a/a3/0fbc7afdf7cb1aa12a086b02959307848eb6bcc8f66fcb66c0cb57e2a2c1/multidict-6.6.3-cp312-cp312-win_amd64.whl", hash = "sha256:04cbcce84f63b9af41bad04a54d4cc4e60e90c35b9e6ccb130be2d75b71f8c17", size = 45982 }, + { url = "https://files.pythonhosted.org/packages/b8/95/8c825bd70ff9b02462dc18d1295dd08d3e9e4eb66856d292ffa62cfe1920/multidict-6.6.3-cp312-cp312-win_arm64.whl", hash = "sha256:0f1130b896ecb52d2a1e615260f3ea2af55fa7dc3d7c3003ba0c3121a759b18b", size = 43210 }, + { url = "https://files.pythonhosted.org/packages/52/1d/0bebcbbb4f000751fbd09957257903d6e002943fc668d841a4cf2fb7f872/multidict-6.6.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:540d3c06d48507357a7d57721e5094b4f7093399a0106c211f33540fdc374d55", size = 75843 }, + { url = "https://files.pythonhosted.org/packages/07/8f/cbe241b0434cfe257f65c2b1bcf9e8d5fb52bc708c5061fb29b0fed22bdf/multidict-6.6.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9c19cea2a690f04247d43f366d03e4eb110a0dc4cd1bbeee4d445435428ed35b", size = 45053 }, + { url = "https://files.pythonhosted.org/packages/32/d2/0b3b23f9dbad5b270b22a3ac3ea73ed0a50ef2d9a390447061178ed6bdb8/multidict-6.6.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7af039820cfd00effec86bda5d8debef711a3e86a1d3772e85bea0f243a4bd65", size = 43273 }, + { url = "https://files.pythonhosted.org/packages/fd/fe/6eb68927e823999e3683bc49678eb20374ba9615097d085298fd5b386564/multidict-6.6.3-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:500b84f51654fdc3944e936f2922114349bf8fdcac77c3092b03449f0e5bc2b3", size = 237124 }, + { url = "https://files.pythonhosted.org/packages/e7/ab/320d8507e7726c460cb77117848b3834ea0d59e769f36fdae495f7669929/multidict-6.6.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f3fc723ab8a5c5ed6c50418e9bfcd8e6dceba6c271cee6728a10a4ed8561520c", size = 256892 }, + { url = "https://files.pythonhosted.org/packages/76/60/38ee422db515ac69834e60142a1a69111ac96026e76e8e9aa347fd2e4591/multidict-6.6.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:94c47ea3ade005b5976789baaed66d4de4480d0a0bf31cef6edaa41c1e7b56a6", size = 240547 }, + { url = "https://files.pythonhosted.org/packages/27/fb/905224fde2dff042b030c27ad95a7ae744325cf54b890b443d30a789b80e/multidict-6.6.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:dbc7cf464cc6d67e83e136c9f55726da3a30176f020a36ead246eceed87f1cd8", size = 266223 }, + { url = "https://files.pythonhosted.org/packages/76/35/dc38ab361051beae08d1a53965e3e1a418752fc5be4d3fb983c5582d8784/multidict-6.6.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:900eb9f9da25ada070f8ee4a23f884e0ee66fe4e1a38c3af644256a508ad81ca", size = 267262 }, + { url = "https://files.pythonhosted.org/packages/1f/a3/0a485b7f36e422421b17e2bbb5a81c1af10eac1d4476f2ff92927c730479/multidict-6.6.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7c6df517cf177da5d47ab15407143a89cd1a23f8b335f3a28d57e8b0a3dbb884", size = 254345 }, + { url = "https://files.pythonhosted.org/packages/b4/59/bcdd52c1dab7c0e0d75ff19cac751fbd5f850d1fc39172ce809a74aa9ea4/multidict-6.6.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4ef421045f13879e21c994b36e728d8e7d126c91a64b9185810ab51d474f27e7", size = 252248 }, + { url = "https://files.pythonhosted.org/packages/bb/a4/2d96aaa6eae8067ce108d4acee6f45ced5728beda55c0f02ae1072c730d1/multidict-6.6.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:6c1e61bb4f80895c081790b6b09fa49e13566df8fbff817da3f85b3a8192e36b", size = 250115 }, + { url = "https://files.pythonhosted.org/packages/25/d2/ed9f847fa5c7d0677d4f02ea2c163d5e48573de3f57bacf5670e43a5ffaa/multidict-6.6.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e5e8523bb12d7623cd8300dbd91b9e439a46a028cd078ca695eb66ba31adee3c", size = 249649 }, + { url = "https://files.pythonhosted.org/packages/1f/af/9155850372563fc550803d3f25373308aa70f59b52cff25854086ecb4a79/multidict-6.6.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:ef58340cc896219e4e653dade08fea5c55c6df41bcc68122e3be3e9d873d9a7b", size = 261203 }, + { url = "https://files.pythonhosted.org/packages/36/2f/c6a728f699896252cf309769089568a33c6439626648843f78743660709d/multidict-6.6.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:fc9dc435ec8699e7b602b94fe0cd4703e69273a01cbc34409af29e7820f777f1", size = 258051 }, + { url = "https://files.pythonhosted.org/packages/d0/60/689880776d6b18fa2b70f6cc74ff87dd6c6b9b47bd9cf74c16fecfaa6ad9/multidict-6.6.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9e864486ef4ab07db5e9cb997bad2b681514158d6954dd1958dfb163b83d53e6", size = 249601 }, + { url = "https://files.pythonhosted.org/packages/75/5e/325b11f2222a549019cf2ef879c1f81f94a0d40ace3ef55cf529915ba6cc/multidict-6.6.3-cp313-cp313-win32.whl", hash = "sha256:5633a82fba8e841bc5c5c06b16e21529573cd654f67fd833650a215520a6210e", size = 41683 }, + { url = "https://files.pythonhosted.org/packages/b1/ad/cf46e73f5d6e3c775cabd2a05976547f3f18b39bee06260369a42501f053/multidict-6.6.3-cp313-cp313-win_amd64.whl", hash = "sha256:e93089c1570a4ad54c3714a12c2cef549dc9d58e97bcded193d928649cab78e9", size = 45811 }, + { url = "https://files.pythonhosted.org/packages/c5/c9/2e3fe950db28fb7c62e1a5f46e1e38759b072e2089209bc033c2798bb5ec/multidict-6.6.3-cp313-cp313-win_arm64.whl", hash = "sha256:c60b401f192e79caec61f166da9c924e9f8bc65548d4246842df91651e83d600", size = 43056 }, + { url = "https://files.pythonhosted.org/packages/3a/58/aaf8114cf34966e084a8cc9517771288adb53465188843d5a19862cb6dc3/multidict-6.6.3-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:02fd8f32d403a6ff13864b0851f1f523d4c988051eea0471d4f1fd8010f11134", size = 82811 }, + { url = "https://files.pythonhosted.org/packages/71/af/5402e7b58a1f5b987a07ad98f2501fdba2a4f4b4c30cf114e3ce8db64c87/multidict-6.6.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:f3aa090106b1543f3f87b2041eef3c156c8da2aed90c63a2fbed62d875c49c37", size = 48304 }, + { url = "https://files.pythonhosted.org/packages/39/65/ab3c8cafe21adb45b24a50266fd747147dec7847425bc2a0f6934b3ae9ce/multidict-6.6.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e924fb978615a5e33ff644cc42e6aa241effcf4f3322c09d4f8cebde95aff5f8", size = 46775 }, + { url = "https://files.pythonhosted.org/packages/49/ba/9fcc1b332f67cc0c0c8079e263bfab6660f87fe4e28a35921771ff3eea0d/multidict-6.6.3-cp313-cp313t-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:b9fe5a0e57c6dbd0e2ce81ca66272282c32cd11d31658ee9553849d91289e1c1", size = 229773 }, + { url = "https://files.pythonhosted.org/packages/a4/14/0145a251f555f7c754ce2dcbcd012939bbd1f34f066fa5d28a50e722a054/multidict-6.6.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b24576f208793ebae00280c59927c3b7c2a3b1655e443a25f753c4611bc1c373", size = 250083 }, + { url = "https://files.pythonhosted.org/packages/9e/d4/d5c0bd2bbb173b586c249a151a26d2fb3ec7d53c96e42091c9fef4e1f10c/multidict-6.6.3-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:135631cb6c58eac37d7ac0df380294fecdc026b28837fa07c02e459c7fb9c54e", size = 228980 }, + { url = "https://files.pythonhosted.org/packages/21/32/c9a2d8444a50ec48c4733ccc67254100c10e1c8ae8e40c7a2d2183b59b97/multidict-6.6.3-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:274d416b0df887aef98f19f21578653982cfb8a05b4e187d4a17103322eeaf8f", size = 257776 }, + { url = "https://files.pythonhosted.org/packages/68/d0/14fa1699f4ef629eae08ad6201c6b476098f5efb051b296f4c26be7a9fdf/multidict-6.6.3-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e252017a817fad7ce05cafbe5711ed40faeb580e63b16755a3a24e66fa1d87c0", size = 256882 }, + { url = "https://files.pythonhosted.org/packages/da/88/84a27570fbe303c65607d517a5f147cd2fc046c2d1da02b84b17b9bdc2aa/multidict-6.6.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e4cc8d848cd4fe1cdee28c13ea79ab0ed37fc2e89dd77bac86a2e7959a8c3bc", size = 247816 }, + { url = "https://files.pythonhosted.org/packages/1c/60/dca352a0c999ce96a5d8b8ee0b2b9f729dcad2e0b0c195f8286269a2074c/multidict-6.6.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9e236a7094b9c4c1b7585f6b9cca34b9d833cf079f7e4c49e6a4a6ec9bfdc68f", size = 245341 }, + { url = "https://files.pythonhosted.org/packages/50/ef/433fa3ed06028f03946f3993223dada70fb700f763f70c00079533c34578/multidict-6.6.3-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:e0cb0ab69915c55627c933f0b555a943d98ba71b4d1c57bc0d0a66e2567c7471", size = 235854 }, + { url = "https://files.pythonhosted.org/packages/1b/1f/487612ab56fbe35715320905215a57fede20de7db40a261759690dc80471/multidict-6.6.3-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:81ef2f64593aba09c5212a3d0f8c906a0d38d710a011f2f42759704d4557d3f2", size = 243432 }, + { url = "https://files.pythonhosted.org/packages/da/6f/ce8b79de16cd885c6f9052c96a3671373d00c59b3ee635ea93e6e81b8ccf/multidict-6.6.3-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:b9cbc60010de3562545fa198bfc6d3825df430ea96d2cc509c39bd71e2e7d648", size = 252731 }, + { url = "https://files.pythonhosted.org/packages/bb/fe/a2514a6aba78e5abefa1624ca85ae18f542d95ac5cde2e3815a9fbf369aa/multidict-6.6.3-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:70d974eaaa37211390cd02ef93b7e938de564bbffa866f0b08d07e5e65da783d", size = 247086 }, + { url = "https://files.pythonhosted.org/packages/8c/22/b788718d63bb3cce752d107a57c85fcd1a212c6c778628567c9713f9345a/multidict-6.6.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:3713303e4a6663c6d01d648a68f2848701001f3390a030edaaf3fc949c90bf7c", size = 243338 }, + { url = "https://files.pythonhosted.org/packages/22/d6/fdb3d0670819f2228f3f7d9af613d5e652c15d170c83e5f1c94fbc55a25b/multidict-6.6.3-cp313-cp313t-win32.whl", hash = "sha256:639ecc9fe7cd73f2495f62c213e964843826f44505a3e5d82805aa85cac6f89e", size = 47812 }, + { url = "https://files.pythonhosted.org/packages/b6/d6/a9d2c808f2c489ad199723197419207ecbfbc1776f6e155e1ecea9c883aa/multidict-6.6.3-cp313-cp313t-win_amd64.whl", hash = "sha256:9f97e181f344a0ef3881b573d31de8542cc0dbc559ec68c8f8b5ce2c2e91646d", size = 53011 }, + { url = "https://files.pythonhosted.org/packages/f2/40/b68001cba8188dd267590a111f9661b6256debc327137667e832bf5d66e8/multidict-6.6.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ce8b7693da41a3c4fde5871c738a81490cea5496c671d74374c8ab889e1834fb", size = 45254 }, + { url = "https://files.pythonhosted.org/packages/d8/30/9aec301e9772b098c1f5c0ca0279237c9766d94b97802e9888010c64b0ed/multidict-6.6.3-py3-none-any.whl", hash = "sha256:8db10f29c7541fc5da4defd8cd697e1ca429db743fa716325f236079b96f775a", size = 12313 }, +] + +[[package]] +name = "multion" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, + { name = "httpx-sse" }, + { name = "pydantic" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a2/88/08fe355223be0ff0f9d6c975958235a0306de091c16a0fa2b5eea533a3b4/multion-1.1.0.tar.gz", hash = "sha256:a71780426a5401a528eadc89206e2217e8a5b1e4fd332952418716675f32cf81", size = 19245 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/9e/b7f6b33222978688afc613e25e73776076e996cb5e545e37af8e373d3b3c/multion-1.1.0-py3-none-any.whl", hash = "sha256:6a4ffa2d71c5667e41492993e7136fa71eb4b52f0c11914f3a737ffd543195ca", size = 39968 }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963 }, +] + +[[package]] +name = "nest-asyncio" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/83/f8/51569ac65d696c8ecbee95938f89d4abf00f47d58d48f6fbabfe8f0baefe/nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe", size = 7418 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c", size = 5195 }, +] + +[[package]] +name = "networkx" +version = "3.4.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11' and platform_python_implementation == 'PyPy' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and platform_python_implementation == 'PyPy' and sys_platform == 'linux'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and platform_python_implementation == 'PyPy' and sys_platform == 'linux') or (python_full_version < '3.11' and platform_python_implementation == 'PyPy' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version < '3.11' and platform_python_implementation != 'PyPy' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and platform_python_implementation != 'PyPy' and sys_platform == 'linux'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and platform_python_implementation != 'PyPy' and sys_platform == 'linux') or (python_full_version < '3.11' and platform_python_implementation != 'PyPy' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +sdist = { url = "https://files.pythonhosted.org/packages/fd/1d/06475e1cd5264c0b870ea2cc6fdb3e37177c1e565c43f56ff17a10e3937f/networkx-3.4.2.tar.gz", hash = "sha256:307c3669428c5362aab27c8a1260aa8f47c4e91d3891f48be0141738d8d053e1", size = 2151368 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/54/dd730b32ea14ea797530a4479b2ed46a6fb250f682a9cfb997e968bf0261/networkx-3.4.2-py3-none-any.whl", hash = "sha256:df5d4365b724cf81b8c6a7312509d0c22386097011ad1abe274afd5e9d3bbc5f", size = 1723263 }, +] + +[[package]] +name = "networkx" +version = "3.5" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and platform_python_implementation == 'PyPy' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and platform_python_implementation == 'PyPy' and sys_platform == 'linux'", + "(python_full_version >= '3.13' and platform_machine != 'aarch64' and platform_python_implementation == 'PyPy' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_python_implementation == 'PyPy' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version >= '3.13' and platform_python_implementation != 'PyPy' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and platform_python_implementation != 'PyPy' and sys_platform == 'linux'", + "(python_full_version >= '3.13' and platform_machine != 'aarch64' and platform_python_implementation != 'PyPy' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_python_implementation != 'PyPy' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version >= '3.12.4' and python_full_version < '3.13' and platform_python_implementation == 'PyPy' and sys_platform == 'darwin'", + "python_full_version >= '3.12.4' and python_full_version < '3.13' and platform_machine == 'aarch64' and platform_python_implementation == 'PyPy' and sys_platform == 'linux'", + "(python_full_version >= '3.12.4' and python_full_version < '3.13' and platform_machine != 'aarch64' and platform_python_implementation == 'PyPy' and sys_platform == 'linux') or (python_full_version >= '3.12.4' and python_full_version < '3.13' and platform_python_implementation == 'PyPy' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version >= '3.12.4' and python_full_version < '3.13' and platform_python_implementation != 'PyPy' and sys_platform == 'darwin'", + "python_full_version >= '3.12.4' and python_full_version < '3.13' and platform_machine == 'aarch64' and platform_python_implementation != 'PyPy' and sys_platform == 'linux'", + "(python_full_version >= '3.12.4' and python_full_version < '3.13' and platform_machine != 'aarch64' and platform_python_implementation != 'PyPy' and sys_platform == 'linux') or (python_full_version >= '3.12.4' and python_full_version < '3.13' and platform_python_implementation != 'PyPy' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version >= '3.12' and python_full_version < '3.12.4' and platform_python_implementation == 'PyPy' and sys_platform == 'darwin'", + "python_full_version >= '3.12' and python_full_version < '3.12.4' and platform_machine == 'aarch64' and platform_python_implementation == 'PyPy' and sys_platform == 'linux'", + "(python_full_version >= '3.12' and python_full_version < '3.12.4' and platform_machine != 'aarch64' and platform_python_implementation == 'PyPy' and sys_platform == 'linux') or (python_full_version >= '3.12' and python_full_version < '3.12.4' and platform_python_implementation == 'PyPy' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version >= '3.12' and python_full_version < '3.12.4' and platform_python_implementation != 'PyPy' and sys_platform == 'darwin'", + "python_full_version >= '3.12' and python_full_version < '3.12.4' and platform_machine == 'aarch64' and platform_python_implementation != 'PyPy' and sys_platform == 'linux'", + "(python_full_version >= '3.12' and python_full_version < '3.12.4' and platform_machine != 'aarch64' and platform_python_implementation != 'PyPy' and sys_platform == 'linux') or (python_full_version >= '3.12' and python_full_version < '3.12.4' and platform_python_implementation != 'PyPy' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version == '3.11.*' and platform_python_implementation == 'PyPy' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and platform_python_implementation == 'PyPy' and sys_platform == 'linux'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and platform_python_implementation == 'PyPy' and sys_platform == 'linux') or (python_full_version == '3.11.*' and platform_python_implementation == 'PyPy' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version == '3.11.*' and platform_python_implementation != 'PyPy' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and platform_python_implementation != 'PyPy' and sys_platform == 'linux'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and platform_python_implementation != 'PyPy' and sys_platform == 'linux') or (python_full_version == '3.11.*' and platform_python_implementation != 'PyPy' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +sdist = { url = "https://files.pythonhosted.org/packages/6c/4f/ccdb8ad3a38e583f214547fd2f7ff1fc160c43a75af88e6aec213404b96a/networkx-3.5.tar.gz", hash = "sha256:d4c6f9cf81f52d69230866796b82afbccdec3db7ae4fbd1b65ea750feed50037", size = 2471065 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/8d/776adee7bbf76365fdd7f2552710282c79a4ead5d2a46408c9043a2b70ba/networkx-3.5-py3-none-any.whl", hash = "sha256:0030d386a9a06dee3565298b4a734b68589749a544acbb6c412dc9e2489ec6ec", size = 2034406 }, +] + +[[package]] +name = "nltk" +version = "3.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "joblib" }, + { name = "regex" }, + { name = "tqdm" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3c/87/db8be88ad32c2d042420b6fd9ffd4a149f9a0d7f0e86b3f543be2eeeedd2/nltk-3.9.1.tar.gz", hash = "sha256:87d127bd3de4bd89a4f81265e5fa59cb1b199b27440175370f7417d2bc7ae868", size = 2904691 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/66/7d9e26593edda06e8cb531874633f7c2372279c3b0f46235539fe546df8b/nltk-3.9.1-py3-none-any.whl", hash = "sha256:4fa26829c5b00715afe3061398a8989dc643b92ce7dd93fb4585a70930d168a1", size = 1505442 }, +] + +[[package]] +name = "nodeenv" +version = "1.9.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314 }, +] + +[[package]] +name = "numpy" +version = "2.2.6" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11' and platform_python_implementation == 'PyPy' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and platform_python_implementation == 'PyPy' and sys_platform == 'linux'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and platform_python_implementation == 'PyPy' and sys_platform == 'linux') or (python_full_version < '3.11' and platform_python_implementation == 'PyPy' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version < '3.11' and platform_python_implementation != 'PyPy' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and platform_python_implementation != 'PyPy' and sys_platform == 'linux'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and platform_python_implementation != 'PyPy' and sys_platform == 'linux') or (python_full_version < '3.11' and platform_python_implementation != 'PyPy' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +sdist = { url = "https://files.pythonhosted.org/packages/76/21/7d2a95e4bba9dc13d043ee156a356c0a8f0c6309dff6b21b4d71a073b8a8/numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd", size = 20276440 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/3e/ed6db5be21ce87955c0cbd3009f2803f59fa08df21b5df06862e2d8e2bdd/numpy-2.2.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b412caa66f72040e6d268491a59f2c43bf03eb6c96dd8f0307829feb7fa2b6fb", size = 21165245 }, + { url = "https://files.pythonhosted.org/packages/22/c2/4b9221495b2a132cc9d2eb862e21d42a009f5a60e45fc44b00118c174bff/numpy-2.2.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e41fd67c52b86603a91c1a505ebaef50b3314de0213461c7a6e99c9a3beff90", size = 14360048 }, + { url = "https://files.pythonhosted.org/packages/fd/77/dc2fcfc66943c6410e2bf598062f5959372735ffda175b39906d54f02349/numpy-2.2.6-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:37e990a01ae6ec7fe7fa1c26c55ecb672dd98b19c3d0e1d1f326fa13cb38d163", size = 5340542 }, + { url = "https://files.pythonhosted.org/packages/7a/4f/1cb5fdc353a5f5cc7feb692db9b8ec2c3d6405453f982435efc52561df58/numpy-2.2.6-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:5a6429d4be8ca66d889b7cf70f536a397dc45ba6faeb5f8c5427935d9592e9cf", size = 6878301 }, + { url = "https://files.pythonhosted.org/packages/eb/17/96a3acd228cec142fcb8723bd3cc39c2a474f7dcf0a5d16731980bcafa95/numpy-2.2.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efd28d4e9cd7d7a8d39074a4d44c63eda73401580c5c76acda2ce969e0a38e83", size = 14297320 }, + { url = "https://files.pythonhosted.org/packages/b4/63/3de6a34ad7ad6646ac7d2f55ebc6ad439dbbf9c4370017c50cf403fb19b5/numpy-2.2.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc7b73d02efb0e18c000e9ad8b83480dfcd5dfd11065997ed4c6747470ae8915", size = 16801050 }, + { url = "https://files.pythonhosted.org/packages/07/b6/89d837eddef52b3d0cec5c6ba0456c1bf1b9ef6a6672fc2b7873c3ec4e2e/numpy-2.2.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:74d4531beb257d2c3f4b261bfb0fc09e0f9ebb8842d82a7b4209415896adc680", size = 15807034 }, + { url = "https://files.pythonhosted.org/packages/01/c8/dc6ae86e3c61cfec1f178e5c9f7858584049b6093f843bca541f94120920/numpy-2.2.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8fc377d995680230e83241d8a96def29f204b5782f371c532579b4f20607a289", size = 18614185 }, + { url = "https://files.pythonhosted.org/packages/5b/c5/0064b1b7e7c89137b471ccec1fd2282fceaae0ab3a9550f2568782d80357/numpy-2.2.6-cp310-cp310-win32.whl", hash = "sha256:b093dd74e50a8cba3e873868d9e93a85b78e0daf2e98c6797566ad8044e8363d", size = 6527149 }, + { url = "https://files.pythonhosted.org/packages/a3/dd/4b822569d6b96c39d1215dbae0582fd99954dcbcf0c1a13c61783feaca3f/numpy-2.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:f0fd6321b839904e15c46e0d257fdd101dd7f530fe03fd6359c1ea63738703f3", size = 12904620 }, + { url = "https://files.pythonhosted.org/packages/da/a8/4f83e2aa666a9fbf56d6118faaaf5f1974d456b1823fda0a176eff722839/numpy-2.2.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f9f1adb22318e121c5c69a09142811a201ef17ab257a1e66ca3025065b7f53ae", size = 21176963 }, + { url = "https://files.pythonhosted.org/packages/b3/2b/64e1affc7972decb74c9e29e5649fac940514910960ba25cd9af4488b66c/numpy-2.2.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c820a93b0255bc360f53eca31a0e676fd1101f673dda8da93454a12e23fc5f7a", size = 14406743 }, + { url = "https://files.pythonhosted.org/packages/4a/9f/0121e375000b5e50ffdd8b25bf78d8e1a5aa4cca3f185d41265198c7b834/numpy-2.2.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3d70692235e759f260c3d837193090014aebdf026dfd167834bcba43e30c2a42", size = 5352616 }, + { url = "https://files.pythonhosted.org/packages/31/0d/b48c405c91693635fbe2dcd7bc84a33a602add5f63286e024d3b6741411c/numpy-2.2.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:481b49095335f8eed42e39e8041327c05b0f6f4780488f61286ed3c01368d491", size = 6889579 }, + { url = "https://files.pythonhosted.org/packages/52/b8/7f0554d49b565d0171eab6e99001846882000883998e7b7d9f0d98b1f934/numpy-2.2.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b64d8d4d17135e00c8e346e0a738deb17e754230d7e0810ac5012750bbd85a5a", size = 14312005 }, + { url = "https://files.pythonhosted.org/packages/b3/dd/2238b898e51bd6d389b7389ffb20d7f4c10066d80351187ec8e303a5a475/numpy-2.2.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba10f8411898fc418a521833e014a77d3ca01c15b0c6cdcce6a0d2897e6dbbdf", size = 16821570 }, + { url = "https://files.pythonhosted.org/packages/83/6c/44d0325722cf644f191042bf47eedad61c1e6df2432ed65cbe28509d404e/numpy-2.2.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bd48227a919f1bafbdda0583705e547892342c26fb127219d60a5c36882609d1", size = 15818548 }, + { url = "https://files.pythonhosted.org/packages/ae/9d/81e8216030ce66be25279098789b665d49ff19eef08bfa8cb96d4957f422/numpy-2.2.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9551a499bf125c1d4f9e250377c1ee2eddd02e01eac6644c080162c0c51778ab", size = 18620521 }, + { url = "https://files.pythonhosted.org/packages/6a/fd/e19617b9530b031db51b0926eed5345ce8ddc669bb3bc0044b23e275ebe8/numpy-2.2.6-cp311-cp311-win32.whl", hash = "sha256:0678000bb9ac1475cd454c6b8c799206af8107e310843532b04d49649c717a47", size = 6525866 }, + { url = "https://files.pythonhosted.org/packages/31/0a/f354fb7176b81747d870f7991dc763e157a934c717b67b58456bc63da3df/numpy-2.2.6-cp311-cp311-win_amd64.whl", hash = "sha256:e8213002e427c69c45a52bbd94163084025f533a55a59d6f9c5b820774ef3303", size = 12907455 }, + { url = "https://files.pythonhosted.org/packages/82/5d/c00588b6cf18e1da539b45d3598d3557084990dcc4331960c15ee776ee41/numpy-2.2.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41c5a21f4a04fa86436124d388f6ed60a9343a6f767fced1a8a71c3fbca038ff", size = 20875348 }, + { url = "https://files.pythonhosted.org/packages/66/ee/560deadcdde6c2f90200450d5938f63a34b37e27ebff162810f716f6a230/numpy-2.2.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:de749064336d37e340f640b05f24e9e3dd678c57318c7289d222a8a2f543e90c", size = 14119362 }, + { url = "https://files.pythonhosted.org/packages/3c/65/4baa99f1c53b30adf0acd9a5519078871ddde8d2339dc5a7fde80d9d87da/numpy-2.2.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:894b3a42502226a1cac872f840030665f33326fc3dac8e57c607905773cdcde3", size = 5084103 }, + { url = "https://files.pythonhosted.org/packages/cc/89/e5a34c071a0570cc40c9a54eb472d113eea6d002e9ae12bb3a8407fb912e/numpy-2.2.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:71594f7c51a18e728451bb50cc60a3ce4e6538822731b2933209a1f3614e9282", size = 6625382 }, + { url = "https://files.pythonhosted.org/packages/f8/35/8c80729f1ff76b3921d5c9487c7ac3de9b2a103b1cd05e905b3090513510/numpy-2.2.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2618db89be1b4e05f7a1a847a9c1c0abd63e63a1607d892dd54668dd92faf87", size = 14018462 }, + { url = "https://files.pythonhosted.org/packages/8c/3d/1e1db36cfd41f895d266b103df00ca5b3cbe965184df824dec5c08c6b803/numpy-2.2.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd83c01228a688733f1ded5201c678f0c53ecc1006ffbc404db9f7a899ac6249", size = 16527618 }, + { url = "https://files.pythonhosted.org/packages/61/c6/03ed30992602c85aa3cd95b9070a514f8b3c33e31124694438d88809ae36/numpy-2.2.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37c0ca431f82cd5fa716eca9506aefcabc247fb27ba69c5062a6d3ade8cf8f49", size = 15505511 }, + { url = "https://files.pythonhosted.org/packages/b7/25/5761d832a81df431e260719ec45de696414266613c9ee268394dd5ad8236/numpy-2.2.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de", size = 18313783 }, + { url = "https://files.pythonhosted.org/packages/57/0a/72d5a3527c5ebffcd47bde9162c39fae1f90138c961e5296491ce778e682/numpy-2.2.6-cp312-cp312-win32.whl", hash = "sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4", size = 6246506 }, + { url = "https://files.pythonhosted.org/packages/36/fa/8c9210162ca1b88529ab76b41ba02d433fd54fecaf6feb70ef9f124683f1/numpy-2.2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2", size = 12614190 }, + { url = "https://files.pythonhosted.org/packages/f9/5c/6657823f4f594f72b5471f1db1ab12e26e890bb2e41897522d134d2a3e81/numpy-2.2.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0811bb762109d9708cca4d0b13c4f67146e3c3b7cf8d34018c722adb2d957c84", size = 20867828 }, + { url = "https://files.pythonhosted.org/packages/dc/9e/14520dc3dadf3c803473bd07e9b2bd1b69bc583cb2497b47000fed2fa92f/numpy-2.2.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:287cc3162b6f01463ccd86be154f284d0893d2b3ed7292439ea97eafa8170e0b", size = 14143006 }, + { url = "https://files.pythonhosted.org/packages/4f/06/7e96c57d90bebdce9918412087fc22ca9851cceaf5567a45c1f404480e9e/numpy-2.2.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f1372f041402e37e5e633e586f62aa53de2eac8d98cbfb822806ce4bbefcb74d", size = 5076765 }, + { url = "https://files.pythonhosted.org/packages/73/ed/63d920c23b4289fdac96ddbdd6132e9427790977d5457cd132f18e76eae0/numpy-2.2.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:55a4d33fa519660d69614a9fad433be87e5252f4b03850642f88993f7b2ca566", size = 6617736 }, + { url = "https://files.pythonhosted.org/packages/85/c5/e19c8f99d83fd377ec8c7e0cf627a8049746da54afc24ef0a0cb73d5dfb5/numpy-2.2.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f92729c95468a2f4f15e9bb94c432a9229d0d50de67304399627a943201baa2f", size = 14010719 }, + { url = "https://files.pythonhosted.org/packages/19/49/4df9123aafa7b539317bf6d342cb6d227e49f7a35b99c287a6109b13dd93/numpy-2.2.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bc23a79bfabc5d056d106f9befb8d50c31ced2fbc70eedb8155aec74a45798f", size = 16526072 }, + { url = "https://files.pythonhosted.org/packages/b2/6c/04b5f47f4f32f7c2b0e7260442a8cbcf8168b0e1a41ff1495da42f42a14f/numpy-2.2.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e3143e4451880bed956e706a3220b4e5cf6172ef05fcc397f6f36a550b1dd868", size = 15503213 }, + { url = "https://files.pythonhosted.org/packages/17/0a/5cd92e352c1307640d5b6fec1b2ffb06cd0dabe7d7b8227f97933d378422/numpy-2.2.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4f13750ce79751586ae2eb824ba7e1e8dba64784086c98cdbbcc6a42112ce0d", size = 18316632 }, + { url = "https://files.pythonhosted.org/packages/f0/3b/5cba2b1d88760ef86596ad0f3d484b1cbff7c115ae2429678465057c5155/numpy-2.2.6-cp313-cp313-win32.whl", hash = "sha256:5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd", size = 6244532 }, + { url = "https://files.pythonhosted.org/packages/cb/3b/d58c12eafcb298d4e6d0d40216866ab15f59e55d148a5658bb3132311fcf/numpy-2.2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c", size = 12610885 }, + { url = "https://files.pythonhosted.org/packages/6b/9e/4bf918b818e516322db999ac25d00c75788ddfd2d2ade4fa66f1f38097e1/numpy-2.2.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0bca768cd85ae743b2affdc762d617eddf3bcf8724435498a1e80132d04879e6", size = 20963467 }, + { url = "https://files.pythonhosted.org/packages/61/66/d2de6b291507517ff2e438e13ff7b1e2cdbdb7cb40b3ed475377aece69f9/numpy-2.2.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fc0c5673685c508a142ca65209b4e79ed6740a4ed6b2267dbba90f34b0b3cfda", size = 14225144 }, + { url = "https://files.pythonhosted.org/packages/e4/25/480387655407ead912e28ba3a820bc69af9adf13bcbe40b299d454ec011f/numpy-2.2.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:5bd4fc3ac8926b3819797a7c0e2631eb889b4118a9898c84f585a54d475b7e40", size = 5200217 }, + { url = "https://files.pythonhosted.org/packages/aa/4a/6e313b5108f53dcbf3aca0c0f3e9c92f4c10ce57a0a721851f9785872895/numpy-2.2.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8", size = 6712014 }, + { url = "https://files.pythonhosted.org/packages/b7/30/172c2d5c4be71fdf476e9de553443cf8e25feddbe185e0bd88b096915bcc/numpy-2.2.6-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1dda9c7e08dc141e0247a5b8f49cf05984955246a327d4c48bda16821947b2f", size = 14077935 }, + { url = "https://files.pythonhosted.org/packages/12/fb/9e743f8d4e4d3c710902cf87af3512082ae3d43b945d5d16563f26ec251d/numpy-2.2.6-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f447e6acb680fd307f40d3da4852208af94afdfab89cf850986c3ca00562f4fa", size = 16600122 }, + { url = "https://files.pythonhosted.org/packages/12/75/ee20da0e58d3a66f204f38916757e01e33a9737d0b22373b3eb5a27358f9/numpy-2.2.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:389d771b1623ec92636b0786bc4ae56abafad4a4c513d36a55dce14bd9ce8571", size = 15586143 }, + { url = "https://files.pythonhosted.org/packages/76/95/bef5b37f29fc5e739947e9ce5179ad402875633308504a52d188302319c8/numpy-2.2.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8e9ace4a37db23421249ed236fdcdd457d671e25146786dfc96835cd951aa7c1", size = 18385260 }, + { url = "https://files.pythonhosted.org/packages/09/04/f2f83279d287407cf36a7a8053a5abe7be3622a4363337338f2585e4afda/numpy-2.2.6-cp313-cp313t-win32.whl", hash = "sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff", size = 6377225 }, + { url = "https://files.pythonhosted.org/packages/67/0e/35082d13c09c02c011cf21570543d202ad929d961c02a147493cb0c2bdf5/numpy-2.2.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06", size = 12771374 }, + { url = "https://files.pythonhosted.org/packages/9e/3b/d94a75f4dbf1ef5d321523ecac21ef23a3cd2ac8b78ae2aac40873590229/numpy-2.2.6-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0b605b275d7bd0c640cad4e5d30fa701a8d59302e127e5f79138ad62762c3e3d", size = 21040391 }, + { url = "https://files.pythonhosted.org/packages/17/f4/09b2fa1b58f0fb4f7c7963a1649c64c4d315752240377ed74d9cd878f7b5/numpy-2.2.6-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:7befc596a7dc9da8a337f79802ee8adb30a552a94f792b9c9d18c840055907db", size = 6786754 }, + { url = "https://files.pythonhosted.org/packages/af/30/feba75f143bdc868a1cc3f44ccfa6c4b9ec522b36458e738cd00f67b573f/numpy-2.2.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce47521a4754c8f4593837384bd3424880629f718d87c5d44f8ed763edd63543", size = 16643476 }, + { url = "https://files.pythonhosted.org/packages/37/48/ac2a9584402fb6c0cd5b5d1a91dcf176b15760130dd386bbafdbfe3640bf/numpy-2.2.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d042d24c90c41b54fd506da306759e06e568864df8ec17ccc17e9e884634fd00", size = 12812666 }, +] + +[[package]] +name = "numpy" +version = "2.3.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and platform_python_implementation == 'PyPy' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and platform_python_implementation == 'PyPy' and sys_platform == 'linux'", + "(python_full_version >= '3.13' and platform_machine != 'aarch64' and platform_python_implementation == 'PyPy' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_python_implementation == 'PyPy' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version >= '3.13' and platform_python_implementation != 'PyPy' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and platform_python_implementation != 'PyPy' and sys_platform == 'linux'", + "(python_full_version >= '3.13' and platform_machine != 'aarch64' and platform_python_implementation != 'PyPy' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_python_implementation != 'PyPy' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version >= '3.12.4' and python_full_version < '3.13' and platform_python_implementation == 'PyPy' and sys_platform == 'darwin'", + "python_full_version >= '3.12.4' and python_full_version < '3.13' and platform_machine == 'aarch64' and platform_python_implementation == 'PyPy' and sys_platform == 'linux'", + "(python_full_version >= '3.12.4' and python_full_version < '3.13' and platform_machine != 'aarch64' and platform_python_implementation == 'PyPy' and sys_platform == 'linux') or (python_full_version >= '3.12.4' and python_full_version < '3.13' and platform_python_implementation == 'PyPy' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version >= '3.12.4' and python_full_version < '3.13' and platform_python_implementation != 'PyPy' and sys_platform == 'darwin'", + "python_full_version >= '3.12.4' and python_full_version < '3.13' and platform_machine == 'aarch64' and platform_python_implementation != 'PyPy' and sys_platform == 'linux'", + "(python_full_version >= '3.12.4' and python_full_version < '3.13' and platform_machine != 'aarch64' and platform_python_implementation != 'PyPy' and sys_platform == 'linux') or (python_full_version >= '3.12.4' and python_full_version < '3.13' and platform_python_implementation != 'PyPy' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version >= '3.12' and python_full_version < '3.12.4' and platform_python_implementation == 'PyPy' and sys_platform == 'darwin'", + "python_full_version >= '3.12' and python_full_version < '3.12.4' and platform_machine == 'aarch64' and platform_python_implementation == 'PyPy' and sys_platform == 'linux'", + "(python_full_version >= '3.12' and python_full_version < '3.12.4' and platform_machine != 'aarch64' and platform_python_implementation == 'PyPy' and sys_platform == 'linux') or (python_full_version >= '3.12' and python_full_version < '3.12.4' and platform_python_implementation == 'PyPy' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version >= '3.12' and python_full_version < '3.12.4' and platform_python_implementation != 'PyPy' and sys_platform == 'darwin'", + "python_full_version >= '3.12' and python_full_version < '3.12.4' and platform_machine == 'aarch64' and platform_python_implementation != 'PyPy' and sys_platform == 'linux'", + "(python_full_version >= '3.12' and python_full_version < '3.12.4' and platform_machine != 'aarch64' and platform_python_implementation != 'PyPy' and sys_platform == 'linux') or (python_full_version >= '3.12' and python_full_version < '3.12.4' and platform_python_implementation != 'PyPy' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version == '3.11.*' and platform_python_implementation == 'PyPy' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and platform_python_implementation == 'PyPy' and sys_platform == 'linux'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and platform_python_implementation == 'PyPy' and sys_platform == 'linux') or (python_full_version == '3.11.*' and platform_python_implementation == 'PyPy' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version == '3.11.*' and platform_python_implementation != 'PyPy' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and platform_python_implementation != 'PyPy' and sys_platform == 'linux'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and platform_python_implementation != 'PyPy' and sys_platform == 'linux') or (python_full_version == '3.11.*' and platform_python_implementation != 'PyPy' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +sdist = { url = "https://files.pythonhosted.org/packages/37/7d/3fec4199c5ffb892bed55cff901e4f39a58c81df9c44c280499e92cad264/numpy-2.3.2.tar.gz", hash = "sha256:e0486a11ec30cdecb53f184d496d1c6a20786c81e55e41640270130056f8ee48", size = 20489306 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/26/1320083986108998bd487e2931eed2aeedf914b6e8905431487543ec911d/numpy-2.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:852ae5bed3478b92f093e30f785c98e0cb62fa0a939ed057c31716e18a7a22b9", size = 21259016 }, + { url = "https://files.pythonhosted.org/packages/c4/2b/792b341463fa93fc7e55abbdbe87dac316c5b8cb5e94fb7a59fb6fa0cda5/numpy-2.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7a0e27186e781a69959d0230dd9909b5e26024f8da10683bd6344baea1885168", size = 14451158 }, + { url = "https://files.pythonhosted.org/packages/b7/13/e792d7209261afb0c9f4759ffef6135b35c77c6349a151f488f531d13595/numpy-2.3.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:f0a1a8476ad77a228e41619af2fa9505cf69df928e9aaa165746584ea17fed2b", size = 5379817 }, + { url = "https://files.pythonhosted.org/packages/49/ce/055274fcba4107c022b2113a213c7287346563f48d62e8d2a5176ad93217/numpy-2.3.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:cbc95b3813920145032412f7e33d12080f11dc776262df1712e1638207dde9e8", size = 6913606 }, + { url = "https://files.pythonhosted.org/packages/17/f2/e4d72e6bc5ff01e2ab613dc198d560714971900c03674b41947e38606502/numpy-2.3.2-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f75018be4980a7324edc5930fe39aa391d5734531b1926968605416ff58c332d", size = 14589652 }, + { url = "https://files.pythonhosted.org/packages/c8/b0/fbeee3000a51ebf7222016e2939b5c5ecf8000a19555d04a18f1e02521b8/numpy-2.3.2-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:20b8200721840f5621b7bd03f8dcd78de33ec522fc40dc2641aa09537df010c3", size = 16938816 }, + { url = "https://files.pythonhosted.org/packages/a9/ec/2f6c45c3484cc159621ea8fc000ac5a86f1575f090cac78ac27193ce82cd/numpy-2.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1f91e5c028504660d606340a084db4b216567ded1056ea2b4be4f9d10b67197f", size = 16370512 }, + { url = "https://files.pythonhosted.org/packages/b5/01/dd67cf511850bd7aefd6347aaae0956ed415abea741ae107834aae7d6d4e/numpy-2.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:fb1752a3bb9a3ad2d6b090b88a9a0ae1cd6f004ef95f75825e2f382c183b2097", size = 18884947 }, + { url = "https://files.pythonhosted.org/packages/a7/17/2cf60fd3e6a61d006778735edf67a222787a8c1a7842aed43ef96d777446/numpy-2.3.2-cp311-cp311-win32.whl", hash = "sha256:4ae6863868aaee2f57503c7a5052b3a2807cf7a3914475e637a0ecd366ced220", size = 6599494 }, + { url = "https://files.pythonhosted.org/packages/d5/03/0eade211c504bda872a594f045f98ddcc6caef2b7c63610946845e304d3f/numpy-2.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:240259d6564f1c65424bcd10f435145a7644a65a6811cfc3201c4a429ba79170", size = 13087889 }, + { url = "https://files.pythonhosted.org/packages/13/32/2c7979d39dafb2a25087e12310fc7f3b9d3c7d960df4f4bc97955ae0ce1d/numpy-2.3.2-cp311-cp311-win_arm64.whl", hash = "sha256:4209f874d45f921bde2cff1ffcd8a3695f545ad2ffbef6d3d3c6768162efab89", size = 10459560 }, + { url = "https://files.pythonhosted.org/packages/00/6d/745dd1c1c5c284d17725e5c802ca4d45cfc6803519d777f087b71c9f4069/numpy-2.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bc3186bea41fae9d8e90c2b4fb5f0a1f5a690682da79b92574d63f56b529080b", size = 20956420 }, + { url = "https://files.pythonhosted.org/packages/bc/96/e7b533ea5740641dd62b07a790af5d9d8fec36000b8e2d0472bd7574105f/numpy-2.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2f4f0215edb189048a3c03bd5b19345bdfa7b45a7a6f72ae5945d2a28272727f", size = 14184660 }, + { url = "https://files.pythonhosted.org/packages/2b/53/102c6122db45a62aa20d1b18c9986f67e6b97e0d6fbc1ae13e3e4c84430c/numpy-2.3.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:8b1224a734cd509f70816455c3cffe13a4f599b1bf7130f913ba0e2c0b2006c0", size = 5113382 }, + { url = "https://files.pythonhosted.org/packages/2b/21/376257efcbf63e624250717e82b4fae93d60178f09eb03ed766dbb48ec9c/numpy-2.3.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:3dcf02866b977a38ba3ec10215220609ab9667378a9e2150615673f3ffd6c73b", size = 6647258 }, + { url = "https://files.pythonhosted.org/packages/91/ba/f4ebf257f08affa464fe6036e13f2bf9d4642a40228781dc1235da81be9f/numpy-2.3.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:572d5512df5470f50ada8d1972c5f1082d9a0b7aa5944db8084077570cf98370", size = 14281409 }, + { url = "https://files.pythonhosted.org/packages/59/ef/f96536f1df42c668cbacb727a8c6da7afc9c05ece6d558927fb1722693e1/numpy-2.3.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8145dd6d10df13c559d1e4314df29695613575183fa2e2d11fac4c208c8a1f73", size = 16641317 }, + { url = "https://files.pythonhosted.org/packages/f6/a7/af813a7b4f9a42f498dde8a4c6fcbff8100eed00182cc91dbaf095645f38/numpy-2.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:103ea7063fa624af04a791c39f97070bf93b96d7af7eb23530cd087dc8dbe9dc", size = 16056262 }, + { url = "https://files.pythonhosted.org/packages/8b/5d/41c4ef8404caaa7f05ed1cfb06afe16a25895260eacbd29b4d84dff2920b/numpy-2.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc927d7f289d14f5e037be917539620603294454130b6de200091e23d27dc9be", size = 18579342 }, + { url = "https://files.pythonhosted.org/packages/a1/4f/9950e44c5a11636f4a3af6e825ec23003475cc9a466edb7a759ed3ea63bd/numpy-2.3.2-cp312-cp312-win32.whl", hash = "sha256:d95f59afe7f808c103be692175008bab926b59309ade3e6d25009e9a171f7036", size = 6320610 }, + { url = "https://files.pythonhosted.org/packages/7c/2f/244643a5ce54a94f0a9a2ab578189c061e4a87c002e037b0829dd77293b6/numpy-2.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:9e196ade2400c0c737d93465327d1ae7c06c7cb8a1756121ebf54b06ca183c7f", size = 12786292 }, + { url = "https://files.pythonhosted.org/packages/54/cd/7b5f49d5d78db7badab22d8323c1b6ae458fbf86c4fdfa194ab3cd4eb39b/numpy-2.3.2-cp312-cp312-win_arm64.whl", hash = "sha256:ee807923782faaf60d0d7331f5e86da7d5e3079e28b291973c545476c2b00d07", size = 10194071 }, + { url = "https://files.pythonhosted.org/packages/1c/c0/c6bb172c916b00700ed3bf71cb56175fd1f7dbecebf8353545d0b5519f6c/numpy-2.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c8d9727f5316a256425892b043736d63e89ed15bbfe6556c5ff4d9d4448ff3b3", size = 20949074 }, + { url = "https://files.pythonhosted.org/packages/20/4e/c116466d22acaf4573e58421c956c6076dc526e24a6be0903219775d862e/numpy-2.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:efc81393f25f14d11c9d161e46e6ee348637c0a1e8a54bf9dedc472a3fae993b", size = 14177311 }, + { url = "https://files.pythonhosted.org/packages/78/45/d4698c182895af189c463fc91d70805d455a227261d950e4e0f1310c2550/numpy-2.3.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:dd937f088a2df683cbb79dda9a772b62a3e5a8a7e76690612c2737f38c6ef1b6", size = 5106022 }, + { url = "https://files.pythonhosted.org/packages/9f/76/3e6880fef4420179309dba72a8c11f6166c431cf6dee54c577af8906f914/numpy-2.3.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:11e58218c0c46c80509186e460d79fbdc9ca1eb8d8aee39d8f2dc768eb781089", size = 6640135 }, + { url = "https://files.pythonhosted.org/packages/34/fa/87ff7f25b3c4ce9085a62554460b7db686fef1e0207e8977795c7b7d7ba1/numpy-2.3.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5ad4ebcb683a1f99f4f392cc522ee20a18b2bb12a2c1c42c3d48d5a1adc9d3d2", size = 14278147 }, + { url = "https://files.pythonhosted.org/packages/1d/0f/571b2c7a3833ae419fe69ff7b479a78d313581785203cc70a8db90121b9a/numpy-2.3.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:938065908d1d869c7d75d8ec45f735a034771c6ea07088867f713d1cd3bbbe4f", size = 16635989 }, + { url = "https://files.pythonhosted.org/packages/24/5a/84ae8dca9c9a4c592fe11340b36a86ffa9fd3e40513198daf8a97839345c/numpy-2.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:66459dccc65d8ec98cc7df61307b64bf9e08101f9598755d42d8ae65d9a7a6ee", size = 16053052 }, + { url = "https://files.pythonhosted.org/packages/57/7c/e5725d99a9133b9813fcf148d3f858df98511686e853169dbaf63aec6097/numpy-2.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a7af9ed2aa9ec5950daf05bb11abc4076a108bd3c7db9aa7251d5f107079b6a6", size = 18577955 }, + { url = "https://files.pythonhosted.org/packages/ae/11/7c546fcf42145f29b71e4d6f429e96d8d68e5a7ba1830b2e68d7418f0bbd/numpy-2.3.2-cp313-cp313-win32.whl", hash = "sha256:906a30249315f9c8e17b085cc5f87d3f369b35fedd0051d4a84686967bdbbd0b", size = 6311843 }, + { url = "https://files.pythonhosted.org/packages/aa/6f/a428fd1cb7ed39b4280d057720fed5121b0d7754fd2a9768640160f5517b/numpy-2.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:c63d95dc9d67b676e9108fe0d2182987ccb0f11933c1e8959f42fa0da8d4fa56", size = 12782876 }, + { url = "https://files.pythonhosted.org/packages/65/85/4ea455c9040a12595fb6c43f2c217257c7b52dd0ba332c6a6c1d28b289fe/numpy-2.3.2-cp313-cp313-win_arm64.whl", hash = "sha256:b05a89f2fb84d21235f93de47129dd4f11c16f64c87c33f5e284e6a3a54e43f2", size = 10192786 }, + { url = "https://files.pythonhosted.org/packages/80/23/8278f40282d10c3f258ec3ff1b103d4994bcad78b0cba9208317f6bb73da/numpy-2.3.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4e6ecfeddfa83b02318f4d84acf15fbdbf9ded18e46989a15a8b6995dfbf85ab", size = 21047395 }, + { url = "https://files.pythonhosted.org/packages/1f/2d/624f2ce4a5df52628b4ccd16a4f9437b37c35f4f8a50d00e962aae6efd7a/numpy-2.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:508b0eada3eded10a3b55725b40806a4b855961040180028f52580c4729916a2", size = 14300374 }, + { url = "https://files.pythonhosted.org/packages/f6/62/ff1e512cdbb829b80a6bd08318a58698867bca0ca2499d101b4af063ee97/numpy-2.3.2-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:754d6755d9a7588bdc6ac47dc4ee97867271b17cee39cb87aef079574366db0a", size = 5228864 }, + { url = "https://files.pythonhosted.org/packages/7d/8e/74bc18078fff03192d4032cfa99d5a5ca937807136d6f5790ce07ca53515/numpy-2.3.2-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:a9f66e7d2b2d7712410d3bc5684149040ef5f19856f20277cd17ea83e5006286", size = 6737533 }, + { url = "https://files.pythonhosted.org/packages/19/ea/0731efe2c9073ccca5698ef6a8c3667c4cf4eea53fcdcd0b50140aba03bc/numpy-2.3.2-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de6ea4e5a65d5a90c7d286ddff2b87f3f4ad61faa3db8dabe936b34c2275b6f8", size = 14352007 }, + { url = "https://files.pythonhosted.org/packages/cf/90/36be0865f16dfed20f4bc7f75235b963d5939707d4b591f086777412ff7b/numpy-2.3.2-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a3ef07ec8cbc8fc9e369c8dcd52019510c12da4de81367d8b20bc692aa07573a", size = 16701914 }, + { url = "https://files.pythonhosted.org/packages/94/30/06cd055e24cb6c38e5989a9e747042b4e723535758e6153f11afea88c01b/numpy-2.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:27c9f90e7481275c7800dc9c24b7cc40ace3fdb970ae4d21eaff983a32f70c91", size = 16132708 }, + { url = "https://files.pythonhosted.org/packages/9a/14/ecede608ea73e58267fd7cb78f42341b3b37ba576e778a1a06baffbe585c/numpy-2.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:07b62978075b67eee4065b166d000d457c82a1efe726cce608b9db9dd66a73a5", size = 18651678 }, + { url = "https://files.pythonhosted.org/packages/40/f3/2fe6066b8d07c3685509bc24d56386534c008b462a488b7f503ba82b8923/numpy-2.3.2-cp313-cp313t-win32.whl", hash = "sha256:c771cfac34a4f2c0de8e8c97312d07d64fd8f8ed45bc9f5726a7e947270152b5", size = 6441832 }, + { url = "https://files.pythonhosted.org/packages/0b/ba/0937d66d05204d8f28630c9c60bc3eda68824abde4cf756c4d6aad03b0c6/numpy-2.3.2-cp313-cp313t-win_amd64.whl", hash = "sha256:72dbebb2dcc8305c431b2836bcc66af967df91be793d63a24e3d9b741374c450", size = 12927049 }, + { url = "https://files.pythonhosted.org/packages/e9/ed/13542dd59c104d5e654dfa2ac282c199ba64846a74c2c4bcdbc3a0f75df1/numpy-2.3.2-cp313-cp313t-win_arm64.whl", hash = "sha256:72c6df2267e926a6d5286b0a6d556ebe49eae261062059317837fda12ddf0c1a", size = 10262935 }, + { url = "https://files.pythonhosted.org/packages/cf/ea/50ebc91d28b275b23b7128ef25c3d08152bc4068f42742867e07a870a42a/numpy-2.3.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:14a91ebac98813a49bc6aa1a0dfc09513dcec1d97eaf31ca21a87221a1cdcb15", size = 21130338 }, + { url = "https://files.pythonhosted.org/packages/9f/57/cdd5eac00dd5f137277355c318a955c0d8fb8aa486020c22afd305f8b88f/numpy-2.3.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:71669b5daae692189540cffc4c439468d35a3f84f0c88b078ecd94337f6cb0ec", size = 14375776 }, + { url = "https://files.pythonhosted.org/packages/83/85/27280c7f34fcd305c2209c0cdca4d70775e4859a9eaa92f850087f8dea50/numpy-2.3.2-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:69779198d9caee6e547adb933941ed7520f896fd9656834c300bdf4dd8642712", size = 5304882 }, + { url = "https://files.pythonhosted.org/packages/48/b4/6500b24d278e15dd796f43824e69939d00981d37d9779e32499e823aa0aa/numpy-2.3.2-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:2c3271cc4097beb5a60f010bcc1cc204b300bb3eafb4399376418a83a1c6373c", size = 6818405 }, + { url = "https://files.pythonhosted.org/packages/9b/c9/142c1e03f199d202da8e980c2496213509291b6024fd2735ad28ae7065c7/numpy-2.3.2-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8446acd11fe3dc1830568c941d44449fd5cb83068e5c70bd5a470d323d448296", size = 14419651 }, + { url = "https://files.pythonhosted.org/packages/8b/95/8023e87cbea31a750a6c00ff9427d65ebc5fef104a136bfa69f76266d614/numpy-2.3.2-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aa098a5ab53fa407fded5870865c6275a5cd4101cfdef8d6fafc48286a96e981", size = 16760166 }, + { url = "https://files.pythonhosted.org/packages/78/e3/6690b3f85a05506733c7e90b577e4762517404ea78bab2ca3a5cb1aeb78d/numpy-2.3.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6936aff90dda378c09bea075af0d9c675fe3a977a9d2402f95a87f440f59f619", size = 12977811 }, +] + +[[package]] +name = "nvidia-cublas-cu12" +version = "12.6.4.1" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/af/eb/ff4b8c503fa1f1796679dce648854d58751982426e4e4b37d6fce49d259c/nvidia_cublas_cu12-12.6.4.1-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:08ed2686e9875d01b58e3cb379c6896df8e76c75e0d4a7f7dace3d7b6d9ef8eb", size = 393138322 }, +] + +[[package]] +name = "nvidia-cuda-cupti-cu12" +version = "12.6.80" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/60/7b6497946d74bcf1de852a21824d63baad12cd417db4195fc1bfe59db953/nvidia_cuda_cupti_cu12-12.6.80-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6768bad6cab4f19e8292125e5f1ac8aa7d1718704012a0e3272a6f61c4bce132", size = 8917980 }, + { url = "https://files.pythonhosted.org/packages/a5/24/120ee57b218d9952c379d1e026c4479c9ece9997a4fb46303611ee48f038/nvidia_cuda_cupti_cu12-12.6.80-py3-none-manylinux2014_x86_64.whl", hash = "sha256:a3eff6cdfcc6a4c35db968a06fcadb061cbc7d6dde548609a941ff8701b98b73", size = 8917972 }, +] + +[[package]] +name = "nvidia-cuda-nvrtc-cu12" +version = "12.6.77" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/75/2e/46030320b5a80661e88039f59060d1790298b4718944a65a7f2aeda3d9e9/nvidia_cuda_nvrtc_cu12-12.6.77-py3-none-manylinux2014_x86_64.whl", hash = "sha256:35b0cc6ee3a9636d5409133e79273ce1f3fd087abb0532d2d2e8fff1fe9efc53", size = 23650380 }, +] + +[[package]] +name = "nvidia-cuda-runtime-cu12" +version = "12.6.77" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/23/e717c5ac26d26cf39a27fbc076240fad2e3b817e5889d671b67f4f9f49c5/nvidia_cuda_runtime_cu12-12.6.77-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ba3b56a4f896141e25e19ab287cd71e52a6a0f4b29d0d31609f60e3b4d5219b7", size = 897690 }, + { url = "https://files.pythonhosted.org/packages/f0/62/65c05e161eeddbafeca24dc461f47de550d9fa8a7e04eb213e32b55cfd99/nvidia_cuda_runtime_cu12-12.6.77-py3-none-manylinux2014_x86_64.whl", hash = "sha256:a84d15d5e1da416dd4774cb42edf5e954a3e60cc945698dc1d5be02321c44dc8", size = 897678 }, +] + +[[package]] +name = "nvidia-cudnn-cu12" +version = "9.5.1.17" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas-cu12", marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/78/4535c9c7f859a64781e43c969a3a7e84c54634e319a996d43ef32ce46f83/nvidia_cudnn_cu12-9.5.1.17-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:30ac3869f6db17d170e0e556dd6cc5eee02647abc31ca856634d5a40f82c15b2", size = 570988386 }, +] + +[[package]] +name = "nvidia-cufft-cu12" +version = "11.3.0.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-nvjitlink-cu12", marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/16/73727675941ab8e6ffd86ca3a4b7b47065edcca7a997920b831f8147c99d/nvidia_cufft_cu12-11.3.0.4-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ccba62eb9cef5559abd5e0d54ceed2d9934030f51163df018532142a8ec533e5", size = 200221632 }, + { url = "https://files.pythonhosted.org/packages/60/de/99ec247a07ea40c969d904fc14f3a356b3e2a704121675b75c366b694ee1/nvidia_cufft_cu12-11.3.0.4-py3-none-manylinux2014_x86_64.whl", hash = "sha256:768160ac89f6f7b459bee747e8d175dbf53619cfe74b2a5636264163138013ca", size = 200221622 }, +] + +[[package]] +name = "nvidia-cufile-cu12" +version = "1.11.1.6" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/66/cc9876340ac68ae71b15c743ddb13f8b30d5244af344ec8322b449e35426/nvidia_cufile_cu12-1.11.1.6-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc23469d1c7e52ce6c1d55253273d32c565dd22068647f3aa59b3c6b005bf159", size = 1142103 }, +] + +[[package]] +name = "nvidia-curand-cu12" +version = "10.3.7.77" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/1b/44a01c4e70933637c93e6e1a8063d1e998b50213a6b65ac5a9169c47e98e/nvidia_curand_cu12-10.3.7.77-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a42cd1344297f70b9e39a1e4f467a4e1c10f1da54ff7a85c12197f6c652c8bdf", size = 56279010 }, + { url = "https://files.pythonhosted.org/packages/4a/aa/2c7ff0b5ee02eaef890c0ce7d4f74bc30901871c5e45dee1ae6d0083cd80/nvidia_curand_cu12-10.3.7.77-py3-none-manylinux2014_x86_64.whl", hash = "sha256:99f1a32f1ac2bd134897fc7a203f779303261268a65762a623bf30cc9fe79117", size = 56279000 }, +] + +[[package]] +name = "nvidia-cusolver-cu12" +version = "11.7.1.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas-cu12", marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux')" }, + { name = "nvidia-cusparse-cu12", marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux')" }, + { name = "nvidia-nvjitlink-cu12", marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/f0/6e/c2cf12c9ff8b872e92b4a5740701e51ff17689c4d726fca91875b07f655d/nvidia_cusolver_cu12-11.7.1.2-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e9e49843a7707e42022babb9bcfa33c29857a93b88020c4e4434656a655b698c", size = 158229790 }, + { url = "https://files.pythonhosted.org/packages/9f/81/baba53585da791d043c10084cf9553e074548408e04ae884cfe9193bd484/nvidia_cusolver_cu12-11.7.1.2-py3-none-manylinux2014_x86_64.whl", hash = "sha256:6cf28f17f64107a0c4d7802be5ff5537b2130bfc112f25d5a30df227058ca0e6", size = 158229780 }, +] + +[[package]] +name = "nvidia-cusparse-cu12" +version = "12.5.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-nvjitlink-cu12", marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/1e/b8b7c2f4099a37b96af5c9bb158632ea9e5d9d27d7391d7eb8fc45236674/nvidia_cusparse_cu12-12.5.4.2-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7556d9eca156e18184b94947ade0fba5bb47d69cec46bf8660fd2c71a4b48b73", size = 216561367 }, + { url = "https://files.pythonhosted.org/packages/43/ac/64c4316ba163e8217a99680c7605f779accffc6a4bcd0c778c12948d3707/nvidia_cusparse_cu12-12.5.4.2-py3-none-manylinux2014_x86_64.whl", hash = "sha256:23749a6571191a215cb74d1cdbff4a86e7b19f1200c071b3fcf844a5bea23a2f", size = 216561357 }, +] + +[[package]] +name = "nvidia-cusparselt-cu12" +version = "0.6.3" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/9a/72ef35b399b0e183bc2e8f6f558036922d453c4d8237dab26c666a04244b/nvidia_cusparselt_cu12-0.6.3-py3-none-manylinux2014_x86_64.whl", hash = "sha256:e5c8a26c36445dd2e6812f1177978a24e2d37cacce7e090f297a688d1ec44f46", size = 156785796 }, +] + +[[package]] +name = "nvidia-nccl-cu12" +version = "2.26.2" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/67/ca/f42388aed0fddd64ade7493dbba36e1f534d4e6fdbdd355c6a90030ae028/nvidia_nccl_cu12-2.26.2-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:694cf3879a206553cc9d7dbda76b13efaf610fdb70a50cba303de1b0d1530ac6", size = 201319755 }, +] + +[[package]] +name = "nvidia-nvjitlink-cu12" +version = "12.6.85" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/d7/c5383e47c7e9bf1c99d5bd2a8c935af2b6d705ad831a7ec5c97db4d82f4f/nvidia_nvjitlink_cu12-12.6.85-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:eedc36df9e88b682efe4309aa16b5b4e78c2407eac59e8c10a6a47535164369a", size = 19744971 }, +] + +[[package]] +name = "nvidia-nvtx-cu12" +version = "12.6.77" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/9a/fff8376f8e3d084cd1530e1ef7b879bb7d6d265620c95c1b322725c694f4/nvidia_nvtx_cu12-12.6.77-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b90bed3df379fa79afbd21be8e04a0314336b8ae16768b58f2d34cb1d04cd7d2", size = 89276 }, + { url = "https://files.pythonhosted.org/packages/9e/4e/0d0c945463719429b7bd21dece907ad0bde437a2ff12b9b12fee94722ab0/nvidia_nvtx_cu12-12.6.77-py3-none-manylinux2014_x86_64.whl", hash = "sha256:6574241a3ec5fdc9334353ab8c479fe75841dbe8f4532a8fc97ce63503330ba1", size = 89265 }, +] + +[[package]] +name = "oauthlib" +version = "3.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/5f/19930f824ffeb0ad4372da4812c50edbd1434f678c90c2733e1188edfc63/oauthlib-3.3.1.tar.gz", hash = "sha256:0f0f8aa759826a193cf66c12ea1af1637f87b9b4622d46e866952bb022e538c9", size = 185918 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/9c/92789c596b8df838baa98fa71844d84283302f7604ed565dafe5a6b5041a/oauthlib-3.3.1-py3-none-any.whl", hash = "sha256:88119c938d2b8fb88561af5f6ee0eec8cc8d552b7bb1f712743136eb7523b7a1", size = 160065 }, +] + +[[package]] +name = "olefile" +version = "0.47" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/69/1b/077b508e3e500e1629d366249c3ccb32f95e50258b231705c09e3c7a4366/olefile-0.47.zip", hash = "sha256:599383381a0bf3dfbd932ca0ca6515acd174ed48870cbf7fee123d698c192c1c", size = 112240 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/d3/b64c356a907242d719fc668b71befd73324e47ab46c8ebbbede252c154b2/olefile-0.47-py2.py3-none-any.whl", hash = "sha256:543c7da2a7adadf21214938bb79c83ea12b473a4b6ee4ad4bf854e7715e13d1f", size = 114565 }, +] + +[[package]] +name = "omegaconf" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "antlr4-python3-runtime" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/09/48/6388f1bb9da707110532cb70ec4d2822858ddfb44f1cdf1233c20a80ea4b/omegaconf-2.3.0.tar.gz", hash = "sha256:d5d4b6d29955cc50ad50c46dc269bcd92c6e00f5f90d23ab5fee7bfca4ba4cc7", size = 3298120 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e3/94/1843518e420fa3ed6919835845df698c7e27e183cb997394e4a670973a65/omegaconf-2.3.0-py3-none-any.whl", hash = "sha256:7b4df175cdb08ba400f45cae3bdcae7ba8365db4d165fc65fd04b050ab63b46b", size = 79500 }, +] + +[[package]] +name = "onnx" +version = "1.18.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "protobuf" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/60/e56e8ec44ed34006e6d4a73c92a04d9eea6163cc12440e35045aec069175/onnx-1.18.0.tar.gz", hash = "sha256:3d8dbf9e996629131ba3aa1afd1d8239b660d1f830c6688dd7e03157cccd6b9c", size = 12563009 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8e/e3/ab8a09c0af43373e0422de461956a1737581325260659aeffae22a7dad18/onnx-1.18.0-cp310-cp310-macosx_12_0_universal2.whl", hash = "sha256:4a3b50d94620e2c7c1404d1d59bc53e665883ae3fecbd856cc86da0639fd0fc3", size = 18280145 }, + { url = "https://files.pythonhosted.org/packages/04/5b/3cfd183961a0a872fe29c95f8d07264890ec65c75c94b99a4dabc950df29/onnx-1.18.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e189652dad6e70a0465035c55cc565c27aa38803dd4f4e74e4b952ee1c2de94b", size = 17422721 }, + { url = "https://files.pythonhosted.org/packages/58/52/fa649429016c5790f68c614cdebfbefd3e72ba1c458966305297d540f713/onnx-1.18.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfb1f271b1523b29f324bfd223f6a4cfbdc5a2f2f16e73563671932d33663365", size = 17584220 }, + { url = "https://files.pythonhosted.org/packages/42/52/dc166de41a5f72738b0bdfb2a19e0ebe4743cf3ecc9ae381ea3425bcb332/onnx-1.18.0-cp310-cp310-win32.whl", hash = "sha256:e03071041efd82e0317b3c45433b2f28146385b80f26f82039bc68048ac1a7a0", size = 15734494 }, + { url = "https://files.pythonhosted.org/packages/a6/f9/e766a3b85b7651ddfc5f9648e0e9dc24e88b7e88ea7f8c23187530e818ea/onnx-1.18.0-cp310-cp310-win_amd64.whl", hash = "sha256:9235b3493951e11e75465d56f4cd97e3e9247f096160dd3466bfabe4cbc938bc", size = 15848421 }, + { url = "https://files.pythonhosted.org/packages/ed/3a/a336dac4db1eddba2bf577191e5b7d3e4c26fcee5ec518a5a5b11d13540d/onnx-1.18.0-cp311-cp311-macosx_12_0_universal2.whl", hash = "sha256:735e06d8d0cf250dc498f54038831401063c655a8d6e5975b2527a4e7d24be3e", size = 18281831 }, + { url = "https://files.pythonhosted.org/packages/02/3a/56475a111120d1e5d11939acbcbb17c92198c8e64a205cd68e00bdfd8a1f/onnx-1.18.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:73160799472e1a86083f786fecdf864cf43d55325492a9b5a1cfa64d8a523ecc", size = 17424359 }, + { url = "https://files.pythonhosted.org/packages/cf/03/5eb5e9ef446ed9e78c4627faf3c1bc25e0f707116dd00e9811de232a8df5/onnx-1.18.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6acafb3823238bbe8f4340c7ac32fb218689442e074d797bee1c5c9a02fdae75", size = 17586006 }, + { url = "https://files.pythonhosted.org/packages/b0/4e/70943125729ce453271a6e46bb847b4a612496f64db6cbc6cb1f49f41ce1/onnx-1.18.0-cp311-cp311-win32.whl", hash = "sha256:4c8c4bbda760c654e65eaffddb1a7de71ec02e60092d33f9000521f897c99be9", size = 15734988 }, + { url = "https://files.pythonhosted.org/packages/44/b0/435fd764011911e8f599e3361f0f33425b1004662c1ea33a0ad22e43db2d/onnx-1.18.0-cp311-cp311-win_amd64.whl", hash = "sha256:a5810194f0f6be2e58c8d6dedc6119510df7a14280dd07ed5f0f0a85bd74816a", size = 15849576 }, + { url = "https://files.pythonhosted.org/packages/6c/f0/9e31f4b4626d60f1c034f71b411810bc9fafe31f4e7dd3598effd1b50e05/onnx-1.18.0-cp311-cp311-win_arm64.whl", hash = "sha256:aa1b7483fac6cdec26922174fc4433f8f5c2f239b1133c5625063bb3b35957d0", size = 15822961 }, + { url = "https://files.pythonhosted.org/packages/a7/fe/16228aca685392a7114625b89aae98b2dc4058a47f0f467a376745efe8d0/onnx-1.18.0-cp312-cp312-macosx_12_0_universal2.whl", hash = "sha256:521bac578448667cbb37c50bf05b53c301243ede8233029555239930996a625b", size = 18285770 }, + { url = "https://files.pythonhosted.org/packages/1e/77/ba50a903a9b5e6f9be0fa50f59eb2fca4a26ee653375408fbc72c3acbf9f/onnx-1.18.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4da451bf1c5ae381f32d430004a89f0405bc57a8471b0bddb6325a5b334aa40", size = 17421291 }, + { url = "https://files.pythonhosted.org/packages/11/23/25ec2ba723ac62b99e8fed6d7b59094dadb15e38d4c007331cc9ae3dfa5f/onnx-1.18.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99afac90b4cdb1471432203c3c1f74e16549c526df27056d39f41a9a47cfb4af", size = 17584084 }, + { url = "https://files.pythonhosted.org/packages/6a/4d/2c253a36070fb43f340ff1d2c450df6a9ef50b938adcd105693fee43c4ee/onnx-1.18.0-cp312-cp312-win32.whl", hash = "sha256:ee159b41a3ae58d9c7341cf432fc74b96aaf50bd7bb1160029f657b40dc69715", size = 15734892 }, + { url = "https://files.pythonhosted.org/packages/e8/92/048ba8fafe6b2b9a268ec2fb80def7e66c0b32ab2cae74de886981f05a27/onnx-1.18.0-cp312-cp312-win_amd64.whl", hash = "sha256:102c04edc76b16e9dfeda5a64c1fccd7d3d2913b1544750c01d38f1ac3c04e05", size = 15850336 }, + { url = "https://files.pythonhosted.org/packages/a1/66/bbc4ffedd44165dcc407a51ea4c592802a5391ce3dc94aa5045350f64635/onnx-1.18.0-cp312-cp312-win_arm64.whl", hash = "sha256:911b37d724a5d97396f3c2ef9ea25361c55cbc9aa18d75b12a52b620b67145af", size = 15823802 }, + { url = "https://files.pythonhosted.org/packages/45/da/9fb8824513fae836239276870bfcc433fa2298d34ed282c3a47d3962561b/onnx-1.18.0-cp313-cp313-macosx_12_0_universal2.whl", hash = "sha256:030d9f5f878c5f4c0ff70a4545b90d7812cd6bfe511de2f3e469d3669c8cff95", size = 18285906 }, + { url = "https://files.pythonhosted.org/packages/05/e8/762b5fb5ed1a2b8e9a4bc5e668c82723b1b789c23b74e6b5a3356731ae4e/onnx-1.18.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8521544987d713941ee1e591520044d35e702f73dc87e91e6d4b15a064ae813d", size = 17421486 }, + { url = "https://files.pythonhosted.org/packages/12/bb/471da68df0364f22296456c7f6becebe0a3da1ba435cdb371099f516da6e/onnx-1.18.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c137eecf6bc618c2f9398bcc381474b55c817237992b169dfe728e169549e8f", size = 17583581 }, + { url = "https://files.pythonhosted.org/packages/76/0d/01a95edc2cef6ad916e04e8e1267a9286f15b55c90cce5d3cdeb359d75d6/onnx-1.18.0-cp313-cp313-win32.whl", hash = "sha256:6c093ffc593e07f7e33862824eab9225f86aa189c048dd43ffde207d7041a55f", size = 15734621 }, + { url = "https://files.pythonhosted.org/packages/64/95/253451a751be32b6173a648b68f407188009afa45cd6388780c330ff5d5d/onnx-1.18.0-cp313-cp313-win_amd64.whl", hash = "sha256:230b0fb615e5b798dc4a3718999ec1828360bc71274abd14f915135eab0255f1", size = 15850472 }, + { url = "https://files.pythonhosted.org/packages/0a/b1/6fd41b026836df480a21687076e0f559bc3ceeac90f2be8c64b4a7a1f332/onnx-1.18.0-cp313-cp313-win_arm64.whl", hash = "sha256:6f91930c1a284135db0f891695a263fc876466bf2afbd2215834ac08f600cfca", size = 15823808 }, + { url = "https://files.pythonhosted.org/packages/70/f3/499e53dd41fa7302f914dd18543da01e0786a58b9a9d347497231192001f/onnx-1.18.0-cp313-cp313t-macosx_12_0_universal2.whl", hash = "sha256:2f4d37b0b5c96a873887652d1cbf3f3c70821b8c66302d84b0f0d89dd6e47653", size = 18316526 }, + { url = "https://files.pythonhosted.org/packages/84/dd/6abe5d7bd23f5ed3ade8352abf30dff1c7a9e97fc1b0a17b5d7c726e98a9/onnx-1.18.0-cp313-cp313t-win_amd64.whl", hash = "sha256:a69afd0baa372162948b52c13f3aa2730123381edf926d7ef3f68ca7cec6d0d0", size = 15865055 }, +] + +[[package]] +name = "onnxruntime" +version = "1.22.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coloredlogs" }, + { name = "flatbuffers" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "packaging" }, + { name = "protobuf" }, + { name = "sympy" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/67/3c/c99b21646a782b89c33cffd96fdee02a81bc43f0cb651de84d58ec11e30e/onnxruntime-1.22.0-cp310-cp310-macosx_13_0_universal2.whl", hash = "sha256:85d8826cc8054e4d6bf07f779dc742a363c39094015bdad6a08b3c18cfe0ba8c", size = 34273493 }, + { url = "https://files.pythonhosted.org/packages/54/ab/fd9a3b5285008c060618be92e475337fcfbf8689787953d37273f7b52ab0/onnxruntime-1.22.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:468c9502a12f6f49ec335c2febd22fdceecc1e4cc96dfc27e419ba237dff5aff", size = 14445346 }, + { url = "https://files.pythonhosted.org/packages/1f/ca/a5625644bc079e04e3076a5ac1fb954d1e90309b8eb987a4f800732ffee6/onnxruntime-1.22.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:681fe356d853630a898ee05f01ddb95728c9a168c9460e8361d0a240c9b7cb97", size = 16392959 }, + { url = "https://files.pythonhosted.org/packages/6d/6b/8267490476e8d4dd1883632c7e46a4634384c7ff1c35ae44edc8ab0bb7a9/onnxruntime-1.22.0-cp310-cp310-win_amd64.whl", hash = "sha256:20bca6495d06925631e201f2b257cc37086752e8fe7b6c83a67c6509f4759bc9", size = 12689974 }, + { url = "https://files.pythonhosted.org/packages/7a/08/c008711d1b92ff1272f4fea0fbee57723171f161d42e5c680625535280af/onnxruntime-1.22.0-cp311-cp311-macosx_13_0_universal2.whl", hash = "sha256:8d6725c5b9a681d8fe72f2960c191a96c256367887d076b08466f52b4e0991df", size = 34282151 }, + { url = "https://files.pythonhosted.org/packages/3e/8b/22989f6b59bc4ad1324f07a945c80b9ab825f0a581ad7a6064b93716d9b7/onnxruntime-1.22.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fef17d665a917866d1f68f09edc98223b9a27e6cb167dec69da4c66484ad12fd", size = 14446302 }, + { url = "https://files.pythonhosted.org/packages/7a/d5/aa83d084d05bc8f6cf8b74b499c77431ffd6b7075c761ec48ec0c161a47f/onnxruntime-1.22.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b978aa63a9a22095479c38371a9b359d4c15173cbb164eaad5f2cd27d666aa65", size = 16393496 }, + { url = "https://files.pythonhosted.org/packages/89/a5/1c6c10322201566015183b52ef011dfa932f5dd1b278de8d75c3b948411d/onnxruntime-1.22.0-cp311-cp311-win_amd64.whl", hash = "sha256:03d3ef7fb11adf154149d6e767e21057e0e577b947dd3f66190b212528e1db31", size = 12691517 }, + { url = "https://files.pythonhosted.org/packages/4d/de/9162872c6e502e9ac8c99a98a8738b2fab408123d11de55022ac4f92562a/onnxruntime-1.22.0-cp312-cp312-macosx_13_0_universal2.whl", hash = "sha256:f3c0380f53c1e72a41b3f4d6af2ccc01df2c17844072233442c3a7e74851ab97", size = 34298046 }, + { url = "https://files.pythonhosted.org/packages/03/79/36f910cd9fc96b444b0e728bba14607016079786adf032dae61f7c63b4aa/onnxruntime-1.22.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c8601128eaef79b636152aea76ae6981b7c9fc81a618f584c15d78d42b310f1c", size = 14443220 }, + { url = "https://files.pythonhosted.org/packages/8c/60/16d219b8868cc8e8e51a68519873bdb9f5f24af080b62e917a13fff9989b/onnxruntime-1.22.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6964a975731afc19dc3418fad8d4e08c48920144ff590149429a5ebe0d15fb3c", size = 16406377 }, + { url = "https://files.pythonhosted.org/packages/36/b4/3f1c71ce1d3d21078a6a74c5483bfa2b07e41a8d2b8fb1e9993e6a26d8d3/onnxruntime-1.22.0-cp312-cp312-win_amd64.whl", hash = "sha256:c0d534a43d1264d1273c2d4f00a5a588fa98d21117a3345b7104fa0bbcaadb9a", size = 12692233 }, + { url = "https://files.pythonhosted.org/packages/a9/65/5cb5018d5b0b7cba820d2c4a1d1b02d40df538d49138ba36a509457e4df6/onnxruntime-1.22.0-cp313-cp313-macosx_13_0_universal2.whl", hash = "sha256:fe7c051236aae16d8e2e9ffbfc1e115a0cc2450e873a9c4cb75c0cc96c1dae07", size = 34298715 }, + { url = "https://files.pythonhosted.org/packages/e1/89/1dfe1b368831d1256b90b95cb8d11da8ab769febd5c8833ec85ec1f79d21/onnxruntime-1.22.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6a6bbed10bc5e770c04d422893d3045b81acbbadc9fb759a2cd1ca00993da919", size = 14443266 }, + { url = "https://files.pythonhosted.org/packages/1e/70/342514ade3a33ad9dd505dcee96ff1f0e7be6d0e6e9c911fe0f1505abf42/onnxruntime-1.22.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9fe45ee3e756300fccfd8d61b91129a121d3d80e9d38e01f03ff1295badc32b8", size = 16406707 }, + { url = "https://files.pythonhosted.org/packages/3e/89/2f64e250945fa87140fb917ba377d6d0e9122e029c8512f389a9b7f953f4/onnxruntime-1.22.0-cp313-cp313-win_amd64.whl", hash = "sha256:5a31d84ef82b4b05d794a4ce8ba37b0d9deb768fd580e36e17b39e0b4840253b", size = 12691777 }, + { url = "https://files.pythonhosted.org/packages/9f/48/d61d5f1ed098161edd88c56cbac49207d7b7b149e613d2cd7e33176c63b3/onnxruntime-1.22.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a2ac5bd9205d831541db4e508e586e764a74f14efdd3f89af7fd20e1bf4a1ed", size = 14454003 }, + { url = "https://files.pythonhosted.org/packages/c3/16/873b955beda7bada5b0d798d3a601b2ff210e44ad5169f6d405b93892103/onnxruntime-1.22.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:64845709f9e8a2809e8e009bc4c8f73b788cee9c6619b7d9930344eae4c9cd36", size = 16427482 }, +] + +[[package]] +name = "openai" +version = "1.99.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "httpx" }, + { name = "jiter" }, + { name = "pydantic" }, + { name = "sniffio" }, + { name = "tqdm" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/03/30/f0fb7907a77e733bb801c7bdcde903500b31215141cdb261f04421e6fbec/openai-1.99.1.tar.gz", hash = "sha256:2c9d8e498c298f51bb94bcac724257a3a6cac6139ccdfc1186c6708f7a93120f", size = 497075 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/15/9c85154ffd283abfc43309ff3aaa63c3fd02f7767ee684e73670f6c5ade2/openai-1.99.1-py3-none-any.whl", hash = "sha256:8eeccc69e0ece1357b51ca0d9fb21324afee09b20c3e5b547d02445ca18a4e03", size = 767827 }, +] + +[[package]] +name = "opencv-python" +version = "4.11.0.86" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/17/06/68c27a523103dad5837dc5b87e71285280c4f098c60e4fe8a8db6486ab09/opencv-python-4.11.0.86.tar.gz", hash = "sha256:03d60ccae62304860d232272e4a4fda93c39d595780cb40b161b310244b736a4", size = 95171956 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/4d/53b30a2a3ac1f75f65a59eb29cf2ee7207ce64867db47036ad61743d5a23/opencv_python-4.11.0.86-cp37-abi3-macosx_13_0_arm64.whl", hash = "sha256:432f67c223f1dc2824f5e73cdfcd9db0efc8710647d4e813012195dc9122a52a", size = 37326322 }, + { url = "https://files.pythonhosted.org/packages/3b/84/0a67490741867eacdfa37bc18df96e08a9d579583b419010d7f3da8ff503/opencv_python-4.11.0.86-cp37-abi3-macosx_13_0_x86_64.whl", hash = "sha256:9d05ef13d23fe97f575153558653e2d6e87103995d54e6a35db3f282fe1f9c66", size = 56723197 }, + { url = "https://files.pythonhosted.org/packages/f3/bd/29c126788da65c1fb2b5fb621b7fed0ed5f9122aa22a0868c5e2c15c6d23/opencv_python-4.11.0.86-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1b92ae2c8852208817e6776ba1ea0d6b1e0a1b5431e971a2a0ddd2a8cc398202", size = 42230439 }, + { url = "https://files.pythonhosted.org/packages/2c/8b/90eb44a40476fa0e71e05a0283947cfd74a5d36121a11d926ad6f3193cc4/opencv_python-4.11.0.86-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b02611523803495003bd87362db3e1d2a0454a6a63025dc6658a9830570aa0d", size = 62986597 }, + { url = "https://files.pythonhosted.org/packages/fb/d7/1d5941a9dde095468b288d989ff6539dd69cd429dbf1b9e839013d21b6f0/opencv_python-4.11.0.86-cp37-abi3-win32.whl", hash = "sha256:810549cb2a4aedaa84ad9a1c92fbfdfc14090e2749cedf2c1589ad8359aa169b", size = 29384337 }, + { url = "https://files.pythonhosted.org/packages/a4/7d/f1c30a92854540bf789e9cd5dde7ef49bbe63f855b85a2e6b3db8135c591/opencv_python-4.11.0.86-cp37-abi3-win_amd64.whl", hash = "sha256:085ad9b77c18853ea66283e98affefe2de8cc4c1f43eda4c100cf9b2721142ec", size = 39488044 }, +] + +[[package]] +name = "openpyxl" +version = "3.1.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "et-xmlfile" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/f9/88d94a75de065ea32619465d2f77b29a0469500e99012523b91cc4141cd1/openpyxl-3.1.5.tar.gz", hash = "sha256:cf0e3cf56142039133628b5acffe8ef0c12bc902d2aadd3e0fe5878dc08d1050", size = 186464 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/da/977ded879c29cbd04de313843e76868e6e13408a94ed6b987245dc7c8506/openpyxl-3.1.5-py2.py3-none-any.whl", hash = "sha256:5282c12b107bffeef825f4617dc029afaf41d0ea60823bbb665ef3079dc79de2", size = 250910 }, +] + +[[package]] +name = "opentelemetry-api" +version = "1.36.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/27/d2/c782c88b8afbf961d6972428821c302bd1e9e7bc361352172f0ca31296e2/opentelemetry_api-1.36.0.tar.gz", hash = "sha256:9a72572b9c416d004d492cbc6e61962c0501eaf945ece9b5a0f56597d8348aa0", size = 64780 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bb/ee/6b08dde0a022c463b88f55ae81149584b125a42183407dc1045c486cc870/opentelemetry_api-1.36.0-py3-none-any.whl", hash = "sha256:02f20bcacf666e1333b6b1f04e647dc1d5111f86b8e510238fcc56d7762cda8c", size = 65564 }, +] + +[[package]] +name = "opentelemetry-exporter-otlp" +version = "1.36.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-exporter-otlp-proto-grpc" }, + { name = "opentelemetry-exporter-otlp-proto-http" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/95/7f/d31294ac28d567a14aefd855756bab79fed69c5a75df712f228f10c47e04/opentelemetry_exporter_otlp-1.36.0.tar.gz", hash = "sha256:72f166ea5a8923ac42889337f903e93af57db8893de200369b07401e98e4e06b", size = 6144 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/a2/8966111a285124f3d6156a663ddf2aeddd52843c1a3d6b56cbd9b6c3fd0e/opentelemetry_exporter_otlp-1.36.0-py3-none-any.whl", hash = "sha256:de93b7c45bcc78296998775d52add7c63729e83ef2cd6560730a6b336d7f6494", size = 7018 }, +] + +[[package]] +name = "opentelemetry-exporter-otlp-proto-common" +version = "1.36.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-proto" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/34/da/7747e57eb341c59886052d733072bc878424bf20f1d8cf203d508bbece5b/opentelemetry_exporter_otlp_proto_common-1.36.0.tar.gz", hash = "sha256:6c496ccbcbe26b04653cecadd92f73659b814c6e3579af157d8716e5f9f25cbf", size = 20302 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/ed/22290dca7db78eb32e0101738366b5bbda00d0407f00feffb9bf8c3fdf87/opentelemetry_exporter_otlp_proto_common-1.36.0-py3-none-any.whl", hash = "sha256:0fc002a6ed63eac235ada9aa7056e5492e9a71728214a61745f6ad04b923f840", size = 18349 }, +] + +[[package]] +name = "opentelemetry-exporter-otlp-proto-grpc" +version = "1.36.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "googleapis-common-protos" }, + { name = "grpcio" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp-proto-common" }, + { name = "opentelemetry-proto" }, + { name = "opentelemetry-sdk" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/6f/6c1b0bdd0446e5532294d1d41bf11fbaea39c8a2423a4cdfe4fe6b708127/opentelemetry_exporter_otlp_proto_grpc-1.36.0.tar.gz", hash = "sha256:b281afbf7036b325b3588b5b6c8bb175069e3978d1bd24071f4a59d04c1e5bbf", size = 23822 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/67/5f6bd188d66d0fd8e81e681bbf5822e53eb150034e2611dd2b935d3ab61a/opentelemetry_exporter_otlp_proto_grpc-1.36.0-py3-none-any.whl", hash = "sha256:734e841fc6a5d6f30e7be4d8053adb703c70ca80c562ae24e8083a28fadef211", size = 18828 }, +] + +[[package]] +name = "opentelemetry-exporter-otlp-proto-http" +version = "1.36.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "googleapis-common-protos" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp-proto-common" }, + { name = "opentelemetry-proto" }, + { name = "opentelemetry-sdk" }, + { name = "requests" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/25/85/6632e7e5700ba1ce5b8a065315f92c1e6d787ccc4fb2bdab15139eaefc82/opentelemetry_exporter_otlp_proto_http-1.36.0.tar.gz", hash = "sha256:dd3637f72f774b9fc9608ab1ac479f8b44d09b6fb5b2f3df68a24ad1da7d356e", size = 16213 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/41/a680d38b34f8f5ddbd78ed9f0042e1cc712d58ec7531924d71cb1e6c629d/opentelemetry_exporter_otlp_proto_http-1.36.0-py3-none-any.whl", hash = "sha256:3d769f68e2267e7abe4527f70deb6f598f40be3ea34c6adc35789bea94a32902", size = 18752 }, +] + +[[package]] +name = "opentelemetry-instrumentation" +version = "0.57b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "packaging" }, + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/12/37/cf17cf28f945a3aca5a038cfbb45ee01317d4f7f3a0e5209920883fe9b08/opentelemetry_instrumentation-0.57b0.tar.gz", hash = "sha256:f2a30135ba77cdea2b0e1df272f4163c154e978f57214795d72f40befd4fcf05", size = 30807 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/6f/f20cd1542959f43fb26a5bf9bb18cd81a1ea0700e8870c8f369bd07f5c65/opentelemetry_instrumentation-0.57b0-py3-none-any.whl", hash = "sha256:9109280f44882e07cec2850db28210b90600ae9110b42824d196de357cbddf7e", size = 32460 }, +] + +[[package]] +name = "opentelemetry-instrumentation-asgi" +version = "0.57b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "asgiref" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-instrumentation" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "opentelemetry-util-http" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/97/10/7ba59b586eb099fa0155521b387d857de476687c670096597f618d889323/opentelemetry_instrumentation_asgi-0.57b0.tar.gz", hash = "sha256:a6f880b5d1838f65688fc992c65fbb1d3571f319d370990c32e759d3160e510b", size = 24654 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/07/ab97dd7e8bc680b479203f7d3b2771b7a097468135a669a38da3208f96cb/opentelemetry_instrumentation_asgi-0.57b0-py3-none-any.whl", hash = "sha256:47debbde6af066a7e8e911f7193730d5e40d62effc1ac2e1119908347790a3ea", size = 16599 }, +] + +[[package]] +name = "opentelemetry-instrumentation-fastapi" +version = "0.57b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-instrumentation" }, + { name = "opentelemetry-instrumentation-asgi" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "opentelemetry-util-http" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/47/a8/7c22a33ff5986523a7f9afcb5f4d749533842c3cc77ef55b46727580edd0/opentelemetry_instrumentation_fastapi-0.57b0.tar.gz", hash = "sha256:73ac22f3c472a8f9cb21d1fbe5a4bf2797690c295fff4a1c040e9b1b1688a105", size = 20277 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/df/f20fc21c88c7af5311bfefc15fc4e606bab5edb7c193aa8c73c354904c35/opentelemetry_instrumentation_fastapi-0.57b0-py3-none-any.whl", hash = "sha256:61e6402749ffe0bfec582e58155e0d81dd38723cd9bc4562bca1acca80334006", size = 12712 }, +] + +[[package]] +name = "opentelemetry-proto" +version = "1.36.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fd/02/f6556142301d136e3b7e95ab8ea6a5d9dc28d879a99f3dd673b5f97dca06/opentelemetry_proto-1.36.0.tar.gz", hash = "sha256:0f10b3c72f74c91e0764a5ec88fd8f1c368ea5d9c64639fb455e2854ef87dd2f", size = 46152 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/57/3361e06136225be8180e879199caea520f38026f8071366241ac458beb8d/opentelemetry_proto-1.36.0-py3-none-any.whl", hash = "sha256:151b3bf73a09f94afc658497cf77d45a565606f62ce0c17acb08cd9937ca206e", size = 72537 }, +] + +[[package]] +name = "opentelemetry-sdk" +version = "1.36.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4c/85/8567a966b85a2d3f971c4d42f781c305b2b91c043724fa08fd37d158e9dc/opentelemetry_sdk-1.36.0.tar.gz", hash = "sha256:19c8c81599f51b71670661ff7495c905d8fdf6976e41622d5245b791b06fa581", size = 162557 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/59/7bed362ad1137ba5886dac8439e84cd2df6d087be7c09574ece47ae9b22c/opentelemetry_sdk-1.36.0-py3-none-any.whl", hash = "sha256:19fe048b42e98c5c1ffe85b569b7073576ad4ce0bcb6e9b4c6a39e890a6c45fb", size = 119995 }, +] + +[[package]] +name = "opentelemetry-semantic-conventions" +version = "0.57b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7e/31/67dfa252ee88476a29200b0255bda8dfc2cf07b56ad66dc9a6221f7dc787/opentelemetry_semantic_conventions-0.57b0.tar.gz", hash = "sha256:609a4a79c7891b4620d64c7aac6898f872d790d75f22019913a660756f27ff32", size = 124225 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/75/7d591371c6c39c73de5ce5da5a2cc7b72d1d1cd3f8f4638f553c01c37b11/opentelemetry_semantic_conventions-0.57b0-py3-none-any.whl", hash = "sha256:757f7e76293294f124c827e514c2a3144f191ef175b069ce8d1211e1e38e9e78", size = 201627 }, +] + +[[package]] +name = "opentelemetry-util-http" +version = "0.57b0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9b/1b/6229c45445e08e798fa825f5376f6d6a4211d29052a4088eed6d577fa653/opentelemetry_util_http-0.57b0.tar.gz", hash = "sha256:f7417595ead0eb42ed1863ec9b2f839fc740368cd7bbbfc1d0a47bc1ab0aba11", size = 9405 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/a6/b98d508d189b9c208f5978d0906141747d7e6df7c7cafec03657ed1ed559/opentelemetry_util_http-0.57b0-py3-none-any.whl", hash = "sha256:e54c0df5543951e471c3d694f85474977cd5765a3b7654398c83bab3d2ffb8e9", size = 7643 }, +] + +[[package]] +name = "orjson" +version = "3.11.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/19/3b/fd9ff8ff64ae3900f11554d5cfc835fb73e501e043c420ad32ec574fe27f/orjson-3.11.1.tar.gz", hash = "sha256:48d82770a5fd88778063604c566f9c7c71820270c9cc9338d25147cbf34afd96", size = 5393373 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/8b/7dd88f416e2e5834fd9809d871f471aae7d12dfd83d4786166fa5a926601/orjson-3.11.1-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:92d771c492b64119456afb50f2dff3e03a2db8b5af0eba32c5932d306f970532", size = 241312 }, + { url = "https://files.pythonhosted.org/packages/f3/5d/5bfc371bd010ffbec90e64338aa59abcb13ed94191112199048653ee2f34/orjson-3.11.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0085ef83a4141c2ed23bfec5fecbfdb1e95dd42fc8e8c76057bdeeec1608ea65", size = 132791 }, + { url = "https://files.pythonhosted.org/packages/48/e2/c07854a6bad71e4249345efadb686c0aff250073bdab8ba9be7626af6516/orjson-3.11.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5caf7f13f2e1b4e137060aed892d4541d07dabc3f29e6d891e2383c7ed483440", size = 128690 }, + { url = "https://files.pythonhosted.org/packages/48/e4/2e075348e7772aa1404d51d8df25ff4d6ee3daf682732cb21308e3b59c32/orjson-3.11.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f716bcc166524eddfcf9f13f8209ac19a7f27b05cf591e883419079d98c8c99d", size = 130646 }, + { url = "https://files.pythonhosted.org/packages/97/09/50daacd3ac7ae564186924c8d1121940f2c78c64d6804dbe81dd735ab087/orjson-3.11.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:507d6012fab05465d8bf21f5d7f4635ba4b6d60132874e349beff12fb51af7fe", size = 132620 }, + { url = "https://files.pythonhosted.org/packages/da/21/5f22093fa90e6d6fcf8111942b530a4ad19ee1cc0b06ddad4a63b16ab852/orjson-3.11.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b1545083b0931f754c80fd2422a73d83bea7a6d1b6de104a5f2c8dd3d64c291e", size = 135121 }, + { url = "https://files.pythonhosted.org/packages/48/90/77ad4bfa6bd400a3d241695e3e39975e32fe027aea5cb0b171bd2080c427/orjson-3.11.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e217ce3bad76351e1eb29ebe5ca630326f45cd2141f62620107a229909501a3", size = 131131 }, + { url = "https://files.pythonhosted.org/packages/5a/64/d383675229f7ffd971b6ec6cdd3016b00877bb6b2d5fc1fd099c2ec2ad57/orjson-3.11.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:06ef26e009304bda4df42e4afe518994cde6f89b4b04c0ff24021064f83f4fbb", size = 131025 }, + { url = "https://files.pythonhosted.org/packages/d4/82/e4017d8d98597f6056afaf75021ff390154d1e2722c66ba45a4d50f82606/orjson-3.11.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:ba49683b87bea3ae1489a88e766e767d4f423a669a61270b6d6a7ead1c33bd65", size = 404464 }, + { url = "https://files.pythonhosted.org/packages/77/7e/45c7f813c30d386c0168a32ce703494262458af6b222a3eeac1c0bb88822/orjson-3.11.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5072488fcc5cbcda2ece966d248e43ea1d222e19dd4c56d3f82747777f24d864", size = 146416 }, + { url = "https://files.pythonhosted.org/packages/41/71/6ccb4d7875ec3349409960769a28349f477856f05de9fd961454c2b99230/orjson-3.11.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f58ae2bcd119226fe4aa934b5880fe57b8e97b69e51d5d91c88a89477a307016", size = 135497 }, + { url = "https://files.pythonhosted.org/packages/2c/ce/df8dac7da075962fdbfca55d53e3601aa910c9f23606033bf0f084835720/orjson-3.11.1-cp310-cp310-win32.whl", hash = "sha256:6723be919c07906781b9c63cc52dc7d2fb101336c99dd7e85d3531d73fb493f7", size = 136807 }, + { url = "https://files.pythonhosted.org/packages/7b/a0/f6c2be24709d1742d878b4530fa0c3f4a5e190d51397b680abbf44d11dbf/orjson-3.11.1-cp310-cp310-win_amd64.whl", hash = "sha256:5fd44d69ddfdfb4e8d0d83f09d27a4db34930fba153fbf79f8d4ae8b47914e04", size = 131561 }, + { url = "https://files.pythonhosted.org/packages/a5/92/7ab270b5b3df8d5b0d3e572ddf2f03c9f6a79726338badf1ec8594e1469d/orjson-3.11.1-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:15e2a57ce3b57c1a36acffcc02e823afefceee0a532180c2568c62213c98e3ef", size = 240918 }, + { url = "https://files.pythonhosted.org/packages/80/41/df44684cfbd2e2e03bf9b09fdb14b7abcfff267998790b6acfb69ad435f0/orjson-3.11.1-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:17040a83ecaa130474af05bbb59a13cfeb2157d76385556041f945da936b1afd", size = 129386 }, + { url = "https://files.pythonhosted.org/packages/c1/08/958f56edd18ba1827ad0c74b2b41a7ae0864718adee8ccb5d1a5528f8761/orjson-3.11.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a68f23f09e5626cc0867a96cf618f68b91acb4753d33a80bf16111fd7f9928c", size = 132508 }, + { url = "https://files.pythonhosted.org/packages/cc/b6/5e56e189dacbf51e53ba8150c20e61ee746f6d57b697f5c52315ffc88a83/orjson-3.11.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47e07528bb6ccbd6e32a55e330979048b59bfc5518b47c89bc7ab9e3de15174a", size = 128501 }, + { url = "https://files.pythonhosted.org/packages/fe/de/f6c301a514f5934405fd4b8f3d3efc758c911d06c3de3f4be1e30d675fa4/orjson-3.11.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3807cce72bf40a9d251d689cbec28d2efd27e0f6673709f948f971afd52cb09", size = 130465 }, + { url = "https://files.pythonhosted.org/packages/47/08/f7dbaab87d6f05eebff2d7b8e6a8ed5f13b2fe3e3ae49472b527d03dbd7a/orjson-3.11.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b2dc7e88da4ca201c940f5e6127998d9e89aa64264292334dad62854bc7fc27", size = 132416 }, + { url = "https://files.pythonhosted.org/packages/43/3f/dd5a185273b7ba6aa238cfc67bf9edaa1885ae51ce942bc1a71d0f99f574/orjson-3.11.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3091dad33ac9e67c0a550cfff8ad5be156e2614d6f5d2a9247df0627751a1495", size = 134924 }, + { url = "https://files.pythonhosted.org/packages/db/ef/729d23510eaa81f0ce9d938d99d72dcf5e4ed3609d9d0bcf9c8a282cc41a/orjson-3.11.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ed0fce2307843b79a0c83de49f65b86197f1e2310de07af9db2a1a77a61ce4c", size = 130938 }, + { url = "https://files.pythonhosted.org/packages/82/96/120feb6807f9e1f4c68fc842a0f227db8575eafb1a41b2537567b91c19d8/orjson-3.11.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5a31e84782a18c30abd56774c0cfa7b9884589f4d37d9acabfa0504dad59bb9d", size = 130811 }, + { url = "https://files.pythonhosted.org/packages/89/66/4695e946a453fa22ff945da4b1ed0691b3f4ec86b828d398288db4a0ff79/orjson-3.11.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:26b6c821abf1ae515fbb8e140a2406c9f9004f3e52acb780b3dee9bfffddbd84", size = 404272 }, + { url = "https://files.pythonhosted.org/packages/cd/7b/1c953e2c9e55af126c6cb678a30796deb46d7713abdeb706b8765929464c/orjson-3.11.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f857b3d134b36a8436f1e24dcb525b6b945108b30746c1b0b556200b5cb76d39", size = 146196 }, + { url = "https://files.pythonhosted.org/packages/bf/c2/bef5d3bc83f2e178592ff317e2cf7bd38ebc16b641f076ea49f27aadd1d3/orjson-3.11.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:df146f2a14116ce80f7da669785fcb411406d8e80136558b0ecda4c924b9ac55", size = 135336 }, + { url = "https://files.pythonhosted.org/packages/92/95/bc6006881ebdb4608ed900a763c3e3c6be0d24c3aadd62beb774f9464ec6/orjson-3.11.1-cp311-cp311-win32.whl", hash = "sha256:d777c57c1f86855fe5492b973f1012be776e0398571f7cc3970e9a58ecf4dc17", size = 136665 }, + { url = "https://files.pythonhosted.org/packages/59/c3/1f2b9cc0c60ea2473d386fed2df2b25ece50aeb73c798d4669aadff3061e/orjson-3.11.1-cp311-cp311-win_amd64.whl", hash = "sha256:e9a5fd589951f02ec2fcb8d69339258bbf74b41b104c556e6d4420ea5e059313", size = 131388 }, + { url = "https://files.pythonhosted.org/packages/b0/e5/40c97e5a6b85944022fe54b463470045b8651b7bb2f1e16a95c42812bf97/orjson-3.11.1-cp311-cp311-win_arm64.whl", hash = "sha256:4cddbe41ee04fddad35d75b9cf3e3736ad0b80588280766156b94783167777af", size = 126786 }, + { url = "https://files.pythonhosted.org/packages/98/77/e55513826b712807caadb2b733eee192c1df105c6bbf0d965c253b72f124/orjson-3.11.1-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:2b7c8be96db3a977367250c6367793a3c5851a6ca4263f92f0b48d00702f9910", size = 240955 }, + { url = "https://files.pythonhosted.org/packages/c9/88/a78132dddcc9c3b80a9fa050b3516bb2c996a9d78ca6fb47c8da2a80a696/orjson-3.11.1-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:72e18088f567bd4a45db5e3196677d9ed1605e356e500c8e32dd6e303167a13d", size = 129294 }, + { url = "https://files.pythonhosted.org/packages/09/02/6591e0dcb2af6bceea96cb1b5f4b48c1445492a3ef2891ac4aa306bb6f73/orjson-3.11.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d346e2ae1ce17888f7040b65a5a4a0c9734cb20ffbd228728661e020b4c8b3a5", size = 132310 }, + { url = "https://files.pythonhosted.org/packages/e9/36/c1cfbc617bcfa4835db275d5e0fe9bbdbe561a4b53d3b2de16540ec29c50/orjson-3.11.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4bda5426ebb02ceb806a7d7ec9ba9ee5e0c93fca62375151a7b1c00bc634d06b", size = 128529 }, + { url = "https://files.pythonhosted.org/packages/7c/bd/91a156c5df3aaf1d68b2ab5be06f1969955a8d3e328d7794f4338ac1d017/orjson-3.11.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:10506cebe908542c4f024861102673db534fd2e03eb9b95b30d94438fa220abf", size = 130925 }, + { url = "https://files.pythonhosted.org/packages/a3/4c/a65cc24e9a5f87c9833a50161ab97b5edbec98bec99dfbba13827549debc/orjson-3.11.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:45202ee3f5494644e064c41abd1320497fb92fd31fc73af708708af664ac3b56", size = 132432 }, + { url = "https://files.pythonhosted.org/packages/2e/4d/3fc3e5d7115f4f7d01b481e29e5a79bcbcc45711a2723242787455424f40/orjson-3.11.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5adaf01b92e0402a9ac5c3ebe04effe2bbb115f0914a0a53d34ea239a746289", size = 135069 }, + { url = "https://files.pythonhosted.org/packages/dc/c6/7585aa8522af896060dc0cd7c336ba6c574ae854416811ee6642c505cc95/orjson-3.11.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6162a1a757a1f1f4a94bc6ffac834a3602e04ad5db022dd8395a54ed9dd51c81", size = 131045 }, + { url = "https://files.pythonhosted.org/packages/6a/4e/b8a0a943793d2708ebc39e743c943251e08ee0f3279c880aefd8e9cb0c70/orjson-3.11.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:78404206977c9f946613d3f916727c189d43193e708d760ea5d4b2087d6b0968", size = 130597 }, + { url = "https://files.pythonhosted.org/packages/72/2b/7d30e2aed2f585d5d385fb45c71d9b16ba09be58c04e8767ae6edc6c9282/orjson-3.11.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:db48f8e81072e26df6cdb0e9fff808c28597c6ac20a13d595756cf9ba1fed48a", size = 404207 }, + { url = "https://files.pythonhosted.org/packages/1b/7e/772369ec66fcbce79477f0891918309594cd00e39b67a68d4c445d2ab754/orjson-3.11.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0c1e394e67ced6bb16fea7054d99fbdd99a539cf4d446d40378d4c06e0a8548d", size = 146628 }, + { url = "https://files.pythonhosted.org/packages/b4/c8/62bdb59229d7e393ae309cef41e32cc1f0b567b21dfd0742da70efb8b40c/orjson-3.11.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e7a840752c93d4eecd1378e9bb465c3703e127b58f675cd5c620f361b6cf57a4", size = 135449 }, + { url = "https://files.pythonhosted.org/packages/02/47/1c99aa60e19f781424eabeaacd9e999eafe5b59c81ead4273b773f0f3af1/orjson-3.11.1-cp312-cp312-win32.whl", hash = "sha256:4537b0e09f45d2b74cb69c7f39ca1e62c24c0488d6bf01cd24673c74cd9596bf", size = 136653 }, + { url = "https://files.pythonhosted.org/packages/31/9a/132999929a2892ab07e916669accecc83e5bff17e11a1186b4c6f23231f0/orjson-3.11.1-cp312-cp312-win_amd64.whl", hash = "sha256:dbee6b050062540ae404530cacec1bf25e56e8d87d8d9b610b935afeb6725cae", size = 131426 }, + { url = "https://files.pythonhosted.org/packages/9c/77/d984ee5a1ca341090902e080b187721ba5d1573a8d9759e0c540975acfb2/orjson-3.11.1-cp312-cp312-win_arm64.whl", hash = "sha256:f55e557d4248322d87c4673e085c7634039ff04b47bfc823b87149ae12bef60d", size = 126635 }, + { url = "https://files.pythonhosted.org/packages/c9/e9/880ef869e6f66279ce3a381a32afa0f34e29a94250146911eee029e56efc/orjson-3.11.1-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:53cfefe4af059e65aabe9683f76b9c88bf34b4341a77d329227c2424e0e59b0e", size = 240835 }, + { url = "https://files.pythonhosted.org/packages/f0/1f/52039ef3d03eeea21763b46bc99ebe11d9de8510c72b7b5569433084a17e/orjson-3.11.1-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:93d5abed5a6f9e1b6f9b5bf6ed4423c11932b5447c2f7281d3b64e0f26c6d064", size = 129226 }, + { url = "https://files.pythonhosted.org/packages/ee/da/59fdffc9465a760be2cd3764ef9cd5535eec8f095419f972fddb123b6d0e/orjson-3.11.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5dbf06642f3db2966df504944cdd0eb68ca2717f0353bb20b20acd78109374a6", size = 132261 }, + { url = "https://files.pythonhosted.org/packages/bb/5c/8610911c7e969db7cf928c8baac4b2f1e68d314bc3057acf5ca64f758435/orjson-3.11.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:dddf4e78747fa7f2188273f84562017a3c4f0824485b78372513c1681ea7a894", size = 128614 }, + { url = "https://files.pythonhosted.org/packages/f7/a1/a1db9d4310d014c90f3b7e9b72c6fb162cba82c5f46d0b345669eaebdd3a/orjson-3.11.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fa3fe8653c9f57f0e16f008e43626485b6723b84b2f741f54d1258095b655912", size = 130968 }, + { url = "https://files.pythonhosted.org/packages/56/ff/11acd1fd7c38ea7a1b5d6bf582ae3da05931bee64620995eb08fd63c77fe/orjson-3.11.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6334d2382aff975a61f6f4d1c3daf39368b887c7de08f7c16c58f485dcf7adb2", size = 132439 }, + { url = "https://files.pythonhosted.org/packages/70/f9/bb564dd9450bf8725e034a8ad7f4ae9d4710a34caf63b85ce1c0c6d40af0/orjson-3.11.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a3d0855b643f259ee0cb76fe3df4c04483354409a520a902b067c674842eb6b8", size = 135299 }, + { url = "https://files.pythonhosted.org/packages/94/bb/c8eafe6051405e241dda3691db4d9132d3c3462d1d10a17f50837dd130b4/orjson-3.11.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0eacdfeefd0a79987926476eb16e0245546bedeb8febbbbcf4b653e79257a8e4", size = 131004 }, + { url = "https://files.pythonhosted.org/packages/a2/40/bed8d7dcf1bd2df8813bf010a25f645863a2f75e8e0ebdb2b55784cf1a62/orjson-3.11.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0ed07faf9e4873518c60480325dcbc16d17c59a165532cccfb409b4cdbaeff24", size = 130583 }, + { url = "https://files.pythonhosted.org/packages/57/e7/cfa2eb803ad52d74fbb5424a429b5be164e51d23f1d853e5e037173a5c48/orjson-3.11.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:d6d308dd578ae3658f62bb9eba54801533225823cd3248c902be1ebc79b5e014", size = 404218 }, + { url = "https://files.pythonhosted.org/packages/d5/21/bc703af5bc6e9c7e18dcf4404dcc4ec305ab9bb6c82d3aee5952c0c56abf/orjson-3.11.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:c4aa13ca959ba6b15c0a98d3d204b850f9dc36c08c9ce422ffb024eb30d6e058", size = 146605 }, + { url = "https://files.pythonhosted.org/packages/8f/fe/d26a0150534c4965a06f556aa68bf3c3b82999d5d7b0facd3af7b390c4af/orjson-3.11.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:be3d0653322abc9b68e5bcdaee6cfd58fcbe9973740ab222b87f4d687232ab1f", size = 135434 }, + { url = "https://files.pythonhosted.org/packages/89/b6/1cb28365f08cbcffc464f8512320c6eb6db6a653f03d66de47ea3c19385f/orjson-3.11.1-cp313-cp313-win32.whl", hash = "sha256:4dd34e7e2518de8d7834268846f8cab7204364f427c56fb2251e098da86f5092", size = 136596 }, + { url = "https://files.pythonhosted.org/packages/f9/35/7870d0d3ed843652676d84d8a6038791113eacc85237b673b925802826b8/orjson-3.11.1-cp313-cp313-win_amd64.whl", hash = "sha256:d6895d32032b6362540e6d0694b19130bb4f2ad04694002dce7d8af588ca5f77", size = 131319 }, + { url = "https://files.pythonhosted.org/packages/b7/3e/5bcd50fd865eb664d4edfdaaaff51e333593ceb5695a22c0d0a0d2b187ba/orjson-3.11.1-cp313-cp313-win_arm64.whl", hash = "sha256:bb7c36d5d3570fcbb01d24fa447a21a7fe5a41141fd88e78f7994053cc4e28f4", size = 126613 }, +] + +[[package]] +name = "outcome" +version = "1.3.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/98/df/77698abfac98571e65ffeb0c1fba8ffd692ab8458d617a0eed7d9a8d38f2/outcome-1.3.0.post0.tar.gz", hash = "sha256:9dcf02e65f2971b80047b377468e72a268e15c0af3cf1238e6ff14f7f91143b8", size = 21060 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/55/8b/5ab7257531a5d830fc8000c476e63c935488d74609b50f9384a643ec0a62/outcome-1.3.0.post0-py2.py3-none-any.whl", hash = "sha256:e771c5ce06d1415e356078d3bdd68523f284b4ce5419828922b6871e65eda82b", size = 10692 }, +] + +[[package]] +name = "overrides" +version = "7.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/36/86/b585f53236dec60aba864e050778b25045f857e17f6e5ea0ae95fe80edd2/overrides-7.7.0.tar.gz", hash = "sha256:55158fa3d93b98cc75299b1e67078ad9003ca27945c76162c1c0766d6f91820a", size = 22812 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/ab/fc8290c6a4c722e5514d80f62b2dc4c4df1a68a41d1364e625c35990fcf3/overrides-7.7.0-py3-none-any.whl", hash = "sha256:c7ed9d062f78b8e4c1a7b70bd8796b35ead4d9f510227ef9c5dc7626c60d7e49", size = 17832 }, +] + +[[package]] +name = "oxylabs" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/56/03/eb10466e12d2a7aba1ff1e70264c443dedeba0e5721a9a1be7e9ac9e9092/oxylabs-2.0.0.tar.gz", hash = "sha256:a6ee24140509c7ea7935ce4c878469558402dd43657718a1cae399740b66beb0", size = 29130 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/c1/88bf70a327c86f8529ad3a4ae35e92fcebf05295668fca7973279e189afe/oxylabs-2.0.0-py3-none-any.whl", hash = "sha256:3848d53bc47acdcea16ea829dc52416cdf96edae130e17bb3ac7146b012387d7", size = 34274 }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469 }, +] + +[[package]] +name = "pandas" +version = "2.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "python-dateutil" }, + { name = "pytz" }, + { name = "tzdata" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/6f/75aa71f8a14267117adeeed5d21b204770189c0a0025acbdc03c337b28fc/pandas-2.3.1.tar.gz", hash = "sha256:0a95b9ac964fe83ce317827f80304d37388ea77616b1425f0ae41c9d2d0d7bb2", size = 4487493 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c4/ca/aa97b47287221fa37a49634532e520300088e290b20d690b21ce3e448143/pandas-2.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:22c2e866f7209ebc3a8f08d75766566aae02bcc91d196935a1d9e59c7b990ac9", size = 11542731 }, + { url = "https://files.pythonhosted.org/packages/80/bf/7938dddc5f01e18e573dcfb0f1b8c9357d9b5fa6ffdee6e605b92efbdff2/pandas-2.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3583d348546201aff730c8c47e49bc159833f971c2899d6097bce68b9112a4f1", size = 10790031 }, + { url = "https://files.pythonhosted.org/packages/ee/2f/9af748366763b2a494fed477f88051dbf06f56053d5c00eba652697e3f94/pandas-2.3.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0f951fbb702dacd390561e0ea45cdd8ecfa7fb56935eb3dd78e306c19104b9b0", size = 11724083 }, + { url = "https://files.pythonhosted.org/packages/2c/95/79ab37aa4c25d1e7df953dde407bb9c3e4ae47d154bc0dd1692f3a6dcf8c/pandas-2.3.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cd05b72ec02ebfb993569b4931b2e16fbb4d6ad6ce80224a3ee838387d83a191", size = 12342360 }, + { url = "https://files.pythonhosted.org/packages/75/a7/d65e5d8665c12c3c6ff5edd9709d5836ec9b6f80071b7f4a718c6106e86e/pandas-2.3.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:1b916a627919a247d865aed068eb65eb91a344b13f5b57ab9f610b7716c92de1", size = 13202098 }, + { url = "https://files.pythonhosted.org/packages/65/f3/4c1dbd754dbaa79dbf8b537800cb2fa1a6e534764fef50ab1f7533226c5c/pandas-2.3.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:fe67dc676818c186d5a3d5425250e40f179c2a89145df477dd82945eaea89e97", size = 13837228 }, + { url = "https://files.pythonhosted.org/packages/3f/d6/d7f5777162aa9b48ec3910bca5a58c9b5927cfd9cfde3aa64322f5ba4b9f/pandas-2.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:2eb789ae0274672acbd3c575b0598d213345660120a257b47b5dafdc618aec83", size = 11336561 }, + { url = "https://files.pythonhosted.org/packages/76/1c/ccf70029e927e473a4476c00e0d5b32e623bff27f0402d0a92b7fc29bb9f/pandas-2.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2b0540963d83431f5ce8870ea02a7430adca100cec8a050f0811f8e31035541b", size = 11566608 }, + { url = "https://files.pythonhosted.org/packages/ec/d3/3c37cb724d76a841f14b8f5fe57e5e3645207cc67370e4f84717e8bb7657/pandas-2.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fe7317f578c6a153912bd2292f02e40c1d8f253e93c599e82620c7f69755c74f", size = 10823181 }, + { url = "https://files.pythonhosted.org/packages/8a/4c/367c98854a1251940edf54a4df0826dcacfb987f9068abf3e3064081a382/pandas-2.3.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e6723a27ad7b244c0c79d8e7007092d7c8f0f11305770e2f4cd778b3ad5f9f85", size = 11793570 }, + { url = "https://files.pythonhosted.org/packages/07/5f/63760ff107bcf5146eee41b38b3985f9055e710a72fdd637b791dea3495c/pandas-2.3.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3462c3735fe19f2638f2c3a40bd94ec2dc5ba13abbb032dd2fa1f540a075509d", size = 12378887 }, + { url = "https://files.pythonhosted.org/packages/15/53/f31a9b4dfe73fe4711c3a609bd8e60238022f48eacedc257cd13ae9327a7/pandas-2.3.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:98bcc8b5bf7afed22cc753a28bc4d9e26e078e777066bc53fac7904ddef9a678", size = 13230957 }, + { url = "https://files.pythonhosted.org/packages/e0/94/6fce6bf85b5056d065e0a7933cba2616dcb48596f7ba3c6341ec4bcc529d/pandas-2.3.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4d544806b485ddf29e52d75b1f559142514e60ef58a832f74fb38e48d757b299", size = 13883883 }, + { url = "https://files.pythonhosted.org/packages/c8/7b/bdcb1ed8fccb63d04bdb7635161d0ec26596d92c9d7a6cce964e7876b6c1/pandas-2.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:b3cd4273d3cb3707b6fffd217204c52ed92859533e31dc03b7c5008aa933aaab", size = 11340212 }, + { url = "https://files.pythonhosted.org/packages/46/de/b8445e0f5d217a99fe0eeb2f4988070908979bec3587c0633e5428ab596c/pandas-2.3.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:689968e841136f9e542020698ee1c4fbe9caa2ed2213ae2388dc7b81721510d3", size = 11588172 }, + { url = "https://files.pythonhosted.org/packages/1e/e0/801cdb3564e65a5ac041ab99ea6f1d802a6c325bb6e58c79c06a3f1cd010/pandas-2.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:025e92411c16cbe5bb2a4abc99732a6b132f439b8aab23a59fa593eb00704232", size = 10717365 }, + { url = "https://files.pythonhosted.org/packages/51/a5/c76a8311833c24ae61a376dbf360eb1b1c9247a5d9c1e8b356563b31b80c/pandas-2.3.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b7ff55f31c4fcb3e316e8f7fa194566b286d6ac430afec0d461163312c5841e", size = 11280411 }, + { url = "https://files.pythonhosted.org/packages/da/01/e383018feba0a1ead6cf5fe8728e5d767fee02f06a3d800e82c489e5daaf/pandas-2.3.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7dcb79bf373a47d2a40cf7232928eb7540155abbc460925c2c96d2d30b006eb4", size = 11988013 }, + { url = "https://files.pythonhosted.org/packages/5b/14/cec7760d7c9507f11c97d64f29022e12a6cc4fc03ac694535e89f88ad2ec/pandas-2.3.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:56a342b231e8862c96bdb6ab97170e203ce511f4d0429589c8ede1ee8ece48b8", size = 12767210 }, + { url = "https://files.pythonhosted.org/packages/50/b9/6e2d2c6728ed29fb3d4d4d302504fb66f1a543e37eb2e43f352a86365cdf/pandas-2.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ca7ed14832bce68baef331f4d7f294411bed8efd032f8109d690df45e00c4679", size = 13440571 }, + { url = "https://files.pythonhosted.org/packages/80/a5/3a92893e7399a691bad7664d977cb5e7c81cf666c81f89ea76ba2bff483d/pandas-2.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:ac942bfd0aca577bef61f2bc8da8147c4ef6879965ef883d8e8d5d2dc3e744b8", size = 10987601 }, + { url = "https://files.pythonhosted.org/packages/32/ed/ff0a67a2c5505e1854e6715586ac6693dd860fbf52ef9f81edee200266e7/pandas-2.3.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9026bd4a80108fac2239294a15ef9003c4ee191a0f64b90f170b40cfb7cf2d22", size = 11531393 }, + { url = "https://files.pythonhosted.org/packages/c7/db/d8f24a7cc9fb0972adab0cc80b6817e8bef888cfd0024eeb5a21c0bb5c4a/pandas-2.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6de8547d4fdb12421e2d047a2c446c623ff4c11f47fddb6b9169eb98ffba485a", size = 10668750 }, + { url = "https://files.pythonhosted.org/packages/0f/b0/80f6ec783313f1e2356b28b4fd8d2148c378370045da918c73145e6aab50/pandas-2.3.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:782647ddc63c83133b2506912cc6b108140a38a37292102aaa19c81c83db2928", size = 11342004 }, + { url = "https://files.pythonhosted.org/packages/e9/e2/20a317688435470872885e7fc8f95109ae9683dec7c50be29b56911515a5/pandas-2.3.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ba6aff74075311fc88504b1db890187a3cd0f887a5b10f5525f8e2ef55bfdb9", size = 12050869 }, + { url = "https://files.pythonhosted.org/packages/55/79/20d746b0a96c67203a5bee5fb4e00ac49c3e8009a39e1f78de264ecc5729/pandas-2.3.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e5635178b387bd2ba4ac040f82bc2ef6e6b500483975c4ebacd34bec945fda12", size = 12750218 }, + { url = "https://files.pythonhosted.org/packages/7c/0f/145c8b41e48dbf03dd18fdd7f24f8ba95b8254a97a3379048378f33e7838/pandas-2.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6f3bf5ec947526106399a9e1d26d40ee2b259c66422efdf4de63c848492d91bb", size = 13416763 }, + { url = "https://files.pythonhosted.org/packages/b2/c0/54415af59db5cdd86a3d3bf79863e8cc3fa9ed265f0745254061ac09d5f2/pandas-2.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:1c78cf43c8fde236342a1cb2c34bcff89564a7bfed7e474ed2fffa6aed03a956", size = 10987482 }, + { url = "https://files.pythonhosted.org/packages/48/64/2fd2e400073a1230e13b8cd604c9bc95d9e3b962e5d44088ead2e8f0cfec/pandas-2.3.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:8dfc17328e8da77be3cf9f47509e5637ba8f137148ed0e9b5241e1baf526e20a", size = 12029159 }, + { url = "https://files.pythonhosted.org/packages/d8/0a/d84fd79b0293b7ef88c760d7dca69828d867c89b6d9bc52d6a27e4d87316/pandas-2.3.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:ec6c851509364c59a5344458ab935e6451b31b818be467eb24b0fe89bd05b6b9", size = 11393287 }, + { url = "https://files.pythonhosted.org/packages/50/ae/ff885d2b6e88f3c7520bb74ba319268b42f05d7e583b5dded9837da2723f/pandas-2.3.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:911580460fc4884d9b05254b38a6bfadddfcc6aaef856fb5859e7ca202e45275", size = 11309381 }, + { url = "https://files.pythonhosted.org/packages/85/86/1fa345fc17caf5d7780d2699985c03dbe186c68fee00b526813939062bb0/pandas-2.3.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2f4d6feeba91744872a600e6edbbd5b033005b431d5ae8379abee5bcfa479fab", size = 11883998 }, + { url = "https://files.pythonhosted.org/packages/81/aa/e58541a49b5e6310d89474333e994ee57fea97c8aaa8fc7f00b873059bbf/pandas-2.3.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:fe37e757f462d31a9cd7580236a82f353f5713a80e059a29753cf938c6775d96", size = 12704705 }, + { url = "https://files.pythonhosted.org/packages/d5/f9/07086f5b0f2a19872554abeea7658200824f5835c58a106fa8f2ae96a46c/pandas-2.3.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5db9637dbc24b631ff3707269ae4559bce4b7fd75c1c4d7e13f40edc42df4444", size = 13189044 }, +] + +[[package]] +name = "paramiko" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "bcrypt" }, + { name = "cryptography" }, + { name = "invoke" }, + { name = "pynacl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1f/e7/81fdcbc7f190cdb058cffc9431587eb289833bdd633e2002455ca9bb13d4/paramiko-4.0.0.tar.gz", hash = "sha256:6a25f07b380cc9c9a88d2b920ad37167ac4667f8d9886ccebd8f90f654b5d69f", size = 1630743 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/90/a744336f5af32c433bd09af7854599682a383b37cfd78f7de263de6ad6cb/paramiko-4.0.0-py3-none-any.whl", hash = "sha256:0e20e00ac666503bf0b4eda3b6d833465a2b7aff2e2b3d79a8bba5ef144ee3b9", size = 223932 }, +] + +[[package]] +name = "parsimonious" +version = "0.10.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "regex" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7b/91/abdc50c4ef06fdf8d047f60ee777ca9b2a7885e1a9cea81343fbecda52d7/parsimonious-0.10.0.tar.gz", hash = "sha256:8281600da180ec8ae35427a4ab4f7b82bfec1e3d1e52f80cb60ea82b9512501c", size = 52172 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/aa/0f/c8b64d9b54ea631fcad4e9e3c8dbe8c11bb32a623be94f22974c88e71eaf/parsimonious-0.10.0-py3-none-any.whl", hash = "sha256:982ab435fabe86519b57f6b35610aa4e4e977e9f02a14353edf4bbc75369fc0f", size = 48427 }, +] + +[[package]] +name = "parso" +version = "0.8.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/66/94/68e2e17afaa9169cf6412ab0f28623903be73d1b32e208d9e8e541bb086d/parso-0.8.4.tar.gz", hash = "sha256:eb3a7b58240fb99099a345571deecc0f9540ea5f4dd2fe14c2a99d6b281ab92d", size = 400609 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/ac/dac4a63f978e4dcb3c6d3a78c4d8e0192a113d288502a1216950c41b1027/parso-0.8.4-py2.py3-none-any.whl", hash = "sha256:a418670a20291dacd2dddc80c377c5c3791378ee1e8d12bffc35420643d43f18", size = 103650 }, +] + +[[package]] +name = "patronus" +version = "0.1.18" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp" }, + { name = "opentelemetry-sdk" }, + { name = "patronus-api" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "pyyaml" }, + { name = "tqdm" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a4/bd/3605cfa39d7edbc4570b4e0208bb36b7ccbfec78ea505a7fd4210edcd41f/patronus-0.1.18.tar.gz", hash = "sha256:2e20cb03b2808acd1e64ec9ab69890319eac6284a5699258ebcc3ac9db861993", size = 357175 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/e2/9e30ceecd7b1d6d331acb6e1ecc75d83aa3605755bb935d0510d4b0dd3a0/patronus-0.1.18-py3-none-any.whl", hash = "sha256:bca91fc7ce25721eb130ec732dda6f7dcc6193ed1debbc4dacf68d531116eb67", size = 79148 }, +] + +[[package]] +name = "patronus-api" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "httpx" }, + { name = "pydantic" }, + { name = "sniffio" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c3/fd/c7574e8557c7b695ed8e59463b5bf97329050618be5ffa1cf2d89ba76b7b/patronus_api-0.3.0.tar.gz", hash = "sha256:1fac77b4e1bf1678aa3210cf986e7a8c6ba9f8de7afe199a4ff0ba304da839b0", size = 127515 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/99/dc4e4073a5b4a9cf2bcfb7c370d394d952ccf8eeb33d06b64e1dabe301fc/patronus_api-0.3.0-py3-none-any.whl", hash = "sha256:80739867685e56b874cc16cb8ee097cdd2a7fd0bd436af30e180779af81ade09", size = 131306 }, +] + +[[package]] +name = "pdf2image" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pillow" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/00/d8/b280f01045555dc257b8153c00dee3bc75830f91a744cd5f84ef3a0a64b1/pdf2image-1.17.0.tar.gz", hash = "sha256:eaa959bc116b420dd7ec415fcae49b98100dda3dd18cd2fdfa86d09f112f6d57", size = 12811 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/33/61766ae033518957f877ab246f87ca30a85b778ebaad65b7f74fa7e52988/pdf2image-1.17.0-py3-none-any.whl", hash = "sha256:ecdd58d7afb810dffe21ef2b1bbc057ef434dabbac6c33778a38a3f7744a27e2", size = 11618 }, +] + +[[package]] +name = "pdfminer-six" +version = "20250506" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "charset-normalizer" }, + { name = "cryptography" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/78/46/5223d613ac4963e1f7c07b2660fe0e9e770102ec6bda8c038400113fb215/pdfminer_six-20250506.tar.gz", hash = "sha256:b03cc8df09cf3c7aba8246deae52e0bca7ebb112a38895b5e1d4f5dd2b8ca2e7", size = 7387678 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/16/7a432c0101fa87457e75cb12c879e1749c5870a786525e2e0f42871d6462/pdfminer_six-20250506-py3-none-any.whl", hash = "sha256:d81ad173f62e5f841b53a8ba63af1a4a355933cfc0ffabd608e568b9193909e3", size = 5620187 }, +] + +[[package]] +name = "pdfplumber" +version = "0.11.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pdfminer-six" }, + { name = "pillow" }, + { name = "pypdfium2" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6d/0d/4135821aa7b1a0b77a29fac881ef0890b46b0b002290d04915ed7acc0043/pdfplumber-0.11.7.tar.gz", hash = "sha256:fa67773e5e599de1624255e9b75d1409297c5e1d7493b386ce63648637c67368", size = 115518 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/e0/52b67d4f00e09e497aec4f71bc44d395605e8ebcea52543242ed34c25ef9/pdfplumber-0.11.7-py3-none-any.whl", hash = "sha256:edd2195cca68bd770da479cf528a737e362968ec2351e62a6c0b71ff612ac25e", size = 60029 }, +] + +[[package]] +name = "pexpect" +version = "4.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ptyprocess" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772 }, +] + +[[package]] +name = "pi-heif" +version = "0.22.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pillow" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4f/90/ff6dcd9aa3b725f7eba9d70e1a12003effe45aa5bd438e3a20d14818f846/pi_heif-0.22.0.tar.gz", hash = "sha256:489ddda3c9fed948715a9c8642c6ee24c3b438a7fbf85b3a8f097d632d7082a8", size = 18548972 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/7a/6e1750a6d8de0295213a65276edda3905cf61f324e7258622fae4ecfbaf7/pi_heif-0.22.0-cp310-cp310-macosx_13_0_x86_64.whl", hash = "sha256:fca84436339eee2c91ff09cd7e301cfa2a0f7a9d83d5bc6a9d1db8587221d239", size = 623000 }, + { url = "https://files.pythonhosted.org/packages/68/23/7c5fe76e81f1889d1f301eaa92fc61c34ac37448bfcdc0b8e4acd20092ee/pi_heif-0.22.0-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:46b0fcf876d85c8684d3bc1a0b7a4e4bc5673b72084807dc6bf85caa2da9173b", size = 559829 }, + { url = "https://files.pythonhosted.org/packages/6a/5f/648efbf9673c46631c0a495cc2d3d3e3c30ff464438eb9c6cb8f6f1f2336/pi_heif-0.22.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d85a8b09e28f3234a9a64796fc3ed71516b14a9ba08cad416ebd0db251e5f263", size = 1141202 }, + { url = "https://files.pythonhosted.org/packages/34/56/6ef7c1f7ec3a5fd61b0800933a97b092c71b4e9842056c391af7fb38bf2a/pi_heif-0.22.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21416131308fabaeadbd1eae4d4daf218443832409f91ea6571edb64a0dc8d1c", size = 1204953 }, + { url = "https://files.pythonhosted.org/packages/2a/78/3325bbfec1cfb23547dbe7b1c7878e24da79c4461631f0eb7293c5dbfeb7/pi_heif-0.22.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d308f32ec557ec9f8cfee1225d83d391ffc72a1a8f03106a5805693c02359678", size = 2063369 }, + { url = "https://files.pythonhosted.org/packages/78/5a/5eb7b8509844e150e5ddf101d4249221b387209daaeb85a065e801965cfc/pi_heif-0.22.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:94359418200d7ed61f1910c5b3318fcaf0bb6e25c3e6361fbf986b320d4b7e80", size = 2203661 }, + { url = "https://files.pythonhosted.org/packages/05/e8/73450f77cb9958014ed50bf039445a447bb8d3450cc913108f72e210aa1f/pi_heif-0.22.0-cp310-cp310-win_amd64.whl", hash = "sha256:0292a1c4b58a7bfeaad0e315ca713beee3051600cf2c100a0fa96fb32377c8fd", size = 1848762 }, + { url = "https://files.pythonhosted.org/packages/44/f7/d817d2633b162fed5945525f51eb4f46d69d132dc776bac8a650cd1f5a8f/pi_heif-0.22.0-cp311-cp311-macosx_13_0_x86_64.whl", hash = "sha256:98dab5eb6bd70bdbe8ce021b4287c42ca779f6ee6d6f6fc91609d950e135d6dd", size = 622998 }, + { url = "https://files.pythonhosted.org/packages/b9/c2/e338c1ed0da8084692479a399a331c8360792fba235bfb359d4f71376e82/pi_heif-0.22.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:ed1731ebece9dcaea50db251b891318ebfc6971161664cca1fd1367e75aa815f", size = 559829 }, + { url = "https://files.pythonhosted.org/packages/29/ff/05277f849452a4dc3422615c7835bbe327354f03123a7c00b5fb0d11ef06/pi_heif-0.22.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d92149bad299390a96f29dc584bc0020c88d36d3edf073f03a6ac6b595673f63", size = 1142910 }, + { url = "https://files.pythonhosted.org/packages/ed/7f/6cb7646b6d9fb820ad6cbdd90aae9b4494ca97b1d2ed1e9556a851f4ef9e/pi_heif-0.22.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd9f1688caa359ad9c6a66fc167fa41fa24dc0fa8ceed65be2c31563d42eb700", size = 1206673 }, + { url = "https://files.pythonhosted.org/packages/ca/9c/bf4426c582b513fea184de84f499ef265addf91477ca4fa0a511af946568/pi_heif-0.22.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6339784cd447664faa4705373b7f4d7bc9c4133bc0e0a1140516614cd047e9a8", size = 2064984 }, + { url = "https://files.pythonhosted.org/packages/56/71/84e0c841fe3dfa3e13485ddd0c019d9257b0190afff190c4ed5856e00801/pi_heif-0.22.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2c5cfa7b8610750751cd414f7e276093080b38e1728d721f5d315f03a9ebd25c", size = 2205064 }, + { url = "https://files.pythonhosted.org/packages/d4/ce/674ce6a06892a6aed81b12eb7edbc14edc6f2f9b61b1d0a95b2fb88cfcd6/pi_heif-0.22.0-cp311-cp311-win_amd64.whl", hash = "sha256:e739bfe4a1785e34b52eecf092d5c511b673f20f053c728472167fe3ddcbe202", size = 1848761 }, + { url = "https://files.pythonhosted.org/packages/d5/68/7859ee94039258440e83c9f6b66c0ea3a5280f65e2397a78eec49dc3d04e/pi_heif-0.22.0-cp312-cp312-macosx_13_0_x86_64.whl", hash = "sha256:fe7b539c1924973de96a58477dab29475ed8bfbc81cb4588db9655e3661710ba", size = 623217 }, + { url = "https://files.pythonhosted.org/packages/5e/a8/5db1c5d863140c543a6e1bc035e01ea7f8fdd73d2406ecd2f3af5de0c5bb/pi_heif-0.22.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:322fd33c75ccf1208f08d07aea06c7582eed6e577a3400fe6efcbaab0c1677ff", size = 559791 }, + { url = "https://files.pythonhosted.org/packages/b4/37/efab6f350972d45ad654f701d58496729bbed2fd592c7a7964ff68b9d1df/pi_heif-0.22.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3965be305b4a5bbe4c7585f45feeab18ed18228e729a970e9b8a09b25434c885", size = 1141237 }, + { url = "https://files.pythonhosted.org/packages/41/75/e5e258a18ee0fc8884914cbd0059608b6594f241ef1318693016c184e111/pi_heif-0.22.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ebd91145a1ab9229ce330e5a7cb8a95c875c16a1cb1f2b0b5ed86e61a9fb6bd4", size = 1205641 }, + { url = "https://files.pythonhosted.org/packages/42/72/020fc43bd7ba0b1092c70d72b8d08f50ba060026bdd5a2c201b9b52d5430/pi_heif-0.22.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ed229d31a4e0037f0ba417a21f403fb8f965a40e3e5abaedafe717f6b710f544", size = 2063731 }, + { url = "https://files.pythonhosted.org/packages/be/40/b829f243662030098bef13cfa25774e9b84d1cadca7bdb2acfa14890cd8c/pi_heif-0.22.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6d95b90d5b005c35839120e934bfa5746fdf88ba344d1e58a814a33e5e9f057c", size = 2204410 }, + { url = "https://files.pythonhosted.org/packages/b4/09/6049351d6a4804debb9e4eddd209f308c7e1f6d4a5f877dbc5bbf7e99f49/pi_heif-0.22.0-cp312-cp312-win_amd64.whl", hash = "sha256:943dee9b05c768acbc06662b327518b2a257dd08ced79dce7c11fab5ac2d5c4b", size = 1848798 }, + { url = "https://files.pythonhosted.org/packages/ca/cb/b40f273b3e7648502cb8aad423caf1994c9551bb03a97689ee368199b9e7/pi_heif-0.22.0-cp313-cp313-macosx_13_0_x86_64.whl", hash = "sha256:95dd7ec2cbcef6ef1110c6ba539fa7e1489a023589076ca8b3eebcb1e38d256c", size = 623206 }, + { url = "https://files.pythonhosted.org/packages/c7/53/e257ef3118a49b298dc30f18b50e33b25a5d6d12822866b1f398fbeb7a3c/pi_heif-0.22.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:0e635dceb40424b5d88c7a2183d8dabb844c7776118df12f275ead2a10d275f6", size = 559790 }, + { url = "https://files.pythonhosted.org/packages/a0/71/1dce73941df5fbbaf9ca06d06aa130059eb8e2d56b82652419cbc1f847a3/pi_heif-0.22.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f668c27a564c7373a462c0484d49166084ec608b65f9d6763fef7a1c80eee8c0", size = 1141202 }, + { url = "https://files.pythonhosted.org/packages/cf/1a/8b7aa4a2d9ae55f091271287f7f9a937d2791c4dd5967efae9567acd56f6/pi_heif-0.22.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24ea5ba8cbd871ae09a856dbb9a7e6376ba70b5207085d0302f539574614b9e0", size = 1205581 }, + { url = "https://files.pythonhosted.org/packages/a4/2a/c1663f0389266ac93009fb00c35f09ec12f428e0fa98ad7f67e516e166fe/pi_heif-0.22.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a89b57cd839b09ee749d12397d2027e20fe7a64a44883688ab44a873b16b507b", size = 2063804 }, + { url = "https://files.pythonhosted.org/packages/a3/8b/564fd36aa3e7dfcb16c5452aff229474f63e46fc4886fb266e322b1def74/pi_heif-0.22.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93acd60ef14e3ea835b7e3dafe284c07116349b0df05507520f10520c3ad09c1", size = 2204461 }, + { url = "https://files.pythonhosted.org/packages/1c/bf/fb00ef1a6f12ddeafa4a869a6366d939f07e4a24bf8735dfb5a5bf2f0e08/pi_heif-0.22.0-cp313-cp313-win_amd64.whl", hash = "sha256:6415b0005216ad08f86d0ef75ec24e13e60bf5f45273ab54a4a22f008b9f41ac", size = 1848795 }, + { url = "https://files.pythonhosted.org/packages/c2/8d/446718f005cca79620a2ef39a5e4a884ca87df01f203ff0a53b2c5774d82/pi_heif-0.22.0-pp310-pypy310_pp73-macosx_13_0_x86_64.whl", hash = "sha256:6b83ec2f6db2dd61e09940006ee0a854eb58d91a52023be057da13a08a9f0517", size = 611769 }, + { url = "https://files.pythonhosted.org/packages/f5/9e/b7fa8c0a2e1171cce0441a98aa277563879a61e39fe481197f5801e6d678/pi_heif-0.22.0-pp310-pypy310_pp73-macosx_14_0_arm64.whl", hash = "sha256:f33211fa2afa756b13a63e21aeab577cdc7ddb18a929a012cbbcd3b7d8a772d0", size = 556401 }, + { url = "https://files.pythonhosted.org/packages/14/00/8d5a4a676675af1702491a2ef59e44f5b11824b68ccac130a9db67b75786/pi_heif-0.22.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a82bb03e5ab429b6aee5f1446c7c1925b1fb4fd58d74c960c7995734285db269", size = 1100066 }, + { url = "https://files.pythonhosted.org/packages/df/48/51ed9722094a40f9ad9aa4de6191f71de2989260e9f093b6824e9502d6bd/pi_heif-0.22.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79d72744708949bd9028516d860bd2c341371bca13aa2196e4f2267263834608", size = 1161772 }, + { url = "https://files.pythonhosted.org/packages/fe/4b/dafa303afe098e46c309f9529724c66261c9bd6ad41baf6563002a73b85d/pi_heif-0.22.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7bb583f93bb4c1dfaf3b6e689a9fa0de7c83182730c16ec8798c459cf8c3e8cf", size = 1849146 }, +] + +[[package]] +name = "pikepdf" +version = "9.10.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "deprecated" }, + { name = "lxml" }, + { name = "packaging" }, + { name = "pillow" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/34/a8/0a6c5a135b5e4c39ab42ad1e068335eb6e9ec08bd458f6c5299a915b8e1f/pikepdf-9.10.2.tar.gz", hash = "sha256:f62fc2183888f2ca1d271bf4faa440a2e2d0159221620a9c6a314f9c9a95680c", size = 4545737 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/86/0f71d002d0586f8e99507d02a89ef7830243f0f5b53f20144d9f65099122/pikepdf-9.10.2-cp310-cp310-macosx_13_0_x86_64.whl", hash = "sha256:c2b40697c8aa48316c1846195afb8f12a3adf242c31fb3e960f067b4e3f47256", size = 5008004 }, + { url = "https://files.pythonhosted.org/packages/b8/62/f7439a1b5d60d2b34857f57a6946f4b1d9d59daf7658dbc8b9865b050dd8/pikepdf-9.10.2-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:975b2f2924617cae299f5cc219cd6a4d07576566fac4d28aa87a2c93024f9d74", size = 4671002 }, + { url = "https://files.pythonhosted.org/packages/38/50/9df122a2d1699af001809f5d7a6128214bdba3743a309ea72aeca484c74d/pikepdf-9.10.2-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df5e66acc1f24c22cbf76089603045b9fab3e881e7bc3fd8d63630b395ee4865", size = 2364701 }, + { url = "https://files.pythonhosted.org/packages/b3/c9/0d80973240cf87f6b171f43825f58ae7d0876d99d2f6d3b73ae9e3a623fe/pikepdf-9.10.2-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cb83e0296ea74b18bf5fec5860d16167e3cef0ce074a21bd93b73bdd60daf6e4", size = 2577208 }, + { url = "https://files.pythonhosted.org/packages/c9/af/9d446abec1b6c3212afc43f4161bc8710cc4d05897ffc982e4d4d5467f0d/pikepdf-9.10.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5ea08e7df49e5e75b5f03d18ec901b77b202333393a01d88bfc73374cffd12a8", size = 3555247 }, + { url = "https://files.pythonhosted.org/packages/9d/af/41e59764fba1be98c7c45681d2acd1e9b566b8f9891ddd9374909bd4d2a5/pikepdf-9.10.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ea12192f0cc3bc6fcfaedd0f98161a7f0ca8630cbf972d55d208fb56e7f57120", size = 3736006 }, + { url = "https://files.pythonhosted.org/packages/ee/ca/2fade2ecdcf4b4495b2fbad5342c468191a077a7cc00c67d55f1cf856ad1/pikepdf-9.10.2-cp310-cp310-win_amd64.whl", hash = "sha256:5aa2d4b8f28588cd4755211058ecb46941e0c73ec59ffd9744c59f1b924c6bd7", size = 3716906 }, + { url = "https://files.pythonhosted.org/packages/5f/5a/7f3b2abeeedf2a5e61f7beae2e94b45169989c662bdcab71e796f4833f59/pikepdf-9.10.2-cp311-cp311-macosx_13_0_x86_64.whl", hash = "sha256:fa1cfcd725624910fc57c5b6305c5958cd28f1d40b1f9ad26723aba7caaae345", size = 5010113 }, + { url = "https://files.pythonhosted.org/packages/b0/b9/9bfc8e90da5a3d96cab386b018c0d3cb4b8754a33e929457521a59fb79b0/pikepdf-9.10.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:1575cb082b4ea39913ed90b96ff55d12d40f21a322f06144ab531d097c03b58c", size = 4672824 }, + { url = "https://files.pythonhosted.org/packages/44/4a/3c44d64370872c1cbd8c1a22a08a6ad081afccdfb001c9f789e97e699147/pikepdf-9.10.2-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a3e92458f2fc0a5e0a98a65a69534deac7a5fdf0791618afed6ca1a3623e972", size = 2371696 }, + { url = "https://files.pythonhosted.org/packages/6e/bc/26bf0c71913de0ccebb18531db105352d4c0abdae1eb86ed8a6aeb9b57ee/pikepdf-9.10.2-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c245099f9187d3c636430b941d72fa9e639b1dbed2b8f291b95b561a315fca4", size = 2585441 }, + { url = "https://files.pythonhosted.org/packages/2e/23/1253c444fdea6228cdc4cf635704e96fec02ddd1681baa054f9c7474a785/pikepdf-9.10.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c4bfe38e2dfa47f6c5e7e4ff166c6663b149c071e7b7c745595d3e3272cdc625", size = 3561122 }, + { url = "https://files.pythonhosted.org/packages/61/8e/55239cb97627463411b828aab23b8fc0282edb0f2c4ade8d299500d310a2/pikepdf-9.10.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f49f12fef155bf92174f57d21724507427ee20ec43b61460120b8f7870905028", size = 3747107 }, + { url = "https://files.pythonhosted.org/packages/f3/0f/4d52c9c4286c8f7d833056115157bbef2a4dffc1d570013648baef3e1980/pikepdf-9.10.2-cp311-cp311-win_amd64.whl", hash = "sha256:58105543a2b671cc2ffb2d2da385e383d4731a19def86de656bd7da36755e444", size = 3717357 }, + { url = "https://files.pythonhosted.org/packages/df/a6/6b36897ba9e13c4850c00cee6d87645eca1473ac42a7e43df324f9820204/pikepdf-9.10.2-cp312-cp312-macosx_13_0_x86_64.whl", hash = "sha256:3b14cacd1f0275654a7803af2611e933f5d57a98cba08aa9041792bb0f38c073", size = 5026734 }, + { url = "https://files.pythonhosted.org/packages/3b/87/9dce0097e6d2874de0b861c1e99ba6226a3aecb93a8855337f95c6792946/pikepdf-9.10.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:aaeee4676b99655c0f655404c1fca7ba483c5b4d96a790786dd4caa21e11ac18", size = 4684566 }, + { url = "https://files.pythonhosted.org/packages/1a/3a/b1babcbde33ee98ff5b7126ee321cc3d5aaa62712b98dfc67493deaa49f3/pikepdf-9.10.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:efee3a3cd8047e796508f56cefac4eb45d1173e81813dbeb3d8e9dd2e857de60", size = 2377711 }, + { url = "https://files.pythonhosted.org/packages/07/91/4ce3387d3d2494fbf6018ef6f0b669c7e0d0a7209b836ef13d093ecfb7cf/pikepdf-9.10.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:83cb30d947fae647876d2dba3c0295c0e7aa75e915bf0ea2350c72a6b652b2fa", size = 2611955 }, + { url = "https://files.pythonhosted.org/packages/1d/6a/96709de34814e8ad00378b5435469b09b21361e83a910194b139ddfdc0fa/pikepdf-9.10.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:cc498904eabec3f9d144f1c259080508b3c5809720ba8f142c3971b1525ebed8", size = 3568418 }, + { url = "https://files.pythonhosted.org/packages/d9/3c/6cdd01f219abcab401081c6fb9d65c6ebeed9df961db88ad232977f41fe5/pikepdf-9.10.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:25fb3e0d15c2c3cd77735335d09ca968df693dd0f9c6f028e9c9ce7b0ac86b48", size = 3770159 }, + { url = "https://files.pythonhosted.org/packages/68/77/7055a810520eef52fa335d5f96dd0610a059651f89d3ffbdd64371ff7ad0/pikepdf-9.10.2-cp312-cp312-win_amd64.whl", hash = "sha256:66819bd6edbca64fe2ec2020e85d339bee969aed051c2b7f256574da1a073ff6", size = 3723577 }, + { url = "https://files.pythonhosted.org/packages/ea/0c/f0b11b22767f872170c2927151ab06afa5d16688122fa4b8c4ecbf5fcdf6/pikepdf-9.10.2-cp313-cp313-macosx_13_0_x86_64.whl", hash = "sha256:a2ed7c8eabfe35b4ae2564b26cc6946b40c4efccfaa9acf91bac8e0cfc31a467", size = 5026678 }, + { url = "https://files.pythonhosted.org/packages/a3/de/4c0e3eb68b6cd45bc7225f98ce06543fa1c031e07e6ca284caac4a8e5726/pikepdf-9.10.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:c7aec253420d69cbaf6228ade29ab1e2b501dd0d9561ea4c90f16c849ec5f9ea", size = 4684938 }, + { url = "https://files.pythonhosted.org/packages/c2/f1/ef49bcb8f189999c9ee980044924bfa58b42dbe7f547b1e8b7039acbfd0a/pikepdf-9.10.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6c6bc4851b2978198143908b9a0e845ecc6587904754436bf0ee488fd6ec4aba", size = 2380266 }, + { url = "https://files.pythonhosted.org/packages/44/ab/4e0aca28c535b6cd549fbb032fbfee25c614518ff9dc257f501add053ebf/pikepdf-9.10.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:860eec8cda5d7b6d168d6fd4a956d8101577d9ea4a585fafab3fc0b1bbaddea1", size = 2613028 }, + { url = "https://files.pythonhosted.org/packages/c6/3b/1c7a2c1dc426761f2bcb82b556d661178dc96f5f0069e00715bbf368c88d/pikepdf-9.10.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:048f3d5138c44f8c452d818e14130fa30d809f61d70063b6e615e91148342188", size = 3570265 }, + { url = "https://files.pythonhosted.org/packages/05/5a/dab255e1beba374e147e26453bbb4bf751f41fb9836f9024eef97628056e/pikepdf-9.10.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0fff140da5a75b41b4cdf34354366620c206f31fc513356c70cf5da6b81d2483", size = 3771008 }, + { url = "https://files.pythonhosted.org/packages/f4/02/d05420efcf6409a04d6d4854fcc0e935b04f2b8e2c658755810fe30aead2/pikepdf-9.10.2-cp313-cp313-win_amd64.whl", hash = "sha256:1b5af8e233ed232f02e31a281134eed94504c72e9de88326433e34641f04a113", size = 3723583 }, +] + +[[package]] +name = "pillow" +version = "10.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/74/ad3d526f3bf7b6d3f408b73fde271ec69dfac8b81341a318ce825f2b3812/pillow-10.4.0.tar.gz", hash = "sha256:166c1cd4d24309b30d61f79f4a9114b7b2313d7450912277855ff5dfd7cd4a06", size = 46555059 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/69/a31cccd538ca0b5272be2a38347f8839b97a14be104ea08b0db92f749c74/pillow-10.4.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:4d9667937cfa347525b319ae34375c37b9ee6b525440f3ef48542fcf66f2731e", size = 3509271 }, + { url = "https://files.pythonhosted.org/packages/9a/9e/4143b907be8ea0bce215f2ae4f7480027473f8b61fcedfda9d851082a5d2/pillow-10.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:543f3dc61c18dafb755773efc89aae60d06b6596a63914107f75459cf984164d", size = 3375658 }, + { url = "https://files.pythonhosted.org/packages/8a/25/1fc45761955f9359b1169aa75e241551e74ac01a09f487adaaf4c3472d11/pillow-10.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7928ecbf1ece13956b95d9cbcfc77137652b02763ba384d9ab508099a2eca856", size = 4332075 }, + { url = "https://files.pythonhosted.org/packages/5e/dd/425b95d0151e1d6c951f45051112394f130df3da67363b6bc75dc4c27aba/pillow-10.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4d49b85c4348ea0b31ea63bc75a9f3857869174e2bf17e7aba02945cd218e6f", size = 4444808 }, + { url = "https://files.pythonhosted.org/packages/b1/84/9a15cc5726cbbfe7f9f90bfb11f5d028586595907cd093815ca6644932e3/pillow-10.4.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:6c762a5b0997f5659a5ef2266abc1d8851ad7749ad9a6a5506eb23d314e4f46b", size = 4356290 }, + { url = "https://files.pythonhosted.org/packages/b5/5b/6651c288b08df3b8c1e2f8c1152201e0b25d240e22ddade0f1e242fc9fa0/pillow-10.4.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:a985e028fc183bf12a77a8bbf36318db4238a3ded7fa9df1b9a133f1cb79f8fc", size = 4525163 }, + { url = "https://files.pythonhosted.org/packages/07/8b/34854bf11a83c248505c8cb0fcf8d3d0b459a2246c8809b967963b6b12ae/pillow-10.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:812f7342b0eee081eaec84d91423d1b4650bb9828eb53d8511bcef8ce5aecf1e", size = 4463100 }, + { url = "https://files.pythonhosted.org/packages/78/63/0632aee4e82476d9cbe5200c0cdf9ba41ee04ed77887432845264d81116d/pillow-10.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ac1452d2fbe4978c2eec89fb5a23b8387aba707ac72810d9490118817d9c0b46", size = 4592880 }, + { url = "https://files.pythonhosted.org/packages/df/56/b8663d7520671b4398b9d97e1ed9f583d4afcbefbda3c6188325e8c297bd/pillow-10.4.0-cp310-cp310-win32.whl", hash = "sha256:bcd5e41a859bf2e84fdc42f4edb7d9aba0a13d29a2abadccafad99de3feff984", size = 2235218 }, + { url = "https://files.pythonhosted.org/packages/f4/72/0203e94a91ddb4a9d5238434ae6c1ca10e610e8487036132ea9bf806ca2a/pillow-10.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:ecd85a8d3e79cd7158dec1c9e5808e821feea088e2f69a974db5edf84dc53141", size = 2554487 }, + { url = "https://files.pythonhosted.org/packages/bd/52/7e7e93d7a6e4290543f17dc6f7d3af4bd0b3dd9926e2e8a35ac2282bc5f4/pillow-10.4.0-cp310-cp310-win_arm64.whl", hash = "sha256:ff337c552345e95702c5fde3158acb0625111017d0e5f24bf3acdb9cc16b90d1", size = 2243219 }, + { url = "https://files.pythonhosted.org/packages/a7/62/c9449f9c3043c37f73e7487ec4ef0c03eb9c9afc91a92b977a67b3c0bbc5/pillow-10.4.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:0a9ec697746f268507404647e531e92889890a087e03681a3606d9b920fbee3c", size = 3509265 }, + { url = "https://files.pythonhosted.org/packages/f4/5f/491dafc7bbf5a3cc1845dc0430872e8096eb9e2b6f8161509d124594ec2d/pillow-10.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dfe91cb65544a1321e631e696759491ae04a2ea11d36715eca01ce07284738be", size = 3375655 }, + { url = "https://files.pythonhosted.org/packages/73/d5/c4011a76f4207a3c151134cd22a1415741e42fa5ddecec7c0182887deb3d/pillow-10.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5dc6761a6efc781e6a1544206f22c80c3af4c8cf461206d46a1e6006e4429ff3", size = 4340304 }, + { url = "https://files.pythonhosted.org/packages/ac/10/c67e20445a707f7a610699bba4fe050583b688d8cd2d202572b257f46600/pillow-10.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e84b6cc6a4a3d76c153a6b19270b3526a5a8ed6b09501d3af891daa2a9de7d6", size = 4452804 }, + { url = "https://files.pythonhosted.org/packages/a9/83/6523837906d1da2b269dee787e31df3b0acb12e3d08f024965a3e7f64665/pillow-10.4.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:bbc527b519bd3aa9d7f429d152fea69f9ad37c95f0b02aebddff592688998abe", size = 4365126 }, + { url = "https://files.pythonhosted.org/packages/ba/e5/8c68ff608a4203085158cff5cc2a3c534ec384536d9438c405ed6370d080/pillow-10.4.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:76a911dfe51a36041f2e756b00f96ed84677cdeb75d25c767f296c1c1eda1319", size = 4533541 }, + { url = "https://files.pythonhosted.org/packages/f4/7c/01b8dbdca5bc6785573f4cee96e2358b0918b7b2c7b60d8b6f3abf87a070/pillow-10.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:59291fb29317122398786c2d44427bbd1a6d7ff54017075b22be9d21aa59bd8d", size = 4471616 }, + { url = "https://files.pythonhosted.org/packages/c8/57/2899b82394a35a0fbfd352e290945440e3b3785655a03365c0ca8279f351/pillow-10.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:416d3a5d0e8cfe4f27f574362435bc9bae57f679a7158e0096ad2beb427b8696", size = 4600802 }, + { url = "https://files.pythonhosted.org/packages/4d/d7/a44f193d4c26e58ee5d2d9db3d4854b2cfb5b5e08d360a5e03fe987c0086/pillow-10.4.0-cp311-cp311-win32.whl", hash = "sha256:7086cc1d5eebb91ad24ded9f58bec6c688e9f0ed7eb3dbbf1e4800280a896496", size = 2235213 }, + { url = "https://files.pythonhosted.org/packages/c1/d0/5866318eec2b801cdb8c82abf190c8343d8a1cd8bf5a0c17444a6f268291/pillow-10.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cbed61494057c0f83b83eb3a310f0bf774b09513307c434d4366ed64f4128a91", size = 2554498 }, + { url = "https://files.pythonhosted.org/packages/d4/c8/310ac16ac2b97e902d9eb438688de0d961660a87703ad1561fd3dfbd2aa0/pillow-10.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:f5f0c3e969c8f12dd2bb7e0b15d5c468b51e5017e01e2e867335c81903046a22", size = 2243219 }, + { url = "https://files.pythonhosted.org/packages/05/cb/0353013dc30c02a8be34eb91d25e4e4cf594b59e5a55ea1128fde1e5f8ea/pillow-10.4.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:673655af3eadf4df6b5457033f086e90299fdd7a47983a13827acf7459c15d94", size = 3509350 }, + { url = "https://files.pythonhosted.org/packages/e7/cf/5c558a0f247e0bf9cec92bff9b46ae6474dd736f6d906315e60e4075f737/pillow-10.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:866b6942a92f56300012f5fbac71f2d610312ee65e22f1aa2609e491284e5597", size = 3374980 }, + { url = "https://files.pythonhosted.org/packages/84/48/6e394b86369a4eb68b8a1382c78dc092245af517385c086c5094e3b34428/pillow-10.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29dbdc4207642ea6aad70fbde1a9338753d33fb23ed6956e706936706f52dd80", size = 4343799 }, + { url = "https://files.pythonhosted.org/packages/3b/f3/a8c6c11fa84b59b9df0cd5694492da8c039a24cd159f0f6918690105c3be/pillow-10.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf2342ac639c4cf38799a44950bbc2dfcb685f052b9e262f446482afaf4bffca", size = 4459973 }, + { url = "https://files.pythonhosted.org/packages/7d/1b/c14b4197b80150fb64453585247e6fb2e1d93761fa0fa9cf63b102fde822/pillow-10.4.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:f5b92f4d70791b4a67157321c4e8225d60b119c5cc9aee8ecf153aace4aad4ef", size = 4370054 }, + { url = "https://files.pythonhosted.org/packages/55/77/40daddf677897a923d5d33329acd52a2144d54a9644f2a5422c028c6bf2d/pillow-10.4.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:86dcb5a1eb778d8b25659d5e4341269e8590ad6b4e8b44d9f4b07f8d136c414a", size = 4539484 }, + { url = "https://files.pythonhosted.org/packages/40/54/90de3e4256b1207300fb2b1d7168dd912a2fb4b2401e439ba23c2b2cabde/pillow-10.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:780c072c2e11c9b2c7ca37f9a2ee8ba66f44367ac3e5c7832afcfe5104fd6d1b", size = 4477375 }, + { url = "https://files.pythonhosted.org/packages/13/24/1bfba52f44193860918ff7c93d03d95e3f8748ca1de3ceaf11157a14cf16/pillow-10.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:37fb69d905be665f68f28a8bba3c6d3223c8efe1edf14cc4cfa06c241f8c81d9", size = 4608773 }, + { url = "https://files.pythonhosted.org/packages/55/04/5e6de6e6120451ec0c24516c41dbaf80cce1b6451f96561235ef2429da2e/pillow-10.4.0-cp312-cp312-win32.whl", hash = "sha256:7dfecdbad5c301d7b5bde160150b4db4c659cee2b69589705b6f8a0c509d9f42", size = 2235690 }, + { url = "https://files.pythonhosted.org/packages/74/0a/d4ce3c44bca8635bd29a2eab5aa181b654a734a29b263ca8efe013beea98/pillow-10.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:1d846aea995ad352d4bdcc847535bd56e0fd88d36829d2c90be880ef1ee4668a", size = 2554951 }, + { url = "https://files.pythonhosted.org/packages/b5/ca/184349ee40f2e92439be9b3502ae6cfc43ac4b50bc4fc6b3de7957563894/pillow-10.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:e553cad5179a66ba15bb18b353a19020e73a7921296a7979c4a2b7f6a5cd57f9", size = 2243427 }, + { url = "https://files.pythonhosted.org/packages/c3/00/706cebe7c2c12a6318aabe5d354836f54adff7156fd9e1bd6c89f4ba0e98/pillow-10.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8bc1a764ed8c957a2e9cacf97c8b2b053b70307cf2996aafd70e91a082e70df3", size = 3525685 }, + { url = "https://files.pythonhosted.org/packages/cf/76/f658cbfa49405e5ecbfb9ba42d07074ad9792031267e782d409fd8fe7c69/pillow-10.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6209bb41dc692ddfee4942517c19ee81b86c864b626dbfca272ec0f7cff5d9fb", size = 3374883 }, + { url = "https://files.pythonhosted.org/packages/46/2b/99c28c4379a85e65378211971c0b430d9c7234b1ec4d59b2668f6299e011/pillow-10.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bee197b30783295d2eb680b311af15a20a8b24024a19c3a26431ff83eb8d1f70", size = 4339837 }, + { url = "https://files.pythonhosted.org/packages/f1/74/b1ec314f624c0c43711fdf0d8076f82d9d802afd58f1d62c2a86878e8615/pillow-10.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ef61f5dd14c300786318482456481463b9d6b91ebe5ef12f405afbba77ed0be", size = 4455562 }, + { url = "https://files.pythonhosted.org/packages/4a/2a/4b04157cb7b9c74372fa867096a1607e6fedad93a44deeff553ccd307868/pillow-10.4.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:297e388da6e248c98bc4a02e018966af0c5f92dfacf5a5ca22fa01cb3179bca0", size = 4366761 }, + { url = "https://files.pythonhosted.org/packages/ac/7b/8f1d815c1a6a268fe90481232c98dd0e5fa8c75e341a75f060037bd5ceae/pillow-10.4.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:e4db64794ccdf6cb83a59d73405f63adbe2a1887012e308828596100a0b2f6cc", size = 4536767 }, + { url = "https://files.pythonhosted.org/packages/e5/77/05fa64d1f45d12c22c314e7b97398ffb28ef2813a485465017b7978b3ce7/pillow-10.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bd2880a07482090a3bcb01f4265f1936a903d70bc740bfcb1fd4e8a2ffe5cf5a", size = 4477989 }, + { url = "https://files.pythonhosted.org/packages/12/63/b0397cfc2caae05c3fb2f4ed1b4fc4fc878f0243510a7a6034ca59726494/pillow-10.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b35b21b819ac1dbd1233317adeecd63495f6babf21b7b2512d244ff6c6ce309", size = 4610255 }, + { url = "https://files.pythonhosted.org/packages/7b/f9/cfaa5082ca9bc4a6de66ffe1c12c2d90bf09c309a5f52b27759a596900e7/pillow-10.4.0-cp313-cp313-win32.whl", hash = "sha256:551d3fd6e9dc15e4c1eb6fc4ba2b39c0c7933fa113b220057a34f4bb3268a060", size = 2235603 }, + { url = "https://files.pythonhosted.org/packages/01/6a/30ff0eef6e0c0e71e55ded56a38d4859bf9d3634a94a88743897b5f96936/pillow-10.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:030abdbe43ee02e0de642aee345efa443740aa4d828bfe8e2eb11922ea6a21ea", size = 2554972 }, + { url = "https://files.pythonhosted.org/packages/48/2c/2e0a52890f269435eee38b21c8218e102c621fe8d8df8b9dd06fabf879ba/pillow-10.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:5b001114dd152cfd6b23befeb28d7aee43553e2402c9f159807bf55f33af8a8d", size = 2243375 }, + { url = "https://files.pythonhosted.org/packages/38/30/095d4f55f3a053392f75e2eae45eba3228452783bab3d9a920b951ac495c/pillow-10.4.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5b4815f2e65b30f5fbae9dfffa8636d992d49705723fe86a3661806e069352d4", size = 3493889 }, + { url = "https://files.pythonhosted.org/packages/f3/e8/4ff79788803a5fcd5dc35efdc9386af153569853767bff74540725b45863/pillow-10.4.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:8f0aef4ef59694b12cadee839e2ba6afeab89c0f39a3adc02ed51d109117b8da", size = 3346160 }, + { url = "https://files.pythonhosted.org/packages/d7/ac/4184edd511b14f760c73f5bb8a5d6fd85c591c8aff7c2229677a355c4179/pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9f4727572e2918acaa9077c919cbbeb73bd2b3ebcfe033b72f858fc9fbef0026", size = 3435020 }, + { url = "https://files.pythonhosted.org/packages/da/21/1749cd09160149c0a246a81d646e05f35041619ce76f6493d6a96e8d1103/pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff25afb18123cea58a591ea0244b92eb1e61a1fd497bf6d6384f09bc3262ec3e", size = 3490539 }, + { url = "https://files.pythonhosted.org/packages/b6/f5/f71fe1888b96083b3f6dfa0709101f61fc9e972c0c8d04e9d93ccef2a045/pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:dc3e2db6ba09ffd7d02ae9141cfa0ae23393ee7687248d46a7507b75d610f4f5", size = 3476125 }, + { url = "https://files.pythonhosted.org/packages/96/b9/c0362c54290a31866c3526848583a2f45a535aa9d725fd31e25d318c805f/pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:02a2be69f9c9b8c1e97cf2713e789d4e398c751ecfd9967c18d0ce304efbf885", size = 3579373 }, + { url = "https://files.pythonhosted.org/packages/52/3b/ce7a01026a7cf46e5452afa86f97a5e88ca97f562cafa76570178ab56d8d/pillow-10.4.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:0755ffd4a0c6f267cccbae2e9903d95477ca2f77c4fcf3a3a09570001856c8a5", size = 2554661 }, +] + +[[package]] +name = "platformdirs" +version = "4.3.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/8b/3c73abc9c759ecd3f1f7ceff6685840859e8070c4d947c93fae71f6a0bf2/platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc", size = 21362 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/39/979e8e21520d4e47a0bbe349e2713c0aac6f3d853d0e5b34d76206c439aa/platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4", size = 18567 }, +] + +[[package]] +name = "playwright" +version = "1.54.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "greenlet" }, + { name = "pyee" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/09/33d5bfe393a582d8dac72165a9e88b274143c9df411b65ece1cc13f42988/playwright-1.54.0-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:bf3b845af744370f1bd2286c2a9536f474cc8a88dc995b72ea9a5be714c9a77d", size = 40439034 }, + { url = "https://files.pythonhosted.org/packages/e1/7b/51882dc584f7aa59f446f2bb34e33c0e5f015de4e31949e5b7c2c10e54f0/playwright-1.54.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:780928b3ca2077aea90414b37e54edd0c4bbb57d1aafc42f7aa0b3fd2c2fac02", size = 38702308 }, + { url = "https://files.pythonhosted.org/packages/73/a1/7aa8ae175b240c0ec8849fcf000e078f3c693f9aa2ffd992da6550ea0dff/playwright-1.54.0-py3-none-macosx_11_0_universal2.whl", hash = "sha256:81d0b6f28843b27f288cfe438af0a12a4851de57998009a519ea84cee6fbbfb9", size = 40439037 }, + { url = "https://files.pythonhosted.org/packages/34/a9/45084fd23b6206f954198296ce39b0acf50debfdf3ec83a593e4d73c9c8a/playwright-1.54.0-py3-none-manylinux1_x86_64.whl", hash = "sha256:09919f45cc74c64afb5432646d7fef0d19fff50990c862cb8d9b0577093f40cc", size = 45920135 }, + { url = "https://files.pythonhosted.org/packages/02/d4/6a692f4c6db223adc50a6e53af405b45308db39270957a6afebddaa80ea2/playwright-1.54.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:13ae206c55737e8e3eae51fb385d61c0312eeef31535643bb6232741b41b6fdc", size = 45302695 }, + { url = "https://files.pythonhosted.org/packages/72/7a/4ee60a1c3714321db187bebbc40d52cea5b41a856925156325058b5fca5a/playwright-1.54.0-py3-none-win32.whl", hash = "sha256:0b108622ffb6906e28566f3f31721cd57dda637d7e41c430287804ac01911f56", size = 35469309 }, + { url = "https://files.pythonhosted.org/packages/aa/77/8f8fae05a242ef639de963d7ae70a69d0da61d6d72f1207b8bbf74ffd3e7/playwright-1.54.0-py3-none-win_amd64.whl", hash = "sha256:9e5aee9ae5ab1fdd44cd64153313a2045b136fcbcfb2541cc0a3d909132671a2", size = 35469311 }, + { url = "https://files.pythonhosted.org/packages/33/ff/99a6f4292a90504f2927d34032a4baf6adb498dc3f7cf0f3e0e22899e310/playwright-1.54.0-py3-none-win_arm64.whl", hash = "sha256:a975815971f7b8dca505c441a4c56de1aeb56a211290f8cc214eeef5524e8d75", size = 31239119 }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538 }, +] + +[[package]] +name = "portalocker" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pywin32", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1f/f8/969e6f280201b40b31bcb62843c619f343dcc351dff83a5891530c9dd60e/portalocker-2.7.0.tar.gz", hash = "sha256:032e81d534a88ec1736d03f780ba073f047a06c478b06e2937486f334e955c51", size = 20183 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8c/df/d4f711d168524f5aebd7fb30969eaa31e3048cf8979688cde3b08f6e5eb8/portalocker-2.7.0-py2.py3-none-any.whl", hash = "sha256:a07c5b4f3985c3cf4798369631fb7011adb498e2a46d8440efc75a8f29a0f983", size = 15502 }, +] + +[[package]] +name = "posthog" +version = "3.25.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "backoff" }, + { name = "distro" }, + { name = "monotonic" }, + { name = "python-dateutil" }, + { name = "requests" }, + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/85/a9/ec3bbc23b6f3c23c52e0b5795b1357cca74aa5cfb254213f1e471fef9b4d/posthog-3.25.0.tar.gz", hash = "sha256:9168f3e7a0a5571b6b1065c41b3c171fbc68bfe72c3ac0bfd6e3d2fcdb7df2ca", size = 75968 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/e2/c158366e621562ef224f132e75c1d1c1fce6b078a19f7d8060451a12d4b9/posthog-3.25.0-py2.py3-none-any.whl", hash = "sha256:85db78c13d1ecb11aed06fad53759c4e8fb3633442c2f3d0336bc0ce8a585d30", size = 89115 }, +] + +[[package]] +name = "prompt-toolkit" +version = "3.0.51" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wcwidth" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bb/6e/9d084c929dfe9e3bfe0c6a47e31f78a25c54627d64a66e884a8bf5474f1c/prompt_toolkit-3.0.51.tar.gz", hash = "sha256:931a162e3b27fc90c86f1b48bb1fb2c528c2761475e57c9c06de13311c7b54ed", size = 428940 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/4f/5249960887b1fbe561d9ff265496d170b55a735b76724f10ef19f9e40716/prompt_toolkit-3.0.51-py3-none-any.whl", hash = "sha256:52742911fde84e2d423e2f9a4cf1de7d7ac4e51958f648d9540e0fb8db077b07", size = 387810 }, +] + +[[package]] +name = "propcache" +version = "0.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a6/16/43264e4a779dd8588c21a70f0709665ee8f611211bdd2c87d952cfa7c776/propcache-0.3.2.tar.gz", hash = "sha256:20d7d62e4e7ef05f221e0db2856b979540686342e7dd9973b815599c7057e168", size = 44139 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ab/14/510deed325e262afeb8b360043c5d7c960da7d3ecd6d6f9496c9c56dc7f4/propcache-0.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:22d9962a358aedbb7a2e36187ff273adeaab9743373a272976d2e348d08c7770", size = 73178 }, + { url = "https://files.pythonhosted.org/packages/cd/4e/ad52a7925ff01c1325653a730c7ec3175a23f948f08626a534133427dcff/propcache-0.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0d0fda578d1dc3f77b6b5a5dce3b9ad69a8250a891760a548df850a5e8da87f3", size = 43133 }, + { url = "https://files.pythonhosted.org/packages/63/7c/e9399ba5da7780871db4eac178e9c2e204c23dd3e7d32df202092a1ed400/propcache-0.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3def3da3ac3ce41562d85db655d18ebac740cb3fa4367f11a52b3da9d03a5cc3", size = 43039 }, + { url = "https://files.pythonhosted.org/packages/22/e1/58da211eb8fdc6fc854002387d38f415a6ca5f5c67c1315b204a5d3e9d7a/propcache-0.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9bec58347a5a6cebf239daba9bda37dffec5b8d2ce004d9fe4edef3d2815137e", size = 201903 }, + { url = "https://files.pythonhosted.org/packages/c4/0a/550ea0f52aac455cb90111c8bab995208443e46d925e51e2f6ebdf869525/propcache-0.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:55ffda449a507e9fbd4aca1a7d9aa6753b07d6166140e5a18d2ac9bc49eac220", size = 213362 }, + { url = "https://files.pythonhosted.org/packages/5a/af/9893b7d878deda9bb69fcf54600b247fba7317761b7db11fede6e0f28bd0/propcache-0.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:64a67fb39229a8a8491dd42f864e5e263155e729c2e7ff723d6e25f596b1e8cb", size = 210525 }, + { url = "https://files.pythonhosted.org/packages/7c/bb/38fd08b278ca85cde36d848091ad2b45954bc5f15cce494bb300b9285831/propcache-0.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9da1cf97b92b51253d5b68cf5a2b9e0dafca095e36b7f2da335e27dc6172a614", size = 198283 }, + { url = "https://files.pythonhosted.org/packages/78/8c/9fe55bd01d362bafb413dfe508c48753111a1e269737fa143ba85693592c/propcache-0.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5f559e127134b07425134b4065be45b166183fdcb433cb6c24c8e4149056ad50", size = 191872 }, + { url = "https://files.pythonhosted.org/packages/54/14/4701c33852937a22584e08abb531d654c8bcf7948a8f87ad0a4822394147/propcache-0.3.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:aff2e4e06435d61f11a428360a932138d0ec288b0a31dd9bd78d200bd4a2b339", size = 199452 }, + { url = "https://files.pythonhosted.org/packages/16/44/447f2253d859602095356007657ee535e0093215ea0b3d1d6a41d16e5201/propcache-0.3.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:4927842833830942a5d0a56e6f4839bc484785b8e1ce8d287359794818633ba0", size = 191567 }, + { url = "https://files.pythonhosted.org/packages/f2/b3/e4756258749bb2d3b46defcff606a2f47410bab82be5824a67e84015b267/propcache-0.3.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:6107ddd08b02654a30fb8ad7a132021759d750a82578b94cd55ee2772b6ebea2", size = 193015 }, + { url = "https://files.pythonhosted.org/packages/1e/df/e6d3c7574233164b6330b9fd697beeac402afd367280e6dc377bb99b43d9/propcache-0.3.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:70bd8b9cd6b519e12859c99f3fc9a93f375ebd22a50296c3a295028bea73b9e7", size = 204660 }, + { url = "https://files.pythonhosted.org/packages/b2/53/e4d31dd5170b4a0e2e6b730f2385a96410633b4833dc25fe5dffd1f73294/propcache-0.3.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2183111651d710d3097338dd1893fcf09c9f54e27ff1a8795495a16a469cc90b", size = 206105 }, + { url = "https://files.pythonhosted.org/packages/7f/fe/74d54cf9fbe2a20ff786e5f7afcfde446588f0cf15fb2daacfbc267b866c/propcache-0.3.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:fb075ad271405dcad8e2a7ffc9a750a3bf70e533bd86e89f0603e607b93aa64c", size = 196980 }, + { url = "https://files.pythonhosted.org/packages/22/ec/c469c9d59dada8a7679625e0440b544fe72e99311a4679c279562051f6fc/propcache-0.3.2-cp310-cp310-win32.whl", hash = "sha256:404d70768080d3d3bdb41d0771037da19d8340d50b08e104ca0e7f9ce55fce70", size = 37679 }, + { url = "https://files.pythonhosted.org/packages/38/35/07a471371ac89d418f8d0b699c75ea6dca2041fbda360823de21f6a9ce0a/propcache-0.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:7435d766f978b4ede777002e6b3b6641dd229cd1da8d3d3106a45770365f9ad9", size = 41459 }, + { url = "https://files.pythonhosted.org/packages/80/8d/e8b436717ab9c2cfc23b116d2c297305aa4cd8339172a456d61ebf5669b8/propcache-0.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0b8d2f607bd8f80ddc04088bc2a037fdd17884a6fcadc47a96e334d72f3717be", size = 74207 }, + { url = "https://files.pythonhosted.org/packages/d6/29/1e34000e9766d112171764b9fa3226fa0153ab565d0c242c70e9945318a7/propcache-0.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:06766d8f34733416e2e34f46fea488ad5d60726bb9481d3cddf89a6fa2d9603f", size = 43648 }, + { url = "https://files.pythonhosted.org/packages/46/92/1ad5af0df781e76988897da39b5f086c2bf0f028b7f9bd1f409bb05b6874/propcache-0.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a2dc1f4a1df4fecf4e6f68013575ff4af84ef6f478fe5344317a65d38a8e6dc9", size = 43496 }, + { url = "https://files.pythonhosted.org/packages/b3/ce/e96392460f9fb68461fabab3e095cb00c8ddf901205be4eae5ce246e5b7e/propcache-0.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be29c4f4810c5789cf10ddf6af80b041c724e629fa51e308a7a0fb19ed1ef7bf", size = 217288 }, + { url = "https://files.pythonhosted.org/packages/c5/2a/866726ea345299f7ceefc861a5e782b045545ae6940851930a6adaf1fca6/propcache-0.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:59d61f6970ecbd8ff2e9360304d5c8876a6abd4530cb752c06586849ac8a9dc9", size = 227456 }, + { url = "https://files.pythonhosted.org/packages/de/03/07d992ccb6d930398689187e1b3c718339a1c06b8b145a8d9650e4726166/propcache-0.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:62180e0b8dbb6b004baec00a7983e4cc52f5ada9cd11f48c3528d8cfa7b96a66", size = 225429 }, + { url = "https://files.pythonhosted.org/packages/5d/e6/116ba39448753b1330f48ab8ba927dcd6cf0baea8a0ccbc512dfb49ba670/propcache-0.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c144ca294a204c470f18cf4c9d78887810d04a3e2fbb30eea903575a779159df", size = 213472 }, + { url = "https://files.pythonhosted.org/packages/a6/85/f01f5d97e54e428885a5497ccf7f54404cbb4f906688a1690cd51bf597dc/propcache-0.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c5c2a784234c28854878d68978265617aa6dc0780e53d44b4d67f3651a17a9a2", size = 204480 }, + { url = "https://files.pythonhosted.org/packages/e3/79/7bf5ab9033b8b8194cc3f7cf1aaa0e9c3256320726f64a3e1f113a812dce/propcache-0.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5745bc7acdafa978ca1642891b82c19238eadc78ba2aaa293c6863b304e552d7", size = 214530 }, + { url = "https://files.pythonhosted.org/packages/31/0b/bd3e0c00509b609317df4a18e6b05a450ef2d9a963e1d8bc9c9415d86f30/propcache-0.3.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:c0075bf773d66fa8c9d41f66cc132ecc75e5bb9dd7cce3cfd14adc5ca184cb95", size = 205230 }, + { url = "https://files.pythonhosted.org/packages/7a/23/fae0ff9b54b0de4e819bbe559508da132d5683c32d84d0dc2ccce3563ed4/propcache-0.3.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5f57aa0847730daceff0497f417c9de353c575d8da3579162cc74ac294c5369e", size = 206754 }, + { url = "https://files.pythonhosted.org/packages/b7/7f/ad6a3c22630aaa5f618b4dc3c3598974a72abb4c18e45a50b3cdd091eb2f/propcache-0.3.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:eef914c014bf72d18efb55619447e0aecd5fb7c2e3fa7441e2e5d6099bddff7e", size = 218430 }, + { url = "https://files.pythonhosted.org/packages/5b/2c/ba4f1c0e8a4b4c75910742f0d333759d441f65a1c7f34683b4a74c0ee015/propcache-0.3.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2a4092e8549031e82facf3decdbc0883755d5bbcc62d3aea9d9e185549936dcf", size = 223884 }, + { url = "https://files.pythonhosted.org/packages/88/e4/ebe30fc399e98572019eee82ad0caf512401661985cbd3da5e3140ffa1b0/propcache-0.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:85871b050f174bc0bfb437efbdb68aaf860611953ed12418e4361bc9c392749e", size = 211480 }, + { url = "https://files.pythonhosted.org/packages/96/0a/7d5260b914e01d1d0906f7f38af101f8d8ed0dc47426219eeaf05e8ea7c2/propcache-0.3.2-cp311-cp311-win32.whl", hash = "sha256:36c8d9b673ec57900c3554264e630d45980fd302458e4ac801802a7fd2ef7897", size = 37757 }, + { url = "https://files.pythonhosted.org/packages/e1/2d/89fe4489a884bc0da0c3278c552bd4ffe06a1ace559db5ef02ef24ab446b/propcache-0.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:e53af8cb6a781b02d2ea079b5b853ba9430fcbe18a8e3ce647d5982a3ff69f39", size = 41500 }, + { url = "https://files.pythonhosted.org/packages/a8/42/9ca01b0a6f48e81615dca4765a8f1dd2c057e0540f6116a27dc5ee01dfb6/propcache-0.3.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:8de106b6c84506b31c27168582cd3cb3000a6412c16df14a8628e5871ff83c10", size = 73674 }, + { url = "https://files.pythonhosted.org/packages/af/6e/21293133beb550f9c901bbece755d582bfaf2176bee4774000bd4dd41884/propcache-0.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:28710b0d3975117239c76600ea351934ac7b5ff56e60953474342608dbbb6154", size = 43570 }, + { url = "https://files.pythonhosted.org/packages/0c/c8/0393a0a3a2b8760eb3bde3c147f62b20044f0ddac81e9d6ed7318ec0d852/propcache-0.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce26862344bdf836650ed2487c3d724b00fbfec4233a1013f597b78c1cb73615", size = 43094 }, + { url = "https://files.pythonhosted.org/packages/37/2c/489afe311a690399d04a3e03b069225670c1d489eb7b044a566511c1c498/propcache-0.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bca54bd347a253af2cf4544bbec232ab982f4868de0dd684246b67a51bc6b1db", size = 226958 }, + { url = "https://files.pythonhosted.org/packages/9d/ca/63b520d2f3d418c968bf596839ae26cf7f87bead026b6192d4da6a08c467/propcache-0.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:55780d5e9a2ddc59711d727226bb1ba83a22dd32f64ee15594b9392b1f544eb1", size = 234894 }, + { url = "https://files.pythonhosted.org/packages/11/60/1d0ed6fff455a028d678df30cc28dcee7af77fa2b0e6962ce1df95c9a2a9/propcache-0.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:035e631be25d6975ed87ab23153db6a73426a48db688070d925aa27e996fe93c", size = 233672 }, + { url = "https://files.pythonhosted.org/packages/37/7c/54fd5301ef38505ab235d98827207176a5c9b2aa61939b10a460ca53e123/propcache-0.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee6f22b6eaa39297c751d0e80c0d3a454f112f5c6481214fcf4c092074cecd67", size = 224395 }, + { url = "https://files.pythonhosted.org/packages/ee/1a/89a40e0846f5de05fdc6779883bf46ba980e6df4d2ff8fb02643de126592/propcache-0.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7ca3aee1aa955438c4dba34fc20a9f390e4c79967257d830f137bd5a8a32ed3b", size = 212510 }, + { url = "https://files.pythonhosted.org/packages/5e/33/ca98368586c9566a6b8d5ef66e30484f8da84c0aac3f2d9aec6d31a11bd5/propcache-0.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7a4f30862869fa2b68380d677cc1c5fcf1e0f2b9ea0cf665812895c75d0ca3b8", size = 222949 }, + { url = "https://files.pythonhosted.org/packages/ba/11/ace870d0aafe443b33b2f0b7efdb872b7c3abd505bfb4890716ad7865e9d/propcache-0.3.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b77ec3c257d7816d9f3700013639db7491a434644c906a2578a11daf13176251", size = 217258 }, + { url = "https://files.pythonhosted.org/packages/5b/d2/86fd6f7adffcfc74b42c10a6b7db721d1d9ca1055c45d39a1a8f2a740a21/propcache-0.3.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:cab90ac9d3f14b2d5050928483d3d3b8fb6b4018893fc75710e6aa361ecb2474", size = 213036 }, + { url = "https://files.pythonhosted.org/packages/07/94/2d7d1e328f45ff34a0a284cf5a2847013701e24c2a53117e7c280a4316b3/propcache-0.3.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:0b504d29f3c47cf6b9e936c1852246c83d450e8e063d50562115a6be6d3a2535", size = 227684 }, + { url = "https://files.pythonhosted.org/packages/b7/05/37ae63a0087677e90b1d14710e532ff104d44bc1efa3b3970fff99b891dc/propcache-0.3.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:ce2ac2675a6aa41ddb2a0c9cbff53780a617ac3d43e620f8fd77ba1c84dcfc06", size = 234562 }, + { url = "https://files.pythonhosted.org/packages/a4/7c/3f539fcae630408d0bd8bf3208b9a647ccad10976eda62402a80adf8fc34/propcache-0.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:62b4239611205294cc433845b914131b2a1f03500ff3c1ed093ed216b82621e1", size = 222142 }, + { url = "https://files.pythonhosted.org/packages/7c/d2/34b9eac8c35f79f8a962546b3e97e9d4b990c420ee66ac8255d5d9611648/propcache-0.3.2-cp312-cp312-win32.whl", hash = "sha256:df4a81b9b53449ebc90cc4deefb052c1dd934ba85012aa912c7ea7b7e38b60c1", size = 37711 }, + { url = "https://files.pythonhosted.org/packages/19/61/d582be5d226cf79071681d1b46b848d6cb03d7b70af7063e33a2787eaa03/propcache-0.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:7046e79b989d7fe457bb755844019e10f693752d169076138abf17f31380800c", size = 41479 }, + { url = "https://files.pythonhosted.org/packages/dc/d1/8c747fafa558c603c4ca19d8e20b288aa0c7cda74e9402f50f31eb65267e/propcache-0.3.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ca592ed634a73ca002967458187109265e980422116c0a107cf93d81f95af945", size = 71286 }, + { url = "https://files.pythonhosted.org/packages/61/99/d606cb7986b60d89c36de8a85d58764323b3a5ff07770a99d8e993b3fa73/propcache-0.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9ecb0aad4020e275652ba3975740f241bd12a61f1a784df044cf7477a02bc252", size = 42425 }, + { url = "https://files.pythonhosted.org/packages/8c/96/ef98f91bbb42b79e9bb82bdd348b255eb9d65f14dbbe3b1594644c4073f7/propcache-0.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7f08f1cc28bd2eade7a8a3d2954ccc673bb02062e3e7da09bc75d843386b342f", size = 41846 }, + { url = "https://files.pythonhosted.org/packages/5b/ad/3f0f9a705fb630d175146cd7b1d2bf5555c9beaed54e94132b21aac098a6/propcache-0.3.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1a342c834734edb4be5ecb1e9fb48cb64b1e2320fccbd8c54bf8da8f2a84c33", size = 208871 }, + { url = "https://files.pythonhosted.org/packages/3a/38/2085cda93d2c8b6ec3e92af2c89489a36a5886b712a34ab25de9fbca7992/propcache-0.3.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8a544caaae1ac73f1fecfae70ded3e93728831affebd017d53449e3ac052ac1e", size = 215720 }, + { url = "https://files.pythonhosted.org/packages/61/c1/d72ea2dc83ac7f2c8e182786ab0fc2c7bd123a1ff9b7975bee671866fe5f/propcache-0.3.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:310d11aa44635298397db47a3ebce7db99a4cc4b9bbdfcf6c98a60c8d5261cf1", size = 215203 }, + { url = "https://files.pythonhosted.org/packages/af/81/b324c44ae60c56ef12007105f1460d5c304b0626ab0cc6b07c8f2a9aa0b8/propcache-0.3.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c1396592321ac83157ac03a2023aa6cc4a3cc3cfdecb71090054c09e5a7cce3", size = 206365 }, + { url = "https://files.pythonhosted.org/packages/09/73/88549128bb89e66d2aff242488f62869014ae092db63ccea53c1cc75a81d/propcache-0.3.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8cabf5b5902272565e78197edb682017d21cf3b550ba0460ee473753f28d23c1", size = 196016 }, + { url = "https://files.pythonhosted.org/packages/b9/3f/3bdd14e737d145114a5eb83cb172903afba7242f67c5877f9909a20d948d/propcache-0.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0a2f2235ac46a7aa25bdeb03a9e7060f6ecbd213b1f9101c43b3090ffb971ef6", size = 205596 }, + { url = "https://files.pythonhosted.org/packages/0f/ca/2f4aa819c357d3107c3763d7ef42c03980f9ed5c48c82e01e25945d437c1/propcache-0.3.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:92b69e12e34869a6970fd2f3da91669899994b47c98f5d430b781c26f1d9f387", size = 200977 }, + { url = "https://files.pythonhosted.org/packages/cd/4a/e65276c7477533c59085251ae88505caf6831c0e85ff8b2e31ebcbb949b1/propcache-0.3.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:54e02207c79968ebbdffc169591009f4474dde3b4679e16634d34c9363ff56b4", size = 197220 }, + { url = "https://files.pythonhosted.org/packages/7c/54/fc7152e517cf5578278b242396ce4d4b36795423988ef39bb8cd5bf274c8/propcache-0.3.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4adfb44cb588001f68c5466579d3f1157ca07f7504fc91ec87862e2b8e556b88", size = 210642 }, + { url = "https://files.pythonhosted.org/packages/b9/80/abeb4a896d2767bf5f1ea7b92eb7be6a5330645bd7fb844049c0e4045d9d/propcache-0.3.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:fd3e6019dc1261cd0291ee8919dd91fbab7b169bb76aeef6c716833a3f65d206", size = 212789 }, + { url = "https://files.pythonhosted.org/packages/b3/db/ea12a49aa7b2b6d68a5da8293dcf50068d48d088100ac016ad92a6a780e6/propcache-0.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4c181cad81158d71c41a2bce88edce078458e2dd5ffee7eddd6b05da85079f43", size = 205880 }, + { url = "https://files.pythonhosted.org/packages/d1/e5/9076a0bbbfb65d1198007059c65639dfd56266cf8e477a9707e4b1999ff4/propcache-0.3.2-cp313-cp313-win32.whl", hash = "sha256:8a08154613f2249519e549de2330cf8e2071c2887309a7b07fb56098f5170a02", size = 37220 }, + { url = "https://files.pythonhosted.org/packages/d3/f5/b369e026b09a26cd77aa88d8fffd69141d2ae00a2abaaf5380d2603f4b7f/propcache-0.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:e41671f1594fc4ab0a6dec1351864713cb3a279910ae8b58f884a88a0a632c05", size = 40678 }, + { url = "https://files.pythonhosted.org/packages/a4/3a/6ece377b55544941a08d03581c7bc400a3c8cd3c2865900a68d5de79e21f/propcache-0.3.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:9a3cf035bbaf035f109987d9d55dc90e4b0e36e04bbbb95af3055ef17194057b", size = 76560 }, + { url = "https://files.pythonhosted.org/packages/0c/da/64a2bb16418740fa634b0e9c3d29edff1db07f56d3546ca2d86ddf0305e1/propcache-0.3.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:156c03d07dc1323d8dacaa221fbe028c5c70d16709cdd63502778e6c3ccca1b0", size = 44676 }, + { url = "https://files.pythonhosted.org/packages/36/7b/f025e06ea51cb72c52fb87e9b395cced02786610b60a3ed51da8af017170/propcache-0.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:74413c0ba02ba86f55cf60d18daab219f7e531620c15f1e23d95563f505efe7e", size = 44701 }, + { url = "https://files.pythonhosted.org/packages/a4/00/faa1b1b7c3b74fc277f8642f32a4c72ba1d7b2de36d7cdfb676db7f4303e/propcache-0.3.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f066b437bb3fa39c58ff97ab2ca351db465157d68ed0440abecb21715eb24b28", size = 276934 }, + { url = "https://files.pythonhosted.org/packages/74/ab/935beb6f1756e0476a4d5938ff44bf0d13a055fed880caf93859b4f1baf4/propcache-0.3.2-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f1304b085c83067914721e7e9d9917d41ad87696bf70f0bc7dee450e9c71ad0a", size = 278316 }, + { url = "https://files.pythonhosted.org/packages/f8/9d/994a5c1ce4389610838d1caec74bdf0e98b306c70314d46dbe4fcf21a3e2/propcache-0.3.2-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ab50cef01b372763a13333b4e54021bdcb291fc9a8e2ccb9c2df98be51bcde6c", size = 282619 }, + { url = "https://files.pythonhosted.org/packages/2b/00/a10afce3d1ed0287cef2e09506d3be9822513f2c1e96457ee369adb9a6cd/propcache-0.3.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fad3b2a085ec259ad2c2842666b2a0a49dea8463579c606426128925af1ed725", size = 265896 }, + { url = "https://files.pythonhosted.org/packages/2e/a8/2aa6716ffa566ca57c749edb909ad27884680887d68517e4be41b02299f3/propcache-0.3.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:261fa020c1c14deafd54c76b014956e2f86991af198c51139faf41c4d5e83892", size = 252111 }, + { url = "https://files.pythonhosted.org/packages/36/4f/345ca9183b85ac29c8694b0941f7484bf419c7f0fea2d1e386b4f7893eed/propcache-0.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:46d7f8aa79c927e5f987ee3a80205c987717d3659f035c85cf0c3680526bdb44", size = 268334 }, + { url = "https://files.pythonhosted.org/packages/3e/ca/fcd54f78b59e3f97b3b9715501e3147f5340167733d27db423aa321e7148/propcache-0.3.2-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:6d8f3f0eebf73e3c0ff0e7853f68be638b4043c65a70517bb575eff54edd8dbe", size = 255026 }, + { url = "https://files.pythonhosted.org/packages/8b/95/8e6a6bbbd78ac89c30c225210a5c687790e532ba4088afb8c0445b77ef37/propcache-0.3.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:03c89c1b14a5452cf15403e291c0ccd7751d5b9736ecb2c5bab977ad6c5bcd81", size = 250724 }, + { url = "https://files.pythonhosted.org/packages/ee/b0/0dd03616142baba28e8b2d14ce5df6631b4673850a3d4f9c0f9dd714a404/propcache-0.3.2-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:0cc17efde71e12bbaad086d679ce575268d70bc123a5a71ea7ad76f70ba30bba", size = 268868 }, + { url = "https://files.pythonhosted.org/packages/c5/98/2c12407a7e4fbacd94ddd32f3b1e3d5231e77c30ef7162b12a60e2dd5ce3/propcache-0.3.2-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:acdf05d00696bc0447e278bb53cb04ca72354e562cf88ea6f9107df8e7fd9770", size = 271322 }, + { url = "https://files.pythonhosted.org/packages/35/91/9cb56efbb428b006bb85db28591e40b7736847b8331d43fe335acf95f6c8/propcache-0.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4445542398bd0b5d32df908031cb1b30d43ac848e20470a878b770ec2dcc6330", size = 265778 }, + { url = "https://files.pythonhosted.org/packages/9a/4c/b0fe775a2bdd01e176b14b574be679d84fc83958335790f7c9a686c1f468/propcache-0.3.2-cp313-cp313t-win32.whl", hash = "sha256:f86e5d7cd03afb3a1db8e9f9f6eff15794e79e791350ac48a8c924e6f439f394", size = 41175 }, + { url = "https://files.pythonhosted.org/packages/a4/ff/47f08595e3d9b5e149c150f88d9714574f1a7cbd89fe2817158a952674bf/propcache-0.3.2-cp313-cp313t-win_amd64.whl", hash = "sha256:9704bedf6e7cbe3c65eca4379a9b53ee6a83749f047808cbb5044d40d7d72198", size = 44857 }, + { url = "https://files.pythonhosted.org/packages/cc/35/cc0aaecf278bb4575b8555f2b137de5ab821595ddae9da9d3cd1da4072c7/propcache-0.3.2-py3-none-any.whl", hash = "sha256:98f1ec44fb675f5052cccc8e609c46ed23a35a1cfd18545ad4e29002d858a43f", size = 12663 }, +] + +[[package]] +name = "proto-plus" +version = "1.26.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f4/ac/87285f15f7cce6d4a008f33f1757fb5a13611ea8914eb58c3d0d26243468/proto_plus-1.26.1.tar.gz", hash = "sha256:21a515a4c4c0088a773899e23c7bbade3d18f9c66c73edd4c7ee3816bc96a012", size = 56142 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4e/6d/280c4c2ce28b1593a19ad5239c8b826871fc6ec275c21afc8e1820108039/proto_plus-1.26.1-py3-none-any.whl", hash = "sha256:13285478c2dcf2abb829db158e1047e2f1e8d63a077d94263c2b88b043c75a66", size = 50163 }, +] + +[[package]] +name = "protobuf" +version = "6.31.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/f3/b9655a711b32c19720253f6f06326faf90580834e2e83f840472d752bc8b/protobuf-6.31.1.tar.gz", hash = "sha256:d8cac4c982f0b957a4dc73a80e2ea24fab08e679c0de9deb835f4a12d69aca9a", size = 441797 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/6f/6ab8e4bf962fd5570d3deaa2d5c38f0a363f57b4501047b5ebeb83ab1125/protobuf-6.31.1-cp310-abi3-win32.whl", hash = "sha256:7fa17d5a29c2e04b7d90e5e32388b8bfd0e7107cd8e616feef7ed3fa6bdab5c9", size = 423603 }, + { url = "https://files.pythonhosted.org/packages/44/3a/b15c4347dd4bf3a1b0ee882f384623e2063bb5cf9fa9d57990a4f7df2fb6/protobuf-6.31.1-cp310-abi3-win_amd64.whl", hash = "sha256:426f59d2964864a1a366254fa703b8632dcec0790d8862d30034d8245e1cd447", size = 435283 }, + { url = "https://files.pythonhosted.org/packages/6a/c9/b9689a2a250264a84e66c46d8862ba788ee7a641cdca39bccf64f59284b7/protobuf-6.31.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:6f1227473dc43d44ed644425268eb7c2e488ae245d51c6866d19fe158e207402", size = 425604 }, + { url = "https://files.pythonhosted.org/packages/76/a1/7a5a94032c83375e4fe7e7f56e3976ea6ac90c5e85fac8576409e25c39c3/protobuf-6.31.1-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:a40fc12b84c154884d7d4c4ebd675d5b3b5283e155f324049ae396b95ddebc39", size = 322115 }, + { url = "https://files.pythonhosted.org/packages/fa/b1/b59d405d64d31999244643d88c45c8241c58f17cc887e73bcb90602327f8/protobuf-6.31.1-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:4ee898bf66f7a8b0bd21bce523814e6fbd8c6add948045ce958b73af7e8878c6", size = 321070 }, + { url = "https://files.pythonhosted.org/packages/f7/af/ab3c51ab7507a7325e98ffe691d9495ee3d3aa5f589afad65ec920d39821/protobuf-6.31.1-py3-none-any.whl", hash = "sha256:720a6c7e6b77288b85063569baae8536671b39f15cc22037ec7045658d80489e", size = 168724 }, +] + +[[package]] +name = "psutil" +version = "7.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2a/80/336820c1ad9286a4ded7e845b2eccfcb27851ab8ac6abece774a6ff4d3de/psutil-7.0.0.tar.gz", hash = "sha256:7be9c3eba38beccb6495ea33afd982a44074b78f28c434a1f51cc07fd315c456", size = 497003 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/e6/2d26234410f8b8abdbf891c9da62bee396583f713fb9f3325a4760875d22/psutil-7.0.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:101d71dc322e3cffd7cea0650b09b3d08b8e7c4109dd6809fe452dfd00e58b25", size = 238051 }, + { url = "https://files.pythonhosted.org/packages/04/8b/30f930733afe425e3cbfc0e1468a30a18942350c1a8816acfade80c005c4/psutil-7.0.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:39db632f6bb862eeccf56660871433e111b6ea58f2caea825571951d4b6aa3da", size = 239535 }, + { url = "https://files.pythonhosted.org/packages/2a/ed/d362e84620dd22876b55389248e522338ed1bf134a5edd3b8231d7207f6d/psutil-7.0.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fcee592b4c6f146991ca55919ea3d1f8926497a713ed7faaf8225e174581e91", size = 275004 }, + { url = "https://files.pythonhosted.org/packages/bf/b9/b0eb3f3cbcb734d930fdf839431606844a825b23eaf9a6ab371edac8162c/psutil-7.0.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b1388a4f6875d7e2aff5c4ca1cc16c545ed41dd8bb596cefea80111db353a34", size = 277986 }, + { url = "https://files.pythonhosted.org/packages/eb/a2/709e0fe2f093556c17fbafda93ac032257242cabcc7ff3369e2cb76a97aa/psutil-7.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5f098451abc2828f7dc6b58d44b532b22f2088f4999a937557b603ce72b1993", size = 279544 }, + { url = "https://files.pythonhosted.org/packages/50/e6/eecf58810b9d12e6427369784efe814a1eec0f492084ce8eb8f4d89d6d61/psutil-7.0.0-cp37-abi3-win32.whl", hash = "sha256:ba3fcef7523064a6c9da440fc4d6bd07da93ac726b5733c29027d7dc95b39d99", size = 241053 }, + { url = "https://files.pythonhosted.org/packages/50/1b/6921afe68c74868b4c9fa424dad3be35b095e16687989ebbb50ce4fceb7c/psutil-7.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:4cf3d4eb1aa9b348dec30105c55cd9b7d4629285735a102beb4441e38db90553", size = 244885 }, +] + +[[package]] +name = "ptyprocess" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/e5/16ff212c1e452235a90aeb09066144d0c5a6a8c0834397e03f5224495c4e/ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220", size = 70762 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", size = 13993 }, +] + +[[package]] +name = "pure-eval" +version = "0.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/05/0a34433a064256a578f1783a10da6df098ceaa4a57bbeaa96a6c0352786b/pure_eval-0.2.3.tar.gz", hash = "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42", size = 19752 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0", size = 11842 }, +] + +[[package]] +name = "pyarrow" +version = "21.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ef/c2/ea068b8f00905c06329a3dfcd40d0fcc2b7d0f2e355bdb25b65e0a0e4cd4/pyarrow-21.0.0.tar.gz", hash = "sha256:5051f2dccf0e283ff56335760cbc8622cf52264d67e359d5569541ac11b6d5bc", size = 1133487 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/d9/110de31880016e2afc52d8580b397dbe47615defbf09ca8cf55f56c62165/pyarrow-21.0.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:e563271e2c5ff4d4a4cbeb2c83d5cf0d4938b891518e676025f7268c6fe5fe26", size = 31196837 }, + { url = "https://files.pythonhosted.org/packages/df/5f/c1c1997613abf24fceb087e79432d24c19bc6f7259cab57c2c8e5e545fab/pyarrow-21.0.0-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:fee33b0ca46f4c85443d6c450357101e47d53e6c3f008d658c27a2d020d44c79", size = 32659470 }, + { url = "https://files.pythonhosted.org/packages/3e/ed/b1589a777816ee33ba123ba1e4f8f02243a844fed0deec97bde9fb21a5cf/pyarrow-21.0.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:7be45519b830f7c24b21d630a31d48bcebfd5d4d7f9d3bdb49da9cdf6d764edb", size = 41055619 }, + { url = "https://files.pythonhosted.org/packages/44/28/b6672962639e85dc0ac36f71ab3a8f5f38e01b51343d7aa372a6b56fa3f3/pyarrow-21.0.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:26bfd95f6bff443ceae63c65dc7e048670b7e98bc892210acba7e4995d3d4b51", size = 42733488 }, + { url = "https://files.pythonhosted.org/packages/f8/cc/de02c3614874b9089c94eac093f90ca5dfa6d5afe45de3ba847fd950fdf1/pyarrow-21.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:bd04ec08f7f8bd113c55868bd3fc442a9db67c27af098c5f814a3091e71cc61a", size = 43329159 }, + { url = "https://files.pythonhosted.org/packages/a6/3e/99473332ac40278f196e105ce30b79ab8affab12f6194802f2593d6b0be2/pyarrow-21.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9b0b14b49ac10654332a805aedfc0147fb3469cbf8ea951b3d040dab12372594", size = 45050567 }, + { url = "https://files.pythonhosted.org/packages/7b/f5/c372ef60593d713e8bfbb7e0c743501605f0ad00719146dc075faf11172b/pyarrow-21.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:9d9f8bcb4c3be7738add259738abdeddc363de1b80e3310e04067aa1ca596634", size = 26217959 }, + { url = "https://files.pythonhosted.org/packages/94/dc/80564a3071a57c20b7c32575e4a0120e8a330ef487c319b122942d665960/pyarrow-21.0.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:c077f48aab61738c237802836fc3844f85409a46015635198761b0d6a688f87b", size = 31243234 }, + { url = "https://files.pythonhosted.org/packages/ea/cc/3b51cb2db26fe535d14f74cab4c79b191ed9a8cd4cbba45e2379b5ca2746/pyarrow-21.0.0-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:689f448066781856237eca8d1975b98cace19b8dd2ab6145bf49475478bcaa10", size = 32714370 }, + { url = "https://files.pythonhosted.org/packages/24/11/a4431f36d5ad7d83b87146f515c063e4d07ef0b7240876ddb885e6b44f2e/pyarrow-21.0.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:479ee41399fcddc46159a551705b89c05f11e8b8cb8e968f7fec64f62d91985e", size = 41135424 }, + { url = "https://files.pythonhosted.org/packages/74/dc/035d54638fc5d2971cbf1e987ccd45f1091c83bcf747281cf6cc25e72c88/pyarrow-21.0.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:40ebfcb54a4f11bcde86bc586cbd0272bac0d516cfa539c799c2453768477569", size = 42823810 }, + { url = "https://files.pythonhosted.org/packages/2e/3b/89fced102448a9e3e0d4dded1f37fa3ce4700f02cdb8665457fcc8015f5b/pyarrow-21.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8d58d8497814274d3d20214fbb24abcad2f7e351474357d552a8d53bce70c70e", size = 43391538 }, + { url = "https://files.pythonhosted.org/packages/fb/bb/ea7f1bd08978d39debd3b23611c293f64a642557e8141c80635d501e6d53/pyarrow-21.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:585e7224f21124dd57836b1530ac8f2df2afc43c861d7bf3d58a4870c42ae36c", size = 45120056 }, + { url = "https://files.pythonhosted.org/packages/6e/0b/77ea0600009842b30ceebc3337639a7380cd946061b620ac1a2f3cb541e2/pyarrow-21.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:555ca6935b2cbca2c0e932bedd853e9bc523098c39636de9ad4693b5b1df86d6", size = 26220568 }, + { url = "https://files.pythonhosted.org/packages/ca/d4/d4f817b21aacc30195cf6a46ba041dd1be827efa4a623cc8bf39a1c2a0c0/pyarrow-21.0.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:3a302f0e0963db37e0a24a70c56cf91a4faa0bca51c23812279ca2e23481fccd", size = 31160305 }, + { url = "https://files.pythonhosted.org/packages/a2/9c/dcd38ce6e4b4d9a19e1d36914cb8e2b1da4e6003dd075474c4cfcdfe0601/pyarrow-21.0.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:b6b27cf01e243871390474a211a7922bfbe3bda21e39bc9160daf0da3fe48876", size = 32684264 }, + { url = "https://files.pythonhosted.org/packages/4f/74/2a2d9f8d7a59b639523454bec12dba35ae3d0a07d8ab529dc0809f74b23c/pyarrow-21.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:e72a8ec6b868e258a2cd2672d91f2860ad532d590ce94cdf7d5e7ec674ccf03d", size = 41108099 }, + { url = "https://files.pythonhosted.org/packages/ad/90/2660332eeb31303c13b653ea566a9918484b6e4d6b9d2d46879a33ab0622/pyarrow-21.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:b7ae0bbdc8c6674259b25bef5d2a1d6af5d39d7200c819cf99e07f7dfef1c51e", size = 42829529 }, + { url = "https://files.pythonhosted.org/packages/33/27/1a93a25c92717f6aa0fca06eb4700860577d016cd3ae51aad0e0488ac899/pyarrow-21.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:58c30a1729f82d201627c173d91bd431db88ea74dcaa3885855bc6203e433b82", size = 43367883 }, + { url = "https://files.pythonhosted.org/packages/05/d9/4d09d919f35d599bc05c6950095e358c3e15148ead26292dfca1fb659b0c/pyarrow-21.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:072116f65604b822a7f22945a7a6e581cfa28e3454fdcc6939d4ff6090126623", size = 45133802 }, + { url = "https://files.pythonhosted.org/packages/71/30/f3795b6e192c3ab881325ffe172e526499eb3780e306a15103a2764916a2/pyarrow-21.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:cf56ec8b0a5c8c9d7021d6fd754e688104f9ebebf1bf4449613c9531f5346a18", size = 26203175 }, + { url = "https://files.pythonhosted.org/packages/16/ca/c7eaa8e62db8fb37ce942b1ea0c6d7abfe3786ca193957afa25e71b81b66/pyarrow-21.0.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:e99310a4ebd4479bcd1964dff9e14af33746300cb014aa4a3781738ac63baf4a", size = 31154306 }, + { url = "https://files.pythonhosted.org/packages/ce/e8/e87d9e3b2489302b3a1aea709aaca4b781c5252fcb812a17ab6275a9a484/pyarrow-21.0.0-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:d2fe8e7f3ce329a71b7ddd7498b3cfac0eeb200c2789bd840234f0dc271a8efe", size = 32680622 }, + { url = "https://files.pythonhosted.org/packages/84/52/79095d73a742aa0aba370c7942b1b655f598069489ab387fe47261a849e1/pyarrow-21.0.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:f522e5709379d72fb3da7785aa489ff0bb87448a9dc5a75f45763a795a089ebd", size = 41104094 }, + { url = "https://files.pythonhosted.org/packages/89/4b/7782438b551dbb0468892a276b8c789b8bbdb25ea5c5eb27faadd753e037/pyarrow-21.0.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:69cbbdf0631396e9925e048cfa5bce4e8c3d3b41562bbd70c685a8eb53a91e61", size = 42825576 }, + { url = "https://files.pythonhosted.org/packages/b3/62/0f29de6e0a1e33518dec92c65be0351d32d7ca351e51ec5f4f837a9aab91/pyarrow-21.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:731c7022587006b755d0bdb27626a1a3bb004bb56b11fb30d98b6c1b4718579d", size = 43368342 }, + { url = "https://files.pythonhosted.org/packages/90/c7/0fa1f3f29cf75f339768cc698c8ad4ddd2481c1742e9741459911c9ac477/pyarrow-21.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dc56bc708f2d8ac71bd1dcb927e458c93cec10b98eb4120206a4091db7b67b99", size = 45131218 }, + { url = "https://files.pythonhosted.org/packages/01/63/581f2076465e67b23bc5a37d4a2abff8362d389d29d8105832e82c9c811c/pyarrow-21.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:186aa00bca62139f75b7de8420f745f2af12941595bbbfa7ed3870ff63e25636", size = 26087551 }, + { url = "https://files.pythonhosted.org/packages/c9/ab/357d0d9648bb8241ee7348e564f2479d206ebe6e1c47ac5027c2e31ecd39/pyarrow-21.0.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:a7a102574faa3f421141a64c10216e078df467ab9576684d5cd696952546e2da", size = 31290064 }, + { url = "https://files.pythonhosted.org/packages/3f/8a/5685d62a990e4cac2043fc76b4661bf38d06efed55cf45a334b455bd2759/pyarrow-21.0.0-cp313-cp313t-macosx_12_0_x86_64.whl", hash = "sha256:1e005378c4a2c6db3ada3ad4c217b381f6c886f0a80d6a316fe586b90f77efd7", size = 32727837 }, + { url = "https://files.pythonhosted.org/packages/fc/de/c0828ee09525c2bafefd3e736a248ebe764d07d0fd762d4f0929dbc516c9/pyarrow-21.0.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:65f8e85f79031449ec8706b74504a316805217b35b6099155dd7e227eef0d4b6", size = 41014158 }, + { url = "https://files.pythonhosted.org/packages/6e/26/a2865c420c50b7a3748320b614f3484bfcde8347b2639b2b903b21ce6a72/pyarrow-21.0.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:3a81486adc665c7eb1a2bde0224cfca6ceaba344a82a971ef059678417880eb8", size = 42667885 }, + { url = "https://files.pythonhosted.org/packages/0a/f9/4ee798dc902533159250fb4321267730bc0a107d8c6889e07c3add4fe3a5/pyarrow-21.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:fc0d2f88b81dcf3ccf9a6ae17f89183762c8a94a5bdcfa09e05cfe413acf0503", size = 43276625 }, + { url = "https://files.pythonhosted.org/packages/5a/da/e02544d6997037a4b0d22d8e5f66bc9315c3671371a8b18c79ade1cefe14/pyarrow-21.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6299449adf89df38537837487a4f8d3bd91ec94354fdd2a7d30bc11c48ef6e79", size = 44951890 }, + { url = "https://files.pythonhosted.org/packages/e5/4e/519c1bc1876625fe6b71e9a28287c43ec2f20f73c658b9ae1d485c0c206e/pyarrow-21.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:222c39e2c70113543982c6b34f3077962b44fca38c0bd9e68bb6781534425c10", size = 26371006 }, +] + +[[package]] +name = "pyasn1" +version = "0.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/e9/01f1a64245b89f039897cb0130016d79f77d52669aae6ee7b159a6c4c018/pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034", size = 145322 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/f1/d6a797abb14f6283c0ddff96bbdd46937f64122b8c925cab503dd37f8214/pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629", size = 83135 }, +] + +[[package]] +name = "pyasn1-modules" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyasn1" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/e6/78ebbb10a8c8e4b61a59249394a4a594c1a7af95593dc933a349c8d00964/pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6", size = 307892 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259 }, +] + +[[package]] +name = "pycocotools" +version = "2.0.10" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/35/a6/694fd661f0feb5e91f7049a202ea12de312ca9010c33bd9d9f0c63046c01/pycocotools-2.0.10.tar.gz", hash = "sha256:7a47609cdefc95e5e151313c7d93a61cf06e15d42c7ba99b601e3bc0f9ece2e1", size = 25389 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/f8/24082061458ad62df7e2714a631cc047eddfe752970a2e4a7e7977d96905/pycocotools-2.0.10-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:94d558e6a4b92620dad1684b74b6c1404e20d5ed3b4f3aed64ad817d5dd46c72", size = 152202 }, + { url = "https://files.pythonhosted.org/packages/fe/45/65819da7579e9018506ed3b5401146a394e89eee84f57592174962f0fba2/pycocotools-2.0.10-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4d61959f505f1333afd1666ece1a9f8dad318de160c56c7d03f22d7b5556478", size = 445796 }, + { url = "https://files.pythonhosted.org/packages/61/d7/32996d713921c504875a4cebf241c182aa37e58daab5c3c4737f539ac0d4/pycocotools-2.0.10-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0bb54826c5d3b651597ec15ae5f4226b727159ec7798af81aa3895f734518993", size = 455015 }, + { url = "https://files.pythonhosted.org/packages/fe/5f/91ad9e46ec6709d24a9ed8ac3969f6a550715c08b22f85bc045d1395fdf6/pycocotools-2.0.10-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9d3b4d0aa38c76153ec244f17939bbc65d24b6a119eb99184f7f636421ef0d8a", size = 464739 }, + { url = "https://files.pythonhosted.org/packages/40/e3/9684edbd996a35d8da7c38c1dfc151d6e1bcf66bd32de6fb88f6d2f2bcf5/pycocotools-2.0.10-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:714dda1fccc3a9a1f10893530df6e927678daf6c49bc8a932d7ec2042e9a11f2", size = 481572 }, + { url = "https://files.pythonhosted.org/packages/4e/84/1832144e8effe700660489d6e2a7687c99d14c3ea29fa0142dac0e7322d6/pycocotools-2.0.10-cp310-cp310-win_amd64.whl", hash = "sha256:8b4f26d44dde3e0b1e3df3ddcc7e27560e52dfe53db708c26af22a57e8ea3d47", size = 80166 }, + { url = "https://files.pythonhosted.org/packages/03/bf/ea288c16d2d2e4da740545f30f7ebf58f2343bcf5e0a7f3e3aef582a116c/pycocotools-2.0.10-cp310-cp310-win_arm64.whl", hash = "sha256:16836530552d6ce5e7f1cbcdfe6ead94c0cee71d61bfa3e3c832aef57d21c027", size = 69633 }, + { url = "https://files.pythonhosted.org/packages/ee/36/aebbbddd9c659f1fc9d78daeaf6e39860813bb014b0de873073361ad40f1/pycocotools-2.0.10-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:68846da0ee3ea82d71bcbd99ed28271633a67a899cfbacd2ef309b2e455524b2", size = 155033 }, + { url = "https://files.pythonhosted.org/packages/57/c2/e4c96950604c709fbd71c49828968fadd9d8ca8cf74f52be4cd4b2ff9300/pycocotools-2.0.10-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20831839a771d4bc60a814e7b54a92d9a45a773dee47959d30888d00066059c3", size = 470328 }, + { url = "https://files.pythonhosted.org/packages/a7/ec/7827cd9ce6e80f739fab0163ecb3765df54af744a9bab64b0058bdce47ef/pycocotools-2.0.10-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1760c10459dfb4229e7436ae380228428efb0115bbe332a51b72d07fa085d8c0", size = 477331 }, + { url = "https://files.pythonhosted.org/packages/81/74/33ce685ae1cd6312b2526f701e43dfeb73d1c860878b72a30ac1cc322536/pycocotools-2.0.10-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5146bc881f380e8fb493e49216083298e4a06f778841f8b9b1d45b21e211d0e4", size = 489735 }, + { url = "https://files.pythonhosted.org/packages/17/79/0e02ce700ff9c9fd30e57a84add42bd6fc033e743b76870ef68215d3f3f4/pycocotools-2.0.10-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:23f7d0c551d4c31cab629ce177186db9562f10414320add5267707a84cf6cdfa", size = 507779 }, + { url = "https://files.pythonhosted.org/packages/d5/12/00fac39ad26f762c50e5428cc8b3c83de28c5d64b5b858181583522a4e28/pycocotools-2.0.10-cp311-cp311-win_amd64.whl", hash = "sha256:03c3aacec2a6aa5171016303a539d07a7b22a34557456eadf0eb40853bdd813e", size = 80808 }, + { url = "https://files.pythonhosted.org/packages/3d/cd/50970a64365f013151086d54d60b40369cf612f117d72cd9d6bd2966932c/pycocotools-2.0.10-cp311-cp311-win_arm64.whl", hash = "sha256:1f942352b1ab11b9732443ab832cbe5836441f4ec30e1f61b44e1421dbb0a0f5", size = 69566 }, + { url = "https://files.pythonhosted.org/packages/d7/b4/3b87dce90fc81b8283b2b0e32b22642939e25f3a949581cb6777f5eebb12/pycocotools-2.0.10-cp312-abi3-macosx_10_13_universal2.whl", hash = "sha256:e1359f556986c8c4ac996bf8e473ff891d87630491357aaabd12601687af5edb", size = 142896 }, + { url = "https://files.pythonhosted.org/packages/29/d5/b17bb67722432a191cb86121cda33cd8edb4d5b15beda43bc97a7d5ae404/pycocotools-2.0.10-cp312-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:075788c90bfa6a8989d628932854f3e32c25dac3c1bf7c1183cefad29aee16c8", size = 390111 }, + { url = "https://files.pythonhosted.org/packages/49/80/912b4c60f94e747dd2c3adbda5d4a4edc1d735fbfa0d91ab2eb231decb5d/pycocotools-2.0.10-cp312-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4539d8b29230de042f574012edd0b5227528da083c4f12bbd6488567aabd3920", size = 397099 }, + { url = "https://files.pythonhosted.org/packages/df/d7/b3c2f731252a096bbae1a47cb1bbeab4560620a82585d40cce67eca5f043/pycocotools-2.0.10-cp312-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:da7b339624d0f78aa5bdc1c86a53f2dcb36ae7e10ab5fe45ba69878bb7837c7a", size = 396111 }, + { url = "https://files.pythonhosted.org/packages/2c/6f/2eceba57245bfc86174263e12716cbe91b329a3677fbeff246148ce6a664/pycocotools-2.0.10-cp312-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ffdbf8810f27b32c5c5c85d9cd65e8e066852fef9775e58a7b23abdffeaf8252", size = 416393 }, + { url = "https://files.pythonhosted.org/packages/e1/31/d87f781759b2ad177dd6d41c5fe0ce154f14fc8b384e9b80cd21a157395b/pycocotools-2.0.10-cp312-abi3-win_amd64.whl", hash = "sha256:998a88f90bb663548e767470181175343d406b6673b8b9ef5bdbb3a6d3eb3b11", size = 76824 }, + { url = "https://files.pythonhosted.org/packages/27/13/7674d61658b58b8310e3de1270bce18f92a6ee8136e54a7e5696d6f72fd4/pycocotools-2.0.10-cp312-abi3-win_arm64.whl", hash = "sha256:76cd86a80171f8f7da3250be0e40d75084f1f1505d376ae0d08ed0be1ba8a90d", size = 64753 }, + { url = "https://files.pythonhosted.org/packages/b4/a0/5ee60d0ad7fc54b58aab57445f29649566d2f603edbde81dbd30b4be27a5/pycocotools-2.0.10-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:df7796ec8b9e32879028f929b77968039ca7ced7ecdad23147da55f144e753c8", size = 163169 }, + { url = "https://files.pythonhosted.org/packages/8b/39/98f0f682abafe881ce7cdcb7e65318784bcf2898ac98fd32c293e6f960bb/pycocotools-2.0.10-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d76ab632494f5dd8578230e5123e595446389598e0832a86f3dc8d7f236c3e5", size = 476768 }, + { url = "https://files.pythonhosted.org/packages/e9/f3/1073ba0e77d034124f5aa9873255d3ed43b5b59e07520fbacdae9b8b27d4/pycocotools-2.0.10-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b165aaa9d435571ce34cdb5fae9d47cfe923db2c687362c2607c1e5f1a7ffa8", size = 469313 }, + { url = "https://files.pythonhosted.org/packages/96/ac/ae1143587a9ccc49767afbcc0bf1d6e21d1d1989682bf9604a6c514d4115/pycocotools-2.0.10-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5faf8bb60228c44fb171eb0674ae31d72a82bcc0d099c0fececfe7cae49010f3", size = 478806 }, + { url = "https://files.pythonhosted.org/packages/8a/ea/d872975a47605458fc2dc9096d06c317c9945694a871459935e8c0ae14e5/pycocotools-2.0.10-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:63c8aa107c96f19634ec9795c9c34d563c7da45009a342ca7ad36070d82792e1", size = 487347 }, + { url = "https://files.pythonhosted.org/packages/42/4d/89a6d94afc95bb155e9c3144ca66d6cb63c0d80c75103dba72128624492b/pycocotools-2.0.10-cp313-cp313t-win_amd64.whl", hash = "sha256:d1fcf39acdee901de7665b1853e4f79f7a8c2f88eb100a9c24229a255c9efc59", size = 88805 }, + { url = "https://files.pythonhosted.org/packages/c4/b8/4da7f02655dd39ce9f7251a0d95c51e5924db9a80155b4cd654fed13345c/pycocotools-2.0.10-cp313-cp313t-win_arm64.whl", hash = "sha256:3e323b0ed7c15df34929b2d99ff720be8d6a35c58c7566e29559d9bebd2d09f6", size = 69741 }, +] + +[[package]] +name = "pycparser" +version = "2.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552 }, +] + +[[package]] +name = "pydantic" +version = "2.11.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/00/dd/4325abf92c39ba8623b5af936ddb36ffcfe0beae70405d456ab1fb2f5b8c/pydantic-2.11.7.tar.gz", hash = "sha256:d989c3c6cb79469287b1569f7447a17848c998458d49ebe294e975b9baf0f0db", size = 788350 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/c0/ec2b1c8712ca690e5d61979dee872603e92b8a32f94cc1b72d53beab008a/pydantic-2.11.7-py3-none-any.whl", hash = "sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b", size = 444782 }, +] + +[[package]] +name = "pydantic-core" +version = "2.33.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/92/b31726561b5dae176c2d2c2dc43a9c5bfba5d32f96f8b4c0a600dd492447/pydantic_core-2.33.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2b3d326aaef0c0399d9afffeb6367d5e26ddc24d351dbc9c636840ac355dc5d8", size = 2028817 }, + { url = "https://files.pythonhosted.org/packages/a3/44/3f0b95fafdaca04a483c4e685fe437c6891001bf3ce8b2fded82b9ea3aa1/pydantic_core-2.33.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e5b2671f05ba48b94cb90ce55d8bdcaaedb8ba00cc5359f6810fc918713983d", size = 1861357 }, + { url = "https://files.pythonhosted.org/packages/30/97/e8f13b55766234caae05372826e8e4b3b96e7b248be3157f53237682e43c/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0069c9acc3f3981b9ff4cdfaf088e98d83440a4c7ea1bc07460af3d4dc22e72d", size = 1898011 }, + { url = "https://files.pythonhosted.org/packages/9b/a3/99c48cf7bafc991cc3ee66fd544c0aae8dc907b752f1dad2d79b1b5a471f/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d53b22f2032c42eaaf025f7c40c2e3b94568ae077a606f006d206a463bc69572", size = 1982730 }, + { url = "https://files.pythonhosted.org/packages/de/8e/a5b882ec4307010a840fb8b58bd9bf65d1840c92eae7534c7441709bf54b/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0405262705a123b7ce9f0b92f123334d67b70fd1f20a9372b907ce1080c7ba02", size = 2136178 }, + { url = "https://files.pythonhosted.org/packages/e4/bb/71e35fc3ed05af6834e890edb75968e2802fe98778971ab5cba20a162315/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4b25d91e288e2c4e0662b8038a28c6a07eaac3e196cfc4ff69de4ea3db992a1b", size = 2736462 }, + { url = "https://files.pythonhosted.org/packages/31/0d/c8f7593e6bc7066289bbc366f2235701dcbebcd1ff0ef8e64f6f239fb47d/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bdfe4b3789761f3bcb4b1ddf33355a71079858958e3a552f16d5af19768fef2", size = 2005652 }, + { url = "https://files.pythonhosted.org/packages/d2/7a/996d8bd75f3eda405e3dd219ff5ff0a283cd8e34add39d8ef9157e722867/pydantic_core-2.33.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:efec8db3266b76ef9607c2c4c419bdb06bf335ae433b80816089ea7585816f6a", size = 2113306 }, + { url = "https://files.pythonhosted.org/packages/ff/84/daf2a6fb2db40ffda6578a7e8c5a6e9c8affb251a05c233ae37098118788/pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:031c57d67ca86902726e0fae2214ce6770bbe2f710dc33063187a68744a5ecac", size = 2073720 }, + { url = "https://files.pythonhosted.org/packages/77/fb/2258da019f4825128445ae79456a5499c032b55849dbd5bed78c95ccf163/pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:f8de619080e944347f5f20de29a975c2d815d9ddd8be9b9b7268e2e3ef68605a", size = 2244915 }, + { url = "https://files.pythonhosted.org/packages/d8/7a/925ff73756031289468326e355b6fa8316960d0d65f8b5d6b3a3e7866de7/pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:73662edf539e72a9440129f231ed3757faab89630d291b784ca99237fb94db2b", size = 2241884 }, + { url = "https://files.pythonhosted.org/packages/0b/b0/249ee6d2646f1cdadcb813805fe76265745c4010cf20a8eba7b0e639d9b2/pydantic_core-2.33.2-cp310-cp310-win32.whl", hash = "sha256:0a39979dcbb70998b0e505fb1556a1d550a0781463ce84ebf915ba293ccb7e22", size = 1910496 }, + { url = "https://files.pythonhosted.org/packages/66/ff/172ba8f12a42d4b552917aa65d1f2328990d3ccfc01d5b7c943ec084299f/pydantic_core-2.33.2-cp310-cp310-win_amd64.whl", hash = "sha256:b0379a2b24882fef529ec3b4987cb5d003b9cda32256024e6fe1586ac45fc640", size = 1955019 }, + { url = "https://files.pythonhosted.org/packages/3f/8d/71db63483d518cbbf290261a1fc2839d17ff89fce7089e08cad07ccfce67/pydantic_core-2.33.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4c5b0a576fb381edd6d27f0a85915c6daf2f8138dc5c267a57c08a62900758c7", size = 2028584 }, + { url = "https://files.pythonhosted.org/packages/24/2f/3cfa7244ae292dd850989f328722d2aef313f74ffc471184dc509e1e4e5a/pydantic_core-2.33.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e799c050df38a639db758c617ec771fd8fb7a5f8eaaa4b27b101f266b216a246", size = 1855071 }, + { url = "https://files.pythonhosted.org/packages/b3/d3/4ae42d33f5e3f50dd467761304be2fa0a9417fbf09735bc2cce003480f2a/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc46a01bf8d62f227d5ecee74178ffc448ff4e5197c756331f71efcc66dc980f", size = 1897823 }, + { url = "https://files.pythonhosted.org/packages/f4/f3/aa5976e8352b7695ff808599794b1fba2a9ae2ee954a3426855935799488/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a144d4f717285c6d9234a66778059f33a89096dfb9b39117663fd8413d582dcc", size = 1983792 }, + { url = "https://files.pythonhosted.org/packages/d5/7a/cda9b5a23c552037717f2b2a5257e9b2bfe45e687386df9591eff7b46d28/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73cf6373c21bc80b2e0dc88444f41ae60b2f070ed02095754eb5a01df12256de", size = 2136338 }, + { url = "https://files.pythonhosted.org/packages/2b/9f/b8f9ec8dd1417eb9da784e91e1667d58a2a4a7b7b34cf4af765ef663a7e5/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3dc625f4aa79713512d1976fe9f0bc99f706a9dee21dfd1810b4bbbf228d0e8a", size = 2730998 }, + { url = "https://files.pythonhosted.org/packages/47/bc/cd720e078576bdb8255d5032c5d63ee5c0bf4b7173dd955185a1d658c456/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b21b5549499972441da4758d662aeea93f1923f953e9cbaff14b8b9565aef", size = 2003200 }, + { url = "https://files.pythonhosted.org/packages/ca/22/3602b895ee2cd29d11a2b349372446ae9727c32e78a94b3d588a40fdf187/pydantic_core-2.33.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bdc25f3681f7b78572699569514036afe3c243bc3059d3942624e936ec93450e", size = 2113890 }, + { url = "https://files.pythonhosted.org/packages/ff/e6/e3c5908c03cf00d629eb38393a98fccc38ee0ce8ecce32f69fc7d7b558a7/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fe5b32187cbc0c862ee201ad66c30cf218e5ed468ec8dc1cf49dec66e160cc4d", size = 2073359 }, + { url = "https://files.pythonhosted.org/packages/12/e7/6a36a07c59ebefc8777d1ffdaf5ae71b06b21952582e4b07eba88a421c79/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:bc7aee6f634a6f4a95676fcb5d6559a2c2a390330098dba5e5a5f28a2e4ada30", size = 2245883 }, + { url = "https://files.pythonhosted.org/packages/16/3f/59b3187aaa6cc0c1e6616e8045b284de2b6a87b027cce2ffcea073adf1d2/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:235f45e5dbcccf6bd99f9f472858849f73d11120d76ea8707115415f8e5ebebf", size = 2241074 }, + { url = "https://files.pythonhosted.org/packages/e0/ed/55532bb88f674d5d8f67ab121a2a13c385df382de2a1677f30ad385f7438/pydantic_core-2.33.2-cp311-cp311-win32.whl", hash = "sha256:6368900c2d3ef09b69cb0b913f9f8263b03786e5b2a387706c5afb66800efd51", size = 1910538 }, + { url = "https://files.pythonhosted.org/packages/fe/1b/25b7cccd4519c0b23c2dd636ad39d381abf113085ce4f7bec2b0dc755eb1/pydantic_core-2.33.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e063337ef9e9820c77acc768546325ebe04ee38b08703244c1309cccc4f1bab", size = 1952909 }, + { url = "https://files.pythonhosted.org/packages/49/a9/d809358e49126438055884c4366a1f6227f0f84f635a9014e2deb9b9de54/pydantic_core-2.33.2-cp311-cp311-win_arm64.whl", hash = "sha256:6b99022f1d19bc32a4c2a0d544fc9a76e3be90f0b3f4af413f87d38749300e65", size = 1897786 }, + { url = "https://files.pythonhosted.org/packages/18/8a/2b41c97f554ec8c71f2a8a5f85cb56a8b0956addfe8b0efb5b3d77e8bdc3/pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc", size = 2009000 }, + { url = "https://files.pythonhosted.org/packages/a1/02/6224312aacb3c8ecbaa959897af57181fb6cf3a3d7917fd44d0f2917e6f2/pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7", size = 1847996 }, + { url = "https://files.pythonhosted.org/packages/d6/46/6dcdf084a523dbe0a0be59d054734b86a981726f221f4562aed313dbcb49/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025", size = 1880957 }, + { url = "https://files.pythonhosted.org/packages/ec/6b/1ec2c03837ac00886ba8160ce041ce4e325b41d06a034adbef11339ae422/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011", size = 1964199 }, + { url = "https://files.pythonhosted.org/packages/2d/1d/6bf34d6adb9debd9136bd197ca72642203ce9aaaa85cfcbfcf20f9696e83/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f", size = 2120296 }, + { url = "https://files.pythonhosted.org/packages/e0/94/2bd0aaf5a591e974b32a9f7123f16637776c304471a0ab33cf263cf5591a/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88", size = 2676109 }, + { url = "https://files.pythonhosted.org/packages/f9/41/4b043778cf9c4285d59742281a769eac371b9e47e35f98ad321349cc5d61/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1", size = 2002028 }, + { url = "https://files.pythonhosted.org/packages/cb/d5/7bb781bf2748ce3d03af04d5c969fa1308880e1dca35a9bd94e1a96a922e/pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b", size = 2100044 }, + { url = "https://files.pythonhosted.org/packages/fe/36/def5e53e1eb0ad896785702a5bbfd25eed546cdcf4087ad285021a90ed53/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1", size = 2058881 }, + { url = "https://files.pythonhosted.org/packages/01/6c/57f8d70b2ee57fc3dc8b9610315949837fa8c11d86927b9bb044f8705419/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6", size = 2227034 }, + { url = "https://files.pythonhosted.org/packages/27/b9/9c17f0396a82b3d5cbea4c24d742083422639e7bb1d5bf600e12cb176a13/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea", size = 2234187 }, + { url = "https://files.pythonhosted.org/packages/b0/6a/adf5734ffd52bf86d865093ad70b2ce543415e0e356f6cacabbc0d9ad910/pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290", size = 1892628 }, + { url = "https://files.pythonhosted.org/packages/43/e4/5479fecb3606c1368d496a825d8411e126133c41224c1e7238be58b87d7e/pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2", size = 1955866 }, + { url = "https://files.pythonhosted.org/packages/0d/24/8b11e8b3e2be9dd82df4b11408a67c61bb4dc4f8e11b5b0fc888b38118b5/pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab", size = 1888894 }, + { url = "https://files.pythonhosted.org/packages/46/8c/99040727b41f56616573a28771b1bfa08a3d3fe74d3d513f01251f79f172/pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f", size = 2015688 }, + { url = "https://files.pythonhosted.org/packages/3a/cc/5999d1eb705a6cefc31f0b4a90e9f7fc400539b1a1030529700cc1b51838/pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6", size = 1844808 }, + { url = "https://files.pythonhosted.org/packages/6f/5e/a0a7b8885c98889a18b6e376f344da1ef323d270b44edf8174d6bce4d622/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef", size = 1885580 }, + { url = "https://files.pythonhosted.org/packages/3b/2a/953581f343c7d11a304581156618c3f592435523dd9d79865903272c256a/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a", size = 1973859 }, + { url = "https://files.pythonhosted.org/packages/e6/55/f1a813904771c03a3f97f676c62cca0c0a4138654107c1b61f19c644868b/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916", size = 2120810 }, + { url = "https://files.pythonhosted.org/packages/aa/c3/053389835a996e18853ba107a63caae0b9deb4a276c6b472931ea9ae6e48/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a", size = 2676498 }, + { url = "https://files.pythonhosted.org/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d", size = 2000611 }, + { url = "https://files.pythonhosted.org/packages/59/a7/63ef2fed1837d1121a894d0ce88439fe3e3b3e48c7543b2a4479eb99c2bd/pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56", size = 2107924 }, + { url = "https://files.pythonhosted.org/packages/04/8f/2551964ef045669801675f1cfc3b0d74147f4901c3ffa42be2ddb1f0efc4/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5", size = 2063196 }, + { url = "https://files.pythonhosted.org/packages/26/bd/d9602777e77fc6dbb0c7db9ad356e9a985825547dce5ad1d30ee04903918/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e", size = 2236389 }, + { url = "https://files.pythonhosted.org/packages/42/db/0e950daa7e2230423ab342ae918a794964b053bec24ba8af013fc7c94846/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162", size = 2239223 }, + { url = "https://files.pythonhosted.org/packages/58/4d/4f937099c545a8a17eb52cb67fe0447fd9a373b348ccfa9a87f141eeb00f/pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849", size = 1900473 }, + { url = "https://files.pythonhosted.org/packages/a0/75/4a0a9bac998d78d889def5e4ef2b065acba8cae8c93696906c3a91f310ca/pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9", size = 1955269 }, + { url = "https://files.pythonhosted.org/packages/f9/86/1beda0576969592f1497b4ce8e7bc8cbdf614c352426271b1b10d5f0aa64/pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9", size = 1893921 }, + { url = "https://files.pythonhosted.org/packages/a4/7d/e09391c2eebeab681df2b74bfe6c43422fffede8dc74187b2b0bf6fd7571/pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac", size = 1806162 }, + { url = "https://files.pythonhosted.org/packages/f1/3d/847b6b1fed9f8ed3bb95a9ad04fbd0b212e832d4f0f50ff4d9ee5a9f15cf/pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5", size = 1981560 }, + { url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777 }, + { url = "https://files.pythonhosted.org/packages/30/68/373d55e58b7e83ce371691f6eaa7175e3a24b956c44628eb25d7da007917/pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5c4aa4e82353f65e548c476b37e64189783aa5384903bfea4f41580f255fddfa", size = 2023982 }, + { url = "https://files.pythonhosted.org/packages/a4/16/145f54ac08c96a63d8ed6442f9dec17b2773d19920b627b18d4f10a061ea/pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d946c8bf0d5c24bf4fe333af284c59a19358aa3ec18cb3dc4370080da1e8ad29", size = 1858412 }, + { url = "https://files.pythonhosted.org/packages/41/b1/c6dc6c3e2de4516c0bb2c46f6a373b91b5660312342a0cf5826e38ad82fa/pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87b31b6846e361ef83fedb187bb5b4372d0da3f7e28d85415efa92d6125d6e6d", size = 1892749 }, + { url = "https://files.pythonhosted.org/packages/12/73/8cd57e20afba760b21b742106f9dbdfa6697f1570b189c7457a1af4cd8a0/pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa9d91b338f2df0508606f7009fde642391425189bba6d8c653afd80fd6bb64e", size = 2067527 }, + { url = "https://files.pythonhosted.org/packages/e3/d5/0bb5d988cc019b3cba4a78f2d4b3854427fc47ee8ec8e9eaabf787da239c/pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2058a32994f1fde4ca0480ab9d1e75a0e8c87c22b53a3ae66554f9af78f2fe8c", size = 2108225 }, + { url = "https://files.pythonhosted.org/packages/f1/c5/00c02d1571913d496aabf146106ad8239dc132485ee22efe08085084ff7c/pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:0e03262ab796d986f978f79c943fc5f620381be7287148b8010b4097f79a39ec", size = 2069490 }, + { url = "https://files.pythonhosted.org/packages/22/a8/dccc38768274d3ed3a59b5d06f59ccb845778687652daa71df0cab4040d7/pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:1a8695a8d00c73e50bff9dfda4d540b7dee29ff9b8053e38380426a85ef10052", size = 2237525 }, + { url = "https://files.pythonhosted.org/packages/d4/e7/4f98c0b125dda7cf7ccd14ba936218397b44f50a56dd8c16a3091df116c3/pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:fa754d1850735a0b0e03bcffd9d4b4343eb417e47196e4485d9cca326073a42c", size = 2238446 }, + { url = "https://files.pythonhosted.org/packages/ce/91/2ec36480fdb0b783cd9ef6795753c1dea13882f2e68e73bce76ae8c21e6a/pydantic_core-2.33.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a11c8d26a50bfab49002947d3d237abe4d9e4b5bdc8846a63537b6488e197808", size = 2066678 }, + { url = "https://files.pythonhosted.org/packages/7b/27/d4ae6487d73948d6f20dddcd94be4ea43e74349b56eba82e9bdee2d7494c/pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:dd14041875d09cc0f9308e37a6f8b65f5585cf2598a53aa0123df8b129d481f8", size = 2025200 }, + { url = "https://files.pythonhosted.org/packages/f1/b8/b3cb95375f05d33801024079b9392a5ab45267a63400bf1866e7ce0f0de4/pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d87c561733f66531dced0da6e864f44ebf89a8fba55f31407b00c2f7f9449593", size = 1859123 }, + { url = "https://files.pythonhosted.org/packages/05/bc/0d0b5adeda59a261cd30a1235a445bf55c7e46ae44aea28f7bd6ed46e091/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f82865531efd18d6e07a04a17331af02cb7a651583c418df8266f17a63c6612", size = 1892852 }, + { url = "https://files.pythonhosted.org/packages/3e/11/d37bdebbda2e449cb3f519f6ce950927b56d62f0b84fd9cb9e372a26a3d5/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bfb5112df54209d820d7bf9317c7a6c9025ea52e49f46b6a2060104bba37de7", size = 2067484 }, + { url = "https://files.pythonhosted.org/packages/8c/55/1f95f0a05ce72ecb02a8a8a1c3be0579bbc29b1d5ab68f1378b7bebc5057/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:64632ff9d614e5eecfb495796ad51b0ed98c453e447a76bcbeeb69615079fc7e", size = 2108896 }, + { url = "https://files.pythonhosted.org/packages/53/89/2b2de6c81fa131f423246a9109d7b2a375e83968ad0800d6e57d0574629b/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:f889f7a40498cc077332c7ab6b4608d296d852182211787d4f3ee377aaae66e8", size = 2069475 }, + { url = "https://files.pythonhosted.org/packages/b8/e9/1f7efbe20d0b2b10f6718944b5d8ece9152390904f29a78e68d4e7961159/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:de4b83bb311557e439b9e186f733f6c645b9417c84e2eb8203f3f820a4b988bf", size = 2239013 }, + { url = "https://files.pythonhosted.org/packages/3c/b2/5309c905a93811524a49b4e031e9851a6b00ff0fb668794472ea7746b448/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:82f68293f055f51b51ea42fafc74b6aad03e70e191799430b90c13d643059ebb", size = 2238715 }, + { url = "https://files.pythonhosted.org/packages/32/56/8a7ca5d2cd2cda1d245d34b1c9a942920a718082ae8e54e5f3e5a58b7add/pydantic_core-2.33.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:329467cecfb529c925cf2bbd4d60d2c509bc2fb52a20c1045bf09bb70971a9c1", size = 2066757 }, +] + +[[package]] +name = "pydantic-settings" +version = "2.10.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/68/85/1ea668bbab3c50071ca613c6ab30047fb36ab0da1b92fa8f17bbc38fd36c/pydantic_settings-2.10.1.tar.gz", hash = "sha256:06f0062169818d0f5524420a360d632d5857b83cffd4d42fe29597807a1614ee", size = 172583 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/f0/427018098906416f580e3cf1366d3b1abfb408a0652e9f31600c24a1903c/pydantic_settings-2.10.1-py3-none-any.whl", hash = "sha256:a60952460b99cf661dc25c29c0ef171721f98bfcb52ef8d9ea4c943d7c8cc796", size = 45235 }, +] + +[[package]] +name = "pyee" +version = "13.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/95/03/1fd98d5841cd7964a27d729ccf2199602fe05eb7a405c1462eb7277945ed/pyee-13.0.0.tar.gz", hash = "sha256:b391e3c5a434d1f5118a25615001dbc8f669cf410ab67d04c4d4e07c55481c37", size = 31250 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/4d/b9add7c84060d4c1906abe9a7e5359f2a60f7a9a4f67268b2766673427d8/pyee-13.0.0-py3-none-any.whl", hash = "sha256:48195a3cddb3b1515ce0695ed76036b5ccc2ef3a9f963ff9f77aec0139845498", size = 15730 }, +] + +[[package]] +name = "pygithub" +version = "1.59.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "deprecated" }, + { name = "pyjwt", extra = ["crypto"] }, + { name = "pynacl" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fb/30/203d3420960853e399de3b85d6613cea1cf17c1cf7fc9716f7ee7e17e0fc/PyGithub-1.59.1.tar.gz", hash = "sha256:c44e3a121c15bf9d3a5cc98d94c9a047a5132a9b01d22264627f58ade9ddc217", size = 3295328 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/71/aff5465d9e3d448a5d4beab1dc7c8dec72037e3ae7e0d856ee08538dc934/PyGithub-1.59.1-py3-none-any.whl", hash = "sha256:3d87a822e6c868142f0c2c4bf16cce4696b5a7a4d142a7bd160e1bdf75bc54a9", size = 342171 }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217 }, +] + +[[package]] +name = "pyjwt" +version = "2.10.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/46/bd74733ff231675599650d3e47f361794b22ef3e3770998dda30d3b63726/pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953", size = 87785 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb", size = 22997 }, +] + +[package.optional-dependencies] +crypto = [ + { name = "cryptography" }, +] + +[[package]] +name = "pymongo" +version = "4.13.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dnspython" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4b/5a/d664298bf54762f0c89b8aa2c276868070e06afb853b4a8837de5741e5f9/pymongo-4.13.2.tar.gz", hash = "sha256:0f64c6469c2362962e6ce97258ae1391abba1566a953a492562d2924b44815c2", size = 2167844 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/a8/293dfd3accda06ae94c54e7c15ac5108614d31263708236b4743554ad6ee/pymongo-4.13.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:01065eb1838e3621a30045ab14d1a60ee62e01f65b7cf154e69c5c722ef14d2f", size = 802768 }, + { url = "https://files.pythonhosted.org/packages/ce/7f/2cbc897dd2867b9b5f8e9e6587dc4bf23e3777a4ddd712064ed21aea99e0/pymongo-4.13.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9ab0325d436075f5f1901cde95afae811141d162bc42d9a5befb647fda585ae6", size = 803053 }, + { url = "https://files.pythonhosted.org/packages/b6/da/07cdbaf507cccfdac837f612ea276523d2cdd380c5253c86ceae0369f0e2/pymongo-4.13.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdd8041902963c84dc4e27034fa045ac55fabcb2a4ba5b68b880678557573e70", size = 1180427 }, + { url = "https://files.pythonhosted.org/packages/2b/5c/5f61269c87e565a6f4016e644e2bd20473b4b5a47c362ad3d57a1428ef33/pymongo-4.13.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b00ab04630aa4af97294e9abdbe0506242396269619c26f5761fd7b2524ef501", size = 1214655 }, + { url = "https://files.pythonhosted.org/packages/26/51/757ee06299e2bb61c0ae7b886ca845a78310cf94fc95bbc044bbe7892392/pymongo-4.13.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:16440d0da30ba804c6c01ea730405fdbbb476eae760588ea09e6e7d28afc06de", size = 1197586 }, + { url = "https://files.pythonhosted.org/packages/5a/a8/9ddf0ad0884046c34c5eb3de9a944c47d37e39989ae782ded2b207462a97/pymongo-4.13.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad9a2d1357aed5d6750deb315f62cb6f5b3c4c03ffb650da559cb09cb29e6fe8", size = 1183599 }, + { url = "https://files.pythonhosted.org/packages/7b/57/61b289b440e77524e4b0d6881f6c6f50cf9a55a72b5ba2adaa43d70531e6/pymongo-4.13.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c793223aef21a8c415c840af1ca36c55a05d6fa3297378da35de3fb6661c0174", size = 1162761 }, + { url = "https://files.pythonhosted.org/packages/05/22/bd328cedc79768ab03942fd828f0cd1d50a3ae2c3caf3aebad65a644eb75/pymongo-4.13.2-cp310-cp310-win32.whl", hash = "sha256:8ef6ae029a3390565a0510c872624514dde350007275ecd8126b09175aa02cca", size = 790062 }, + { url = "https://files.pythonhosted.org/packages/9f/70/2d8bbdac28e869cebb8081a43f8b16c6dd2384f6aef28fcc6ec0693a7042/pymongo-4.13.2-cp310-cp310-win_amd64.whl", hash = "sha256:66f168f8c5b1e2e3d518507cf9f200f0c86ac79e2b2be9e7b6c8fd1e2f7d7824", size = 800198 }, + { url = "https://files.pythonhosted.org/packages/94/df/4c4ef17b48c70120f834ba7151860c300924915696c4a57170cb5b09787f/pymongo-4.13.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7af8c56d0a7fcaf966d5292e951f308fb1f8bac080257349e14742725fd7990d", size = 857145 }, + { url = "https://files.pythonhosted.org/packages/e7/41/480ca82b3b3320fc70fe699a01df28db15a4ea154c8759ab4a437a74c808/pymongo-4.13.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ad24f5864706f052b05069a6bc59ff875026e28709548131448fe1e40fc5d80f", size = 857437 }, + { url = "https://files.pythonhosted.org/packages/50/d4/eb74e98ea980a5e1ec4f06f383ec6c52ab02076802de24268f477ef616d2/pymongo-4.13.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a10069454195d1d2dda98d681b1dbac9a425f4b0fe744aed5230c734021c1cb9", size = 1426516 }, + { url = "https://files.pythonhosted.org/packages/aa/fe/c5960c0e6438bd489367261e5ef1a5db01e34349f0dbf7529fb938d3d2ef/pymongo-4.13.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e20862b81e3863bcd72334e3577a3107604553b614a8d25ee1bb2caaea4eb90", size = 1477477 }, + { url = "https://files.pythonhosted.org/packages/f6/9f/ef4395175fc97876978736c8493d8ffa4d13aa7a4e12269a2cb0d52a1246/pymongo-4.13.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6b4d5794ca408317c985d7acfb346a60f96f85a7c221d512ff0ecb3cce9d6110", size = 1451921 }, + { url = "https://files.pythonhosted.org/packages/2a/b9/397cb2a3ec03f880e882102eddcb46c3d516c6cf47a05f44db48067924d9/pymongo-4.13.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c8e0420fb4901006ae7893e76108c2a36a343b4f8922466d51c45e9e2ceb717", size = 1431045 }, + { url = "https://files.pythonhosted.org/packages/f5/0d/e150a414e5cb07f2fefca817fa071a6da8d96308469a85a777244c8c4337/pymongo-4.13.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:239b5f83b83008471d54095e145d4c010f534af99e87cc8877fc6827736451a0", size = 1399697 }, + { url = "https://files.pythonhosted.org/packages/b8/29/5190eafb994721c30a38a8a62df225c47a9da364ab5c8cffe90aabf6a54e/pymongo-4.13.2-cp311-cp311-win32.whl", hash = "sha256:6bceb524110c32319eb7119422e400dbcafc5b21bcc430d2049a894f69b604e5", size = 836261 }, + { url = "https://files.pythonhosted.org/packages/d3/da/30bdcc83b23fc4f2996b39b41b2ff0ff2184230a78617c7b8636aac4d81d/pymongo-4.13.2-cp311-cp311-win_amd64.whl", hash = "sha256:ab87484c97ae837b0a7bbdaa978fa932fbb6acada3f42c3b2bee99121a594715", size = 851451 }, + { url = "https://files.pythonhosted.org/packages/03/e0/0e187750e23eed4227282fcf568fdb61f2b53bbcf8cbe3a71dde2a860d12/pymongo-4.13.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ec89516622dfc8b0fdff499612c0bd235aa45eeb176c9e311bcc0af44bf952b6", size = 912004 }, + { url = "https://files.pythonhosted.org/packages/57/c2/9b79795382daaf41e5f7379bffdef1880d68160adea352b796d6948cb5be/pymongo-4.13.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f30eab4d4326df54fee54f31f93e532dc2918962f733ee8e115b33e6fe151d92", size = 911698 }, + { url = "https://files.pythonhosted.org/packages/6f/e4/f04dc9ed5d1d9dbc539dc2d8758dd359c5373b0e06fcf25418b2c366737c/pymongo-4.13.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cce9428d12ba396ea245fc4c51f20228cead01119fcc959e1c80791ea45f820", size = 1690357 }, + { url = "https://files.pythonhosted.org/packages/bb/de/41478a7d527d38f1b98b084f4a78bbb805439a6ebd8689fbbee0a3dfacba/pymongo-4.13.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac9241b727a69c39117c12ac1e52d817ea472260dadc66262c3fdca0bab0709b", size = 1754593 }, + { url = "https://files.pythonhosted.org/packages/df/d9/8fa2eb110291e154f4312779b1a5b815090b8b05a59ecb4f4a32427db1df/pymongo-4.13.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3efc4c515b371a9fa1d198b6e03340985bfe1a55ae2d2b599a714934e7bc61ab", size = 1723637 }, + { url = "https://files.pythonhosted.org/packages/27/7b/9863fa60a4a51ea09f5e3cd6ceb231af804e723671230f2daf3bd1b59c2b/pymongo-4.13.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f57a664aa74610eb7a52fa93f2cf794a1491f4f76098343485dd7da5b3bcff06", size = 1693613 }, + { url = "https://files.pythonhosted.org/packages/9b/89/a42efa07820a59089836f409a63c96e7a74e33313e50dc39c554db99ac42/pymongo-4.13.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3dcb0b8cdd499636017a53f63ef64cf9b6bd3fd9355796c5a1d228e4be4a4c94", size = 1652745 }, + { url = "https://files.pythonhosted.org/packages/6a/cf/2c77d1acda61d281edd3e3f00d5017d3fac0c29042c769efd3b8018cb469/pymongo-4.13.2-cp312-cp312-win32.whl", hash = "sha256:bf43ae07804d7762b509f68e5ec73450bb8824e960b03b861143ce588b41f467", size = 883232 }, + { url = "https://files.pythonhosted.org/packages/d2/4f/727f59156e3798850c3c2901f106804053cb0e057ed1bd9883f5fa5aa8fa/pymongo-4.13.2-cp312-cp312-win_amd64.whl", hash = "sha256:812a473d584bcb02ab819d379cd5e752995026a2bb0d7713e78462b6650d3f3a", size = 903304 }, + { url = "https://files.pythonhosted.org/packages/e0/95/b44b8e24b161afe7b244f6d43c09a7a1f93308cad04198de1c14c67b24ce/pymongo-4.13.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d6044ca0eb74d97f7d3415264de86a50a401b7b0b136d30705f022f9163c3124", size = 966232 }, + { url = "https://files.pythonhosted.org/packages/6d/fc/d4d59799a52033acb187f7bd1f09bc75bebb9fd12cef4ba2964d235ad3f9/pymongo-4.13.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dd326bcb92d28d28a3e7ef0121602bad78691b6d4d1f44b018a4616122f1ba8b", size = 965935 }, + { url = "https://files.pythonhosted.org/packages/07/a8/67502899d89b317ea9952e4769bc193ca15efee561b24b38a86c59edde6f/pymongo-4.13.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dfb0c21bdd58e58625c9cd8de13e859630c29c9537944ec0a14574fdf88c2ac4", size = 1954070 }, + { url = "https://files.pythonhosted.org/packages/da/3b/0dac5d81d1af1b96b3200da7ccc52fc261a35efb7d2ac493252eb40a2b11/pymongo-4.13.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c9c7d345d57f17b1361008aea78a37e8c139631a46aeb185dd2749850883c7ba", size = 2031424 }, + { url = "https://files.pythonhosted.org/packages/31/ed/7a5af49a153224ca7e31e9915703e612ad9c45808cc39540e9dd1a2a7537/pymongo-4.13.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8860445a8da1b1545406fab189dc20319aff5ce28e65442b2b4a8f4228a88478", size = 1995339 }, + { url = "https://files.pythonhosted.org/packages/f1/e9/9c72eceae8439c4f1bdebc4e6b290bf035e3f050a80eeb74abb5e12ef8e2/pymongo-4.13.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:01c184b612f67d5a4c8f864ae7c40b6cc33c0e9bb05e39d08666f8831d120504", size = 1956066 }, + { url = "https://files.pythonhosted.org/packages/ac/79/9b019c47923395d5fced03856996465fb9340854b0f5a2ddf16d47e2437c/pymongo-4.13.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae2ea8c62d5f3c6529407c12471385d9a05f9fb890ce68d64976340c85cd661b", size = 1905642 }, + { url = "https://files.pythonhosted.org/packages/93/2f/ebf56c7fa9298fa2f9716e7b66cf62b29e7fc6e11774f3b87f55d214d466/pymongo-4.13.2-cp313-cp313-win32.whl", hash = "sha256:d13556e91c4a8cb07393b8c8be81e66a11ebc8335a40fa4af02f4d8d3b40c8a1", size = 930184 }, + { url = "https://files.pythonhosted.org/packages/76/2f/49c35464cbd5d116d950ff5d24b4b20491aaae115d35d40b945c33b29250/pymongo-4.13.2-cp313-cp313-win_amd64.whl", hash = "sha256:cfc69d7bc4d4d5872fd1e6de25e6a16e2372c7d5556b75c3b8e2204dce73e3fb", size = 955111 }, + { url = "https://files.pythonhosted.org/packages/57/56/b17c8b5329b1842b7847cf0fa224ef0a272bf2e5126360f4da8065c855a1/pymongo-4.13.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a457d2ac34c05e9e8a6bb724115b093300bf270f0655fb897df8d8604b2e3700", size = 1022735 }, + { url = "https://files.pythonhosted.org/packages/83/e6/66fec65a7919bf5f35be02e131b4dc4bf3152b5e8d78cd04b6d266a44514/pymongo-4.13.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:02f131a6e61559613b1171b53fbe21fed64e71b0cb4858c47fc9bc7c8e0e501c", size = 1022740 }, + { url = "https://files.pythonhosted.org/packages/17/92/cda7383df0d5e71dc007f172c1ecae6313d64ea05d82bbba06df7f6b3e49/pymongo-4.13.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c942d1c6334e894271489080404b1a2e3b8bd5de399f2a0c14a77d966be5bc9", size = 2282430 }, + { url = "https://files.pythonhosted.org/packages/84/da/285e05eb1d617b30dc7a7a98ebeb264353a8903e0e816a4eec6487c81f18/pymongo-4.13.2-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:850168d115680ab66a0931a6aa9dd98ed6aa5e9c3b9a6c12128049b9a5721bc5", size = 2369470 }, + { url = "https://files.pythonhosted.org/packages/89/c0/c0d5eae236de9ca293497dc58fc1e4872382223c28ec223f76afc701392c/pymongo-4.13.2-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:af7dfff90647ee77c53410f7fe8ca4fe343f8b768f40d2d0f71a5602f7b5a541", size = 2328857 }, + { url = "https://files.pythonhosted.org/packages/2b/5a/d8639fba60def128ce9848b99c56c54c8a4d0cd60342054cd576f0bfdf26/pymongo-4.13.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8057f9bc9c94a8fd54ee4f5e5106e445a8f406aff2df74746f21c8791ee2403", size = 2280053 }, + { url = "https://files.pythonhosted.org/packages/a1/69/d56f0897cc4932a336820c5d2470ffed50be04c624b07d1ad6ea75aaa975/pymongo-4.13.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:51040e1ba78d6671f8c65b29e2864483451e789ce93b1536de9cc4456ede87fa", size = 2219378 }, + { url = "https://files.pythonhosted.org/packages/04/1e/427e7f99801ee318b6331062d682d3816d7e1d6b6013077636bd75d49c87/pymongo-4.13.2-cp313-cp313t-win32.whl", hash = "sha256:7ab86b98a18c8689514a9f8d0ec7d9ad23a949369b31c9a06ce4a45dcbffcc5e", size = 979460 }, + { url = "https://files.pythonhosted.org/packages/b5/9c/00301a6df26f0f8d5c5955192892241e803742e7c3da8c2c222efabc0df6/pymongo-4.13.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c38168263ed94a250fc5cf9c6d33adea8ab11c9178994da1c3481c2a49d235f8", size = 1011057 }, +] + +[[package]] +name = "pynacl" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a7/22/27582568be639dfe22ddb3902225f91f2f17ceff88ce80e4db396c8986da/PyNaCl-1.5.0.tar.gz", hash = "sha256:8ac7448f09ab85811607bdd21ec2464495ac8b7c66d146bf545b0f08fb9220ba", size = 3392854 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/75/0b8ede18506041c0bf23ac4d8e2971b4161cd6ce630b177d0a08eb0d8857/PyNaCl-1.5.0-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:401002a4aaa07c9414132aaed7f6836ff98f59277a234704ff66878c2ee4a0d1", size = 349920 }, + { url = "https://files.pythonhosted.org/packages/59/bb/fddf10acd09637327a97ef89d2a9d621328850a72f1fdc8c08bdf72e385f/PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:52cb72a79269189d4e0dc537556f4740f7f0a9ec41c1322598799b0bdad4ef92", size = 601722 }, + { url = "https://files.pythonhosted.org/packages/5d/70/87a065c37cca41a75f2ce113a5a2c2aa7533be648b184ade58971b5f7ccc/PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a36d4a9dda1f19ce6e03c9a784a2921a4b726b02e1c736600ca9c22029474394", size = 680087 }, + { url = "https://files.pythonhosted.org/packages/ee/87/f1bb6a595f14a327e8285b9eb54d41fef76c585a0edef0a45f6fc95de125/PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:0c84947a22519e013607c9be43706dd42513f9e6ae5d39d3613ca1e142fba44d", size = 856678 }, + { url = "https://files.pythonhosted.org/packages/66/28/ca86676b69bf9f90e710571b67450508484388bfce09acf8a46f0b8c785f/PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06b8f6fa7f5de8d5d2f7573fe8c863c051225a27b61e6860fd047b1775807858", size = 1133660 }, + { url = "https://files.pythonhosted.org/packages/3d/85/c262db650e86812585e2bc59e497a8f59948a005325a11bbbc9ecd3fe26b/PyNaCl-1.5.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:a422368fc821589c228f4c49438a368831cb5bbc0eab5ebe1d7fac9dded6567b", size = 663824 }, + { url = "https://files.pythonhosted.org/packages/fd/1a/cc308a884bd299b651f1633acb978e8596c71c33ca85e9dc9fa33a5399b9/PyNaCl-1.5.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:61f642bf2378713e2c2e1de73444a3778e5f0a38be6fee0fe532fe30060282ff", size = 1117912 }, + { url = "https://files.pythonhosted.org/packages/25/2d/b7df6ddb0c2a33afdb358f8af6ea3b8c4d1196ca45497dd37a56f0c122be/PyNaCl-1.5.0-cp36-abi3-win32.whl", hash = "sha256:e46dae94e34b085175f8abb3b0aaa7da40767865ac82c928eeb9e57e1ea8a543", size = 204624 }, + { url = "https://files.pythonhosted.org/packages/5e/22/d3db169895faaf3e2eda892f005f433a62db2decbcfbc2f61e6517adfa87/PyNaCl-1.5.0-cp36-abi3-win_amd64.whl", hash = "sha256:20f42270d27e1b6a29f54032090b972d97f0a1b0948cc52392041ef7831fee93", size = 212141 }, +] + +[[package]] +name = "pyopenssl" +version = "25.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/04/8c/cd89ad05804f8e3c17dea8f178c3f40eeab5694c30e0c9f5bcd49f576fc3/pyopenssl-25.1.0.tar.gz", hash = "sha256:8d031884482e0c67ee92bf9a4d8cceb08d92aba7136432ffb0703c5280fc205b", size = 179937 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/80/28/2659c02301b9500751f8d42f9a6632e1508aa5120de5e43042b8b30f8d5d/pyopenssl-25.1.0-py3-none-any.whl", hash = "sha256:2b11f239acc47ac2e5aca04fd7fa829800aeee22a2eb30d744572a157bd8a1ab", size = 56771 }, +] + +[[package]] +name = "pypandoc" +version = "1.15" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e1/88/26e650d053df5f3874aa3c05901a14166ce3271f58bfe114fd776987efbd/pypandoc-1.15.tar.gz", hash = "sha256:ea25beebe712ae41d63f7410c08741a3cab0e420f6703f95bc9b3a749192ce13", size = 32940 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/06/0763e0ccc81754d3eadb21b2cb86cf21bdedc9b52698c2ad6785db7f0a4e/pypandoc-1.15-py3-none-any.whl", hash = "sha256:4ededcc76c8770f27aaca6dff47724578428eca84212a31479403a9731fc2b16", size = 21321 }, +] + +[[package]] +name = "pyparsing" +version = "3.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bb/22/f1129e69d94ffff626bdb5c835506b3a5b4f3d070f17ea295e12c2c6f60f/pyparsing-3.2.3.tar.gz", hash = "sha256:b9c13f1ab8b3b542f72e28f634bad4de758ab3ce4546e4301970ad6fa77c38be", size = 1088608 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/e7/df2285f3d08fee213f2d041540fa4fc9ca6c2d44cf36d3a035bf2a8d2bcc/pyparsing-3.2.3-py3-none-any.whl", hash = "sha256:a749938e02d6fd0b59b356ca504a24982314bb090c383e3cf201c95ef7e2bfcf", size = 111120 }, +] + +[[package]] +name = "pypdf" +version = "5.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/89/3a/584b97a228950ed85aec97c811c68473d9b8d149e6a8c155668287cf1a28/pypdf-5.9.0.tar.gz", hash = "sha256:30f67a614d558e495e1fbb157ba58c1de91ffc1718f5e0dfeb82a029233890a1", size = 5035118 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/d9/6cff57c80a6963e7dd183bf09e9f21604a77716644b1e580e97b259f7612/pypdf-5.9.0-py3-none-any.whl", hash = "sha256:be10a4c54202f46d9daceaa8788be07aa8cd5ea8c25c529c50dd509206382c35", size = 313193 }, +] + +[[package]] +name = "pypdfium2" +version = "4.30.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/14/838b3ba247a0ba92e4df5d23f2bea9478edcfd72b78a39d6ca36ccd84ad2/pypdfium2-4.30.0.tar.gz", hash = "sha256:48b5b7e5566665bc1015b9d69c1ebabe21f6aee468b509531c3c8318eeee2e16", size = 140239 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/9a/c8ff5cc352c1b60b0b97642ae734f51edbab6e28b45b4fcdfe5306ee3c83/pypdfium2-4.30.0-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:b33ceded0b6ff5b2b93bc1fe0ad4b71aa6b7e7bd5875f1ca0cdfb6ba6ac01aab", size = 2837254 }, + { url = "https://files.pythonhosted.org/packages/21/8b/27d4d5409f3c76b985f4ee4afe147b606594411e15ac4dc1c3363c9a9810/pypdfium2-4.30.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:4e55689f4b06e2d2406203e771f78789bd4f190731b5d57383d05cf611d829de", size = 2707624 }, + { url = "https://files.pythonhosted.org/packages/11/63/28a73ca17c24b41a205d658e177d68e198d7dde65a8c99c821d231b6ee3d/pypdfium2-4.30.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e6e50f5ce7f65a40a33d7c9edc39f23140c57e37144c2d6d9e9262a2a854854", size = 2793126 }, + { url = "https://files.pythonhosted.org/packages/d1/96/53b3ebf0955edbd02ac6da16a818ecc65c939e98fdeb4e0958362bd385c8/pypdfium2-4.30.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3d0dd3ecaffd0b6dbda3da663220e705cb563918249bda26058c6036752ba3a2", size = 2591077 }, + { url = "https://files.pythonhosted.org/packages/ec/ee/0394e56e7cab8b5b21f744d988400948ef71a9a892cbeb0b200d324ab2c7/pypdfium2-4.30.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cc3bf29b0db8c76cdfaac1ec1cde8edf211a7de7390fbf8934ad2aa9b4d6dfad", size = 2864431 }, + { url = "https://files.pythonhosted.org/packages/65/cd/3f1edf20a0ef4a212a5e20a5900e64942c5a374473671ac0780eaa08ea80/pypdfium2-4.30.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f1f78d2189e0ddf9ac2b7a9b9bd4f0c66f54d1389ff6c17e9fd9dc034d06eb3f", size = 2812008 }, + { url = "https://files.pythonhosted.org/packages/c8/91/2d517db61845698f41a2a974de90762e50faeb529201c6b3574935969045/pypdfium2-4.30.0-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:5eda3641a2da7a7a0b2f4dbd71d706401a656fea521b6b6faa0675b15d31a163", size = 6181543 }, + { url = "https://files.pythonhosted.org/packages/ba/c4/ed1315143a7a84b2c7616569dfb472473968d628f17c231c39e29ae9d780/pypdfium2-4.30.0-py3-none-musllinux_1_1_i686.whl", hash = "sha256:0dfa61421b5eb68e1188b0b2231e7ba35735aef2d867d86e48ee6cab6975195e", size = 6175911 }, + { url = "https://files.pythonhosted.org/packages/7a/c4/9e62d03f414e0e3051c56d5943c3bf42aa9608ede4e19dc96438364e9e03/pypdfium2-4.30.0-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:f33bd79e7a09d5f7acca3b0b69ff6c8a488869a7fab48fdf400fec6e20b9c8be", size = 6267430 }, + { url = "https://files.pythonhosted.org/packages/90/47/eda4904f715fb98561e34012826e883816945934a851745570521ec89520/pypdfium2-4.30.0-py3-none-win32.whl", hash = "sha256:ee2410f15d576d976c2ab2558c93d392a25fb9f6635e8dd0a8a3a5241b275e0e", size = 2775951 }, + { url = "https://files.pythonhosted.org/packages/25/bd/56d9ec6b9f0fc4e0d95288759f3179f0fcd34b1a1526b75673d2f6d5196f/pypdfium2-4.30.0-py3-none-win_amd64.whl", hash = "sha256:90dbb2ac07be53219f56be09961eb95cf2473f834d01a42d901d13ccfad64b4c", size = 2892098 }, + { url = "https://files.pythonhosted.org/packages/be/7a/097801205b991bc3115e8af1edb850d30aeaf0118520b016354cf5ccd3f6/pypdfium2-4.30.0-py3-none-win_arm64.whl", hash = "sha256:119b2969a6d6b1e8d55e99caaf05290294f2d0fe49c12a3f17102d01c441bd29", size = 2752118 }, +] + +[[package]] +name = "pyperclip" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/30/23/2f0a3efc4d6a32f3b63cdff36cd398d9701d26cda58e3ab97ac79fb5e60d/pyperclip-1.9.0.tar.gz", hash = "sha256:b7de0142ddc81bfc5c7507eea19da920b92252b548b96186caf94a5e2527d310", size = 20961 } + +[[package]] +name = "pypika" +version = "0.48.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/2c/94ed7b91db81d61d7096ac8f2d325ec562fc75e35f3baea8749c85b28784/PyPika-0.48.9.tar.gz", hash = "sha256:838836a61747e7c8380cd1b7ff638694b7a7335345d0f559b04b2cd832ad5378", size = 67259 } + +[[package]] +name = "pyproject-hooks" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/82/28175b2414effca1cdac8dc99f76d660e7a4fb0ceefa4b4ab8f5f6742925/pyproject_hooks-1.2.0.tar.gz", hash = "sha256:1e859bd5c40fae9448642dd871adf459e5e2084186e8d2c2a79a824c970da1f8", size = 19228 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl", hash = "sha256:9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913", size = 10216 }, +] + +[[package]] +name = "pyreadline3" +version = "3.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/49/4cea918a08f02817aabae639e3d0ac046fef9f9180518a3ad394e22da148/pyreadline3-3.5.4.tar.gz", hash = "sha256:8d57d53039a1c75adba8e50dd3d992b28143480816187ea5efbd5c78e6c885b7", size = 99839 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/dc/491b7661614ab97483abf2056be1deee4dc2490ecbf7bff9ab5cdbac86e1/pyreadline3-3.5.4-py3-none-any.whl", hash = "sha256:eaf8e6cc3c49bcccf145fc6067ba8643d1df34d604a1ec0eccbf7a18e6d3fae6", size = 83178 }, +] + +[[package]] +name = "pyright" +version = "1.1.403" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nodeenv" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fe/f6/35f885264ff08c960b23d1542038d8da86971c5d8c955cfab195a4f672d7/pyright-1.1.403.tar.gz", hash = "sha256:3ab69b9f41c67fb5bbb4d7a36243256f0d549ed3608678d381d5f51863921104", size = 3913526 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/b6/b04e5c2f41a5ccad74a1a4759da41adb20b4bc9d59a5e08d29ba60084d07/pyright-1.1.403-py3-none-any.whl", hash = "sha256:c0eeca5aa76cbef3fcc271259bbd785753c7ad7bcac99a9162b4c4c7daed23b3", size = 5684504 }, +] + +[[package]] +name = "pysbd" +version = "0.3.4" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/0a/c99fb7d7e176f8b176ef19704a32e6a9c6aafdf19ef75a187f701fc15801/pysbd-0.3.4-py3-none-any.whl", hash = "sha256:cd838939b7b0b185fcf86b0baf6636667dfb6e474743beeff878e9f42e022953", size = 71082 }, +] + +[[package]] +name = "pysher" +version = "1.0.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "requests" }, + { name = "websocket-client" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/12/a0/d0638470df605ce266991fb04f74c69ab1bed3b90ac3838e9c3c8b69b66a/Pysher-1.0.8.tar.gz", hash = "sha256:7849c56032b208e49df67d7bd8d49029a69042ab0bb45b2ed59fa08f11ac5988", size = 9071 } + +[[package]] +name = "pysocks" +version = "1.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/11/293dd436aea955d45fc4e8a35b6ae7270f5b8e00b53cf6c024c83b657a11/PySocks-1.7.1.tar.gz", hash = "sha256:3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0", size = 284429 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8d/59/b4572118e098ac8e46e399a1dd0f2d85403ce8bbaad9ec79373ed6badaf9/PySocks-1.7.1-py3-none-any.whl", hash = "sha256:2725bd0a9925919b9b51739eea5f9e2bae91e83288108a9ad338b2e3a4435ee5", size = 16725 }, +] + +[[package]] +name = "pytest" +version = "8.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/08/ba/45911d754e8eba3d5a841a5ce61a65a685ff1798421ac054f85aa8747dfb/pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c", size = 1517714 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/29/16/c8a903f4c4dffe7a12843191437d7cd8e32751d5de349d45d3fe69544e87/pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7", size = 365474 }, +] + +[[package]] +name = "pytest-asyncio" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "backports-asyncio-runner", marker = "python_full_version < '3.11'" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4e/51/f8794af39eeb870e87a8c8068642fc07bce0c854d6865d7dd0f2a9d338c2/pytest_asyncio-1.1.0.tar.gz", hash = "sha256:796aa822981e01b68c12e4827b8697108f7205020f24b5793b3c41555dab68ea", size = 46652 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/9d/bf86eddabf8c6c9cb1ea9a869d6873b46f105a5d292d3a6f7071f5b07935/pytest_asyncio-1.1.0-py3-none-any.whl", hash = "sha256:5fe2d69607b0bd75c656d1211f969cadba035030156745ee09e7d71740e58ecf", size = 15157 }, +] + +[[package]] +name = "pytest-recording" +version = "0.13.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, + { name = "vcrpy", version = "5.1.0", source = { registry = "https://pypi.org/simple" }, marker = "platform_python_implementation == 'PyPy'" }, + { name = "vcrpy", version = "7.0.0", source = { registry = "https://pypi.org/simple" }, marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/32/9c/f4027c5f1693847b06d11caf4b4f6bb09f22c1581ada4663877ec166b8c6/pytest_recording-0.13.4.tar.gz", hash = "sha256:568d64b2a85992eec4ae0a419c855d5fd96782c5fb016784d86f18053792768c", size = 26576 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/c2/ce34735972cc42d912173e79f200fe66530225190c06655c5632a9d88f1e/pytest_recording-0.13.4-py3-none-any.whl", hash = "sha256:ad49a434b51b1c4f78e85b1e6b74fdcc2a0a581ca16e52c798c6ace971f7f439", size = 13723 }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892 }, +] + +[[package]] +name = "python-docx" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "lxml" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a9/f7/eddfe33871520adab45aaa1a71f0402a2252050c14c7e3009446c8f4701c/python_docx-1.2.0.tar.gz", hash = "sha256:7bc9d7b7d8a69c9c02ca09216118c86552704edc23bac179283f2e38f86220ce", size = 5723256 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/00/1e03a4989fa5795da308cd774f05b704ace555a70f9bf9d3be057b680bcf/python_docx-1.2.0-py3-none-any.whl", hash = "sha256:3fd478f3250fbbbfd3b94fe1e985955737c145627498896a8a6bf81f4baf66c7", size = 252987 }, +] + +[[package]] +name = "python-dotenv" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/b0/4bc07ccd3572a2f9df7e6782f52b0c6c90dcbb803ac4a167702d7d0dfe1e/python_dotenv-1.1.1.tar.gz", hash = "sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab", size = 41978 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/ed/539768cf28c661b5b068d66d96a2f155c4971a5d55684a514c1a0e0dec2f/python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc", size = 20556 }, +] + +[[package]] +name = "python-iso639" +version = "2025.2.18" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d5/19/45aa1917c7b1f4eb71104795b9b0cbf97169b99ec46cd303445883536549/python_iso639-2025.2.18.tar.gz", hash = "sha256:34e31e8e76eb3fc839629e257b12bcfd957c6edcbd486bbf66ba5185d1f566e8", size = 173552 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/a3/3ceaf89a17a1e1d5e7bbdfe5514aa3055d91285b37a5c8fed662969e3d56/python_iso639-2025.2.18-py3-none-any.whl", hash = "sha256:b2d471c37483a26f19248458b20e7bd96492e15368b01053b540126bcc23152f", size = 167631 }, +] + +[[package]] +name = "python-magic" +version = "0.4.27" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/da/db/0b3e28ac047452d079d375ec6798bf76a036a08182dbb39ed38116a49130/python-magic-0.4.27.tar.gz", hash = "sha256:c1ba14b08e4a5f5c31a302b7721239695b2f0f058d125bd5ce1ee36b9d9d3c3b", size = 14677 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6c/73/9f872cb81fc5c3bb48f7227872c28975f998f3e7c2b1c16e95e6432bbb90/python_magic-0.4.27-py2.py3-none-any.whl", hash = "sha256:c212960ad306f700aa0d01e5d7a325d20548ff97eb9920dcd29513174f0294d3", size = 13840 }, +] + +[[package]] +name = "python-multipart" +version = "0.0.20" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/87/f44d7c9f274c7ee665a29b885ec97089ec5dc034c7f3fafa03da9e39a09e/python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13", size = 37158 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546 }, +] + +[[package]] +name = "python-oxmsg" +version = "0.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "olefile" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a2/4e/869f34faedbc968796d2c7e9837dede079c9cb9750917356b1f1eda926e9/python_oxmsg-0.0.2.tar.gz", hash = "sha256:a6aff4deb1b5975d44d49dab1d9384089ffeec819e19c6940bc7ffbc84775fad", size = 34713 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/53/67/f56c69a98c7eb244025845506387d0f961681657c9fcd8b2d2edd148f9d2/python_oxmsg-0.0.2-py3-none-any.whl", hash = "sha256:22be29b14c46016bcd05e34abddfd8e05ee82082f53b82753d115da3fc7d0355", size = 31455 }, +] + +[[package]] +name = "python-pptx" +version = "1.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "lxml" }, + { name = "pillow" }, + { name = "typing-extensions" }, + { name = "xlsxwriter" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/52/a9/0c0db8d37b2b8a645666f7fd8accea4c6224e013c42b1d5c17c93590cd06/python_pptx-1.0.2.tar.gz", hash = "sha256:479a8af0eaf0f0d76b6f00b0887732874ad2e3188230315290cd1f9dd9cc7095", size = 10109297 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/4f/00be2196329ebbff56ce564aa94efb0fbc828d00de250b1980de1a34ab49/python_pptx-1.0.2-py3-none-any.whl", hash = "sha256:160838e0b8565a8b1f67947675886e9fea18aa5e795db7ae531606d68e785cba", size = 472788 }, +] + +[[package]] +name = "pytube" +version = "15.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/e7/16fec46c8d255c4bbc4b185d89c91dc92cdb802836570d8004d0db169c91/pytube-15.0.0.tar.gz", hash = "sha256:076052efe76f390dfa24b1194ff821d4e86c17d41cb5562f3a276a8bcbfc9d1d", size = 67229 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/64/bcf8632ed2b7a36bbf84a0544885ffa1d0b4bcf25cc0903dba66ec5fdad9/pytube-15.0.0-py3-none-any.whl", hash = "sha256:07b9904749e213485780d7eb606e5e5b8e4341aa4dccf699160876da00e12d78", size = 57594 }, +] + +[[package]] +name = "pytz" +version = "2025.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225 }, +] + +[[package]] +name = "pyvis" +version = "0.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ipython", version = "8.37.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "ipython", version = "9.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "jinja2" }, + { name = "jsonpickle" }, + { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "networkx", version = "3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/ab/4b/e37e4e5d5ee1179694917b445768bdbfb084f5a59ecd38089d3413d4c70f/pyvis-0.3.2-py3-none-any.whl", hash = "sha256:5720c4ca8161dc5d9ab352015723abb7a8bb8fb443edeb07f7a322db34a97555", size = 756038 }, +] + +[[package]] +name = "pywin32" +version = "311" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/40/44efbb0dfbd33aca6a6483191dae0716070ed99e2ecb0c53683f400a0b4f/pywin32-311-cp310-cp310-win32.whl", hash = "sha256:d03ff496d2a0cd4a5893504789d4a15399133fe82517455e78bad62efbb7f0a3", size = 8760432 }, + { url = "https://files.pythonhosted.org/packages/5e/bf/360243b1e953bd254a82f12653974be395ba880e7ec23e3731d9f73921cc/pywin32-311-cp310-cp310-win_amd64.whl", hash = "sha256:797c2772017851984b97180b0bebe4b620bb86328e8a884bb626156295a63b3b", size = 9590103 }, + { url = "https://files.pythonhosted.org/packages/57/38/d290720e6f138086fb3d5ffe0b6caa019a791dd57866940c82e4eeaf2012/pywin32-311-cp310-cp310-win_arm64.whl", hash = "sha256:0502d1facf1fed4839a9a51ccbcc63d952cf318f78ffc00a7e78528ac27d7a2b", size = 8778557 }, + { url = "https://files.pythonhosted.org/packages/7c/af/449a6a91e5d6db51420875c54f6aff7c97a86a3b13a0b4f1a5c13b988de3/pywin32-311-cp311-cp311-win32.whl", hash = "sha256:184eb5e436dea364dcd3d2316d577d625c0351bf237c4e9a5fabbcfa5a58b151", size = 8697031 }, + { url = "https://files.pythonhosted.org/packages/51/8f/9bb81dd5bb77d22243d33c8397f09377056d5c687aa6d4042bea7fbf8364/pywin32-311-cp311-cp311-win_amd64.whl", hash = "sha256:3ce80b34b22b17ccbd937a6e78e7225d80c52f5ab9940fe0506a1a16f3dab503", size = 9508308 }, + { url = "https://files.pythonhosted.org/packages/44/7b/9c2ab54f74a138c491aba1b1cd0795ba61f144c711daea84a88b63dc0f6c/pywin32-311-cp311-cp311-win_arm64.whl", hash = "sha256:a733f1388e1a842abb67ffa8e7aad0e70ac519e09b0f6a784e65a136ec7cefd2", size = 8703930 }, + { url = "https://files.pythonhosted.org/packages/e7/ab/01ea1943d4eba0f850c3c61e78e8dd59757ff815ff3ccd0a84de5f541f42/pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31", size = 8706543 }, + { url = "https://files.pythonhosted.org/packages/d1/a8/a0e8d07d4d051ec7502cd58b291ec98dcc0c3fff027caad0470b72cfcc2f/pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067", size = 9495040 }, + { url = "https://files.pythonhosted.org/packages/ba/3a/2ae996277b4b50f17d61f0603efd8253cb2d79cc7ae159468007b586396d/pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852", size = 8710102 }, + { url = "https://files.pythonhosted.org/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700 }, + { url = "https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700 }, + { url = "https://files.pythonhosted.org/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318 }, +] + +[[package]] +name = "pyyaml" +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/95/a3fac87cb7158e231b5a6012e438c647e1a87f09f8e0d123acec8ab8bf71/PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", size = 184199 }, + { url = "https://files.pythonhosted.org/packages/c7/7a/68bd47624dab8fd4afbfd3c48e3b79efe09098ae941de5b58abcbadff5cb/PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", size = 171758 }, + { url = "https://files.pythonhosted.org/packages/49/ee/14c54df452143b9ee9f0f29074d7ca5516a36edb0b4cc40c3f280131656f/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", size = 718463 }, + { url = "https://files.pythonhosted.org/packages/4d/61/de363a97476e766574650d742205be468921a7b532aa2499fcd886b62530/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", size = 719280 }, + { url = "https://files.pythonhosted.org/packages/6b/4e/1523cb902fd98355e2e9ea5e5eb237cbc5f3ad5f3075fa65087aa0ecb669/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", size = 751239 }, + { url = "https://files.pythonhosted.org/packages/b7/33/5504b3a9a4464893c32f118a9cc045190a91637b119a9c881da1cf6b7a72/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", size = 695802 }, + { url = "https://files.pythonhosted.org/packages/5c/20/8347dcabd41ef3a3cdc4f7b7a2aff3d06598c8779faa189cdbf878b626a4/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", size = 720527 }, + { url = "https://files.pythonhosted.org/packages/be/aa/5afe99233fb360d0ff37377145a949ae258aaab831bde4792b32650a4378/PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", size = 144052 }, + { url = "https://files.pythonhosted.org/packages/b5/84/0fa4b06f6d6c958d207620fc60005e241ecedceee58931bb20138e1e5776/PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", size = 161774 }, + { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612 }, + { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040 }, + { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829 }, + { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167 }, + { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952 }, + { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301 }, + { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638 }, + { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850 }, + { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980 }, + { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873 }, + { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302 }, + { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154 }, + { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223 }, + { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542 }, + { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164 }, + { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611 }, + { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591 }, + { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338 }, + { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309 }, + { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679 }, + { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428 }, + { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361 }, + { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523 }, + { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660 }, + { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597 }, + { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527 }, + { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446 }, +] + +[[package]] +name = "qdrant-client" +version = "1.15.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "grpcio" }, + { name = "httpx", extra = ["http2"] }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "portalocker" }, + { name = "protobuf" }, + { name = "pydantic" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/79/8b/76c7d325e11d97cb8eb5e261c3759e9ed6664735afbf32fdded5b580690c/qdrant_client-1.15.1.tar.gz", hash = "sha256:631f1f3caebfad0fd0c1fba98f41be81d9962b7bf3ca653bed3b727c0e0cbe0e", size = 295297 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/33/d8df6a2b214ffbe4138db9a1efe3248f67dc3c671f82308bea1582ecbbb7/qdrant_client-1.15.1-py3-none-any.whl", hash = "sha256:2b975099b378382f6ca1cfb43f0d59e541be6e16a5892f282a4b8de7eff5cb63", size = 337331 }, +] + +[[package]] +name = "rapidfuzz" +version = "3.13.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ed/f6/6895abc3a3d056b9698da3199b04c0e56226d530ae44a470edabf8b664f0/rapidfuzz-3.13.0.tar.gz", hash = "sha256:d2eaf3839e52cbcc0accbe9817a67b4b0fcf70aaeb229cfddc1c28061f9ce5d8", size = 57904226 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/27/ca10b3166024ae19a7e7c21f73c58dfd4b7fef7420e5497ee64ce6b73453/rapidfuzz-3.13.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:aafc42a1dc5e1beeba52cd83baa41372228d6d8266f6d803c16dbabbcc156255", size = 1998899 }, + { url = "https://files.pythonhosted.org/packages/f0/38/c4c404b13af0315483a6909b3a29636e18e1359307fb74a333fdccb3730d/rapidfuzz-3.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:85c9a131a44a95f9cac2eb6e65531db014e09d89c4f18c7b1fa54979cb9ff1f3", size = 1449949 }, + { url = "https://files.pythonhosted.org/packages/12/ae/15c71d68a6df6b8e24595421fdf5bcb305888318e870b7be8d935a9187ee/rapidfuzz-3.13.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7d7cec4242d30dd521ef91c0df872e14449d1dffc2a6990ede33943b0dae56c3", size = 1424199 }, + { url = "https://files.pythonhosted.org/packages/dc/9a/765beb9e14d7b30d12e2d6019e8b93747a0bedbc1d0cce13184fa3825426/rapidfuzz-3.13.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e297c09972698c95649e89121e3550cee761ca3640cd005e24aaa2619175464e", size = 5352400 }, + { url = "https://files.pythonhosted.org/packages/e2/b8/49479fe6f06b06cd54d6345ed16de3d1ac659b57730bdbe897df1e059471/rapidfuzz-3.13.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ef0f5f03f61b0e5a57b1df7beafd83df993fd5811a09871bad6038d08e526d0d", size = 1652465 }, + { url = "https://files.pythonhosted.org/packages/6f/d8/08823d496b7dd142a7b5d2da04337df6673a14677cfdb72f2604c64ead69/rapidfuzz-3.13.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d8cf5f7cd6e4d5eb272baf6a54e182b2c237548d048e2882258336533f3f02b7", size = 1616590 }, + { url = "https://files.pythonhosted.org/packages/38/d4/5cfbc9a997e544f07f301c54d42aac9e0d28d457d543169e4ec859b8ce0d/rapidfuzz-3.13.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9256218ac8f1a957806ec2fb9a6ddfc6c32ea937c0429e88cf16362a20ed8602", size = 3086956 }, + { url = "https://files.pythonhosted.org/packages/25/1e/06d8932a72fa9576095234a15785136407acf8f9a7dbc8136389a3429da1/rapidfuzz-3.13.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e1bdd2e6d0c5f9706ef7595773a81ca2b40f3b33fd7f9840b726fb00c6c4eb2e", size = 2494220 }, + { url = "https://files.pythonhosted.org/packages/03/16/5acf15df63119d5ca3d9a54b82807866ff403461811d077201ca351a40c3/rapidfuzz-3.13.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5280be8fd7e2bee5822e254fe0a5763aa0ad57054b85a32a3d9970e9b09bbcbf", size = 7585481 }, + { url = "https://files.pythonhosted.org/packages/e1/cf/ebade4009431ea8e715e59e882477a970834ddaacd1a670095705b86bd0d/rapidfuzz-3.13.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fd742c03885db1fce798a1cd87a20f47f144ccf26d75d52feb6f2bae3d57af05", size = 2894842 }, + { url = "https://files.pythonhosted.org/packages/a7/bd/0732632bd3f906bf613229ee1b7cbfba77515db714a0e307becfa8a970ae/rapidfuzz-3.13.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:5435fcac94c9ecf0504bf88a8a60c55482c32e18e108d6079a0089c47f3f8cf6", size = 3438517 }, + { url = "https://files.pythonhosted.org/packages/83/89/d3bd47ec9f4b0890f62aea143a1e35f78f3d8329b93d9495b4fa8a3cbfc3/rapidfuzz-3.13.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:93a755266856599be4ab6346273f192acde3102d7aa0735e2f48b456397a041f", size = 4412773 }, + { url = "https://files.pythonhosted.org/packages/b3/57/1a152a07883e672fc117c7f553f5b933f6e43c431ac3fd0e8dae5008f481/rapidfuzz-3.13.0-cp310-cp310-win32.whl", hash = "sha256:3abe6a4e8eb4cfc4cda04dd650a2dc6d2934cbdeda5def7e6fd1c20f6e7d2a0b", size = 1842334 }, + { url = "https://files.pythonhosted.org/packages/a7/68/7248addf95b6ca51fc9d955161072285da3059dd1472b0de773cff910963/rapidfuzz-3.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:e8ddb58961401da7d6f55f185512c0d6bd24f529a637078d41dd8ffa5a49c107", size = 1624392 }, + { url = "https://files.pythonhosted.org/packages/68/23/f41c749f2c61ed1ed5575eaf9e73ef9406bfedbf20a3ffa438d15b5bf87e/rapidfuzz-3.13.0-cp310-cp310-win_arm64.whl", hash = "sha256:c523620d14ebd03a8d473c89e05fa1ae152821920c3ff78b839218ff69e19ca3", size = 865584 }, + { url = "https://files.pythonhosted.org/packages/87/17/9be9eff5a3c7dfc831c2511262082c6786dca2ce21aa8194eef1cb71d67a/rapidfuzz-3.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d395a5cad0c09c7f096433e5fd4224d83b53298d53499945a9b0e5a971a84f3a", size = 1999453 }, + { url = "https://files.pythonhosted.org/packages/75/67/62e57896ecbabe363f027d24cc769d55dd49019e576533ec10e492fcd8a2/rapidfuzz-3.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b7b3eda607a019169f7187328a8d1648fb9a90265087f6903d7ee3a8eee01805", size = 1450881 }, + { url = "https://files.pythonhosted.org/packages/96/5c/691c5304857f3476a7b3df99e91efc32428cbe7d25d234e967cc08346c13/rapidfuzz-3.13.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:98e0bfa602e1942d542de077baf15d658bd9d5dcfe9b762aff791724c1c38b70", size = 1422990 }, + { url = "https://files.pythonhosted.org/packages/46/81/7a7e78f977496ee2d613154b86b203d373376bcaae5de7bde92f3ad5a192/rapidfuzz-3.13.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bef86df6d59667d9655905b02770a0c776d2853971c0773767d5ef8077acd624", size = 5342309 }, + { url = "https://files.pythonhosted.org/packages/51/44/12fdd12a76b190fe94bf38d252bb28ddf0ab7a366b943e792803502901a2/rapidfuzz-3.13.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fedd316c165beed6307bf754dee54d3faca2c47e1f3bcbd67595001dfa11e969", size = 1656881 }, + { url = "https://files.pythonhosted.org/packages/27/ae/0d933e660c06fcfb087a0d2492f98322f9348a28b2cc3791a5dbadf6e6fb/rapidfuzz-3.13.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5158da7f2ec02a930be13bac53bb5903527c073c90ee37804090614cab83c29e", size = 1608494 }, + { url = "https://files.pythonhosted.org/packages/3d/2c/4b2f8aafdf9400e5599b6ed2f14bc26ca75f5a923571926ccbc998d4246a/rapidfuzz-3.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b6f913ee4618ddb6d6f3e387b76e8ec2fc5efee313a128809fbd44e65c2bbb2", size = 3072160 }, + { url = "https://files.pythonhosted.org/packages/60/7d/030d68d9a653c301114101c3003b31ce01cf2c3224034cd26105224cd249/rapidfuzz-3.13.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d25fdbce6459ccbbbf23b4b044f56fbd1158b97ac50994eaae2a1c0baae78301", size = 2491549 }, + { url = "https://files.pythonhosted.org/packages/8e/cd/7040ba538fc6a8ddc8816a05ecf46af9988b46c148ddd7f74fb0fb73d012/rapidfuzz-3.13.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:25343ccc589a4579fbde832e6a1e27258bfdd7f2eb0f28cb836d6694ab8591fc", size = 7584142 }, + { url = "https://files.pythonhosted.org/packages/c1/96/85f7536fbceb0aa92c04a1c37a3fc4fcd4e80649e9ed0fb585382df82edc/rapidfuzz-3.13.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a9ad1f37894e3ffb76bbab76256e8a8b789657183870be11aa64e306bb5228fd", size = 2896234 }, + { url = "https://files.pythonhosted.org/packages/55/fd/460e78438e7019f2462fe9d4ecc880577ba340df7974c8a4cfe8d8d029df/rapidfuzz-3.13.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5dc71ef23845bb6b62d194c39a97bb30ff171389c9812d83030c1199f319098c", size = 3437420 }, + { url = "https://files.pythonhosted.org/packages/cc/df/c3c308a106a0993befd140a414c5ea78789d201cf1dfffb8fd9749718d4f/rapidfuzz-3.13.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b7f4c65facdb94f44be759bbd9b6dda1fa54d0d6169cdf1a209a5ab97d311a75", size = 4410860 }, + { url = "https://files.pythonhosted.org/packages/75/ee/9d4ece247f9b26936cdeaae600e494af587ce9bf8ddc47d88435f05cfd05/rapidfuzz-3.13.0-cp311-cp311-win32.whl", hash = "sha256:b5104b62711565e0ff6deab2a8f5dbf1fbe333c5155abe26d2cfd6f1849b6c87", size = 1843161 }, + { url = "https://files.pythonhosted.org/packages/c9/5a/d00e1f63564050a20279015acb29ecaf41646adfacc6ce2e1e450f7f2633/rapidfuzz-3.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:9093cdeb926deb32a4887ebe6910f57fbcdbc9fbfa52252c10b56ef2efb0289f", size = 1629962 }, + { url = "https://files.pythonhosted.org/packages/3b/74/0a3de18bc2576b794f41ccd07720b623e840fda219ab57091897f2320fdd/rapidfuzz-3.13.0-cp311-cp311-win_arm64.whl", hash = "sha256:f70f646751b6aa9d05be1fb40372f006cc89d6aad54e9d79ae97bd1f5fce5203", size = 866631 }, + { url = "https://files.pythonhosted.org/packages/13/4b/a326f57a4efed8f5505b25102797a58e37ee11d94afd9d9422cb7c76117e/rapidfuzz-3.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a1a6a906ba62f2556372282b1ef37b26bca67e3d2ea957277cfcefc6275cca7", size = 1989501 }, + { url = "https://files.pythonhosted.org/packages/b7/53/1f7eb7ee83a06c400089ec7cb841cbd581c2edd7a4b21eb2f31030b88daa/rapidfuzz-3.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2fd0975e015b05c79a97f38883a11236f5a24cca83aa992bd2558ceaa5652b26", size = 1445379 }, + { url = "https://files.pythonhosted.org/packages/07/09/de8069a4599cc8e6d194e5fa1782c561151dea7d5e2741767137e2a8c1f0/rapidfuzz-3.13.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d4e13593d298c50c4f94ce453f757b4b398af3fa0fd2fde693c3e51195b7f69", size = 1405986 }, + { url = "https://files.pythonhosted.org/packages/5d/77/d9a90b39c16eca20d70fec4ca377fbe9ea4c0d358c6e4736ab0e0e78aaf6/rapidfuzz-3.13.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed6f416bda1c9133000009d84d9409823eb2358df0950231cc936e4bf784eb97", size = 5310809 }, + { url = "https://files.pythonhosted.org/packages/1e/7d/14da291b0d0f22262d19522afaf63bccf39fc027c981233fb2137a57b71f/rapidfuzz-3.13.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1dc82b6ed01acb536b94a43996a94471a218f4d89f3fdd9185ab496de4b2a981", size = 1629394 }, + { url = "https://files.pythonhosted.org/packages/b7/e4/79ed7e4fa58f37c0f8b7c0a62361f7089b221fe85738ae2dbcfb815e985a/rapidfuzz-3.13.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e9d824de871daa6e443b39ff495a884931970d567eb0dfa213d234337343835f", size = 1600544 }, + { url = "https://files.pythonhosted.org/packages/4e/20/e62b4d13ba851b0f36370060025de50a264d625f6b4c32899085ed51f980/rapidfuzz-3.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2d18228a2390375cf45726ce1af9d36ff3dc1f11dce9775eae1f1b13ac6ec50f", size = 3052796 }, + { url = "https://files.pythonhosted.org/packages/cd/8d/55fdf4387dec10aa177fe3df8dbb0d5022224d95f48664a21d6b62a5299d/rapidfuzz-3.13.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9f5fe634c9482ec5d4a6692afb8c45d370ae86755e5f57aa6c50bfe4ca2bdd87", size = 2464016 }, + { url = "https://files.pythonhosted.org/packages/9b/be/0872f6a56c0f473165d3b47d4170fa75263dc5f46985755aa9bf2bbcdea1/rapidfuzz-3.13.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:694eb531889f71022b2be86f625a4209c4049e74be9ca836919b9e395d5e33b3", size = 7556725 }, + { url = "https://files.pythonhosted.org/packages/5d/f3/6c0750e484d885a14840c7a150926f425d524982aca989cdda0bb3bdfa57/rapidfuzz-3.13.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:11b47b40650e06147dee5e51a9c9ad73bb7b86968b6f7d30e503b9f8dd1292db", size = 2859052 }, + { url = "https://files.pythonhosted.org/packages/6f/98/5a3a14701b5eb330f444f7883c9840b43fb29c575e292e09c90a270a6e07/rapidfuzz-3.13.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:98b8107ff14f5af0243f27d236bcc6e1ef8e7e3b3c25df114e91e3a99572da73", size = 3390219 }, + { url = "https://files.pythonhosted.org/packages/e9/7d/f4642eaaeb474b19974332f2a58471803448be843033e5740965775760a5/rapidfuzz-3.13.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b836f486dba0aceb2551e838ff3f514a38ee72b015364f739e526d720fdb823a", size = 4377924 }, + { url = "https://files.pythonhosted.org/packages/8e/83/fa33f61796731891c3e045d0cbca4436a5c436a170e7f04d42c2423652c3/rapidfuzz-3.13.0-cp312-cp312-win32.whl", hash = "sha256:4671ee300d1818d7bdfd8fa0608580d7778ba701817216f0c17fb29e6b972514", size = 1823915 }, + { url = "https://files.pythonhosted.org/packages/03/25/5ee7ab6841ca668567d0897905eebc79c76f6297b73bf05957be887e9c74/rapidfuzz-3.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:6e2065f68fb1d0bf65adc289c1bdc45ba7e464e406b319d67bb54441a1b9da9e", size = 1616985 }, + { url = "https://files.pythonhosted.org/packages/76/5e/3f0fb88db396cb692aefd631e4805854e02120a2382723b90dcae720bcc6/rapidfuzz-3.13.0-cp312-cp312-win_arm64.whl", hash = "sha256:65cc97c2fc2c2fe23586599686f3b1ceeedeca8e598cfcc1b7e56dc8ca7e2aa7", size = 860116 }, + { url = "https://files.pythonhosted.org/packages/0a/76/606e71e4227790750f1646f3c5c873e18d6cfeb6f9a77b2b8c4dec8f0f66/rapidfuzz-3.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:09e908064d3684c541d312bd4c7b05acb99a2c764f6231bd507d4b4b65226c23", size = 1982282 }, + { url = "https://files.pythonhosted.org/packages/0a/f5/d0b48c6b902607a59fd5932a54e3518dae8223814db8349b0176e6e9444b/rapidfuzz-3.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:57c390336cb50d5d3bfb0cfe1467478a15733703af61f6dffb14b1cd312a6fae", size = 1439274 }, + { url = "https://files.pythonhosted.org/packages/59/cf/c3ac8c80d8ced6c1f99b5d9674d397ce5d0e9d0939d788d67c010e19c65f/rapidfuzz-3.13.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0da54aa8547b3c2c188db3d1c7eb4d1bb6dd80baa8cdaeaec3d1da3346ec9caa", size = 1399854 }, + { url = "https://files.pythonhosted.org/packages/09/5d/ca8698e452b349c8313faf07bfa84e7d1c2d2edf7ccc67bcfc49bee1259a/rapidfuzz-3.13.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:df8e8c21e67afb9d7fbe18f42c6111fe155e801ab103c81109a61312927cc611", size = 5308962 }, + { url = "https://files.pythonhosted.org/packages/66/0a/bebada332854e78e68f3d6c05226b23faca79d71362509dbcf7b002e33b7/rapidfuzz-3.13.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:461fd13250a2adf8e90ca9a0e1e166515cbcaa5e9c3b1f37545cbbeff9e77f6b", size = 1625016 }, + { url = "https://files.pythonhosted.org/packages/de/0c/9e58d4887b86d7121d1c519f7050d1be5eb189d8a8075f5417df6492b4f5/rapidfuzz-3.13.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c2b3dd5d206a12deca16870acc0d6e5036abeb70e3cad6549c294eff15591527", size = 1600414 }, + { url = "https://files.pythonhosted.org/packages/9b/df/6096bc669c1311568840bdcbb5a893edc972d1c8d2b4b4325c21d54da5b1/rapidfuzz-3.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1343d745fbf4688e412d8f398c6e6d6f269db99a54456873f232ba2e7aeb4939", size = 3053179 }, + { url = "https://files.pythonhosted.org/packages/f9/46/5179c583b75fce3e65a5cd79a3561bd19abd54518cb7c483a89b284bf2b9/rapidfuzz-3.13.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b1b065f370d54551dcc785c6f9eeb5bd517ae14c983d2784c064b3aa525896df", size = 2456856 }, + { url = "https://files.pythonhosted.org/packages/6b/64/e9804212e3286d027ac35bbb66603c9456c2bce23f823b67d2f5cabc05c1/rapidfuzz-3.13.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:11b125d8edd67e767b2295eac6eb9afe0b1cdc82ea3d4b9257da4b8e06077798", size = 7567107 }, + { url = "https://files.pythonhosted.org/packages/8a/f2/7d69e7bf4daec62769b11757ffc31f69afb3ce248947aadbb109fefd9f65/rapidfuzz-3.13.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c33f9c841630b2bb7e69a3fb5c84a854075bb812c47620978bddc591f764da3d", size = 2854192 }, + { url = "https://files.pythonhosted.org/packages/05/21/ab4ad7d7d0f653e6fe2e4ccf11d0245092bef94cdff587a21e534e57bda8/rapidfuzz-3.13.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ae4574cb66cf1e85d32bb7e9ec45af5409c5b3970b7ceb8dea90168024127566", size = 3398876 }, + { url = "https://files.pythonhosted.org/packages/0f/a8/45bba94c2489cb1ee0130dcb46e1df4fa2c2b25269e21ffd15240a80322b/rapidfuzz-3.13.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e05752418b24bbd411841b256344c26f57da1148c5509e34ea39c7eb5099ab72", size = 4377077 }, + { url = "https://files.pythonhosted.org/packages/0c/f3/5e0c6ae452cbb74e5436d3445467447e8c32f3021f48f93f15934b8cffc2/rapidfuzz-3.13.0-cp313-cp313-win32.whl", hash = "sha256:0e1d08cb884805a543f2de1f6744069495ef527e279e05370dd7c83416af83f8", size = 1822066 }, + { url = "https://files.pythonhosted.org/packages/96/e3/a98c25c4f74051df4dcf2f393176b8663bfd93c7afc6692c84e96de147a2/rapidfuzz-3.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:9a7c6232be5f809cd39da30ee5d24e6cadd919831e6020ec6c2391f4c3bc9264", size = 1615100 }, + { url = "https://files.pythonhosted.org/packages/60/b1/05cd5e697c00cd46d7791915f571b38c8531f714832eff2c5e34537c49ee/rapidfuzz-3.13.0-cp313-cp313-win_arm64.whl", hash = "sha256:3f32f15bacd1838c929b35c84b43618481e1b3d7a61b5ed2db0291b70ae88b53", size = 858976 }, + { url = "https://files.pythonhosted.org/packages/d5/e1/f5d85ae3c53df6f817ca70dbdd37c83f31e64caced5bb867bec6b43d1fdf/rapidfuzz-3.13.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:fe5790a36d33a5d0a6a1f802aa42ecae282bf29ac6f7506d8e12510847b82a45", size = 1904437 }, + { url = "https://files.pythonhosted.org/packages/db/d7/ded50603dddc5eb182b7ce547a523ab67b3bf42b89736f93a230a398a445/rapidfuzz-3.13.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:cdb33ee9f8a8e4742c6b268fa6bd739024f34651a06b26913381b1413ebe7590", size = 1383126 }, + { url = "https://files.pythonhosted.org/packages/c4/48/6f795e793babb0120b63a165496d64f989b9438efbeed3357d9a226ce575/rapidfuzz-3.13.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c99b76b93f7b495eee7dcb0d6a38fb3ce91e72e99d9f78faa5664a881cb2b7d", size = 1365565 }, + { url = "https://files.pythonhosted.org/packages/f0/50/0062a959a2d72ed17815824e40e2eefdb26f6c51d627389514510a7875f3/rapidfuzz-3.13.0-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6af42f2ede8b596a6aaf6d49fdee3066ca578f4856b85ab5c1e2145de367a12d", size = 5251719 }, + { url = "https://files.pythonhosted.org/packages/e7/02/bd8b70cd98b7a88e1621264778ac830c9daa7745cd63e838bd773b1aeebd/rapidfuzz-3.13.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c0efa73afbc5b265aca0d8a467ae2a3f40d6854cbe1481cb442a62b7bf23c99", size = 2991095 }, + { url = "https://files.pythonhosted.org/packages/9f/8d/632d895cdae8356826184864d74a5f487d40cb79f50a9137510524a1ba86/rapidfuzz-3.13.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7ac21489de962a4e2fc1e8f0b0da4aa1adc6ab9512fd845563fecb4b4c52093a", size = 1553888 }, + { url = "https://files.pythonhosted.org/packages/88/df/6060c5a9c879b302bd47a73fc012d0db37abf6544c57591bcbc3459673bd/rapidfuzz-3.13.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1ba007f4d35a45ee68656b2eb83b8715e11d0f90e5b9f02d615a8a321ff00c27", size = 1905935 }, + { url = "https://files.pythonhosted.org/packages/a2/6c/a0b819b829e20525ef1bd58fc776fb8d07a0c38d819e63ba2b7c311a2ed4/rapidfuzz-3.13.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d7a217310429b43be95b3b8ad7f8fc41aba341109dc91e978cd7c703f928c58f", size = 1383714 }, + { url = "https://files.pythonhosted.org/packages/6a/c1/3da3466cc8a9bfb9cd345ad221fac311143b6a9664b5af4adb95b5e6ce01/rapidfuzz-3.13.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:558bf526bcd777de32b7885790a95a9548ffdcce68f704a81207be4a286c1095", size = 1367329 }, + { url = "https://files.pythonhosted.org/packages/da/f0/9f2a9043bfc4e66da256b15d728c5fc2d865edf0028824337f5edac36783/rapidfuzz-3.13.0-pp311-pypy311_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:202a87760f5145140d56153b193a797ae9338f7939eb16652dd7ff96f8faf64c", size = 5251057 }, + { url = "https://files.pythonhosted.org/packages/6a/ff/af2cb1d8acf9777d52487af5c6b34ce9d13381a753f991d95ecaca813407/rapidfuzz-3.13.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cfcccc08f671646ccb1e413c773bb92e7bba789e3a1796fd49d23c12539fe2e4", size = 2992401 }, + { url = "https://files.pythonhosted.org/packages/c1/c5/c243b05a15a27b946180db0d1e4c999bef3f4221505dff9748f1f6c917be/rapidfuzz-3.13.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:1f219f1e3c3194d7a7de222f54450ce12bc907862ff9a8962d83061c1f923c86", size = 1553782 }, +] + +[[package]] +name = "referencing" +version = "0.36.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "rpds-py" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2f/db/98b5c277be99dd18bfd91dd04e1b759cad18d1a338188c936e92f921c7e2/referencing-0.36.2.tar.gz", hash = "sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa", size = 74744 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/b1/3baf80dc6d2b7bc27a95a67752d0208e410351e3feb4eb78de5f77454d8d/referencing-0.36.2-py3-none-any.whl", hash = "sha256:e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0", size = 26775 }, +] + +[[package]] +name = "regex" +version = "2025.7.34" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/de/e13fa6dc61d78b30ba47481f99933a3b49a57779d625c392d8036770a60d/regex-2025.7.34.tar.gz", hash = "sha256:9ead9765217afd04a86822dfcd4ed2747dfe426e887da413b15ff0ac2457e21a", size = 400714 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/50/d2/0a44a9d92370e5e105f16669acf801b215107efea9dea4317fe96e9aad67/regex-2025.7.34-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d856164d25e2b3b07b779bfed813eb4b6b6ce73c2fd818d46f47c1eb5cd79bd6", size = 484591 }, + { url = "https://files.pythonhosted.org/packages/2e/b1/00c4f83aa902f1048495de9f2f33638ce970ce1cf9447b477d272a0e22bb/regex-2025.7.34-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2d15a9da5fad793e35fb7be74eec450d968e05d2e294f3e0e77ab03fa7234a83", size = 289293 }, + { url = "https://files.pythonhosted.org/packages/f3/b0/5bc5c8ddc418e8be5530b43ae1f7c9303f43aeff5f40185c4287cf6732f2/regex-2025.7.34-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:95b4639c77d414efa93c8de14ce3f7965a94d007e068a94f9d4997bb9bd9c81f", size = 285932 }, + { url = "https://files.pythonhosted.org/packages/46/c7/a1a28d050b23665a5e1eeb4d7f13b83ea86f0bc018da7b8f89f86ff7f094/regex-2025.7.34-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d7de1ceed5a5f84f342ba4a9f4ae589524adf9744b2ee61b5da884b5b659834", size = 780361 }, + { url = "https://files.pythonhosted.org/packages/cb/0d/82e7afe7b2c9fe3d488a6ab6145d1d97e55f822dfb9b4569aba2497e3d09/regex-2025.7.34-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:02e5860a250cd350c4933cf376c3bc9cb28948e2c96a8bc042aee7b985cfa26f", size = 849176 }, + { url = "https://files.pythonhosted.org/packages/bf/16/3036e16903d8194f1490af457a7e33b06d9e9edd9576b1fe6c7ac660e9ed/regex-2025.7.34-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0a5966220b9a1a88691282b7e4350e9599cf65780ca60d914a798cb791aa1177", size = 897222 }, + { url = "https://files.pythonhosted.org/packages/5a/c2/010e089ae00d31418e7d2c6601760eea1957cde12be719730c7133b8c165/regex-2025.7.34-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:48fb045bbd4aab2418dc1ba2088a5e32de4bfe64e1457b948bb328a8dc2f1c2e", size = 789831 }, + { url = "https://files.pythonhosted.org/packages/dd/86/b312b7bf5c46d21dbd9a3fdc4a80fde56ea93c9c0b89cf401879635e094d/regex-2025.7.34-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:20ff8433fa45e131f7316594efe24d4679c5449c0ca69d91c2f9d21846fdf064", size = 780665 }, + { url = "https://files.pythonhosted.org/packages/40/e5/674b82bfff112c820b09e3c86a423d4a568143ede7f8440fdcbce259e895/regex-2025.7.34-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c436fd1e95c04c19039668cfb548450a37c13f051e8659f40aed426e36b3765f", size = 773511 }, + { url = "https://files.pythonhosted.org/packages/2d/18/39e7c578eb6cf1454db2b64e4733d7e4f179714867a75d84492ec44fa9b2/regex-2025.7.34-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:0b85241d3cfb9f8a13cefdfbd58a2843f208f2ed2c88181bf84e22e0c7fc066d", size = 843990 }, + { url = "https://files.pythonhosted.org/packages/b6/d9/522a6715aefe2f463dc60c68924abeeb8ab6893f01adf5720359d94ede8c/regex-2025.7.34-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:075641c94126b064c65ab86e7e71fc3d63e7ff1bea1fb794f0773c97cdad3a03", size = 834676 }, + { url = "https://files.pythonhosted.org/packages/59/53/c4d5284cb40543566542e24f1badc9f72af68d01db21e89e36e02292eee0/regex-2025.7.34-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:70645cad3407d103d1dbcb4841839d2946f7d36cf38acbd40120fee1682151e5", size = 778420 }, + { url = "https://files.pythonhosted.org/packages/ea/4a/b779a7707d4a44a7e6ee9d0d98e40b2a4de74d622966080e9c95e25e2d24/regex-2025.7.34-cp310-cp310-win32.whl", hash = "sha256:3b836eb4a95526b263c2a3359308600bd95ce7848ebd3c29af0c37c4f9627cd3", size = 263999 }, + { url = "https://files.pythonhosted.org/packages/ef/6e/33c7583f5427aa039c28bff7f4103c2de5b6aa5b9edc330c61ec576b1960/regex-2025.7.34-cp310-cp310-win_amd64.whl", hash = "sha256:cbfaa401d77334613cf434f723c7e8ba585df162be76474bccc53ae4e5520b3a", size = 276023 }, + { url = "https://files.pythonhosted.org/packages/9f/fc/00b32e0ac14213d76d806d952826402b49fd06d42bfabacdf5d5d016bc47/regex-2025.7.34-cp310-cp310-win_arm64.whl", hash = "sha256:bca11d3c38a47c621769433c47f364b44e8043e0de8e482c5968b20ab90a3986", size = 268357 }, + { url = "https://files.pythonhosted.org/packages/0d/85/f497b91577169472f7c1dc262a5ecc65e39e146fc3a52c571e5daaae4b7d/regex-2025.7.34-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:da304313761b8500b8e175eb2040c4394a875837d5635f6256d6fa0377ad32c8", size = 484594 }, + { url = "https://files.pythonhosted.org/packages/1c/c5/ad2a5c11ce9e6257fcbfd6cd965d07502f6054aaa19d50a3d7fd991ec5d1/regex-2025.7.34-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:35e43ebf5b18cd751ea81455b19acfdec402e82fe0dc6143edfae4c5c4b3909a", size = 289294 }, + { url = "https://files.pythonhosted.org/packages/8e/01/83ffd9641fcf5e018f9b51aa922c3e538ac9439424fda3df540b643ecf4f/regex-2025.7.34-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:96bbae4c616726f4661fe7bcad5952e10d25d3c51ddc388189d8864fbc1b3c68", size = 285933 }, + { url = "https://files.pythonhosted.org/packages/77/20/5edab2e5766f0259bc1da7381b07ce6eb4401b17b2254d02f492cd8a81a8/regex-2025.7.34-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9feab78a1ffa4f2b1e27b1bcdaad36f48c2fed4870264ce32f52a393db093c78", size = 792335 }, + { url = "https://files.pythonhosted.org/packages/30/bd/744d3ed8777dce8487b2606b94925e207e7c5931d5870f47f5b643a4580a/regex-2025.7.34-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f14b36e6d4d07f1a5060f28ef3b3561c5d95eb0651741474ce4c0a4c56ba8719", size = 858605 }, + { url = "https://files.pythonhosted.org/packages/99/3d/93754176289718d7578c31d151047e7b8acc7a8c20e7706716f23c49e45e/regex-2025.7.34-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:85c3a958ef8b3d5079c763477e1f09e89d13ad22198a37e9d7b26b4b17438b33", size = 905780 }, + { url = "https://files.pythonhosted.org/packages/ee/2e/c689f274a92deffa03999a430505ff2aeace408fd681a90eafa92fdd6930/regex-2025.7.34-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:37555e4ae0b93358fa7c2d240a4291d4a4227cc7c607d8f85596cdb08ec0a083", size = 798868 }, + { url = "https://files.pythonhosted.org/packages/0d/9e/39673688805d139b33b4a24851a71b9978d61915c4d72b5ffda324d0668a/regex-2025.7.34-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ee38926f31f1aa61b0232a3a11b83461f7807661c062df9eb88769d86e6195c3", size = 781784 }, + { url = "https://files.pythonhosted.org/packages/18/bd/4c1cab12cfabe14beaa076523056b8ab0c882a8feaf0a6f48b0a75dab9ed/regex-2025.7.34-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a664291c31cae9c4a30589bd8bc2ebb56ef880c9c6264cb7643633831e606a4d", size = 852837 }, + { url = "https://files.pythonhosted.org/packages/cb/21/663d983cbb3bba537fc213a579abbd0f263fb28271c514123f3c547ab917/regex-2025.7.34-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:f3e5c1e0925e77ec46ddc736b756a6da50d4df4ee3f69536ffb2373460e2dafd", size = 844240 }, + { url = "https://files.pythonhosted.org/packages/8e/2d/9beeeb913bc5d32faa913cf8c47e968da936af61ec20af5d269d0f84a100/regex-2025.7.34-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d428fc7731dcbb4e2ffe43aeb8f90775ad155e7db4347a639768bc6cd2df881a", size = 787139 }, + { url = "https://files.pythonhosted.org/packages/eb/f5/9b9384415fdc533551be2ba805dd8c4621873e5df69c958f403bfd3b2b6e/regex-2025.7.34-cp311-cp311-win32.whl", hash = "sha256:e154a7ee7fa18333ad90b20e16ef84daaeac61877c8ef942ec8dfa50dc38b7a1", size = 264019 }, + { url = "https://files.pythonhosted.org/packages/18/9d/e069ed94debcf4cc9626d652a48040b079ce34c7e4fb174f16874958d485/regex-2025.7.34-cp311-cp311-win_amd64.whl", hash = "sha256:24257953d5c1d6d3c129ab03414c07fc1a47833c9165d49b954190b2b7f21a1a", size = 276047 }, + { url = "https://files.pythonhosted.org/packages/fd/cf/3bafbe9d1fd1db77355e7fbbbf0d0cfb34501a8b8e334deca14f94c7b315/regex-2025.7.34-cp311-cp311-win_arm64.whl", hash = "sha256:3157aa512b9e606586900888cd469a444f9b898ecb7f8931996cb715f77477f0", size = 268362 }, + { url = "https://files.pythonhosted.org/packages/ff/f0/31d62596c75a33f979317658e8d261574785c6cd8672c06741ce2e2e2070/regex-2025.7.34-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:7f7211a746aced993bef487de69307a38c5ddd79257d7be83f7b202cb59ddb50", size = 485492 }, + { url = "https://files.pythonhosted.org/packages/d8/16/b818d223f1c9758c3434be89aa1a01aae798e0e0df36c1f143d1963dd1ee/regex-2025.7.34-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fb31080f2bd0681484b275461b202b5ad182f52c9ec606052020fe13eb13a72f", size = 290000 }, + { url = "https://files.pythonhosted.org/packages/cd/70/69506d53397b4bd6954061bae75677ad34deb7f6ca3ba199660d6f728ff5/regex-2025.7.34-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0200a5150c4cf61e407038f4b4d5cdad13e86345dac29ff9dab3d75d905cf130", size = 286072 }, + { url = "https://files.pythonhosted.org/packages/b0/73/536a216d5f66084fb577bb0543b5cb7de3272eb70a157f0c3a542f1c2551/regex-2025.7.34-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:739a74970e736df0773788377969c9fea3876c2fc13d0563f98e5503e5185f46", size = 797341 }, + { url = "https://files.pythonhosted.org/packages/26/af/733f8168449e56e8f404bb807ea7189f59507cbea1b67a7bbcd92f8bf844/regex-2025.7.34-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4fef81b2f7ea6a2029161ed6dea9ae13834c28eb5a95b8771828194a026621e4", size = 862556 }, + { url = "https://files.pythonhosted.org/packages/19/dd/59c464d58c06c4f7d87de4ab1f590e430821345a40c5d345d449a636d15f/regex-2025.7.34-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ea74cf81fe61a7e9d77989050d0089a927ab758c29dac4e8e1b6c06fccf3ebf0", size = 910762 }, + { url = "https://files.pythonhosted.org/packages/37/a8/b05ccf33ceca0815a1e253693b2c86544932ebcc0049c16b0fbdf18b688b/regex-2025.7.34-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e4636a7f3b65a5f340ed9ddf53585c42e3ff37101d383ed321bfe5660481744b", size = 801892 }, + { url = "https://files.pythonhosted.org/packages/5f/9a/b993cb2e634cc22810afd1652dba0cae156c40d4864285ff486c73cd1996/regex-2025.7.34-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6cef962d7834437fe8d3da6f9bfc6f93f20f218266dcefec0560ed7765f5fe01", size = 786551 }, + { url = "https://files.pythonhosted.org/packages/2d/79/7849d67910a0de4e26834b5bb816e028e35473f3d7ae563552ea04f58ca2/regex-2025.7.34-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:cbe1698e5b80298dbce8df4d8d1182279fbdaf1044e864cbc9d53c20e4a2be77", size = 856457 }, + { url = "https://files.pythonhosted.org/packages/91/c6/de516bc082524b27e45cb4f54e28bd800c01efb26d15646a65b87b13a91e/regex-2025.7.34-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:32b9f9bcf0f605eb094b08e8da72e44badabb63dde6b83bd530580b488d1c6da", size = 848902 }, + { url = "https://files.pythonhosted.org/packages/7d/22/519ff8ba15f732db099b126f039586bd372da6cd4efb810d5d66a5daeda1/regex-2025.7.34-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:524c868ba527eab4e8744a9287809579f54ae8c62fbf07d62aacd89f6026b282", size = 788038 }, + { url = "https://files.pythonhosted.org/packages/3f/7d/aabb467d8f57d8149895d133c88eb809a1a6a0fe262c1d508eb9dfabb6f9/regex-2025.7.34-cp312-cp312-win32.whl", hash = "sha256:d600e58ee6d036081c89696d2bdd55d507498a7180df2e19945c6642fac59588", size = 264417 }, + { url = "https://files.pythonhosted.org/packages/3b/39/bd922b55a4fc5ad5c13753274e5b536f5b06ec8eb9747675668491c7ab7a/regex-2025.7.34-cp312-cp312-win_amd64.whl", hash = "sha256:9a9ab52a466a9b4b91564437b36417b76033e8778e5af8f36be835d8cb370d62", size = 275387 }, + { url = "https://files.pythonhosted.org/packages/f7/3c/c61d2fdcecb754a40475a3d1ef9a000911d3e3fc75c096acf44b0dfb786a/regex-2025.7.34-cp312-cp312-win_arm64.whl", hash = "sha256:c83aec91af9c6fbf7c743274fd952272403ad9a9db05fe9bfc9df8d12b45f176", size = 268482 }, + { url = "https://files.pythonhosted.org/packages/15/16/b709b2119975035169a25aa8e4940ca177b1a2e25e14f8d996d09130368e/regex-2025.7.34-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c3c9740a77aeef3f5e3aaab92403946a8d34437db930a0280e7e81ddcada61f5", size = 485334 }, + { url = "https://files.pythonhosted.org/packages/94/a6/c09136046be0595f0331bc58a0e5f89c2d324cf734e0b0ec53cf4b12a636/regex-2025.7.34-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:69ed3bc611540f2ea70a4080f853741ec698be556b1df404599f8724690edbcd", size = 289942 }, + { url = "https://files.pythonhosted.org/packages/36/91/08fc0fd0f40bdfb0e0df4134ee37cfb16e66a1044ac56d36911fd01c69d2/regex-2025.7.34-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d03c6f9dcd562c56527c42b8530aad93193e0b3254a588be1f2ed378cdfdea1b", size = 285991 }, + { url = "https://files.pythonhosted.org/packages/be/2f/99dc8f6f756606f0c214d14c7b6c17270b6bbe26d5c1f05cde9dbb1c551f/regex-2025.7.34-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6164b1d99dee1dfad33f301f174d8139d4368a9fb50bf0a3603b2eaf579963ad", size = 797415 }, + { url = "https://files.pythonhosted.org/packages/62/cf/2fcdca1110495458ba4e95c52ce73b361cf1cafd8a53b5c31542cde9a15b/regex-2025.7.34-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1e4f4f62599b8142362f164ce776f19d79bdd21273e86920a7b604a4275b4f59", size = 862487 }, + { url = "https://files.pythonhosted.org/packages/90/38/899105dd27fed394e3fae45607c1983e138273ec167e47882fc401f112b9/regex-2025.7.34-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:72a26dcc6a59c057b292f39d41465d8233a10fd69121fa24f8f43ec6294e5415", size = 910717 }, + { url = "https://files.pythonhosted.org/packages/ee/f6/4716198dbd0bcc9c45625ac4c81a435d1c4d8ad662e8576dac06bab35b17/regex-2025.7.34-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5273fddf7a3e602695c92716c420c377599ed3c853ea669c1fe26218867002f", size = 801943 }, + { url = "https://files.pythonhosted.org/packages/40/5d/cff8896d27e4e3dd11dd72ac78797c7987eb50fe4debc2c0f2f1682eb06d/regex-2025.7.34-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c1844be23cd40135b3a5a4dd298e1e0c0cb36757364dd6cdc6025770363e06c1", size = 786664 }, + { url = "https://files.pythonhosted.org/packages/10/29/758bf83cf7b4c34f07ac3423ea03cee3eb3176941641e4ccc05620f6c0b8/regex-2025.7.34-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:dde35e2afbbe2272f8abee3b9fe6772d9b5a07d82607b5788e8508974059925c", size = 856457 }, + { url = "https://files.pythonhosted.org/packages/d7/30/c19d212b619963c5b460bfed0ea69a092c6a43cba52a973d46c27b3e2975/regex-2025.7.34-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:f3f6e8e7af516a7549412ce57613e859c3be27d55341a894aacaa11703a4c31a", size = 849008 }, + { url = "https://files.pythonhosted.org/packages/9e/b8/3c35da3b12c87e3cc00010ef6c3a4ae787cff0bc381aa3d251def219969a/regex-2025.7.34-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:469142fb94a869beb25b5f18ea87646d21def10fbacb0bcb749224f3509476f0", size = 788101 }, + { url = "https://files.pythonhosted.org/packages/47/80/2f46677c0b3c2b723b2c358d19f9346e714113865da0f5f736ca1a883bde/regex-2025.7.34-cp313-cp313-win32.whl", hash = "sha256:da7507d083ee33ccea1310447410c27ca11fb9ef18c95899ca57ff60a7e4d8f1", size = 264401 }, + { url = "https://files.pythonhosted.org/packages/be/fa/917d64dd074682606a003cba33585c28138c77d848ef72fc77cbb1183849/regex-2025.7.34-cp313-cp313-win_amd64.whl", hash = "sha256:9d644de5520441e5f7e2db63aec2748948cc39ed4d7a87fd5db578ea4043d997", size = 275368 }, + { url = "https://files.pythonhosted.org/packages/65/cd/f94383666704170a2154a5df7b16be28f0c27a266bffcd843e58bc84120f/regex-2025.7.34-cp313-cp313-win_arm64.whl", hash = "sha256:7bf1c5503a9f2cbd2f52d7e260acb3131b07b6273c470abb78568174fe6bde3f", size = 268482 }, +] + +[[package]] +name = "requests" +version = "2.32.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e1/0a/929373653770d8a0d7ea76c37de6e41f11eb07559b103b1c02cafb3f7cf8/requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422", size = 135258 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847 }, +] + +[[package]] +name = "requests-oauthlib" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "oauthlib" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/f2/05f29bc3913aea15eb670be136045bf5c5bbf4b99ecb839da9b422bb2c85/requests-oauthlib-2.0.0.tar.gz", hash = "sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9", size = 55650 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/5d/63d4ae3b9daea098d5d6f5da83984853c1bbacd5dc826764b249fe119d24/requests_oauthlib-2.0.0-py2.py3-none-any.whl", hash = "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36", size = 24179 }, +] + +[[package]] +name = "requests-toolbelt" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/61/d7545dafb7ac2230c70d38d31cbfe4cc64f7144dc41f6e4e4b78ecd9f5bb/requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", size = 206888 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06", size = 54481 }, +] + +[[package]] +name = "rich" +version = "13.9.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ab/3a/0316b28d0761c6734d6bc14e770d85506c986c85ffb239e688eeaab2c2bc/rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098", size = 223149 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/19/71/39c7c0d87f8d4e6c020a393182060eaefeeae6c01dab6a84ec346f2567df/rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90", size = 242424 }, +] + +[[package]] +name = "rpds-py" +version = "0.26.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a5/aa/4456d84bbb54adc6a916fb10c9b374f78ac840337644e4a5eda229c81275/rpds_py-0.26.0.tar.gz", hash = "sha256:20dae58a859b0906f0685642e591056f1e787f3a8b39c8e8749a45dc7d26bdb0", size = 27385 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/31/1459645f036c3dfeacef89e8e5825e430c77dde8489f3b99eaafcd4a60f5/rpds_py-0.26.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:4c70c70f9169692b36307a95f3d8c0a9fcd79f7b4a383aad5eaa0e9718b79b37", size = 372466 }, + { url = "https://files.pythonhosted.org/packages/dd/ff/3d0727f35836cc8773d3eeb9a46c40cc405854e36a8d2e951f3a8391c976/rpds_py-0.26.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:777c62479d12395bfb932944e61e915741e364c843afc3196b694db3d669fcd0", size = 357825 }, + { url = "https://files.pythonhosted.org/packages/bf/ce/badc5e06120a54099ae287fa96d82cbb650a5f85cf247ffe19c7b157fd1f/rpds_py-0.26.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec671691e72dff75817386aa02d81e708b5a7ec0dec6669ec05213ff6b77e1bd", size = 381530 }, + { url = "https://files.pythonhosted.org/packages/1e/a5/fa5d96a66c95d06c62d7a30707b6a4cfec696ab8ae280ee7be14e961e118/rpds_py-0.26.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6a1cb5d6ce81379401bbb7f6dbe3d56de537fb8235979843f0d53bc2e9815a79", size = 396933 }, + { url = "https://files.pythonhosted.org/packages/00/a7/7049d66750f18605c591a9db47d4a059e112a0c9ff8de8daf8fa0f446bba/rpds_py-0.26.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4f789e32fa1fb6a7bf890e0124e7b42d1e60d28ebff57fe806719abb75f0e9a3", size = 513973 }, + { url = "https://files.pythonhosted.org/packages/0e/f1/528d02c7d6b29d29fac8fd784b354d3571cc2153f33f842599ef0cf20dd2/rpds_py-0.26.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c55b0a669976cf258afd718de3d9ad1b7d1fe0a91cd1ab36f38b03d4d4aeaaf", size = 402293 }, + { url = "https://files.pythonhosted.org/packages/15/93/fde36cd6e4685df2cd08508f6c45a841e82f5bb98c8d5ecf05649522acb5/rpds_py-0.26.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c70d9ec912802ecfd6cd390dadb34a9578b04f9bcb8e863d0a7598ba5e9e7ccc", size = 383787 }, + { url = "https://files.pythonhosted.org/packages/69/f2/5007553aaba1dcae5d663143683c3dfd03d9395289f495f0aebc93e90f24/rpds_py-0.26.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3021933c2cb7def39d927b9862292e0f4c75a13d7de70eb0ab06efed4c508c19", size = 416312 }, + { url = "https://files.pythonhosted.org/packages/8f/a7/ce52c75c1e624a79e48a69e611f1c08844564e44c85db2b6f711d76d10ce/rpds_py-0.26.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8a7898b6ca3b7d6659e55cdac825a2e58c638cbf335cde41f4619e290dd0ad11", size = 558403 }, + { url = "https://files.pythonhosted.org/packages/79/d5/e119db99341cc75b538bf4cb80504129fa22ce216672fb2c28e4a101f4d9/rpds_py-0.26.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:12bff2ad9447188377f1b2794772f91fe68bb4bbfa5a39d7941fbebdbf8c500f", size = 588323 }, + { url = "https://files.pythonhosted.org/packages/93/94/d28272a0b02f5fe24c78c20e13bbcb95f03dc1451b68e7830ca040c60bd6/rpds_py-0.26.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:191aa858f7d4902e975d4cf2f2d9243816c91e9605070aeb09c0a800d187e323", size = 554541 }, + { url = "https://files.pythonhosted.org/packages/93/e0/8c41166602f1b791da892d976057eba30685486d2e2c061ce234679c922b/rpds_py-0.26.0-cp310-cp310-win32.whl", hash = "sha256:b37a04d9f52cb76b6b78f35109b513f6519efb481d8ca4c321f6a3b9580b3f45", size = 220442 }, + { url = "https://files.pythonhosted.org/packages/87/f0/509736bb752a7ab50fb0270c2a4134d671a7b3038030837e5536c3de0e0b/rpds_py-0.26.0-cp310-cp310-win_amd64.whl", hash = "sha256:38721d4c9edd3eb6670437d8d5e2070063f305bfa2d5aa4278c51cedcd508a84", size = 231314 }, + { url = "https://files.pythonhosted.org/packages/09/4c/4ee8f7e512030ff79fda1df3243c88d70fc874634e2dbe5df13ba4210078/rpds_py-0.26.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:9e8cb77286025bdb21be2941d64ac6ca016130bfdcd228739e8ab137eb4406ed", size = 372610 }, + { url = "https://files.pythonhosted.org/packages/fa/9d/3dc16be00f14fc1f03c71b1d67c8df98263ab2710a2fbd65a6193214a527/rpds_py-0.26.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5e09330b21d98adc8ccb2dbb9fc6cb434e8908d4c119aeaa772cb1caab5440a0", size = 358032 }, + { url = "https://files.pythonhosted.org/packages/e7/5a/7f1bf8f045da2866324a08ae80af63e64e7bfaf83bd31f865a7b91a58601/rpds_py-0.26.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c9c1b92b774b2e68d11193dc39620d62fd8ab33f0a3c77ecdabe19c179cdbc1", size = 381525 }, + { url = "https://files.pythonhosted.org/packages/45/8a/04479398c755a066ace10e3d158866beb600867cacae194c50ffa783abd0/rpds_py-0.26.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:824e6d3503ab990d7090768e4dfd9e840837bae057f212ff9f4f05ec6d1975e7", size = 397089 }, + { url = "https://files.pythonhosted.org/packages/72/88/9203f47268db488a1b6d469d69c12201ede776bb728b9d9f29dbfd7df406/rpds_py-0.26.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8ad7fd2258228bf288f2331f0a6148ad0186b2e3643055ed0db30990e59817a6", size = 514255 }, + { url = "https://files.pythonhosted.org/packages/f5/b4/01ce5d1e853ddf81fbbd4311ab1eff0b3cf162d559288d10fd127e2588b5/rpds_py-0.26.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0dc23bbb3e06ec1ea72d515fb572c1fea59695aefbffb106501138762e1e915e", size = 402283 }, + { url = "https://files.pythonhosted.org/packages/34/a2/004c99936997bfc644d590a9defd9e9c93f8286568f9c16cdaf3e14429a7/rpds_py-0.26.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d80bf832ac7b1920ee29a426cdca335f96a2b5caa839811803e999b41ba9030d", size = 383881 }, + { url = "https://files.pythonhosted.org/packages/05/1b/ef5fba4a8f81ce04c427bfd96223f92f05e6cd72291ce9d7523db3b03a6c/rpds_py-0.26.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0919f38f5542c0a87e7b4afcafab6fd2c15386632d249e9a087498571250abe3", size = 415822 }, + { url = "https://files.pythonhosted.org/packages/16/80/5c54195aec456b292f7bd8aa61741c8232964063fd8a75fdde9c1e982328/rpds_py-0.26.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d422b945683e409000c888e384546dbab9009bb92f7c0b456e217988cf316107", size = 558347 }, + { url = "https://files.pythonhosted.org/packages/f2/1c/1845c1b1fd6d827187c43afe1841d91678d7241cbdb5420a4c6de180a538/rpds_py-0.26.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:77a7711fa562ba2da1aa757e11024ad6d93bad6ad7ede5afb9af144623e5f76a", size = 587956 }, + { url = "https://files.pythonhosted.org/packages/2e/ff/9e979329dd131aa73a438c077252ddabd7df6d1a7ad7b9aacf6261f10faa/rpds_py-0.26.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:238e8c8610cb7c29460e37184f6799547f7e09e6a9bdbdab4e8edb90986a2318", size = 554363 }, + { url = "https://files.pythonhosted.org/packages/00/8b/d78cfe034b71ffbe72873a136e71acc7a831a03e37771cfe59f33f6de8a2/rpds_py-0.26.0-cp311-cp311-win32.whl", hash = "sha256:893b022bfbdf26d7bedb083efeea624e8550ca6eb98bf7fea30211ce95b9201a", size = 220123 }, + { url = "https://files.pythonhosted.org/packages/94/c1/3c8c94c7dd3905dbfde768381ce98778500a80db9924731d87ddcdb117e9/rpds_py-0.26.0-cp311-cp311-win_amd64.whl", hash = "sha256:87a5531de9f71aceb8af041d72fc4cab4943648d91875ed56d2e629bef6d4c03", size = 231732 }, + { url = "https://files.pythonhosted.org/packages/67/93/e936fbed1b734eabf36ccb5d93c6a2e9246fbb13c1da011624b7286fae3e/rpds_py-0.26.0-cp311-cp311-win_arm64.whl", hash = "sha256:de2713f48c1ad57f89ac25b3cb7daed2156d8e822cf0eca9b96a6f990718cc41", size = 221917 }, + { url = "https://files.pythonhosted.org/packages/ea/86/90eb87c6f87085868bd077c7a9938006eb1ce19ed4d06944a90d3560fce2/rpds_py-0.26.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:894514d47e012e794f1350f076c427d2347ebf82f9b958d554d12819849a369d", size = 363933 }, + { url = "https://files.pythonhosted.org/packages/63/78/4469f24d34636242c924626082b9586f064ada0b5dbb1e9d096ee7a8e0c6/rpds_py-0.26.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc921b96fa95a097add244da36a1d9e4f3039160d1d30f1b35837bf108c21136", size = 350447 }, + { url = "https://files.pythonhosted.org/packages/ad/91/c448ed45efdfdade82348d5e7995e15612754826ea640afc20915119734f/rpds_py-0.26.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e1157659470aa42a75448b6e943c895be8c70531c43cb78b9ba990778955582", size = 384711 }, + { url = "https://files.pythonhosted.org/packages/ec/43/e5c86fef4be7f49828bdd4ecc8931f0287b1152c0bb0163049b3218740e7/rpds_py-0.26.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:521ccf56f45bb3a791182dc6b88ae5f8fa079dd705ee42138c76deb1238e554e", size = 400865 }, + { url = "https://files.pythonhosted.org/packages/55/34/e00f726a4d44f22d5c5fe2e5ddd3ac3d7fd3f74a175607781fbdd06fe375/rpds_py-0.26.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9def736773fd56b305c0eef698be5192c77bfa30d55a0e5885f80126c4831a15", size = 517763 }, + { url = "https://files.pythonhosted.org/packages/52/1c/52dc20c31b147af724b16104500fba13e60123ea0334beba7b40e33354b4/rpds_py-0.26.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cdad4ea3b4513b475e027be79e5a0ceac8ee1c113a1a11e5edc3c30c29f964d8", size = 406651 }, + { url = "https://files.pythonhosted.org/packages/2e/77/87d7bfabfc4e821caa35481a2ff6ae0b73e6a391bb6b343db2c91c2b9844/rpds_py-0.26.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82b165b07f416bdccf5c84546a484cc8f15137ca38325403864bfdf2b5b72f6a", size = 386079 }, + { url = "https://files.pythonhosted.org/packages/e3/d4/7f2200c2d3ee145b65b3cddc4310d51f7da6a26634f3ac87125fd789152a/rpds_py-0.26.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d04cab0a54b9dba4d278fe955a1390da3cf71f57feb78ddc7cb67cbe0bd30323", size = 421379 }, + { url = "https://files.pythonhosted.org/packages/ae/13/9fdd428b9c820869924ab62236b8688b122baa22d23efdd1c566938a39ba/rpds_py-0.26.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:79061ba1a11b6a12743a2b0f72a46aa2758613d454aa6ba4f5a265cc48850158", size = 562033 }, + { url = "https://files.pythonhosted.org/packages/f3/e1/b69686c3bcbe775abac3a4c1c30a164a2076d28df7926041f6c0eb5e8d28/rpds_py-0.26.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:f405c93675d8d4c5ac87364bb38d06c988e11028a64b52a47158a355079661f3", size = 591639 }, + { url = "https://files.pythonhosted.org/packages/5c/c9/1e3d8c8863c84a90197ac577bbc3d796a92502124c27092413426f670990/rpds_py-0.26.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dafd4c44b74aa4bed4b250f1aed165b8ef5de743bcca3b88fc9619b6087093d2", size = 557105 }, + { url = "https://files.pythonhosted.org/packages/9f/c5/90c569649057622959f6dcc40f7b516539608a414dfd54b8d77e3b201ac0/rpds_py-0.26.0-cp312-cp312-win32.whl", hash = "sha256:3da5852aad63fa0c6f836f3359647870e21ea96cf433eb393ffa45263a170d44", size = 223272 }, + { url = "https://files.pythonhosted.org/packages/7d/16/19f5d9f2a556cfed454eebe4d354c38d51c20f3db69e7b4ce6cff904905d/rpds_py-0.26.0-cp312-cp312-win_amd64.whl", hash = "sha256:cf47cfdabc2194a669dcf7a8dbba62e37a04c5041d2125fae0233b720da6f05c", size = 234995 }, + { url = "https://files.pythonhosted.org/packages/83/f0/7935e40b529c0e752dfaa7880224771b51175fce08b41ab4a92eb2fbdc7f/rpds_py-0.26.0-cp312-cp312-win_arm64.whl", hash = "sha256:20ab1ae4fa534f73647aad289003f1104092890849e0266271351922ed5574f8", size = 223198 }, + { url = "https://files.pythonhosted.org/packages/6a/67/bb62d0109493b12b1c6ab00de7a5566aa84c0e44217c2d94bee1bd370da9/rpds_py-0.26.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:696764a5be111b036256c0b18cd29783fab22154690fc698062fc1b0084b511d", size = 363917 }, + { url = "https://files.pythonhosted.org/packages/4b/f3/34e6ae1925a5706c0f002a8d2d7f172373b855768149796af87bd65dcdb9/rpds_py-0.26.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1e6c15d2080a63aaed876e228efe4f814bc7889c63b1e112ad46fdc8b368b9e1", size = 350073 }, + { url = "https://files.pythonhosted.org/packages/75/83/1953a9d4f4e4de7fd0533733e041c28135f3c21485faaef56a8aadbd96b5/rpds_py-0.26.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:390e3170babf42462739a93321e657444f0862c6d722a291accc46f9d21ed04e", size = 384214 }, + { url = "https://files.pythonhosted.org/packages/48/0e/983ed1b792b3322ea1d065e67f4b230f3b96025f5ce3878cc40af09b7533/rpds_py-0.26.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7da84c2c74c0f5bc97d853d9e17bb83e2dcafcff0dc48286916001cc114379a1", size = 400113 }, + { url = "https://files.pythonhosted.org/packages/69/7f/36c0925fff6f660a80be259c5b4f5e53a16851f946eb080351d057698528/rpds_py-0.26.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4c5fe114a6dd480a510b6d3661d09d67d1622c4bf20660a474507aaee7eeeee9", size = 515189 }, + { url = "https://files.pythonhosted.org/packages/13/45/cbf07fc03ba7a9b54662c9badb58294ecfb24f828b9732970bd1a431ed5c/rpds_py-0.26.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3100b3090269f3a7ea727b06a6080d4eb7439dca4c0e91a07c5d133bb1727ea7", size = 406998 }, + { url = "https://files.pythonhosted.org/packages/6c/b0/8fa5e36e58657997873fd6a1cf621285ca822ca75b4b3434ead047daa307/rpds_py-0.26.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c03c9b0c64afd0320ae57de4c982801271c0c211aa2d37f3003ff5feb75bb04", size = 385903 }, + { url = "https://files.pythonhosted.org/packages/4b/f7/b25437772f9f57d7a9fbd73ed86d0dcd76b4c7c6998348c070d90f23e315/rpds_py-0.26.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5963b72ccd199ade6ee493723d18a3f21ba7d5b957017607f815788cef50eaf1", size = 419785 }, + { url = "https://files.pythonhosted.org/packages/a7/6b/63ffa55743dfcb4baf2e9e77a0b11f7f97ed96a54558fcb5717a4b2cd732/rpds_py-0.26.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9da4e873860ad5bab3291438525cae80169daecbfafe5657f7f5fb4d6b3f96b9", size = 561329 }, + { url = "https://files.pythonhosted.org/packages/2f/07/1f4f5e2886c480a2346b1e6759c00278b8a69e697ae952d82ae2e6ee5db0/rpds_py-0.26.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:5afaddaa8e8c7f1f7b4c5c725c0070b6eed0228f705b90a1732a48e84350f4e9", size = 590875 }, + { url = "https://files.pythonhosted.org/packages/cc/bc/e6639f1b91c3a55f8c41b47d73e6307051b6e246254a827ede730624c0f8/rpds_py-0.26.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4916dc96489616a6f9667e7526af8fa693c0fdb4f3acb0e5d9f4400eb06a47ba", size = 556636 }, + { url = "https://files.pythonhosted.org/packages/05/4c/b3917c45566f9f9a209d38d9b54a1833f2bb1032a3e04c66f75726f28876/rpds_py-0.26.0-cp313-cp313-win32.whl", hash = "sha256:2a343f91b17097c546b93f7999976fd6c9d5900617aa848c81d794e062ab302b", size = 222663 }, + { url = "https://files.pythonhosted.org/packages/e0/0b/0851bdd6025775aaa2365bb8de0697ee2558184c800bfef8d7aef5ccde58/rpds_py-0.26.0-cp313-cp313-win_amd64.whl", hash = "sha256:0a0b60701f2300c81b2ac88a5fb893ccfa408e1c4a555a77f908a2596eb875a5", size = 234428 }, + { url = "https://files.pythonhosted.org/packages/ed/e8/a47c64ed53149c75fb581e14a237b7b7cd18217e969c30d474d335105622/rpds_py-0.26.0-cp313-cp313-win_arm64.whl", hash = "sha256:257d011919f133a4746958257f2c75238e3ff54255acd5e3e11f3ff41fd14256", size = 222571 }, + { url = "https://files.pythonhosted.org/packages/89/bf/3d970ba2e2bcd17d2912cb42874107390f72873e38e79267224110de5e61/rpds_py-0.26.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:529c8156d7506fba5740e05da8795688f87119cce330c244519cf706a4a3d618", size = 360475 }, + { url = "https://files.pythonhosted.org/packages/82/9f/283e7e2979fc4ec2d8ecee506d5a3675fce5ed9b4b7cb387ea5d37c2f18d/rpds_py-0.26.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f53ec51f9d24e9638a40cabb95078ade8c99251945dad8d57bf4aabe86ecee35", size = 346692 }, + { url = "https://files.pythonhosted.org/packages/e3/03/7e50423c04d78daf391da3cc4330bdb97042fc192a58b186f2d5deb7befd/rpds_py-0.26.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ab504c4d654e4a29558eaa5bb8cea5fdc1703ea60a8099ffd9c758472cf913f", size = 379415 }, + { url = "https://files.pythonhosted.org/packages/57/00/d11ee60d4d3b16808432417951c63df803afb0e0fc672b5e8d07e9edaaae/rpds_py-0.26.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fd0641abca296bc1a00183fe44f7fced8807ed49d501f188faa642d0e4975b83", size = 391783 }, + { url = "https://files.pythonhosted.org/packages/08/b3/1069c394d9c0d6d23c5b522e1f6546b65793a22950f6e0210adcc6f97c3e/rpds_py-0.26.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:69b312fecc1d017b5327afa81d4da1480f51c68810963a7336d92203dbb3d4f1", size = 512844 }, + { url = "https://files.pythonhosted.org/packages/08/3b/c4fbf0926800ed70b2c245ceca99c49f066456755f5d6eb8863c2c51e6d0/rpds_py-0.26.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c741107203954f6fc34d3066d213d0a0c40f7bb5aafd698fb39888af277c70d8", size = 402105 }, + { url = "https://files.pythonhosted.org/packages/1c/b0/db69b52ca07413e568dae9dc674627a22297abb144c4d6022c6d78f1e5cc/rpds_py-0.26.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc3e55a7db08dc9a6ed5fb7103019d2c1a38a349ac41901f9f66d7f95750942f", size = 383440 }, + { url = "https://files.pythonhosted.org/packages/4c/e1/c65255ad5b63903e56b3bb3ff9dcc3f4f5c3badde5d08c741ee03903e951/rpds_py-0.26.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9e851920caab2dbcae311fd28f4313c6953993893eb5c1bb367ec69d9a39e7ed", size = 412759 }, + { url = "https://files.pythonhosted.org/packages/e4/22/bb731077872377a93c6e93b8a9487d0406c70208985831034ccdeed39c8e/rpds_py-0.26.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:dfbf280da5f876d0b00c81f26bedce274e72a678c28845453885a9b3c22ae632", size = 556032 }, + { url = "https://files.pythonhosted.org/packages/e0/8b/393322ce7bac5c4530fb96fc79cc9ea2f83e968ff5f6e873f905c493e1c4/rpds_py-0.26.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:1cc81d14ddfa53d7f3906694d35d54d9d3f850ef8e4e99ee68bc0d1e5fed9a9c", size = 585416 }, + { url = "https://files.pythonhosted.org/packages/49/ae/769dc372211835bf759319a7aae70525c6eb523e3371842c65b7ef41c9c6/rpds_py-0.26.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dca83c498b4650a91efcf7b88d669b170256bf8017a5db6f3e06c2bf031f57e0", size = 554049 }, + { url = "https://files.pythonhosted.org/packages/6b/f9/4c43f9cc203d6ba44ce3146246cdc38619d92c7bd7bad4946a3491bd5b70/rpds_py-0.26.0-cp313-cp313t-win32.whl", hash = "sha256:4d11382bcaf12f80b51d790dee295c56a159633a8e81e6323b16e55d81ae37e9", size = 218428 }, + { url = "https://files.pythonhosted.org/packages/7e/8b/9286b7e822036a4a977f2f1e851c7345c20528dbd56b687bb67ed68a8ede/rpds_py-0.26.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ff110acded3c22c033e637dd8896e411c7d3a11289b2edf041f86663dbc791e9", size = 231524 }, + { url = "https://files.pythonhosted.org/packages/ef/9a/1f033b0b31253d03d785b0cd905bc127e555ab496ea6b4c7c2e1f951f2fd/rpds_py-0.26.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3c0909c5234543ada2515c05dc08595b08d621ba919629e94427e8e03539c958", size = 373226 }, + { url = "https://files.pythonhosted.org/packages/58/29/5f88023fd6aaaa8ca3c4a6357ebb23f6f07da6079093ccf27c99efce87db/rpds_py-0.26.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:c1fb0cda2abcc0ac62f64e2ea4b4e64c57dfd6b885e693095460c61bde7bb18e", size = 359230 }, + { url = "https://files.pythonhosted.org/packages/6c/6c/13eaebd28b439da6964dde22712b52e53fe2824af0223b8e403249d10405/rpds_py-0.26.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:84d142d2d6cf9b31c12aa4878d82ed3b2324226270b89b676ac62ccd7df52d08", size = 382363 }, + { url = "https://files.pythonhosted.org/packages/55/fc/3bb9c486b06da19448646f96147796de23c5811ef77cbfc26f17307b6a9d/rpds_py-0.26.0-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a547e21c5610b7e9093d870be50682a6a6cf180d6da0f42c47c306073bfdbbf6", size = 397146 }, + { url = "https://files.pythonhosted.org/packages/15/18/9d1b79eb4d18e64ba8bba9e7dec6f9d6920b639f22f07ee9368ca35d4673/rpds_py-0.26.0-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:35e9a70a0f335371275cdcd08bc5b8051ac494dd58bff3bbfb421038220dc871", size = 514804 }, + { url = "https://files.pythonhosted.org/packages/4f/5a/175ad7191bdbcd28785204621b225ad70e85cdfd1e09cc414cb554633b21/rpds_py-0.26.0-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0dfa6115c6def37905344d56fb54c03afc49104e2ca473d5dedec0f6606913b4", size = 402820 }, + { url = "https://files.pythonhosted.org/packages/11/45/6a67ecf6d61c4d4aff4bc056e864eec4b2447787e11d1c2c9a0242c6e92a/rpds_py-0.26.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:313cfcd6af1a55a286a3c9a25f64af6d0e46cf60bc5798f1db152d97a216ff6f", size = 384567 }, + { url = "https://files.pythonhosted.org/packages/a1/ba/16589da828732b46454c61858950a78fe4c931ea4bf95f17432ffe64b241/rpds_py-0.26.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f7bf2496fa563c046d05e4d232d7b7fd61346e2402052064b773e5c378bf6f73", size = 416520 }, + { url = "https://files.pythonhosted.org/packages/81/4b/00092999fc7c0c266045e984d56b7314734cc400a6c6dc4d61a35f135a9d/rpds_py-0.26.0-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:aa81873e2c8c5aa616ab8e017a481a96742fdf9313c40f14338ca7dbf50cb55f", size = 559362 }, + { url = "https://files.pythonhosted.org/packages/96/0c/43737053cde1f93ac4945157f7be1428724ab943e2132a0d235a7e161d4e/rpds_py-0.26.0-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:68ffcf982715f5b5b7686bdd349ff75d422e8f22551000c24b30eaa1b7f7ae84", size = 588113 }, + { url = "https://files.pythonhosted.org/packages/46/46/8e38f6161466e60a997ed7e9951ae5de131dedc3cf778ad35994b4af823d/rpds_py-0.26.0-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:6188de70e190847bb6db3dc3981cbadff87d27d6fe9b4f0e18726d55795cee9b", size = 555429 }, + { url = "https://files.pythonhosted.org/packages/2c/ac/65da605e9f1dd643ebe615d5bbd11b6efa1d69644fc4bf623ea5ae385a82/rpds_py-0.26.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:1c962145c7473723df9722ba4c058de12eb5ebedcb4e27e7d902920aa3831ee8", size = 231950 }, + { url = "https://files.pythonhosted.org/packages/51/f2/b5c85b758a00c513bb0389f8fc8e61eb5423050c91c958cdd21843faa3e6/rpds_py-0.26.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f61a9326f80ca59214d1cceb0a09bb2ece5b2563d4e0cd37bfd5515c28510674", size = 373505 }, + { url = "https://files.pythonhosted.org/packages/23/e0/25db45e391251118e915e541995bb5f5ac5691a3b98fb233020ba53afc9b/rpds_py-0.26.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:183f857a53bcf4b1b42ef0f57ca553ab56bdd170e49d8091e96c51c3d69ca696", size = 359468 }, + { url = "https://files.pythonhosted.org/packages/0b/73/dd5ee6075bb6491be3a646b301dfd814f9486d924137a5098e61f0487e16/rpds_py-0.26.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:941c1cfdf4799d623cf3aa1d326a6b4fdb7a5799ee2687f3516738216d2262fb", size = 382680 }, + { url = "https://files.pythonhosted.org/packages/2f/10/84b522ff58763a5c443f5bcedc1820240e454ce4e620e88520f04589e2ea/rpds_py-0.26.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72a8d9564a717ee291f554eeb4bfeafe2309d5ec0aa6c475170bdab0f9ee8e88", size = 397035 }, + { url = "https://files.pythonhosted.org/packages/06/ea/8667604229a10a520fcbf78b30ccc278977dcc0627beb7ea2c96b3becef0/rpds_py-0.26.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:511d15193cbe013619dd05414c35a7dedf2088fcee93c6bbb7c77859765bd4e8", size = 514922 }, + { url = "https://files.pythonhosted.org/packages/24/e6/9ed5b625c0661c4882fc8cdf302bf8e96c73c40de99c31e0b95ed37d508c/rpds_py-0.26.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aea1f9741b603a8d8fedb0ed5502c2bc0accbc51f43e2ad1337fe7259c2b77a5", size = 402822 }, + { url = "https://files.pythonhosted.org/packages/8a/58/212c7b6fd51946047fb45d3733da27e2fa8f7384a13457c874186af691b1/rpds_py-0.26.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4019a9d473c708cf2f16415688ef0b4639e07abaa569d72f74745bbeffafa2c7", size = 384336 }, + { url = "https://files.pythonhosted.org/packages/aa/f5/a40ba78748ae8ebf4934d4b88e77b98497378bc2c24ba55ebe87a4e87057/rpds_py-0.26.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:093d63b4b0f52d98ebae33b8c50900d3d67e0666094b1be7a12fffd7f65de74b", size = 416871 }, + { url = "https://files.pythonhosted.org/packages/d5/a6/33b1fc0c9f7dcfcfc4a4353daa6308b3ece22496ceece348b3e7a7559a09/rpds_py-0.26.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:2abe21d8ba64cded53a2a677e149ceb76dcf44284202d737178afe7ba540c1eb", size = 559439 }, + { url = "https://files.pythonhosted.org/packages/71/2d/ceb3f9c12f8cfa56d34995097f6cd99da1325642c60d1b6680dd9df03ed8/rpds_py-0.26.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:4feb7511c29f8442cbbc28149a92093d32e815a28aa2c50d333826ad2a20fdf0", size = 588380 }, + { url = "https://files.pythonhosted.org/packages/c8/ed/9de62c2150ca8e2e5858acf3f4f4d0d180a38feef9fdab4078bea63d8dba/rpds_py-0.26.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:e99685fc95d386da368013e7fb4269dd39c30d99f812a8372d62f244f662709c", size = 555334 }, +] + +[[package]] +name = "rsa" +version = "4.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyasn1" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/da/8a/22b7beea3ee0d44b1916c0c1cb0ee3af23b700b6da9f04991899d0c555d4/rsa-4.9.1.tar.gz", hash = "sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75", size = 29034 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762", size = 34696 }, +] + +[[package]] +name = "s3transfer" +version = "0.13.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6d/05/d52bf1e65044b4e5e27d4e63e8d1579dbdec54fce685908ae09bc3720030/s3transfer-0.13.1.tar.gz", hash = "sha256:c3fdba22ba1bd367922f27ec8032d6a1cf5f10c934fb5d68cf60fd5a23d936cf", size = 150589 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/4f/d073e09df851cfa251ef7840007d04db3293a0482ce607d2b993926089be/s3transfer-0.13.1-py3-none-any.whl", hash = "sha256:a981aa7429be23fe6dfc13e80e4020057cbab622b08c0315288758d67cabc724", size = 85308 }, +] + +[[package]] +name = "safetensors" +version = "0.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6c/d2/94fe37355a1d4ff86b0f43b9a018515d5d29bf7ad6d01318a80f5db2fd6a/safetensors-0.6.1.tar.gz", hash = "sha256:a766ba6e19b198eff09be05f24cd89eda1670ed404ae828e2aa3fc09816ba8d8", size = 197968 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6b/c0/40263a2103511917f9a92b4e114ecaff68586df07f12d1d877312f1261f3/safetensors-0.6.1-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:81ed1b69d6f8acd7e759a71197ce3a69da4b7e9faa9dbb005eb06a83b1a4e52d", size = 455232 }, + { url = "https://files.pythonhosted.org/packages/86/bf/432cb4bb1c336d338dd9b29f78622b1441ee06e5868bf1de2ca2bec74c08/safetensors-0.6.1-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:01b51af8cb7a3870203f2735e3c7c24d1a65fb2846e75613c8cf9d284271eccc", size = 432150 }, + { url = "https://files.pythonhosted.org/packages/05/d7/820c99032a53d57279ae199df7d114a8c9e2bbce4fa69bc0de53743495f0/safetensors-0.6.1-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:64a733886d79e726899b9d9643813e48a2eec49f3ef0fdb8cd4b8152046101c3", size = 471634 }, + { url = "https://files.pythonhosted.org/packages/ea/8b/bcd960087eded7690f118ceeda294912f92a3b508a1d9a504f9c2e02041b/safetensors-0.6.1-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f233dc3b12fb641b36724844754b6bb41349615a0e258087560968d6da92add5", size = 487855 }, + { url = "https://files.pythonhosted.org/packages/41/64/b44eac4ad87c4e1c0cf5ba5e204c032b1b1eac8ce2b8f65f87791e647bd6/safetensors-0.6.1-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6f16289e2af54affd591dd78ed12b5465e4dc5823f818beaeddd49a010cf3ba7", size = 607240 }, + { url = "https://files.pythonhosted.org/packages/52/75/0347fa0c080af8bd3341af26a30b85939f6362d4f5240add1a0c9d793354/safetensors-0.6.1-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1b62eab84e2c69918b598272504c5d2ebfe64da6c16fdf8682054eec9572534d", size = 519864 }, + { url = "https://files.pythonhosted.org/packages/ea/f3/83843d1fe9164f44a267373c55cba706530b209b58415f807b40edddcd3e/safetensors-0.6.1-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d498363746555dccffc02a47dfe1dee70f7784f3f37f1d66b408366c5d3a989e", size = 485926 }, + { url = "https://files.pythonhosted.org/packages/b8/26/f6b0cb5210bab0e343214fdba7c2df80a69b019e62e760ddc61b18bec383/safetensors-0.6.1-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:eed2079dca3ca948d7b0d7120396e776bbc6680637cf199d393e157fde25c937", size = 518999 }, + { url = "https://files.pythonhosted.org/packages/90/b7/8910b165c97d3bd6d445c6ca8b704ec23d0fa33849ce9a51dc783827a302/safetensors-0.6.1-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:294040ff20ebe079a2b4976cfa9a5be0202f56ca4f7f190b4e52009e8c026ceb", size = 650669 }, + { url = "https://files.pythonhosted.org/packages/00/bc/2eeb025381d0834ae038aae2d383dfa830c2e0068e2e4e512ea99b135a4b/safetensors-0.6.1-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:75693208b492a026b926edeebbae888cc644433bee4993573ead2dc44810b519", size = 750019 }, + { url = "https://files.pythonhosted.org/packages/f9/38/5dda9a8e056eb1f17ed3a7846698fd94623a1648013cdf522538845755da/safetensors-0.6.1-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:a8687b71ac67a0b3f8ce87df9e8024edf087e94c34ef46eaaad694dce8d2f83f", size = 689888 }, + { url = "https://files.pythonhosted.org/packages/dd/60/15ee3961996d951002378d041bd82863a5c70738a71375b42d6dd5d2a6d3/safetensors-0.6.1-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5dd969a01c738104f707fa0e306b757f5beb3ebdcd682fe0724170a0bf1c21fb", size = 655539 }, + { url = "https://files.pythonhosted.org/packages/91/d6/01172a9c77c566800286d379bfc341d75370eae2118dfd339edfd0394c4a/safetensors-0.6.1-cp38-abi3-win32.whl", hash = "sha256:7c3d8d34d01673d1a917445c9437ee73a9d48bc6af10352b84bbd46c5da93ca5", size = 308594 }, + { url = "https://files.pythonhosted.org/packages/6c/5d/195dc1917d7ae93dd990d9b2f8b9c88e451bcc78e0b63ee107beebc1e4be/safetensors-0.6.1-cp38-abi3-win_amd64.whl", hash = "sha256:4720957052d57c5ac48912c3f6e07e9a334d9632758c9b0c054afba477fcbe2d", size = 320282 }, +] + +[[package]] +name = "schema" +version = "0.7.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d4/01/0ea2e66bad2f13271e93b729c653747614784d3ebde219679e41ccdceecd/schema-0.7.7.tar.gz", hash = "sha256:7da553abd2958a19dc2547c388cde53398b39196175a9be59ea1caf5ab0a1807", size = 44245 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ad/1b/81855a88c6db2b114d5b2e9f96339190d5ee4d1b981d217fa32127bb00e0/schema-0.7.7-py2.py3-none-any.whl", hash = "sha256:5d976a5b50f36e74e2157b47097b60002bd4d42e65425fcc9c9befadb4255dde", size = 18632 }, +] + +[[package]] +name = "scipy" +version = "1.15.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11' and platform_python_implementation == 'PyPy' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and platform_python_implementation == 'PyPy' and sys_platform == 'linux'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and platform_python_implementation == 'PyPy' and sys_platform == 'linux') or (python_full_version < '3.11' and platform_python_implementation == 'PyPy' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version < '3.11' and platform_python_implementation != 'PyPy' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and platform_python_implementation != 'PyPy' and sys_platform == 'linux'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and platform_python_implementation != 'PyPy' and sys_platform == 'linux') or (python_full_version < '3.11' and platform_python_implementation != 'PyPy' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0f/37/6964b830433e654ec7485e45a00fc9a27cf868d622838f6b6d9c5ec0d532/scipy-1.15.3.tar.gz", hash = "sha256:eae3cf522bc7df64b42cad3925c876e1b0b6c35c1337c93e12c0f366f55b0eaf", size = 59419214 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/2f/4966032c5f8cc7e6a60f1b2e0ad686293b9474b65246b0c642e3ef3badd0/scipy-1.15.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:a345928c86d535060c9c2b25e71e87c39ab2f22fc96e9636bd74d1dbf9de448c", size = 38702770 }, + { url = "https://files.pythonhosted.org/packages/a0/6e/0c3bf90fae0e910c274db43304ebe25a6b391327f3f10b5dcc638c090795/scipy-1.15.3-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:ad3432cb0f9ed87477a8d97f03b763fd1d57709f1bbde3c9369b1dff5503b253", size = 30094511 }, + { url = "https://files.pythonhosted.org/packages/ea/b1/4deb37252311c1acff7f101f6453f0440794f51b6eacb1aad4459a134081/scipy-1.15.3-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:aef683a9ae6eb00728a542b796f52a5477b78252edede72b8327a886ab63293f", size = 22368151 }, + { url = "https://files.pythonhosted.org/packages/38/7d/f457626e3cd3c29b3a49ca115a304cebb8cc6f31b04678f03b216899d3c6/scipy-1.15.3-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:1c832e1bd78dea67d5c16f786681b28dd695a8cb1fb90af2e27580d3d0967e92", size = 25121732 }, + { url = "https://files.pythonhosted.org/packages/db/0a/92b1de4a7adc7a15dcf5bddc6e191f6f29ee663b30511ce20467ef9b82e4/scipy-1.15.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:263961f658ce2165bbd7b99fa5135195c3a12d9bef045345016b8b50c315cb82", size = 35547617 }, + { url = "https://files.pythonhosted.org/packages/8e/6d/41991e503e51fc1134502694c5fa7a1671501a17ffa12716a4a9151af3df/scipy-1.15.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e2abc762b0811e09a0d3258abee2d98e0c703eee49464ce0069590846f31d40", size = 37662964 }, + { url = "https://files.pythonhosted.org/packages/25/e1/3df8f83cb15f3500478c889be8fb18700813b95e9e087328230b98d547ff/scipy-1.15.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ed7284b21a7a0c8f1b6e5977ac05396c0d008b89e05498c8b7e8f4a1423bba0e", size = 37238749 }, + { url = "https://files.pythonhosted.org/packages/93/3e/b3257cf446f2a3533ed7809757039016b74cd6f38271de91682aa844cfc5/scipy-1.15.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5380741e53df2c566f4d234b100a484b420af85deb39ea35a1cc1be84ff53a5c", size = 40022383 }, + { url = "https://files.pythonhosted.org/packages/d1/84/55bc4881973d3f79b479a5a2e2df61c8c9a04fcb986a213ac9c02cfb659b/scipy-1.15.3-cp310-cp310-win_amd64.whl", hash = "sha256:9d61e97b186a57350f6d6fd72640f9e99d5a4a2b8fbf4b9ee9a841eab327dc13", size = 41259201 }, + { url = "https://files.pythonhosted.org/packages/96/ab/5cc9f80f28f6a7dff646c5756e559823614a42b1939d86dd0ed550470210/scipy-1.15.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:993439ce220d25e3696d1b23b233dd010169b62f6456488567e830654ee37a6b", size = 38714255 }, + { url = "https://files.pythonhosted.org/packages/4a/4a/66ba30abe5ad1a3ad15bfb0b59d22174012e8056ff448cb1644deccbfed2/scipy-1.15.3-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:34716e281f181a02341ddeaad584205bd2fd3c242063bd3423d61ac259ca7eba", size = 30111035 }, + { url = "https://files.pythonhosted.org/packages/4b/fa/a7e5b95afd80d24313307f03624acc65801846fa75599034f8ceb9e2cbf6/scipy-1.15.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3b0334816afb8b91dab859281b1b9786934392aa3d527cd847e41bb6f45bee65", size = 22384499 }, + { url = "https://files.pythonhosted.org/packages/17/99/f3aaddccf3588bb4aea70ba35328c204cadd89517a1612ecfda5b2dd9d7a/scipy-1.15.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:6db907c7368e3092e24919b5e31c76998b0ce1684d51a90943cb0ed1b4ffd6c1", size = 25152602 }, + { url = "https://files.pythonhosted.org/packages/56/c5/1032cdb565f146109212153339f9cb8b993701e9fe56b1c97699eee12586/scipy-1.15.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:721d6b4ef5dc82ca8968c25b111e307083d7ca9091bc38163fb89243e85e3889", size = 35503415 }, + { url = "https://files.pythonhosted.org/packages/bd/37/89f19c8c05505d0601ed5650156e50eb881ae3918786c8fd7262b4ee66d3/scipy-1.15.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39cb9c62e471b1bb3750066ecc3a3f3052b37751c7c3dfd0fd7e48900ed52982", size = 37652622 }, + { url = "https://files.pythonhosted.org/packages/7e/31/be59513aa9695519b18e1851bb9e487de66f2d31f835201f1b42f5d4d475/scipy-1.15.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:795c46999bae845966368a3c013e0e00947932d68e235702b5c3f6ea799aa8c9", size = 37244796 }, + { url = "https://files.pythonhosted.org/packages/10/c0/4f5f3eeccc235632aab79b27a74a9130c6c35df358129f7ac8b29f562ac7/scipy-1.15.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:18aaacb735ab38b38db42cb01f6b92a2d0d4b6aabefeb07f02849e47f8fb3594", size = 40047684 }, + { url = "https://files.pythonhosted.org/packages/ab/a7/0ddaf514ce8a8714f6ed243a2b391b41dbb65251affe21ee3077ec45ea9a/scipy-1.15.3-cp311-cp311-win_amd64.whl", hash = "sha256:ae48a786a28412d744c62fd7816a4118ef97e5be0bee968ce8f0a2fba7acf3bb", size = 41246504 }, + { url = "https://files.pythonhosted.org/packages/37/4b/683aa044c4162e10ed7a7ea30527f2cbd92e6999c10a8ed8edb253836e9c/scipy-1.15.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6ac6310fdbfb7aa6612408bd2f07295bcbd3fda00d2d702178434751fe48e019", size = 38766735 }, + { url = "https://files.pythonhosted.org/packages/7b/7e/f30be3d03de07f25dc0ec926d1681fed5c732d759ac8f51079708c79e680/scipy-1.15.3-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:185cd3d6d05ca4b44a8f1595af87f9c372bb6acf9c808e99aa3e9aa03bd98cf6", size = 30173284 }, + { url = "https://files.pythonhosted.org/packages/07/9c/0ddb0d0abdabe0d181c1793db51f02cd59e4901da6f9f7848e1f96759f0d/scipy-1.15.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:05dc6abcd105e1a29f95eada46d4a3f251743cfd7d3ae8ddb4088047f24ea477", size = 22446958 }, + { url = "https://files.pythonhosted.org/packages/af/43/0bce905a965f36c58ff80d8bea33f1f9351b05fad4beaad4eae34699b7a1/scipy-1.15.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:06efcba926324df1696931a57a176c80848ccd67ce6ad020c810736bfd58eb1c", size = 25242454 }, + { url = "https://files.pythonhosted.org/packages/56/30/a6f08f84ee5b7b28b4c597aca4cbe545535c39fe911845a96414700b64ba/scipy-1.15.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c05045d8b9bfd807ee1b9f38761993297b10b245f012b11b13b91ba8945f7e45", size = 35210199 }, + { url = "https://files.pythonhosted.org/packages/0b/1f/03f52c282437a168ee2c7c14a1a0d0781a9a4a8962d84ac05c06b4c5b555/scipy-1.15.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:271e3713e645149ea5ea3e97b57fdab61ce61333f97cfae392c28ba786f9bb49", size = 37309455 }, + { url = "https://files.pythonhosted.org/packages/89/b1/fbb53137f42c4bf630b1ffdfc2151a62d1d1b903b249f030d2b1c0280af8/scipy-1.15.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6cfd56fc1a8e53f6e89ba3a7a7251f7396412d655bca2aa5611c8ec9a6784a1e", size = 36885140 }, + { url = "https://files.pythonhosted.org/packages/2e/2e/025e39e339f5090df1ff266d021892694dbb7e63568edcfe43f892fa381d/scipy-1.15.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0ff17c0bb1cb32952c09217d8d1eed9b53d1463e5f1dd6052c7857f83127d539", size = 39710549 }, + { url = "https://files.pythonhosted.org/packages/e6/eb/3bf6ea8ab7f1503dca3a10df2e4b9c3f6b3316df07f6c0ded94b281c7101/scipy-1.15.3-cp312-cp312-win_amd64.whl", hash = "sha256:52092bc0472cfd17df49ff17e70624345efece4e1a12b23783a1ac59a1b728ed", size = 40966184 }, + { url = "https://files.pythonhosted.org/packages/73/18/ec27848c9baae6e0d6573eda6e01a602e5649ee72c27c3a8aad673ebecfd/scipy-1.15.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2c620736bcc334782e24d173c0fdbb7590a0a436d2fdf39310a8902505008759", size = 38728256 }, + { url = "https://files.pythonhosted.org/packages/74/cd/1aef2184948728b4b6e21267d53b3339762c285a46a274ebb7863c9e4742/scipy-1.15.3-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:7e11270a000969409d37ed399585ee530b9ef6aa99d50c019de4cb01e8e54e62", size = 30109540 }, + { url = "https://files.pythonhosted.org/packages/5b/d8/59e452c0a255ec352bd0a833537a3bc1bfb679944c4938ab375b0a6b3a3e/scipy-1.15.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:8c9ed3ba2c8a2ce098163a9bdb26f891746d02136995df25227a20e71c396ebb", size = 22383115 }, + { url = "https://files.pythonhosted.org/packages/08/f5/456f56bbbfccf696263b47095291040655e3cbaf05d063bdc7c7517f32ac/scipy-1.15.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:0bdd905264c0c9cfa74a4772cdb2070171790381a5c4d312c973382fc6eaf730", size = 25163884 }, + { url = "https://files.pythonhosted.org/packages/a2/66/a9618b6a435a0f0c0b8a6d0a2efb32d4ec5a85f023c2b79d39512040355b/scipy-1.15.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79167bba085c31f38603e11a267d862957cbb3ce018d8b38f79ac043bc92d825", size = 35174018 }, + { url = "https://files.pythonhosted.org/packages/b5/09/c5b6734a50ad4882432b6bb7c02baf757f5b2f256041da5df242e2d7e6b6/scipy-1.15.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9deabd6d547aee2c9a81dee6cc96c6d7e9a9b1953f74850c179f91fdc729cb7", size = 37269716 }, + { url = "https://files.pythonhosted.org/packages/77/0a/eac00ff741f23bcabd352731ed9b8995a0a60ef57f5fd788d611d43d69a1/scipy-1.15.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:dde4fc32993071ac0c7dd2d82569e544f0bdaff66269cb475e0f369adad13f11", size = 36872342 }, + { url = "https://files.pythonhosted.org/packages/fe/54/4379be86dd74b6ad81551689107360d9a3e18f24d20767a2d5b9253a3f0a/scipy-1.15.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f77f853d584e72e874d87357ad70f44b437331507d1c311457bed8ed2b956126", size = 39670869 }, + { url = "https://files.pythonhosted.org/packages/87/2e/892ad2862ba54f084ffe8cc4a22667eaf9c2bcec6d2bff1d15713c6c0703/scipy-1.15.3-cp313-cp313-win_amd64.whl", hash = "sha256:b90ab29d0c37ec9bf55424c064312930ca5f4bde15ee8619ee44e69319aab163", size = 40988851 }, + { url = "https://files.pythonhosted.org/packages/1b/e9/7a879c137f7e55b30d75d90ce3eb468197646bc7b443ac036ae3fe109055/scipy-1.15.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3ac07623267feb3ae308487c260ac684b32ea35fd81e12845039952f558047b8", size = 38863011 }, + { url = "https://files.pythonhosted.org/packages/51/d1/226a806bbd69f62ce5ef5f3ffadc35286e9fbc802f606a07eb83bf2359de/scipy-1.15.3-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:6487aa99c2a3d509a5227d9a5e889ff05830a06b2ce08ec30df6d79db5fcd5c5", size = 30266407 }, + { url = "https://files.pythonhosted.org/packages/e5/9b/f32d1d6093ab9eeabbd839b0f7619c62e46cc4b7b6dbf05b6e615bbd4400/scipy-1.15.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:50f9e62461c95d933d5c5ef4a1f2ebf9a2b4e83b0db374cb3f1de104d935922e", size = 22540030 }, + { url = "https://files.pythonhosted.org/packages/e7/29/c278f699b095c1a884f29fda126340fcc201461ee8bfea5c8bdb1c7c958b/scipy-1.15.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:14ed70039d182f411ffc74789a16df3835e05dc469b898233a245cdfd7f162cb", size = 25218709 }, + { url = "https://files.pythonhosted.org/packages/24/18/9e5374b617aba742a990581373cd6b68a2945d65cc588482749ef2e64467/scipy-1.15.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a769105537aa07a69468a0eefcd121be52006db61cdd8cac8a0e68980bbb723", size = 34809045 }, + { url = "https://files.pythonhosted.org/packages/e1/fe/9c4361e7ba2927074360856db6135ef4904d505e9b3afbbcb073c4008328/scipy-1.15.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9db984639887e3dffb3928d118145ffe40eff2fa40cb241a306ec57c219ebbbb", size = 36703062 }, + { url = "https://files.pythonhosted.org/packages/b7/8e/038ccfe29d272b30086b25a4960f757f97122cb2ec42e62b460d02fe98e9/scipy-1.15.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:40e54d5c7e7ebf1aa596c374c49fa3135f04648a0caabcb66c52884b943f02b4", size = 36393132 }, + { url = "https://files.pythonhosted.org/packages/10/7e/5c12285452970be5bdbe8352c619250b97ebf7917d7a9a9e96b8a8140f17/scipy-1.15.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5e721fed53187e71d0ccf382b6bf977644c533e506c4d33c3fb24de89f5c3ed5", size = 38979503 }, + { url = "https://files.pythonhosted.org/packages/81/06/0a5e5349474e1cbc5757975b21bd4fad0e72ebf138c5592f191646154e06/scipy-1.15.3-cp313-cp313t-win_amd64.whl", hash = "sha256:76ad1fb5f8752eabf0fa02e4cc0336b4e8f021e2d5f061ed37d6d264db35e3ca", size = 40308097 }, +] + +[[package]] +name = "scipy" +version = "1.16.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and platform_python_implementation == 'PyPy' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and platform_python_implementation == 'PyPy' and sys_platform == 'linux'", + "(python_full_version >= '3.13' and platform_machine != 'aarch64' and platform_python_implementation == 'PyPy' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_python_implementation == 'PyPy' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version >= '3.13' and platform_python_implementation != 'PyPy' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and platform_python_implementation != 'PyPy' and sys_platform == 'linux'", + "(python_full_version >= '3.13' and platform_machine != 'aarch64' and platform_python_implementation != 'PyPy' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_python_implementation != 'PyPy' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version >= '3.12.4' and python_full_version < '3.13' and platform_python_implementation == 'PyPy' and sys_platform == 'darwin'", + "python_full_version >= '3.12.4' and python_full_version < '3.13' and platform_machine == 'aarch64' and platform_python_implementation == 'PyPy' and sys_platform == 'linux'", + "(python_full_version >= '3.12.4' and python_full_version < '3.13' and platform_machine != 'aarch64' and platform_python_implementation == 'PyPy' and sys_platform == 'linux') or (python_full_version >= '3.12.4' and python_full_version < '3.13' and platform_python_implementation == 'PyPy' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version >= '3.12.4' and python_full_version < '3.13' and platform_python_implementation != 'PyPy' and sys_platform == 'darwin'", + "python_full_version >= '3.12.4' and python_full_version < '3.13' and platform_machine == 'aarch64' and platform_python_implementation != 'PyPy' and sys_platform == 'linux'", + "(python_full_version >= '3.12.4' and python_full_version < '3.13' and platform_machine != 'aarch64' and platform_python_implementation != 'PyPy' and sys_platform == 'linux') or (python_full_version >= '3.12.4' and python_full_version < '3.13' and platform_python_implementation != 'PyPy' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version >= '3.12' and python_full_version < '3.12.4' and platform_python_implementation == 'PyPy' and sys_platform == 'darwin'", + "python_full_version >= '3.12' and python_full_version < '3.12.4' and platform_machine == 'aarch64' and platform_python_implementation == 'PyPy' and sys_platform == 'linux'", + "(python_full_version >= '3.12' and python_full_version < '3.12.4' and platform_machine != 'aarch64' and platform_python_implementation == 'PyPy' and sys_platform == 'linux') or (python_full_version >= '3.12' and python_full_version < '3.12.4' and platform_python_implementation == 'PyPy' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version >= '3.12' and python_full_version < '3.12.4' and platform_python_implementation != 'PyPy' and sys_platform == 'darwin'", + "python_full_version >= '3.12' and python_full_version < '3.12.4' and platform_machine == 'aarch64' and platform_python_implementation != 'PyPy' and sys_platform == 'linux'", + "(python_full_version >= '3.12' and python_full_version < '3.12.4' and platform_machine != 'aarch64' and platform_python_implementation != 'PyPy' and sys_platform == 'linux') or (python_full_version >= '3.12' and python_full_version < '3.12.4' and platform_python_implementation != 'PyPy' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version == '3.11.*' and platform_python_implementation == 'PyPy' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and platform_python_implementation == 'PyPy' and sys_platform == 'linux'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and platform_python_implementation == 'PyPy' and sys_platform == 'linux') or (python_full_version == '3.11.*' and platform_python_implementation == 'PyPy' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version == '3.11.*' and platform_python_implementation != 'PyPy' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and platform_python_implementation != 'PyPy' and sys_platform == 'linux'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and platform_python_implementation != 'PyPy' and sys_platform == 'linux') or (python_full_version == '3.11.*' and platform_python_implementation != 'PyPy' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "numpy", version = "2.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/4a/b927028464795439faec8eaf0b03b011005c487bb2d07409f28bf30879c4/scipy-1.16.1.tar.gz", hash = "sha256:44c76f9e8b6e8e488a586190ab38016e4ed2f8a038af7cd3defa903c0a2238b3", size = 30580861 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/91/812adc6f74409b461e3a5fa97f4f74c769016919203138a3bf6fc24ba4c5/scipy-1.16.1-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:c033fa32bab91dc98ca59d0cf23bb876454e2bb02cbe592d5023138778f70030", size = 36552519 }, + { url = "https://files.pythonhosted.org/packages/47/18/8e355edcf3b71418d9e9f9acd2708cc3a6c27e8f98fde0ac34b8a0b45407/scipy-1.16.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:6e5c2f74e5df33479b5cd4e97a9104c511518fbd979aa9b8f6aec18b2e9ecae7", size = 28638010 }, + { url = "https://files.pythonhosted.org/packages/d9/eb/e931853058607bdfbc11b86df19ae7a08686121c203483f62f1ecae5989c/scipy-1.16.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:0a55ffe0ba0f59666e90951971a884d1ff6f4ec3275a48f472cfb64175570f77", size = 20909790 }, + { url = "https://files.pythonhosted.org/packages/45/0c/be83a271d6e96750cd0be2e000f35ff18880a46f05ce8b5d3465dc0f7a2a/scipy-1.16.1-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:f8a5d6cd147acecc2603fbd382fed6c46f474cccfcf69ea32582e033fb54dcfe", size = 23513352 }, + { url = "https://files.pythonhosted.org/packages/7c/bf/fe6eb47e74f762f933cca962db7f2c7183acfdc4483bd1c3813cfe83e538/scipy-1.16.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cb18899127278058bcc09e7b9966d41a5a43740b5bb8dcba401bd983f82e885b", size = 33534643 }, + { url = "https://files.pythonhosted.org/packages/bb/ba/63f402e74875486b87ec6506a4f93f6d8a0d94d10467280f3d9d7837ce3a/scipy-1.16.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:adccd93a2fa937a27aae826d33e3bfa5edf9aa672376a4852d23a7cd67a2e5b7", size = 35376776 }, + { url = "https://files.pythonhosted.org/packages/c3/b4/04eb9d39ec26a1b939689102da23d505ea16cdae3dbb18ffc53d1f831044/scipy-1.16.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:18aca1646a29ee9a0625a1be5637fa798d4d81fdf426481f06d69af828f16958", size = 35698906 }, + { url = "https://files.pythonhosted.org/packages/04/d6/bb5468da53321baeb001f6e4e0d9049eadd175a4a497709939128556e3ec/scipy-1.16.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d85495cef541729a70cdddbbf3e6b903421bc1af3e8e3a9a72a06751f33b7c39", size = 38129275 }, + { url = "https://files.pythonhosted.org/packages/c4/94/994369978509f227cba7dfb9e623254d0d5559506fe994aef4bea3ed469c/scipy-1.16.1-cp311-cp311-win_amd64.whl", hash = "sha256:226652fca853008119c03a8ce71ffe1b3f6d2844cc1686e8f9806edafae68596", size = 38644572 }, + { url = "https://files.pythonhosted.org/packages/f8/d9/ec4864f5896232133f51382b54a08de91a9d1af7a76dfa372894026dfee2/scipy-1.16.1-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:81b433bbeaf35728dad619afc002db9b189e45eebe2cd676effe1fb93fef2b9c", size = 36575194 }, + { url = "https://files.pythonhosted.org/packages/5c/6d/40e81ecfb688e9d25d34a847dca361982a6addf8e31f0957b1a54fbfa994/scipy-1.16.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:886cc81fdb4c6903a3bb0464047c25a6d1016fef77bb97949817d0c0d79f9e04", size = 28594590 }, + { url = "https://files.pythonhosted.org/packages/0e/37/9f65178edfcc629377ce9a64fc09baebea18c80a9e57ae09a52edf84880b/scipy-1.16.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:15240c3aac087a522b4eaedb09f0ad061753c5eebf1ea430859e5bf8640d5919", size = 20866458 }, + { url = "https://files.pythonhosted.org/packages/2c/7b/749a66766871ea4cb1d1ea10f27004db63023074c22abed51f22f09770e0/scipy-1.16.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:65f81a25805f3659b48126b5053d9e823d3215e4a63730b5e1671852a1705921", size = 23539318 }, + { url = "https://files.pythonhosted.org/packages/c4/db/8d4afec60eb833a666434d4541a3151eedbf2494ea6d4d468cbe877f00cd/scipy-1.16.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6c62eea7f607f122069b9bad3f99489ddca1a5173bef8a0c75555d7488b6f725", size = 33292899 }, + { url = "https://files.pythonhosted.org/packages/51/1e/79023ca3bbb13a015d7d2757ecca3b81293c663694c35d6541b4dca53e98/scipy-1.16.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f965bbf3235b01c776115ab18f092a95aa74c271a52577bcb0563e85738fd618", size = 35162637 }, + { url = "https://files.pythonhosted.org/packages/b6/49/0648665f9c29fdaca4c679182eb972935b3b4f5ace41d323c32352f29816/scipy-1.16.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f006e323874ffd0b0b816d8c6a8e7f9a73d55ab3b8c3f72b752b226d0e3ac83d", size = 35490507 }, + { url = "https://files.pythonhosted.org/packages/62/8f/66cbb9d6bbb18d8c658f774904f42a92078707a7c71e5347e8bf2f52bb89/scipy-1.16.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e8fd15fc5085ab4cca74cb91fe0a4263b1f32e4420761ddae531ad60934c2119", size = 37923998 }, + { url = "https://files.pythonhosted.org/packages/14/c3/61f273ae550fbf1667675701112e380881905e28448c080b23b5a181df7c/scipy-1.16.1-cp312-cp312-win_amd64.whl", hash = "sha256:f7b8013c6c066609577d910d1a2a077021727af07b6fab0ee22c2f901f22352a", size = 38508060 }, + { url = "https://files.pythonhosted.org/packages/93/0b/b5c99382b839854a71ca9482c684e3472badc62620287cbbdab499b75ce6/scipy-1.16.1-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:5451606823a5e73dfa621a89948096c6528e2896e40b39248295d3a0138d594f", size = 36533717 }, + { url = "https://files.pythonhosted.org/packages/eb/e5/69ab2771062c91e23e07c12e7d5033a6b9b80b0903ee709c3c36b3eb520c/scipy-1.16.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:89728678c5ca5abd610aee148c199ac1afb16e19844401ca97d43dc548a354eb", size = 28570009 }, + { url = "https://files.pythonhosted.org/packages/f4/69/bd75dbfdd3cf524f4d753484d723594aed62cfaac510123e91a6686d520b/scipy-1.16.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:e756d688cb03fd07de0fffad475649b03cb89bee696c98ce508b17c11a03f95c", size = 20841942 }, + { url = "https://files.pythonhosted.org/packages/ea/74/add181c87663f178ba7d6144b370243a87af8476664d5435e57d599e6874/scipy-1.16.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:5aa2687b9935da3ed89c5dbed5234576589dd28d0bf7cd237501ccfbdf1ad608", size = 23498507 }, + { url = "https://files.pythonhosted.org/packages/1d/74/ece2e582a0d9550cee33e2e416cc96737dce423a994d12bbe59716f47ff1/scipy-1.16.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0851f6a1e537fe9399f35986897e395a1aa61c574b178c0d456be5b1a0f5ca1f", size = 33286040 }, + { url = "https://files.pythonhosted.org/packages/e4/82/08e4076df538fb56caa1d489588d880ec7c52d8273a606bb54d660528f7c/scipy-1.16.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fedc2cbd1baed37474b1924c331b97bdff611d762c196fac1a9b71e67b813b1b", size = 35176096 }, + { url = "https://files.pythonhosted.org/packages/fa/79/cd710aab8c921375711a8321c6be696e705a120e3011a643efbbcdeeabcc/scipy-1.16.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2ef500e72f9623a6735769e4b93e9dcb158d40752cdbb077f305487e3e2d1f45", size = 35490328 }, + { url = "https://files.pythonhosted.org/packages/71/73/e9cc3d35ee4526d784520d4494a3e1ca969b071fb5ae5910c036a375ceec/scipy-1.16.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:978d8311674b05a8f7ff2ea6c6bce5d8b45a0cb09d4c5793e0318f448613ea65", size = 37939921 }, + { url = "https://files.pythonhosted.org/packages/21/12/c0efd2941f01940119b5305c375ae5c0fcb7ec193f806bd8f158b73a1782/scipy-1.16.1-cp313-cp313-win_amd64.whl", hash = "sha256:81929ed0fa7a5713fcdd8b2e6f73697d3b4c4816d090dd34ff937c20fa90e8ab", size = 38479462 }, + { url = "https://files.pythonhosted.org/packages/7a/19/c3d08b675260046a991040e1ea5d65f91f40c7df1045fffff412dcfc6765/scipy-1.16.1-cp313-cp313t-macosx_10_14_x86_64.whl", hash = "sha256:bcc12db731858abda693cecdb3bdc9e6d4bd200213f49d224fe22df82687bdd6", size = 36938832 }, + { url = "https://files.pythonhosted.org/packages/81/f2/ce53db652c033a414a5b34598dba6b95f3d38153a2417c5a3883da429029/scipy-1.16.1-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:744d977daa4becb9fc59135e75c069f8d301a87d64f88f1e602a9ecf51e77b27", size = 29093084 }, + { url = "https://files.pythonhosted.org/packages/a9/ae/7a10ff04a7dc15f9057d05b33737ade244e4bd195caa3f7cc04d77b9e214/scipy-1.16.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:dc54f76ac18073bcecffb98d93f03ed6b81a92ef91b5d3b135dcc81d55a724c7", size = 21365098 }, + { url = "https://files.pythonhosted.org/packages/36/ac/029ff710959932ad3c2a98721b20b405f05f752f07344622fd61a47c5197/scipy-1.16.1-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:367d567ee9fc1e9e2047d31f39d9d6a7a04e0710c86e701e053f237d14a9b4f6", size = 23896858 }, + { url = "https://files.pythonhosted.org/packages/71/13/d1ef77b6bd7898720e1f0b6b3743cb945f6c3cafa7718eaac8841035ab60/scipy-1.16.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4cf5785e44e19dcd32a0e4807555e1e9a9b8d475c6afff3d21c3c543a6aa84f4", size = 33438311 }, + { url = "https://files.pythonhosted.org/packages/2d/e0/e64a6821ffbb00b4c5b05169f1c1fddb4800e9307efe3db3788995a82a2c/scipy-1.16.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3d0b80fb26d3e13a794c71d4b837e2a589d839fd574a6bbb4ee1288c213ad4a3", size = 35279542 }, + { url = "https://files.pythonhosted.org/packages/57/59/0dc3c8b43e118f1e4ee2b798dcc96ac21bb20014e5f1f7a8e85cc0653bdb/scipy-1.16.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:8503517c44c18d1030d666cb70aaac1cc8913608816e06742498833b128488b7", size = 35667665 }, + { url = "https://files.pythonhosted.org/packages/45/5f/844ee26e34e2f3f9f8febb9343748e72daeaec64fe0c70e9bf1ff84ec955/scipy-1.16.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:30cc4bb81c41831ecfd6dc450baf48ffd80ef5aed0f5cf3ea775740e80f16ecc", size = 38045210 }, + { url = "https://files.pythonhosted.org/packages/8d/d7/210f2b45290f444f1de64bc7353aa598ece9f0e90c384b4a156f9b1a5063/scipy-1.16.1-cp313-cp313t-win_amd64.whl", hash = "sha256:c24fa02f7ed23ae514460a22c57eca8f530dbfa50b1cfdbf4f37c05b5309cc39", size = 38593661 }, +] + +[[package]] +name = "scrapegraph-py" +version = "1.18.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, + { name = "beautifulsoup4" }, + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/16/98/c5fdafabdcaa0ca929b0e483723f76b888e8ad5e82d9a9b443df297c33a1/scrapegraph_py-1.18.2.tar.gz", hash = "sha256:2e47373e934302ffc02bd9ee6cba32cd5347ca59a73aed15d55c7db881933574", size = 182672 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/73/e9fa7a4b587d228c88438ecc6f9b1c24a61e894ac65d0776a3303e322c59/scrapegraph_py-1.18.2-py3-none-any.whl", hash = "sha256:b1e483ff7c8fbe338a8d6c8e496322671dce08f31235eb6cc6273a9f48974999", size = 21008 }, +] + +[[package]] +name = "scrapfly-sdk" +version = "0.8.23" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "backoff" }, + { name = "decorator" }, + { name = "loguru" }, + { name = "python-dateutil" }, + { name = "requests" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4c/e7/f6ed9d4259e78874dcfcc7a2f4aeb86b3035844ea73ddc430bfa0b9baab0/scrapfly_sdk-0.8.23.tar.gz", hash = "sha256:2668f7a82bf3a6b240be2f1e4090cf140d74181de57bb46543719554fbed55ae", size = 42258 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b0/5b/ad296ac36293e7967767411827e58e5cd7ccd7120de8b124780f8e52e699/scrapfly_sdk-0.8.23-py3-none-any.whl", hash = "sha256:ddc098f1670a8dcc38b8121093433df9f9415a10bd5f797b506bce5ce67b3eef", size = 44302 }, +] + +[[package]] +name = "selenium" +version = "4.34.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "trio" }, + { name = "trio-websocket" }, + { name = "typing-extensions" }, + { name = "urllib3", extra = ["socks"] }, + { name = "websocket-client" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b0/e6/646d0a41fb9a64572043c3de80be2a4941f2aeb578f273cf3dae54fc9437/selenium-4.34.2.tar.gz", hash = "sha256:0f6d147595f08c6d4bad87b34c39dcacb4650aedc78e3956c8eac1bb752a3854", size = 896309 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/2b/dee1c58bde0a747b2d75fa7282a190885a726fe95b18b8ce1dc52f9c0983/selenium-4.34.2-py3-none-any.whl", hash = "sha256:ea208f7db9e3b26e58c4a817ea9dd29454576d6ea55937d754df079ad588e1ad", size = 9410676 }, +] + +[[package]] +name = "semver" +version = "3.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/d1/d3159231aec234a59dd7d601e9dd9fe96f3afff15efd33c1070019b26132/semver-3.0.4.tar.gz", hash = "sha256:afc7d8c584a5ed0a11033af086e8af226a9c0b206f313e0301f8dd7b6b589602", size = 269730 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a6/24/4d91e05817e92e3a61c8a21e08fd0f390f5301f1c448b137c57c4bc6e543/semver-3.0.4-py3-none-any.whl", hash = "sha256:9c824d87ba7f7ab4a1890799cec8596f15c1241cb473404ea1cb0c55e4b04746", size = 17912 }, +] + +[[package]] +name = "sentry-sdk" +version = "2.34.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3a/38/10d6bfe23df1bfc65ac2262ed10b45823f47f810b0057d3feeea1ca5c7ed/sentry_sdk-2.34.1.tar.gz", hash = "sha256:69274eb8c5c38562a544c3e9f68b5be0a43be4b697f5fd385bf98e4fbe672687", size = 336969 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2d/3e/bb34de65a5787f76848a533afbb6610e01fbcdd59e76d8679c254e02255c/sentry_sdk-2.34.1-py2.py3-none-any.whl", hash = "sha256:b7a072e1cdc5abc48101d5146e1ae680fa81fe886d8d95aaa25a0b450c818d32", size = 357743 }, +] + +[[package]] +name = "serpapi" +version = "0.1.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f0/fa/3fd8809287f3977a3e752bb88610e918d49cb1038b14f4bc51e13e594197/serpapi-0.1.5.tar.gz", hash = "sha256:b9707ed54750fdd2f62dc3a17c6a3fb7fa421dc37902fd65b2263c0ac765a1a5", size = 14191 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/6a/21deade04100d64844e494353a5d65e7971fbdfddf78eb1f248423593ad0/serpapi-0.1.5-py2.py3-none-any.whl", hash = "sha256:6467b6adec1231059f754ccaa952b229efeaa8b9cae6e71f879703ec9e5bb3d1", size = 10966 }, +] + +[[package]] +name = "setuptools" +version = "80.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/5d/3bf57dcd21979b887f014ea83c24ae194cfcd12b9e0fda66b957c69d1fca/setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c", size = 1319958 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", size = 1201486 }, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755 }, +] + +[[package]] +name = "singlestoredb" +version = "1.12.4" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11' and platform_python_implementation == 'PyPy' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and platform_python_implementation == 'PyPy' and sys_platform == 'linux'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and platform_python_implementation == 'PyPy' and sys_platform == 'linux') or (python_full_version < '3.11' and platform_python_implementation == 'PyPy' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version < '3.11' and platform_python_implementation != 'PyPy' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and platform_python_implementation != 'PyPy' and sys_platform == 'linux'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and platform_python_implementation != 'PyPy' and sys_platform == 'linux') or (python_full_version < '3.11' and platform_python_implementation != 'PyPy' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "build", marker = "python_full_version < '3.11'" }, + { name = "parsimonious", marker = "python_full_version < '3.11'" }, + { name = "pyjwt", marker = "python_full_version < '3.11'" }, + { name = "requests", marker = "python_full_version < '3.11'" }, + { name = "setuptools", marker = "python_full_version < '3.11'" }, + { name = "sqlparams", marker = "python_full_version < '3.11'" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "wheel", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/29/6e/8278a773383ccd0adcceaefd767fd48021fedd271d22778add7c7f4b6dca/singlestoredb-1.12.4.tar.gz", hash = "sha256:b64e3a71b5c0a5375af79dc6523a14d6744798f5a2ec884cbbf5613d6672e56a", size = 306450 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d5/fc/2af1e415d8d3aee43b8828712c1772d85b9695835342272e85510c5ba166/singlestoredb-1.12.4-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:59bd60125a94779fc8d86ee462ebe503d2d5dce1f9c7e4dd825fefd8cd02f6bb", size = 389316 }, + { url = "https://files.pythonhosted.org/packages/60/29/a11f5989b2ad62037a2dbe858c7ef91fbeac342243c6d61f31e5adb5e009/singlestoredb-1.12.4-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0089d7dc88eb155adaf195adbe03997e96d3a77e807c3cc99fcfcc2eced4a8c6", size = 426241 }, + { url = "https://files.pythonhosted.org/packages/d4/02/244f896b1c0126733c886c4965ada141a9faaffd0fac0238167725ae3d2a/singlestoredb-1.12.4-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cd6a8d7324fcac24fa9de2b8de5e8c4c0ec6986784597656f436ead52632c236", size = 428570 }, + { url = "https://files.pythonhosted.org/packages/2c/40/971eacb90dc0299c311c4df0063d0a358f7099c9171a30c0ff2f899a391c/singlestoredb-1.12.4-cp38-abi3-win32.whl", hash = "sha256:ffab0550b6b64447b02d0404ade357a9b8775b3053e6b0ea7c778d663879a184", size = 367194 }, + { url = "https://files.pythonhosted.org/packages/02/93/984fca3bf8c05d6588d54c99f127e26f679008f986a3262183a3759aa6bf/singlestoredb-1.12.4-cp38-abi3-win_amd64.whl", hash = "sha256:340b34c481dcbd8ace404dfbcf4b251363b0f133c8bf4b4e5762d82b32a07191", size = 365909 }, + { url = "https://files.pythonhosted.org/packages/2d/db/2c598597983637cac218a2b81c7c5f08d28669fa318a97c8c9c0249fa3a6/singlestoredb-1.12.4-py3-none-any.whl", hash = "sha256:0d98d626363d6b354c0f9fb3c706bfa0b7ba48365704b31b13ff9f7e1598f4db", size = 336023 }, +] + +[[package]] +name = "singlestoredb" +version = "1.14.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.11.*' and platform_python_implementation == 'PyPy' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and platform_python_implementation == 'PyPy' and sys_platform == 'linux'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and platform_python_implementation == 'PyPy' and sys_platform == 'linux') or (python_full_version == '3.11.*' and platform_python_implementation == 'PyPy' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version == '3.11.*' and platform_python_implementation != 'PyPy' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and platform_python_implementation != 'PyPy' and sys_platform == 'linux'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and platform_python_implementation != 'PyPy' and sys_platform == 'linux') or (python_full_version == '3.11.*' and platform_python_implementation != 'PyPy' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "build", marker = "python_full_version == '3.11.*'" }, + { name = "parsimonious", marker = "python_full_version == '3.11.*'" }, + { name = "pyjwt", marker = "python_full_version == '3.11.*'" }, + { name = "requests", marker = "python_full_version == '3.11.*'" }, + { name = "setuptools", marker = "python_full_version == '3.11.*'" }, + { name = "sqlparams", marker = "python_full_version == '3.11.*'" }, + { name = "wheel", marker = "python_full_version == '3.11.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f8/8a/be48b3f6158cf59f99d15b898b53e08e79f427bf60ac64b4b002f49ab6e3/singlestoredb-1.14.2.tar.gz", hash = "sha256:fc947ea7cb99f6bd0a7d027a8e27d3b5f80e35aac7f24f3fb31df81b39e611ba", size = 324031 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/70/48/36bd3b6813e770314b277f01b4bd91c010ff3522c684b5a7d894aad128d2/singlestoredb-1.14.2-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:b86ce1d156c099d4a4453f7c663334e197572d11db2c2a5d59128ea43e9a6f38", size = 411027 }, + { url = "https://files.pythonhosted.org/packages/e1/0c/dadc95430357bdd96458e271b7cc448fab6917341792fe2141ffa16cdc50/singlestoredb-1.14.2-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:638f27f256004f1fff4d7f71dee556025deb55a616524d06048988c180bc4b40", size = 449250 }, + { url = "https://files.pythonhosted.org/packages/5e/74/5ebc6f67a5e0b12e8e4ac4788293f07f4e5757876a08600cc379d2e22c02/singlestoredb-1.14.2-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd3e0fa7b097644e739da0c2039903caa46a9c08b758f38cc237923a63c23df4", size = 450664 }, + { url = "https://files.pythonhosted.org/packages/be/54/1859f4212bcbb8dc48cf0c62824cb139bbdc979f19fadc682db609822f0e/singlestoredb-1.14.2-cp38-abi3-win32.whl", hash = "sha256:eac51596b54c6526f414a8fb1a52945964ad24462c4890de73124beded61a7b2", size = 388670 }, + { url = "https://files.pythonhosted.org/packages/a3/fc/53413119ab061c8dddc734024190d707df60f82b0d441054800cb1ce6999/singlestoredb-1.14.2-cp38-abi3-win_amd64.whl", hash = "sha256:854fec1fd6be5801759fdbef6c763103593e46c3d5aad8028575a3e2eb788eea", size = 387061 }, + { url = "https://files.pythonhosted.org/packages/68/6e/3c35f345fa230036a09c1ffa276b1e8b524a97b92d2b6e961f857d56c29c/singlestoredb-1.14.2-py3-none-any.whl", hash = "sha256:06fa7e3a2006c6980c1e034fef0f88aa0281be13d3604434a4d5f47d8b42369d", size = 356036 }, +] + +[[package]] +name = "singlestoredb" +version = "1.15.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and platform_python_implementation == 'PyPy' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and platform_python_implementation == 'PyPy' and sys_platform == 'linux'", + "(python_full_version >= '3.13' and platform_machine != 'aarch64' and platform_python_implementation == 'PyPy' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_python_implementation == 'PyPy' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version >= '3.13' and platform_python_implementation != 'PyPy' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and platform_python_implementation != 'PyPy' and sys_platform == 'linux'", + "(python_full_version >= '3.13' and platform_machine != 'aarch64' and platform_python_implementation != 'PyPy' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_python_implementation != 'PyPy' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version >= '3.12.4' and python_full_version < '3.13' and platform_python_implementation == 'PyPy' and sys_platform == 'darwin'", + "python_full_version >= '3.12.4' and python_full_version < '3.13' and platform_machine == 'aarch64' and platform_python_implementation == 'PyPy' and sys_platform == 'linux'", + "(python_full_version >= '3.12.4' and python_full_version < '3.13' and platform_machine != 'aarch64' and platform_python_implementation == 'PyPy' and sys_platform == 'linux') or (python_full_version >= '3.12.4' and python_full_version < '3.13' and platform_python_implementation == 'PyPy' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version >= '3.12.4' and python_full_version < '3.13' and platform_python_implementation != 'PyPy' and sys_platform == 'darwin'", + "python_full_version >= '3.12.4' and python_full_version < '3.13' and platform_machine == 'aarch64' and platform_python_implementation != 'PyPy' and sys_platform == 'linux'", + "(python_full_version >= '3.12.4' and python_full_version < '3.13' and platform_machine != 'aarch64' and platform_python_implementation != 'PyPy' and sys_platform == 'linux') or (python_full_version >= '3.12.4' and python_full_version < '3.13' and platform_python_implementation != 'PyPy' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version >= '3.12' and python_full_version < '3.12.4' and platform_python_implementation == 'PyPy' and sys_platform == 'darwin'", + "python_full_version >= '3.12' and python_full_version < '3.12.4' and platform_machine == 'aarch64' and platform_python_implementation == 'PyPy' and sys_platform == 'linux'", + "(python_full_version >= '3.12' and python_full_version < '3.12.4' and platform_machine != 'aarch64' and platform_python_implementation == 'PyPy' and sys_platform == 'linux') or (python_full_version >= '3.12' and python_full_version < '3.12.4' and platform_python_implementation == 'PyPy' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version >= '3.12' and python_full_version < '3.12.4' and platform_python_implementation != 'PyPy' and sys_platform == 'darwin'", + "python_full_version >= '3.12' and python_full_version < '3.12.4' and platform_machine == 'aarch64' and platform_python_implementation != 'PyPy' and sys_platform == 'linux'", + "(python_full_version >= '3.12' and python_full_version < '3.12.4' and platform_machine != 'aarch64' and platform_python_implementation != 'PyPy' and sys_platform == 'linux') or (python_full_version >= '3.12' and python_full_version < '3.12.4' and platform_python_implementation != 'PyPy' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "build", marker = "python_full_version >= '3.12'" }, + { name = "parsimonious", marker = "python_full_version >= '3.12'" }, + { name = "pyjwt", marker = "python_full_version >= '3.12'" }, + { name = "requests", marker = "python_full_version >= '3.12'" }, + { name = "setuptools", marker = "python_full_version >= '3.12'" }, + { name = "sqlparams", marker = "python_full_version >= '3.12'" }, + { name = "wheel", marker = "python_full_version >= '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ef/34/f4e0453479f4f65d4d93418ceade369f2822532b59b57533ca65c2dfacda/singlestoredb-1.15.0.tar.gz", hash = "sha256:9eca4ee68942a55680694b0a9c42869df95982d3fe43fa573894e341aba6c698", size = 330920 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a6/3f/a31e1b54dc4c35b2ab9a7a6d15f0c1529ef02fdec49e6de961f31e543dab/singlestoredb-1.15.0-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:30e9432febe831784cb363135cd1103ce0065a05c834901b021977bffb5f1a74", size = 420312 }, + { url = "https://files.pythonhosted.org/packages/05/da/07f4ffeb32ab28600a1fd74a422e5ffdd712d882de60cc8cd6b673b6b221/singlestoredb-1.15.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6641ca8479c836fe80440cbfc2c082b41b2c176990504db86ed65a60368873cf", size = 458513 }, + { url = "https://files.pythonhosted.org/packages/f5/55/dc8355415f21f348d028af3039eef9ed3dcbaa0893345a6c843f9a63b551/singlestoredb-1.15.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:859a52695818ffa4de1aae31345b32e1b4bcfd9565e708fe7966b33e6eef48f9", size = 459925 }, + { url = "https://files.pythonhosted.org/packages/3e/4e/94c8428567efe8e08be038d4da8477970bcf26a908589f18a35ec776ff68/singlestoredb-1.15.0-cp38-abi3-win32.whl", hash = "sha256:66508c0e1a2b2832aaef8c676ae503a74a98c61bafbed7c287cc8ba49be43825", size = 397812 }, + { url = "https://files.pythonhosted.org/packages/b8/38/ae2867044880b060c7cc058302caea5666cda4a630cc53168016a83bac33/singlestoredb-1.15.0-cp38-abi3-win_amd64.whl", hash = "sha256:9b60ea1a56d459ff3d2d2bdf9c38ac48564e7e86de6b0679adcab7e139396056", size = 396543 }, + { url = "https://files.pythonhosted.org/packages/48/94/ca0d35fe00a0263ded58e9188c58a2b7b6f8e8c2f5f42591d806f98e3aa2/singlestoredb-1.15.0-py3-none-any.whl", hash = "sha256:b9abbf700a56e9db23531c99365b4ade8e0107dd63dacbc6ae41db9c8954bb1c", size = 365184 }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050 }, +] + +[[package]] +name = "smmap" +version = "5.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/44/cd/a040c4b3119bbe532e5b0732286f805445375489fceaec1f48306068ee3b/smmap-5.0.2.tar.gz", hash = "sha256:26ea65a03958fa0c8a1c7e8c7a58fdc77221b8910f6be2131affade476898ad5", size = 22329 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/be/d09147ad1ec7934636ad912901c5fd7667e1c858e19d355237db0d0cd5e4/smmap-5.0.2-py3-none-any.whl", hash = "sha256:b30115f0def7d7531d22a0fb6502488d879e75b260a9db4d0819cfb25403af5e", size = 24303 }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, +] + +[[package]] +name = "snowflake-connector-python" +version = "3.16.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "asn1crypto" }, + { name = "boto3" }, + { name = "botocore" }, + { name = "certifi" }, + { name = "cffi" }, + { name = "charset-normalizer" }, + { name = "cryptography" }, + { name = "filelock" }, + { name = "idna" }, + { name = "packaging" }, + { name = "platformdirs" }, + { name = "pyjwt" }, + { name = "pyopenssl" }, + { name = "pytz" }, + { name = "requests" }, + { name = "sortedcontainers" }, + { name = "tomlkit" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c3/31/b80afde8830afef0871dbcb4912545f326958c4d2ce36187c668fecb1913/snowflake_connector_python-3.16.0.tar.gz", hash = "sha256:88ca9438cc44cbd0bc078ecdf3273bd25bb69e3255c0416647281c5b2f490bb5", size = 779782 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e2/6f/d7f3f75424a5e594a5fc5b201908df1bfa69243550802bf9af7b2fa52040/snowflake_connector_python-3.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:40928f889159e9af4d3057688bbb73559b1b67b104ebe6ee55cb2e727df52dab", size = 993097 }, + { url = "https://files.pythonhosted.org/packages/80/11/d44b0681a0b8a060a87211c59fe4070be307e2c5443e35450a58f024b019/snowflake_connector_python-3.16.0-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:fbfe84ffb69a94793481c6f26695a65ca07010f37866d086677f4addda78594b", size = 1005698 }, + { url = "https://files.pythonhosted.org/packages/99/bb/33dc75ee0ebf948436b1a7e20a359f3aabc3b8f415cb98e9ebfe9cce728f/snowflake_connector_python-3.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:675d1d87154521e45c914aea5cfa48674338411c72a89dc3fd06075f8424f795", size = 2618897 }, + { url = "https://files.pythonhosted.org/packages/96/b7/32ad953cadf11a082cfe2f5f40aa8a3bd1dfb09d9f625b627c61a67483e4/snowflake_connector_python-3.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:38a49468a3746078dbcc7afeb708e83031e11beb4a2681ac786a57ebee232b78", size = 2641035 }, + { url = "https://files.pythonhosted.org/packages/48/f1/674b5d5dd58917098ab112be30ec583f92fc5540c83583920eccad3390ed/snowflake_connector_python-3.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:2aad699a82278ea1f61570ff89650315b82bed71cc84ba8c1d8229af06100332", size = 951648 }, + { url = "https://files.pythonhosted.org/packages/cf/40/2b5d1df656564f53952c94f176db5362c3dd8f1cffd00695235df57bf44e/snowflake_connector_python-3.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ab3e19123b60305347b3486566000a0a05ac4540e6e5eaf8e31a03b6c6de0132", size = 993304 }, + { url = "https://files.pythonhosted.org/packages/3d/97/9276ed9be967f1b5e926c31fd92138366a2d7110d5713ce5b351095034a4/snowflake_connector_python-3.16.0-cp311-cp311-macosx_11_0_x86_64.whl", hash = "sha256:9a153cb2ec837381cdeb4ac94c99d18aabfb495a5afe4769477b4b456cbd8c1a", size = 1005637 }, + { url = "https://files.pythonhosted.org/packages/d6/c4/1314043eb41794391f8398190c01e1197d0d3042c7f6c159ba1318d0dcec/snowflake_connector_python-3.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4db863e23edbc1439b3f09ff682abdd551b45c12e1c44a969a7631fefa5e87ee", size = 2632106 }, + { url = "https://files.pythonhosted.org/packages/c3/6c/f36774bc03be5d892c5241d790c34d458253c9a3ae6943b649c84c3fc813/snowflake_connector_python-3.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c555263c61fd126eb8a795034dcf5e56df42e2e7b3cdf0823f6e61eae425ef65", size = 2653236 }, + { url = "https://files.pythonhosted.org/packages/5e/11/e70896521ff209969fbe75d0811182f9b59ab84b00951aaa4d2a4b5854a2/snowflake_connector_python-3.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:dd8f5f20066ccc140911ba96c77825cbcb1c2b59c189eab561f616e75d59c831", size = 951746 }, + { url = "https://files.pythonhosted.org/packages/ce/ce/2b4799464b76b36c9f8148fed51abc7c1cc2b92360130807e8ba150dcad2/snowflake_connector_python-3.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1943f3d682a516ef1217ffb137e583d4c4a200f286cc77ef8a8a53cb04b0a727", size = 992346 }, + { url = "https://files.pythonhosted.org/packages/e3/ea/100b3fa7a9fad4544013c49e34ba918f6ad0a43d8c184ea9f6a28995599c/snowflake_connector_python-3.16.0-cp312-cp312-macosx_11_0_x86_64.whl", hash = "sha256:9067363257a55bc020df956a5977237c7e1ea5bfbee3d0f2b1037c370eed0331", size = 1003984 }, + { url = "https://files.pythonhosted.org/packages/4d/fd/2678f518e47b4cd22b3e4e9f15121d15ef08d3efdca8860f1535cecc4b22/snowflake_connector_python-3.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5af8c6294a70c12d883b56261e79b2a9b7e8318df4b2671adfc0ecc593f71fce", size = 2652814 }, + { url = "https://files.pythonhosted.org/packages/dd/a6/7d37bdff77b1419e95280f2c1f39f77fb4f3d449790acb032296eb698807/snowflake_connector_python-3.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c919abb9dc67f3ed3ca82c396a666760a4a3eefdc33947e3fa8fd4d4aafd796", size = 2676671 }, + { url = "https://files.pythonhosted.org/packages/d5/c0/c02ed21e1fb46a481b656dac58b7c60644ad57745ba2a46cc8cd172944d7/snowflake_connector_python-3.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:f4a5ba8ffa1bfcaf6f4bca41115711dcc938783e1f4e93ba6a155d07caa43e19", size = 950687 }, + { url = "https://files.pythonhosted.org/packages/52/90/941f75e97cbea26f5cb90bce9a1fd8e3315de98460bdd6a5a9bf43048994/snowflake_connector_python-3.16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b0a165e6a29c44bdeee5b23cbec0880fa23e7f099257737e292dce5d678a08c4", size = 993289 }, + { url = "https://files.pythonhosted.org/packages/d1/52/f356d2ddd56d595883a9fd91da99632535d7ae6d8b82ea55a8605d60c33c/snowflake_connector_python-3.16.0-cp313-cp313-macosx_11_0_x86_64.whl", hash = "sha256:136ea110211958a3748c0e562051d25bd87b3aa58948cdc0816dfdf08afbb8fc", size = 1004683 }, + { url = "https://files.pythonhosted.org/packages/84/19/856c19da0fb001bf4fd46a239ab33cd8b03ec4651d482cfa1354f1966945/snowflake_connector_python-3.16.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41e861b3d6abfe927639554cb81ff2c70bc12691e8ac9a2919b863e805c6caf8", size = 2655306 }, + { url = "https://files.pythonhosted.org/packages/fb/ed/90cc1776d3b5c9eb8f7c4e0203c3aa6cc9199920e30c53b937b5ff159774/snowflake_connector_python-3.16.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6474a2617c609461cbfa4122862aec1ba12ece2bb16505032dad4457d38e13b9", size = 2678862 }, + { url = "https://files.pythonhosted.org/packages/87/6a/dcad484e2a6bcd26fe641659f2f09f598068cedc0a351cb09fc0a9939e30/snowflake_connector_python-3.16.0-cp313-cp313-win_amd64.whl", hash = "sha256:bab36467b6a9f696ae9fbae35d733557724d70ab779d89fe430844e876ce02c6", size = 950736 }, +] + +[[package]] +name = "snowflake-sqlalchemy" +version = "1.7.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "snowflake-connector-python" }, + { name = "sqlalchemy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ec/5c/b8fb0c3d635f2a3eeed705c86571e6a1311e325db478d877e3f1115bdeca/snowflake_sqlalchemy-1.7.6.tar.gz", hash = "sha256:afb0d058da752342e08d747fb8384207a34e1a402130f7febdc454c50e1ddb7c", size = 119658 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/9b/2eb13e877636311122602c3c0efa20cb0b1233dbc460803f30b8fd294999/snowflake_sqlalchemy-1.7.6-py3-none-any.whl", hash = "sha256:ce472b90755e9f7cc1ff4943e27ff4732bcf860e812249bea7491090b85d2b91", size = 70518 }, +] + +[[package]] +name = "sortedcontainers" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575 }, +] + +[[package]] +name = "soupsieve" +version = "2.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3f/f4/4a80cd6ef364b2e8b65b15816a843c0980f7a5a2b4dc701fc574952aa19f/soupsieve-2.7.tar.gz", hash = "sha256:ad282f9b6926286d2ead4750552c8a6142bc4c783fd66b0293547c8fe6ae126a", size = 103418 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/9c/0e6afc12c269578be5c0c1c9f4b49a8d32770a080260c333ac04cc1c832d/soupsieve-2.7-py3-none-any.whl", hash = "sha256:6e60cc5c1ffaf1cebcc12e8188320b72071e922c2e897f737cadce79ad5d30c4", size = 36677 }, +] + +[[package]] +name = "spider-client" +version = "0.1.71" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, + { name = "ijson" }, + { name = "requests" }, + { name = "tenacity" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/23/54608c7ca1abc07d46ebcd5cdd81a20e6f71613708da2f503e04a84adb21/spider_client-0.1.71.tar.gz", hash = "sha256:b81225c8d958969b5030488278a3720b5bdd75ab85751e69e73133c5865b206c", size = 16492 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/20/6934f871fa5f5bf52d1eaad25b584eeb03b07aafe94d782543ac3d160faa/spider_client-0.1.71-py3-none-any.whl", hash = "sha256:addc00203e3b11e73ef1effc81d561e95e1aef31eea902f6f166ae771966ce45", size = 14070 }, +] + +[[package]] +name = "sqlalchemy" +version = "2.0.42" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5a/03/a0af991e3a43174d6b83fca4fb399745abceddd1171bdabae48ce877ff47/sqlalchemy-2.0.42.tar.gz", hash = "sha256:160bedd8a5c28765bd5be4dec2d881e109e33b34922e50a3b881a7681773ac5f", size = 9749972 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/12/33ff43214c2c6cc87499b402fe419869d2980a08101c991daae31345e901/sqlalchemy-2.0.42-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:172b244753e034d91a826f80a9a70f4cbac690641207f2217f8404c261473efe", size = 2130469 }, + { url = "https://files.pythonhosted.org/packages/63/c4/4d2f2c21ddde9a2c7f7b258b202d6af0bac9fc5abfca5de367461c86d766/sqlalchemy-2.0.42-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:be28f88abd74af8519a4542185ee80ca914933ca65cdfa99504d82af0e4210df", size = 2120393 }, + { url = "https://files.pythonhosted.org/packages/a8/0d/5ff2f2dfbac10e4a9ade1942f8985ffc4bd8f157926b1f8aed553dfe3b88/sqlalchemy-2.0.42-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:98b344859d282fde388047f1710860bb23f4098f705491e06b8ab52a48aafea9", size = 3206173 }, + { url = "https://files.pythonhosted.org/packages/1f/59/71493fe74bd76a773ae8fa0c50bfc2ccac1cbf7cfa4f9843ad92897e6dcf/sqlalchemy-2.0.42-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97978d223b11f1d161390a96f28c49a13ce48fdd2fed7683167c39bdb1b8aa09", size = 3206910 }, + { url = "https://files.pythonhosted.org/packages/a9/51/01b1d85bbb492a36b25df54a070a0f887052e9b190dff71263a09f48576b/sqlalchemy-2.0.42-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e35b9b000c59fcac2867ab3a79fc368a6caca8706741beab3b799d47005b3407", size = 3145479 }, + { url = "https://files.pythonhosted.org/packages/fa/78/10834f010e2a3df689f6d1888ea6ea0074ff10184e6a550b8ed7f9189a89/sqlalchemy-2.0.42-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:bc7347ad7a7b1c78b94177f2d57263113bb950e62c59b96ed839b131ea4234e1", size = 3169605 }, + { url = "https://files.pythonhosted.org/packages/0c/75/e6fdd66d237582c8488dd1dfa90899f6502822fbd866363ab70e8ac4a2ce/sqlalchemy-2.0.42-cp310-cp310-win32.whl", hash = "sha256:739e58879b20a179156b63aa21f05ccacfd3e28e08e9c2b630ff55cd7177c4f1", size = 2098759 }, + { url = "https://files.pythonhosted.org/packages/a5/a8/366db192641c2c2d1ea8977e7c77b65a0d16a7858907bb76ea68b9dd37af/sqlalchemy-2.0.42-cp310-cp310-win_amd64.whl", hash = "sha256:1aef304ada61b81f1955196f584b9e72b798ed525a7c0b46e09e98397393297b", size = 2122423 }, + { url = "https://files.pythonhosted.org/packages/ea/3c/7bfd65f3c2046e2fb4475b21fa0b9d7995f8c08bfa0948df7a4d2d0de869/sqlalchemy-2.0.42-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c34100c0b7ea31fbc113c124bcf93a53094f8951c7bf39c45f39d327bad6d1e7", size = 2133779 }, + { url = "https://files.pythonhosted.org/packages/66/17/19be542fe9dd64a766090e90e789e86bdaa608affda6b3c1e118a25a2509/sqlalchemy-2.0.42-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ad59dbe4d1252448c19d171dfba14c74e7950b46dc49d015722a4a06bfdab2b0", size = 2123843 }, + { url = "https://files.pythonhosted.org/packages/14/fc/83e45fc25f0acf1c26962ebff45b4c77e5570abb7c1a425a54b00bcfa9c7/sqlalchemy-2.0.42-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9187498c2149919753a7fd51766ea9c8eecdec7da47c1b955fa8090bc642eaa", size = 3294824 }, + { url = "https://files.pythonhosted.org/packages/b9/81/421efc09837104cd1a267d68b470e5b7b6792c2963b8096ca1e060ba0975/sqlalchemy-2.0.42-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f092cf83ebcafba23a247f5e03f99f5436e3ef026d01c8213b5eca48ad6efa9", size = 3294662 }, + { url = "https://files.pythonhosted.org/packages/2f/ba/55406e09d32ed5e5f9e8aaec5ef70c4f20b4ae25b9fa9784f4afaa28e7c3/sqlalchemy-2.0.42-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fc6afee7e66fdba4f5a68610b487c1f754fccdc53894a9567785932dbb6a265e", size = 3229413 }, + { url = "https://files.pythonhosted.org/packages/d4/c4/df596777fce27bde2d1a4a2f5a7ddea997c0c6d4b5246aafba966b421cc0/sqlalchemy-2.0.42-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:260ca1d2e5910f1f1ad3fe0113f8fab28657cee2542cb48c2f342ed90046e8ec", size = 3255563 }, + { url = "https://files.pythonhosted.org/packages/16/ed/b9c4a939b314400f43f972c9eb0091da59d8466ef9c51d0fd5b449edc495/sqlalchemy-2.0.42-cp311-cp311-win32.whl", hash = "sha256:2eb539fd83185a85e5fcd6b19214e1c734ab0351d81505b0f987705ba0a1e231", size = 2098513 }, + { url = "https://files.pythonhosted.org/packages/91/72/55b0c34e39feb81991aa3c974d85074c356239ac1170dfb81a474b4c23b3/sqlalchemy-2.0.42-cp311-cp311-win_amd64.whl", hash = "sha256:9193fa484bf00dcc1804aecbb4f528f1123c04bad6a08d7710c909750fa76aeb", size = 2123380 }, + { url = "https://files.pythonhosted.org/packages/61/66/ac31a9821fc70a7376321fb2c70fdd7eadbc06dadf66ee216a22a41d6058/sqlalchemy-2.0.42-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:09637a0872689d3eb71c41e249c6f422e3e18bbd05b4cd258193cfc7a9a50da2", size = 2132203 }, + { url = "https://files.pythonhosted.org/packages/fc/ba/fd943172e017f955d7a8b3a94695265b7114efe4854feaa01f057e8f5293/sqlalchemy-2.0.42-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a3cb3ec67cc08bea54e06b569398ae21623534a7b1b23c258883a7c696ae10df", size = 2120373 }, + { url = "https://files.pythonhosted.org/packages/ea/a2/b5f7d233d063ffadf7e9fff3898b42657ba154a5bec95a96f44cba7f818b/sqlalchemy-2.0.42-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e87e6a5ef6f9d8daeb2ce5918bf5fddecc11cae6a7d7a671fcc4616c47635e01", size = 3317685 }, + { url = "https://files.pythonhosted.org/packages/86/00/fcd8daab13a9119d41f3e485a101c29f5d2085bda459154ba354c616bf4e/sqlalchemy-2.0.42-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b718011a9d66c0d2f78e1997755cd965f3414563b31867475e9bc6efdc2281d", size = 3326967 }, + { url = "https://files.pythonhosted.org/packages/a3/85/e622a273d648d39d6771157961956991a6d760e323e273d15e9704c30ccc/sqlalchemy-2.0.42-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:16d9b544873fe6486dddbb859501a07d89f77c61d29060bb87d0faf7519b6a4d", size = 3255331 }, + { url = "https://files.pythonhosted.org/packages/3a/a0/2c2338b592c7b0a61feffd005378c084b4c01fabaf1ed5f655ab7bd446f0/sqlalchemy-2.0.42-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:21bfdf57abf72fa89b97dd74d3187caa3172a78c125f2144764a73970810c4ee", size = 3291791 }, + { url = "https://files.pythonhosted.org/packages/41/19/b8a2907972a78285fdce4c880ecaab3c5067eb726882ca6347f7a4bf64f6/sqlalchemy-2.0.42-cp312-cp312-win32.whl", hash = "sha256:78b46555b730a24901ceb4cb901c6b45c9407f8875209ed3c5d6bcd0390a6ed1", size = 2096180 }, + { url = "https://files.pythonhosted.org/packages/48/1f/67a78f3dfd08a2ed1c7be820fe7775944f5126080b5027cc859084f8e223/sqlalchemy-2.0.42-cp312-cp312-win_amd64.whl", hash = "sha256:4c94447a016f36c4da80072e6c6964713b0af3c8019e9c4daadf21f61b81ab53", size = 2123533 }, + { url = "https://files.pythonhosted.org/packages/e9/7e/25d8c28b86730c9fb0e09156f601d7a96d1c634043bf8ba36513eb78887b/sqlalchemy-2.0.42-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:941804f55c7d507334da38133268e3f6e5b0340d584ba0f277dd884197f4ae8c", size = 2127905 }, + { url = "https://files.pythonhosted.org/packages/e5/a1/9d8c93434d1d983880d976400fcb7895a79576bd94dca61c3b7b90b1ed0d/sqlalchemy-2.0.42-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:95d3d06a968a760ce2aa6a5889fefcbdd53ca935735e0768e1db046ec08cbf01", size = 2115726 }, + { url = "https://files.pythonhosted.org/packages/a2/cc/d33646fcc24c87cc4e30a03556b611a4e7bcfa69a4c935bffb923e3c89f4/sqlalchemy-2.0.42-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4cf10396a8a700a0f38ccd220d940be529c8f64435c5d5b29375acab9267a6c9", size = 3246007 }, + { url = "https://files.pythonhosted.org/packages/67/08/4e6c533d4c7f5e7c4cbb6fe8a2c4e813202a40f05700d4009a44ec6e236d/sqlalchemy-2.0.42-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9cae6c2b05326d7c2c7c0519f323f90e0fb9e8afa783c6a05bb9ee92a90d0f04", size = 3250919 }, + { url = "https://files.pythonhosted.org/packages/5c/82/f680e9a636d217aece1b9a8030d18ad2b59b5e216e0c94e03ad86b344af3/sqlalchemy-2.0.42-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f50f7b20677b23cfb35b6afcd8372b2feb348a38e3033f6447ee0704540be894", size = 3180546 }, + { url = "https://files.pythonhosted.org/packages/7d/a2/8c8f6325f153894afa3775584c429cc936353fb1db26eddb60a549d0ff4b/sqlalchemy-2.0.42-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9d88a1c0d66d24e229e3938e1ef16ebdbd2bf4ced93af6eff55225f7465cf350", size = 3216683 }, + { url = "https://files.pythonhosted.org/packages/39/44/3a451d7fa4482a8ffdf364e803ddc2cfcafc1c4635fb366f169ecc2c3b11/sqlalchemy-2.0.42-cp313-cp313-win32.whl", hash = "sha256:45c842c94c9ad546c72225a0c0d1ae8ef3f7c212484be3d429715a062970e87f", size = 2093990 }, + { url = "https://files.pythonhosted.org/packages/4b/9e/9bce34f67aea0251c8ac104f7bdb2229d58fb2e86a4ad8807999c4bee34b/sqlalchemy-2.0.42-cp313-cp313-win_amd64.whl", hash = "sha256:eb9905f7f1e49fd57a7ed6269bc567fcbbdac9feadff20ad6bd7707266a91577", size = 2120473 }, + { url = "https://files.pythonhosted.org/packages/ee/55/ba2546ab09a6adebc521bf3974440dc1d8c06ed342cceb30ed62a8858835/sqlalchemy-2.0.42-py3-none-any.whl", hash = "sha256:defcdff7e661f0043daa381832af65d616e060ddb54d3fe4476f51df7eaa1835", size = 1922072 }, +] + +[[package]] +name = "sqlparams" +version = "6.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/76/ec/5d6a5ca217ecd7b08d404b7dc2025c752bdb393c9b34fcc6d48e1f70bb7e/sqlparams-6.2.0.tar.gz", hash = "sha256:3744a2ad16f71293db6505b21fd5229b4757489a9b09f3553656a1ae97ba7ca5", size = 34932 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/97/e2/f1355629bb1eeb274babc947e2ba4e2e49250e934c86adcce3e54943bc8a/sqlparams-6.2.0-py3-none-any.whl", hash = "sha256:63b32ed9051bdc52e7e8b38bc4f78aed51796cdd9135e730f4c6a7db1048dedf", size = 17629 }, +] + +[[package]] +name = "sse-starlette" +version = "3.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/6f/22ed6e33f8a9e76ca0a412405f31abb844b779d52c5f96660766edcd737c/sse_starlette-3.0.2.tar.gz", hash = "sha256:ccd60b5765ebb3584d0de2d7a6e4f745672581de4f5005ab31c3a25d10b52b3a", size = 20985 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/10/c78f463b4ef22eef8491f218f692be838282cd65480f6e423d7730dfd1fb/sse_starlette-3.0.2-py3-none-any.whl", hash = "sha256:16b7cbfddbcd4eaca11f7b586f3b8a080f1afe952c15813455b162edea619e5a", size = 11297 }, +] + +[[package]] +name = "stack-data" +version = "0.6.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "asttokens" }, + { name = "executing" }, + { name = "pure-eval" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/28/e3/55dcc2cfbc3ca9c29519eb6884dd1415ecb53b0e934862d3559ddcb7e20b/stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9", size = 44707 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695", size = 24521 }, +] + +[[package]] +name = "stagehand" +version = "0.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anthropic" }, + { name = "browserbase" }, + { name = "httpx" }, + { name = "litellm" }, + { name = "openai" }, + { name = "playwright" }, + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "requests" }, + { name = "rich" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b8/36/e1e5f5c1048e345bc4b09cdaa638134c613f8c6d056b32ac542a7f38c91e/stagehand-0.5.0.tar.gz", hash = "sha256:58d11bc05178033e0f224c2d7969cff8945d0e5b1416dc88b30e4d578f309cdc", size = 90959 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/5c/9adaf1c9ee3457d906d84071a705cbe22583ab581d533c6483251feaef60/stagehand-0.5.0-py3-none-any.whl", hash = "sha256:4b7a61e414c8680ed601d7b3ddc1ea46b4b308d649a286f65db0f17b28f19a68", size = 102142 }, +] + +[[package]] +name = "starlette" +version = "0.47.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/04/57/d062573f391d062710d4088fa1369428c38d51460ab6fedff920efef932e/starlette-0.47.2.tar.gz", hash = "sha256:6ae9aa5db235e4846decc1e7b79c4f346adf41e9777aebeb49dfd09bbd7023d8", size = 2583948 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/1f/b876b1f83aef204198a42dc101613fefccb32258e5428b5f9259677864b4/starlette-0.47.2-py3-none-any.whl", hash = "sha256:c5847e96134e5c5371ee9fac6fdf1a67336d5815e09eb2a01fdb57a351ef915b", size = 72984 }, +] + +[[package]] +name = "sympy" +version = "1.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mpmath" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/83/d3/803453b36afefb7c2bb238361cd4ae6125a569b4db67cd9e79846ba2d68c/sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517", size = 7793921 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353 }, +] + +[[package]] +name = "tabulate" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/fe/802052aecb21e3797b8f7902564ab6ea0d60ff8ca23952079064155d1ae1/tabulate-0.9.0.tar.gz", hash = "sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c", size = 81090 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/44/4a5f08c96eb108af5cb50b41f76142f0afa346dfa99d5296fe7202a11854/tabulate-0.9.0-py3-none-any.whl", hash = "sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f", size = 35252 }, +] + +[[package]] +name = "tavily-python" +version = "0.7.10" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, + { name = "requests" }, + { name = "tiktoken" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/04/0e/d4aa0f4dec298298b510ee5209f5ff29352bbbba106fd7ea0221ba8840dc/tavily_python-0.7.10.tar.gz", hash = "sha256:c87b4c0549ab2e416cf4ac3da8fe3ce5db106288408b06e197d4b5ba8ec7ead9", size = 19275 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/60/4c4678a28b3b5061aa2ab45b215290d3a71810e7996bafdf6b7313e75fb3/tavily_python-0.7.10-py3-none-any.whl", hash = "sha256:a99958e14dd091271611be7fb1e1a8a86f5bff3a9022b9626f4c4f1513338088", size = 15786 }, +] + +[[package]] +name = "tenacity" +version = "9.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0a/d4/2b0cd0fe285e14b36db076e78c93766ff1d529d70408bd1d2a5a84f1d929/tenacity-9.1.2.tar.gz", hash = "sha256:1169d376c297e7de388d18b4481760d478b0e99a777cad3a9c86e556f4b697cb", size = 48036 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/30/643397144bfbfec6f6ef821f36f33e57d35946c44a2352d3c9f0ae847619/tenacity-9.1.2-py3-none-any.whl", hash = "sha256:f77bf36710d8b73a50b2dd155c97b870017ad21afe6ab300326b0371b3b05138", size = 28248 }, +] + +[[package]] +name = "tiktoken" +version = "0.10.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "regex" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fd/6a/e0f66d5cf612979fec2c18f9069962c73ed51949403967ba0de9ea6859e5/tiktoken-0.10.0.tar.gz", hash = "sha256:7cd88c11699b18081822e6ae1beee55e8f20ea361d73c507d33f5a89a1898f1c", size = 36451 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/69/68ac82e68cd03df75b4fbe280331ad5f1bab879464d679c5b37fdafcb4e6/tiktoken-0.10.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:1db7a65b196d757d18ef53193957d44549b88f373d4b87db532f04d18193b847", size = 1047220 }, + { url = "https://files.pythonhosted.org/packages/e4/1a/6396b6987d6dd1508fddc645fd41ae1b505c4afd2c5ed310f28fb7fbc2b6/tiktoken-0.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f55701461267d025597ebb2290d612fe9c5c5fbb625ebf7495c9f0f8e4c30f01", size = 989183 }, + { url = "https://files.pythonhosted.org/packages/d0/75/1ddd7ee07332966a5686c5dab91bb862d41edc7b144b405104c6e5993f41/tiktoken-0.10.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f83693279af9e8deac0363cbf21dc3d666807f22dcc1091f51e69e6fe6433f71", size = 1120360 }, + { url = "https://files.pythonhosted.org/packages/a1/14/6083daba660141b580b7da6ee2b3ed7084c73822ad52739071164ea26786/tiktoken-0.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f973bdeb68d645f73dcce60c7795cb5c6f8b7f3dcf92c40c39ad4aee398c075", size = 1175483 }, + { url = "https://files.pythonhosted.org/packages/df/19/9a16880dbf592007eec7a7010f42225253eb669c1787d2f6b1bd5a84e25e/tiktoken-0.10.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d5cab2a8e8974d6c9744264e94886a58b9087a39737c53fd65dbe2fe8522e719", size = 1237876 }, + { url = "https://files.pythonhosted.org/packages/92/2a/d80d05a53a21f4da7b38c975f220639e2829bd598b17028ccaca64db679a/tiktoken-0.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:7a49d2aedf911b68f23c9c84abce8eaf569cb02613f307cee4fb1bebd8c55ae9", size = 876340 }, + { url = "https://files.pythonhosted.org/packages/e9/ba/d392a2ffa94dbbcf1cd187b70df18f61a96f76d1de9d5d2bc8414b402d05/tiktoken-0.10.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:29f55c6b7c9a6a7ea953691c3962ee8fe4ee2f0ceb2a3fded3925acfc45e4b0a", size = 1047150 }, + { url = "https://files.pythonhosted.org/packages/98/e0/86b36df9ab5f014ef976f75b000b9daf06c6edff229186e05e56b94f4cdf/tiktoken-0.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f641d0735059b48252c4409d6546b6a62edeb4c4e48306499db11bbe403b872f", size = 988709 }, + { url = "https://files.pythonhosted.org/packages/c7/43/24813c6e7db16314229b5cd13bc23719a58401242dd42be2bac3b36516d4/tiktoken-0.10.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad76e03676b36c2a5e5304b428cff48eb86a033af55212d41a6ac6faec25b2ad", size = 1120621 }, + { url = "https://files.pythonhosted.org/packages/e5/94/fd4156ccc20d4c7986352353385d768307e4c6d8e2502c3d6ee061847474/tiktoken-0.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6420ecbd4dc4db3b82ca3421f50d1207d750de1a2856e6ca0544294fe58d2853", size = 1175233 }, + { url = "https://files.pythonhosted.org/packages/f4/db/32f4b4f1c13f84118924258b7a92c2a20a1d72f974abcdd73c3cd6308983/tiktoken-0.10.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d2e032cd44700a9ed511c50b9f302bb9e9e77e2ebf8e2d9b8ec12ce64ebf00c6", size = 1237489 }, + { url = "https://files.pythonhosted.org/packages/d1/a4/dc6558ce48a6c339a30a9e532bb5767e0ff511f8f948bca1340f57aa2fea/tiktoken-0.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:eb520960fa1788caf033489e3b63bb675c0f321db40569b2d3ca24d7fab5ca72", size = 876253 }, + { url = "https://files.pythonhosted.org/packages/2f/8a/2dad6ea056f88f12602bdbdba19ed1d29a574bb8dbe034af0842eb2ee29f/tiktoken-0.10.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:25c27021a605b2778af07f84a33fe7c74b5c960a8cfac5d2a7a1075ed592cbe4", size = 1046690 }, + { url = "https://files.pythonhosted.org/packages/84/88/b0ec9383e7126f7cf033971dac81c4dea0954607db8c2fbfe109061bf730/tiktoken-0.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:45d654f049b4f7ed617ad0c66462c15cc41e5db120f37f122d5b57ffa2aec062", size = 988269 }, + { url = "https://files.pythonhosted.org/packages/28/83/ed1900dd68890c891f55ded1f760f7878c5e16c8c68e576145105d66aa63/tiktoken-0.10.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34f73d93641b9cc73e19b18dbf9ce1baf086f1c4e18cfa36b952956843bca23c", size = 1118515 }, + { url = "https://files.pythonhosted.org/packages/9a/a8/84ad6c23cfd51d2c39ba1bfbc70b6208c6be3e5cd15132a9d11cc93bfdd5/tiktoken-0.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4fbfc5dda624c0f85c56bbf8f2ffb42964c798fc6fc1c321a452bf31d4ba21f5", size = 1174193 }, + { url = "https://files.pythonhosted.org/packages/7b/b5/7be4c78a10566ae068d88788a6fdf3a8b1162e7f78997999c46c919cdb69/tiktoken-0.10.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:853671af10636b581d632fe7b64b755301c73bc1f2ce837a8cdd9a44ab51f846", size = 1235834 }, + { url = "https://files.pythonhosted.org/packages/55/da/a0e90c85f36bd21ef4dbe881f9e259403c6fd6287841ff3691c1d2a5baab/tiktoken-0.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:56c4e7544be63c78265c82857b9f2a4e2190ae29541fe3467708fc7aabcb1522", size = 875661 }, + { url = "https://files.pythonhosted.org/packages/1a/44/03c259d8df262181f0efab0159373435284f1074adf966418663879023e1/tiktoken-0.10.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:10e779827ddb0490a415d09d95f8e7a982fd8e14f88c4c2348358f722af3c415", size = 1046536 }, + { url = "https://files.pythonhosted.org/packages/e3/16/98ee535d30f7abf528ac25e78d67171cc2dfc1366d762cf79eb0dcc9054d/tiktoken-0.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:783892cdbec0d33f0f51373d8969e059ab1244af531a52338f702131a032a116", size = 987947 }, + { url = "https://files.pythonhosted.org/packages/2f/bd/c3847b55e4fd6c6ab89ab75d258c725c67890734a4e58e28315ae2208ee1/tiktoken-0.10.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:43f38d62b6a62a7c82be3770fcdc24cdf5bff3bdf718909d038b804ac774a025", size = 1118091 }, + { url = "https://files.pythonhosted.org/packages/8c/07/9a2c6e1d1ef138f79a6c943bf1c7c7e7cc159e2b7fbbe6ce5d176f963232/tiktoken-0.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72d0b30e08f1e856fa76ee423a3d3f0b1bc984cb6e86a21dbd7c5eee8f67f8f5", size = 1174129 }, + { url = "https://files.pythonhosted.org/packages/ba/4f/ae9e36e43c0abbeb631b848697b22662cca5dbbe90a1b692b5c0ceda0fac/tiktoken-0.10.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa840eba09a0c1db0b436baf6cfe1ab7906f33fb663cf96f772a818bad5856a6", size = 1236097 }, + { url = "https://files.pythonhosted.org/packages/10/74/c6f69e46f98e42d6727853e4017d6287b57075905ff715fae0f35fa53173/tiktoken-0.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:642b8d9f266004629ea2457aa29a9eed4f1a031fd804f8c489edbf32df7026c3", size = 876020 }, +] + +[[package]] +name = "timm" +version = "1.0.19" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, + { name = "pyyaml" }, + { name = "safetensors" }, + { name = "torch" }, + { name = "torchvision" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1c/78/0789838cf20ba1cc09907914a008c1823d087132b48aa1ccde5e7934175a/timm-1.0.19.tar.gz", hash = "sha256:6e71e1f67ac80c229d3a78ca58347090514c508aeba8f2e2eb5289eda86e9f43", size = 2353261 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/74/661c63260cccf19ed5932e8b3f22f95ecd8bb34b9d9e6af9e1e7b961f254/timm-1.0.19-py3-none-any.whl", hash = "sha256:c07b56c32f3d3226c656f75c1b5479c08eb34eefed927c82fd8751a852f47931", size = 2497950 }, +] + +[[package]] +name = "tokenizers" +version = "0.20.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/da/25/b1681c1c30ea3ea6e584ae3fffd552430b12faa599b558c4c4783f56d7ff/tokenizers-0.20.3.tar.gz", hash = "sha256:2278b34c5d0dd78e087e1ca7f9b1dcbf129d80211afa645f214bd6e051037539", size = 340513 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/51/421bb0052fc4333f7c1e3231d8c6607552933d919b628c8fabd06f60ba1e/tokenizers-0.20.3-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:31ccab28dbb1a9fe539787210b0026e22debeab1662970f61c2d921f7557f7e4", size = 2674308 }, + { url = "https://files.pythonhosted.org/packages/a6/e9/f651f8d27614fd59af387f4dfa568b55207e5fac8d06eec106dc00b921c4/tokenizers-0.20.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c6361191f762bda98c773da418cf511cbaa0cb8d0a1196f16f8c0119bde68ff8", size = 2559363 }, + { url = "https://files.pythonhosted.org/packages/e3/e8/0e9f81a09ab79f409eabfd99391ca519e315496694671bebca24c3e90448/tokenizers-0.20.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f128d5da1202b78fa0a10d8d938610472487da01b57098d48f7e944384362514", size = 2892896 }, + { url = "https://files.pythonhosted.org/packages/b0/72/15fdbc149e05005e99431ecd471807db2241983deafe1e704020f608f40e/tokenizers-0.20.3-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:79c4121a2e9433ad7ef0769b9ca1f7dd7fa4c0cd501763d0a030afcbc6384481", size = 2802785 }, + { url = "https://files.pythonhosted.org/packages/26/44/1f8aea48f9bb117d966b7272484671b33a509f6217a8e8544d79442c90db/tokenizers-0.20.3-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b7850fde24197fe5cd6556e2fdba53a6d3bae67c531ea33a3d7c420b90904141", size = 3086060 }, + { url = "https://files.pythonhosted.org/packages/2e/83/82ba40da99870b3a0b801cffaf4f099f088a84c7e07d32cc6ca751ce08e6/tokenizers-0.20.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b357970c095dc134978a68c67d845a1e3803ab7c4fbb39195bde914e7e13cf8b", size = 3096760 }, + { url = "https://files.pythonhosted.org/packages/f3/46/7a025404201d937f86548928616c0a164308aa3998e546efdf798bf5ee9c/tokenizers-0.20.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a333d878c4970b72d6c07848b90c05f6b045cf9273fc2bc04a27211721ad6118", size = 3380165 }, + { url = "https://files.pythonhosted.org/packages/aa/49/15fae66ac62e49255eeedbb7f4127564b2c3f3aef2009913f525732d1a08/tokenizers-0.20.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1fd9fee817f655a8f50049f685e224828abfadd436b8ff67979fc1d054b435f1", size = 2994038 }, + { url = "https://files.pythonhosted.org/packages/f4/64/693afc9ba2393c2eed85c02bacb44762f06a29f0d1a5591fa5b40b39c0a2/tokenizers-0.20.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9e7816808b402129393a435ea2a509679b41246175d6e5e9f25b8692bfaa272b", size = 8977285 }, + { url = "https://files.pythonhosted.org/packages/be/7e/6126c18694310fe07970717929e889898767c41fbdd95b9078e8aec0f9ef/tokenizers-0.20.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ba96367db9d8a730d3a1d5996b4b7babb846c3994b8ef14008cd8660f55db59d", size = 9294890 }, + { url = "https://files.pythonhosted.org/packages/71/7d/5e3307a1091c8608a1e58043dff49521bc19553c6e9548c7fac6840cc2c4/tokenizers-0.20.3-cp310-none-win32.whl", hash = "sha256:ee31ba9d7df6a98619426283e80c6359f167e2e9882d9ce1b0254937dbd32f3f", size = 2196883 }, + { url = "https://files.pythonhosted.org/packages/47/62/aaf5b2a526b3b10c20985d9568ff8c8f27159345eaef3347831e78cd5894/tokenizers-0.20.3-cp310-none-win_amd64.whl", hash = "sha256:a845c08fdad554fe0871d1255df85772f91236e5fd6b9287ef8b64f5807dbd0c", size = 2381637 }, + { url = "https://files.pythonhosted.org/packages/c6/93/6742ef9206409d5ce1fdf44d5ca1687cdc3847ba0485424e2c731e6bcf67/tokenizers-0.20.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:585b51e06ca1f4839ce7759941e66766d7b060dccfdc57c4ca1e5b9a33013a90", size = 2674224 }, + { url = "https://files.pythonhosted.org/packages/aa/14/e75ece72e99f6ef9ae07777ca9fdd78608f69466a5cecf636e9bd2f25d5c/tokenizers-0.20.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:61cbf11954f3b481d08723ebd048ba4b11e582986f9be74d2c3bdd9293a4538d", size = 2558991 }, + { url = "https://files.pythonhosted.org/packages/46/54/033b5b2ba0c3ae01e026c6f7ced147d41a2fa1c573d00a66cb97f6d7f9b3/tokenizers-0.20.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef820880d5e4e8484e2fa54ff8d297bb32519eaa7815694dc835ace9130a3eea", size = 2892476 }, + { url = "https://files.pythonhosted.org/packages/e6/b0/cc369fb3297d61f3311cab523d16d48c869dc2f0ba32985dbf03ff811041/tokenizers-0.20.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:67ef4dcb8841a4988cd00dd288fb95dfc8e22ed021f01f37348fd51c2b055ba9", size = 2802775 }, + { url = "https://files.pythonhosted.org/packages/1a/74/62ad983e8ea6a63e04ed9c5be0b605056bf8aac2f0125f9b5e0b3e2b89fa/tokenizers-0.20.3-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ff1ef8bd47a02b0dc191688ccb4da53600df5d4c9a05a4b68e1e3de4823e78eb", size = 3086138 }, + { url = "https://files.pythonhosted.org/packages/6b/ac/4637ba619db25094998523f9e6f5b456e1db1f8faa770a3d925d436db0c3/tokenizers-0.20.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:444d188186eab3148baf0615b522461b41b1f0cd58cd57b862ec94b6ac9780f1", size = 3098076 }, + { url = "https://files.pythonhosted.org/packages/58/ce/9793f2dc2ce529369807c9c74e42722b05034af411d60f5730b720388c7d/tokenizers-0.20.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:37c04c032c1442740b2c2d925f1857885c07619224a533123ac7ea71ca5713da", size = 3379650 }, + { url = "https://files.pythonhosted.org/packages/50/f6/2841de926bc4118af996eaf0bdf0ea5b012245044766ffc0347e6c968e63/tokenizers-0.20.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:453c7769d22231960ee0e883d1005c93c68015025a5e4ae56275406d94a3c907", size = 2994005 }, + { url = "https://files.pythonhosted.org/packages/a3/b2/00915c4fed08e9505d37cf6eaab45b12b4bff8f6719d459abcb9ead86a4b/tokenizers-0.20.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4bb31f7b2847e439766aaa9cc7bccf7ac7088052deccdb2275c952d96f691c6a", size = 8977488 }, + { url = "https://files.pythonhosted.org/packages/e9/ac/1c069e7808181ff57bcf2d39e9b6fbee9133a55410e6ebdaa89f67c32e83/tokenizers-0.20.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:843729bf0f991b29655a069a2ff58a4c24375a553c70955e15e37a90dd4e045c", size = 9294935 }, + { url = "https://files.pythonhosted.org/packages/50/47/722feb70ee68d1c4412b12d0ea4acc2713179fd63f054913990f9e259492/tokenizers-0.20.3-cp311-none-win32.whl", hash = "sha256:efcce3a927b1e20ca694ba13f7a68c59b0bd859ef71e441db68ee42cf20c2442", size = 2197175 }, + { url = "https://files.pythonhosted.org/packages/75/68/1b4f928b15a36ed278332ac75d66d7eb65d865bf344d049c452c18447bf9/tokenizers-0.20.3-cp311-none-win_amd64.whl", hash = "sha256:88301aa0801f225725b6df5dea3d77c80365ff2362ca7e252583f2b4809c4cc0", size = 2381616 }, + { url = "https://files.pythonhosted.org/packages/07/00/92a08af2a6b0c88c50f1ab47d7189e695722ad9714b0ee78ea5e1e2e1def/tokenizers-0.20.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:49d12a32e190fad0e79e5bdb788d05da2f20d8e006b13a70859ac47fecf6ab2f", size = 2667951 }, + { url = "https://files.pythonhosted.org/packages/ec/9a/e17a352f0bffbf415cf7d73756f5c73a3219225fc5957bc2f39d52c61684/tokenizers-0.20.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:282848cacfb9c06d5e51489f38ec5aa0b3cd1e247a023061945f71f41d949d73", size = 2555167 }, + { url = "https://files.pythonhosted.org/packages/27/37/d108df55daf4f0fcf1f58554692ff71687c273d870a34693066f0847be96/tokenizers-0.20.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:abe4e08c7d0cd6154c795deb5bf81d2122f36daf075e0c12a8b050d824ef0a64", size = 2898389 }, + { url = "https://files.pythonhosted.org/packages/b2/27/32f29da16d28f59472fa7fb38e7782069748c7e9ab9854522db20341624c/tokenizers-0.20.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ca94fc1b73b3883c98f0c88c77700b13d55b49f1071dfd57df2b06f3ff7afd64", size = 2795866 }, + { url = "https://files.pythonhosted.org/packages/29/4e/8a9a3c89e128c4a40f247b501c10279d2d7ade685953407c4d94c8c0f7a7/tokenizers-0.20.3-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef279c7e239f95c8bdd6ff319d9870f30f0d24915b04895f55b1adcf96d6c60d", size = 3085446 }, + { url = "https://files.pythonhosted.org/packages/b4/3b/a2a7962c496ebcd95860ca99e423254f760f382cd4bd376f8895783afaf5/tokenizers-0.20.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:16384073973f6ccbde9852157a4fdfe632bb65208139c9d0c0bd0176a71fd67f", size = 3094378 }, + { url = "https://files.pythonhosted.org/packages/1f/f4/a8a33f0192a1629a3bd0afcad17d4d221bbf9276da4b95d226364208d5eb/tokenizers-0.20.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:312d522caeb8a1a42ebdec87118d99b22667782b67898a76c963c058a7e41d4f", size = 3385755 }, + { url = "https://files.pythonhosted.org/packages/9e/65/c83cb3545a65a9eaa2e13b22c93d5e00bd7624b354a44adbdc93d5d9bd91/tokenizers-0.20.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2b7cb962564785a83dafbba0144ecb7f579f1d57d8c406cdaa7f32fe32f18ad", size = 2997679 }, + { url = "https://files.pythonhosted.org/packages/55/e9/a80d4e592307688a67c7c59ab77e03687b6a8bd92eb5db763a2c80f93f57/tokenizers-0.20.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:124c5882ebb88dadae1fc788a582299fcd3a8bd84fc3e260b9918cf28b8751f5", size = 8989296 }, + { url = "https://files.pythonhosted.org/packages/90/af/60c957af8d2244321124e893828f1a4817cde1a2d08d09d423b73f19bd2f/tokenizers-0.20.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2b6e54e71f84c4202111a489879005cb14b92616a87417f6c102c833af961ea2", size = 9303621 }, + { url = "https://files.pythonhosted.org/packages/be/a9/96172310ee141009646d63a1ca267c099c462d747fe5ef7e33f74e27a683/tokenizers-0.20.3-cp312-none-win32.whl", hash = "sha256:83d9bfbe9af86f2d9df4833c22e94d94750f1d0cd9bfb22a7bb90a86f61cdb1c", size = 2188979 }, + { url = "https://files.pythonhosted.org/packages/bd/68/61d85ae7ae96dde7d0974ff3538db75d5cdc29be2e4329cd7fc51a283e22/tokenizers-0.20.3-cp312-none-win_amd64.whl", hash = "sha256:44def74cee574d609a36e17c8914311d1b5dbcfe37c55fd29369d42591b91cf2", size = 2380725 }, + { url = "https://files.pythonhosted.org/packages/07/19/36e9eaafb229616cb8502b42030fa7fe347550e76cb618de71b498fc3222/tokenizers-0.20.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:e0b630e0b536ef0e3c8b42c685c1bc93bd19e98c0f1543db52911f8ede42cf84", size = 2666813 }, + { url = "https://files.pythonhosted.org/packages/b9/c7/e2ce1d4f756c8a62ef93fdb4df877c2185339b6d63667b015bf70ea9d34b/tokenizers-0.20.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a02d160d2b19bcbfdf28bd9a4bf11be4cb97d0499c000d95d4c4b1a4312740b6", size = 2555354 }, + { url = "https://files.pythonhosted.org/packages/7c/cf/5309c2d173a6a67f9ec8697d8e710ea32418de6fd8541778032c202a1c3e/tokenizers-0.20.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e3d80d89b068bc30034034b5319218c7c0a91b00af19679833f55f3becb6945", size = 2897745 }, + { url = "https://files.pythonhosted.org/packages/2c/e5/af3078e32f225e680e69d61f78855880edb8d53f5850a1834d519b2b103f/tokenizers-0.20.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:174a54910bed1b089226512b4458ea60d6d6fd93060254734d3bc3540953c51c", size = 2794385 }, + { url = "https://files.pythonhosted.org/packages/0b/a7/bc421fe46650cc4eb4a913a236b88c243204f32c7480684d2f138925899e/tokenizers-0.20.3-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:098b8a632b8656aa5802c46689462c5c48f02510f24029d71c208ec2c822e771", size = 3084580 }, + { url = "https://files.pythonhosted.org/packages/c6/22/97e1e95ee81f75922c9f569c23cb2b1fdc7f5a7a29c4c9fae17e63f751a6/tokenizers-0.20.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:78c8c143e3ae41e718588281eb3e212c2b31623c9d6d40410ec464d7d6221fb5", size = 3093581 }, + { url = "https://files.pythonhosted.org/packages/d5/14/f0df0ee3b9e516121e23c0099bccd7b9f086ba9150021a750e99b16ce56f/tokenizers-0.20.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b26b0aadb18cd8701077362ba359a06683662d5cafe3e8e8aba10eb05c037f1", size = 3385934 }, + { url = "https://files.pythonhosted.org/packages/66/52/7a171bd4929e3ffe61a29b4340fe5b73484709f92a8162a18946e124c34c/tokenizers-0.20.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:07d7851a72717321022f3774e84aa9d595a041d643fafa2e87fbc9b18711dac0", size = 2997311 }, + { url = "https://files.pythonhosted.org/packages/7c/64/f1993bb8ebf775d56875ca0d50a50f2648bfbbb143da92fe2e6ceeb4abd5/tokenizers-0.20.3-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:bd44e48a430ada902c6266a8245f5036c4fe744fcb51f699999fbe82aa438797", size = 8988601 }, + { url = "https://files.pythonhosted.org/packages/d6/3f/49fa63422159bbc2f2a4ac5bfc597d04d4ec0ad3d2ef46649b5e9a340e37/tokenizers-0.20.3-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:a4c186bb006ccbe1f5cc4e0380d1ce7806f5955c244074fd96abc55e27b77f01", size = 9303950 }, + { url = "https://files.pythonhosted.org/packages/66/11/79d91aeb2817ad1993ef61c690afe73e6dbedbfb21918b302ef5a2ba9bfb/tokenizers-0.20.3-cp313-none-win32.whl", hash = "sha256:6e19e0f1d854d6ab7ea0c743d06e764d1d9a546932be0a67f33087645f00fe13", size = 2188941 }, + { url = "https://files.pythonhosted.org/packages/c2/ff/ac8410f868fb8b14b5e619efa304aa119cb8a40bd7df29fc81a898e64f99/tokenizers-0.20.3-cp313-none-win_amd64.whl", hash = "sha256:d50ede425c7e60966a9680d41b58b3a0950afa1bb570488e2972fa61662c4273", size = 2380269 }, + { url = "https://files.pythonhosted.org/packages/29/cd/ff1586dd572aaf1637d59968df3f6f6532fa255f4638fbc29f6d27e0b690/tokenizers-0.20.3-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:e919f2e3e68bb51dc31de4fcbbeff3bdf9c1cad489044c75e2b982a91059bd3c", size = 2672044 }, + { url = "https://files.pythonhosted.org/packages/b5/9e/7a2c00abbc8edb021ee0b1f12aab76a7b7824b49f94bcd9f075d0818d4b0/tokenizers-0.20.3-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b8e9608f2773996cc272156e305bd79066163a66b0390fe21750aff62df1ac07", size = 2558841 }, + { url = "https://files.pythonhosted.org/packages/8e/c1/6af62ef61316f33ecf785bbb2bee4292f34ea62b491d4480ad9b09acf6b6/tokenizers-0.20.3-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:39270a7050deaf50f7caff4c532c01b3c48f6608d42b3eacdebdc6795478c8df", size = 2897936 }, + { url = "https://files.pythonhosted.org/packages/9a/0b/c076b2ff3ee6dc70c805181fbe325668b89cfee856f8dfa24cc9aa293c84/tokenizers-0.20.3-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e005466632b1c5d2d2120f6de8aa768cc9d36cd1ab7d51d0c27a114c91a1e6ee", size = 3082688 }, + { url = "https://files.pythonhosted.org/packages/0a/60/56510124933136c2e90879e1c81603cfa753ae5a87830e3ef95056b20d8f/tokenizers-0.20.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a07962340b36189b6c8feda552ea1bfeee6cf067ff922a1d7760662c2ee229e5", size = 2998924 }, + { url = "https://files.pythonhosted.org/packages/68/60/4107b618b7b9155cb34ad2e0fc90946b7e71f041b642122fb6314f660688/tokenizers-0.20.3-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:55046ad3dd5f2b3c67501fcc8c9cbe3e901d8355f08a3b745e9b57894855f85b", size = 8989514 }, + { url = "https://files.pythonhosted.org/packages/e8/bd/48475818e614b73316baf37ac1e4e51b578bbdf58651812d7e55f43b88d8/tokenizers-0.20.3-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:efcf0eb939988b627558aaf2b9dc3e56d759cad2e0cfa04fcab378e4b48fc4fd", size = 9303476 }, +] + +[[package]] +name = "tomli" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077 }, + { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429 }, + { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067 }, + { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030 }, + { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898 }, + { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894 }, + { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319 }, + { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273 }, + { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310 }, + { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309 }, + { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762 }, + { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453 }, + { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486 }, + { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349 }, + { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159 }, + { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243 }, + { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645 }, + { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584 }, + { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875 }, + { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418 }, + { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708 }, + { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582 }, + { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543 }, + { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691 }, + { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170 }, + { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530 }, + { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666 }, + { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954 }, + { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724 }, + { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383 }, + { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257 }, +] + +[[package]] +name = "tomli-w" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/19/75/241269d1da26b624c0d5e110e8149093c759b7a286138f4efd61a60e75fe/tomli_w-1.2.0.tar.gz", hash = "sha256:2dd14fac5a47c27be9cd4c976af5a12d87fb1f0b4512f81d69cce3b35ae25021", size = 7184 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/18/c86eb8e0202e32dd3df50d43d7ff9854f8e0603945ff398974c1d91ac1ef/tomli_w-1.2.0-py3-none-any.whl", hash = "sha256:188306098d013b691fcadc011abd66727d3c414c571bb01b1a174ba8c983cf90", size = 6675 }, +] + +[[package]] +name = "tomlkit" +version = "0.13.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cc/18/0bbf3884e9eaa38819ebe46a7bd25dcd56b67434402b66a58c4b8e552575/tomlkit-0.13.3.tar.gz", hash = "sha256:430cf247ee57df2b94ee3fbe588e71d362a941ebb545dec29b53961d61add2a1", size = 185207 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bd/75/8539d011f6be8e29f339c42e633aae3cb73bffa95dd0f9adec09b9c58e85/tomlkit-0.13.3-py3-none-any.whl", hash = "sha256:c89c649d79ee40629a9fda55f8ace8c6a1b42deb912b2a8fd8d942ddadb606b0", size = 38901 }, +] + +[[package]] +name = "torch" +version = "2.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "fsspec" }, + { name = "jinja2" }, + { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "networkx", version = "3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "nvidia-cublas-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cuda-cupti-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cuda-nvrtc-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cuda-runtime-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cudnn-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cufft-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cufile-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-curand-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cusolver-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cusparse-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cusparselt-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-nccl-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-nvjitlink-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-nvtx-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "setuptools", marker = "python_full_version >= '3.12'" }, + { name = "sympy" }, + { name = "triton", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "typing-extensions" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/27/2e06cb52adf89fe6e020963529d17ed51532fc73c1e6d1b18420ef03338c/torch-2.7.1-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:a103b5d782af5bd119b81dbcc7ffc6fa09904c423ff8db397a1e6ea8fd71508f", size = 99089441 }, + { url = "https://files.pythonhosted.org/packages/0a/7c/0a5b3aee977596459ec45be2220370fde8e017f651fecc40522fd478cb1e/torch-2.7.1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:fe955951bdf32d182ee8ead6c3186ad54781492bf03d547d31771a01b3d6fb7d", size = 821154516 }, + { url = "https://files.pythonhosted.org/packages/f9/91/3d709cfc5e15995fb3fe7a6b564ce42280d3a55676dad672205e94f34ac9/torch-2.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:885453d6fba67d9991132143bf7fa06b79b24352f4506fd4d10b309f53454162", size = 216093147 }, + { url = "https://files.pythonhosted.org/packages/92/f6/5da3918414e07da9866ecb9330fe6ffdebe15cb9a4c5ada7d4b6e0a6654d/torch-2.7.1-cp310-none-macosx_11_0_arm64.whl", hash = "sha256:d72acfdb86cee2a32c0ce0101606f3758f0d8bb5f8f31e7920dc2809e963aa7c", size = 68630914 }, + { url = "https://files.pythonhosted.org/packages/11/56/2eae3494e3d375533034a8e8cf0ba163363e996d85f0629441fa9d9843fe/torch-2.7.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:236f501f2e383f1cb861337bdf057712182f910f10aeaf509065d54d339e49b2", size = 99093039 }, + { url = "https://files.pythonhosted.org/packages/e5/94/34b80bd172d0072c9979708ccd279c2da2f55c3ef318eceec276ab9544a4/torch-2.7.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:06eea61f859436622e78dd0cdd51dbc8f8c6d76917a9cf0555a333f9eac31ec1", size = 821174704 }, + { url = "https://files.pythonhosted.org/packages/50/9e/acf04ff375b0b49a45511c55d188bcea5c942da2aaf293096676110086d1/torch-2.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:8273145a2e0a3c6f9fd2ac36762d6ee89c26d430e612b95a99885df083b04e52", size = 216095937 }, + { url = "https://files.pythonhosted.org/packages/5b/2b/d36d57c66ff031f93b4fa432e86802f84991477e522adcdffd314454326b/torch-2.7.1-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:aea4fc1bf433d12843eb2c6b2204861f43d8364597697074c8d38ae2507f8730", size = 68640034 }, + { url = "https://files.pythonhosted.org/packages/87/93/fb505a5022a2e908d81fe9a5e0aa84c86c0d5f408173be71c6018836f34e/torch-2.7.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:27ea1e518df4c9de73af7e8a720770f3628e7f667280bce2be7a16292697e3fa", size = 98948276 }, + { url = "https://files.pythonhosted.org/packages/56/7e/67c3fe2b8c33f40af06326a3d6ae7776b3e3a01daa8f71d125d78594d874/torch-2.7.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:c33360cfc2edd976c2633b3b66c769bdcbbf0e0b6550606d188431c81e7dd1fc", size = 821025792 }, + { url = "https://files.pythonhosted.org/packages/a1/37/a37495502bc7a23bf34f89584fa5a78e25bae7b8da513bc1b8f97afb7009/torch-2.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:d8bf6e1856ddd1807e79dc57e54d3335f2b62e6f316ed13ed3ecfe1fc1df3d8b", size = 216050349 }, + { url = "https://files.pythonhosted.org/packages/3a/60/04b77281c730bb13460628e518c52721257814ac6c298acd25757f6a175c/torch-2.7.1-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:787687087412c4bd68d315e39bc1223f08aae1d16a9e9771d95eabbb04ae98fb", size = 68645146 }, + { url = "https://files.pythonhosted.org/packages/66/81/e48c9edb655ee8eb8c2a6026abdb6f8d2146abd1f150979ede807bb75dcb/torch-2.7.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:03563603d931e70722dce0e11999d53aa80a375a3d78e6b39b9f6805ea0a8d28", size = 98946649 }, + { url = "https://files.pythonhosted.org/packages/3a/24/efe2f520d75274fc06b695c616415a1e8a1021d87a13c68ff9dce733d088/torch-2.7.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:d632f5417b6980f61404a125b999ca6ebd0b8b4bbdbb5fbbba44374ab619a412", size = 821033192 }, + { url = "https://files.pythonhosted.org/packages/dd/d9/9c24d230333ff4e9b6807274f6f8d52a864210b52ec794c5def7925f4495/torch-2.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:23660443e13995ee93e3d844786701ea4ca69f337027b05182f5ba053ce43b38", size = 216055668 }, + { url = "https://files.pythonhosted.org/packages/95/bf/e086ee36ddcef9299f6e708d3b6c8487c1651787bb9ee2939eb2a7f74911/torch-2.7.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:0da4f4dba9f65d0d203794e619fe7ca3247a55ffdcbd17ae8fb83c8b2dc9b585", size = 68925988 }, + { url = "https://files.pythonhosted.org/packages/69/6a/67090dcfe1cf9048448b31555af6efb149f7afa0a310a366adbdada32105/torch-2.7.1-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:e08d7e6f21a617fe38eeb46dd2213ded43f27c072e9165dc27300c9ef9570934", size = 99028857 }, + { url = "https://files.pythonhosted.org/packages/90/1c/48b988870823d1cc381f15ec4e70ed3d65e043f43f919329b0045ae83529/torch-2.7.1-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:30207f672328a42df4f2174b8f426f354b2baa0b7cca3a0adb3d6ab5daf00dc8", size = 821098066 }, + { url = "https://files.pythonhosted.org/packages/7b/eb/10050d61c9d5140c5dc04a89ed3257ef1a6b93e49dd91b95363d757071e0/torch-2.7.1-cp313-cp313t-win_amd64.whl", hash = "sha256:79042feca1c634aaf6603fe6feea8c6b30dfa140a6bbc0b973e2260c7e79a22e", size = 216336310 }, + { url = "https://files.pythonhosted.org/packages/b1/29/beb45cdf5c4fc3ebe282bf5eafc8dfd925ead7299b3c97491900fe5ed844/torch-2.7.1-cp313-none-macosx_11_0_arm64.whl", hash = "sha256:988b0cbc4333618a1056d2ebad9eb10089637b659eb645434d0809d8d937b946", size = 68645708 }, +] + +[[package]] +name = "torchvision" +version = "0.22.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "pillow" }, + { name = "torch" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/15/2c/7b67117b14c6cc84ae3126ca6981abfa3af2ac54eb5252b80d9475fb40df/torchvision-0.22.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3b47d8369ee568c067795c0da0b4078f39a9dfea6f3bc1f3ac87530dfda1dd56", size = 1947825 }, + { url = "https://files.pythonhosted.org/packages/6c/9f/c4dcf1d232b75e28bc37e21209ab2458d6d60235e16163544ed693de54cb/torchvision-0.22.1-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:990de4d657a41ed71680cd8be2e98ebcab55371f30993dc9bd2e676441f7180e", size = 2512611 }, + { url = "https://files.pythonhosted.org/packages/e2/99/db71d62d12628111d59147095527a0ab492bdfecfba718d174c04ae6c505/torchvision-0.22.1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:3347f690c2eed6d02aa0edfb9b01d321e7f7cf1051992d96d8d196c39b881d49", size = 7485668 }, + { url = "https://files.pythonhosted.org/packages/32/ff/4a93a4623c3e5f97e8552af0f9f81d289dcf7f2ac71f1493f1c93a6b973d/torchvision-0.22.1-cp310-cp310-win_amd64.whl", hash = "sha256:86ad938f5a6ca645f0d5fb19484b1762492c2188c0ffb05c602e9e9945b7b371", size = 1707961 }, + { url = "https://files.pythonhosted.org/packages/f6/00/bdab236ef19da050290abc2b5203ff9945c84a1f2c7aab73e8e9c8c85669/torchvision-0.22.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4addf626e2b57fc22fd6d329cf1346d474497672e6af8383b7b5b636fba94a53", size = 1947827 }, + { url = "https://files.pythonhosted.org/packages/ac/d0/18f951b2be3cfe48c0027b349dcc6fde950e3dc95dd83e037e86f284f6fd/torchvision-0.22.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:8b4a53a6067d63adba0c52f2b8dd2290db649d642021674ee43c0c922f0c6a69", size = 2514021 }, + { url = "https://files.pythonhosted.org/packages/c3/1a/63eb241598b36d37a0221e10af357da34bd33402ccf5c0765e389642218a/torchvision-0.22.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:b7866a3b326413e67724ac46f1ee594996735e10521ba9e6cdbe0fa3cd98c2f2", size = 7487300 }, + { url = "https://files.pythonhosted.org/packages/e5/73/1b009b42fe4a7774ba19c23c26bb0f020d68525c417a348b166f1c56044f/torchvision-0.22.1-cp311-cp311-win_amd64.whl", hash = "sha256:bb3f6df6f8fd415ce38ec4fd338376ad40c62e86052d7fc706a0dd51efac1718", size = 1707989 }, + { url = "https://files.pythonhosted.org/packages/02/90/f4e99a5112dc221cf68a485e853cc3d9f3f1787cb950b895f3ea26d1ea98/torchvision-0.22.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:153f1790e505bd6da123e21eee6e83e2e155df05c0fe7d56347303067d8543c5", size = 1947827 }, + { url = "https://files.pythonhosted.org/packages/25/f6/53e65384cdbbe732cc2106bb04f7fb908487e4fb02ae4a1613ce6904a122/torchvision-0.22.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:964414eef19459d55a10e886e2fca50677550e243586d1678f65e3f6f6bac47a", size = 2514576 }, + { url = "https://files.pythonhosted.org/packages/17/8b/155f99042f9319bd7759536779b2a5b67cbd4f89c380854670850f89a2f4/torchvision-0.22.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:699c2d70d33951187f6ed910ea05720b9b4aaac1dcc1135f53162ce7d42481d3", size = 7485962 }, + { url = "https://files.pythonhosted.org/packages/05/17/e45d5cd3627efdb47587a0634179a3533593436219de3f20c743672d2a79/torchvision-0.22.1-cp312-cp312-win_amd64.whl", hash = "sha256:75e0897da7a8e43d78632f66f2bdc4f6e26da8d3f021a7c0fa83746073c2597b", size = 1707992 }, + { url = "https://files.pythonhosted.org/packages/7a/30/fecdd09fb973e963da68207fe9f3d03ec6f39a935516dc2a98397bf495c6/torchvision-0.22.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9c3ae3319624c43cc8127020f46c14aa878406781f0899bb6283ae474afeafbf", size = 1947818 }, + { url = "https://files.pythonhosted.org/packages/55/f4/b45f6cd92fa0acfac5e31b8e9258232f25bcdb0709a604e8b8a39d76e411/torchvision-0.22.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:4a614a6a408d2ed74208d0ea6c28a2fbb68290e9a7df206c5fef3f0b6865d307", size = 2471597 }, + { url = "https://files.pythonhosted.org/packages/8d/b0/3cffd6a285b5ffee3fe4a31caff49e350c98c5963854474d1c4f7a51dea5/torchvision-0.22.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:7ee682be589bb1a002b7704f06b8ec0b89e4b9068f48e79307d2c6e937a9fdf4", size = 7485894 }, + { url = "https://files.pythonhosted.org/packages/fd/1d/0ede596fedc2080d18108149921278b59f220fbb398f29619495337b0f86/torchvision-0.22.1-cp313-cp313-win_amd64.whl", hash = "sha256:2566cafcfa47ecfdbeed04bab8cef1307c8d4ef75046f7624b9e55f384880dfe", size = 1708020 }, + { url = "https://files.pythonhosted.org/packages/0f/ca/e9a06bd61ee8e04fb4962a3fb524fe6ee4051662db07840b702a9f339b24/torchvision-0.22.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:043d9e35ed69c2e586aff6eb9e2887382e7863707115668ac9d140da58f42cba", size = 2137623 }, + { url = "https://files.pythonhosted.org/packages/ab/c8/2ebe90f18e7ffa2120f5c3eab62aa86923185f78d2d051a455ea91461608/torchvision-0.22.1-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:27142bcc8a984227a6dcf560985e83f52b82a7d3f5fe9051af586a2ccc46ef26", size = 2476561 }, + { url = "https://files.pythonhosted.org/packages/94/8b/04c6b15f8c29b39f0679589753091cec8b192ab296d4fdaf9055544c4ec9/torchvision-0.22.1-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:ef46e065502f7300ad6abc98554131c35dc4c837b978d91306658f1a65c00baa", size = 7658543 }, + { url = "https://files.pythonhosted.org/packages/ab/c0/131628e6d42682b0502c63fd7f647b8b5ca4bd94088f6c85ca7225db8ac4/torchvision-0.22.1-cp313-cp313t-win_amd64.whl", hash = "sha256:7414eeacfb941fa21acddcd725f1617da5630ec822e498660a4b864d7d998075", size = 1629892 }, +] + +[[package]] +name = "tqdm" +version = "4.67.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540 }, +] + +[[package]] +name = "traitlets" +version = "5.14.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/eb/79/72064e6a701c2183016abbbfedaba506d81e30e232a68c9f0d6f6fcd1574/traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7", size = 161621 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f", size = 85359 }, +] + +[[package]] +name = "transformers" +version = "4.46.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "huggingface-hub" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "regex" }, + { name = "requests" }, + { name = "safetensors" }, + { name = "tokenizers" }, + { name = "tqdm" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/37/5a/58f96c83e566f907ae39f16d4401bbefd8bb85c60bd1e6a95c419752ab90/transformers-4.46.3.tar.gz", hash = "sha256:8ee4b3ae943fe33e82afff8e837f4b052058b07ca9be3cb5b729ed31295f72cc", size = 8627944 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/51/b87caa939fedf307496e4dbf412f4b909af3d9ca8b189fc3b65c1faa456f/transformers-4.46.3-py3-none-any.whl", hash = "sha256:a12ef6f52841fd190a3e5602145b542d03507222f2c64ebb7ee92e8788093aef", size = 10034536 }, +] + +[[package]] +name = "trio" +version = "0.30.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "cffi", marker = "(implementation_name != 'pypy' and os_name == 'nt' and platform_machine != 'aarch64' and sys_platform == 'linux') or (implementation_name != 'pypy' and os_name == 'nt' and sys_platform != 'darwin' and sys_platform != 'linux')" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "idna" }, + { name = "outcome" }, + { name = "sniffio" }, + { name = "sortedcontainers" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/01/c1/68d582b4d3a1c1f8118e18042464bb12a7c1b75d64d75111b297687041e3/trio-0.30.0.tar.gz", hash = "sha256:0781c857c0c81f8f51e0089929a26b5bb63d57f927728a5586f7e36171f064df", size = 593776 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/8e/3f6dfda475ecd940e786defe6df6c500734e686c9cd0a0f8ef6821e9b2f2/trio-0.30.0-py3-none-any.whl", hash = "sha256:3bf4f06b8decf8d3cf00af85f40a89824669e2d033bb32469d34840edcfc22a5", size = 499194 }, +] + +[[package]] +name = "trio-websocket" +version = "0.12.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "outcome" }, + { name = "trio" }, + { name = "wsproto" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/3c/8b4358e81f2f2cfe71b66a267f023a91db20a817b9425dd964873796980a/trio_websocket-0.12.2.tar.gz", hash = "sha256:22c72c436f3d1e264d0910a3951934798dcc5b00ae56fc4ee079d46c7cf20fae", size = 33549 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/19/eb640a397bba49ba49ef9dbe2e7e5c04202ba045b6ce2ec36e9cadc51e04/trio_websocket-0.12.2-py3-none-any.whl", hash = "sha256:df605665f1db533f4a386c94525870851096a223adcb97f72a07e8b4beba45b6", size = 21221 }, +] + +[[package]] +name = "triton" +version = "3.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "setuptools", marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/8d/a9/549e51e9b1b2c9b854fd761a1d23df0ba2fbc60bd0c13b489ffa518cfcb7/triton-3.3.1-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b74db445b1c562844d3cfad6e9679c72e93fdfb1a90a24052b03bb5c49d1242e", size = 155600257 }, + { url = "https://files.pythonhosted.org/packages/21/2f/3e56ea7b58f80ff68899b1dbe810ff257c9d177d288c6b0f55bf2fe4eb50/triton-3.3.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b31e3aa26f8cb3cc5bf4e187bf737cbacf17311e1112b781d4a059353dfd731b", size = 155689937 }, + { url = "https://files.pythonhosted.org/packages/24/5f/950fb373bf9c01ad4eb5a8cd5eaf32cdf9e238c02f9293557a2129b9c4ac/triton-3.3.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9999e83aba21e1a78c1f36f21bce621b77bcaa530277a50484a7cb4a822f6e43", size = 155669138 }, + { url = "https://files.pythonhosted.org/packages/74/1f/dfb531f90a2d367d914adfee771babbd3f1a5b26c3f5fbc458dee21daa78/triton-3.3.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b89d846b5a4198317fec27a5d3a609ea96b6d557ff44b56c23176546023c4240", size = 155673035 }, + { url = "https://files.pythonhosted.org/packages/28/71/bd20ffcb7a64c753dc2463489a61bf69d531f308e390ad06390268c4ea04/triton-3.3.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a3198adb9d78b77818a5388bff89fa72ff36f9da0bc689db2f0a651a67ce6a42", size = 155735832 }, +] + +[[package]] +name = "typer" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "rich" }, + { name = "shellingham" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c5/8c/7d682431efca5fd290017663ea4588bf6f2c6aad085c7f108c5dbc316e70/typer-0.16.0.tar.gz", hash = "sha256:af377ffaee1dbe37ae9440cb4e8f11686ea5ce4e9bae01b84ae7c63b87f1dd3b", size = 102625 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/42/3efaf858001d2c2913de7f354563e3a3a2f0decae3efe98427125a8f441e/typer-0.16.0-py3-none-any.whl", hash = "sha256:1f79bed11d4d02d4310e3c1b7ba594183bcedb0ac73b27a9e5f28f6fb5b98855", size = 46317 }, +] + +[[package]] +name = "types-requests" +version = "2.32.4.20250611" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6d/7f/73b3a04a53b0fd2a911d4ec517940ecd6600630b559e4505cc7b68beb5a0/types_requests-2.32.4.20250611.tar.gz", hash = "sha256:741c8777ed6425830bf51e54d6abe245f79b4dcb9019f1622b773463946bf826", size = 23118 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/ea/0be9258c5a4fa1ba2300111aa5a0767ee6d18eb3fd20e91616c12082284d/types_requests-2.32.4.20250611-py3-none-any.whl", hash = "sha256:ad2fe5d3b0cb3c2c902c8815a70e7fb2302c4b8c1f77bdcd738192cdb3878072", size = 20643 }, +] + +[[package]] +name = "typing-extensions" +version = "4.14.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/98/5a/da40306b885cc8c09109dc2e1abd358d5684b1425678151cdaed4731c822/typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36", size = 107673 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76", size = 43906 }, +] + +[[package]] +name = "typing-inspect" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mypy-extensions" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dc/74/1789779d91f1961fa9438e9a8710cdae6bd138c80d7303996933d117264a/typing_inspect-0.9.0.tar.gz", hash = "sha256:b23fc42ff6f6ef6954e4852c1fb512cdd18dbea03134f91f856a95ccc9461f78", size = 13825 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/65/f3/107a22063bf27bdccf2024833d3445f4eea42b2e598abfbd46f6a63b6cb0/typing_inspect-0.9.0-py3-none-any.whl", hash = "sha256:9ee6fc59062311ef8547596ab6b955e1b8aa46242d854bfc78f4f6b0eff35f9f", size = 8827 }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f8/b1/0c11f5058406b3af7609f121aaa6b609744687f1d158b3c3a5bf4cc94238/typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28", size = 75726 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552 }, +] + +[[package]] +name = "tzdata" +version = "2025.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839 }, +] + +[[package]] +name = "unstructured" +version = "0.18.11" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "backoff" }, + { name = "beautifulsoup4" }, + { name = "charset-normalizer" }, + { name = "dataclasses-json" }, + { name = "emoji" }, + { name = "filetype" }, + { name = "html5lib" }, + { name = "langdetect" }, + { name = "lxml" }, + { name = "nltk" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "psutil" }, + { name = "python-iso639" }, + { name = "python-magic" }, + { name = "python-oxmsg" }, + { name = "rapidfuzz" }, + { name = "requests" }, + { name = "tqdm" }, + { name = "typing-extensions" }, + { name = "unstructured-client" }, + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/be/7b/991fc1d9bf3f5a08f5c2bdaeb6523cb882171fa9bb20bd087818a50fb1dd/unstructured-0.18.11.tar.gz", hash = "sha256:cf177d2a212a8bcd3a5a6750d8400c079764d30dabdb6605e7ba0a65679e00a0", size = 1689741 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8c/73/7b15fe1f31e8904e2df8a4dbfbb6364ee5567a2fc0d5fa69856d684f5751/unstructured-0.18.11-py3-none-any.whl", hash = "sha256:85aec87032177b826ec278e57e3f74ef3a2aa7e887ab137875e572293cf9659b", size = 1777194 }, +] + +[package.optional-dependencies] +all-docs = [ + { name = "effdet" }, + { name = "google-cloud-vision" }, + { name = "markdown" }, + { name = "msoffcrypto-tool" }, + { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "networkx", version = "3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "onnx" }, + { name = "onnxruntime" }, + { name = "openpyxl" }, + { name = "pandas" }, + { name = "pdf2image" }, + { name = "pdfminer-six" }, + { name = "pi-heif" }, + { name = "pikepdf" }, + { name = "pypandoc" }, + { name = "pypdf" }, + { name = "python-docx" }, + { name = "python-pptx" }, + { name = "unstructured-inference" }, + { name = "unstructured-pytesseract" }, + { name = "xlrd" }, +] +local-inference = [ + { name = "effdet" }, + { name = "google-cloud-vision" }, + { name = "markdown" }, + { name = "msoffcrypto-tool" }, + { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "networkx", version = "3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "onnx" }, + { name = "onnxruntime" }, + { name = "openpyxl" }, + { name = "pandas" }, + { name = "pdf2image" }, + { name = "pdfminer-six" }, + { name = "pi-heif" }, + { name = "pikepdf" }, + { name = "pypandoc" }, + { name = "pypdf" }, + { name = "python-docx" }, + { name = "python-pptx" }, + { name = "unstructured-inference" }, + { name = "unstructured-pytesseract" }, + { name = "xlrd" }, +] + +[[package]] +name = "unstructured-client" +version = "0.42.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiofiles" }, + { name = "cryptography" }, + { name = "httpcore" }, + { name = "httpx" }, + { name = "pydantic" }, + { name = "pypdf" }, + { name = "requests-toolbelt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e5/3f/a837a41832d7fafa344a97663b23a77a510419002cc50f8fb680661ffc43/unstructured_client-0.42.1.tar.gz", hash = "sha256:4bde9a5e7f760c00d86bdf6712bba8e37792898d529025272999efb975deaea5", size = 90888 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/d5/3cec76bdb564148b935cfce2c5ea14feb9cd5362047bab43654bd89d858a/unstructured_client-0.42.1-py3-none-any.whl", hash = "sha256:4fd1b2336e7f5f17ed7eb50d92e954bed08bcdeae5ba8c430fed729ded8a29ac", size = 207171 }, +] + +[[package]] +name = "unstructured-inference" +version = "1.0.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "accelerate" }, + { name = "huggingface-hub" }, + { name = "matplotlib" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "onnx" }, + { name = "onnxruntime" }, + { name = "opencv-python" }, + { name = "pandas" }, + { name = "pdfminer-six" }, + { name = "pypdfium2" }, + { name = "python-multipart" }, + { name = "rapidfuzz" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "scipy", version = "1.16.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "timm" }, + { name = "torch" }, + { name = "transformers" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2c/51/bfe73d1992d5e5c083674e17993dc0b9809dfdad64a682802f52f9d1d961/unstructured_inference-1.0.5.tar.gz", hash = "sha256:ccd6881b0f03c533418bde6c9bd178a6660da8efbbe8c06a08afda9f25fe732b", size = 44097 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/7e/5385f97fa3c5c64e0c9116bf911c996c747c5f96f73fdddc55cafdc0d98b/unstructured_inference-1.0.5-py3-none-any.whl", hash = "sha256:ecbe385a6c58ca6b68b5723ed3cb540b70fd6317eecd1d5e6541516edf7071d0", size = 48060 }, +] + +[[package]] +name = "unstructured-pytesseract" +version = "0.3.15" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, + { name = "pillow" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ef/b1/4b3a976b76549f22c3f5493a622603617cbe08804402978e1dac9c387997/unstructured.pytesseract-0.3.15.tar.gz", hash = "sha256:4b81bc76cfff4e2ef37b04863f0e48bd66184c0b39c3b2b4e017483bca1a7394", size = 15703 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/6d/adb955ecf60811a3735d508974bbb5358e7745b635dc001329267529c6f2/unstructured.pytesseract-0.3.15-py3-none-any.whl", hash = "sha256:a3f505c5efb7ff9f10379051a7dd6aa624b3be6b0f023ed6767cc80d0b1613d1", size = 14992 }, +] + +[[package]] +name = "urllib3" +version = "2.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795 }, +] + +[package.optional-dependencies] +socks = [ + { name = "pysocks" }, +] + +[[package]] +name = "uv" +version = "0.8.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/83/94/e18a40fe6f6d724c1fbf2c9328806359e341710b2fd42dc928a1a8fc636b/uv-0.8.5.tar.gz", hash = "sha256:078cf2935062d5b61816505f9d6f30b0221943a1433b4a1de8f31a1dfe55736b", size = 3451272 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/b9/78cde56283b6b9a8a84b0bf9334442ed75a843310229aaf7f1a71fe67818/uv-0.8.5-py3-none-linux_armv6l.whl", hash = "sha256:e236372a260e312aef5485a0e5819a0ec16c9197af06d162ad5a3e8bd62f9bba", size = 18146198 }, + { url = "https://files.pythonhosted.org/packages/ed/83/5deda1a19362ce426da7f9cc4764a0dd57e665ecbaddd9900d4200bc10ab/uv-0.8.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:53a40628329e543a5c5414553f5898131d5c1c6f963708cb0afc2ecf3e8d8167", size = 18242690 }, + { url = "https://files.pythonhosted.org/packages/06/6e/80b08ee544728317d9c8003d4c10234007e12f384da1c3dfe579489833c9/uv-0.8.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:43a689027696bc9c62e6da3f06900c52eafc4debbf4fba9ecb906196730b34c8", size = 16913881 }, + { url = "https://files.pythonhosted.org/packages/34/f6/47a44dabfc25b598ea6f2ab9aa32ebf1cbd87ed8af18ccde6c5d36f35476/uv-0.8.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:a34d783f5cef00f1918357c0cd9226666e22640794e9e3862820abf4ee791141", size = 17527439 }, + { url = "https://files.pythonhosted.org/packages/ef/7d/ee7c2514e064412133ee9f01c4c42de20da24617b8c25d81cf7021b774d8/uv-0.8.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2140383bc25228281090cc34c00500d8e5822877c955f691d69bbf967e8efa73", size = 17833275 }, + { url = "https://files.pythonhosted.org/packages/f9/e7/5233cf5cbcca8ea65aa1f1e48bf210dc9773fb86b8104ffbc523be7f6a3f/uv-0.8.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6b449779ff463b059504dc30316a634f810149e02482ce36ea35daea8f6ce7af", size = 18568916 }, + { url = "https://files.pythonhosted.org/packages/d8/54/6cabb2a0347c51c8366ca3bffeeebd7f829a15f6b29ad20f51fd5ca9c4bd/uv-0.8.5-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:a7f8739d05cc513eee2f1f8a7e6c482a9c1e8860d77cd078d1ea7c3fe36d7a65", size = 19993334 }, + { url = "https://files.pythonhosted.org/packages/3c/7a/b84d994d52f20bc56229840c31e77aff4653e5902ea7b7c2616e9381b5b8/uv-0.8.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:62ebbd22f780ba2585690332765caf9e29c9758e48a678148e8b1ea90580cdb9", size = 19643358 }, + { url = "https://files.pythonhosted.org/packages/c8/f1/7552f2bea528456d34bc245f2959ce910631e01571c4b7ea421ead9a9fc6/uv-0.8.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4f8dd0555f05d66ff46fdab551137cc2b1ea9c5363358913e2af175e367f4398", size = 18947757 }, + { url = "https://files.pythonhosted.org/packages/57/9b/46aadd186a1e16a23cd0701dda0e640197db49a3add074a47231fed45a4f/uv-0.8.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:38c04408ad5eae7a178a1e3b0e09afeb436d0c97075530a3c82de453b78d0448", size = 18906135 }, + { url = "https://files.pythonhosted.org/packages/c0/31/6661adedaba9ebac8bb449ec9901f8cbf124fa25e0db3a9e6cf3053cee88/uv-0.8.5-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:73e772caf7310af4b21eaf8c25531b934391f1e84f3afa8e67822d7c432f6dad", size = 17787943 }, + { url = "https://files.pythonhosted.org/packages/11/f2/73fb5c3156fdae830b83edec2f430db84cb4bc4b78f61d21694bd59004cb/uv-0.8.5-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:3ddd7d8c01073f23ba2a4929ab246adb30d4f8a55c5e007ad7c8341f7bf06978", size = 18675864 }, + { url = "https://files.pythonhosted.org/packages/b5/29/774c6f174c53d68ae9a51c2fabf1b09003b93a53c24591a108be0dc338d7/uv-0.8.5-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:7d601f021cbc179320ea3a75cd1d91bd49af03d2a630c4d04ebd38ff6b87d419", size = 17808770 }, + { url = "https://files.pythonhosted.org/packages/a9/b0/5d164ce84691f5018c5832e9e3371c0196631b1f1025474a179de1d6a70a/uv-0.8.5-py3-none-musllinux_1_1_i686.whl", hash = "sha256:6ee97b7299990026619c20e30e253972c6c0fb6fba4f5658144e62aa1c07785a", size = 18076516 }, + { url = "https://files.pythonhosted.org/packages/d1/73/4d8baefb4f4b07df6a4db7bbd604cb361d4f5215b94d3f66553ea26edfd4/uv-0.8.5-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:09804055d6346febf0767767c04bdd2fab7d911535639f9c18de2ea744b2954c", size = 19031195 }, + { url = "https://files.pythonhosted.org/packages/44/2a/3d074391df2c16c79fc6bf333e4bde75662e64dac465050a03391c75b289/uv-0.8.5-py3-none-win32.whl", hash = "sha256:6362a2e1fa535af0e4c0a01f83e666a4d5f9024d808f9e64e3b6ef07c97aff54", size = 18026273 }, + { url = "https://files.pythonhosted.org/packages/3c/2f/e850d3e745ccd1125b7a48898421824700fd3e996d27d835139160650124/uv-0.8.5-py3-none-win_amd64.whl", hash = "sha256:dd89836735860461c3a5563731e77c011d1831f14ada540f94bf1a7011dbea14", size = 19822158 }, + { url = "https://files.pythonhosted.org/packages/6f/df/e5565b3faf2c6147a877ab7e96ef31e2333f08c5138a98ce77003b1bf65e/uv-0.8.5-py3-none-win_arm64.whl", hash = "sha256:37c1a22915392014d8b4ade9e69e157c8e5ccdf32f37070a84f749a708268335", size = 18430102 }, +] + +[[package]] +name = "uvicorn" +version = "0.35.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/42/e0e305207bb88c6b8d3061399c6a961ffe5fbb7e2aa63c9234df7259e9cd/uvicorn-0.35.0.tar.gz", hash = "sha256:bc662f087f7cf2ce11a1d7fd70b90c9f98ef2e2831556dd078d131b96cc94a01", size = 78473 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/e2/dc81b1bd1dcfe91735810265e9d26bc8ec5da45b4c0f6237e286819194c3/uvicorn-0.35.0-py3-none-any.whl", hash = "sha256:197535216b25ff9b785e29a0b79199f55222193d47f820816e7da751e9bc8d4a", size = 66406 }, +] + +[package.optional-dependencies] +standard = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "httptools" }, + { name = "python-dotenv" }, + { name = "pyyaml" }, + { name = "uvloop", marker = "platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'" }, + { name = "watchfiles" }, + { name = "websockets" }, +] + +[[package]] +name = "uvloop" +version = "0.21.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/c0/854216d09d33c543f12a44b393c402e89a920b1a0a7dc634c42de91b9cf6/uvloop-0.21.0.tar.gz", hash = "sha256:3bf12b0fda68447806a7ad847bfa591613177275d35b6724b1ee573faa3704e3", size = 2492741 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/76/44a55515e8c9505aa1420aebacf4dd82552e5e15691654894e90d0bd051a/uvloop-0.21.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ec7e6b09a6fdded42403182ab6b832b71f4edaf7f37a9a0e371a01db5f0cb45f", size = 1442019 }, + { url = "https://files.pythonhosted.org/packages/35/5a/62d5800358a78cc25c8a6c72ef8b10851bdb8cca22e14d9c74167b7f86da/uvloop-0.21.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:196274f2adb9689a289ad7d65700d37df0c0930fd8e4e743fa4834e850d7719d", size = 801898 }, + { url = "https://files.pythonhosted.org/packages/f3/96/63695e0ebd7da6c741ccd4489b5947394435e198a1382349c17b1146bb97/uvloop-0.21.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f38b2e090258d051d68a5b14d1da7203a3c3677321cf32a95a6f4db4dd8b6f26", size = 3827735 }, + { url = "https://files.pythonhosted.org/packages/61/e0/f0f8ec84979068ffae132c58c79af1de9cceeb664076beea86d941af1a30/uvloop-0.21.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87c43e0f13022b998eb9b973b5e97200c8b90823454d4bc06ab33829e09fb9bb", size = 3825126 }, + { url = "https://files.pythonhosted.org/packages/bf/fe/5e94a977d058a54a19df95f12f7161ab6e323ad49f4dabc28822eb2df7ea/uvloop-0.21.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:10d66943def5fcb6e7b37310eb6b5639fd2ccbc38df1177262b0640c3ca68c1f", size = 3705789 }, + { url = "https://files.pythonhosted.org/packages/26/dd/c7179618e46092a77e036650c1f056041a028a35c4d76945089fcfc38af8/uvloop-0.21.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:67dd654b8ca23aed0a8e99010b4c34aca62f4b7fce88f39d452ed7622c94845c", size = 3800523 }, + { url = "https://files.pythonhosted.org/packages/57/a7/4cf0334105c1160dd6819f3297f8700fda7fc30ab4f61fbf3e725acbc7cc/uvloop-0.21.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c0f3fa6200b3108919f8bdabb9a7f87f20e7097ea3c543754cabc7d717d95cf8", size = 1447410 }, + { url = "https://files.pythonhosted.org/packages/8c/7c/1517b0bbc2dbe784b563d6ab54f2ef88c890fdad77232c98ed490aa07132/uvloop-0.21.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0878c2640cf341b269b7e128b1a5fed890adc4455513ca710d77d5e93aa6d6a0", size = 805476 }, + { url = "https://files.pythonhosted.org/packages/ee/ea/0bfae1aceb82a503f358d8d2fa126ca9dbdb2ba9c7866974faec1cb5875c/uvloop-0.21.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9fb766bb57b7388745d8bcc53a359b116b8a04c83a2288069809d2b3466c37e", size = 3960855 }, + { url = "https://files.pythonhosted.org/packages/8a/ca/0864176a649838b838f36d44bf31c451597ab363b60dc9e09c9630619d41/uvloop-0.21.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a375441696e2eda1c43c44ccb66e04d61ceeffcd76e4929e527b7fa401b90fb", size = 3973185 }, + { url = "https://files.pythonhosted.org/packages/30/bf/08ad29979a936d63787ba47a540de2132169f140d54aa25bc8c3df3e67f4/uvloop-0.21.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:baa0e6291d91649c6ba4ed4b2f982f9fa165b5bbd50a9e203c416a2797bab3c6", size = 3820256 }, + { url = "https://files.pythonhosted.org/packages/da/e2/5cf6ef37e3daf2f06e651aae5ea108ad30df3cb269102678b61ebf1fdf42/uvloop-0.21.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4509360fcc4c3bd2c70d87573ad472de40c13387f5fda8cb58350a1d7475e58d", size = 3937323 }, + { url = "https://files.pythonhosted.org/packages/8c/4c/03f93178830dc7ce8b4cdee1d36770d2f5ebb6f3d37d354e061eefc73545/uvloop-0.21.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:359ec2c888397b9e592a889c4d72ba3d6befba8b2bb01743f72fffbde663b59c", size = 1471284 }, + { url = "https://files.pythonhosted.org/packages/43/3e/92c03f4d05e50f09251bd8b2b2b584a2a7f8fe600008bcc4523337abe676/uvloop-0.21.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f7089d2dc73179ce5ac255bdf37c236a9f914b264825fdaacaded6990a7fb4c2", size = 821349 }, + { url = "https://files.pythonhosted.org/packages/a6/ef/a02ec5da49909dbbfb1fd205a9a1ac4e88ea92dcae885e7c961847cd51e2/uvloop-0.21.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:baa4dcdbd9ae0a372f2167a207cd98c9f9a1ea1188a8a526431eef2f8116cc8d", size = 4580089 }, + { url = "https://files.pythonhosted.org/packages/06/a7/b4e6a19925c900be9f98bec0a75e6e8f79bb53bdeb891916609ab3958967/uvloop-0.21.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86975dca1c773a2c9864f4c52c5a55631038e387b47eaf56210f873887b6c8dc", size = 4693770 }, + { url = "https://files.pythonhosted.org/packages/ce/0c/f07435a18a4b94ce6bd0677d8319cd3de61f3a9eeb1e5f8ab4e8b5edfcb3/uvloop-0.21.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:461d9ae6660fbbafedd07559c6a2e57cd553b34b0065b6550685f6653a98c1cb", size = 4451321 }, + { url = "https://files.pythonhosted.org/packages/8f/eb/f7032be105877bcf924709c97b1bf3b90255b4ec251f9340cef912559f28/uvloop-0.21.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:183aef7c8730e54c9a3ee3227464daed66e37ba13040bb3f350bc2ddc040f22f", size = 4659022 }, + { url = "https://files.pythonhosted.org/packages/3f/8d/2cbef610ca21539f0f36e2b34da49302029e7c9f09acef0b1c3b5839412b/uvloop-0.21.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:bfd55dfcc2a512316e65f16e503e9e450cab148ef11df4e4e679b5e8253a5281", size = 1468123 }, + { url = "https://files.pythonhosted.org/packages/93/0d/b0038d5a469f94ed8f2b2fce2434a18396d8fbfb5da85a0a9781ebbdec14/uvloop-0.21.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:787ae31ad8a2856fc4e7c095341cccc7209bd657d0e71ad0dc2ea83c4a6fa8af", size = 819325 }, + { url = "https://files.pythonhosted.org/packages/50/94/0a687f39e78c4c1e02e3272c6b2ccdb4e0085fda3b8352fecd0410ccf915/uvloop-0.21.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ee4d4ef48036ff6e5cfffb09dd192c7a5027153948d85b8da7ff705065bacc6", size = 4582806 }, + { url = "https://files.pythonhosted.org/packages/d2/19/f5b78616566ea68edd42aacaf645adbf71fbd83fc52281fba555dc27e3f1/uvloop-0.21.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3df876acd7ec037a3d005b3ab85a7e4110422e4d9c1571d4fc89b0fc41b6816", size = 4701068 }, + { url = "https://files.pythonhosted.org/packages/47/57/66f061ee118f413cd22a656de622925097170b9380b30091b78ea0c6ea75/uvloop-0.21.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bd53ecc9a0f3d87ab847503c2e1552b690362e005ab54e8a48ba97da3924c0dc", size = 4454428 }, + { url = "https://files.pythonhosted.org/packages/63/9a/0962b05b308494e3202d3f794a6e85abe471fe3cafdbcf95c2e8c713aabd/uvloop-0.21.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a5c39f217ab3c663dc699c04cbd50c13813e31d917642d459fdcec07555cc553", size = 4660018 }, +] + +[[package]] +name = "validators" +version = "0.35.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/66/a435d9ae49850b2f071f7ebd8119dd4e84872b01630d6736761e6e7fd847/validators-0.35.0.tar.gz", hash = "sha256:992d6c48a4e77c81f1b4daba10d16c3a9bb0dbb79b3a19ea847ff0928e70497a", size = 73399 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/6e/3e955517e22cbdd565f2f8b2e73d52528b14b8bcfdb04f62466b071de847/validators-0.35.0-py3-none-any.whl", hash = "sha256:e8c947097eae7892cb3d26868d637f79f47b4a0554bc6b80065dfe5aac3705dd", size = 44712 }, +] + +[[package]] +name = "vcrpy" +version = "5.1.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and platform_python_implementation == 'PyPy' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and platform_python_implementation == 'PyPy' and sys_platform == 'linux'", + "(python_full_version >= '3.13' and platform_machine != 'aarch64' and platform_python_implementation == 'PyPy' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_python_implementation == 'PyPy' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version >= '3.12.4' and python_full_version < '3.13' and platform_python_implementation == 'PyPy' and sys_platform == 'darwin'", + "python_full_version >= '3.12.4' and python_full_version < '3.13' and platform_machine == 'aarch64' and platform_python_implementation == 'PyPy' and sys_platform == 'linux'", + "(python_full_version >= '3.12.4' and python_full_version < '3.13' and platform_machine != 'aarch64' and platform_python_implementation == 'PyPy' and sys_platform == 'linux') or (python_full_version >= '3.12.4' and python_full_version < '3.13' and platform_python_implementation == 'PyPy' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version >= '3.12' and python_full_version < '3.12.4' and platform_python_implementation == 'PyPy' and sys_platform == 'darwin'", + "python_full_version >= '3.12' and python_full_version < '3.12.4' and platform_machine == 'aarch64' and platform_python_implementation == 'PyPy' and sys_platform == 'linux'", + "(python_full_version >= '3.12' and python_full_version < '3.12.4' and platform_machine != 'aarch64' and platform_python_implementation == 'PyPy' and sys_platform == 'linux') or (python_full_version >= '3.12' and python_full_version < '3.12.4' and platform_python_implementation == 'PyPy' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version == '3.11.*' and platform_python_implementation == 'PyPy' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and platform_python_implementation == 'PyPy' and sys_platform == 'linux'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and platform_python_implementation == 'PyPy' and sys_platform == 'linux') or (python_full_version == '3.11.*' and platform_python_implementation == 'PyPy' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version < '3.11' and platform_python_implementation == 'PyPy' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and platform_python_implementation == 'PyPy' and sys_platform == 'linux'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and platform_python_implementation == 'PyPy' and sys_platform == 'linux') or (python_full_version < '3.11' and platform_python_implementation == 'PyPy' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "pyyaml", marker = "platform_python_implementation == 'PyPy'" }, + { name = "wrapt", marker = "platform_python_implementation == 'PyPy'" }, + { name = "yarl", marker = "platform_python_implementation == 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a5/ea/a166a3cce4ac5958ba9bbd9768acdb1ba38ae17ff7986da09fa5b9dbc633/vcrpy-5.1.0.tar.gz", hash = "sha256:bbf1532f2618a04f11bce2a99af3a9647a32c880957293ff91e0a5f187b6b3d2", size = 84576 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/5b/3f70bcb279ad30026cc4f1df0a0491a0205a24dddd88301f396c485de9e7/vcrpy-5.1.0-py2.py3-none-any.whl", hash = "sha256:605e7b7a63dcd940db1df3ab2697ca7faf0e835c0852882142bafb19649d599e", size = 41969 }, +] + +[[package]] +name = "vcrpy" +version = "7.0.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and platform_python_implementation != 'PyPy' and sys_platform == 'darwin'", + "python_full_version >= '3.13' and platform_machine == 'aarch64' and platform_python_implementation != 'PyPy' and sys_platform == 'linux'", + "(python_full_version >= '3.13' and platform_machine != 'aarch64' and platform_python_implementation != 'PyPy' and sys_platform == 'linux') or (python_full_version >= '3.13' and platform_python_implementation != 'PyPy' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version >= '3.12.4' and python_full_version < '3.13' and platform_python_implementation != 'PyPy' and sys_platform == 'darwin'", + "python_full_version >= '3.12.4' and python_full_version < '3.13' and platform_machine == 'aarch64' and platform_python_implementation != 'PyPy' and sys_platform == 'linux'", + "(python_full_version >= '3.12.4' and python_full_version < '3.13' and platform_machine != 'aarch64' and platform_python_implementation != 'PyPy' and sys_platform == 'linux') or (python_full_version >= '3.12.4' and python_full_version < '3.13' and platform_python_implementation != 'PyPy' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version >= '3.12' and python_full_version < '3.12.4' and platform_python_implementation != 'PyPy' and sys_platform == 'darwin'", + "python_full_version >= '3.12' and python_full_version < '3.12.4' and platform_machine == 'aarch64' and platform_python_implementation != 'PyPy' and sys_platform == 'linux'", + "(python_full_version >= '3.12' and python_full_version < '3.12.4' and platform_machine != 'aarch64' and platform_python_implementation != 'PyPy' and sys_platform == 'linux') or (python_full_version >= '3.12' and python_full_version < '3.12.4' and platform_python_implementation != 'PyPy' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version == '3.11.*' and platform_python_implementation != 'PyPy' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and platform_python_implementation != 'PyPy' and sys_platform == 'linux'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and platform_python_implementation != 'PyPy' and sys_platform == 'linux') or (python_full_version == '3.11.*' and platform_python_implementation != 'PyPy' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version < '3.11' and platform_python_implementation != 'PyPy' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and platform_python_implementation != 'PyPy' and sys_platform == 'linux'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and platform_python_implementation != 'PyPy' and sys_platform == 'linux') or (python_full_version < '3.11' and platform_python_implementation != 'PyPy' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "pyyaml", marker = "platform_python_implementation != 'PyPy'" }, + { name = "urllib3", marker = "platform_python_implementation != 'PyPy'" }, + { name = "wrapt", marker = "platform_python_implementation != 'PyPy'" }, + { name = "yarl", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/25/d3/856e06184d4572aada1dd559ddec3bedc46df1f2edc5ab2c91121a2cccdb/vcrpy-7.0.0.tar.gz", hash = "sha256:176391ad0425edde1680c5b20738ea3dc7fb942520a48d2993448050986b3a50", size = 85502 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/5d/1f15b252890c968d42b348d1e9b0aa12d5bf3e776704178ec37cceccdb63/vcrpy-7.0.0-py2.py3-none-any.whl", hash = "sha256:55791e26c18daa363435054d8b35bd41a4ac441b6676167635d1b37a71dbe124", size = 42321 }, +] + +[[package]] +name = "watchfiles" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2a/9a/d451fcc97d029f5812e898fd30a53fd8c15c7bbd058fd75cfc6beb9bd761/watchfiles-1.1.0.tar.gz", hash = "sha256:693ed7ec72cbfcee399e92c895362b6e66d63dac6b91e2c11ae03d10d503e575", size = 94406 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/dd/579d1dc57f0f895426a1211c4ef3b0cb37eb9e642bb04bdcd962b5df206a/watchfiles-1.1.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:27f30e14aa1c1e91cb653f03a63445739919aef84c8d2517997a83155e7a2fcc", size = 405757 }, + { url = "https://files.pythonhosted.org/packages/1c/a0/7a0318cd874393344d48c34d53b3dd419466adf59a29ba5b51c88dd18b86/watchfiles-1.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3366f56c272232860ab45c77c3ca7b74ee819c8e1f6f35a7125556b198bbc6df", size = 397511 }, + { url = "https://files.pythonhosted.org/packages/06/be/503514656d0555ec2195f60d810eca29b938772e9bfb112d5cd5ad6f6a9e/watchfiles-1.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8412eacef34cae2836d891836a7fff7b754d6bcac61f6c12ba5ca9bc7e427b68", size = 450739 }, + { url = "https://files.pythonhosted.org/packages/4e/0d/a05dd9e5f136cdc29751816d0890d084ab99f8c17b86f25697288ca09bc7/watchfiles-1.1.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:df670918eb7dd719642e05979fc84704af913d563fd17ed636f7c4783003fdcc", size = 458106 }, + { url = "https://files.pythonhosted.org/packages/f1/fa/9cd16e4dfdb831072b7ac39e7bea986e52128526251038eb481effe9f48e/watchfiles-1.1.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d7642b9bc4827b5518ebdb3b82698ada8c14c7661ddec5fe719f3e56ccd13c97", size = 484264 }, + { url = "https://files.pythonhosted.org/packages/32/04/1da8a637c7e2b70e750a0308e9c8e662ada0cca46211fa9ef24a23937e0b/watchfiles-1.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:199207b2d3eeaeb80ef4411875a6243d9ad8bc35b07fc42daa6b801cc39cc41c", size = 597612 }, + { url = "https://files.pythonhosted.org/packages/30/01/109f2762e968d3e58c95731a206e5d7d2a7abaed4299dd8a94597250153c/watchfiles-1.1.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a479466da6db5c1e8754caee6c262cd373e6e6c363172d74394f4bff3d84d7b5", size = 477242 }, + { url = "https://files.pythonhosted.org/packages/b5/b8/46f58cf4969d3b7bc3ca35a98e739fa4085b0657a1540ccc29a1a0bc016f/watchfiles-1.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:935f9edd022ec13e447e5723a7d14456c8af254544cefbc533f6dd276c9aa0d9", size = 453148 }, + { url = "https://files.pythonhosted.org/packages/a5/cd/8267594263b1770f1eb76914940d7b2d03ee55eca212302329608208e061/watchfiles-1.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:8076a5769d6bdf5f673a19d51da05fc79e2bbf25e9fe755c47595785c06a8c72", size = 626574 }, + { url = "https://files.pythonhosted.org/packages/a1/2f/7f2722e85899bed337cba715723e19185e288ef361360718973f891805be/watchfiles-1.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:86b1e28d4c37e89220e924305cd9f82866bb0ace666943a6e4196c5df4d58dcc", size = 624378 }, + { url = "https://files.pythonhosted.org/packages/bf/20/64c88ec43d90a568234d021ab4b2a6f42a5230d772b987c3f9c00cc27b8b/watchfiles-1.1.0-cp310-cp310-win32.whl", hash = "sha256:d1caf40c1c657b27858f9774d5c0e232089bca9cb8ee17ce7478c6e9264d2587", size = 279829 }, + { url = "https://files.pythonhosted.org/packages/39/5c/a9c1ed33de7af80935e4eac09570de679c6e21c07070aa99f74b4431f4d6/watchfiles-1.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:a89c75a5b9bc329131115a409d0acc16e8da8dfd5867ba59f1dd66ae7ea8fa82", size = 292192 }, + { url = "https://files.pythonhosted.org/packages/8b/78/7401154b78ab484ccaaeef970dc2af0cb88b5ba8a1b415383da444cdd8d3/watchfiles-1.1.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:c9649dfc57cc1f9835551deb17689e8d44666315f2e82d337b9f07bd76ae3aa2", size = 405751 }, + { url = "https://files.pythonhosted.org/packages/76/63/e6c3dbc1f78d001589b75e56a288c47723de28c580ad715eb116639152b5/watchfiles-1.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:406520216186b99374cdb58bc48e34bb74535adec160c8459894884c983a149c", size = 397313 }, + { url = "https://files.pythonhosted.org/packages/6c/a2/8afa359ff52e99af1632f90cbf359da46184207e893a5f179301b0c8d6df/watchfiles-1.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb45350fd1dc75cd68d3d72c47f5b513cb0578da716df5fba02fff31c69d5f2d", size = 450792 }, + { url = "https://files.pythonhosted.org/packages/1d/bf/7446b401667f5c64972a57a0233be1104157fc3abf72c4ef2666c1bd09b2/watchfiles-1.1.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:11ee4444250fcbeb47459a877e5e80ed994ce8e8d20283857fc128be1715dac7", size = 458196 }, + { url = "https://files.pythonhosted.org/packages/58/2f/501ddbdfa3fa874ea5597c77eeea3d413579c29af26c1091b08d0c792280/watchfiles-1.1.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bda8136e6a80bdea23e5e74e09df0362744d24ffb8cd59c4a95a6ce3d142f79c", size = 484788 }, + { url = "https://files.pythonhosted.org/packages/61/1e/9c18eb2eb5c953c96bc0e5f626f0e53cfef4bd19bd50d71d1a049c63a575/watchfiles-1.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b915daeb2d8c1f5cee4b970f2e2c988ce6514aace3c9296e58dd64dc9aa5d575", size = 597879 }, + { url = "https://files.pythonhosted.org/packages/8b/6c/1467402e5185d89388b4486745af1e0325007af0017c3384cc786fff0542/watchfiles-1.1.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ed8fc66786de8d0376f9f913c09e963c66e90ced9aa11997f93bdb30f7c872a8", size = 477447 }, + { url = "https://files.pythonhosted.org/packages/2b/a1/ec0a606bde4853d6c4a578f9391eeb3684a9aea736a8eb217e3e00aa89a1/watchfiles-1.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe4371595edf78c41ef8ac8df20df3943e13defd0efcb732b2e393b5a8a7a71f", size = 453145 }, + { url = "https://files.pythonhosted.org/packages/90/b9/ef6f0c247a6a35d689fc970dc7f6734f9257451aefb30def5d100d6246a5/watchfiles-1.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b7c5f6fe273291f4d414d55b2c80d33c457b8a42677ad14b4b47ff025d0893e4", size = 626539 }, + { url = "https://files.pythonhosted.org/packages/34/44/6ffda5537085106ff5aaa762b0d130ac6c75a08015dd1621376f708c94de/watchfiles-1.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7738027989881e70e3723c75921f1efa45225084228788fc59ea8c6d732eb30d", size = 624472 }, + { url = "https://files.pythonhosted.org/packages/c3/e3/71170985c48028fa3f0a50946916a14055e741db11c2e7bc2f3b61f4d0e3/watchfiles-1.1.0-cp311-cp311-win32.whl", hash = "sha256:622d6b2c06be19f6e89b1d951485a232e3b59618def88dbeda575ed8f0d8dbf2", size = 279348 }, + { url = "https://files.pythonhosted.org/packages/89/1b/3e39c68b68a7a171070f81fc2561d23ce8d6859659406842a0e4bebf3bba/watchfiles-1.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:48aa25e5992b61debc908a61ab4d3f216b64f44fdaa71eb082d8b2de846b7d12", size = 292607 }, + { url = "https://files.pythonhosted.org/packages/61/9f/2973b7539f2bdb6ea86d2c87f70f615a71a1fc2dba2911795cea25968aea/watchfiles-1.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:00645eb79a3faa70d9cb15c8d4187bb72970b2470e938670240c7998dad9f13a", size = 285056 }, + { url = "https://files.pythonhosted.org/packages/f6/b8/858957045a38a4079203a33aaa7d23ea9269ca7761c8a074af3524fbb240/watchfiles-1.1.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9dc001c3e10de4725c749d4c2f2bdc6ae24de5a88a339c4bce32300a31ede179", size = 402339 }, + { url = "https://files.pythonhosted.org/packages/80/28/98b222cca751ba68e88521fabd79a4fab64005fc5976ea49b53fa205d1fa/watchfiles-1.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d9ba68ec283153dead62cbe81872d28e053745f12335d037de9cbd14bd1877f5", size = 394409 }, + { url = "https://files.pythonhosted.org/packages/86/50/dee79968566c03190677c26f7f47960aff738d32087087bdf63a5473e7df/watchfiles-1.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:130fc497b8ee68dce163e4254d9b0356411d1490e868bd8790028bc46c5cc297", size = 450939 }, + { url = "https://files.pythonhosted.org/packages/40/45/a7b56fb129700f3cfe2594a01aa38d033b92a33dddce86c8dfdfc1247b72/watchfiles-1.1.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:50a51a90610d0845a5931a780d8e51d7bd7f309ebc25132ba975aca016b576a0", size = 457270 }, + { url = "https://files.pythonhosted.org/packages/b5/c8/fa5ef9476b1d02dc6b5e258f515fcaaecf559037edf8b6feffcbc097c4b8/watchfiles-1.1.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dc44678a72ac0910bac46fa6a0de6af9ba1355669b3dfaf1ce5f05ca7a74364e", size = 483370 }, + { url = "https://files.pythonhosted.org/packages/98/68/42cfcdd6533ec94f0a7aab83f759ec11280f70b11bfba0b0f885e298f9bd/watchfiles-1.1.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a543492513a93b001975ae283a51f4b67973662a375a403ae82f420d2c7205ee", size = 598654 }, + { url = "https://files.pythonhosted.org/packages/d3/74/b2a1544224118cc28df7e59008a929e711f9c68ce7d554e171b2dc531352/watchfiles-1.1.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ac164e20d17cc285f2b94dc31c384bc3aa3dd5e7490473b3db043dd70fbccfd", size = 478667 }, + { url = "https://files.pythonhosted.org/packages/8c/77/e3362fe308358dc9f8588102481e599c83e1b91c2ae843780a7ded939a35/watchfiles-1.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f7590d5a455321e53857892ab8879dce62d1f4b04748769f5adf2e707afb9d4f", size = 452213 }, + { url = "https://files.pythonhosted.org/packages/6e/17/c8f1a36540c9a1558d4faf08e909399e8133599fa359bf52ec8fcee5be6f/watchfiles-1.1.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:37d3d3f7defb13f62ece99e9be912afe9dd8a0077b7c45ee5a57c74811d581a4", size = 626718 }, + { url = "https://files.pythonhosted.org/packages/26/45/fb599be38b4bd38032643783d7496a26a6f9ae05dea1a42e58229a20ac13/watchfiles-1.1.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:7080c4bb3efd70a07b1cc2df99a7aa51d98685be56be6038c3169199d0a1c69f", size = 623098 }, + { url = "https://files.pythonhosted.org/packages/a1/e7/fdf40e038475498e160cd167333c946e45d8563ae4dd65caf757e9ffe6b4/watchfiles-1.1.0-cp312-cp312-win32.whl", hash = "sha256:cbcf8630ef4afb05dc30107bfa17f16c0896bb30ee48fc24bf64c1f970f3b1fd", size = 279209 }, + { url = "https://files.pythonhosted.org/packages/3f/d3/3ae9d5124ec75143bdf088d436cba39812122edc47709cd2caafeac3266f/watchfiles-1.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:cbd949bdd87567b0ad183d7676feb98136cde5bb9025403794a4c0db28ed3a47", size = 292786 }, + { url = "https://files.pythonhosted.org/packages/26/2f/7dd4fc8b5f2b34b545e19629b4a018bfb1de23b3a496766a2c1165ca890d/watchfiles-1.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:0a7d40b77f07be87c6faa93d0951a0fcd8cbca1ddff60a1b65d741bac6f3a9f6", size = 284343 }, + { url = "https://files.pythonhosted.org/packages/d3/42/fae874df96595556a9089ade83be34a2e04f0f11eb53a8dbf8a8a5e562b4/watchfiles-1.1.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5007f860c7f1f8df471e4e04aaa8c43673429047d63205d1630880f7637bca30", size = 402004 }, + { url = "https://files.pythonhosted.org/packages/fa/55/a77e533e59c3003d9803c09c44c3651224067cbe7fb5d574ddbaa31e11ca/watchfiles-1.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:20ecc8abbd957046f1fe9562757903f5eaf57c3bce70929fda6c7711bb58074a", size = 393671 }, + { url = "https://files.pythonhosted.org/packages/05/68/b0afb3f79c8e832e6571022611adbdc36e35a44e14f129ba09709aa4bb7a/watchfiles-1.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2f0498b7d2a3c072766dba3274fe22a183dbea1f99d188f1c6c72209a1063dc", size = 449772 }, + { url = "https://files.pythonhosted.org/packages/ff/05/46dd1f6879bc40e1e74c6c39a1b9ab9e790bf1f5a2fe6c08b463d9a807f4/watchfiles-1.1.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:239736577e848678e13b201bba14e89718f5c2133dfd6b1f7846fa1b58a8532b", size = 456789 }, + { url = "https://files.pythonhosted.org/packages/8b/ca/0eeb2c06227ca7f12e50a47a3679df0cd1ba487ea19cf844a905920f8e95/watchfiles-1.1.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eff4b8d89f444f7e49136dc695599a591ff769300734446c0a86cba2eb2f9895", size = 482551 }, + { url = "https://files.pythonhosted.org/packages/31/47/2cecbd8694095647406645f822781008cc524320466ea393f55fe70eed3b/watchfiles-1.1.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12b0a02a91762c08f7264e2e79542f76870c3040bbc847fb67410ab81474932a", size = 597420 }, + { url = "https://files.pythonhosted.org/packages/d9/7e/82abc4240e0806846548559d70f0b1a6dfdca75c1b4f9fa62b504ae9b083/watchfiles-1.1.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:29e7bc2eee15cbb339c68445959108803dc14ee0c7b4eea556400131a8de462b", size = 477950 }, + { url = "https://files.pythonhosted.org/packages/25/0d/4d564798a49bf5482a4fa9416dea6b6c0733a3b5700cb8a5a503c4b15853/watchfiles-1.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d9481174d3ed982e269c090f780122fb59cee6c3796f74efe74e70f7780ed94c", size = 451706 }, + { url = "https://files.pythonhosted.org/packages/81/b5/5516cf46b033192d544102ea07c65b6f770f10ed1d0a6d388f5d3874f6e4/watchfiles-1.1.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:80f811146831c8c86ab17b640801c25dc0a88c630e855e2bef3568f30434d52b", size = 625814 }, + { url = "https://files.pythonhosted.org/packages/0c/dd/7c1331f902f30669ac3e754680b6edb9a0dd06dea5438e61128111fadd2c/watchfiles-1.1.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:60022527e71d1d1fda67a33150ee42869042bce3d0fcc9cc49be009a9cded3fb", size = 622820 }, + { url = "https://files.pythonhosted.org/packages/1b/14/36d7a8e27cd128d7b1009e7715a7c02f6c131be9d4ce1e5c3b73d0e342d8/watchfiles-1.1.0-cp313-cp313-win32.whl", hash = "sha256:32d6d4e583593cb8576e129879ea0991660b935177c0f93c6681359b3654bfa9", size = 279194 }, + { url = "https://files.pythonhosted.org/packages/25/41/2dd88054b849aa546dbeef5696019c58f8e0774f4d1c42123273304cdb2e/watchfiles-1.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:f21af781a4a6fbad54f03c598ab620e3a77032c5878f3d780448421a6e1818c7", size = 292349 }, + { url = "https://files.pythonhosted.org/packages/c8/cf/421d659de88285eb13941cf11a81f875c176f76a6d99342599be88e08d03/watchfiles-1.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:5366164391873ed76bfdf618818c82084c9db7fac82b64a20c44d335eec9ced5", size = 283836 }, + { url = "https://files.pythonhosted.org/packages/45/10/6faf6858d527e3599cc50ec9fcae73590fbddc1420bd4fdccfebffeedbc6/watchfiles-1.1.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:17ab167cca6339c2b830b744eaf10803d2a5b6683be4d79d8475d88b4a8a4be1", size = 400343 }, + { url = "https://files.pythonhosted.org/packages/03/20/5cb7d3966f5e8c718006d0e97dfe379a82f16fecd3caa7810f634412047a/watchfiles-1.1.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:328dbc9bff7205c215a7807da7c18dce37da7da718e798356212d22696404339", size = 392916 }, + { url = "https://files.pythonhosted.org/packages/8c/07/d8f1176328fa9e9581b6f120b017e286d2a2d22ae3f554efd9515c8e1b49/watchfiles-1.1.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f7208ab6e009c627b7557ce55c465c98967e8caa8b11833531fdf95799372633", size = 449582 }, + { url = "https://files.pythonhosted.org/packages/66/e8/80a14a453cf6038e81d072a86c05276692a1826471fef91df7537dba8b46/watchfiles-1.1.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a8f6f72974a19efead54195bc9bed4d850fc047bb7aa971268fd9a8387c89011", size = 456752 }, + { url = "https://files.pythonhosted.org/packages/5a/25/0853b3fe0e3c2f5af9ea60eb2e781eade939760239a72c2d38fc4cc335f6/watchfiles-1.1.0-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d181ef50923c29cf0450c3cd47e2f0557b62218c50b2ab8ce2ecaa02bd97e670", size = 481436 }, + { url = "https://files.pythonhosted.org/packages/fe/9e/4af0056c258b861fbb29dcb36258de1e2b857be4a9509e6298abcf31e5c9/watchfiles-1.1.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:adb4167043d3a78280d5d05ce0ba22055c266cf8655ce942f2fb881262ff3cdf", size = 596016 }, + { url = "https://files.pythonhosted.org/packages/c5/fa/95d604b58aa375e781daf350897aaaa089cff59d84147e9ccff2447c8294/watchfiles-1.1.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c5701dc474b041e2934a26d31d39f90fac8a3dee2322b39f7729867f932b1d4", size = 476727 }, + { url = "https://files.pythonhosted.org/packages/65/95/fe479b2664f19be4cf5ceeb21be05afd491d95f142e72d26a42f41b7c4f8/watchfiles-1.1.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b067915e3c3936966a8607f6fe5487df0c9c4afb85226613b520890049deea20", size = 451864 }, + { url = "https://files.pythonhosted.org/packages/d3/8a/3c4af14b93a15ce55901cd7a92e1a4701910f1768c78fb30f61d2b79785b/watchfiles-1.1.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:9c733cda03b6d636b4219625a4acb5c6ffb10803338e437fb614fef9516825ef", size = 625626 }, + { url = "https://files.pythonhosted.org/packages/da/f5/cf6aa047d4d9e128f4b7cde615236a915673775ef171ff85971d698f3c2c/watchfiles-1.1.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:cc08ef8b90d78bfac66f0def80240b0197008e4852c9f285907377b2947ffdcb", size = 622744 }, + { url = "https://files.pythonhosted.org/packages/be/7c/a3d7c55cfa377c2f62c4ae3c6502b997186bc5e38156bafcb9b653de9a6d/watchfiles-1.1.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3a6fd40bbb50d24976eb275ccb55cd1951dfb63dbc27cae3066a6ca5f4beabd5", size = 406748 }, + { url = "https://files.pythonhosted.org/packages/38/d0/c46f1b2c0ca47f3667b144de6f0515f6d1c670d72f2ca29861cac78abaa1/watchfiles-1.1.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9f811079d2f9795b5d48b55a37aa7773680a5659afe34b54cc1d86590a51507d", size = 398801 }, + { url = "https://files.pythonhosted.org/packages/70/9c/9a6a42e97f92eeed77c3485a43ea96723900aefa3ac739a8c73f4bff2cd7/watchfiles-1.1.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a2726d7bfd9f76158c84c10a409b77a320426540df8c35be172444394b17f7ea", size = 451528 }, + { url = "https://files.pythonhosted.org/packages/51/7b/98c7f4f7ce7ff03023cf971cd84a3ee3b790021ae7584ffffa0eb2554b96/watchfiles-1.1.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:df32d59cb9780f66d165a9a7a26f19df2c7d24e3bd58713108b41d0ff4f929c6", size = 454095 }, + { url = "https://files.pythonhosted.org/packages/8c/6b/686dcf5d3525ad17b384fd94708e95193529b460a1b7bf40851f1328ec6e/watchfiles-1.1.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:0ece16b563b17ab26eaa2d52230c9a7ae46cf01759621f4fbbca280e438267b3", size = 406910 }, + { url = "https://files.pythonhosted.org/packages/f3/d3/71c2dcf81dc1edcf8af9f4d8d63b1316fb0a2dd90cbfd427e8d9dd584a90/watchfiles-1.1.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:51b81e55d40c4b4aa8658427a3ee7ea847c591ae9e8b81ef94a90b668999353c", size = 398816 }, + { url = "https://files.pythonhosted.org/packages/b8/fa/12269467b2fc006f8fce4cd6c3acfa77491dd0777d2a747415f28ccc8c60/watchfiles-1.1.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2bcdc54ea267fe72bfc7d83c041e4eb58d7d8dc6f578dfddb52f037ce62f432", size = 451584 }, + { url = "https://files.pythonhosted.org/packages/bd/d3/254cea30f918f489db09d6a8435a7de7047f8cb68584477a515f160541d6/watchfiles-1.1.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:923fec6e5461c42bd7e3fd5ec37492c6f3468be0499bc0707b4bbbc16ac21792", size = 454009 }, +] + +[[package]] +name = "wcwidth" +version = "0.2.13" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6c/63/53559446a878410fc5a5974feb13d31d78d752eb18aeba59c7fef1af7598/wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5", size = 101301 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/84/fd2ba7aafacbad3c4201d395674fc6348826569da3c0937e75505ead3528/wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859", size = 34166 }, +] + +[[package]] +name = "weaviate-client" +version = "4.16.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "authlib" }, + { name = "deprecation" }, + { name = "grpcio" }, + { name = "grpcio-health-checking" }, + { name = "httpx" }, + { name = "pydantic" }, + { name = "validators" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8f/1e/b44262cd9edff939f7a6e40b6134d737a28bcdb0445cbdf2af9544953658/weaviate_client-4.16.6.tar.gz", hash = "sha256:79064bd976b0ec6bee09507f74481711bcbc861bcc097ca37db22bcf948771e6", size = 779904 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/d2/7cf098b1d14dd237a81b84012f0c4cdea355d2312b10410148384fa8b39a/weaviate_client-4.16.6-py3-none-any.whl", hash = "sha256:8eafcac785876bc731b7dedd7272a93b530fc5ed807ab54b6d74f9493a014dec", size = 597469 }, +] + +[[package]] +name = "webencodings" +version = "0.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/02/ae6ceac1baeda530866a85075641cec12989bd8d31af6d5ab4a3e8c92f47/webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923", size = 9721 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", size = 11774 }, +] + +[[package]] +name = "websocket-client" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e6/30/fba0d96b4b5fbf5948ed3f4681f7da2f9f64512e1d303f94b4cc174c24a5/websocket_client-1.8.0.tar.gz", hash = "sha256:3239df9f44da632f96012472805d40a23281a991027ce11d2f45a6f24ac4c3da", size = 54648 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/84/44687a29792a70e111c5c477230a72c4b957d88d16141199bf9acb7537a3/websocket_client-1.8.0-py3-none-any.whl", hash = "sha256:17b44cc997f5c498e809b22cdf2d9c7a9e71c02c8cc2b6c56e7c2d1239bfa526", size = 58826 }, +] + +[[package]] +name = "websockets" +version = "15.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/da/6462a9f510c0c49837bbc9345aca92d767a56c1fb2939e1579df1e1cdcf7/websockets-15.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d63efaa0cd96cf0c5fe4d581521d9fa87744540d4bc999ae6e08595a1014b45b", size = 175423 }, + { url = "https://files.pythonhosted.org/packages/1c/9f/9d11c1a4eb046a9e106483b9ff69bce7ac880443f00e5ce64261b47b07e7/websockets-15.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ac60e3b188ec7574cb761b08d50fcedf9d77f1530352db4eef1707fe9dee7205", size = 173080 }, + { url = "https://files.pythonhosted.org/packages/d5/4f/b462242432d93ea45f297b6179c7333dd0402b855a912a04e7fc61c0d71f/websockets-15.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5756779642579d902eed757b21b0164cd6fe338506a8083eb58af5c372e39d9a", size = 173329 }, + { url = "https://files.pythonhosted.org/packages/6e/0c/6afa1f4644d7ed50284ac59cc70ef8abd44ccf7d45850d989ea7310538d0/websockets-15.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fdfe3e2a29e4db3659dbd5bbf04560cea53dd9610273917799f1cde46aa725e", size = 182312 }, + { url = "https://files.pythonhosted.org/packages/dd/d4/ffc8bd1350b229ca7a4db2a3e1c482cf87cea1baccd0ef3e72bc720caeec/websockets-15.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c2529b320eb9e35af0fa3016c187dffb84a3ecc572bcee7c3ce302bfeba52bf", size = 181319 }, + { url = "https://files.pythonhosted.org/packages/97/3a/5323a6bb94917af13bbb34009fac01e55c51dfde354f63692bf2533ffbc2/websockets-15.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac1e5c9054fe23226fb11e05a6e630837f074174c4c2f0fe442996112a6de4fb", size = 181631 }, + { url = "https://files.pythonhosted.org/packages/a6/cc/1aeb0f7cee59ef065724041bb7ed667b6ab1eeffe5141696cccec2687b66/websockets-15.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5df592cd503496351d6dc14f7cdad49f268d8e618f80dce0cd5a36b93c3fc08d", size = 182016 }, + { url = "https://files.pythonhosted.org/packages/79/f9/c86f8f7af208e4161a7f7e02774e9d0a81c632ae76db2ff22549e1718a51/websockets-15.0.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0a34631031a8f05657e8e90903e656959234f3a04552259458aac0b0f9ae6fd9", size = 181426 }, + { url = "https://files.pythonhosted.org/packages/c7/b9/828b0bc6753db905b91df6ae477c0b14a141090df64fb17f8a9d7e3516cf/websockets-15.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3d00075aa65772e7ce9e990cab3ff1de702aa09be3940d1dc88d5abf1ab8a09c", size = 181360 }, + { url = "https://files.pythonhosted.org/packages/89/fb/250f5533ec468ba6327055b7d98b9df056fb1ce623b8b6aaafb30b55d02e/websockets-15.0.1-cp310-cp310-win32.whl", hash = "sha256:1234d4ef35db82f5446dca8e35a7da7964d02c127b095e172e54397fb6a6c256", size = 176388 }, + { url = "https://files.pythonhosted.org/packages/1c/46/aca7082012768bb98e5608f01658ff3ac8437e563eca41cf068bd5849a5e/websockets-15.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:39c1fec2c11dc8d89bba6b2bf1556af381611a173ac2b511cf7231622058af41", size = 176830 }, + { url = "https://files.pythonhosted.org/packages/9f/32/18fcd5919c293a398db67443acd33fde142f283853076049824fc58e6f75/websockets-15.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431", size = 175423 }, + { url = "https://files.pythonhosted.org/packages/76/70/ba1ad96b07869275ef42e2ce21f07a5b0148936688c2baf7e4a1f60d5058/websockets-15.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57", size = 173082 }, + { url = "https://files.pythonhosted.org/packages/86/f2/10b55821dd40eb696ce4704a87d57774696f9451108cff0d2824c97e0f97/websockets-15.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905", size = 173330 }, + { url = "https://files.pythonhosted.org/packages/a5/90/1c37ae8b8a113d3daf1065222b6af61cc44102da95388ac0018fcb7d93d9/websockets-15.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562", size = 182878 }, + { url = "https://files.pythonhosted.org/packages/8e/8d/96e8e288b2a41dffafb78e8904ea7367ee4f891dafc2ab8d87e2124cb3d3/websockets-15.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66dd88c918e3287efc22409d426c8f729688d89a0c587c88971a0faa2c2f3792", size = 181883 }, + { url = "https://files.pythonhosted.org/packages/93/1f/5d6dbf551766308f6f50f8baf8e9860be6182911e8106da7a7f73785f4c4/websockets-15.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dd8327c795b3e3f219760fa603dcae1dcc148172290a8ab15158cf85a953413", size = 182252 }, + { url = "https://files.pythonhosted.org/packages/d4/78/2d4fed9123e6620cbf1706c0de8a1632e1a28e7774d94346d7de1bba2ca3/websockets-15.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8", size = 182521 }, + { url = "https://files.pythonhosted.org/packages/e7/3b/66d4c1b444dd1a9823c4a81f50231b921bab54eee2f69e70319b4e21f1ca/websockets-15.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3", size = 181958 }, + { url = "https://files.pythonhosted.org/packages/08/ff/e9eed2ee5fed6f76fdd6032ca5cd38c57ca9661430bb3d5fb2872dc8703c/websockets-15.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf", size = 181918 }, + { url = "https://files.pythonhosted.org/packages/d8/75/994634a49b7e12532be6a42103597b71098fd25900f7437d6055ed39930a/websockets-15.0.1-cp311-cp311-win32.whl", hash = "sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85", size = 176388 }, + { url = "https://files.pythonhosted.org/packages/98/93/e36c73f78400a65f5e236cd376713c34182e6663f6889cd45a4a04d8f203/websockets-15.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065", size = 176828 }, + { url = "https://files.pythonhosted.org/packages/51/6b/4545a0d843594f5d0771e86463606a3988b5a09ca5123136f8a76580dd63/websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3", size = 175437 }, + { url = "https://files.pythonhosted.org/packages/f4/71/809a0f5f6a06522af902e0f2ea2757f71ead94610010cf570ab5c98e99ed/websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665", size = 173096 }, + { url = "https://files.pythonhosted.org/packages/3d/69/1a681dd6f02180916f116894181eab8b2e25b31e484c5d0eae637ec01f7c/websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2", size = 173332 }, + { url = "https://files.pythonhosted.org/packages/a6/02/0073b3952f5bce97eafbb35757f8d0d54812b6174ed8dd952aa08429bcc3/websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215", size = 183152 }, + { url = "https://files.pythonhosted.org/packages/74/45/c205c8480eafd114b428284840da0b1be9ffd0e4f87338dc95dc6ff961a1/websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5", size = 182096 }, + { url = "https://files.pythonhosted.org/packages/14/8f/aa61f528fba38578ec553c145857a181384c72b98156f858ca5c8e82d9d3/websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65", size = 182523 }, + { url = "https://files.pythonhosted.org/packages/ec/6d/0267396610add5bc0d0d3e77f546d4cd287200804fe02323797de77dbce9/websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe", size = 182790 }, + { url = "https://files.pythonhosted.org/packages/02/05/c68c5adbf679cf610ae2f74a9b871ae84564462955d991178f95a1ddb7dd/websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4", size = 182165 }, + { url = "https://files.pythonhosted.org/packages/29/93/bb672df7b2f5faac89761cb5fa34f5cec45a4026c383a4b5761c6cea5c16/websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597", size = 182160 }, + { url = "https://files.pythonhosted.org/packages/ff/83/de1f7709376dc3ca9b7eeb4b9a07b4526b14876b6d372a4dc62312bebee0/websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9", size = 176395 }, + { url = "https://files.pythonhosted.org/packages/7d/71/abf2ebc3bbfa40f391ce1428c7168fb20582d0ff57019b69ea20fa698043/websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7", size = 176841 }, + { url = "https://files.pythonhosted.org/packages/cb/9f/51f0cf64471a9d2b4d0fc6c534f323b664e7095640c34562f5182e5a7195/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931", size = 175440 }, + { url = "https://files.pythonhosted.org/packages/8a/05/aa116ec9943c718905997412c5989f7ed671bc0188ee2ba89520e8765d7b/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675", size = 173098 }, + { url = "https://files.pythonhosted.org/packages/ff/0b/33cef55ff24f2d92924923c99926dcce78e7bd922d649467f0eda8368923/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151", size = 173329 }, + { url = "https://files.pythonhosted.org/packages/31/1d/063b25dcc01faa8fada1469bdf769de3768b7044eac9d41f734fd7b6ad6d/websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22", size = 183111 }, + { url = "https://files.pythonhosted.org/packages/93/53/9a87ee494a51bf63e4ec9241c1ccc4f7c2f45fff85d5bde2ff74fcb68b9e/websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f", size = 182054 }, + { url = "https://files.pythonhosted.org/packages/ff/b2/83a6ddf56cdcbad4e3d841fcc55d6ba7d19aeb89c50f24dd7e859ec0805f/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8", size = 182496 }, + { url = "https://files.pythonhosted.org/packages/98/41/e7038944ed0abf34c45aa4635ba28136f06052e08fc2168520bb8b25149f/websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375", size = 182829 }, + { url = "https://files.pythonhosted.org/packages/e0/17/de15b6158680c7623c6ef0db361da965ab25d813ae54fcfeae2e5b9ef910/websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d", size = 182217 }, + { url = "https://files.pythonhosted.org/packages/33/2b/1f168cb6041853eef0362fb9554c3824367c5560cbdaad89ac40f8c2edfc/websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4", size = 182195 }, + { url = "https://files.pythonhosted.org/packages/86/eb/20b6cdf273913d0ad05a6a14aed4b9a85591c18a987a3d47f20fa13dcc47/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa", size = 176393 }, + { url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837 }, + { url = "https://files.pythonhosted.org/packages/02/9e/d40f779fa16f74d3468357197af8d6ad07e7c5a27ea1ca74ceb38986f77a/websockets-15.0.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0c9e74d766f2818bb95f84c25be4dea09841ac0f734d1966f415e4edfc4ef1c3", size = 173109 }, + { url = "https://files.pythonhosted.org/packages/bc/cd/5b887b8585a593073fd92f7c23ecd3985cd2c3175025a91b0d69b0551372/websockets-15.0.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1009ee0c7739c08a0cd59de430d6de452a55e42d6b522de7aa15e6f67db0b8e1", size = 173343 }, + { url = "https://files.pythonhosted.org/packages/fe/ae/d34f7556890341e900a95acf4886833646306269f899d58ad62f588bf410/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76d1f20b1c7a2fa82367e04982e708723ba0e7b8d43aa643d3dcd404d74f1475", size = 174599 }, + { url = "https://files.pythonhosted.org/packages/71/e6/5fd43993a87db364ec60fc1d608273a1a465c0caba69176dd160e197ce42/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f29d80eb9a9263b8d109135351caf568cc3f80b9928bccde535c235de55c22d9", size = 174207 }, + { url = "https://files.pythonhosted.org/packages/2b/fb/c492d6daa5ec067c2988ac80c61359ace5c4c674c532985ac5a123436cec/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b359ed09954d7c18bbc1680f380c7301f92c60bf924171629c5db97febb12f04", size = 174155 }, + { url = "https://files.pythonhosted.org/packages/68/a1/dcb68430b1d00b698ae7a7e0194433bce4f07ded185f0ee5fb21e2a2e91e/websockets-15.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:cad21560da69f4ce7658ca2cb83138fb4cf695a2ba3e475e0559e05991aa8122", size = 176884 }, + { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743 }, +] + +[[package]] +name = "wheel" +version = "0.45.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8a/98/2d9906746cdc6a6ef809ae6338005b3f21bb568bea3165cfc6a243fdc25c/wheel-0.45.1.tar.gz", hash = "sha256:661e1abd9198507b1409a20c02106d9670b2576e916d58f520316666abca6729", size = 107545 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/2c/87f3254fd8ffd29e4c02732eee68a83a1d3c346ae39bc6822dcbcb697f2b/wheel-0.45.1-py3-none-any.whl", hash = "sha256:708e7481cc80179af0e556bbf0cc00b8444c7321e2700b8d8580231d13017248", size = 72494 }, +] + +[[package]] +name = "win32-setctime" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b3/8f/705086c9d734d3b663af0e9bb3d4de6578d08f46b1b101c2442fd9aecaa2/win32_setctime-1.2.0.tar.gz", hash = "sha256:ae1fdf948f5640aae05c511ade119313fb6a30d7eabe25fef9764dca5873c4c0", size = 4867 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/07/c6fe3ad3e685340704d314d765b7912993bcb8dc198f0e7a89382d37974b/win32_setctime-1.2.0-py3-none-any.whl", hash = "sha256:95d644c4e708aba81dc3704a116d8cbc974d70b3bdb8be1d150e36be6e9d1390", size = 4083 }, +] + +[[package]] +name = "wrapt" +version = "1.17.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/fc/e91cc220803d7bc4db93fb02facd8461c37364151b8494762cc88b0fbcef/wrapt-1.17.2.tar.gz", hash = "sha256:41388e9d4d1522446fe79d3213196bd9e3b301a336965b9e27ca2788ebd122f3", size = 55531 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/d1/1daec934997e8b160040c78d7b31789f19b122110a75eca3d4e8da0049e1/wrapt-1.17.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3d57c572081fed831ad2d26fd430d565b76aa277ed1d30ff4d40670b1c0dd984", size = 53307 }, + { url = "https://files.pythonhosted.org/packages/1b/7b/13369d42651b809389c1a7153baa01d9700430576c81a2f5c5e460df0ed9/wrapt-1.17.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b5e251054542ae57ac7f3fba5d10bfff615b6c2fb09abeb37d2f1463f841ae22", size = 38486 }, + { url = "https://files.pythonhosted.org/packages/62/bf/e0105016f907c30b4bd9e377867c48c34dc9c6c0c104556c9c9126bd89ed/wrapt-1.17.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:80dd7db6a7cb57ffbc279c4394246414ec99537ae81ffd702443335a61dbf3a7", size = 38777 }, + { url = "https://files.pythonhosted.org/packages/27/70/0f6e0679845cbf8b165e027d43402a55494779295c4b08414097b258ac87/wrapt-1.17.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a6e821770cf99cc586d33833b2ff32faebdbe886bd6322395606cf55153246c", size = 83314 }, + { url = "https://files.pythonhosted.org/packages/0f/77/0576d841bf84af8579124a93d216f55d6f74374e4445264cb378a6ed33eb/wrapt-1.17.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b60fb58b90c6d63779cb0c0c54eeb38941bae3ecf7a73c764c52c88c2dcb9d72", size = 74947 }, + { url = "https://files.pythonhosted.org/packages/90/ec/00759565518f268ed707dcc40f7eeec38637d46b098a1f5143bff488fe97/wrapt-1.17.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b870b5df5b71d8c3359d21be8f0d6c485fa0ebdb6477dda51a1ea54a9b558061", size = 82778 }, + { url = "https://files.pythonhosted.org/packages/f8/5a/7cffd26b1c607b0b0c8a9ca9d75757ad7620c9c0a9b4a25d3f8a1480fafc/wrapt-1.17.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4011d137b9955791f9084749cba9a367c68d50ab8d11d64c50ba1688c9b457f2", size = 81716 }, + { url = "https://files.pythonhosted.org/packages/7e/09/dccf68fa98e862df7e6a60a61d43d644b7d095a5fc36dbb591bbd4a1c7b2/wrapt-1.17.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:1473400e5b2733e58b396a04eb7f35f541e1fb976d0c0724d0223dd607e0f74c", size = 74548 }, + { url = "https://files.pythonhosted.org/packages/b7/8e/067021fa3c8814952c5e228d916963c1115b983e21393289de15128e867e/wrapt-1.17.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3cedbfa9c940fdad3e6e941db7138e26ce8aad38ab5fe9dcfadfed9db7a54e62", size = 81334 }, + { url = "https://files.pythonhosted.org/packages/4b/0d/9d4b5219ae4393f718699ca1c05f5ebc0c40d076f7e65fd48f5f693294fb/wrapt-1.17.2-cp310-cp310-win32.whl", hash = "sha256:582530701bff1dec6779efa00c516496968edd851fba224fbd86e46cc6b73563", size = 36427 }, + { url = "https://files.pythonhosted.org/packages/72/6a/c5a83e8f61aec1e1aeef939807602fb880e5872371e95df2137142f5c58e/wrapt-1.17.2-cp310-cp310-win_amd64.whl", hash = "sha256:58705da316756681ad3c9c73fd15499aa4d8c69f9fd38dc8a35e06c12468582f", size = 38774 }, + { url = "https://files.pythonhosted.org/packages/cd/f7/a2aab2cbc7a665efab072344a8949a71081eed1d2f451f7f7d2b966594a2/wrapt-1.17.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ff04ef6eec3eee8a5efef2401495967a916feaa353643defcc03fc74fe213b58", size = 53308 }, + { url = "https://files.pythonhosted.org/packages/50/ff/149aba8365fdacef52b31a258c4dc1c57c79759c335eff0b3316a2664a64/wrapt-1.17.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4db983e7bca53819efdbd64590ee96c9213894272c776966ca6306b73e4affda", size = 38488 }, + { url = "https://files.pythonhosted.org/packages/65/46/5a917ce85b5c3b490d35c02bf71aedaa9f2f63f2d15d9949cc4ba56e8ba9/wrapt-1.17.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9abc77a4ce4c6f2a3168ff34b1da9b0f311a8f1cfd694ec96b0603dff1c79438", size = 38776 }, + { url = "https://files.pythonhosted.org/packages/ca/74/336c918d2915a4943501c77566db41d1bd6e9f4dbc317f356b9a244dfe83/wrapt-1.17.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b929ac182f5ace000d459c59c2c9c33047e20e935f8e39371fa6e3b85d56f4a", size = 83776 }, + { url = "https://files.pythonhosted.org/packages/09/99/c0c844a5ccde0fe5761d4305485297f91d67cf2a1a824c5f282e661ec7ff/wrapt-1.17.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f09b286faeff3c750a879d336fb6d8713206fc97af3adc14def0cdd349df6000", size = 75420 }, + { url = "https://files.pythonhosted.org/packages/b4/b0/9fc566b0fe08b282c850063591a756057c3247b2362b9286429ec5bf1721/wrapt-1.17.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a7ed2d9d039bd41e889f6fb9364554052ca21ce823580f6a07c4ec245c1f5d6", size = 83199 }, + { url = "https://files.pythonhosted.org/packages/9d/4b/71996e62d543b0a0bd95dda485219856def3347e3e9380cc0d6cf10cfb2f/wrapt-1.17.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:129a150f5c445165ff941fc02ee27df65940fcb8a22a61828b1853c98763a64b", size = 82307 }, + { url = "https://files.pythonhosted.org/packages/39/35/0282c0d8789c0dc9bcc738911776c762a701f95cfe113fb8f0b40e45c2b9/wrapt-1.17.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1fb5699e4464afe5c7e65fa51d4f99e0b2eadcc176e4aa33600a3df7801d6662", size = 75025 }, + { url = "https://files.pythonhosted.org/packages/4f/6d/90c9fd2c3c6fee181feecb620d95105370198b6b98a0770cba090441a828/wrapt-1.17.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9a2bce789a5ea90e51a02dfcc39e31b7f1e662bc3317979aa7e5538e3a034f72", size = 81879 }, + { url = "https://files.pythonhosted.org/packages/8f/fa/9fb6e594f2ce03ef03eddbdb5f4f90acb1452221a5351116c7c4708ac865/wrapt-1.17.2-cp311-cp311-win32.whl", hash = "sha256:4afd5814270fdf6380616b321fd31435a462019d834f83c8611a0ce7484c7317", size = 36419 }, + { url = "https://files.pythonhosted.org/packages/47/f8/fb1773491a253cbc123c5d5dc15c86041f746ed30416535f2a8df1f4a392/wrapt-1.17.2-cp311-cp311-win_amd64.whl", hash = "sha256:acc130bc0375999da18e3d19e5a86403667ac0c4042a094fefb7eec8ebac7cf3", size = 38773 }, + { url = "https://files.pythonhosted.org/packages/a1/bd/ab55f849fd1f9a58ed7ea47f5559ff09741b25f00c191231f9f059c83949/wrapt-1.17.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d5e2439eecc762cd85e7bd37161d4714aa03a33c5ba884e26c81559817ca0925", size = 53799 }, + { url = "https://files.pythonhosted.org/packages/53/18/75ddc64c3f63988f5a1d7e10fb204ffe5762bc663f8023f18ecaf31a332e/wrapt-1.17.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fc7cb4c1c744f8c05cd5f9438a3caa6ab94ce8344e952d7c45a8ed59dd88392", size = 38821 }, + { url = "https://files.pythonhosted.org/packages/48/2a/97928387d6ed1c1ebbfd4efc4133a0633546bec8481a2dd5ec961313a1c7/wrapt-1.17.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8fdbdb757d5390f7c675e558fd3186d590973244fab0c5fe63d373ade3e99d40", size = 38919 }, + { url = "https://files.pythonhosted.org/packages/73/54/3bfe5a1febbbccb7a2f77de47b989c0b85ed3a6a41614b104204a788c20e/wrapt-1.17.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5bb1d0dbf99411f3d871deb6faa9aabb9d4e744d67dcaaa05399af89d847a91d", size = 88721 }, + { url = "https://files.pythonhosted.org/packages/25/cb/7262bc1b0300b4b64af50c2720ef958c2c1917525238d661c3e9a2b71b7b/wrapt-1.17.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d18a4865f46b8579d44e4fe1e2bcbc6472ad83d98e22a26c963d46e4c125ef0b", size = 80899 }, + { url = "https://files.pythonhosted.org/packages/2a/5a/04cde32b07a7431d4ed0553a76fdb7a61270e78c5fd5a603e190ac389f14/wrapt-1.17.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc570b5f14a79734437cb7b0500376b6b791153314986074486e0b0fa8d71d98", size = 89222 }, + { url = "https://files.pythonhosted.org/packages/09/28/2e45a4f4771fcfb109e244d5dbe54259e970362a311b67a965555ba65026/wrapt-1.17.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6d9187b01bebc3875bac9b087948a2bccefe464a7d8f627cf6e48b1bbae30f82", size = 86707 }, + { url = "https://files.pythonhosted.org/packages/c6/d2/dcb56bf5f32fcd4bd9aacc77b50a539abdd5b6536872413fd3f428b21bed/wrapt-1.17.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:9e8659775f1adf02eb1e6f109751268e493c73716ca5761f8acb695e52a756ae", size = 79685 }, + { url = "https://files.pythonhosted.org/packages/80/4e/eb8b353e36711347893f502ce91c770b0b0929f8f0bed2670a6856e667a9/wrapt-1.17.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e8b2816ebef96d83657b56306152a93909a83f23994f4b30ad4573b00bd11bb9", size = 87567 }, + { url = "https://files.pythonhosted.org/packages/17/27/4fe749a54e7fae6e7146f1c7d914d28ef599dacd4416566c055564080fe2/wrapt-1.17.2-cp312-cp312-win32.whl", hash = "sha256:468090021f391fe0056ad3e807e3d9034e0fd01adcd3bdfba977b6fdf4213ea9", size = 36672 }, + { url = "https://files.pythonhosted.org/packages/15/06/1dbf478ea45c03e78a6a8c4be4fdc3c3bddea5c8de8a93bc971415e47f0f/wrapt-1.17.2-cp312-cp312-win_amd64.whl", hash = "sha256:ec89ed91f2fa8e3f52ae53cd3cf640d6feff92ba90d62236a81e4e563ac0e991", size = 38865 }, + { url = "https://files.pythonhosted.org/packages/ce/b9/0ffd557a92f3b11d4c5d5e0c5e4ad057bd9eb8586615cdaf901409920b14/wrapt-1.17.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6ed6ffac43aecfe6d86ec5b74b06a5be33d5bb9243d055141e8cabb12aa08125", size = 53800 }, + { url = "https://files.pythonhosted.org/packages/c0/ef/8be90a0b7e73c32e550c73cfb2fa09db62234227ece47b0e80a05073b375/wrapt-1.17.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:35621ae4c00e056adb0009f8e86e28eb4a41a4bfa8f9bfa9fca7d343fe94f998", size = 38824 }, + { url = "https://files.pythonhosted.org/packages/36/89/0aae34c10fe524cce30fe5fc433210376bce94cf74d05b0d68344c8ba46e/wrapt-1.17.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a604bf7a053f8362d27eb9fefd2097f82600b856d5abe996d623babd067b1ab5", size = 38920 }, + { url = "https://files.pythonhosted.org/packages/3b/24/11c4510de906d77e0cfb5197f1b1445d4fec42c9a39ea853d482698ac681/wrapt-1.17.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5cbabee4f083b6b4cd282f5b817a867cf0b1028c54d445b7ec7cfe6505057cf8", size = 88690 }, + { url = "https://files.pythonhosted.org/packages/71/d7/cfcf842291267bf455b3e266c0c29dcb675b5540ee8b50ba1699abf3af45/wrapt-1.17.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:49703ce2ddc220df165bd2962f8e03b84c89fee2d65e1c24a7defff6f988f4d6", size = 80861 }, + { url = "https://files.pythonhosted.org/packages/d5/66/5d973e9f3e7370fd686fb47a9af3319418ed925c27d72ce16b791231576d/wrapt-1.17.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8112e52c5822fc4253f3901b676c55ddf288614dc7011634e2719718eaa187dc", size = 89174 }, + { url = "https://files.pythonhosted.org/packages/a7/d3/8e17bb70f6ae25dabc1aaf990f86824e4fd98ee9cadf197054e068500d27/wrapt-1.17.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9fee687dce376205d9a494e9c121e27183b2a3df18037f89d69bd7b35bcf59e2", size = 86721 }, + { url = "https://files.pythonhosted.org/packages/6f/54/f170dfb278fe1c30d0ff864513cff526d624ab8de3254b20abb9cffedc24/wrapt-1.17.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:18983c537e04d11cf027fbb60a1e8dfd5190e2b60cc27bc0808e653e7b218d1b", size = 79763 }, + { url = "https://files.pythonhosted.org/packages/4a/98/de07243751f1c4a9b15c76019250210dd3486ce098c3d80d5f729cba029c/wrapt-1.17.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:703919b1633412ab54bcf920ab388735832fdcb9f9a00ae49387f0fe67dad504", size = 87585 }, + { url = "https://files.pythonhosted.org/packages/f9/f0/13925f4bd6548013038cdeb11ee2cbd4e37c30f8bfd5db9e5a2a370d6e20/wrapt-1.17.2-cp313-cp313-win32.whl", hash = "sha256:abbb9e76177c35d4e8568e58650aa6926040d6a9f6f03435b7a522bf1c487f9a", size = 36676 }, + { url = "https://files.pythonhosted.org/packages/bf/ae/743f16ef8c2e3628df3ddfd652b7d4c555d12c84b53f3d8218498f4ade9b/wrapt-1.17.2-cp313-cp313-win_amd64.whl", hash = "sha256:69606d7bb691b50a4240ce6b22ebb319c1cfb164e5f6569835058196e0f3a845", size = 38871 }, + { url = "https://files.pythonhosted.org/packages/3d/bc/30f903f891a82d402ffb5fda27ec1d621cc97cb74c16fea0b6141f1d4e87/wrapt-1.17.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:4a721d3c943dae44f8e243b380cb645a709ba5bd35d3ad27bc2ed947e9c68192", size = 56312 }, + { url = "https://files.pythonhosted.org/packages/8a/04/c97273eb491b5f1c918857cd26f314b74fc9b29224521f5b83f872253725/wrapt-1.17.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:766d8bbefcb9e00c3ac3b000d9acc51f1b399513f44d77dfe0eb026ad7c9a19b", size = 40062 }, + { url = "https://files.pythonhosted.org/packages/4e/ca/3b7afa1eae3a9e7fefe499db9b96813f41828b9fdb016ee836c4c379dadb/wrapt-1.17.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e496a8ce2c256da1eb98bd15803a79bee00fc351f5dfb9ea82594a3f058309e0", size = 40155 }, + { url = "https://files.pythonhosted.org/packages/89/be/7c1baed43290775cb9030c774bc53c860db140397047cc49aedaf0a15477/wrapt-1.17.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40d615e4fe22f4ad3528448c193b218e077656ca9ccb22ce2cb20db730f8d306", size = 113471 }, + { url = "https://files.pythonhosted.org/packages/32/98/4ed894cf012b6d6aae5f5cc974006bdeb92f0241775addad3f8cd6ab71c8/wrapt-1.17.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a5aaeff38654462bc4b09023918b7f21790efb807f54c000a39d41d69cf552cb", size = 101208 }, + { url = "https://files.pythonhosted.org/packages/ea/fd/0c30f2301ca94e655e5e057012e83284ce8c545df7661a78d8bfca2fac7a/wrapt-1.17.2-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a7d15bbd2bc99e92e39f49a04653062ee6085c0e18b3b7512a4f2fe91f2d681", size = 109339 }, + { url = "https://files.pythonhosted.org/packages/75/56/05d000de894c4cfcb84bcd6b1df6214297b8089a7bd324c21a4765e49b14/wrapt-1.17.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e3890b508a23299083e065f435a492b5435eba6e304a7114d2f919d400888cc6", size = 110232 }, + { url = "https://files.pythonhosted.org/packages/53/f8/c3f6b2cf9b9277fb0813418e1503e68414cd036b3b099c823379c9575e6d/wrapt-1.17.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:8c8b293cd65ad716d13d8dd3624e42e5a19cc2a2f1acc74b30c2c13f15cb61a6", size = 100476 }, + { url = "https://files.pythonhosted.org/packages/a7/b1/0bb11e29aa5139d90b770ebbfa167267b1fc548d2302c30c8f7572851738/wrapt-1.17.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c82b8785d98cdd9fed4cac84d765d234ed3251bd6afe34cb7ac523cb93e8b4f", size = 106377 }, + { url = "https://files.pythonhosted.org/packages/6a/e1/0122853035b40b3f333bbb25f1939fc1045e21dd518f7f0922b60c156f7c/wrapt-1.17.2-cp313-cp313t-win32.whl", hash = "sha256:13e6afb7fe71fe7485a4550a8844cc9ffbe263c0f1a1eea569bc7091d4898555", size = 37986 }, + { url = "https://files.pythonhosted.org/packages/09/5e/1655cf481e079c1f22d0cabdd4e51733679932718dc23bf2db175f329b76/wrapt-1.17.2-cp313-cp313t-win_amd64.whl", hash = "sha256:eaf675418ed6b3b31c7a989fd007fa7c3be66ce14e5c3b27336383604c9da85c", size = 40750 }, + { url = "https://files.pythonhosted.org/packages/2d/82/f56956041adef78f849db6b289b282e72b55ab8045a75abad81898c28d19/wrapt-1.17.2-py3-none-any.whl", hash = "sha256:b18f2d1533a71f069c7f82d524a52599053d4c7166e9dd374ae2136b7f40f7c8", size = 23594 }, +] + +[[package]] +name = "wsproto" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/4a/44d3c295350d776427904d73c189e10aeae66d7f555bb2feee16d1e4ba5a/wsproto-1.2.0.tar.gz", hash = "sha256:ad565f26ecb92588a3e43bc3d96164de84cd9902482b130d0ddbaa9664a85065", size = 53425 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/58/e860788190eba3bcce367f74d29c4675466ce8dddfba85f7827588416f01/wsproto-1.2.0-py3-none-any.whl", hash = "sha256:b9acddd652b585d75b20477888c56642fdade28bdfd3579aa24a4d2c037dd736", size = 24226 }, +] + +[[package]] +name = "xlrd" +version = "2.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/07/5a/377161c2d3538d1990d7af382c79f3b2372e880b65de21b01b1a2b78691e/xlrd-2.0.2.tar.gz", hash = "sha256:08b5e25de58f21ce71dc7db3b3b8106c1fa776f3024c54e45b45b374e89234c9", size = 100167 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1a/62/c8d562e7766786ba6587d09c5a8ba9f718ed3fa8af7f4553e8f91c36f302/xlrd-2.0.2-py2.py3-none-any.whl", hash = "sha256:ea762c3d29f4cca48d82df517b6d89fbce4db3107f9d78713e48cd321d5c9aa9", size = 96555 }, +] + +[[package]] +name = "xlsxwriter" +version = "3.2.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a7/47/7704bac42ac6fe1710ae099b70e6a1e68ed173ef14792b647808c357da43/xlsxwriter-3.2.5.tar.gz", hash = "sha256:7e88469d607cdc920151c0ab3ce9cf1a83992d4b7bc730c5ffdd1a12115a7dbe", size = 213306 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/34/a22e6664211f0c8879521328000bdcae9bf6dbafa94a923e531f6d5b3f73/xlsxwriter-3.2.5-py3-none-any.whl", hash = "sha256:4f4824234e1eaf9d95df9a8fe974585ff91d0f5e3d3f12ace5b71e443c1c6abd", size = 172347 }, +] + +[[package]] +name = "yarl" +version = "1.20.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "multidict" }, + { name = "propcache" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3c/fb/efaa23fa4e45537b827620f04cf8f3cd658b76642205162e072703a5b963/yarl-1.20.1.tar.gz", hash = "sha256:d017a4997ee50c91fd5466cef416231bb82177b93b029906cefc542ce14c35ac", size = 186428 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/65/7fed0d774abf47487c64be14e9223749468922817b5e8792b8a64792a1bb/yarl-1.20.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:6032e6da6abd41e4acda34d75a816012717000fa6839f37124a47fcefc49bec4", size = 132910 }, + { url = "https://files.pythonhosted.org/packages/8a/7b/988f55a52da99df9e56dc733b8e4e5a6ae2090081dc2754fc8fd34e60aa0/yarl-1.20.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2c7b34d804b8cf9b214f05015c4fee2ebe7ed05cf581e7192c06555c71f4446a", size = 90644 }, + { url = "https://files.pythonhosted.org/packages/f7/de/30d98f03e95d30c7e3cc093759982d038c8833ec2451001d45ef4854edc1/yarl-1.20.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0c869f2651cc77465f6cd01d938d91a11d9ea5d798738c1dc077f3de0b5e5fed", size = 89322 }, + { url = "https://files.pythonhosted.org/packages/e0/7a/f2f314f5ebfe9200724b0b748de2186b927acb334cf964fd312eb86fc286/yarl-1.20.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62915e6688eb4d180d93840cda4110995ad50c459bf931b8b3775b37c264af1e", size = 323786 }, + { url = "https://files.pythonhosted.org/packages/15/3f/718d26f189db96d993d14b984ce91de52e76309d0fd1d4296f34039856aa/yarl-1.20.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:41ebd28167bc6af8abb97fec1a399f412eec5fd61a3ccbe2305a18b84fb4ca73", size = 319627 }, + { url = "https://files.pythonhosted.org/packages/a5/76/8fcfbf5fa2369157b9898962a4a7d96764b287b085b5b3d9ffae69cdefd1/yarl-1.20.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:21242b4288a6d56f04ea193adde174b7e347ac46ce6bc84989ff7c1b1ecea84e", size = 339149 }, + { url = "https://files.pythonhosted.org/packages/3c/95/d7fc301cc4661785967acc04f54a4a42d5124905e27db27bb578aac49b5c/yarl-1.20.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bea21cdae6c7eb02ba02a475f37463abfe0a01f5d7200121b03e605d6a0439f8", size = 333327 }, + { url = "https://files.pythonhosted.org/packages/65/94/e21269718349582eee81efc5c1c08ee71c816bfc1585b77d0ec3f58089eb/yarl-1.20.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f8a891e4a22a89f5dde7862994485e19db246b70bb288d3ce73a34422e55b23", size = 326054 }, + { url = "https://files.pythonhosted.org/packages/32/ae/8616d1f07853704523519f6131d21f092e567c5af93de7e3e94b38d7f065/yarl-1.20.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dd803820d44c8853a109a34e3660e5a61beae12970da479cf44aa2954019bf70", size = 315035 }, + { url = "https://files.pythonhosted.org/packages/48/aa/0ace06280861ef055855333707db5e49c6e3a08840a7ce62682259d0a6c0/yarl-1.20.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b982fa7f74c80d5c0c7b5b38f908971e513380a10fecea528091405f519b9ebb", size = 338962 }, + { url = "https://files.pythonhosted.org/packages/20/52/1e9d0e6916f45a8fb50e6844f01cb34692455f1acd548606cbda8134cd1e/yarl-1.20.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:33f29ecfe0330c570d997bcf1afd304377f2e48f61447f37e846a6058a4d33b2", size = 335399 }, + { url = "https://files.pythonhosted.org/packages/f2/65/60452df742952c630e82f394cd409de10610481d9043aa14c61bf846b7b1/yarl-1.20.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:835ab2cfc74d5eb4a6a528c57f05688099da41cf4957cf08cad38647e4a83b30", size = 338649 }, + { url = "https://files.pythonhosted.org/packages/7b/f5/6cd4ff38dcde57a70f23719a838665ee17079640c77087404c3d34da6727/yarl-1.20.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:46b5e0ccf1943a9a6e766b2c2b8c732c55b34e28be57d8daa2b3c1d1d4009309", size = 358563 }, + { url = "https://files.pythonhosted.org/packages/d1/90/c42eefd79d0d8222cb3227bdd51b640c0c1d0aa33fe4cc86c36eccba77d3/yarl-1.20.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:df47c55f7d74127d1b11251fe6397d84afdde0d53b90bedb46a23c0e534f9d24", size = 357609 }, + { url = "https://files.pythonhosted.org/packages/03/c8/cea6b232cb4617514232e0f8a718153a95b5d82b5290711b201545825532/yarl-1.20.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:76d12524d05841276b0e22573f28d5fbcb67589836772ae9244d90dd7d66aa13", size = 350224 }, + { url = "https://files.pythonhosted.org/packages/ce/a3/eaa0ab9712f1f3d01faf43cf6f1f7210ce4ea4a7e9b28b489a2261ca8db9/yarl-1.20.1-cp310-cp310-win32.whl", hash = "sha256:6c4fbf6b02d70e512d7ade4b1f998f237137f1417ab07ec06358ea04f69134f8", size = 81753 }, + { url = "https://files.pythonhosted.org/packages/8f/34/e4abde70a9256465fe31c88ed02c3f8502b7b5dead693a4f350a06413f28/yarl-1.20.1-cp310-cp310-win_amd64.whl", hash = "sha256:aef6c4d69554d44b7f9d923245f8ad9a707d971e6209d51279196d8e8fe1ae16", size = 86817 }, + { url = "https://files.pythonhosted.org/packages/b1/18/893b50efc2350e47a874c5c2d67e55a0ea5df91186b2a6f5ac52eff887cd/yarl-1.20.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:47ee6188fea634bdfaeb2cc420f5b3b17332e6225ce88149a17c413c77ff269e", size = 133833 }, + { url = "https://files.pythonhosted.org/packages/89/ed/b8773448030e6fc47fa797f099ab9eab151a43a25717f9ac043844ad5ea3/yarl-1.20.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d0f6500f69e8402d513e5eedb77a4e1818691e8f45e6b687147963514d84b44b", size = 91070 }, + { url = "https://files.pythonhosted.org/packages/e3/e3/409bd17b1e42619bf69f60e4f031ce1ccb29bd7380117a55529e76933464/yarl-1.20.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7a8900a42fcdaad568de58887c7b2f602962356908eedb7628eaf6021a6e435b", size = 89818 }, + { url = "https://files.pythonhosted.org/packages/f8/77/64d8431a4d77c856eb2d82aa3de2ad6741365245a29b3a9543cd598ed8c5/yarl-1.20.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bad6d131fda8ef508b36be3ece16d0902e80b88ea7200f030a0f6c11d9e508d4", size = 347003 }, + { url = "https://files.pythonhosted.org/packages/8d/d2/0c7e4def093dcef0bd9fa22d4d24b023788b0a33b8d0088b51aa51e21e99/yarl-1.20.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:df018d92fe22aaebb679a7f89fe0c0f368ec497e3dda6cb81a567610f04501f1", size = 336537 }, + { url = "https://files.pythonhosted.org/packages/f0/f3/fc514f4b2cf02cb59d10cbfe228691d25929ce8f72a38db07d3febc3f706/yarl-1.20.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8f969afbb0a9b63c18d0feecf0db09d164b7a44a053e78a7d05f5df163e43833", size = 362358 }, + { url = "https://files.pythonhosted.org/packages/ea/6d/a313ac8d8391381ff9006ac05f1d4331cee3b1efaa833a53d12253733255/yarl-1.20.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:812303eb4aa98e302886ccda58d6b099e3576b1b9276161469c25803a8db277d", size = 357362 }, + { url = "https://files.pythonhosted.org/packages/00/70/8f78a95d6935a70263d46caa3dd18e1f223cf2f2ff2037baa01a22bc5b22/yarl-1.20.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98c4a7d166635147924aa0bf9bfe8d8abad6fffa6102de9c99ea04a1376f91e8", size = 348979 }, + { url = "https://files.pythonhosted.org/packages/cb/05/42773027968968f4f15143553970ee36ead27038d627f457cc44bbbeecf3/yarl-1.20.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:12e768f966538e81e6e7550f9086a6236b16e26cd964cf4df35349970f3551cf", size = 337274 }, + { url = "https://files.pythonhosted.org/packages/05/be/665634aa196954156741ea591d2f946f1b78ceee8bb8f28488bf28c0dd62/yarl-1.20.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fe41919b9d899661c5c28a8b4b0acf704510b88f27f0934ac7a7bebdd8938d5e", size = 363294 }, + { url = "https://files.pythonhosted.org/packages/eb/90/73448401d36fa4e210ece5579895731f190d5119c4b66b43b52182e88cd5/yarl-1.20.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:8601bc010d1d7780592f3fc1bdc6c72e2b6466ea34569778422943e1a1f3c389", size = 358169 }, + { url = "https://files.pythonhosted.org/packages/c3/b0/fce922d46dc1eb43c811f1889f7daa6001b27a4005587e94878570300881/yarl-1.20.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:daadbdc1f2a9033a2399c42646fbd46da7992e868a5fe9513860122d7fe7a73f", size = 362776 }, + { url = "https://files.pythonhosted.org/packages/f1/0d/b172628fce039dae8977fd22caeff3eeebffd52e86060413f5673767c427/yarl-1.20.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:03aa1e041727cb438ca762628109ef1333498b122e4c76dd858d186a37cec845", size = 381341 }, + { url = "https://files.pythonhosted.org/packages/6b/9b/5b886d7671f4580209e855974fe1cecec409aa4a89ea58b8f0560dc529b1/yarl-1.20.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:642980ef5e0fa1de5fa96d905c7e00cb2c47cb468bfcac5a18c58e27dbf8d8d1", size = 379988 }, + { url = "https://files.pythonhosted.org/packages/73/be/75ef5fd0fcd8f083a5d13f78fd3f009528132a1f2a1d7c925c39fa20aa79/yarl-1.20.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:86971e2795584fe8c002356d3b97ef6c61862720eeff03db2a7c86b678d85b3e", size = 371113 }, + { url = "https://files.pythonhosted.org/packages/50/4f/62faab3b479dfdcb741fe9e3f0323e2a7d5cd1ab2edc73221d57ad4834b2/yarl-1.20.1-cp311-cp311-win32.whl", hash = "sha256:597f40615b8d25812f14562699e287f0dcc035d25eb74da72cae043bb884d773", size = 81485 }, + { url = "https://files.pythonhosted.org/packages/f0/09/d9c7942f8f05c32ec72cd5c8e041c8b29b5807328b68b4801ff2511d4d5e/yarl-1.20.1-cp311-cp311-win_amd64.whl", hash = "sha256:26ef53a9e726e61e9cd1cda6b478f17e350fb5800b4bd1cd9fe81c4d91cfeb2e", size = 86686 }, + { url = "https://files.pythonhosted.org/packages/5f/9a/cb7fad7d73c69f296eda6815e4a2c7ed53fc70c2f136479a91c8e5fbdb6d/yarl-1.20.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdcc4cd244e58593a4379fe60fdee5ac0331f8eb70320a24d591a3be197b94a9", size = 133667 }, + { url = "https://files.pythonhosted.org/packages/67/38/688577a1cb1e656e3971fb66a3492501c5a5df56d99722e57c98249e5b8a/yarl-1.20.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b29a2c385a5f5b9c7d9347e5812b6f7ab267193c62d282a540b4fc528c8a9d2a", size = 91025 }, + { url = "https://files.pythonhosted.org/packages/50/ec/72991ae51febeb11a42813fc259f0d4c8e0507f2b74b5514618d8b640365/yarl-1.20.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1112ae8154186dfe2de4732197f59c05a83dc814849a5ced892b708033f40dc2", size = 89709 }, + { url = "https://files.pythonhosted.org/packages/99/da/4d798025490e89426e9f976702e5f9482005c548c579bdae792a4c37769e/yarl-1.20.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90bbd29c4fe234233f7fa2b9b121fb63c321830e5d05b45153a2ca68f7d310ee", size = 352287 }, + { url = "https://files.pythonhosted.org/packages/1a/26/54a15c6a567aac1c61b18aa0f4b8aa2e285a52d547d1be8bf48abe2b3991/yarl-1.20.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:680e19c7ce3710ac4cd964e90dad99bf9b5029372ba0c7cbfcd55e54d90ea819", size = 345429 }, + { url = "https://files.pythonhosted.org/packages/d6/95/9dcf2386cb875b234353b93ec43e40219e14900e046bf6ac118f94b1e353/yarl-1.20.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4a979218c1fdb4246a05efc2cc23859d47c89af463a90b99b7c56094daf25a16", size = 365429 }, + { url = "https://files.pythonhosted.org/packages/91/b2/33a8750f6a4bc224242a635f5f2cff6d6ad5ba651f6edcccf721992c21a0/yarl-1.20.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:255b468adf57b4a7b65d8aad5b5138dce6a0752c139965711bdcb81bc370e1b6", size = 363862 }, + { url = "https://files.pythonhosted.org/packages/98/28/3ab7acc5b51f4434b181b0cee8f1f4b77a65919700a355fb3617f9488874/yarl-1.20.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a97d67108e79cfe22e2b430d80d7571ae57d19f17cda8bb967057ca8a7bf5bfd", size = 355616 }, + { url = "https://files.pythonhosted.org/packages/36/a3/f666894aa947a371724ec7cd2e5daa78ee8a777b21509b4252dd7bd15e29/yarl-1.20.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8570d998db4ddbfb9a590b185a0a33dbf8aafb831d07a5257b4ec9948df9cb0a", size = 339954 }, + { url = "https://files.pythonhosted.org/packages/f1/81/5f466427e09773c04219d3450d7a1256138a010b6c9f0af2d48565e9ad13/yarl-1.20.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:97c75596019baae7c71ccf1d8cc4738bc08134060d0adfcbe5642f778d1dca38", size = 365575 }, + { url = "https://files.pythonhosted.org/packages/2e/e3/e4b0ad8403e97e6c9972dd587388940a032f030ebec196ab81a3b8e94d31/yarl-1.20.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:1c48912653e63aef91ff988c5432832692ac5a1d8f0fb8a33091520b5bbe19ef", size = 365061 }, + { url = "https://files.pythonhosted.org/packages/ac/99/b8a142e79eb86c926f9f06452eb13ecb1bb5713bd01dc0038faf5452e544/yarl-1.20.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4c3ae28f3ae1563c50f3d37f064ddb1511ecc1d5584e88c6b7c63cf7702a6d5f", size = 364142 }, + { url = "https://files.pythonhosted.org/packages/34/f2/08ed34a4a506d82a1a3e5bab99ccd930a040f9b6449e9fd050320e45845c/yarl-1.20.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c5e9642f27036283550f5f57dc6156c51084b458570b9d0d96100c8bebb186a8", size = 381894 }, + { url = "https://files.pythonhosted.org/packages/92/f8/9a3fbf0968eac704f681726eff595dce9b49c8a25cd92bf83df209668285/yarl-1.20.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:2c26b0c49220d5799f7b22c6838409ee9bc58ee5c95361a4d7831f03cc225b5a", size = 383378 }, + { url = "https://files.pythonhosted.org/packages/af/85/9363f77bdfa1e4d690957cd39d192c4cacd1c58965df0470a4905253b54f/yarl-1.20.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:564ab3d517e3d01c408c67f2e5247aad4019dcf1969982aba3974b4093279004", size = 374069 }, + { url = "https://files.pythonhosted.org/packages/35/99/9918c8739ba271dcd935400cff8b32e3cd319eaf02fcd023d5dcd487a7c8/yarl-1.20.1-cp312-cp312-win32.whl", hash = "sha256:daea0d313868da1cf2fac6b2d3a25c6e3a9e879483244be38c8e6a41f1d876a5", size = 81249 }, + { url = "https://files.pythonhosted.org/packages/eb/83/5d9092950565481b413b31a23e75dd3418ff0a277d6e0abf3729d4d1ce25/yarl-1.20.1-cp312-cp312-win_amd64.whl", hash = "sha256:48ea7d7f9be0487339828a4de0360d7ce0efc06524a48e1810f945c45b813698", size = 86710 }, + { url = "https://files.pythonhosted.org/packages/8a/e1/2411b6d7f769a07687acee88a062af5833cf1966b7266f3d8dfb3d3dc7d3/yarl-1.20.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:0b5ff0fbb7c9f1b1b5ab53330acbfc5247893069e7716840c8e7d5bb7355038a", size = 131811 }, + { url = "https://files.pythonhosted.org/packages/b2/27/584394e1cb76fb771371770eccad35de400e7b434ce3142c2dd27392c968/yarl-1.20.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:14f326acd845c2b2e2eb38fb1346c94f7f3b01a4f5c788f8144f9b630bfff9a3", size = 90078 }, + { url = "https://files.pythonhosted.org/packages/bf/9a/3246ae92d4049099f52d9b0fe3486e3b500e29b7ea872d0f152966fc209d/yarl-1.20.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f60e4ad5db23f0b96e49c018596707c3ae89f5d0bd97f0ad3684bcbad899f1e7", size = 88748 }, + { url = "https://files.pythonhosted.org/packages/a3/25/35afe384e31115a1a801fbcf84012d7a066d89035befae7c5d4284df1e03/yarl-1.20.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:49bdd1b8e00ce57e68ba51916e4bb04461746e794e7c4d4bbc42ba2f18297691", size = 349595 }, + { url = "https://files.pythonhosted.org/packages/28/2d/8aca6cb2cabc8f12efcb82749b9cefecbccfc7b0384e56cd71058ccee433/yarl-1.20.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:66252d780b45189975abfed839616e8fd2dbacbdc262105ad7742c6ae58f3e31", size = 342616 }, + { url = "https://files.pythonhosted.org/packages/0b/e9/1312633d16b31acf0098d30440ca855e3492d66623dafb8e25b03d00c3da/yarl-1.20.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:59174e7332f5d153d8f7452a102b103e2e74035ad085f404df2e40e663a22b28", size = 361324 }, + { url = "https://files.pythonhosted.org/packages/bc/a0/688cc99463f12f7669eec7c8acc71ef56a1521b99eab7cd3abb75af887b0/yarl-1.20.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e3968ec7d92a0c0f9ac34d5ecfd03869ec0cab0697c91a45db3fbbd95fe1b653", size = 359676 }, + { url = "https://files.pythonhosted.org/packages/af/44/46407d7f7a56e9a85a4c207724c9f2c545c060380718eea9088f222ba697/yarl-1.20.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d1a4fbb50e14396ba3d375f68bfe02215d8e7bc3ec49da8341fe3157f59d2ff5", size = 352614 }, + { url = "https://files.pythonhosted.org/packages/b1/91/31163295e82b8d5485d31d9cf7754d973d41915cadce070491778d9c9825/yarl-1.20.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:11a62c839c3a8eac2410e951301309426f368388ff2f33799052787035793b02", size = 336766 }, + { url = "https://files.pythonhosted.org/packages/b4/8e/c41a5bc482121f51c083c4c2bcd16b9e01e1cf8729e380273a952513a21f/yarl-1.20.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:041eaa14f73ff5a8986b4388ac6bb43a77f2ea09bf1913df7a35d4646db69e53", size = 364615 }, + { url = "https://files.pythonhosted.org/packages/e3/5b/61a3b054238d33d70ea06ebba7e58597891b71c699e247df35cc984ab393/yarl-1.20.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:377fae2fef158e8fd9d60b4c8751387b8d1fb121d3d0b8e9b0be07d1b41e83dc", size = 360982 }, + { url = "https://files.pythonhosted.org/packages/df/a3/6a72fb83f8d478cb201d14927bc8040af901811a88e0ff2da7842dd0ed19/yarl-1.20.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1c92f4390e407513f619d49319023664643d3339bd5e5a56a3bebe01bc67ec04", size = 369792 }, + { url = "https://files.pythonhosted.org/packages/7c/af/4cc3c36dfc7c077f8dedb561eb21f69e1e9f2456b91b593882b0b18c19dc/yarl-1.20.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d25ddcf954df1754ab0f86bb696af765c5bfaba39b74095f27eececa049ef9a4", size = 382049 }, + { url = "https://files.pythonhosted.org/packages/19/3a/e54e2c4752160115183a66dc9ee75a153f81f3ab2ba4bf79c3c53b33de34/yarl-1.20.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:909313577e9619dcff8c31a0ea2aa0a2a828341d92673015456b3ae492e7317b", size = 384774 }, + { url = "https://files.pythonhosted.org/packages/9c/20/200ae86dabfca89060ec6447649f219b4cbd94531e425e50d57e5f5ac330/yarl-1.20.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:793fd0580cb9664548c6b83c63b43c477212c0260891ddf86809e1c06c8b08f1", size = 374252 }, + { url = "https://files.pythonhosted.org/packages/83/75/11ee332f2f516b3d094e89448da73d557687f7d137d5a0f48c40ff211487/yarl-1.20.1-cp313-cp313-win32.whl", hash = "sha256:468f6e40285de5a5b3c44981ca3a319a4b208ccc07d526b20b12aeedcfa654b7", size = 81198 }, + { url = "https://files.pythonhosted.org/packages/ba/ba/39b1ecbf51620b40ab402b0fc817f0ff750f6d92712b44689c2c215be89d/yarl-1.20.1-cp313-cp313-win_amd64.whl", hash = "sha256:495b4ef2fea40596bfc0affe3837411d6aa3371abcf31aac0ccc4bdd64d4ef5c", size = 86346 }, + { url = "https://files.pythonhosted.org/packages/43/c7/669c52519dca4c95153c8ad96dd123c79f354a376346b198f438e56ffeb4/yarl-1.20.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:f60233b98423aab21d249a30eb27c389c14929f47be8430efa7dbd91493a729d", size = 138826 }, + { url = "https://files.pythonhosted.org/packages/6a/42/fc0053719b44f6ad04a75d7f05e0e9674d45ef62f2d9ad2c1163e5c05827/yarl-1.20.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6f3eff4cc3f03d650d8755c6eefc844edde99d641d0dcf4da3ab27141a5f8ddf", size = 93217 }, + { url = "https://files.pythonhosted.org/packages/4f/7f/fa59c4c27e2a076bba0d959386e26eba77eb52ea4a0aac48e3515c186b4c/yarl-1.20.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:69ff8439d8ba832d6bed88af2c2b3445977eba9a4588b787b32945871c2444e3", size = 92700 }, + { url = "https://files.pythonhosted.org/packages/2f/d4/062b2f48e7c93481e88eff97a6312dca15ea200e959f23e96d8ab898c5b8/yarl-1.20.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3cf34efa60eb81dd2645a2e13e00bb98b76c35ab5061a3989c7a70f78c85006d", size = 347644 }, + { url = "https://files.pythonhosted.org/packages/89/47/78b7f40d13c8f62b499cc702fdf69e090455518ae544c00a3bf4afc9fc77/yarl-1.20.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8e0fe9364ad0fddab2688ce72cb7a8e61ea42eff3c7caeeb83874a5d479c896c", size = 323452 }, + { url = "https://files.pythonhosted.org/packages/eb/2b/490d3b2dc66f52987d4ee0d3090a147ea67732ce6b4d61e362c1846d0d32/yarl-1.20.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8f64fbf81878ba914562c672024089e3401974a39767747691c65080a67b18c1", size = 346378 }, + { url = "https://files.pythonhosted.org/packages/66/ad/775da9c8a94ce925d1537f939a4f17d782efef1f973039d821cbe4bcc211/yarl-1.20.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f6342d643bf9a1de97e512e45e4b9560a043347e779a173250824f8b254bd5ce", size = 353261 }, + { url = "https://files.pythonhosted.org/packages/4b/23/0ed0922b47a4f5c6eb9065d5ff1e459747226ddce5c6a4c111e728c9f701/yarl-1.20.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56dac5f452ed25eef0f6e3c6a066c6ab68971d96a9fb441791cad0efba6140d3", size = 335987 }, + { url = "https://files.pythonhosted.org/packages/3e/49/bc728a7fe7d0e9336e2b78f0958a2d6b288ba89f25a1762407a222bf53c3/yarl-1.20.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7d7f497126d65e2cad8dc5f97d34c27b19199b6414a40cb36b52f41b79014be", size = 329361 }, + { url = "https://files.pythonhosted.org/packages/93/8f/b811b9d1f617c83c907e7082a76e2b92b655400e61730cd61a1f67178393/yarl-1.20.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:67e708dfb8e78d8a19169818eeb5c7a80717562de9051bf2413aca8e3696bf16", size = 346460 }, + { url = "https://files.pythonhosted.org/packages/70/fd/af94f04f275f95da2c3b8b5e1d49e3e79f1ed8b6ceb0f1664cbd902773ff/yarl-1.20.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:595c07bc79af2494365cc96ddeb772f76272364ef7c80fb892ef9d0649586513", size = 334486 }, + { url = "https://files.pythonhosted.org/packages/84/65/04c62e82704e7dd0a9b3f61dbaa8447f8507655fd16c51da0637b39b2910/yarl-1.20.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7bdd2f80f4a7df852ab9ab49484a4dee8030023aa536df41f2d922fd57bf023f", size = 342219 }, + { url = "https://files.pythonhosted.org/packages/91/95/459ca62eb958381b342d94ab9a4b6aec1ddec1f7057c487e926f03c06d30/yarl-1.20.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c03bfebc4ae8d862f853a9757199677ab74ec25424d0ebd68a0027e9c639a390", size = 350693 }, + { url = "https://files.pythonhosted.org/packages/a6/00/d393e82dd955ad20617abc546a8f1aee40534d599ff555ea053d0ec9bf03/yarl-1.20.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:344d1103e9c1523f32a5ed704d576172d2cabed3122ea90b1d4e11fe17c66458", size = 355803 }, + { url = "https://files.pythonhosted.org/packages/9e/ed/c5fb04869b99b717985e244fd93029c7a8e8febdfcffa06093e32d7d44e7/yarl-1.20.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:88cab98aa4e13e1ade8c141daeedd300a4603b7132819c484841bb7af3edce9e", size = 341709 }, + { url = "https://files.pythonhosted.org/packages/24/fd/725b8e73ac2a50e78a4534ac43c6addf5c1c2d65380dd48a9169cc6739a9/yarl-1.20.1-cp313-cp313t-win32.whl", hash = "sha256:b121ff6a7cbd4abc28985b6028235491941b9fe8fe226e6fdc539c977ea1739d", size = 86591 }, + { url = "https://files.pythonhosted.org/packages/94/c3/b2e9f38bc3e11191981d57ea08cab2166e74ea770024a646617c9cddd9f6/yarl-1.20.1-cp313-cp313t-win_amd64.whl", hash = "sha256:541d050a355bbbc27e55d906bc91cb6fe42f96c01413dd0f4ed5a5240513874f", size = 93003 }, + { url = "https://files.pythonhosted.org/packages/b4/2d/2345fce04cfd4bee161bf1e7d9cdc702e3e16109021035dbb24db654a622/yarl-1.20.1-py3-none-any.whl", hash = "sha256:83b8eb083fe4683c6115795d9fc1cfaf2cbbefb19b3a1cb68f6527460f483a77", size = 46542 }, +] + +[[package]] +name = "zipp" +version = "3.23.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276 }, +] + +[[package]] +name = "zstandard" +version = "0.23.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation == 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ed/f6/2ac0287b442160a89d726b17a9184a4c615bb5237db763791a7fd16d9df1/zstandard-0.23.0.tar.gz", hash = "sha256:b2d8c62d08e7255f68f7a740bae85b3c9b8e5466baa9cbf7f57f1cde0ac6bc09", size = 681701 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/55/bd0487e86679db1823fc9ee0d8c9c78ae2413d34c0b461193b5f4c31d22f/zstandard-0.23.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bf0a05b6059c0528477fba9054d09179beb63744355cab9f38059548fedd46a9", size = 788701 }, + { url = "https://files.pythonhosted.org/packages/e1/8a/ccb516b684f3ad987dfee27570d635822e3038645b1a950c5e8022df1145/zstandard-0.23.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fc9ca1c9718cb3b06634c7c8dec57d24e9438b2aa9a0f02b8bb36bf478538880", size = 633678 }, + { url = "https://files.pythonhosted.org/packages/12/89/75e633d0611c028e0d9af6df199423bf43f54bea5007e6718ab7132e234c/zstandard-0.23.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77da4c6bfa20dd5ea25cbf12c76f181a8e8cd7ea231c673828d0386b1740b8dc", size = 4941098 }, + { url = "https://files.pythonhosted.org/packages/4a/7a/bd7f6a21802de358b63f1ee636ab823711c25ce043a3e9f043b4fcb5ba32/zstandard-0.23.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b2170c7e0367dde86a2647ed5b6f57394ea7f53545746104c6b09fc1f4223573", size = 5308798 }, + { url = "https://files.pythonhosted.org/packages/79/3b/775f851a4a65013e88ca559c8ae42ac1352db6fcd96b028d0df4d7d1d7b4/zstandard-0.23.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c16842b846a8d2a145223f520b7e18b57c8f476924bda92aeee3a88d11cfc391", size = 5341840 }, + { url = "https://files.pythonhosted.org/packages/09/4f/0cc49570141dd72d4d95dd6fcf09328d1b702c47a6ec12fbed3b8aed18a5/zstandard-0.23.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:157e89ceb4054029a289fb504c98c6a9fe8010f1680de0201b3eb5dc20aa6d9e", size = 5440337 }, + { url = "https://files.pythonhosted.org/packages/e7/7c/aaa7cd27148bae2dc095191529c0570d16058c54c4597a7d118de4b21676/zstandard-0.23.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:203d236f4c94cd8379d1ea61db2fce20730b4c38d7f1c34506a31b34edc87bdd", size = 4861182 }, + { url = "https://files.pythonhosted.org/packages/ac/eb/4b58b5c071d177f7dc027129d20bd2a44161faca6592a67f8fcb0b88b3ae/zstandard-0.23.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:dc5d1a49d3f8262be192589a4b72f0d03b72dcf46c51ad5852a4fdc67be7b9e4", size = 4932936 }, + { url = "https://files.pythonhosted.org/packages/44/f9/21a5fb9bb7c9a274b05ad700a82ad22ce82f7ef0f485980a1e98ed6e8c5f/zstandard-0.23.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:752bf8a74412b9892f4e5b58f2f890a039f57037f52c89a740757ebd807f33ea", size = 5464705 }, + { url = "https://files.pythonhosted.org/packages/49/74/b7b3e61db3f88632776b78b1db597af3f44c91ce17d533e14a25ce6a2816/zstandard-0.23.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:80080816b4f52a9d886e67f1f96912891074903238fe54f2de8b786f86baded2", size = 4857882 }, + { url = "https://files.pythonhosted.org/packages/4a/7f/d8eb1cb123d8e4c541d4465167080bec88481ab54cd0b31eb4013ba04b95/zstandard-0.23.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:84433dddea68571a6d6bd4fbf8ff398236031149116a7fff6f777ff95cad3df9", size = 4697672 }, + { url = "https://files.pythonhosted.org/packages/5e/05/f7dccdf3d121309b60342da454d3e706453a31073e2c4dac8e1581861e44/zstandard-0.23.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:ab19a2d91963ed9e42b4e8d77cd847ae8381576585bad79dbd0a8837a9f6620a", size = 5206043 }, + { url = "https://files.pythonhosted.org/packages/86/9d/3677a02e172dccd8dd3a941307621c0cbd7691d77cb435ac3c75ab6a3105/zstandard-0.23.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:59556bf80a7094d0cfb9f5e50bb2db27fefb75d5138bb16fb052b61b0e0eeeb0", size = 5667390 }, + { url = "https://files.pythonhosted.org/packages/41/7e/0012a02458e74a7ba122cd9cafe491facc602c9a17f590367da369929498/zstandard-0.23.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:27d3ef2252d2e62476389ca8f9b0cf2bbafb082a3b6bfe9d90cbcbb5529ecf7c", size = 5198901 }, + { url = "https://files.pythonhosted.org/packages/65/3a/8f715b97bd7bcfc7342d8adcd99a026cb2fb550e44866a3b6c348e1b0f02/zstandard-0.23.0-cp310-cp310-win32.whl", hash = "sha256:5d41d5e025f1e0bccae4928981e71b2334c60f580bdc8345f824e7c0a4c2a813", size = 430596 }, + { url = "https://files.pythonhosted.org/packages/19/b7/b2b9eca5e5a01111e4fe8a8ffb56bdcdf56b12448a24effe6cfe4a252034/zstandard-0.23.0-cp310-cp310-win_amd64.whl", hash = "sha256:519fbf169dfac1222a76ba8861ef4ac7f0530c35dd79ba5727014613f91613d4", size = 495498 }, + { url = "https://files.pythonhosted.org/packages/9e/40/f67e7d2c25a0e2dc1744dd781110b0b60306657f8696cafb7ad7579469bd/zstandard-0.23.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:34895a41273ad33347b2fc70e1bff4240556de3c46c6ea430a7ed91f9042aa4e", size = 788699 }, + { url = "https://files.pythonhosted.org/packages/e8/46/66d5b55f4d737dd6ab75851b224abf0afe5774976fe511a54d2eb9063a41/zstandard-0.23.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:77ea385f7dd5b5676d7fd943292ffa18fbf5c72ba98f7d09fc1fb9e819b34c23", size = 633681 }, + { url = "https://files.pythonhosted.org/packages/63/b6/677e65c095d8e12b66b8f862b069bcf1f1d781b9c9c6f12eb55000d57583/zstandard-0.23.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:983b6efd649723474f29ed42e1467f90a35a74793437d0bc64a5bf482bedfa0a", size = 4944328 }, + { url = "https://files.pythonhosted.org/packages/59/cc/e76acb4c42afa05a9d20827116d1f9287e9c32b7ad58cc3af0721ce2b481/zstandard-0.23.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80a539906390591dd39ebb8d773771dc4db82ace6372c4d41e2d293f8e32b8db", size = 5311955 }, + { url = "https://files.pythonhosted.org/packages/78/e4/644b8075f18fc7f632130c32e8f36f6dc1b93065bf2dd87f03223b187f26/zstandard-0.23.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:445e4cb5048b04e90ce96a79b4b63140e3f4ab5f662321975679b5f6360b90e2", size = 5344944 }, + { url = "https://files.pythonhosted.org/packages/76/3f/dbafccf19cfeca25bbabf6f2dd81796b7218f768ec400f043edc767015a6/zstandard-0.23.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd30d9c67d13d891f2360b2a120186729c111238ac63b43dbd37a5a40670b8ca", size = 5442927 }, + { url = "https://files.pythonhosted.org/packages/0c/c3/d24a01a19b6733b9f218e94d1a87c477d523237e07f94899e1c10f6fd06c/zstandard-0.23.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d20fd853fbb5807c8e84c136c278827b6167ded66c72ec6f9a14b863d809211c", size = 4864910 }, + { url = "https://files.pythonhosted.org/packages/1c/a9/cf8f78ead4597264f7618d0875be01f9bc23c9d1d11afb6d225b867cb423/zstandard-0.23.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ed1708dbf4d2e3a1c5c69110ba2b4eb6678262028afd6c6fbcc5a8dac9cda68e", size = 4935544 }, + { url = "https://files.pythonhosted.org/packages/2c/96/8af1e3731b67965fb995a940c04a2c20997a7b3b14826b9d1301cf160879/zstandard-0.23.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:be9b5b8659dff1f913039c2feee1aca499cfbc19e98fa12bc85e037c17ec6ca5", size = 5467094 }, + { url = "https://files.pythonhosted.org/packages/ff/57/43ea9df642c636cb79f88a13ab07d92d88d3bfe3e550b55a25a07a26d878/zstandard-0.23.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:65308f4b4890aa12d9b6ad9f2844b7ee42c7f7a4fd3390425b242ffc57498f48", size = 4860440 }, + { url = "https://files.pythonhosted.org/packages/46/37/edb78f33c7f44f806525f27baa300341918fd4c4af9472fbc2c3094be2e8/zstandard-0.23.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:98da17ce9cbf3bfe4617e836d561e433f871129e3a7ac16d6ef4c680f13a839c", size = 4700091 }, + { url = "https://files.pythonhosted.org/packages/c1/f1/454ac3962671a754f3cb49242472df5c2cced4eb959ae203a377b45b1a3c/zstandard-0.23.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:8ed7d27cb56b3e058d3cf684d7200703bcae623e1dcc06ed1e18ecda39fee003", size = 5208682 }, + { url = "https://files.pythonhosted.org/packages/85/b2/1734b0fff1634390b1b887202d557d2dd542de84a4c155c258cf75da4773/zstandard-0.23.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:b69bb4f51daf461b15e7b3db033160937d3ff88303a7bc808c67bbc1eaf98c78", size = 5669707 }, + { url = "https://files.pythonhosted.org/packages/52/5a/87d6971f0997c4b9b09c495bf92189fb63de86a83cadc4977dc19735f652/zstandard-0.23.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:034b88913ecc1b097f528e42b539453fa82c3557e414b3de9d5632c80439a473", size = 5201792 }, + { url = "https://files.pythonhosted.org/packages/79/02/6f6a42cc84459d399bd1a4e1adfc78d4dfe45e56d05b072008d10040e13b/zstandard-0.23.0-cp311-cp311-win32.whl", hash = "sha256:f2d4380bf5f62daabd7b751ea2339c1a21d1c9463f1feb7fc2bdcea2c29c3160", size = 430586 }, + { url = "https://files.pythonhosted.org/packages/be/a2/4272175d47c623ff78196f3c10e9dc7045c1b9caf3735bf041e65271eca4/zstandard-0.23.0-cp311-cp311-win_amd64.whl", hash = "sha256:62136da96a973bd2557f06ddd4e8e807f9e13cbb0bfb9cc06cfe6d98ea90dfe0", size = 495420 }, + { url = "https://files.pythonhosted.org/packages/7b/83/f23338c963bd9de687d47bf32efe9fd30164e722ba27fb59df33e6b1719b/zstandard-0.23.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b4567955a6bc1b20e9c31612e615af6b53733491aeaa19a6b3b37f3b65477094", size = 788713 }, + { url = "https://files.pythonhosted.org/packages/5b/b3/1a028f6750fd9227ee0b937a278a434ab7f7fdc3066c3173f64366fe2466/zstandard-0.23.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e172f57cd78c20f13a3415cc8dfe24bf388614324d25539146594c16d78fcc8", size = 633459 }, + { url = "https://files.pythonhosted.org/packages/26/af/36d89aae0c1f95a0a98e50711bc5d92c144939efc1f81a2fcd3e78d7f4c1/zstandard-0.23.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b0e166f698c5a3e914947388c162be2583e0c638a4703fc6a543e23a88dea3c1", size = 4945707 }, + { url = "https://files.pythonhosted.org/packages/cd/2e/2051f5c772f4dfc0aae3741d5fc72c3dcfe3aaeb461cc231668a4db1ce14/zstandard-0.23.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12a289832e520c6bd4dcaad68e944b86da3bad0d339ef7989fb7e88f92e96072", size = 5306545 }, + { url = "https://files.pythonhosted.org/packages/0a/9e/a11c97b087f89cab030fa71206963090d2fecd8eb83e67bb8f3ffb84c024/zstandard-0.23.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d50d31bfedd53a928fed6707b15a8dbeef011bb6366297cc435accc888b27c20", size = 5337533 }, + { url = "https://files.pythonhosted.org/packages/fc/79/edeb217c57fe1bf16d890aa91a1c2c96b28c07b46afed54a5dcf310c3f6f/zstandard-0.23.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72c68dda124a1a138340fb62fa21b9bf4848437d9ca60bd35db36f2d3345f373", size = 5436510 }, + { url = "https://files.pythonhosted.org/packages/81/4f/c21383d97cb7a422ddf1ae824b53ce4b51063d0eeb2afa757eb40804a8ef/zstandard-0.23.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:53dd9d5e3d29f95acd5de6802e909ada8d8d8cfa37a3ac64836f3bc4bc5512db", size = 4859973 }, + { url = "https://files.pythonhosted.org/packages/ab/15/08d22e87753304405ccac8be2493a495f529edd81d39a0870621462276ef/zstandard-0.23.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:6a41c120c3dbc0d81a8e8adc73312d668cd34acd7725f036992b1b72d22c1772", size = 4936968 }, + { url = "https://files.pythonhosted.org/packages/eb/fa/f3670a597949fe7dcf38119a39f7da49a8a84a6f0b1a2e46b2f71a0ab83f/zstandard-0.23.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:40b33d93c6eddf02d2c19f5773196068d875c41ca25730e8288e9b672897c105", size = 5467179 }, + { url = "https://files.pythonhosted.org/packages/4e/a9/dad2ab22020211e380adc477a1dbf9f109b1f8d94c614944843e20dc2a99/zstandard-0.23.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9206649ec587e6b02bd124fb7799b86cddec350f6f6c14bc82a2b70183e708ba", size = 4848577 }, + { url = "https://files.pythonhosted.org/packages/08/03/dd28b4484b0770f1e23478413e01bee476ae8227bbc81561f9c329e12564/zstandard-0.23.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:76e79bc28a65f467e0409098fa2c4376931fd3207fbeb6b956c7c476d53746dd", size = 4693899 }, + { url = "https://files.pythonhosted.org/packages/2b/64/3da7497eb635d025841e958bcd66a86117ae320c3b14b0ae86e9e8627518/zstandard-0.23.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:66b689c107857eceabf2cf3d3fc699c3c0fe8ccd18df2219d978c0283e4c508a", size = 5199964 }, + { url = "https://files.pythonhosted.org/packages/43/a4/d82decbab158a0e8a6ebb7fc98bc4d903266bce85b6e9aaedea1d288338c/zstandard-0.23.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9c236e635582742fee16603042553d276cca506e824fa2e6489db04039521e90", size = 5655398 }, + { url = "https://files.pythonhosted.org/packages/f2/61/ac78a1263bc83a5cf29e7458b77a568eda5a8f81980691bbc6eb6a0d45cc/zstandard-0.23.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a8fffdbd9d1408006baaf02f1068d7dd1f016c6bcb7538682622c556e7b68e35", size = 5191313 }, + { url = "https://files.pythonhosted.org/packages/e7/54/967c478314e16af5baf849b6ee9d6ea724ae5b100eb506011f045d3d4e16/zstandard-0.23.0-cp312-cp312-win32.whl", hash = "sha256:dc1d33abb8a0d754ea4763bad944fd965d3d95b5baef6b121c0c9013eaf1907d", size = 430877 }, + { url = "https://files.pythonhosted.org/packages/75/37/872d74bd7739639c4553bf94c84af7d54d8211b626b352bc57f0fd8d1e3f/zstandard-0.23.0-cp312-cp312-win_amd64.whl", hash = "sha256:64585e1dba664dc67c7cdabd56c1e5685233fbb1fc1966cfba2a340ec0dfff7b", size = 495595 }, + { url = "https://files.pythonhosted.org/packages/80/f1/8386f3f7c10261fe85fbc2c012fdb3d4db793b921c9abcc995d8da1b7a80/zstandard-0.23.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:576856e8594e6649aee06ddbfc738fec6a834f7c85bf7cadd1c53d4a58186ef9", size = 788975 }, + { url = "https://files.pythonhosted.org/packages/16/e8/cbf01077550b3e5dc86089035ff8f6fbbb312bc0983757c2d1117ebba242/zstandard-0.23.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:38302b78a850ff82656beaddeb0bb989a0322a8bbb1bf1ab10c17506681d772a", size = 633448 }, + { url = "https://files.pythonhosted.org/packages/06/27/4a1b4c267c29a464a161aeb2589aff212b4db653a1d96bffe3598f3f0d22/zstandard-0.23.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d2240ddc86b74966c34554c49d00eaafa8200a18d3a5b6ffbf7da63b11d74ee2", size = 4945269 }, + { url = "https://files.pythonhosted.org/packages/7c/64/d99261cc57afd9ae65b707e38045ed8269fbdae73544fd2e4a4d50d0ed83/zstandard-0.23.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2ef230a8fd217a2015bc91b74f6b3b7d6522ba48be29ad4ea0ca3a3775bf7dd5", size = 5306228 }, + { url = "https://files.pythonhosted.org/packages/7a/cf/27b74c6f22541f0263016a0fd6369b1b7818941de639215c84e4e94b2a1c/zstandard-0.23.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:774d45b1fac1461f48698a9d4b5fa19a69d47ece02fa469825b442263f04021f", size = 5336891 }, + { url = "https://files.pythonhosted.org/packages/fa/18/89ac62eac46b69948bf35fcd90d37103f38722968e2981f752d69081ec4d/zstandard-0.23.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f77fa49079891a4aab203d0b1744acc85577ed16d767b52fc089d83faf8d8ed", size = 5436310 }, + { url = "https://files.pythonhosted.org/packages/a8/a8/5ca5328ee568a873f5118d5b5f70d1f36c6387716efe2e369010289a5738/zstandard-0.23.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ac184f87ff521f4840e6ea0b10c0ec90c6b1dcd0bad2f1e4a9a1b4fa177982ea", size = 4859912 }, + { url = "https://files.pythonhosted.org/packages/ea/ca/3781059c95fd0868658b1cf0440edd832b942f84ae60685d0cfdb808bca1/zstandard-0.23.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c363b53e257246a954ebc7c488304b5592b9c53fbe74d03bc1c64dda153fb847", size = 4936946 }, + { url = "https://files.pythonhosted.org/packages/ce/11/41a58986f809532742c2b832c53b74ba0e0a5dae7e8ab4642bf5876f35de/zstandard-0.23.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:e7792606d606c8df5277c32ccb58f29b9b8603bf83b48639b7aedf6df4fe8171", size = 5466994 }, + { url = "https://files.pythonhosted.org/packages/83/e3/97d84fe95edd38d7053af05159465d298c8b20cebe9ccb3d26783faa9094/zstandard-0.23.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a0817825b900fcd43ac5d05b8b3079937073d2b1ff9cf89427590718b70dd840", size = 4848681 }, + { url = "https://files.pythonhosted.org/packages/6e/99/cb1e63e931de15c88af26085e3f2d9af9ce53ccafac73b6e48418fd5a6e6/zstandard-0.23.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:9da6bc32faac9a293ddfdcb9108d4b20416219461e4ec64dfea8383cac186690", size = 4694239 }, + { url = "https://files.pythonhosted.org/packages/ab/50/b1e703016eebbc6501fc92f34db7b1c68e54e567ef39e6e59cf5fb6f2ec0/zstandard-0.23.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:fd7699e8fd9969f455ef2926221e0233f81a2542921471382e77a9e2f2b57f4b", size = 5200149 }, + { url = "https://files.pythonhosted.org/packages/aa/e0/932388630aaba70197c78bdb10cce2c91fae01a7e553b76ce85471aec690/zstandard-0.23.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:d477ed829077cd945b01fc3115edd132c47e6540ddcd96ca169facff28173057", size = 5655392 }, + { url = "https://files.pythonhosted.org/packages/02/90/2633473864f67a15526324b007a9f96c96f56d5f32ef2a56cc12f9548723/zstandard-0.23.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa6ce8b52c5987b3e34d5674b0ab529a4602b632ebab0a93b07bfb4dfc8f8a33", size = 5191299 }, + { url = "https://files.pythonhosted.org/packages/b0/4c/315ca5c32da7e2dc3455f3b2caee5c8c2246074a61aac6ec3378a97b7136/zstandard-0.23.0-cp313-cp313-win32.whl", hash = "sha256:a9b07268d0c3ca5c170a385a0ab9fb7fdd9f5fd866be004c4ea39e44edce47dd", size = 430862 }, + { url = "https://files.pythonhosted.org/packages/a2/bf/c6aaba098e2d04781e8f4f7c0ba3c7aa73d00e4c436bcc0cf059a66691d1/zstandard-0.23.0-cp313-cp313-win_amd64.whl", hash = "sha256:f3513916e8c645d0610815c257cbfd3242adfd5c4cfa78be514e5a3ebb42a41b", size = 495578 }, +]