diff --git a/tests/__pycache__/test_deploy_production_contract.cpython-312-pytest-9.0.2.pyc b/tests/__pycache__/test_deploy_production_contract.cpython-312-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..036b97db2f5a481e03dd2290f6fd6b0450dc1f33 Binary files /dev/null and b/tests/__pycache__/test_deploy_production_contract.cpython-312-pytest-9.0.2.pyc differ diff --git a/tests/__pycache__/test_deploy_production_contract.cpython-312-pytest-9.1.1.pyc b/tests/__pycache__/test_deploy_production_contract.cpython-312-pytest-9.1.1.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f7e65bc6328348e893ac305c5e86c91df92cfd2e Binary files /dev/null and b/tests/__pycache__/test_deploy_production_contract.cpython-312-pytest-9.1.1.pyc differ diff --git a/tests/__pycache__/test_entity_synergies.cpython-312-pytest-9.0.2.pyc b/tests/__pycache__/test_entity_synergies.cpython-312-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b17a4f2204a5ed16b750834f1f6632de30c45339 Binary files /dev/null and b/tests/__pycache__/test_entity_synergies.cpython-312-pytest-9.0.2.pyc differ diff --git a/tests/__pycache__/test_entity_synergy_recency.cpython-312-pytest-9.0.2.pyc b/tests/__pycache__/test_entity_synergy_recency.cpython-312-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..49087e1c20c3474068ab5afe7dafed3884ffdd90 Binary files /dev/null and b/tests/__pycache__/test_entity_synergy_recency.cpython-312-pytest-9.0.2.pyc differ diff --git a/tests/__pycache__/test_event_hub_invalidation.cpython-312-pytest-9.0.2.pyc b/tests/__pycache__/test_event_hub_invalidation.cpython-312-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e3721e758411764927ba3d20e60a54e68b8e56e9 Binary files /dev/null and b/tests/__pycache__/test_event_hub_invalidation.cpython-312-pytest-9.0.2.pyc differ diff --git a/tests/__pycache__/test_graph_upsert_contract.cpython-312-pytest-9.0.2.pyc b/tests/__pycache__/test_graph_upsert_contract.cpython-312-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..475c63b27556b86d2b7e8de8a00fce8916337ac6 Binary files /dev/null and b/tests/__pycache__/test_graph_upsert_contract.cpython-312-pytest-9.0.2.pyc differ diff --git a/tests/__pycache__/test_graphrag_upgrade_helpers.cpython-312-pytest-9.0.2.pyc b/tests/__pycache__/test_graphrag_upgrade_helpers.cpython-312-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0a314912af59960d1ed9bd216f336e24fb5f31ae Binary files /dev/null and b/tests/__pycache__/test_graphrag_upgrade_helpers.cpython-312-pytest-9.0.2.pyc differ diff --git a/tests/__pycache__/test_grounding.cpython-312-pytest-9.0.2.pyc b/tests/__pycache__/test_grounding.cpython-312-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a5a351350169204081d58c26ed5405f1986e53dc Binary files /dev/null and b/tests/__pycache__/test_grounding.cpython-312-pytest-9.0.2.pyc differ diff --git a/tests/__pycache__/test_halfvec_ann.cpython-312-pytest-9.0.2.pyc b/tests/__pycache__/test_halfvec_ann.cpython-312-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bdfaa3bf42679b1191afddfc45cf5b33eeccb86b Binary files /dev/null and b/tests/__pycache__/test_halfvec_ann.cpython-312-pytest-9.0.2.pyc differ diff --git a/tests/__pycache__/test_ingest_cost.cpython-312-pytest-9.0.2.pyc b/tests/__pycache__/test_ingest_cost.cpython-312-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..fe82edd87ca9fe3bd10fb153757b6755c3749f86 Binary files /dev/null and b/tests/__pycache__/test_ingest_cost.cpython-312-pytest-9.0.2.pyc differ diff --git a/tests/__pycache__/test_ingestion_identity.cpython-312-pytest-9.0.2.pyc b/tests/__pycache__/test_ingestion_identity.cpython-312-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b5cbe5d236a7f7723b31ab50bb907b0d245a87c7 Binary files /dev/null and b/tests/__pycache__/test_ingestion_identity.cpython-312-pytest-9.0.2.pyc differ diff --git a/tests/__pycache__/test_ingestion_orchestration_collection.cpython-312-pytest-9.0.2.pyc b/tests/__pycache__/test_ingestion_orchestration_collection.cpython-312-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..62cb088a9558bc133149a6cad24d95e34deebeaa Binary files /dev/null and b/tests/__pycache__/test_ingestion_orchestration_collection.cpython-312-pytest-9.0.2.pyc differ diff --git a/tests/__pycache__/test_ingestion_standard.cpython-312-pytest-9.0.2.pyc b/tests/__pycache__/test_ingestion_standard.cpython-312-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6027e447bb6cebb3cc056edf3d86d4ca7e61772c Binary files /dev/null and b/tests/__pycache__/test_ingestion_standard.cpython-312-pytest-9.0.2.pyc differ diff --git a/tests/__pycache__/test_ingestion_structured_contract.cpython-312-pytest-9.0.2.pyc b/tests/__pycache__/test_ingestion_structured_contract.cpython-312-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..19b10be1106653f78c2ea037e15cecf488215b5c Binary files /dev/null and b/tests/__pycache__/test_ingestion_structured_contract.cpython-312-pytest-9.0.2.pyc differ diff --git a/tests/__pycache__/test_ingestion_task_lifecycle.cpython-312-pytest-9.0.2.pyc b/tests/__pycache__/test_ingestion_task_lifecycle.cpython-312-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e582a03d2524ac71cd8e036967090256b8babbd2 Binary files /dev/null and b/tests/__pycache__/test_ingestion_task_lifecycle.cpython-312-pytest-9.0.2.pyc differ diff --git a/tests/__pycache__/test_janitor_outcomes.cpython-312-pytest-9.0.2.pyc b/tests/__pycache__/test_janitor_outcomes.cpython-312-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e4179832099afc0af13eea85ba3b17e9ce141b4d Binary files /dev/null and b/tests/__pycache__/test_janitor_outcomes.cpython-312-pytest-9.0.2.pyc differ diff --git a/tests/__pycache__/test_kombu_redis_patch.cpython-312-pytest-9.0.2.pyc b/tests/__pycache__/test_kombu_redis_patch.cpython-312-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..065d1730ae24a81a51bd13d3670221d20b042256 Binary files /dev/null and b/tests/__pycache__/test_kombu_redis_patch.cpython-312-pytest-9.0.2.pyc differ diff --git a/tests/__pycache__/test_mcp_oauth_redis_storage.cpython-312-pytest-9.0.2.pyc b/tests/__pycache__/test_mcp_oauth_redis_storage.cpython-312-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bdb9eaead3b66c13d0ae862a0d1ed594a981f153 Binary files /dev/null and b/tests/__pycache__/test_mcp_oauth_redis_storage.cpython-312-pytest-9.0.2.pyc differ diff --git a/tests/__pycache__/test_mcp_traverse_graph.cpython-312-pytest-9.0.2.pyc b/tests/__pycache__/test_mcp_traverse_graph.cpython-312-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c1fc1e03a32a6b2f8d73749b1b5de88bef931f63 Binary files /dev/null and b/tests/__pycache__/test_mcp_traverse_graph.cpython-312-pytest-9.0.2.pyc differ diff --git a/tests/__pycache__/test_networkx_event_centric.cpython-312-pytest-9.0.2.pyc b/tests/__pycache__/test_networkx_event_centric.cpython-312-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..04569b707f4c7ccbeb2d648e6296d61eeab40118 Binary files /dev/null and b/tests/__pycache__/test_networkx_event_centric.cpython-312-pytest-9.0.2.pyc differ diff --git a/tests/__pycache__/test_phase_b_event_graphrag.cpython-312-pytest-9.0.2.pyc b/tests/__pycache__/test_phase_b_event_graphrag.cpython-312-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d864a4926b098cce58af777518b3c0ca44abac1d Binary files /dev/null and b/tests/__pycache__/test_phase_b_event_graphrag.cpython-312-pytest-9.0.2.pyc differ diff --git a/tests/__pycache__/test_phase_c_hub_bridges.cpython-312-pytest-9.0.2.pyc b/tests/__pycache__/test_phase_c_hub_bridges.cpython-312-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..dd7890fd0b97cb4459cded3059ed30eb5df9020a Binary files /dev/null and b/tests/__pycache__/test_phase_c_hub_bridges.cpython-312-pytest-9.0.2.pyc differ diff --git a/tests/__pycache__/test_plugin_refactors.cpython-312-pytest-9.0.2.pyc b/tests/__pycache__/test_plugin_refactors.cpython-312-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..54563742d7d65d0e8054b1bff15a1fc17d64c1ef Binary files /dev/null and b/tests/__pycache__/test_plugin_refactors.cpython-312-pytest-9.0.2.pyc differ diff --git a/tests/__pycache__/test_postgresql_graph_read_query.cpython-312-pytest-9.0.2.pyc b/tests/__pycache__/test_postgresql_graph_read_query.cpython-312-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..86be9d5882f01e4104aab793e83191d034e3e07e Binary files /dev/null and b/tests/__pycache__/test_postgresql_graph_read_query.cpython-312-pytest-9.0.2.pyc differ diff --git a/tests/__pycache__/test_production_hardening.cpython-312-pytest-9.0.2.pyc b/tests/__pycache__/test_production_hardening.cpython-312-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2428dd2aa7a56e9a6c9552ce9bec7d72cc1d7b53 Binary files /dev/null and b/tests/__pycache__/test_production_hardening.cpython-312-pytest-9.0.2.pyc differ diff --git a/tests/__pycache__/test_quantity_node_identity.cpython-312-pytest-9.0.2.pyc b/tests/__pycache__/test_quantity_node_identity.cpython-312-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c90bca67f9932b0f203d31e783c61c0ed8c2b148 Binary files /dev/null and b/tests/__pycache__/test_quantity_node_identity.cpython-312-pytest-9.0.2.pyc differ diff --git a/tests/__pycache__/test_recommend_recsys.cpython-312-pytest-9.0.2.pyc b/tests/__pycache__/test_recommend_recsys.cpython-312-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9f6611a7a654b5adb88c165e43b7f2d2be9899d0 Binary files /dev/null and b/tests/__pycache__/test_recommend_recsys.cpython-312-pytest-9.0.2.pyc differ diff --git a/tests/__pycache__/test_recsys_export_edges.cpython-312-pytest-9.0.2.pyc b/tests/__pycache__/test_recsys_export_edges.cpython-312-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6224deee236eac12166cc1aee5a8615f54325fbb Binary files /dev/null and b/tests/__pycache__/test_recsys_export_edges.cpython-312-pytest-9.0.2.pyc differ diff --git a/tests/__pycache__/test_recsys_lightgcn.cpython-312-pytest-9.0.2.pyc b/tests/__pycache__/test_recsys_lightgcn.cpython-312-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f2871b7415952ebb6822f9b6964d64ecb6032b00 Binary files /dev/null and b/tests/__pycache__/test_recsys_lightgcn.cpython-312-pytest-9.0.2.pyc differ diff --git a/tests/__pycache__/test_recsys_movielens.cpython-312-pytest-9.0.2.pyc b/tests/__pycache__/test_recsys_movielens.cpython-312-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..16c2f3765a58aeebd126c472ae1ee858f77e30ac Binary files /dev/null and b/tests/__pycache__/test_recsys_movielens.cpython-312-pytest-9.0.2.pyc differ diff --git a/tests/__pycache__/test_release_version.cpython-312-pytest-9.0.2.pyc b/tests/__pycache__/test_release_version.cpython-312-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..96395c372e2e57be4f8c1343649363a693a92735 Binary files /dev/null and b/tests/__pycache__/test_release_version.cpython-312-pytest-9.0.2.pyc differ diff --git a/tests/__pycache__/test_search_api.cpython-312-pytest-9.0.2.pyc b/tests/__pycache__/test_search_api.cpython-312-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..aec48654674e4918aee200f86978daca4b9d4985 Binary files /dev/null and b/tests/__pycache__/test_search_api.cpython-312-pytest-9.0.2.pyc differ diff --git a/tests/__pycache__/test_search_bench.cpython-312-pytest-9.0.2.pyc b/tests/__pycache__/test_search_bench.cpython-312-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..367b7893dfac9534c4af17454183bb165f1043f5 Binary files /dev/null and b/tests/__pycache__/test_search_bench.cpython-312-pytest-9.0.2.pyc differ diff --git a/tests/__pycache__/test_search_bm25.cpython-312-pytest-9.0.2.pyc b/tests/__pycache__/test_search_bm25.cpython-312-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..70cee5dd6b7cf794471eacaf1eeb4ae213321f96 Binary files /dev/null and b/tests/__pycache__/test_search_bm25.cpython-312-pytest-9.0.2.pyc differ diff --git a/tests/__pycache__/test_search_catalog.cpython-312-pytest-9.0.2.pyc b/tests/__pycache__/test_search_catalog.cpython-312-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..51807d611b41d832a564688231b406e0545468cb Binary files /dev/null and b/tests/__pycache__/test_search_catalog.cpython-312-pytest-9.0.2.pyc differ diff --git a/tests/__pycache__/test_search_config.cpython-312-pytest-9.0.2.pyc b/tests/__pycache__/test_search_config.cpython-312-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b4c5f13ac03cf333575b6b8fc94d4d9194f39799 Binary files /dev/null and b/tests/__pycache__/test_search_config.cpython-312-pytest-9.0.2.pyc differ diff --git a/tests/__pycache__/test_search_esci_4class.cpython-312-pytest-9.0.2.pyc b/tests/__pycache__/test_search_esci_4class.cpython-312-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2fa6599814924ea67a9eea76ad5baf14904e6b8c Binary files /dev/null and b/tests/__pycache__/test_search_esci_4class.cpython-312-pytest-9.0.2.pyc differ diff --git a/tests/__pycache__/test_search_graph.cpython-312-pytest-9.0.2.pyc b/tests/__pycache__/test_search_graph.cpython-312-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..db03e7b5747ce6cc920adf1bdd64d3db5e09c1a8 Binary files /dev/null and b/tests/__pycache__/test_search_graph.cpython-312-pytest-9.0.2.pyc differ diff --git a/tests/__pycache__/test_search_jdsearch.cpython-312-pytest-9.0.2.pyc b/tests/__pycache__/test_search_jdsearch.cpython-312-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..836a2daae1709793d05e7f28d3f711d7b0063e02 Binary files /dev/null and b/tests/__pycache__/test_search_jdsearch.cpython-312-pytest-9.0.2.pyc differ diff --git a/tests/__pycache__/test_search_personalize.cpython-312-pytest-9.0.2.pyc b/tests/__pycache__/test_search_personalize.cpython-312-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b06ff318ecd6a94924baa23bfcc08b99e5d1ddba Binary files /dev/null and b/tests/__pycache__/test_search_personalize.cpython-312-pytest-9.0.2.pyc differ diff --git a/tests/__pycache__/test_search_plugins.cpython-312-pytest-9.0.2.pyc b/tests/__pycache__/test_search_plugins.cpython-312-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e862a13a6cf73713b290a2efe1d30fbac332d71c Binary files /dev/null and b/tests/__pycache__/test_search_plugins.cpython-312-pytest-9.0.2.pyc differ diff --git a/tests/__pycache__/test_spacy_fixes_verification.cpython-312-pytest-9.0.2.pyc b/tests/__pycache__/test_spacy_fixes_verification.cpython-312-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..985dc5ce5bf7b1df3eeae0858d5ed3ab84278c4b Binary files /dev/null and b/tests/__pycache__/test_spacy_fixes_verification.cpython-312-pytest-9.0.2.pyc differ diff --git a/tests/__pycache__/test_stage_latency_profiler.cpython-312-pytest-9.0.2.pyc b/tests/__pycache__/test_stage_latency_profiler.cpython-312-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..709c6fdda7a97c76598367f0e70223fd75f035df Binary files /dev/null and b/tests/__pycache__/test_stage_latency_profiler.cpython-312-pytest-9.0.2.pyc differ diff --git a/tests/__pycache__/test_static_hardening_refactors.cpython-312-pytest-9.0.2.pyc b/tests/__pycache__/test_static_hardening_refactors.cpython-312-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9f646db2ae7b130344c1ec36042954db2a082f2c Binary files /dev/null and b/tests/__pycache__/test_static_hardening_refactors.cpython-312-pytest-9.0.2.pyc differ diff --git a/tests/__pycache__/test_structured_triple_persistence.cpython-312-pytest-9.0.2.pyc b/tests/__pycache__/test_structured_triple_persistence.cpython-312-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5eaf069f269853a36ae726f8a81036ba21962e74 Binary files /dev/null and b/tests/__pycache__/test_structured_triple_persistence.cpython-312-pytest-9.0.2.pyc differ diff --git a/tests/__pycache__/test_synergy_metrics.cpython-312-pytest-9.0.2.pyc b/tests/__pycache__/test_synergy_metrics.cpython-312-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c267cba11be435fc4a2a2e3dbfff16c726b26ddc Binary files /dev/null and b/tests/__pycache__/test_synergy_metrics.cpython-312-pytest-9.0.2.pyc differ diff --git a/tests/__pycache__/test_topic_hyperedges.cpython-312-pytest-9.0.2.pyc b/tests/__pycache__/test_topic_hyperedges.cpython-312-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..17073925ea11ac745dc7551b91fc28ce818b3fa2 Binary files /dev/null and b/tests/__pycache__/test_topic_hyperedges.cpython-312-pytest-9.0.2.pyc differ diff --git a/tests/__pycache__/test_tracing_runtime_monitor.cpython-312-pytest-9.0.2.pyc b/tests/__pycache__/test_tracing_runtime_monitor.cpython-312-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c7dc29ffc1fb00111b239e0a36c7587532623347 Binary files /dev/null and b/tests/__pycache__/test_tracing_runtime_monitor.cpython-312-pytest-9.0.2.pyc differ diff --git a/tests/__pycache__/test_tracing_sla_tracker.cpython-312-pytest-9.0.2.pyc b/tests/__pycache__/test_tracing_sla_tracker.cpython-312-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8e39c9fc7fb92f0f6cf04a8f238a5ddd9922b854 Binary files /dev/null and b/tests/__pycache__/test_tracing_sla_tracker.cpython-312-pytest-9.0.2.pyc differ diff --git a/tests/__pycache__/test_tracing_workers_and_agents.cpython-312-pytest-9.0.2.pyc b/tests/__pycache__/test_tracing_workers_and_agents.cpython-312-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..50e30b9c1335c8621691850b42918a9b7b752319 Binary files /dev/null and b/tests/__pycache__/test_tracing_workers_and_agents.cpython-312-pytest-9.0.2.pyc differ diff --git a/tests/__pycache__/test_vector_similarity_contract.cpython-312-pytest-9.0.2.pyc b/tests/__pycache__/test_vector_similarity_contract.cpython-312-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6038d061d5b6d0ea3fdcaec8bd64a87f6113ad5d Binary files /dev/null and b/tests/__pycache__/test_vector_similarity_contract.cpython-312-pytest-9.0.2.pyc differ diff --git a/tests/__pycache__/test_workspace_facade.cpython-312-pytest-9.0.2.pyc b/tests/__pycache__/test_workspace_facade.cpython-312-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..202b02e29753f60d3fbe00e50e4af046eb76cff9 Binary files /dev/null and b/tests/__pycache__/test_workspace_facade.cpython-312-pytest-9.0.2.pyc differ diff --git a/tests/__pycache__/test_workspace_facade.cpython-312-pytest-9.1.1.pyc b/tests/__pycache__/test_workspace_facade.cpython-312-pytest-9.1.1.pyc new file mode 100644 index 0000000000000000000000000000000000000000..209c95737b6a1dd0dd267cf7fbb786e2138dfe19 Binary files /dev/null and b/tests/__pycache__/test_workspace_facade.cpython-312-pytest-9.1.1.pyc differ diff --git a/tests/__pycache__/test_workspace_mcp.cpython-312-pytest-9.0.2.pyc b/tests/__pycache__/test_workspace_mcp.cpython-312-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..212e23fa1ae7f7d4544b3a2a94cee089a8fc6159 Binary files /dev/null and b/tests/__pycache__/test_workspace_mcp.cpython-312-pytest-9.0.2.pyc differ diff --git a/tests/__pycache__/test_workspace_plugin_contract.cpython-312-pytest-9.0.2.pyc b/tests/__pycache__/test_workspace_plugin_contract.cpython-312-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..27d60c9680034cbe12c64ccb63a5ed159f9faaf6 Binary files /dev/null and b/tests/__pycache__/test_workspace_plugin_contract.cpython-312-pytest-9.0.2.pyc differ diff --git a/tests/__pycache__/test_workspaces.cpython-312-pytest-9.0.2.pyc b/tests/__pycache__/test_workspaces.cpython-312-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6ca7a93e4c2f9dbda701d4c9bdc4f8308a12f4e0 Binary files /dev/null and b/tests/__pycache__/test_workspaces.cpython-312-pytest-9.0.2.pyc differ diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000000000000000000000000000000000000..e1a3ac9f21c94b825f6b0a4c48bac916cf6bec63 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,73 @@ +"""Process-wide isolation for the BrainAPI test suite.""" + +from __future__ import annotations + +import os +import sys +import tempfile +from pathlib import Path + +import pytest + + +_REPOSITORY_ROOT = Path(__file__).resolve().parents[1] +if str(_REPOSITORY_ROOT) not in sys.path: + sys.path.insert(0, str(_REPOSITORY_ROOT)) + +# Installed development plugins must never change core test collection or imports. +# Plugin-specific tests pass their own temporary directories directly to loaders. +_PLUGIN_SANDBOX = tempfile.TemporaryDirectory(prefix="brainapi-test-plugins-") +os.environ["PLUGINS_DIR"] = _PLUGIN_SANDBOX.name + +# Configuration is instantiated during module import. Establish deterministic +# non-secret test values before collection so file ordering cannot change it. +_TEST_ENV_DEFAULTS = { + "BRAINPAT_TOKEN": "test-token", + "MODELS_MODE": "local", + "EMBEDDINGS_LOCAL_MODEL": "local-model", + "EMBEDDINGS_SMALL_MODEL": "small-model", + "EMBEDDING_NODES_DIMENSION": "3", + "EMBEDDING_TRIPLETS_DIMENSION": "3", + "EMBEDDING_OBSERVATIONS_DIMENSION": "3", + "EMBEDDING_DATA_DIMENSION": "3", + "EMBEDDING_RELATIONSHIPS_DIMENSION": "3", + "REDIS_HOST": "localhost", + "REDIS_PORT": "6379", + "NEO4J_HOST": "localhost", + "NEO4J_PORT": "7687", + "NEO4J_USERNAME": "neo4j", + "NEO4J_PASSWORD": "test-password", + "MILVUS_HOST": "localhost", + "MILVUS_PORT": "19530", + "MONGO_CONNECTION_STRING": "mongodb://localhost:27017", + "POSTGRES_HOST": "localhost", + "POSTGRES_PORT": "5432", + "POSTGRES_USERNAME": "postgres", + "POSTGRES_PASSWORD": "test-password", + "CELERY_WORKER_CONCURRENCY": "1", + "OLLAMA_HOST": "localhost", + "OLLAMA_PORT": "11434", + "OLLAMA_LLM_SMALL_MODEL": "small", + "OLLAMA_LLM_LARGE_MODEL": "large", +} +for _key, _value in _TEST_ENV_DEFAULTS.items(): + os.environ.setdefault(_key, _value) + + +@pytest.fixture(autouse=True) +def _restore_retrieval_globals_after_test(): + """Undo process-global mocks leaked by overlapping async patch contexts.""" + + module = sys.modules.get("src.services.api.controllers.retrieve") + if module is None: + yield + return + + passage_retriever = getattr(module, "_retrieve_passages", None) + extractor = getattr(module, "_entity_extractor", None) + extract_elements = getattr(extractor, "extract_elements", None) + yield + if passage_retriever is not None: + module._retrieve_passages = passage_retriever + if extractor is not None and extract_elements is not None: + extractor.extract_elements = extract_elements diff --git a/tests/fixtures/jdsearch/product_meta_data.txt b/tests/fixtures/jdsearch/product_meta_data.txt new file mode 100644 index 0000000000000000000000000000000000000000..f47d95eb2a4aaaa3dcafec0f82ed06dccb4c60ec --- /dev/null +++ b/tests/fixtures/jdsearch/product_meta_data.txt @@ -0,0 +1,3 @@ +wid name brand_id brand_name cate_id_1 cate_name_1 cate_id_2 cate_name_2 cate_id_3 cate_name_3 cate_id_4 cate_name_4 shop_id +100 111222 b1 brandX c1 cat1 c2 cat2 c3 cat3 c4 cat4 s1 +200 333 b2 brandY c1 cat1 c2 cat2 c3 cat3 c4 cat4 s2 diff --git a/tests/fixtures/jdsearch/user_behavior_data.txt b/tests/fixtures/jdsearch/user_behavior_data.txt new file mode 100644 index 0000000000000000000000000000000000000000..d8d53b77858a869b2b87298129c68328717469eb --- /dev/null +++ b/tests/fixtures/jdsearch/user_behavior_data.txt @@ -0,0 +1,3 @@ +query candidate_wid_list candidate_label_list history_qry_list history_wid_list history_type_list history_time_list +111222 100_200_999 1.0_0.0_0.0 -1 200 CLICK 0_120 +333 200_100 2.0_1.0 111 100_200 CART_FLW 0_50_10 diff --git a/tests/plugins_path_helper.py b/tests/plugins_path_helper.py new file mode 100644 index 0000000000000000000000000000000000000000..31e3b5b9a13dcf47ccbc2aaa6ce58083e1c637b3 --- /dev/null +++ b/tests/plugins_path_helper.py @@ -0,0 +1,34 @@ +"""Load plugin modules without installing the plugin package.""" + +from __future__ import annotations + +import importlib.util +import sys +import unittest +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +PLUGIN = ROOT / "plugins" / "recsys-gnn" + + +def _load(name: str, path: Path): + if not path.is_file(): + raise unittest.SkipTest(f"optional plugin artifact is not installed: {path}") + + # Ensure plugin root is on path so `from models.X` works inside modules. + plugin_str = str(PLUGIN) + if plugin_str not in sys.path: + sys.path.insert(0, plugin_str) + spec = importlib.util.spec_from_file_location(name, path) + assert spec and spec.loader + mod = importlib.util.module_from_spec(spec) + sys.modules[name] = mod + spec.loader.exec_module(mod) + return mod + + +def load_export_edges(): + return _load( + "recsys_gnn_export_edges", + PLUGIN / "models" / "export_edges.py", + ) diff --git a/tests/test_agent_api.py b/tests/test_agent_api.py new file mode 100644 index 0000000000000000000000000000000000000000..e105525cdee4dd684118e19d259c28327ce41a3c --- /dev/null +++ b/tests/test_agent_api.py @@ -0,0 +1,279 @@ +import pytest +from fastapi import FastAPI +from fastapi.testclient import TestClient + +from src.services.api.middlewares.auth import BrainPATMiddleware +from src.services.api.middlewares.brains import BrainMiddleware +from src.services.api.routes.agent import agent_router +from tests.test_workspace_facade import FacadeCache, FacadeData + + +def agent_client(monkeypatch): + from src.services.api.middlewares import auth, brains + from src.services.api.routes import agent + + data = FacadeData() + cache = FacadeCache() + monkeypatch.setenv("BRAINPAT_TOKEN", "system-secret") + monkeypatch.setenv("PUBLIC_BASE_URL", "https://brain.example") + monkeypatch.setattr(brains, "data_adapter", data) + monkeypatch.setattr(brains, "cache_adapter", cache) + monkeypatch.setattr(auth, "data_adapter", data) + monkeypatch.setattr(auth, "cache_adapter", cache) + monkeypatch.setattr(agent, "data_adapter", data) + + calls = [] + + async def search(query, limit, brain_id): + calls.append(("search", brain_id, query)) + return [{"id": brain_id, "type": "chunk", "text": query}] + + async def context(query, max_tokens, brain_id): + calls.append(("context", brain_id, query)) + return {"context": f"{brain_id}:{query}", "sources": []} + + async def store(**kwargs): + calls.append( + ( + "store", + kwargs["brain_id"], + kwargs["text"], + kwargs["metadata"], + kwargs["request"].headers.get("Idempotency-Key"), + ) + ) + return {"task_id": f'{kwargs["brain_id"]}-task'} + + async def task(task_id, brain_id): + if task_id != f"{brain_id}-task": + from fastapi import HTTPException + + raise HTTPException(status_code=404, detail="Task not found") + return { + "task_id": task_id, + "brain_id": brain_id, + "status": "queued", + "workspace": "untrusted-task-payload", + } + + monkeypatch.setattr(agent, "memory_search", search) + monkeypatch.setattr(agent, "memory_context", context) + monkeypatch.setattr(agent, "memory_store", store) + monkeypatch.setattr(agent, "memory_task_status", task) + app = FastAPI() + app.add_middleware(BrainPATMiddleware) + app.add_middleware(BrainMiddleware) + app.include_router(agent_router) + return TestClient(app), calls + + +def test_agent_operations_use_canonical_workspace_scope(monkeypatch): + client, calls = agent_client(monkeypatch) + headers = {"Authorization": "Bearer system-secret"} + + assert client.post( + "/brains/alpha/agent/search", headers=headers, json={"query": "memory"} + ).json()["results"][0]["id"] == "alpha" + assert client.post( + "/brains/beta/agent/context", headers=headers, json={"query": "context"} + ).json()["context"] == "beta:context" + stored = client.post( + "/brains/alpha/agent/memory", + headers={**headers, "Idempotency-Key": "agent-message-1"}, + json={"text": "ALPHA_ONLY_FACT", "metadata": {"source": "hermes"}}, + ) + assert stored.status_code == 202 + assert stored.json() == { + "workspace": "alpha", + "status": "queued", + "task_id": "alpha-task", + } + assert ( + "store", + "alpha", + "ALPHA_ONLY_FACT", + {"source": "hermes"}, + "agent-message-1", + ) in calls + assert client.get( + "/brains/beta/agent/tasks/alpha-task", headers=headers + ).status_code == 404 + own_task = client.get("/brains/alpha/agent/tasks/alpha-task", headers=headers) + assert own_task.json()["workspace"] == "alpha" + + +def test_agent_security_discovery_and_openapi(monkeypatch): + client, _ = agent_client(monkeypatch) + system = {"Authorization": "Bearer system-secret"} + assert client.get( + "/brains/beta/agent/capabilities", + headers={"Authorization": "Bearer alpha-secret"}, + ).status_code == 403 + assert client.get( + "/brains/beta/agent/capabilities", + headers={"Authorization": "Bearer invalid-secret"}, + ).status_code == 401 + assert client.get("/brains/archived/agent/capabilities", headers=system).status_code == 410 + assert client.get("/brains/missing/agent/capabilities", headers=system).status_code == 404 + conflict = client.post( + "/brains/alpha/agent/search", + headers=system, + json={"query": "x", "brain_id": "beta"}, + ) + assert conflict.status_code == 409 + + capabilities = client.get( + "/brains/alpha/agent/capabilities", headers=system + ).json() + assert capabilities["workspace"] == { + "slug": "alpha", + "brain_id": "alpha", + "display_name": "Alpha", + } + assert capabilities["endpoints"]["agent_api"].endswith("/brains/alpha/agent") + assert capabilities["endpoints"]["native_openapi"].endswith( + "/brains/alpha/api/openapi.json" + ) + assert capabilities["endpoints"]["agent_openapi"].endswith( + "/brains/alpha/agent/openapi.json" + ) + assert capabilities["endpoints"]["mcp"].endswith("/brains/alpha/mcp") + schema = client.get("/brains/alpha/agent/openapi.json", headers=system).json() + assert schema["servers"] == [{"url": "https://brain.example/brains/alpha/agent"}] + assert set(schema["paths"]) == { + "/search", + "/context", + "/memory", + "/tasks/{task_id}", + "/capabilities", + } + + +def test_agent_metadata_survives_canonical_ingestion_and_chunk_retrieval(monkeypatch): + import asyncio + from unittest.mock import MagicMock, patch + + from fastapi.responses import JSONResponse + + from src.constants.embeddings import Vector + from src.services import agent_memory + from src.services.api.controllers import structured_data + from src.workers.tasks import ingestion + + queued_payload = {} + + async def queue(body, request, brain_id): + body.brain_id = brain_id + queued_payload.update(body.model_dump()) + return JSONResponse({"message": "accepted", "task_id": "alpha-task"}, 202) + + monkeypatch.setattr(agent_memory, "ingest_data", queue) + result = asyncio.run( + agent_memory.memory_store( + text="Customer prefers annual billing.", + metadata={"source": "hermes", "session_id": "abc"}, + brain_id="alpha", + request=type("Request", (), {"headers": {}})(), + ) + ) + assert result["task_id"] == "alpha-task" + assert queued_payload["brain_id"] == "alpha" + assert queued_payload["meta_keys"] == { + "source": "hermes", + "session_id": "abc", + } + + class MemoryData: + def __init__(self): + self.chunks = {} + + def save_text_chunk(self, chunk, brain_id): + self.chunks.setdefault(brain_id, []).append(chunk) + return chunk + + def get_text_chunks( + self, brain_id, limit, skip, query_text, metadata_eq, order + ): + chunks = self.chunks.get(brain_id, []) + if metadata_eq: + chunks = [ + chunk + for chunk in chunks + if all((chunk.metadata or {}).get(k) == v for k, v in metadata_eq.items()) + ] + return chunks[skip : skip + limit], len(chunks) + + repository = MemoryData() + request = MagicMock(id="alpha-task") + with patch.object(ingestion, "data_adapter", repository), patch.object( + ingestion, "embeddings_adapter" + ) as embeddings, patch.object( + ingestion, "vector_store_adapter" + ), patch.object( + ingestion.config, "pipeline_mode", "lightweight" + ), patch.object( + ingestion, "set_ingestion_task_status" + ), patch.object( + structured_data, "data_adapter", repository + ): + embeddings.embed_text.return_value = Vector( + id="vector-1", embeddings=[0.1], metadata={} + ) + queued_payload["skip_enrichment"] = True + ingestion.ingest_data.run.__func__( + type("Bound", (), {"request": request})(), queued_payload + ) + chunks, total = asyncio.run( + structured_data.get_text_chunks( + "alpha", metadata_eq={"source": "hermes", "session_id": "abc"} + ) + ) + + assert total == 1 + assert chunks[0].metadata == {"source": "hermes", "session_id": "abc"} + + +def test_agent_memory_idempotency_key_reuses_task_identifier(monkeypatch): + import asyncio + + from src.services import agent_memory + from src.services.api.routes import ingest + + queued = [] + monkeypatch.setattr( + ingest.ingest_data_task, + "apply_async", + lambda *, args, task_id: queued.append((args[0]["brain_id"], task_id)), + ) + monkeypatch.setattr(ingest, "set_ingestion_task_status", lambda *args, **kwargs: {}) + request = type("Request", (), {"headers": {"Idempotency-Key": "message-42"}})() + + first = asyncio.run( + agent_memory.memory_store( + text="Customer prefers annual billing.", + metadata={"source": "hermes"}, + brain_id="alpha", + request=request, + ) + ) + second = asyncio.run( + agent_memory.memory_store( + text="Customer prefers annual billing.", + metadata={"source": "hermes"}, + brain_id="alpha", + request=request, + ) + ) + + assert first["task_id"] == second["task_id"] == "message-42" + assert queued == [("alpha", "message-42"), ("alpha", "message-42")] + + +def test_relationship_worker_rejects_missing_brain_scope(): + from src.workers.tasks import ingestion + + with pytest.raises(ValueError, match="requires brain_id"): + ingestion.process_architect_relationships.run.__func__( + type("Bound", (), {"request": type("Request", (), {"id": "task"})()})(), + {"relationships": []}, + ) diff --git a/tests/test_agent_configuration_patterns.py b/tests/test_agent_configuration_patterns.py new file mode 100644 index 0000000000000000000000000000000000000000..2da4ca87a1085d63d96ed3ebfbe67983c4003885 --- /dev/null +++ b/tests/test_agent_configuration_patterns.py @@ -0,0 +1,117 @@ +import ast +from pathlib import Path +import unittest + + +ROOT = Path(__file__).resolve().parent.parent + + +def read_source(relative_path: str) -> str: + return (ROOT / relative_path).read_text(encoding="utf-8") + + +def parse_source(relative_path: str) -> ast.Module: + return ast.parse(read_source(relative_path)) + + +def class_names(relative_path: str) -> set[str]: + tree = parse_source(relative_path) + return {node.name for node in ast.walk(tree) if isinstance(node, ast.ClassDef)} + + +def method_names(relative_path: str, class_name: str) -> set[str]: + tree = parse_source(relative_path) + for node in ast.walk(tree): + if isinstance(node, ast.ClassDef) and node.name == class_name: + return { + method.name + for method in node.body + if isinstance(method, (ast.FunctionDef, ast.AsyncFunctionDef)) + } + raise AssertionError(f"Class {class_name} not found in {relative_path}") + + +class KGAgentPromptStrategyArchitectureTests(unittest.TestCase): + def test_kg_agent_has_prompt_registry_mapping(self): + source = read_source("src/core/agents/kg_agent.py") + self.assertIn("_SYSTEM_PROMPT_BUILDERS", source) + self.assertIn('"normal"', source) + self.assertIn('"graph-consolidator"', source) + + def test_kg_agent_validates_prompt_type_from_registry(self): + source = read_source("src/core/agents/kg_agent.py") + self.assertIn("if type_ not in self._SYSTEM_PROMPT_BUILDERS:", source) + self.assertIn('raise ValueError(f"Invalid type: {type_}")', source) + self.assertIn("system_prompt = self._SYSTEM_PROMPT_BUILDERS[type_](", source) + + +class JanitorAgentPromptAndSchemaArchitectureTests(unittest.TestCase): + def test_janitor_agent_has_prompt_registry_mapping(self): + source = read_source("src/core/agents/janitor_agent.py") + self.assertIn("_SYSTEM_PROMPT_BUILDERS", source) + self.assertIn('"janitor"', source) + self.assertIn('"graph-janitor"', source) + self.assertIn('"atomic-janitor"', source) + + def test_janitor_agent_uses_schema_resolution_methods(self): + methods = method_names("src/core/agents/janitor_agent.py", "JanitorAgent") + self.assertIn("_resolve_response_format", methods) + self.assertIn("_normalize_schema", methods) + source = read_source("src/core/agents/janitor_agent.py") + self.assertIn( + "response_format = self._resolve_response_format(output_schema, output_schemas)", + source, + ) + + def test_janitor_agent_validates_prompt_type(self): + source = read_source("src/core/agents/janitor_agent.py") + self.assertIn("if type_ not in self._SYSTEM_PROMPT_BUILDERS:", source) + self.assertIn('raise ValueError(f"Invalid type: {type_}")', source) + + +class ArchitectAgentPromptStrategyArchitectureTests(unittest.TestCase): + def test_architect_agent_has_prompt_registry_mapping(self): + source = read_source("src/core/agents/architect_agent.py") + self.assertIn("_SYSTEM_PROMPT_BUILDERS", source) + self.assertIn('("single", "granular")', source) + self.assertIn('("single", "coarse")', source) + self.assertIn('("tooler", "granular")', source) + self.assertIn('("tooler", "coarse")', source) + + def test_architect_agent_uses_prompt_resolver(self): + methods = method_names("src/core/agents/architect_agent.py", "ArchitectAgent") + self.assertIn("_resolve_system_prompt", methods) + source = read_source("src/core/agents/architect_agent.py") + self.assertIn( + "system_prompt = self._resolve_system_prompt(type_, mode, extra_system_prompt)", + source, + ) + self.assertIn('raise ValueError(f"Invalid mode for architect agent: {mode}")', source) + self.assertIn('raise ValueError(f"Invalid type for architect agent: {type_}")', source) + + +class JanitorToolReuseArchitectureTests(unittest.TestCase): + def test_execute_graph_operation_tool_is_alias_subclass(self): + source = read_source( + "src/core/agents/tools/janitor_agent/JanitorAgentExecuteGraphOperationTool.py" + ) + self.assertIn( + "class JanitorAgentExecuteGraphOperationTool(JanitorAgentExecuteGraphReadOperationTool):", + source, + ) + self.assertIn("pass", source) + self.assertNotIn("def _run", source) + + def test_janitor_tool_classes_exist(self): + names = class_names( + "src/core/agents/tools/janitor_agent/JanitorAgentExecuteGraphReadOperationTool.py" + ) + self.assertIn("JanitorAgentExecuteGraphReadOperationTool", names) + names_alias = class_names( + "src/core/agents/tools/janitor_agent/JanitorAgentExecuteGraphOperationTool.py" + ) + self.assertIn("JanitorAgentExecuteGraphOperationTool", names_alias) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_agent_readiness_api.py b/tests/test_agent_readiness_api.py new file mode 100644 index 0000000000000000000000000000000000000000..dd27595a53c3bdc216266f70b3c2be0cf09a1449 --- /dev/null +++ b/tests/test_agent_readiness_api.py @@ -0,0 +1,371 @@ +import os +import re +import unittest +from unittest.mock import AsyncMock, Mock, patch + +ENV_DEFAULTS = { + "BRAINPAT_TOKEN": "test-token", + "MODELS_MODE": "local", + "EMBEDDINGS_LOCAL_MODEL": "local-model", + "EMBEDDINGS_SMALL_MODEL": "small-model", + "EMBEDDING_NODES_DIMENSION": "3", + "EMBEDDING_TRIPLETS_DIMENSION": "3", + "EMBEDDING_OBSERVATIONS_DIMENSION": "3", + "EMBEDDING_DATA_DIMENSION": "3", + "EMBEDDING_RELATIONSHIPS_DIMENSION": "3", + "REDIS_HOST": "localhost", + "REDIS_PORT": "6379", + "NEO4J_HOST": "localhost", + "NEO4J_PORT": "7687", + "NEO4J_USERNAME": "neo4j", + "NEO4J_PASSWORD": "password", + "MILVUS_HOST": "localhost", + "MILVUS_PORT": "19530", + "MONGO_CONNECTION_STRING": "mongodb://localhost:27017", + "CELERY_WORKER_CONCURRENCY": "1", + "OLLAMA_HOST": "localhost", + "OLLAMA_PORT": "11434", + "OLLAMA_LLM_SMALL_MODEL": "small", + "OLLAMA_LLM_LARGE_MODEL": "large", + "PIPELINE_MODE": "accurate", +} +for key, value in ENV_DEFAULTS.items(): + os.environ.setdefault(key, value) + +from fastapi import FastAPI, HTTPException, Query +from fastapi.testclient import TestClient + +from src.services.api.constants.requests import SearchResponse +from src.services.api.errors import install_error_handlers +from src.services.api.middlewares.auth import BrainPATMiddleware +from src.services.api.middlewares.brains import BrainMiddleware +from src.services.api.routes.public import public_router + + +def _public_app() -> FastAPI: + app = FastAPI() + install_error_handlers(app) + app.include_router(public_router) + return app + + +class StructuredErrorTests(unittest.TestCase): + def test_http_error_preserves_detail_and_request_id(self): + app = FastAPI() + install_error_handlers(app) + + @app.get("/bad") + async def bad(): + raise HTTPException(status_code=404, detail="missing") + + response = TestClient(app).get( + "/bad", + headers={"X-Request-ID": "request-123"}, + ) + self.assertEqual(response.status_code, 404) + self.assertEqual(response.json()["detail"], "missing") + self.assertEqual(response.json()["error"]["code"], "RESOURCE_NOT_FOUND") + self.assertTrue(response.json()["error"]["resolution"]) + self.assertEqual(response.json()["error"]["request_id"], "request-123") + self.assertEqual(response.headers["X-Request-ID"], "request-123") + + def test_validation_error_keeps_structured_detail(self): + app = FastAPI() + install_error_handlers(app) + + @app.get("/validate") + async def validate(value: int = Query(..., ge=1)): + return {"value": value} + + response = TestClient(app).get("/validate?value=0") + self.assertEqual(response.status_code, 422) + self.assertIsInstance(response.json()["detail"], list) + self.assertEqual(response.json()["error"]["code"], "VALIDATION_ERROR") + + def test_unexpected_error_is_sanitized(self): + app = FastAPI() + install_error_handlers(app) + + @app.get("/explode") + async def explode(): + raise RuntimeError("secret backend detail") + + response = TestClient(app, raise_server_exceptions=False).get("/explode") + self.assertEqual(response.status_code, 500) + self.assertEqual(response.json()["detail"], "Internal server error") + self.assertNotIn("secret backend detail", response.text) + + def test_unknown_route_and_brain_failure_use_stable_codes(self): + app = FastAPI() + install_error_handlers(app) + + @app.get("/brain") + async def brain_failure(): + raise HTTPException(status_code=406, detail="unknown brain") + + missing = TestClient(app).get("/missing") + brain = TestClient(app).get("/brain") + self.assertEqual(missing.status_code, 404) + self.assertEqual(missing.json()["error"]["code"], "RESOURCE_NOT_FOUND") + self.assertEqual(brain.status_code, 406) + self.assertEqual(brain.json()["error"]["code"], "BRAIN_NOT_FOUND") + + def test_auth_middleware_returns_structured_json(self): + app = FastAPI() + install_error_handlers(app) + app.add_middleware(BrainPATMiddleware) + + @app.get("/system/test") + async def protected(): + return {"ok": True} + + with patch.dict(os.environ, {"BRAINPAT_TOKEN": "system-token"}): + response = TestClient(app).get("/system/test") + self.assertEqual(response.status_code, 401) + self.assertEqual(response.json()["error"]["code"], "AUTH_INVALID") + + def test_brain_middleware_validates_before_backing_store_access(self): + app = FastAPI() + install_error_handlers(app) + app.add_middleware(BrainMiddleware) + + @app.get("/protected") + async def protected(): + return {"ok": True} + + response = TestClient(app).get( + "/protected", + headers={"BrainPAT": "invalid", "X-Brain-ID": "not-valid!"}, + ) + self.assertEqual(response.status_code, 400) + self.assertEqual(response.json()["error"]["code"], "BRAIN_ID_INVALID") + + def test_brain_store_failure_is_structured_service_unavailable(self): + app = FastAPI() + install_error_handlers(app) + app.add_middleware(BrainMiddleware) + + @app.get("/protected") + async def protected(): + return {"ok": True} + + with patch( + "src.services.api.middlewares.brains.cache_adapter.get", + side_effect=ConnectionError("redis secret"), + ): + response = TestClient(app).get( + "/protected", + headers={"BrainPAT": "invalid", "X-Brain-ID": "validbrain"}, + ) + self.assertEqual(response.status_code, 503) + self.assertEqual(response.json()["error"]["code"], "SERVICE_UNAVAILABLE") + self.assertNotIn("redis secret", response.text) + + +class PublicApiTests(unittest.TestCase): + def test_health_is_typed_and_public(self): + response = TestClient(_public_app()).get("/health") + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json()["status"], "ok") + self.assertEqual(response.json()["service"], "brainapi") + self.assertTrue(response.json()["version"]) + + def test_full_app_exempts_machine_routes_and_authenticates_protected_routes(self): + from src.services.api.app import app + + client = TestClient(app) + health = client.get("/health", headers={"Origin": "https://agent.example"}) + openapi = client.get("/openapi.json") + protected = client.get("/tasks/", headers={"X-Request-ID": "auth-check"}) + + self.assertEqual(health.status_code, 200) + self.assertNotIn("Access-Control-Allow-Origin", health.headers) + self.assertEqual(openapi.status_code, 200) + self.assertEqual(openapi.json()["openapi"], "3.1.0") + self.assertEqual(protected.status_code, 401) + self.assertEqual(protected.json()["error"]["code"], "AUTH_INVALID") + self.assertEqual(protected.headers["X-Request-ID"], "auth-check") + + def test_demo_is_disabled_by_default(self): + with patch.dict(os.environ, {"PUBLIC_DEMO_ENABLED": "false"}): + response = TestClient(_public_app()).get("/demo/search?query=memory") + self.assertEqual(response.status_code, 404) + self.assertEqual(response.json()["error"]["code"], "RESOURCE_NOT_FOUND") + + def test_demo_forces_brain_passages_and_k_limit(self): + captured = [] + + async def fake_search(request): + captured.append(request) + return SearchResponse(hits=[]) + + env = { + "PUBLIC_DEMO_ENABLED": "true", + "PUBLIC_DEMO_BRAIN_ID": "agentdemo", + "PUBLIC_DEMO_MAX_K": "5", + } + with ( + patch.dict(os.environ, env), + patch("src.services.api.routes.public.config.search_enabled", True), + patch("src.services.api.routes.public.config.search_use_bm25", True), + patch("src.services.api.routes.public.config.search_use_dense", False), + patch( + "src.services.api.routes.public.search_controller", + new=AsyncMock(side_effect=fake_search), + ), + ): + response = TestClient(_public_app()).get( + "/demo/search?query=memory&k=5&brain_id=customerbrain" + ) + + self.assertEqual(response.status_code, 200) + self.assertEqual(len(captured), 1) + self.assertEqual(captured[0].brain_id, "agentdemo") + self.assertEqual(captured[0].channels, ["passages"]) + self.assertEqual(captured[0].k, 5) + + def test_demo_rejects_configured_limit_and_long_query(self): + env = { + "PUBLIC_DEMO_ENABLED": "true", + "PUBLIC_DEMO_BRAIN_ID": "agentdemo", + "PUBLIC_DEMO_MAX_K": "3", + } + with patch.dict(os.environ, env): + too_many = TestClient(_public_app()).get("/demo/search?query=x&k=4") + too_long = TestClient(_public_app()).get( + f"/demo/search?query={'x' * 501}" + ) + self.assertEqual(too_many.status_code, 422) + self.assertEqual(too_long.status_code, 422) + + def test_demo_reports_unavailable_search_dependency(self): + env = { + "PUBLIC_DEMO_ENABLED": "true", + "PUBLIC_DEMO_BRAIN_ID": "agentdemo", + "PUBLIC_DEMO_MAX_K": "10", + } + with ( + patch.dict(os.environ, env), + patch("src.services.api.routes.public.config.search_enabled", False), + ): + response = TestClient(_public_app()).get("/demo/search?query=memory") + + self.assertEqual(response.status_code, 503) + self.assertEqual(response.json()["error"]["code"], "SERVICE_UNAVAILABLE") + self.assertTrue(response.json()["error"]["resolution"]) + + def test_demo_controller_failures_are_structured_and_writes_are_absent(self): + env = { + "PUBLIC_DEMO_ENABLED": "true", + "PUBLIC_DEMO_BRAIN_ID": "agentdemo", + "PUBLIC_DEMO_MAX_K": "10", + } + with ( + patch.dict(os.environ, env), + patch("src.services.api.routes.public.config.search_enabled", True), + patch("src.services.api.routes.public.config.search_use_bm25", True), + patch("src.services.api.routes.public.config.search_use_dense", False), + patch( + "src.services.api.routes.public.search_controller", + new=AsyncMock(side_effect=RuntimeError("backend secret")), + ), + ): + failed = TestClient( + _public_app(), raise_server_exceptions=False + ).get("/demo/search?query=memory") + write = TestClient(_public_app()).post("/demo/search", json={"query": "x"}) + + self.assertEqual(failed.status_code, 500) + self.assertEqual(failed.json()["error"]["code"], "INTERNAL_ERROR") + self.assertNotIn("backend secret", failed.text) + self.assertEqual(write.status_code, 405) + self.assertEqual(write.json()["error"]["code"], "METHOD_NOT_ALLOWED") + self.assertEqual(write.headers["Allow"], "GET") + + +class OpenApiContractTests(unittest.TestCase): + def test_operations_are_function_calling_ready(self): + from src.services.api.app import app + + app.openapi_schema = None + schema = app.openapi() + operations = [] + for path, path_item in schema["paths"].items(): + for method, operation in path_item.items(): + if method not in {"get", "post", "put", "patch", "delete"}: + continue + operations.append((path, method, operation)) + + operation_ids = [operation["operationId"] for _, _, operation in operations] + self.assertEqual(len(operation_ids), len(set(operation_ids))) + for path, method, operation in operations: + operation_id = operation["operationId"] + self.assertRegex(operation_id, re.compile(r"^[a-z][a-z0-9_]{0,63}$")) + self.assertTrue(operation.get("description"), (method, path)) + for parameter in operation.get("parameters", []): + self.assertTrue( + "$ref" in parameter or "schema" in parameter, + (method, path, parameter), + ) + success_schemas = [ + media.get("schema") + for status, response in operation["responses"].items() + if str(status).startswith("2") + for media in response.get("content", {}).values() + ] + self.assertTrue(success_schemas, (method, path)) + self.assertTrue(all(item for item in success_schemas), (method, path)) + expected_security = [] if path in {"/health", "/demo/search"} else [ + {"BrainPAT": []}, + {"BearerAuth": []}, + ] + self.assertEqual(operation.get("security"), expected_security) + if path not in {"/health", "/demo/search"}: + self.assertEqual( + operation["responses"]["422"]["content"]["application/json"][ + "schema" + ]["$ref"], + "#/components/schemas/ErrorResponse", + ) + + self.assertIn("/system/brains/{brain_id}/clear", schema["paths"]) + self.assertIn("post", schema["paths"]["/system/brains/{brain_id}/clear"]) + self.assertIn("/system/brains/{brain_id}", schema["paths"]) + self.assertIn("delete", schema["paths"]["/system/brains/{brain_id}"]) + self.assertIn("202", schema["paths"]["/ingest/file"]["post"]["responses"]) + + generic_refs = [ + response["content"]["application/json"]["schema"].get("$ref", "") + for _, _, operation in operations + for status, response in operation["responses"].items() + if str(status).startswith("2") + and "application/json" in response.get("content", {}) + ] + self.assertFalse(any("Generic" in ref for ref in generic_refs)) + + +class PublicDemoSeedTests(unittest.TestCase): + def test_seed_command_skips_an_up_to_date_brain(self): + from scripts import seed_public_demo + + docs = Mock() + docs.text = "public docs" + docs.raise_for_status.return_value = None + existing = Mock(status_code=200) + existing.json.return_value = {"total": 1} + post = Mock() + + with ( + patch.dict(os.environ, {"BRAINPAT_TOKEN": "secret", "PUBLIC_DEMO_BRAIN_ID": "agentdemo"}), + patch("sys.argv", ["seed_public_demo.py"]), + patch.object(seed_public_demo.requests, "get", side_effect=[docs, existing]), + patch.object(seed_public_demo.requests, "post", post), + ): + result = seed_public_demo.main() + + self.assertEqual(result, 0) + post.assert_not_called() + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_ann_seed_stability.py b/tests/test_ann_seed_stability.py new file mode 100644 index 0000000000000000000000000000000000000000..73b915c49caf55573d83ab1ddba89c535a166d8a --- /dev/null +++ b/tests/test_ann_seed_stability.py @@ -0,0 +1,210 @@ +import os +import random +import sys +import unittest +from concurrent.futures import ThreadPoolExecutor +from unittest.mock import MagicMock, patch + +ENV_DEFAULTS = { + "BRAINPAT_TOKEN": "test-token", + "MODELS_MODE": "local", + "EMBEDDINGS_LOCAL_MODEL": "local-model", + "EMBEDDINGS_SMALL_MODEL": "small-model", + "EMBEDDING_NODES_DIMENSION": "3", + "EMBEDDING_TRIPLETS_DIMENSION": "3", + "EMBEDDING_OBSERVATIONS_DIMENSION": "3", + "EMBEDDING_DATA_DIMENSION": "3", + "EMBEDDING_RELATIONSHIPS_DIMENSION": "3", + "REDIS_HOST": "localhost", + "REDIS_PORT": "6379", + "NEO4J_HOST": "localhost", + "NEO4J_PORT": "7687", + "NEO4J_USERNAME": "neo4j", + "NEO4J_PASSWORD": "password", + "MILVUS_HOST": "localhost", + "MILVUS_PORT": "19530", + "MONGO_CONNECTION_STRING": "mongodb://localhost:27017", + "CELERY_WORKER_CONCURRENCY": "1", + "OLLAMA_HOST": "localhost", + "OLLAMA_PORT": "11434", + "OLLAMA_LLM_SMALL_MODEL": "small", + "OLLAMA_LLM_LARGE_MODEL": "large", + "POSTGRES_HOST": "localhost", + "POSTGRES_PORT": "5432", + "POSTGRES_USERNAME": "postgres", + "POSTGRES_PASSWORD": "postgres", +} +for key, value in ENV_DEFAULTS.items(): + os.environ.setdefault(key, value) + +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from src.constants.embeddings import Vector +from src.services.api.controllers import retrieve as retrieve_mod +from src.utils.vector_search import ( + ann_overfetch_k, + stable_top_k_vectors, + vector_rank_key, +) + + +class StableTopKVectorTests(unittest.TestCase): + def test_equal_distance_uses_uuid_total_order(self): + hits = [ + Vector(id="1", metadata={"uuid": "z-node"}, distance=0.1), + Vector(id="2", metadata={"uuid": "a-node"}, distance=0.1), + Vector(id="3", metadata={"uuid": "m-node"}, distance=0.05), + Vector(id="4", metadata={"uuid": "b-node"}, distance=0.1), + ] + forward = stable_top_k_vectors(hits, 3) + reverse = stable_top_k_vectors(list(reversed(hits)), 3) + self.assertEqual( + [v.metadata["uuid"] for v in forward], + [v.metadata["uuid"] for v in reverse], + ) + self.assertEqual( + [v.metadata["uuid"] for v in forward], + ["m-node", "a-node", "b-node"], + ) + + def test_boundary_k_stable_under_shuffled_equal_distance_pool(self): + pool = [ + Vector(id=str(i), metadata={"uuid": f"u{i:02d}"}, distance=0.2) + for i in range(12) + ] + expected = [f"u{i:02d}" for i in range(5)] + for seed in range(30): + rng = random.Random(seed) + shuffled = list(pool) + rng.shuffle(shuffled) + got = [v.metadata["uuid"] for v in stable_top_k_vectors(shuffled, 5)] + self.assertEqual(got, expected) + + def test_overfetch_is_at_least_4x(self): + self.assertEqual(ann_overfetch_k(25), 100) + self.assertEqual(ann_overfetch_k(5), 37) + self.assertEqual(ann_overfetch_k(0), 0) + + def test_rank_key_falls_back_to_vector_id(self): + v = Vector(id="fallback-id", metadata={}, distance=0.3) + self.assertEqual(vector_rank_key(v), (0.3, "fallback-id")) + + +class SeedStabilizationTests(unittest.TestCase): + def test_stabilize_seed_hits_dedupes_and_orders(self): + seeds = [ + ("b", 0.2, "B"), + ("a", 0.2, "A"), + ("b", 0.1, "B-better"), + ("c", 0.05, "C"), + ("", 0.0, "drop"), + ] + out = retrieve_mod._stabilize_seed_hits(seeds) + self.assertEqual( + out, + [("c", 0.05, "C"), ("b", 0.1, "B-better"), ("a", 0.2, "A")], + ) + + def test_quantize_distance_collapses_embed_float_noise(self): + a = retrieve_mod._stabilize_seed_hits( + [("u", 0.6657524665076711, "A"), ("v", 0.37019320459679717, "B")] + ) + b = retrieve_mod._stabilize_seed_hits( + [("v", 0.3702673417792153, "B"), ("u", 0.6657041188926127, "A")] + ) + self.assertEqual([uid for uid, _, _ in a], [uid for uid, _, _ in b]) + self.assertEqual([d for _, d, _ in a], [d for _, d, _ in b]) + self.assertEqual( + retrieve_mod._quantize_distance(0.6657524665076711), + retrieve_mod._quantize_distance(0.6657041188926127), + ) + self.assertEqual( + retrieve_mod._quantize_distance(0.37019320459679717), + retrieve_mod._quantize_distance(0.3702673417792153), + ) + + def test_equal_distance_concurrent_seed_paths_identical_uuid_lists(self): + equal_hits = [ + Vector( + id=f"n{i}", + embeddings=[0.1, 0.2, 0.3], + metadata={"uuid": f"node-{chr(ord('z') - i)}", "name": f"N{i}"}, + distance=0.42, + ) + for i in range(8) + ] + near = Vector( + id="near", + embeddings=[0.1, 0.2, 0.3], + metadata={"uuid": "node-near", "name": "Near"}, + distance=0.01, + ) + + mock_embeddings = MagicMock() + mock_embeddings.embed_text.return_value = Vector( + id="q", embeddings=[0.1, 0.2, 0.3], metadata={} + ) + + def run_once(order_seed: int): + rng = random.Random(order_seed) + node_hits = [near] + list(equal_hits) + rng.shuffle(node_hits) + rel_hits = [ + Vector( + id="r1", + embeddings=[0.1, 0.2, 0.3], + metadata={"node_ids": ["tail-b", "tail-a"]}, + distance=0.42, + ), + Vector( + id="r0", + embeddings=[0.1, 0.2, 0.3], + metadata={"node_ids": ["tail-c", "tail-a"]}, + distance=0.42, + ), + ] + rng.shuffle(rel_hits) + mock_vs = MagicMock() + mock_vs.search_nodes.return_value = node_hits + mock_vs.search_relationships.return_value = rel_hits + with patch.object(retrieve_mod, "embeddings_adapter", mock_embeddings): + with patch.object(retrieve_mod, "vector_search", mock_vs): + return [ + uid + for uid, _, _ in retrieve_mod._seed_nodes_for_text( + "query", "brain" + ) + ] + + results = [] + with ThreadPoolExecutor(max_workers=8) as pool: + futs = [pool.submit(run_once, seed) for seed in range(24)] + for fut in futs: + results.append(fut.result()) + + self.assertTrue(all(r == results[0] for r in results)) + self.assertEqual(results[0][0], "node-near") + self.assertEqual( + results[0], + sorted(results[0], key=lambda uid: (0.01 if uid == "node-near" else 0.42, uid)), + ) + + +class AdapterStableSearchTests(unittest.TestCase): + def test_adapter_breaks_distance_ties_by_uuid(self): + from src.adapters.embeddings import VectorStoreAdapter + + store = MagicMock() + store.search_vectors.return_value = [ + Vector(id="1", metadata={"uuid": "z"}, distance=0.1), + Vector(id="2", metadata={"uuid": "a"}, distance=0.1), + Vector(id="3", metadata={"uuid": "m"}, distance=0.05), + ] + adapter = VectorStoreAdapter() + adapter.add_client(store) + results = adapter.search_vectors([1.0, 0.0], brain_id="b", store="nodes", k=2) + self.assertEqual([v.metadata["uuid"] for v in results], ["m", "a"]) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_architect_batch_extract.py b/tests/test_architect_batch_extract.py new file mode 100644 index 0000000000000000000000000000000000000000..3b801c15f4fddc29a6f1568486194f9bfe211ee3 --- /dev/null +++ b/tests/test_architect_batch_extract.py @@ -0,0 +1,274 @@ +import os + +ENV_DEFAULTS = { + "BRAINPAT_TOKEN": "test-token", + "MODELS_MODE": "local", + "PIPELINE_MODE": "accurate", + "EMBEDDINGS_LOCAL_MODEL": "local-model", + "EMBEDDINGS_SMALL_MODEL": "small-model", + "EMBEDDING_NODES_DIMENSION": "3", + "EMBEDDING_TRIPLETS_DIMENSION": "3", + "EMBEDDING_OBSERVATIONS_DIMENSION": "3", + "EMBEDDING_DATA_DIMENSION": "3", + "EMBEDDING_RELATIONSHIPS_DIMENSION": "3", + "REDIS_HOST": "localhost", + "REDIS_PORT": "6379", + "NEO4J_HOST": "localhost", + "NEO4J_PORT": "7687", + "NEO4J_USERNAME": "neo4j", + "NEO4J_PASSWORD": "password", + "MILVUS_HOST": "localhost", + "MILVUS_PORT": "19530", + "MONGO_CONNECTION_STRING": "mongodb://localhost:27017", + "CELERY_WORKER_CONCURRENCY": "1", + "OLLAMA_HOST": "localhost", + "OLLAMA_PORT": "11434", + "OLLAMA_LLM_SMALL_MODEL": "small", + "OLLAMA_LLM_LARGE_MODEL": "large", +} +for key, value in ENV_DEFAULTS.items(): + os.environ.setdefault(key, value) + +from unittest.mock import MagicMock, patch + +from src.core.agents.architect_agent import ArchitectAgent +from src.core.agents.scout_agent import ScoutEntity +from src.core.saving.architect_batch import ( + BatchEndpoint, + BatchExtractResponse, + BatchRelationship, +) +from src.core.saving.ingest_cost import IngestCostLedger + + +def test_run_batch_extract_queues_valid_without_escalate(): + agent = ArchitectAgent( + llm_adapter=MagicMock(), + cache_adapter=MagicMock(), + kg=MagicMock(), + vector_store=MagicMock(), + embeddings=MagicMock(), + ingestion_manager=MagicMock(), + ) + source = "Alice hired Bob as a contractor." + entities = [ + ScoutEntity(uuid="u1", name="Alice", type="PERSON"), + ScoutEntity(uuid="u2", name="Hiring", type="EVENT"), + ScoutEntity(uuid="u3", name="Bob", type="PERSON"), + ] + payload = BatchExtractResponse( + relationships=[ + BatchRelationship( + tail=BatchEndpoint(uuid="u1", name="Alice", type="PERSON"), + tip=BatchEndpoint(uuid="u2", name="Hiring", type="EVENT"), + name="MADE", + source_span="Alice hired Bob as a contractor", + ), + BatchRelationship( + tail=BatchEndpoint(uuid="u2", name="Hiring", type="EVENT"), + tip=BatchEndpoint(uuid="u3", name="Bob", type="PERSON"), + name="TARGETED", + source_span="Alice hired Bob as a contractor", + ), + ] + ) + + with patch.object(agent, "_get_agent"), patch.object( + agent, "run_tooler" + ) as tooler: + agent.agent = MagicMock() + agent.agent.invoke.return_value = { + "structured_response": payload, + "messages": [], + } + ledger = IngestCostLedger() + rels = agent.run_batch_extract( + source, + entities, + cost_ledger=ledger, + escalate=True, + timeout=5, + ) + tooler.assert_not_called() + assert len(rels) == 2 + assert ledger.architect_units == 1 + assert ledger.architect_escalations == 0 + assert ledger.architect_schema_calls == 1 + + +def test_run_batch_extract_repairs_then_escalates_on_failure(): + agent = ArchitectAgent( + llm_adapter=MagicMock(), + cache_adapter=MagicMock(), + kg=MagicMock(), + vector_store=MagicMock(), + embeddings=MagicMock(), + ingestion_manager=MagicMock(), + ) + source = "Alice met Bob." + entities = [ + ScoutEntity(uuid="u1", name="Alice", type="PERSON"), + ScoutEntity(uuid="u2", name="Bob", type="PERSON"), + ] + bad = BatchExtractResponse( + relationships=[ + BatchRelationship( + tail=BatchEndpoint(uuid="u1", name="PERSON", type="PERSON"), + tip=BatchEndpoint(uuid="u2", name="Bob", type="PERSON"), + name="MET", + source_span="Alice met Bob", + ) + ] + ) + + with patch.object(agent, "_get_agent"), patch.object( + agent, "run_tooler", return_value=[] + ) as tooler: + agent.agent = MagicMock() + agent.agent.invoke.return_value = { + "structured_response": bad, + "messages": [], + } + ledger = IngestCostLedger() + agent.run_batch_extract( + source, + entities, + cost_ledger=ledger, + escalate=True, + timeout=5, + max_retries=1, + ) + assert agent.agent.invoke.call_count == 2 + tooler.assert_called_once() + assert ledger.architect_escalations == 1 + assert ledger.escalate_rate == 1.0 + assert ledger.architect_repair_calls == 1 + + +def test_run_batch_extract_keeps_partial_without_escalate(): + agent = ArchitectAgent( + llm_adapter=MagicMock(), + cache_adapter=MagicMock(), + kg=MagicMock(), + vector_store=MagicMock(), + embeddings=MagicMock(), + ingestion_manager=MagicMock(), + ) + source = "Alice hired Bob as a contractor." + entities = [ + ScoutEntity(uuid="u1", name="Alice", type="PERSON"), + ScoutEntity(uuid="u2", name="Hiring", type="EVENT"), + ScoutEntity(uuid="u3", name="Bob", type="PERSON"), + ] + mixed = BatchExtractResponse( + relationships=[ + BatchRelationship( + tail=BatchEndpoint(uuid="u1", name="Alice", type="PERSON"), + tip=BatchEndpoint(uuid="u2", name="Hiring", type="EVENT"), + name="MADE", + source_span="Alice hired Bob as a contractor", + ), + BatchRelationship( + tail=BatchEndpoint(uuid="u2", name="Hiring", type="EVENT"), + tip=BatchEndpoint(uuid="u3", name="Bob", type="PERSON"), + name="TARGETED", + source_span="totally fabricated zebra claim", + ), + BatchRelationship( + tail=BatchEndpoint(uuid="u1", name="PERSON", type="PERSON"), + tip=BatchEndpoint(uuid="u3", name="Bob", type="PERSON"), + name="MET", + source_span="Alice hired Bob", + ), + ] + ) + + with patch.object(agent, "_get_agent"), patch.object( + agent, "run_tooler" + ) as tooler: + agent.agent = MagicMock() + agent.agent.invoke.return_value = { + "structured_response": mixed, + "messages": [], + } + ledger = IngestCostLedger() + rels = agent.run_batch_extract( + source, + entities, + cost_ledger=ledger, + escalate=True, + timeout=5, + max_retries=1, + ) + assert agent.agent.invoke.call_count == 2 + tooler.assert_not_called() + assert len(rels) == 1 + assert rels[0].name == "MADE" + assert ledger.architect_escalations == 0 + assert ledger.escalate_rate == 0.0 + assert ledger.architect_repair_calls == 1 + + +def test_run_batch_extract_prefers_primary_when_repair_worse(): + agent = ArchitectAgent( + llm_adapter=MagicMock(), + cache_adapter=MagicMock(), + kg=MagicMock(), + vector_store=MagicMock(), + embeddings=MagicMock(), + ingestion_manager=MagicMock(), + ) + source = "Alice hired Bob as a contractor." + entities = [ + ScoutEntity(uuid="u1", name="Alice", type="PERSON"), + ScoutEntity(uuid="u2", name="Hiring", type="EVENT"), + ScoutEntity(uuid="u3", name="Bob", type="PERSON"), + ] + primary = BatchExtractResponse( + relationships=[ + BatchRelationship( + tail=BatchEndpoint(uuid="u1", name="Alice", type="PERSON"), + tip=BatchEndpoint(uuid="u2", name="Hiring", type="EVENT"), + name="MADE", + source_span="Alice hired Bob as a contractor", + ), + BatchRelationship( + tail=BatchEndpoint(uuid="u1", name="PERSON", type="PERSON"), + tip=BatchEndpoint(uuid="u3", name="Bob", type="PERSON"), + name="MET", + source_span="Alice hired Bob", + ), + ] + ) + worse_repair = BatchExtractResponse( + relationships=[ + BatchRelationship( + tail=BatchEndpoint(uuid="u1", name="PERSON", type="PERSON"), + tip=BatchEndpoint(uuid="u3", name="Bob", type="PERSON"), + name="MET", + source_span="Alice hired Bob", + ) + ] + ) + + with patch.object(agent, "_get_agent"), patch.object( + agent, "run_tooler" + ) as tooler: + agent.agent = MagicMock() + agent.agent.invoke.side_effect = [ + {"structured_response": primary, "messages": []}, + {"structured_response": worse_repair, "messages": []}, + ] + ledger = IngestCostLedger() + rels = agent.run_batch_extract( + source, + entities, + cost_ledger=ledger, + escalate=True, + timeout=5, + max_retries=1, + ) + tooler.assert_not_called() + assert len(rels) == 1 + assert rels[0].name == "MADE" + assert ledger.architect_escalations == 0 diff --git a/tests/test_architect_batch_schema.py b/tests/test_architect_batch_schema.py new file mode 100644 index 0000000000000000000000000000000000000000..a1381d00390ae4f710de4a432f340d3d7d2746ba --- /dev/null +++ b/tests/test_architect_batch_schema.py @@ -0,0 +1,196 @@ +from src.core.saving.architect_batch import ( + BatchEndpoint, + BatchExtractResponse, + BatchRelationship, + event_leg_incomplete, + validate_batch_extract, +) + + +def _entity(uuid: str, name: str, type_: str): + return type("E", (), {"uuid": uuid, "name": name, "type": type_})() + + +def test_validate_accepts_grounded_event_hub(): + source = "Alice hired Bob as a contractor in May 2024 for $5000." + entities = [ + _entity("u1", "Alice", "PERSON"), + _entity("u2", "Hiring", "EVENT"), + _entity("u3", "Bob", "PERSON"), + ] + payload = BatchExtractResponse( + relationships=[ + BatchRelationship( + tail=BatchEndpoint(uuid="u1", name="Alice", type="PERSON"), + tip=BatchEndpoint(uuid="u2", name="Hiring", type="EVENT"), + name="MADE", + description="Alice hired Bob", + amount=5000, + source_span="Alice hired Bob as a contractor", + happened_at="2024-05", + ), + BatchRelationship( + tail=BatchEndpoint(uuid="u2", name="Hiring", type="EVENT"), + tip=BatchEndpoint(uuid="u3", name="Bob", type="PERSON"), + name="TARGETED", + description="Alice hired Bob", + amount=5000, + source_span="Alice hired Bob as a contractor in May 2024", + ), + ] + ) + result = validate_batch_extract( + payload, source_text=source, scout_entities=entities + ) + assert len(result.accepted) == 2 + assert result.rejected == [] + assert not event_leg_incomplete(result.accepted, entities) + + +def test_validate_rejects_type_named_and_ungrounded(): + source = "Alice met Bob at the park." + entities = [ + _entity("u1", "Alice", "PERSON"), + _entity("u2", "Bob", "PERSON"), + ] + payload = BatchExtractResponse( + relationships=[ + BatchRelationship( + tail=BatchEndpoint(uuid="u1", name="PERSON", type="PERSON"), + tip=BatchEndpoint(uuid="u2", name="Bob", type="PERSON"), + name="MET", + source_span="Alice met Bob", + ), + BatchRelationship( + tail=BatchEndpoint(uuid="u1", name="Alice", type="PERSON"), + tip=BatchEndpoint(uuid="u2", name="Bob", type="PERSON"), + name="MET", + source_span="totally fabricated zebra claim", + ), + BatchRelationship( + # well-formed unknown endpoint is soft-admitted (no escalate) + tail=BatchEndpoint(uuid="missing", name="Carol", type="PERSON"), + tip=BatchEndpoint(uuid="u2", name="Bob", type="PERSON"), + name="MET", + source_span="Alice met Bob at the park", + ), + ] + ) + result = validate_batch_extract( + payload, source_text=source, scout_entities=entities + ) + reasons = {issue.reason for issue in result.rejected} + assert "type_named_placeholder_endpoint" in reasons + assert "source_span_not_grounded" in reasons + assert "unknown_tail_endpoint" not in reasons + assert len(result.accepted) == 1 + assert result.accepted[0].tail.name == "Carol" + assert any(n.name == "Carol" for n in result.new_nodes) + + +def test_validate_admits_unknown_well_formed_endpoints(): + source = "Alice introduced Carol to Bob at the park." + entities = [ + _entity("u1", "Alice", "PERSON"), + _entity("u2", "Bob", "PERSON"), + ] + payload = BatchExtractResponse( + relationships=[ + BatchRelationship( + tail=BatchEndpoint(name="Carol", type="PERSON"), + tip=BatchEndpoint(uuid="u2", name="Bob", type="PERSON"), + name="MET", + source_span="Alice introduced Carol to Bob at the park", + ), + ] + ) + result = validate_batch_extract( + payload, source_text=source, scout_entities=entities + ) + assert result.usable + assert len(result.accepted) == 1 + assert result.rejected == [] + + +def test_validate_span_offsets_and_scout_uuid_reuse(): + source = "On Tuesday Alice paid Bob 12 dollars." + entities = [_entity("a", "Alice", "PERSON"), _entity("b", "Bob", "PERSON")] + span = "Alice paid Bob 12 dollars" + start = source.index(span) + payload = { + "new_nodes": [], + "relationships": [ + { + "tail": {"uuid": "a", "name": "Alice", "type": "PERSON"}, + "tip": {"uuid": "b", "name": "Bob", "type": "PERSON"}, + "name": "PAID", + "amount": 12, + "source_span": span, + "span_start": start, + "span_end": start + len(span), + } + ], + } + result = validate_batch_extract( + payload, source_text=source, scout_entities=entities + ) + assert len(result.accepted) == 1 + assert result.accepted[0].amount == 12 + + +def test_event_leg_incomplete_detects_untouched_event(): + entities = [ + _entity("e1", "Trip", "EVENT"), + _entity("p1", "Alice", "PERSON"), + ] + rels = [ + BatchRelationship( + tail=BatchEndpoint(uuid="p1", name="Alice", type="PERSON"), + tip=BatchEndpoint(uuid="p1", name="Alice", type="PERSON"), + name="SELF", + source_span="Alice", + ) + ] + assert event_leg_incomplete(rels, entities) + + +def test_validate_partial_keeps_usable_and_drops_bad_new_nodes(): + source = "Alice hired Bob as a contractor in May 2024." + entities = [ + _entity("u1", "Alice", "PERSON"), + _entity("u2", "Hiring", "EVENT"), + _entity("u3", "Bob", "PERSON"), + ] + payload = BatchExtractResponse( + new_nodes=[ + BatchEndpoint(name="PERSON", type="PERSON"), + BatchEndpoint(name="Contract", type="DOCUMENT"), + ], + relationships=[ + BatchRelationship( + tail=BatchEndpoint(uuid="u1", name="Alice", type="PERSON"), + tip=BatchEndpoint(uuid="u2", name="Hiring", type="EVENT"), + name="MADE", + source_span="Alice hired Bob as a contractor", + ), + BatchRelationship( + tail=BatchEndpoint(uuid="u2", name="Hiring", type="EVENT"), + tip=BatchEndpoint(uuid="missing", name="Carol", type="PERSON"), + name="TARGETED", + source_span="Alice hired Bob as a contractor in May 2024", + ), + ], + ) + result = validate_batch_extract( + payload, source_text=source, scout_entities=entities + ) + assert result.usable + assert not result.ok + assert len(result.accepted) == 2 + assert {r.name for r in result.accepted} == {"MADE", "TARGETED"} + assert len(result.new_nodes) >= 2 + assert any(n.name == "Contract" for n in result.new_nodes) + assert any(n.name == "Carol" for n in result.new_nodes) + reasons = {issue.reason for issue in result.rejected} + assert "unknown_tip_endpoint" not in reasons + assert any(r.startswith("new_node_type_named_placeholder:") for r in reasons) diff --git a/tests/test_architect_escalation.py b/tests/test_architect_escalation.py new file mode 100644 index 0000000000000000000000000000000000000000..0bbd05229716d2548783e7ac37d1312a19f0c6c6 --- /dev/null +++ b/tests/test_architect_escalation.py @@ -0,0 +1,232 @@ +import os + +ENV_DEFAULTS = { + "BRAINPAT_TOKEN": "test-token", + "MODELS_MODE": "local", + "PIPELINE_MODE": "accurate", + "EMBEDDINGS_LOCAL_MODEL": "local-model", + "EMBEDDINGS_SMALL_MODEL": "small-model", + "EMBEDDING_NODES_DIMENSION": "3", + "EMBEDDING_TRIPLETS_DIMENSION": "3", + "EMBEDDING_OBSERVATIONS_DIMENSION": "3", + "EMBEDDING_DATA_DIMENSION": "3", + "EMBEDDING_RELATIONSHIPS_DIMENSION": "3", + "REDIS_HOST": "localhost", + "REDIS_PORT": "6379", + "NEO4J_HOST": "localhost", + "NEO4J_PORT": "7687", + "NEO4J_USERNAME": "neo4j", + "NEO4J_PASSWORD": "password", + "MILVUS_HOST": "localhost", + "MILVUS_PORT": "19530", + "MONGO_CONNECTION_STRING": "mongodb://localhost:27017", + "CELERY_WORKER_CONCURRENCY": "1", + "OLLAMA_HOST": "localhost", + "OLLAMA_PORT": "11434", + "OLLAMA_LLM_SMALL_MODEL": "small", + "OLLAMA_LLM_LARGE_MODEL": "large", +} +for key, value in ENV_DEFAULTS.items(): + os.environ.setdefault(key, value) + +from unittest.mock import MagicMock, patch + +from src.core.agents.architect_agent import ArchitectAgent +from src.core.agents.scout_agent import ScoutEntity +from src.core.saving.architect_batch import BatchExtractResponse +from src.core.saving.auto_kg import _dense_architect_parts, _filter_entities_for_text +from src.core.saving.ingest_cost import IngestCostLedger +from src.utils.text_chunking import chunk_text + + +def _entity(uuid: str, name: str, type_: str = "Event"): + return ScoutEntity(uuid=uuid, name=name, type=type_) + + +def test_chunk_text_splits_dialogue_on_single_newlines(): + lines = [f"Speaker: turn number {i} with some filler text." for i in range(20)] + text = "\n".join(lines) + parts = chunk_text(text, max_chars=200) + assert len(parts) > 1 + assert all(len(p) <= 200 for p in parts) + joined = "\n".join(parts) + assert "Speaker: turn number 0" in joined + assert "Speaker: turn number 19" in joined + + +def test_dense_architect_parts_splits_entity_heavy_dialogue(): + lines = [ + "Caroline: I applied to adoption agencies this week.", + "Melanie: That is huge. How are Oscar and Bailey?", + "Caroline: Oscar hid a bone. Bailey is new.", + "Melanie: Show me Oliver too.", + "Caroline: I also painted a horse with dad.", + "Melanie: Proud of you.", + ] + text = "\n".join(lines) + entities = [ + _entity(f"e{i}", name) + for i, name in enumerate( + [ + "Caroline", + "Melanie", + "Applied to adoption agencies", + "Oscar", + "Bailey", + "Oliver", + "Hid a bone", + "Painted a horse", + "Horseback riding", + "Self-portrait", + "Goodbyes", + "Support", + "Conversation", + ] + ) + ] + assert len(entities) >= 12 + parts = _dense_architect_parts( + text, entities, entity_threshold=12, dense_max_chars=120 + ) + assert len(parts) > 1 + filtered = _filter_entities_for_text(entities, parts[0]) + assert filtered + assert len(filtered) <= len(entities) + + +def test_run_batch_extract_empty_with_escalate_disabled_stays_partial(): + agent = ArchitectAgent( + llm_adapter=MagicMock(), + cache_adapter=MagicMock(), + kg=MagicMock(), + vector_store=MagicMock(), + embeddings=MagicMock(), + ingestion_manager=MagicMock(), + ) + source = "Alice met Bob." + entities = [ + ScoutEntity(uuid="u1", name="Alice", type="PERSON"), + ScoutEntity(uuid="u2", name="Bob", type="PERSON"), + ] + empty = BatchExtractResponse(relationships=[], new_nodes=[]) + + with patch.object(agent, "_get_agent"), patch.object( + agent, "run_tooler" + ) as tooler: + agent.agent = MagicMock() + agent.agent.invoke.return_value = { + "structured_response": empty, + "messages": [], + } + ledger = IngestCostLedger() + rels = agent.run_batch_extract( + source, + entities, + cost_ledger=ledger, + escalate=False, + timeout=5, + max_retries=1, + ) + tooler.assert_not_called() + assert rels == [] + assert ledger.architect_escalations == 0 + + +def test_run_batch_extract_escalate_respects_zero_turn_budget(): + agent = ArchitectAgent( + llm_adapter=MagicMock(), + cache_adapter=MagicMock(), + kg=MagicMock(), + vector_store=MagicMock(), + embeddings=MagicMock(), + ingestion_manager=MagicMock(), + ) + source = "Alice met Bob." + entities = [ + ScoutEntity(uuid="u1", name="Alice", type="PERSON"), + ScoutEntity(uuid="u2", name="Bob", type="PERSON"), + ] + empty = BatchExtractResponse(relationships=[], new_nodes=[]) + + with patch.object(agent, "_get_agent"), patch.object( + agent, "run_tooler" + ) as tooler, patch( + "src.core.agents.architect_agent.config" + ) as mock_config: + mock_config.ingest_architect_max_schema_calls = 2 + mock_config.ingest_architect_escalate_max_turns = 0 + agent.agent = MagicMock() + agent.agent.invoke.return_value = { + "structured_response": empty, + "messages": [], + } + ledger = IngestCostLedger() + rels = agent.run_batch_extract( + source, + entities, + cost_ledger=ledger, + escalate=True, + timeout=5, + max_retries=1, + ) + tooler.assert_not_called() + assert rels == [] + assert ledger.architect_escalations == 1 + assert "escalate_budget_exhausted" in ledger.janitor_drop_reasons + + +def test_run_batched_janitor_caps_llm_calls(): + from src.constants.agents import ArchitectAgentEntity, ArchitectAgentRelationship + + agent = ArchitectAgent( + llm_adapter=MagicMock(), + cache_adapter=MagicMock(), + kg=MagicMock(), + vector_store=MagicMock(), + embeddings=MagicMock(), + ingestion_manager=MagicMock(), + ) + + pending = [ + ArchitectAgentRelationship( + flow_key=f"f{i}", + tail=ArchitectAgentEntity(uuid=f"t{i}", name=f"Tail{i}", type="PERSON"), + tip=ArchitectAgentEntity(uuid=f"p{i}", name=f"Tip{i}", type="PERSON"), + name="RELATED", + description="not in source text at all", + properties={}, + ) + for i in range(5) + ] + agent.pending_persistence_batches = [pending] + agent.relationships_set = list(pending) + + janitor = MagicMock() + janitor.run_atomic_janitor.return_value = "OK" + agent.janitor_agent = janitor + agent._janitor_agent_brain_id = "default" + + ledger = IngestCostLedger() + with patch( + "src.core.agents.architect_agent.config" + ) as mock_config, patch( + "src.core.saving.grounding.triage_relationships_for_janitor" + ) as triage_mock: + mock_config.ingest_janitor_max_llm_calls = 1 + triage = MagicMock() + triage.accept = [] + triage.reject = [] + triage.ambiguous = pending + triage_mock.return_value = triage + agent.run_batched_janitor( + text="Alice met Bob.", + brain_id="default", + batch_size=2, + cost_ledger=ledger, + ) + + assert janitor.run_atomic_janitor.call_count == 1 + assert any( + str(r).startswith("janitor_budget_exhausted") + for r in ledger.janitor_drop_reasons + ) diff --git a/tests/test_architect_grounding_decisions.py b/tests/test_architect_grounding_decisions.py new file mode 100644 index 0000000000000000000000000000000000000000..392d1db15ccd6202d7317a16e5d161b617a34a71 --- /dev/null +++ b/tests/test_architect_grounding_decisions.py @@ -0,0 +1,142 @@ +from types import SimpleNamespace + +from src.core.saving.grounding import ( + decide_relationship_grounding, + triage_relationships_for_janitor, +) +from src.core.saving.architect_batch import ( + BatchEndpoint, + BatchExtractResponse, + BatchRelationship, + validate_batch_extract, +) + + +def _entity(uuid: str, name: str, type_: str, description: str = ""): + return type( + "E", + (), + {"uuid": uuid, "name": name, "type": type_, "description": description}, + )() + + +def test_validate_realigns_mismatched_span_offsets(): + source = "On Tuesday Alice paid Bob 12 dollars." + entities = [_entity("a", "Alice", "PERSON"), _entity("b", "Bob", "PERSON")] + span = "Alice paid Bob 12 dollars" + payload = BatchExtractResponse( + relationships=[ + BatchRelationship( + tail=BatchEndpoint(uuid="a", name="Alice", type="PERSON"), + tip=BatchEndpoint(uuid="b", name="Bob", type="PERSON"), + name="PAID", + source_span=span, + span_start=0, + span_end=3, + ) + ] + ) + result = validate_batch_extract( + payload, source_text=source, scout_entities=entities + ) + assert len(result.accepted) == 1 + accepted = result.accepted[0] + assert accepted.span_start is not None + assert source[accepted.span_start : accepted.span_end] == span + + +def test_validate_fills_event_description_from_scout(): + source = "Alice hired Bob as a contractor in May 2024." + entities = [ + _entity("u1", "Alice", "PERSON"), + _entity( + "u2", + "Hiring", + "EVENT", + description="Alice hired Bob as a contractor in May 2024", + ), + _entity("u3", "Bob", "PERSON"), + ] + payload = BatchExtractResponse( + relationships=[ + BatchRelationship( + tail=BatchEndpoint(uuid="u1", name="Alice", type="PERSON"), + tip=BatchEndpoint(uuid="u2", name="Hiring", type="EVENT"), + name="MADE", + source_span="Alice hired Bob as a contractor", + ) + ] + ) + result = validate_batch_extract( + payload, source_text=source, scout_entities=entities + ) + assert len(result.accepted) == 1 + event = result.accepted[0].tip + assert (event.description or "").strip() + assert "hired" in event.description.lower() + + +def test_validate_fills_event_description_from_source_span(): + source = "Alice hired Bob as a contractor in May 2024." + entities = [ + _entity("u1", "Alice", "PERSON"), + _entity("u2", "Hiring", "EVENT"), + _entity("u3", "Bob", "PERSON"), + ] + payload = BatchExtractResponse( + relationships=[ + BatchRelationship( + tail=BatchEndpoint(uuid="u1", name="Alice", type="PERSON"), + tip=BatchEndpoint(uuid="u2", name="Hiring", type="EVENT"), + name="MADE", + source_span="Alice hired Bob as a contractor", + ) + ] + ) + result = validate_batch_extract( + payload, source_text=source, scout_entities=entities + ) + assert len(result.accepted) == 1 + assert result.accepted[0].tip.description == "Alice hired Bob as a contractor" + + +def test_validate_rejects_new_event_node_without_description(): + source = "Alice hired Bob as a contractor." + entities = [ + _entity("u1", "Alice", "PERSON"), + _entity("u3", "Bob", "PERSON"), + ] + payload = BatchExtractResponse( + new_nodes=[BatchEndpoint(name="Hiring", type="EVENT")], + relationships=[ + BatchRelationship( + tail=BatchEndpoint(uuid="u1", name="Alice", type="PERSON"), + tip=BatchEndpoint(name="Hiring", type="EVENT"), + name="MADE", + source_span="Alice hired Bob as a contractor", + ) + ], + ) + result = validate_batch_extract( + payload, source_text=source, scout_entities=entities + ) + reasons = {issue.reason for issue in result.rejected} + assert any(r.startswith("new_node_missing_event_description:") for r in reasons) + + +def test_grounding_decision_machine_readable(): + source = "Alice met Bob at the park." + rel = SimpleNamespace( + name="MET", + description="Alice met Bob at the park", + properties={"source_span": "Alice met Bob at the park"}, + tip=SimpleNamespace(name="Bob", type="PERSON", uuid="1"), + tail=SimpleNamespace(name="Alice", type="PERSON", uuid="2"), + ) + decision = decide_relationship_grounding(rel, source) + assert decision.decision == "accept" + assert decision.reason == "grounded_endpoints_ok" + triage = triage_relationships_for_janitor([rel], source) + assert len(triage.accept) == 1 + assert triage.reject == [] + assert triage.ambiguous == [] diff --git a/tests/test_architect_prior_context.py b/tests/test_architect_prior_context.py new file mode 100644 index 0000000000000000000000000000000000000000..2b3c5f56f2f17b03e5bbf4c5505e7268eccc4d5f --- /dev/null +++ b/tests/test_architect_prior_context.py @@ -0,0 +1,303 @@ +import os + +ENV_DEFAULTS = { + "BRAINPAT_TOKEN": "test-token", + "MODELS_MODE": "local", + "PIPELINE_MODE": "accurate", + "EMBEDDINGS_LOCAL_MODEL": "local-model", + "EMBEDDINGS_SMALL_MODEL": "small-model", + "EMBEDDING_NODES_DIMENSION": "3", + "EMBEDDING_TRIPLETS_DIMENSION": "3", + "EMBEDDING_OBSERVATIONS_DIMENSION": "3", + "EMBEDDING_DATA_DIMENSION": "3", + "EMBEDDING_RELATIONSHIPS_DIMENSION": "3", + "REDIS_HOST": "localhost", + "REDIS_PORT": "6379", + "NEO4J_HOST": "localhost", + "NEO4J_PORT": "7687", + "NEO4J_USERNAME": "neo4j", + "NEO4J_PASSWORD": "password", + "MILVUS_HOST": "localhost", + "MILVUS_PORT": "19530", + "MONGO_CONNECTION_STRING": "mongodb://localhost:27017", + "CELERY_WORKER_CONCURRENCY": "1", + "OLLAMA_HOST": "localhost", + "OLLAMA_PORT": "11434", + "OLLAMA_LLM_SMALL_MODEL": "small", + "OLLAMA_LLM_LARGE_MODEL": "large", +} +for key, value in ENV_DEFAULTS.items(): + os.environ.setdefault(key, value) + +from unittest.mock import MagicMock, patch + +from src.constants.agents import ArchitectAgentEntity, ArchitectAgentRelationship +from src.core.agents.scout_agent import ScoutEntity +from src.core.saving.architect_scratchpad import ( + build_scratchpad, + fetch_prior_span, + format_unit_with_scratchpad, + resolve_prior_context_mode, + serialize_scratchpad, +) +from src.core.saving.auto_kg import ( + _architect_unit_text, + _architect_unit_text_scratchpad, +) +from src.core.saving.ingest_cost import count_source_tokens + + +def test_resolve_prior_context_defaults_scratchpad_for_batch(): + assert resolve_prior_context_mode("auto", "batch") == "scratchpad" + assert resolve_prior_context_mode("auto", "schema") == "scratchpad" + assert resolve_prior_context_mode("auto", "tooler") == "raw" + assert resolve_prior_context_mode("raw", "batch") == "raw" + assert resolve_prior_context_mode("scratchpad", "tooler") == "scratchpad" + + +def test_build_and_serialize_scratchpad_includes_hubs_and_predicates(): + entities = [ + ScoutEntity(uuid="u1", name="Alice", type="PERSON"), + ScoutEntity( + uuid="u2", + name="Hiring", + type="EVENT", + happened_at="2024-01-15", + description="Alice hired Bob", + ), + ScoutEntity(uuid="u3", name="Bob", type="PERSON"), + ] + rels = [ + ArchitectAgentRelationship( + uuid="r1", + flow_key="fk1", + name="MADE", + tail=ArchitectAgentEntity(uuid="u1", name="Alice", type="PERSON"), + tip=ArchitectAgentEntity( + uuid="u2", + name="Hiring", + type="EVENT", + happened_at="2024-01-15", + description="Alice hired Bob", + ), + properties={"source_span": "Alice hired Bob as a contractor yesterday"}, + ), + ArchitectAgentRelationship( + uuid="r2", + flow_key="fk2", + name="TARGETED", + tail=ArchitectAgentEntity( + uuid="u2", name="Hiring", type="EVENT", happened_at="2024-01-15" + ), + tip=ArchitectAgentEntity(uuid="u3", name="Bob", type="PERSON"), + properties={"source_span": "hired Bob as a contractor"}, + ), + ] + + pad = build_scratchpad(entities, rels) + assert any(e.name == "Alice" for e in pad.entities) + assert any(h.name == "Hiring" for h in pad.event_hubs) + hub = next(h for h in pad.event_hubs if h.name == "Hiring") + assert "Alice" in hub.actors + assert "Bob" in hub.objects + assert "2024-01-15" in pad.dates + assert any(p.predicate == "MADE" for p in pad.recent_predicates) + + text, tokens = serialize_scratchpad(pad, token_cap=500) + assert tokens > 0 + assert tokens <= 500 + assert "Hiring" in text + assert "Alice" in text + assert "MADE" in text + # Span pointers are truncated quotes, not full prior unit bodies. + assert "Alice hired Bob" in text + + +def test_serialize_scratchpad_obeys_token_cap(): + entities = [ + ScoutEntity( + uuid=f"u{i}", + name=f"PersonName{i}WithExtraDetail", + type="PERSON", + happened_at=f"2024-01-{(i % 28) + 1:02d}", + ) + for i in range(80) + ] + rels = [ + ArchitectAgentRelationship( + uuid=f"r{i}", + flow_key=f"fk{i}", + name="MADE", + tail=ArchitectAgentEntity( + uuid=f"u{i}", name=f"PersonName{i}WithExtraDetail", type="PERSON" + ), + tip=ArchitectAgentEntity( + uuid=f"e{i}", + name=f"EventHub{i}LongLabel", + type="EVENT", + happened_at=f"2024-02-{(i % 28) + 1:02d}", + ), + properties={ + "source_span": ( + f"PersonName{i}WithExtraDetail did something notable " + f"during EventHub{i}LongLabel on a long day of narrative text." + ) + }, + ) + for i in range(40) + ] + pad = build_scratchpad(entities, rels) + text, tokens = serialize_scratchpad(pad, token_cap=120) + assert tokens <= 120 + counted, _, _ = count_source_tokens(text) + assert counted <= 120 + + +def test_scratchpad_unit_text_excludes_full_prior_bodies(): + prior_body = ( + "Caroline told Melanie she started a new job at Google last Monday " + "and also mentioned her sister's wedding plans for June in detail. " + * 8 + ) + prior_entities = [ + ScoutEntity(uuid="u1", name="Caroline", type="PERSON"), + ScoutEntity( + uuid="u2", + name="Started new job", + type="EVENT", + happened_at="2023-05-08", + ), + ] + prior_rels = [ + ArchitectAgentRelationship( + uuid="r1", + flow_key="fk1", + name="MADE", + tail=ArchitectAgentEntity(uuid="u1", name="Caroline", type="PERSON"), + tip=ArchitectAgentEntity( + uuid="u2", + name="Started new job", + type="EVENT", + happened_at="2023-05-08", + ), + properties={"source_span": "Caroline told Melanie she started a new job"}, + ) + ] + current = "She said the salary was better than before." + scratch = _architect_unit_text_scratchpad( + current, + prior_entities=prior_entities, + prior_relationships=prior_rels, + token_cap=500, + ) + raw = _architect_unit_text(current, [prior_body]) + + assert "[Prior scratchpad]" in scratch + assert "[Current unit]" in scratch + assert current in scratch + assert "Caroline" in scratch + assert "Started new job" in scratch + # Must not replay the full prior narrative body. + assert prior_body not in scratch + assert prior_body in raw + assert "sister's wedding plans for June in detail" not in scratch + + +def test_fetch_prior_span_returns_window(): + prior = [ + "Intro filler. Alice met Bob at the cafe on Tuesday afternoon. More filler." + ] + span = fetch_prior_span(prior, "Alice met Bob", window_chars=80) + assert span is not None + assert "Alice met Bob" in span + assert len(span) < len(prior[0]) + + +def test_format_unit_with_scratchpad_empty_passthrough(): + assert format_unit_with_scratchpad("only current", "") == "only current" + + +def test_enrich_loop_uses_scratchpad_not_raw_prior_for_batch(): + """Integration-ish: second unit prompt must not contain prior chunk body.""" + from src.core.saving import auto_kg + + prior_chunk = ( + "UNIQUE_PRIOR_MARKER_XYZ Caroline started working at Google last week " + "and talked about her sister wedding plans extensively across many sentences." + ) + current_chunk = "She said the pay was higher." + + scout = MagicMock() + first_entities = [ + ScoutEntity(uuid="u1", name="Caroline", type="PERSON"), + ScoutEntity(uuid="u2", name="Started job", type="EVENT"), + ] + second_entities = [ + ScoutEntity(uuid="u1", name="Caroline", type="PERSON"), + ] + + class _Resp: + def __init__(self, entities): + self.entities = entities + + scout._run_chunk.side_effect = [ + _Resp(first_entities), + _Resp(second_entities), + ] + + architect = MagicMock() + architect.session_id = "s1" + architect.relationships_set = [ + ArchitectAgentRelationship( + uuid="r1", + flow_key="fk1", + name="MADE", + tail=ArchitectAgentEntity(uuid="u1", name="Caroline", type="PERSON"), + tip=ArchitectAgentEntity(uuid="u2", name="Started job", type="EVENT"), + properties={"source_span": "Caroline started working at Google"}, + ) + ] + architect.pending_persistence_batches = [list(architect.relationships_set)] + architect.take_pending_relationships.return_value = list( + architect.relationships_set + ) + architect.defer_janitor = True + + captured_texts: list[str] = [] + + def _capture_batch(text, entities, **kwargs): + captured_texts.append(text) + return list(architect.relationships_set) + + architect.run_batch_extract.side_effect = _capture_batch + + with patch.object(auto_kg, "ScoutAgent", return_value=scout), patch.object( + auto_kg, "ArchitectAgent", return_value=architect + ), patch.object(auto_kg, "IngestionManager", return_value=MagicMock()), patch.object( + auto_kg, "chunk_text", return_value=[prior_chunk, current_chunk] + ), patch.object( + auto_kg.config, "pipeline_mode", "accurate" + ), patch.object( + auto_kg.config, "ingest_architect_mode", "batch" + ), patch.object( + auto_kg.config, "ingest_architect_prior_context", "auto" + ), patch.object( + auto_kg.config, "ingest_architect_per_unit", True + ), patch.object( + auto_kg.config, "ingest_defer_janitor", False + ), patch.object( + auto_kg.config, "run_graph_consolidator", False + ), patch.object( + auto_kg.config, "ingest_architect_scratchpad_token_cap", 500 + ): + auto_kg.enrich_kg_from_input( + prior_chunk + "\n" + current_chunk, brain_id="brain-scratch" + ) + + assert len(captured_texts) == 2 + second = captured_texts[1] + assert "[Prior scratchpad]" in second + assert "UNIQUE_PRIOR_MARKER_XYZ" not in second + assert "wedding plans extensively" not in second + assert current_chunk in second + assert "Caroline" in second diff --git a/tests/test_architecture_refactors.py b/tests/test_architecture_refactors.py new file mode 100644 index 0000000000000000000000000000000000000000..7b0b78aa754a7ec363b737a3b7363480cf79130f --- /dev/null +++ b/tests/test_architecture_refactors.py @@ -0,0 +1,409 @@ +import json +import os +import sys +import types +import unittest +from unittest.mock import patch + + +ENV_DEFAULTS = { + "BRAINPAT_TOKEN": "test-token", + "MODELS_MODE": "local", + "EMBEDDINGS_LOCAL_MODEL": "local-model", + "EMBEDDINGS_SMALL_MODEL": "small-model", + "EMBEDDING_NODES_DIMENSION": "3", + "EMBEDDING_TRIPLETS_DIMENSION": "3", + "EMBEDDING_OBSERVATIONS_DIMENSION": "3", + "EMBEDDING_DATA_DIMENSION": "3", + "EMBEDDING_RELATIONSHIPS_DIMENSION": "3", + "REDIS_HOST": "localhost", + "REDIS_PORT": "6379", + "NEO4J_HOST": "localhost", + "NEO4J_PORT": "7687", + "NEO4J_USERNAME": "neo4j", + "NEO4J_PASSWORD": "password", + "MILVUS_HOST": "localhost", + "MILVUS_PORT": "19530", + "MONGO_CONNECTION_STRING": "mongodb://localhost:27017", + "CELERY_WORKER_CONCURRENCY": "1", + "OLLAMA_HOST": "localhost", + "OLLAMA_PORT": "11434", + "OLLAMA_LLM_SMALL_MODEL": "small", + "OLLAMA_LLM_LARGE_MODEL": "large", +} +for key, value in ENV_DEFAULTS.items(): + os.environ.setdefault(key, value) + +from src.adapters.embeddings import ( + EmbeddingError, + EmbeddingsAdapter, + RaiseEmbeddingFailureStrategy, +) +from src.adapters.graph import GraphAdapter +from src.constants.embeddings import Vector +from src.constants.data import Observation, TextChunk +from src.core.agents.core.runtime_agent_factory import RuntimeAgentFactory + + +class _StubEmbeddingsAdapter: + def embed_text(self, *_args, **_kwargs): + return Vector(id="stub", embeddings=[], metadata={}) + + +class _StubVectorStoreAdapter: + def search_vectors(self, *_args, **_kwargs): + return [] + + +class _StubGraphAdapter: + def get_by_uuids(self, *_args, **_kwargs): + return [] + + def get_neighbors(self, *_args, **_kwargs): + return {} + + def get_nodes_by_uuid(self, *_args, **_kwargs): + return [] + + +class _StubDataAdapter: + def get_observations_list(self, *_args, **_kwargs): + return [] + + def search(self, *_args, **_kwargs): + return types.SimpleNamespace(text_chunks=[], observations=[]) + + def get_text_chunks_by_ids(self, *_args, **_kwargs): + return ([], []) + + +class _StubKGAgent: + def retrieve_neighbors(self, *_args, **_kwargs): + return types.SimpleNamespace(neighbors=[]) + + +_stub_input_agents = types.ModuleType("src.services.input.agents") +_stub_input_agents.embeddings_adapter = _StubEmbeddingsAdapter() +sys.modules.setdefault("src.services.input.agents", _stub_input_agents) + +_stub_kg_main = types.ModuleType("src.services.kg_agent.main") +_stub_kg_main.graph_adapter = _StubGraphAdapter() +_stub_kg_main.vector_store_adapter = _StubVectorStoreAdapter() +_stub_kg_main.embeddings_adapter = _StubEmbeddingsAdapter() +_stub_kg_main.kg_agent = _StubKGAgent() +sys.modules.setdefault("src.services.kg_agent.main", _stub_kg_main) + +_stub_data_main = types.ModuleType("src.services.data.main") +_stub_data_main.data_adapter = _StubDataAdapter() +sys.modules.setdefault("src.services.data.main", _stub_data_main) + +from src.services.api.controllers.entities import get_entity_status +from src.services.api.controllers.retrieve import retrieve_data +from src.utils.vector_search import VectorSearchFacade + + +def raise_embedding_error(*args, **kwargs): + raise EmbeddingError("embedding failed") + + +class RuntimeAgentFactoryTests(unittest.TestCase): + def test_factory_uses_custom_backend_when_forced(self): + class FakeCustomAgent: + def __init__(self, **kwargs): + self.kwargs = kwargs + + def fake_create_agent(**kwargs): + return {"kind": "langchain", "kwargs": kwargs} + + factory = RuntimeAgentFactory( + create_agent_fn=fake_create_agent, custom_agent_cls=FakeCustomAgent + ) + built = factory.build( + model=object(), + tools=[], + system_prompt="system", + output_schema=dict, + architecture="langchain", + use_custom_backend=True, + debug=True, + ) + self.assertIsInstance(built, FakeCustomAgent) + self.assertEqual(built.kwargs["output_schema"], dict) + self.assertEqual(built.kwargs["system_prompt"], "system") + + def test_factory_uses_langchain_backend_when_supported(self): + class FakeCustomAgent: + def __init__(self, **kwargs): + self.kwargs = kwargs + + def fake_create_agent(**kwargs): + return {"kind": "langchain", "kwargs": kwargs} + + factory = RuntimeAgentFactory( + create_agent_fn=fake_create_agent, custom_agent_cls=FakeCustomAgent + ) + built = factory.build( + model=object(), + tools=["t1"], + system_prompt="system", + output_schema=dict, + architecture="langchain", + use_custom_backend=False, + debug=False, + ) + self.assertEqual(built["kind"], "langchain") + self.assertEqual(built["kwargs"]["response_format"], dict) + self.assertEqual(built["kwargs"]["tools"], ["t1"]) + + +class EmbeddingsStrategyTests(unittest.TestCase): + def test_embeddings_default_strategy_returns_empty_vectors(self): + adapter = EmbeddingsAdapter() + adapter._embed_text_with_retry = raise_embedding_error + adapter._embed_texts_with_retry = raise_embedding_error + single = adapter.embed_text("hello") + many = adapter.embed_texts(["a", "b"]) + self.assertEqual(single.embeddings, []) + self.assertEqual(len(many), 2) + self.assertTrue(all(v.embeddings == [] for v in many)) + + def test_embeddings_raise_strategy_raises_error(self): + adapter = EmbeddingsAdapter() + adapter._embed_text_with_retry = raise_embedding_error + adapter._embed_texts_with_retry = raise_embedding_error + adapter.set_failure_strategy(RaiseEmbeddingFailureStrategy()) + with self.assertRaises(EmbeddingError): + adapter.embed_text("hello") + with self.assertRaises(EmbeddingError): + adapter.embed_texts(["a"]) + + +class GraphAdapterReductionTests(unittest.TestCase): + def test_reduce_neighbor_vectors_without_description(self): + adapter = GraphAdapter() + vectors = [ + {"metadata": {"uuid": "a"}, "embeddings": [1.0, 0.0], "description": None}, + {"metadata": {"uuid": "b"}, "embeddings": [0.0, 1.0], "description": None}, + ] + reduced = adapter._reduce_neighbor_vectors( + vectors_with_desc=vectors, + averaged_vector=[1.0, 0.0], + similarity_threshold=0.5, + description=None, + ) + self.assertEqual(reduced, {"a"}) + + def test_reduce_neighbor_vectors_with_description_reranks(self): + adapter = GraphAdapter() + vectors = [ + {"metadata": {"uuid": "a"}, "embeddings": [1.0, 0.0], "description": "d1"}, + {"metadata": {"uuid": "b"}, "embeddings": [1.0, 0.0], "description": "d2"}, + ] + with patch("src.adapters.graph.reduce_list", return_value=[vectors[1]]) as reduced: + result = adapter._reduce_neighbor_vectors( + vectors_with_desc=vectors, + averaged_vector=[1.0, 0.0], + similarity_threshold=0.0, + description="focus", + ) + reduced.assert_called_once() + self.assertEqual(result, {"b"}) + + def test_reduce_neighbor_vectors_skips_invalid_embeddings(self): + adapter = GraphAdapter() + vectors = [ + {"metadata": {"uuid": "a"}, "embeddings": [1.0, 0.0]}, + {"metadata": {"uuid": "b"}, "embeddings": [1.0]}, + {"metadata": {"uuid": "c"}, "embeddings": None}, + {"metadata": {}, "embeddings": [1.0, 0.0]}, + ] + reduced = adapter._reduce_neighbor_vectors( + vectors_with_desc=vectors, + averaged_vector=[1.0, 0.0], + similarity_threshold=0.5, + description=None, + ) + self.assertEqual(reduced, {"a"}) + + def test_average_embeddings_ignores_invalid_and_mismatched_dimensions(self): + adapter = GraphAdapter() + vectors = [ + Vector(id="1", embeddings=[1.0, 2.0], metadata={}), + Vector(id="2", embeddings=[3.0, 4.0], metadata={}), + Vector(id="3", embeddings=[10.0], metadata={}), + Vector(id="4", embeddings=None, metadata={}), + Vector(id="5", embeddings=[], metadata={}), + ] + averaged = adapter._average_embeddings(vectors) + self.assertEqual(averaged, [2.0, 3.0]) + + +class GraphOperationSerializationTests(unittest.TestCase): + def test_execute_operation_serializes_none_result(self): + class FakeGraphClient: + def execute_operation(self, operation: str, brain_id: str): + return None + + adapter = GraphAdapter() + adapter.add_client(FakeGraphClient()) + result = adapter.execute_operation("RETURN 1", brain_id="default") + self.assertEqual(result, "") + + def test_execute_operation_serializes_mapping_result(self): + class FakeGraphClient: + def execute_operation(self, operation: str, brain_id: str): + return {"operation": operation, "brain_id": brain_id, "ok": True} + + adapter = GraphAdapter() + adapter.add_client(FakeGraphClient()) + result = adapter.execute_operation("RETURN 1", brain_id="test-brain") + self.assertEqual( + json.loads(result), + {"operation": "RETURN 1", "brain_id": "test-brain", "ok": True}, + ) + + def test_execute_operation_serializes_neo4j_like_result_with_limit(self): + class FakeRecord: + def __init__(self, value: int): + self.value = value + + def data(self): + return {"value": self.value} + + class FakeNeo4jResult: + def __init__(self): + self.records = [FakeRecord(value) for value in range(25)] + + def keys(self): + return ["value"] + + class FakeGraphClient: + def execute_operation(self, operation: str, brain_id: str): + return FakeNeo4jResult() + + adapter = GraphAdapter() + adapter.add_client(FakeGraphClient()) + result = adapter.execute_operation("MATCH (n) RETURN n", brain_id="default") + payload = json.loads(result) + self.assertTrue(payload["truncated"]) + self.assertEqual(payload["keys"], ["value"]) + self.assertEqual(len(payload["records"]), 20) + self.assertEqual(payload["records"][0], {"value": 0}) + + +class EntityStatusControllerTests(unittest.IsolatedAsyncioTestCase): + async def test_get_entity_status_skips_missing_graph_nodes(self): + fake_vector = type( + "FakeVector", + (), + {"metadata": {"uuid": "missing-node"}, "embeddings": [0.1, 0.2]}, + )() + with ( + patch( + "src.services.api.controllers.entities.embeddings_adapter.embed_text", + return_value=Vector(id="q", embeddings=[0.1, 0.2], metadata={}), + ), + patch( + "src.services.api.controllers.entities.vector_search.search_nodes", + return_value=[fake_vector], + ), + patch( + "src.services.api.controllers.entities.graph_adapter.get_by_uuids", + return_value=[], + ), + ): + response = await get_entity_status("target", types=None, brain_id="default") + self.assertFalse(response.exists) + self.assertFalse(response.has_relationships) + self.assertEqual(response.relationships, []) + self.assertEqual(response.observations, []) + + +class VectorSearchFacadeTests(unittest.TestCase): + def test_facade_routes_search_arguments_with_keywords(self): + class FakeVectorStore: + def __init__(self): + self.calls = [] + + def search_vectors(self, data_vector, brain_id, store, k=10): + self.calls.append( + { + "data_vector": data_vector, + "brain_id": brain_id, + "store": store, + "k": k, + } + ) + return [Vector(id="v1", embeddings=[0.1], metadata={"uuid": "n1"})] + + fake = FakeVectorStore() + facade = VectorSearchFacade(fake) + result = facade.search_nodes([0.1, 0.2], brain_id="brain-a", k=7) + self.assertEqual(result[0].id, "v1") + self.assertEqual( + fake.calls[0], + { + "data_vector": [0.1, 0.2], + "brain_id": "brain-a", + "store": "nodes", + "k": 7, + }, + ) + + +class RetrieveControllerVectorSearchTests(unittest.IsolatedAsyncioTestCase): + async def test_retrieve_data_uses_store_specific_vector_search(self): + fake_data_chunk = TextChunk(text="chunk", brain_version="1.0.0") + fake_observation = Observation(text="obs", resource_id="r1") + fake_search_result = type( + "SearchResult", + (), + { + "text_chunks": [fake_data_chunk], + "observations": [fake_observation], + }, + )() + fake_data_vector = type("FakeVector", (), {"metadata": {"resource_id": "r1"}})() + fake_triplet_vector = type( + "FakeVector", (), {"metadata": {"node_ids": ["n1"], "predicate": "RELATED_TO"}} + )() + with ( + patch( + "src.services.api.controllers.retrieve.embeddings_adapter.embed_text", + return_value=Vector(id="q", embeddings=[0.1, 0.2], metadata={}), + ), + patch( + "src.services.api.controllers.retrieve.vector_search.search_data", + return_value=[fake_data_vector], + ) as search_data, + patch( + "src.services.api.controllers.retrieve.vector_search.search_triplets", + return_value=[fake_triplet_vector], + ) as search_triplets, + patch( + "src.services.api.controllers.retrieve.data_adapter.search", + return_value=fake_search_result, + ), + patch( + "src.services.api.controllers.retrieve.data_adapter.get_text_chunks_by_ids", + return_value=([], []), + ), + patch( + "src.services.api.controllers.retrieve.graph_adapter.get_nodes_by_uuid", + return_value=[], + ), + ): + response = await retrieve_data( + text="sample", + limit=6, + preferred_entities="Person,Company", + brain_id="brain-a", + ) + search_data.assert_called_once_with([0.1, 0.2], brain_id="brain-a", k=6) + search_triplets.assert_called_once_with([0.1, 0.2], brain_id="brain-a", k=6) + self.assertEqual(len(response.data), 1) + self.assertEqual(len(response.observations), 1) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_beam_history_mode.py b/tests/test_beam_history_mode.py new file mode 100644 index 0000000000000000000000000000000000000000..713d86fdcc1aa5061300c39b2efee5ba3c9837d1 --- /dev/null +++ b/tests/test_beam_history_mode.py @@ -0,0 +1,114 @@ +from __future__ import annotations + +import os +import sys +import unittest +from datetime import datetime +from types import SimpleNamespace + +ENV_DEFAULTS = { + "BRAINPAT_TOKEN": "test-token", + "MODELS_MODE": "local", + "EMBEDDINGS_LOCAL_MODEL": "local-model", + "EMBEDDINGS_SMALL_MODEL": "small-model", + "EMBEDDING_NODES_DIMENSION": "3", + "EMBEDDING_TRIPLETS_DIMENSION": "3", + "EMBEDDING_OBSERVATIONS_DIMENSION": "3", + "EMBEDDING_DATA_DIMENSION": "3", + "EMBEDDING_RELATIONSHIPS_DIMENSION": "3", + "REDIS_HOST": "localhost", + "REDIS_PORT": "6379", + "NEO4J_HOST": "localhost", + "NEO4J_PORT": "7687", + "NEO4J_USERNAME": "neo4j", + "NEO4J_PASSWORD": "password", + "MILVUS_HOST": "localhost", + "MILVUS_PORT": "19530", + "MONGO_CONNECTION_STRING": "mongodb://localhost:27017", + "CELERY_WORKER_CONCURRENCY": "1", + "OLLAMA_HOST": "localhost", + "OLLAMA_PORT": "11434", + "OLLAMA_LLM_SMALL_MODEL": "small", + "OLLAMA_LLM_LARGE_MODEL": "large", + "POSTGRES_HOST": "localhost", + "POSTGRES_PORT": "5432", + "POSTGRES_USERNAME": "postgres", + "POSTGRES_PASSWORD": "postgres", +} +for key, value in ENV_DEFAULTS.items(): + os.environ.setdefault(key, value) + +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from src.services.api.controllers import retrieve as retrieve_mod +from src.utils.dates import parse_date_string + + +class HistoryModeTests(unittest.TestCase): + def test_ordering_query_wants_history(self): + self.assertTrue( + retrieve_mod._wants_historical_facts( + "Can you list the order in which I brought up aspects?" + ) + ) + + def test_first_sprint_wants_history(self): + self.assertTrue( + retrieve_mod._wants_historical_facts("When does my first sprint end?") + ) + + def test_have_i_contradiction_probe_wants_history(self): + self.assertTrue( + retrieve_mod._wants_historical_facts( + "Have I integrated Flask-Login for session management?" + ) + ) + + def test_knowledge_update_stays_current_only(self): + self.assertFalse( + retrieve_mod._wants_historical_facts( + "What is the average response time of the dashboard API?" + ) + ) + self.assertFalse( + retrieve_mod._wants_historical_facts( + "How many commits have been merged into the main branch?" + ) + ) + + def test_fact_predicates_include_superseded_in_history_mode(self): + current = SimpleNamespace(properties={}, deprecated=False) + old = SimpleNamespace(properties={"invalid_at": "2024-03-31"}, deprecated=False) + self.assertFalse( + retrieve_mod._fact_predicates_allowed(old, current, include_history=False) + ) + self.assertTrue( + retrieve_mod._fact_predicates_allowed(old, current, include_history=True) + ) + + +class SessionIdParseTests(unittest.TestCase): + def test_session_and_batch_turn_ids(self): + text = "Session id: session_12. Unit id: b3_t2. Also session_4." + ids = retrieve_mod._session_ids_from_text(text) + self.assertIn("session_12", ids) + self.assertIn("session_4", ids) + self.assertIn("session_b3_t2", ids) + + +class BeamDateStampTests(unittest.TestCase): + def test_hyphenated_beam_anchor_parses(self): + self.assertEqual( + parse_date_string("March-15-2024"), + datetime(2024, 3, 15), + ) + + def test_normalized_beam_anchor_parses(self): + self.assertEqual( + parse_date_string("March 15, 2024"), + datetime(2024, 3, 15), + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_benchmark_contract_guardrails.py b/tests/test_benchmark_contract_guardrails.py new file mode 100644 index 0000000000000000000000000000000000000000..2aecc312f44703583fa22402a2639f3812904a74 --- /dev/null +++ b/tests/test_benchmark_contract_guardrails.py @@ -0,0 +1,105 @@ +import os +import unittest +from pathlib import Path + +ENV_DEFAULTS = { + "BRAINPAT_TOKEN": "test-token", + "MODELS_MODE": "local", + "EMBEDDINGS_LOCAL_MODEL": "local-model", + "EMBEDDINGS_SMALL_MODEL": "small-model", + "EMBEDDING_NODES_DIMENSION": "3", + "EMBEDDING_TRIPLETS_DIMENSION": "3", + "EMBEDDING_OBSERVATIONS_DIMENSION": "3", + "EMBEDDING_DATA_DIMENSION": "3", + "EMBEDDING_RELATIONSHIPS_DIMENSION": "3", + "REDIS_HOST": "localhost", + "REDIS_PORT": "6379", + "NEO4J_HOST": "localhost", + "NEO4J_PORT": "7687", + "NEO4J_USERNAME": "neo4j", + "NEO4J_PASSWORD": "password", + "MILVUS_HOST": "localhost", + "MILVUS_PORT": "19530", + "MONGO_CONNECTION_STRING": "mongodb://localhost:27017", + "CELERY_WORKER_CONCURRENCY": "1", + "OLLAMA_HOST": "localhost", + "OLLAMA_PORT": "11434", + "OLLAMA_LLM_SMALL_MODEL": "small", + "OLLAMA_LLM_LARGE_MODEL": "large", +} +for key, value in ENV_DEFAULTS.items(): + os.environ.setdefault(key, value) + +ROOT = Path(__file__).resolve().parents[1] + + +class BenchmarkContractGuardrailsTests(unittest.TestCase): + """Ensure LoCoMo/BEAM/LME clients stay on frozen HTTP contracts.""" + + def _client_sources(self) -> list[Path]: + paths = [] + for suite in ("locomo", "beam", "longmemeval"): + client = ROOT / "benchmarks" / suite / "client.py" + if client.exists(): + paths.append(client) + return paths + + def test_bench_clients_use_ingest_and_context_only(self): + clients = self._client_sources() + self.assertGreaterEqual(len(clients), 1) + forbidden = ( + "/ingest/structured", + "/retrieve/entity/synergies", + "/retrieve/recommend", + "/recsys/", + ) + for path in clients: + text = path.read_text(encoding="utf-8") + for needle in forbidden: + self.assertNotIn( + needle, + text, + msg=f"{path} must not call {needle} (benchmark isolation)", + ) + + def test_bench_clients_call_core_ingest(self): + for path in self._client_sources(): + text = path.read_text(encoding="utf-8") + self.assertTrue( + "/ingest/" in text or "ingest" in text.lower(), + msg=f"{path} should still target core ingest", + ) + + def test_search_client_scores_via_search_not_context(self): + client = ROOT / "benchmarks" / "search" / "client.py" + self.assertTrue(client.exists(), msg="benchmarks/search/client.py is required") + text = client.read_text(encoding="utf-8") + self.assertIn("/retrieve/search", text) + self.assertIn("/ingest/", text) + self.assertIn("skip_enrichment", text) + self.assertNotIn( + "/retrieve/context", + text, + msg="search harness must not score via /retrieve/context", + ) + self.assertNotIn( + "/retrieve/recommend", + text, + msg="search harness must not score via /retrieve/recommend", + ) + for needle in ("locomoconv", "demorecsys"): + self.assertNotIn( + needle, + text, + msg=f"search client must not default to {needle}", + ) + + def test_search_config_defaults_to_searchbench(self): + config = ROOT / "benchmarks" / "search" / "config.py" + text = config.read_text(encoding="utf-8") + self.assertIn('DEFAULT_BRAIN_ID = "searchbenchsmoke"', text) + self.assertIn('REQUIRED_PREFIX = "searchbench"', text) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_brain_lifecycle.py b/tests/test_brain_lifecycle.py new file mode 100644 index 0000000000000000000000000000000000000000..ac7c27325f5fd280595c52edb52b83067f15f57d --- /dev/null +++ b/tests/test_brain_lifecycle.py @@ -0,0 +1,139 @@ +from __future__ import annotations + +import pytest + +from src.services.brain_lifecycle import ( + BrainCleanupError, + BrainLifecycleResult, + BrainLifecycleService, + BrainNotFoundError, + InvalidBrainIdError, + validate_brain_id, +) + + +class FakeStorage: + def __init__(self, name: str, events: list[str], fail: bool = False): + self.name = name + self.events = events + self.fail = fail + + def clear_brain_data(self, brain_id: str) -> None: + self.events.append(f"clear:{self.name}:{brain_id}") + if self.fail: + raise RuntimeError(f"{self.name} unavailable") + + +class FakeData(FakeStorage): + def __init__( + self, + events: list[str], + *, + exists: bool = True, + fail: bool = False, + ): + super().__init__("data", events, fail) + self.exists = exists + + def get_brain(self, name_key: str): + self.events.append(f"get:{name_key}") + return {"name_key": name_key, "pat": "pat"} if self.exists else None + + def delete_brain_registry(self, brain_id: str) -> bool: + self.events.append(f"registry:delete:{brain_id}") + was_present = self.exists + self.exists = False + return was_present + + +class FakeCache(FakeStorage): + client = None + + def __init__(self, events: list[str], fail: bool = False): + super().__init__("cache", events, fail) + + def clear_brain_registry_cache(self, brain_id: str) -> None: + self.events.append(f"registry-cache:clear:{brain_id}") + + +def service( + events: list[str], + *, + exists: bool = True, + failing_backend: str | None = None, +) -> tuple[BrainLifecycleService, FakeData]: + data = FakeData(events, exists=exists, fail=failing_backend == "data") + return ( + BrainLifecycleService( + data_client=data, + graph_client=FakeStorage( + "graph", events, fail=failing_backend == "graph" + ), + vector_client=FakeStorage( + "vectors", events, fail=failing_backend == "vectors" + ), + cache_client=FakeCache(events, fail=failing_backend == "cache"), + ), + data, + ) + + +def test_clear_preserves_brain_identity_and_clears_every_backend(): + events: list[str] = [] + lifecycle, data = service(events) + + result = lifecycle.clear("demo") + + assert result == BrainLifecycleResult( + brain_id="demo", + operation="clear", + existed=True, + cleared_backends=["data", "graph", "vectors", "cache"], + ) + assert data.exists is True + assert "registry:delete:demo" not in events + + +def test_clear_rejects_an_unknown_brain_without_touching_storage(): + events: list[str] = [] + lifecycle, _ = service(events, exists=False) + + with pytest.raises(BrainNotFoundError): + lifecycle.clear("missing") + + assert events == ["get:missing"] + + +def test_delete_is_idempotent_and_invalidates_registry_cache(): + events: list[str] = [] + lifecycle, data = service(events, exists=False) + + result = lifecycle.delete("gone") + + assert result.existed is False + assert result.operation == "delete" + assert data.exists is False + assert events[-2:] == [ + "registry-cache:clear:gone", + "registry:delete:gone", + ] + + +def test_partial_cleanup_keeps_registry_for_a_safe_retry(): + events: list[str] = [] + lifecycle, data = service(events, failing_backend="graph") + + with pytest.raises(BrainCleanupError) as caught: + lifecycle.delete("demo") + + assert caught.value.failed_backends == ["graph"] + assert data.exists is True + assert "clear:vectors:demo" in events + assert "clear:cache:demo" in events + assert "registry:delete:demo" not in events + + +@pytest.mark.parametrize("brain_id", ["", "123", "bad-id", "system"]) +def test_lifecycle_rejects_invalid_or_reserved_brain_ids(brain_id: str): + with pytest.raises(InvalidBrainIdError): + validate_brain_id(brain_id) diff --git a/tests/test_catalog_graph_search.py b/tests/test_catalog_graph_search.py new file mode 100644 index 0000000000000000000000000000000000000000..ebedcf400bafe98858f523cb95ce48952331f43e --- /dev/null +++ b/tests/test_catalog_graph_search.py @@ -0,0 +1,102 @@ +import os +import unittest +from unittest.mock import MagicMock + +ENV_DEFAULTS = { + "BRAINPAT_TOKEN": "test-token", + "MODELS_MODE": "local", + "EMBEDDINGS_LOCAL_MODEL": "local-model", + "EMBEDDINGS_SMALL_MODEL": "small-model", + "EMBEDDING_NODES_DIMENSION": "3", + "EMBEDDING_TRIPLETS_DIMENSION": "3", + "EMBEDDING_OBSERVATIONS_DIMENSION": "3", + "EMBEDDING_DATA_DIMENSION": "3", + "EMBEDDING_RELATIONSHIPS_DIMENSION": "3", + "REDIS_HOST": "localhost", + "REDIS_PORT": "6379", + "NEO4J_HOST": "localhost", + "NEO4J_PORT": "7687", + "NEO4J_USERNAME": "neo4j", + "NEO4J_PASSWORD": "password", + "MILVUS_HOST": "localhost", + "MILVUS_PORT": "19530", + "MONGO_CONNECTION_STRING": "mongodb://localhost:27017", + "CELERY_WORKER_CONCURRENCY": "1", + "OLLAMA_HOST": "localhost", + "OLLAMA_PORT": "11434", + "OLLAMA_LLM_SMALL_MODEL": "small", + "OLLAMA_LLM_LARGE_MODEL": "large", +} +for key, value in ENV_DEFAULTS.items(): + os.environ.setdefault(key, value) + +from src.constants.embeddings import Vector +from src.core.agents.scout_agent import ScoutEntity +from src.core.search.catalog_graph import node_embed_text +from src.core.saving.ingestion_manager import IngestionManager +from src.lib.postgresql.graph_store import PostgreSQLGraphStore +from src.services.api.constants.requests import SearchRequestBody + + +class CatalogGraphSearchUnitTests(unittest.TestCase): + def test_search_request_body_stays_passages_without_catalog_fields(self): + body = SearchRequestBody(query="modern sofas") + self.assertEqual(body.channels, ["passages"]) + fields = set(SearchRequestBody.model_fields) + self.assertIn("target", fields) + for banned in ("product_id", "sku", "brand", "user_id"): + self.assertNotIn(banned, fields) + + def test_node_embed_text_uses_search_text_only_when_set(self): + catalog = ScoutEntity( + type="ENTITY", + name="sofa", + uuid="sofa-1", + properties={"search_text": "velvet sofa navy modern"}, + ) + text, cache_key = node_embed_text(catalog) + self.assertEqual(text, "velvet sofa navy modern") + self.assertEqual(cache_key, "uuid:sofa-1") + memory = ScoutEntity(type="PERSON", name="Ada", uuid="p1") + mem_text, mem_key = node_embed_text(memory) + self.assertEqual(mem_text, "Ada") + self.assertEqual(mem_key, "Ada") + + def test_process_node_vectors_embeds_search_text(self): + embeddings = MagicMock() + embeddings.embed_text.return_value = Vector( + id="v1", embeddings=[0.1, 0.2], metadata={} + ) + store = MagicMock() + store.add_vectors.return_value = ["vid-1"] + manager = IngestionManager(embeddings, store, MagicMock()) + node = ScoutEntity( + type="ENTITY", + name="sofa", + uuid="sofa-1", + properties={"search_text": "velvet sofa for small rooms"}, + ) + manager.process_node_vectors(node, "searchbenchsmoke") + embeddings.embed_text.assert_called_once_with("velvet sofa for small rooms") + meta = embeddings.embed_text.return_value.metadata + self.assertEqual(meta["uuid"], "sofa-1") + self.assertEqual(meta["name"], "sofa") + memory = ScoutEntity(type="PERSON", name="Ada", uuid="p1") + manager.process_node_vectors(memory, "searchbenchsmoke") + embeddings.embed_text.assert_called_with("Ada") + + def test_node_search_ddl_is_english_and_gated(self): + ddl = PostgreSQLGraphStore._SEARCH_DDL + self.assertIn("kg_nodes", ddl) + self.assertIn("search_tsv", ddl) + self.assertIn("to_tsvector", ddl) + self.assertIn("'english'", ddl) + self.assertNotIn("'italian'", ddl) + self.assertIn("data->>'search_text'", ddl) + self.assertNotIn("concat_ws", ddl) + self.assertIn("::regconfig", ddl) + self.assertIn("||", ddl) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_chatbot_inference_plugin.py b/tests/test_chatbot_inference_plugin.py new file mode 100644 index 0000000000000000000000000000000000000000..43d0d1b5730c1f23e3175698165df69b2a6e0375 --- /dev/null +++ b/tests/test_chatbot_inference_plugin.py @@ -0,0 +1,161 @@ +import os +import sys +import unittest +from pathlib import Path + +ENV_DEFAULTS = { + "BRAINPAT_TOKEN": "test-token", + "MODELS_MODE": "local", + "EMBEDDINGS_LOCAL_MODEL": "local-model", + "EMBEDDINGS_SMALL_MODEL": "small-model", + "EMBEDDING_NODES_DIMENSION": "3", + "EMBEDDING_TRIPLETS_DIMENSION": "3", + "EMBEDDING_OBSERVATIONS_DIMENSION": "3", + "EMBEDDING_DATA_DIMENSION": "3", + "EMBEDDING_RELATIONSHIPS_DIMENSION": "3", + "REDIS_HOST": "localhost", + "REDIS_PORT": "6379", + "NEO4J_HOST": "localhost", + "NEO4J_PORT": "7687", + "NEO4J_USERNAME": "neo4j", + "NEO4J_PASSWORD": "password", + "MILVUS_HOST": "localhost", + "MILVUS_PORT": "19530", + "MONGO_CONNECTION_STRING": "mongodb://localhost:27017", + "CELERY_WORKER_CONCURRENCY": "1", + "OLLAMA_HOST": "localhost", + "OLLAMA_PORT": "11434", + "OLLAMA_LLM_SMALL_MODEL": "small", + "OLLAMA_LLM_LARGE_MODEL": "large", +} +for key, value in ENV_DEFAULTS.items(): + os.environ.setdefault(key, value) + +CHATBOT_PLUGIN_DIR = Path(__file__).resolve().parent.parent / "plugins" / "chatbot" +if not (CHATBOT_PLUGIN_DIR / "main.py").is_file(): + raise unittest.SkipTest("optional chatbot plugin is not installed") + +if str(CHATBOT_PLUGIN_DIR) not in sys.path: + sys.path.insert(0, str(CHATBOT_PLUGIN_DIR)) + +from fastapi import FastAPI, Request +from fastapi.testclient import TestClient + +from main import register +from routes import inference as inference_routes +from adapters.inference import parse_model +from src.core.plugins.context import PluginContext + + +class ChatbotInferenceAdapterTests(unittest.TestCase): + def test_parse_model_valid(self): + provider, model = parse_model("azureopenai::gpt-4o-mini") + self.assertEqual(provider, "azure") + self.assertEqual(model, "gpt-4o-mini") + + def test_parse_model_invalid_format(self): + with self.assertRaises(ValueError): + parse_model("azure-gpt-4o-mini") + + def test_parse_model_unsupported_provider(self): + with self.assertRaises(ValueError): + parse_model("unknown::model-a") + + +class ChatbotInferenceRouteTests(unittest.TestCase): + def setUp(self): + self.app = FastAPI() + + @self.app.middleware("http") + async def set_brain_id(request: Request, call_next): + request.state.brain_id = "brain-test" + return await call_next(request) + + self.app.include_router(inference_routes.create_inference_router(PluginContext)) + self.client = TestClient(self.app) + self.original_inference_response = inference_routes.inference_response + + def tearDown(self): + inference_routes.inference_response = self.original_inference_response + + async def _fake_inference_response( + self, model, prompt, stream=False, max_tokens=None, **kwargs + ): + self.assertFalse(stream) + self.assertEqual(model, "openai::gpt-4o-mini") + self.assertIn("hello", prompt) + self.assertEqual(kwargs.get("brain_id"), "brain-test") + self.assertEqual(max_tokens, 16) + return "openai", "gpt-4o-mini", "ok-result" + + def test_inference_non_stream_success(self): + inference_routes.inference_response = self._fake_inference_response + response = self.client.post( + "/chatbot/inference", + json={ + "model": "openai::gpt-4o-mini", + "input": "hello", + "stream": False, + "max_tokens": 16, + }, + ) + self.assertEqual(response.status_code, 200) + payload = response.json() + self.assertEqual(payload["data"]["provider"], "openai") + self.assertEqual(payload["data"]["output"], "ok-result") + self.assertFalse(payload["data"]["stream"]) + + def test_inference_invalid_model_returns_422(self): + async def fake_inference_response(model, prompt, stream=False, max_tokens=None, **kwargs): + raise ValueError("model must be in 'api_provider::llm_name' format") + + inference_routes.inference_response = fake_inference_response + response = self.client.post( + "/chatbot/inference", + json={"model": "bad-format", "input": "hello", "stream": False}, + ) + self.assertEqual(response.status_code, 422) + self.assertIn("api_provider::llm_name", response.json()["detail"]) + + def test_inference_stream_success(self): + async def fake_event_stream(): + yield 'data: {"choices":[{"delta":{"content":"hello"}}]}\n\n' + yield "data: [DONE]\n\n" + + async def fake_inference_response(model, prompt, stream=False, max_tokens=None, **kwargs): + self.assertTrue(stream) + return "openai", "gpt-4o-mini", fake_event_stream() + + inference_routes.inference_response = fake_inference_response + response = self.client.post( + "/chatbot/inference", + json={"model": "openai::gpt-4o-mini", "input": "hello", "stream": True}, + ) + self.assertEqual(response.status_code, 200) + self.assertEqual( + response.headers.get("content-type").split(";")[0], "text/event-stream" + ) + body = response.text + self.assertIn("data: {", body) + self.assertIn("data: [DONE]", body) + + +class ChatbotInferenceRegisterTests(unittest.TestCase): + class DummyContext: + def __init__(self): + self._app = object() + self.routers = [] + + def include_router(self, router, **kwargs): + self.routers.append((router, kwargs)) + + def test_register_adds_chatbot_router(self): + context = self.DummyContext() + register(context) + self.assertEqual(len(context.routers), 1) + router, _ = context.routers[0] + self.assertEqual(router.prefix, "/chatbot") + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_chatbot_mcp_prompt.py b/tests/test_chatbot_mcp_prompt.py new file mode 100644 index 0000000000000000000000000000000000000000..b661ff2561d16e6ddbde8182fcda58188f9d9ba4 --- /dev/null +++ b/tests/test_chatbot_mcp_prompt.py @@ -0,0 +1,22 @@ +import unittest + +from src.services.mcp.prompt import build_mcp_tools_instructions + + +class ChatbotMcpPromptTests(unittest.TestCase): + def test_build_mcp_tools_instructions_includes_brain_id(self): + text = build_mcp_tools_instructions("my-brain") + self.assertIn("my-brain", text) + self.assertIn("search_semantically", text) + self.assertIn("traverse_graph", text) + self.assertIn("tool_name", text) + + def test_prepend_wraps_user_prompt(self): + user_prompt = "User question" + combined = f"{build_mcp_tools_instructions('brain-a')}\n\n{user_prompt}" + self.assertTrue(combined.endswith(user_prompt)) + self.assertIn("brain-a", combined) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_context_defaults_and_fact_filter.py b/tests/test_context_defaults_and_fact_filter.py new file mode 100644 index 0000000000000000000000000000000000000000..4124da3f264a4afe10ae83c56c9fc8904c177b42 --- /dev/null +++ b/tests/test_context_defaults_and_fact_filter.py @@ -0,0 +1,238 @@ +import contextlib +import inspect +import os +import re +import sys +import unittest +from unittest.mock import MagicMock, patch + + +ENV_DEFAULTS = { + "BRAINPAT_TOKEN": "test-token", + "MODELS_MODE": "local", + "EMBEDDINGS_LOCAL_MODEL": "local-model", + "EMBEDDINGS_SMALL_MODEL": "small-model", + "EMBEDDING_NODES_DIMENSION": "3", + "EMBEDDING_TRIPLETS_DIMENSION": "3", + "EMBEDDING_OBSERVATIONS_DIMENSION": "3", + "EMBEDDING_DATA_DIMENSION": "3", + "EMBEDDING_RELATIONSHIPS_DIMENSION": "3", + "REDIS_HOST": "localhost", + "REDIS_PORT": "6379", + "NEO4J_HOST": "localhost", + "NEO4J_PORT": "7687", + "NEO4J_USERNAME": "neo4j", + "NEO4J_PASSWORD": "password", + "MILVUS_HOST": "localhost", + "MILVUS_PORT": "19530", + "MONGO_CONNECTION_STRING": "mongodb://localhost:27017", + "CELERY_WORKER_CONCURRENCY": "1", + "OLLAMA_HOST": "localhost", + "OLLAMA_PORT": "11434", + "OLLAMA_LLM_SMALL_MODEL": "small", + "OLLAMA_LLM_LARGE_MODEL": "large", + "POSTGRES_HOST": "localhost", + "POSTGRES_PORT": "5432", + "POSTGRES_USERNAME": "postgres", + "POSTGRES_PASSWORD": "postgres", +} +for key, value in ENV_DEFAULTS.items(): + os.environ.setdefault(key, value) + +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from src.constants.embeddings import Vector +from src.constants.kg import Node, Predicate +from src.core.search.fact_filter import FactFilterDecision +from src.services.api.constants.requests import GetContextRequestBody +from src.utils.nlp.ner import ExtractElementsResponse + + +def _node(uuid: str, name: str, *, happened_at: str | None = None, labels=None): + return Node( + uuid=uuid, + name=name, + labels=labels or ["PERSON"], + description=f"desc-{name}", + happened_at=happened_at, + ) + + +def _pred(uuid: str, name: str): + return Predicate(uuid=uuid, name=name, description=f"desc-{name}") + + +def _facts(count: int): + return [ + ( + _node("a", "Alice"), + _pred(f"r{i}a", f"ATTENDED_{i}"), + _node( + f"e{i}", + f"Event{i}", + happened_at=f"2024-0{i + 1}-01", + labels=["EVENT"], + ), + _pred(f"r{i}b", f"WITH_{i}"), + _node(f"b{i}", f"Person{i}"), + ) + for i in range(count) + ] + + +@contextlib.contextmanager +def _stubbed_adapters(retrieve_mod, facts, extra=()): + mock_embeddings = MagicMock() + mock_embeddings.embed_text.return_value = Vector( + id="q", embeddings=[0.1, 0.2, 0.3], metadata={} + ) + mock_vs = MagicMock() + mock_vs.search_nodes.return_value = [ + Vector( + id="n1", + embeddings=[0.1, 0.2, 0.3], + metadata={"uuid": "a", "name": "Alice"}, + distance=0.1, + ) + ] + mock_vs.search_relationships.return_value = [] + mock_graph = MagicMock() + mock_graph.get_event_centric_neighbors.return_value = facts + mock_data = MagicMock() + mock_data.get_last_text_chunks.return_value = [] + mock_data.get_last_structured_data.return_value = [] + + with contextlib.ExitStack() as stack: + stack.enter_context( + patch.object(retrieve_mod, "embeddings_adapter", mock_embeddings) + ) + stack.enter_context(patch.object(retrieve_mod, "vector_search", mock_vs)) + stack.enter_context(patch.object(retrieve_mod, "graph_adapter", mock_graph)) + stack.enter_context(patch.object(retrieve_mod, "data_adapter", mock_data)) + stack.enter_context( + patch.object( + retrieve_mod._entity_extractor, + "extract_elements", + return_value=ExtractElementsResponse(tokens=[], noun_chunks=[]), + ) + ) + for context in extra: + stack.enter_context(context) + yield mock_graph + + +class ContextRequestDefaultsTests(unittest.IsolatedAsyncioTestCase): + def test_controller_reads_no_declared_field_through_a_getattr_default(self): + from src.services.api.controllers import retrieve as retrieve_mod + + source = inspect.getsource(retrieve_mod.get_context) + offenders = [ + field + for field in GetContextRequestBody.model_fields + if re.search(rf"getattr\(\s*request\s*,\s*[\"']{field}[\"']", source) + ] + self.assertEqual(offenders, []) + + def test_ppr_is_on_and_sufficiency_retry_is_off_by_default(self): + defaults = { + name: field.default + for name, field in GetContextRequestBody.model_fields.items() + } + self.assertIs(defaults["use_ppr"], True) + self.assertIs(defaults["sufficiency_retry"], False) + + async def test_default_request_runs_ppr(self): + from src.services.api.controllers import retrieve as retrieve_mod + + adjacency = MagicMock(return_value={}) + with _stubbed_adapters( + retrieve_mod, + _facts(3), + extra=[ + patch.object(retrieve_mod, "_build_adjacency_from_seeds", adjacency) + ], + ): + await retrieve_mod.get_context( + GetContextRequestBody(text="Alice", brain_id="brain-a") + ) + + adjacency.assert_called_once() + + async def test_sufficiency_retry_is_opt_in(self): + from src.services.api.controllers import retrieve as retrieve_mod + + for sufficiency_retry in (False, True): + with self.subTest(sufficiency_retry=sufficiency_retry): + insufficient = MagicMock(return_value=True) + passages = MagicMock(return_value=[]) + with _stubbed_adapters( + retrieve_mod, + _facts(2), + extra=[ + patch.object( + retrieve_mod, "_context_looks_insufficient", insufficient + ), + patch.object(retrieve_mod, "_retrieve_passages", passages), + ], + ): + await retrieve_mod.get_context( + GetContextRequestBody( + text="Alice", + brain_id="brain-a", + sufficiency_retry=sufficiency_retry, + ) + ) + + self.assertEqual(insufficient.called, sufficiency_retry) + + +class ContextFactFilterTests(unittest.IsolatedAsyncioTestCase): + async def test_context_path_without_an_adapter_keeps_every_ranked_fact(self): + from src.services.api.controllers import retrieve as retrieve_mod + + with _stubbed_adapters(retrieve_mod, _facts(3)): + response = await retrieve_mod.get_context( + GetContextRequestBody( + text="Alice", brain_id="brain-a", apply_fact_filter=True + ) + ) + + self.assertEqual(len(response.triples), 3) + + async def test_supplied_adapter_actually_filters(self): + from src.services.api.controllers import retrieve as retrieve_mod + + adapter = MagicMock() + adapter.generate_structured.return_value = FactFilterDecision(keep_indices=[1]) + + with _stubbed_adapters(retrieve_mod, _facts(3)): + response = await retrieve_mod.get_context( + GetContextRequestBody( + text="Alice", brain_id="brain-a", apply_fact_filter=True + ), + fact_filter_adapter=adapter, + ) + + adapter.generate_structured.assert_called_once() + self.assertEqual(len(response.triples), 1) + + async def test_adapter_is_ignored_when_the_caller_disables_filtering(self): + from src.services.api.controllers import retrieve as retrieve_mod + + adapter = MagicMock() + adapter.generate_structured.return_value = FactFilterDecision(keep_indices=[1]) + + with _stubbed_adapters(retrieve_mod, _facts(3)): + response = await retrieve_mod.get_context( + GetContextRequestBody( + text="Alice", brain_id="brain-a", apply_fact_filter=False + ), + fact_filter_adapter=adapter, + ) + + adapter.generate_structured.assert_not_called() + self.assertEqual(len(response.triples), 3) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_context_fact_order_determinism.py b/tests/test_context_fact_order_determinism.py new file mode 100644 index 0000000000000000000000000000000000000000..223f64f7d8c62cfdbf191ee2778b7eeca109f53b --- /dev/null +++ b/tests/test_context_fact_order_determinism.py @@ -0,0 +1,274 @@ +import asyncio +import os +import random +import sys +import unittest +from concurrent.futures import ThreadPoolExecutor + +ENV_DEFAULTS = { + "BRAINPAT_TOKEN": "test-token", + "MODELS_MODE": "local", + "EMBEDDINGS_LOCAL_MODEL": "local-model", + "EMBEDDINGS_SMALL_MODEL": "small-model", + "EMBEDDING_NODES_DIMENSION": "3", + "EMBEDDING_TRIPLETS_DIMENSION": "3", + "EMBEDDING_OBSERVATIONS_DIMENSION": "3", + "EMBEDDING_DATA_DIMENSION": "3", + "EMBEDDING_RELATIONSHIPS_DIMENSION": "3", + "REDIS_HOST": "localhost", + "REDIS_PORT": "6379", + "NEO4J_HOST": "localhost", + "NEO4J_PORT": "7687", + "NEO4J_USERNAME": "neo4j", + "NEO4J_PASSWORD": "password", + "MILVUS_HOST": "localhost", + "MILVUS_PORT": "19530", + "MONGO_CONNECTION_STRING": "mongodb://localhost:27017", + "CELERY_WORKER_CONCURRENCY": "1", + "OLLAMA_HOST": "localhost", + "OLLAMA_PORT": "11434", + "OLLAMA_LLM_SMALL_MODEL": "small", + "OLLAMA_LLM_LARGE_MODEL": "large", + "POSTGRES_HOST": "localhost", + "POSTGRES_PORT": "5432", + "POSTGRES_USERNAME": "postgres", + "POSTGRES_PASSWORD": "postgres", +} +for key, value in ENV_DEFAULTS.items(): + os.environ.setdefault(key, value) + +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from src.constants.kg import Node, Predicate +from src.services.api.controllers import retrieve as retrieve_mod + + +def _node(uuid: str, name: str, *, labels=None): + return Node( + uuid=uuid, + name=name, + labels=labels or ["PERSON"], + description=f"desc-{name}", + properties={}, + ) + + +def _pred(uuid: str, name: str, *, flow_key: str = ""): + return Predicate( + uuid=uuid, + name=name, + description=f"desc-{name}", + flow_key=flow_key or None, + ) + + +def _candidate( + *, + score: float, + r_uuid: str, + r2_uuid: str, + flow_key: str = "flow", + n_uuid: str = "n", + m_uuid: str = "m", + b_uuid: str = "b", + entity: str = "q", +): + n = _node(n_uuid, n_uuid) + r = _pred(r_uuid, "REL", flow_key=flow_key) + m = _node(m_uuid, m_uuid, labels=["EVENT"]) + r2 = _pred(r2_uuid, "REL2", flow_key=flow_key) + b = _node(b_uuid, b_uuid) + return { + "identified_entity": entity, + "triple": (n, r, m, r2, b), + "score": score, + "key": (r_uuid, r2_uuid), + "text": f"{n_uuid}|{r_uuid}|{m_uuid}|{r2_uuid}|{b_uuid}", + "chunk_ids": [], + } + + +def _ordered_ids(candidates): + return [c["key"] for c in candidates] + + +class FactOrderDeterminismTests(unittest.TestCase): + def test_equal_scores_use_stable_tiebreakers(self): + a = _candidate(score=0.5, r_uuid="r-b", r2_uuid="r2-b", flow_key="f2") + b = _candidate(score=0.5, r_uuid="r-a", r2_uuid="r2-a", flow_key="f1") + c = _candidate(score=0.1, r_uuid="r-c", r2_uuid="r2-c", flow_key="f0") + ranked_forward = retrieve_mod._dedupe_candidates([a, b, c]) + ranked_reverse = retrieve_mod._dedupe_candidates([c, b, a]) + self.assertEqual(_ordered_ids(ranked_forward), _ordered_ids(ranked_reverse)) + self.assertEqual( + _ordered_ids(ranked_forward), + [("r-c", "r2-c"), ("r-a", "r2-a"), ("r-b", "r2-b")], + ) + + def test_merge_variant_lists_is_order_stable_under_shuffled_arrival(self): + v0 = [ + _candidate(score=0.2, r_uuid="r0", r2_uuid="x0", entity="v0"), + _candidate(score=0.2, r_uuid="r1", r2_uuid="x1", entity="v0"), + ] + v1 = [ + _candidate(score=0.2, r_uuid="r2", r2_uuid="x2", entity="v1"), + _candidate(score=0.2, r_uuid="r3", r2_uuid="x3", entity="v1"), + ] + expected = _ordered_ids( + retrieve_mod._dedupe_candidates( + retrieve_mod._merge_variant_candidate_lists([v0, v1]) + ) + ) + + def race_once(seed: int): + rng = random.Random(seed) + buckets = [None, None] + + def worker(idx, payload): + buckets[idx] = list(payload) + rng.random() + + with ThreadPoolExecutor(max_workers=2) as pool: + order = [0, 1] + rng.shuffle(order) + futs = [ + pool.submit(worker, idx, [v0, v1][idx]) for idx in order + ] + for fut in futs: + fut.result() + merged = retrieve_mod._merge_variant_candidate_lists( + [buckets[0], buckets[1]] + ) + return _ordered_ids(retrieve_mod._dedupe_candidates(merged)) + + results = [race_once(seed) for seed in range(40)] + self.assertTrue(all(r == expected for r in results)) + self.assertEqual( + expected, + [("r0", "x0"), ("r1", "x1"), ("r2", "x2"), ("r3", "x3")], + ) + + def test_nondeterministic_extend_diverges_without_ordered_merge(self): + a = _candidate(score=0.2, r_uuid="r0", r2_uuid="x0", entity="v0") + b = _candidate(score=0.2, r_uuid="r1", r2_uuid="x1", entity="v1") + score_only_ab = [c["key"] for c in sorted([a, b], key=lambda c: c["score"])] + score_only_ba = [c["key"] for c in sorted([b, a], key=lambda c: c["score"])] + self.assertNotEqual(score_only_ab, score_only_ba) + + fixed = { + tuple( + _ordered_ids( + retrieve_mod._dedupe_candidates( + retrieve_mod._merge_variant_candidate_lists(lists) + ) + ) + ) + for lists in ([[a], [b]], [[b], [a]]) + } + self.assertEqual(len(fixed), 1) + self.assertEqual(next(iter(fixed)), (("r0", "x0"), ("r1", "x1"))) + + +class ContextFactOrderIntegrationTests(unittest.IsolatedAsyncioTestCase): + async def test_two_identical_get_context_calls_same_triple_order(self): + import contextlib + from unittest.mock import MagicMock, patch + + from src.constants.embeddings import Vector + from src.services.api.constants.requests import GetContextRequestBody + from src.utils.nlp.ner import ExtractElementsResponse + + facts = [] + for i in range(6): + facts.append( + ( + _node(f"n{i}", f"N{i}"), + _pred(f"r{i}", "REL", flow_key=f"f{i % 3}"), + _node(f"e{i}", f"E{i}", labels=["EVENT"]), + _pred(f"r2{i}", "REL2", flow_key=f"f{i % 3}"), + _node(f"b{i}", f"B{i}"), + ) + ) + + mock_embeddings = MagicMock() + mock_embeddings.embed_text.return_value = Vector( + id="q", embeddings=[0.1, 0.2, 0.3], metadata={} + ) + mock_vs = MagicMock() + mock_vs.search_nodes.return_value = [ + Vector( + id="n0", + embeddings=[0.1, 0.2, 0.3], + metadata={"uuid": "n0", "name": "N0"}, + distance=0.5, + ), + Vector( + id="n1", + embeddings=[0.1, 0.2, 0.3], + metadata={"uuid": "n1", "name": "N1"}, + distance=0.5, + ), + ] + mock_vs.search_relationships.return_value = [] + mock_vs.search_data.return_value = [] + mock_graph = MagicMock() + mock_graph.get_event_centric_neighbors.return_value = facts + mock_data = MagicMock() + mock_data.get_last_text_chunks.return_value = [] + mock_data.get_last_structured_data.return_value = [] + mock_data.get_text_chunks_by_ids.return_value = ([], None) + mock_data.search.return_value = MagicMock(text_chunks=[]) + + async def _once(): + with contextlib.ExitStack() as stack: + stack.enter_context( + patch.object(retrieve_mod, "embeddings_adapter", mock_embeddings) + ) + stack.enter_context(patch.object(retrieve_mod, "vector_search", mock_vs)) + stack.enter_context( + patch.object(retrieve_mod, "graph_adapter", mock_graph) + ) + stack.enter_context(patch.object(retrieve_mod, "data_adapter", mock_data)) + stack.enter_context( + patch.object( + retrieve_mod._entity_extractor, + "extract_elements", + return_value=ExtractElementsResponse( + tokens=[ + {"text": "Alice", "label": "PERSON"}, + {"text": "Bob", "label": "PERSON"}, + ], + noun_chunks=["Alice", "Bob"], + ), + ) + ) + stack.enter_context( + patch.object(retrieve_mod, "_retrieve_passages", return_value=[]) + ) + return await retrieve_mod.get_context( + GetContextRequestBody( + text="Alice and Bob events", + brain_id="brain-a", + max_facts=4, + use_ppr=False, + ) + ) + + first = await _once() + second = await _once() + first_ids = [(t.triple[1].uuid, t.triple[3].uuid) for t in first.triples] + second_ids = [(t.triple[1].uuid, t.triple[3].uuid) for t in second.triples] + self.assertEqual(first_ids, second_ids) + self.assertEqual(len(first_ids), 4) + + parallel = await asyncio.gather(*[_once() for _ in range(12)]) + id_sets = [ + tuple((t.triple[1].uuid, t.triple[3].uuid) for t in resp.triples) + for resp in parallel + ] + self.assertEqual(len(set(id_sets)), 1) + self.assertEqual(id_sets[0], tuple(first_ids)) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_context_max_facts_and_provenance.py b/tests/test_context_max_facts_and_provenance.py new file mode 100644 index 0000000000000000000000000000000000000000..e777d697ad8c6d742b9a82a548b9fed721ff5717 --- /dev/null +++ b/tests/test_context_max_facts_and_provenance.py @@ -0,0 +1,260 @@ +import contextlib +import os +import sys +import unittest +from unittest.mock import MagicMock, patch + +from pydantic import ValidationError + + +ENV_DEFAULTS = { + "BRAINPAT_TOKEN": "test-token", + "MODELS_MODE": "local", + "EMBEDDINGS_LOCAL_MODEL": "local-model", + "EMBEDDINGS_SMALL_MODEL": "small-model", + "EMBEDDING_NODES_DIMENSION": "3", + "EMBEDDING_TRIPLETS_DIMENSION": "3", + "EMBEDDING_OBSERVATIONS_DIMENSION": "3", + "EMBEDDING_DATA_DIMENSION": "3", + "EMBEDDING_RELATIONSHIPS_DIMENSION": "3", + "REDIS_HOST": "localhost", + "REDIS_PORT": "6379", + "NEO4J_HOST": "localhost", + "NEO4J_PORT": "7687", + "NEO4J_USERNAME": "neo4j", + "NEO4J_PASSWORD": "password", + "MILVUS_HOST": "localhost", + "MILVUS_PORT": "19530", + "MONGO_CONNECTION_STRING": "mongodb://localhost:27017", + "CELERY_WORKER_CONCURRENCY": "1", + "OLLAMA_HOST": "localhost", + "OLLAMA_PORT": "11434", + "OLLAMA_LLM_SMALL_MODEL": "small", + "OLLAMA_LLM_LARGE_MODEL": "large", + "POSTGRES_HOST": "localhost", + "POSTGRES_PORT": "5432", + "POSTGRES_USERNAME": "postgres", + "POSTGRES_PASSWORD": "postgres", +} +for key, value in ENV_DEFAULTS.items(): + os.environ.setdefault(key, value) + +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from src.constants.data import TextChunk +from src.constants.embeddings import Vector +from src.constants.kg import Node, Predicate +from src.services.api.constants.requests import GetContextRequestBody +from src.utils.nlp.ner import ExtractElementsResponse + + +def _node(uuid: str, name: str, *, labels=None, source_chunk_ids=None): + props = {} + if source_chunk_ids: + props["source_chunk_ids"] = list(source_chunk_ids) + return Node( + uuid=uuid, + name=name, + labels=labels or ["PERSON"], + description=f"desc-{name}", + properties=props, + ) + + +def _pred(uuid: str, name: str): + return Predicate(uuid=uuid, name=name, description=f"desc-{name}") + + +def _fact(*, chunk_ids=None): + return ( + _node("a", "Alice", source_chunk_ids=chunk_ids), + _pred("r1", "ATTENDED"), + _node("e1", "Event1", labels=["EVENT"], source_chunk_ids=chunk_ids), + _pred("r2", "WITH"), + _node("b", "Bob", source_chunk_ids=chunk_ids), + ) + + +@contextlib.contextmanager +def _stubbed_adapters(retrieve_mod, facts, *, chunks=None, passages=None, extra=()): + mock_embeddings = MagicMock() + mock_embeddings.embed_text.return_value = Vector( + id="q", embeddings=[0.1, 0.2, 0.3], metadata={} + ) + mock_vs = MagicMock() + mock_vs.search_nodes.return_value = [ + Vector( + id="n1", + embeddings=[0.1, 0.2, 0.3], + metadata={"uuid": "a", "name": "Alice"}, + distance=0.1, + ) + ] + mock_vs.search_relationships.return_value = [] + mock_vs.search_data.return_value = [] + mock_graph = MagicMock() + mock_graph.get_event_centric_neighbors.return_value = facts + mock_data = MagicMock() + mock_data.get_last_text_chunks.return_value = [] + mock_data.get_last_structured_data.return_value = [] + mock_data.get_text_chunks_by_ids.return_value = (chunks or [], None) + mock_data.search.return_value = MagicMock(text_chunks=[]) + + with contextlib.ExitStack() as stack: + stack.enter_context( + patch.object(retrieve_mod, "embeddings_adapter", mock_embeddings) + ) + stack.enter_context(patch.object(retrieve_mod, "vector_search", mock_vs)) + stack.enter_context(patch.object(retrieve_mod, "graph_adapter", mock_graph)) + stack.enter_context(patch.object(retrieve_mod, "data_adapter", mock_data)) + stack.enter_context( + patch.object( + retrieve_mod._entity_extractor, + "extract_elements", + return_value=ExtractElementsResponse(tokens=[], noun_chunks=[]), + ) + ) + if passages is not None: + stack.enter_context( + patch.object(retrieve_mod, "_retrieve_passages", return_value=passages) + ) + for context in extra: + stack.enter_context(context) + yield mock_data + + +class MaxFactsZeroTests(unittest.IsolatedAsyncioTestCase): + def test_negative_max_facts_rejected_at_schema(self): + with self.assertRaises(ValidationError): + GetContextRequestBody(text="Alice", brain_id="brain-a", max_facts=-1) + + async def test_max_facts_zero_returns_no_graph_facts(self): + from src.services.api.controllers import retrieve as retrieve_mod + + with _stubbed_adapters( + retrieve_mod, + [_fact(chunk_ids=["chunk-1"]), _fact(chunk_ids=["chunk-2"])], + passages=[("p1", 0.1, "Session id: session_9. Passage body.")], + ): + response = await retrieve_mod.get_context( + GetContextRequestBody( + text="Alice", + brain_id="brain-a", + max_facts=0, + max_passages=2, + ) + ) + + self.assertEqual(response.triples, []) + self.assertIsNone(response.graph_session_ids) + fact_lines = [ + line + for line in response.text_context.splitlines() + if line and not line.startswith("[passage]") + ] + self.assertEqual(fact_lines, []) + self.assertTrue( + any("Session id: session_9" in p for p in response.source_passages) + ) + + async def test_max_facts_one_keeps_a_single_fact(self): + from src.services.api.controllers import retrieve as retrieve_mod + + with _stubbed_adapters( + retrieve_mod, + [_fact(chunk_ids=["chunk-1"]), _fact(chunk_ids=["chunk-2"])], + ): + response = await retrieve_mod.get_context( + GetContextRequestBody( + text="Alice", brain_id="brain-a", max_facts=1 + ) + ) + + self.assertEqual(len(response.triples), 1) + + +class GraphProvenanceTests(unittest.IsolatedAsyncioTestCase): + async def test_triples_expose_chunk_and_session_provenance(self): + from src.services.api.controllers import retrieve as retrieve_mod + + chunks = [ + TextChunk( + id="chunk-3", + text="Session id: session_3. Melanie put the keys in the slipper.", + ) + ] + with _stubbed_adapters( + retrieve_mod, + [_fact(chunk_ids=["chunk-3"])], + chunks=chunks, + passages=[], + ): + response = await retrieve_mod.get_context( + GetContextRequestBody( + text="Alice", brain_id="brain-a", max_facts=5, max_passages=2 + ) + ) + + self.assertEqual(len(response.triples), 1) + triple = response.triples[0] + self.assertEqual(triple.source_chunk_ids, ["chunk-3"]) + self.assertEqual(triple.source_session_ids, ["session_3"]) + self.assertEqual(response.graph_session_ids, ["session_3"]) + self.assertTrue( + any("(session_3)" in line for line in response.text_context.splitlines()) + ) + + async def test_missing_chunk_provenance_omits_session_ids(self): + from src.services.api.controllers import retrieve as retrieve_mod + + with _stubbed_adapters( + retrieve_mod, + [_fact(chunk_ids=["chunk-missing"])], + chunks=[], + passages=[], + ): + response = await retrieve_mod.get_context( + GetContextRequestBody( + text="Alice", brain_id="brain-a", max_facts=5 + ) + ) + + self.assertEqual(len(response.triples), 1) + self.assertEqual(response.triples[0].source_chunk_ids, ["chunk-missing"]) + self.assertIsNone(response.triples[0].source_session_ids) + self.assertIsNone(response.graph_session_ids) + + async def test_instrumentation_still_records_core_stages(self): + from src.services.api.controllers import retrieve as retrieve_mod + + with _stubbed_adapters( + retrieve_mod, + [_fact(chunk_ids=["chunk-3"])], + chunks=[ + TextChunk( + id="chunk-3", + text="Session id: session_3. Evidence.", + ) + ], + passages=[("p1", 0.2, "Session id: session_9. Passage.")], + ): + response = await retrieve_mod.get_context( + GetContextRequestBody( + text="Alice", + brain_id="brain-a", + max_facts=2, + profile_stages=True, + ) + ) + + self.assertIsNotNone(response.stage_timings) + stage_names = { + stage["stage"] for stage in response.stage_timings.get("stages", []) + } + self.assertFalse(any(name.startswith("dossiers") for name in stage_names)) + self.assertTrue(any(name.startswith("facts") for name in stage_names)) + self.assertTrue(any(name.startswith("passages") for name in stage_names)) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_context_no_dossier.py b/tests/test_context_no_dossier.py new file mode 100644 index 0000000000000000000000000000000000000000..2a070e4c6834cf6beabe90470a2338219d6c0960 --- /dev/null +++ b/tests/test_context_no_dossier.py @@ -0,0 +1,144 @@ +import contextlib +import os +import sys +import unittest +from unittest.mock import MagicMock, patch + + +ENV_DEFAULTS = { + "BRAINPAT_TOKEN": "test-token", + "MODELS_MODE": "local", + "EMBEDDINGS_LOCAL_MODEL": "local-model", + "EMBEDDINGS_SMALL_MODEL": "small-model", + "EMBEDDING_NODES_DIMENSION": "3", + "EMBEDDING_TRIPLETS_DIMENSION": "3", + "EMBEDDING_OBSERVATIONS_DIMENSION": "3", + "EMBEDDING_DATA_DIMENSION": "3", + "EMBEDDING_RELATIONSHIPS_DIMENSION": "3", + "REDIS_HOST": "localhost", + "REDIS_PORT": "6379", + "NEO4J_HOST": "localhost", + "NEO4J_PORT": "7687", + "NEO4J_USERNAME": "neo4j", + "NEO4J_PASSWORD": "password", + "MILVUS_HOST": "localhost", + "MILVUS_PORT": "19530", + "MONGO_CONNECTION_STRING": "mongodb://localhost:27017", + "CELERY_WORKER_CONCURRENCY": "1", + "OLLAMA_HOST": "localhost", + "OLLAMA_PORT": "11434", + "OLLAMA_LLM_SMALL_MODEL": "small", + "OLLAMA_LLM_LARGE_MODEL": "large", + "POSTGRES_HOST": "localhost", + "POSTGRES_PORT": "5432", + "POSTGRES_USERNAME": "postgres", + "POSTGRES_PASSWORD": "postgres", +} +for key, value in ENV_DEFAULTS.items(): + os.environ.setdefault(key, value) + +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from src.constants.embeddings import Vector +from src.constants.kg import Node, Predicate +from src.services.api.constants.requests import GetContextRequestBody +from src.utils.nlp.ner import ExtractElementsResponse + + +def _node(uuid: str, name: str, *, labels=None): + return Node( + uuid=uuid, + name=name, + labels=labels or ["PERSON"], + description=f"desc-{name}", + ) + + +def _pred(uuid: str, name: str): + return Predicate(uuid=uuid, name=name, description=f"desc-{name}") + + +class ContextPathNoDossierTests(unittest.IsolatedAsyncioTestCase): + async def test_get_context_does_not_invoke_dossier_retrieval(self): + from src.services.api.controllers import retrieve as retrieve_mod + + self.assertFalse(hasattr(retrieve_mod, "EventSynergyRetriever")) + self.assertFalse(hasattr(retrieve_mod, "_flatten_match_path")) + self.assertFalse(hasattr(retrieve_mod, "_MAX_DOSSIER_ENTITIES")) + + fact = ( + _node("a", "Alice"), + _pred("r1", "ATTENDED"), + _node("e1", "Event1", labels=["EVENT"]), + _pred("r2", "WITH"), + _node("b", "Bob"), + ) + mock_embeddings = MagicMock() + mock_embeddings.embed_text.return_value = Vector( + id="q", embeddings=[0.1, 0.2, 0.3], metadata={} + ) + mock_vs = MagicMock() + mock_vs.search_nodes.return_value = [ + Vector( + id="n1", + embeddings=[0.1, 0.2, 0.3], + metadata={"uuid": "a", "name": "Alice"}, + distance=0.1, + ) + ] + mock_vs.search_relationships.return_value = [] + mock_vs.search_data.return_value = [] + mock_graph = MagicMock() + mock_graph.get_event_centric_neighbors.return_value = [fact] + mock_data = MagicMock() + mock_data.get_last_text_chunks.return_value = [] + mock_data.get_last_structured_data.return_value = [] + mock_data.get_text_chunks_by_ids.return_value = ([], None) + mock_data.search.return_value = MagicMock(text_chunks=[]) + + with contextlib.ExitStack() as stack: + stack.enter_context( + patch.object(retrieve_mod, "embeddings_adapter", mock_embeddings) + ) + stack.enter_context(patch.object(retrieve_mod, "vector_search", mock_vs)) + stack.enter_context(patch.object(retrieve_mod, "graph_adapter", mock_graph)) + stack.enter_context(patch.object(retrieve_mod, "data_adapter", mock_data)) + stack.enter_context( + patch.object( + retrieve_mod._entity_extractor, + "extract_elements", + return_value=ExtractElementsResponse( + tokens=[{"text": "Alice", "lemma": "Alice", "pos": "PROPN"}], + noun_chunks=[], + ), + ) + ) + synergy = stack.enter_context( + patch("src.core.search.entity_info.EventSynergyRetriever") + ) + response = await retrieve_mod.get_context( + GetContextRequestBody( + text="What did Alice do with Bob?", + brain_id="brain-a", + max_facts=5, + max_passages=2, + profile_stages=True, + ) + ) + + synergy.assert_not_called() + self.assertFalse( + any( + line.startswith("[dossier:") + for line in response.text_context.splitlines() + ) + ) + stage_names = { + stage["stage"] for stage in (response.stage_timings or {}).get("stages", []) + } + self.assertFalse(any(name.startswith("dossiers") for name in stage_names)) + self.assertEqual(len(response.triples), 1) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_context_retrieval.py b/tests/test_context_retrieval.py new file mode 100644 index 0000000000000000000000000000000000000000..d7c470967451d50cd015f32a23317d5bbec656be --- /dev/null +++ b/tests/test_context_retrieval.py @@ -0,0 +1,583 @@ +import asyncio +import os +import sys +import types +import unittest +from types import SimpleNamespace +from unittest.mock import MagicMock, patch + + +ENV_DEFAULTS = { + "BRAINPAT_TOKEN": "test-token", + "MODELS_MODE": "local", + "EMBEDDINGS_LOCAL_MODEL": "local-model", + "EMBEDDINGS_SMALL_MODEL": "small-model", + "EMBEDDING_NODES_DIMENSION": "3", + "EMBEDDING_TRIPLETS_DIMENSION": "3", + "EMBEDDING_OBSERVATIONS_DIMENSION": "3", + "EMBEDDING_DATA_DIMENSION": "3", + "EMBEDDING_RELATIONSHIPS_DIMENSION": "3", + "REDIS_HOST": "localhost", + "REDIS_PORT": "6379", + "NEO4J_HOST": "localhost", + "NEO4J_PORT": "7687", + "NEO4J_USERNAME": "neo4j", + "NEO4J_PASSWORD": "password", + "MILVUS_HOST": "localhost", + "MILVUS_PORT": "19530", + "MONGO_CONNECTION_STRING": "mongodb://localhost:27017", + "CELERY_WORKER_CONCURRENCY": "1", + "OLLAMA_HOST": "localhost", + "OLLAMA_PORT": "11434", + "OLLAMA_LLM_SMALL_MODEL": "small", + "OLLAMA_LLM_LARGE_MODEL": "large", + "POSTGRES_HOST": "localhost", + "POSTGRES_PORT": "5432", + "POSTGRES_USERNAME": "postgres", + "POSTGRES_PASSWORD": "postgres", +} +for key, value in ENV_DEFAULTS.items(): + os.environ.setdefault(key, value) + +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from src.constants.embeddings import Vector +from src.constants.data import TextChunk +from src.constants.kg import Node, Predicate +from src.core.search.entity_info import EventSynergyRetriever +from src.services.api.constants.requests import GetContextRequestBody +from src.utils.nlp.ner import ExtractElementsResponse + + +def _node(uuid: str, name: str, *, happened_at: str | None = None, labels=None): + return Node( + uuid=uuid, + name=name, + labels=labels or ["PERSON"], + description=f"desc-{name}", + happened_at=happened_at, + ) + + +def _pred(uuid: str, name: str): + return Predicate(uuid=uuid, name=name, description=f"desc-{name}") + + +class CollectQueryVariantsTests(unittest.TestCase): + def test_includes_noun_chunks_deduped_and_capped(self): + from src.services.api.controllers import retrieve as retrieve_mod + + elements = ExtractElementsResponse( + tokens=[{"text": "Alice", "lemma": "Alice", "pos": "PROPN"}], + noun_chunks=[ + "closest friend", + "Alice", + "charity event", + "weekend trip", + "old school", + "extra chunk", + "another one", + ], + ) + variants = retrieve_mod._collect_query_variants( + "What did Alice do with her closest friend?", elements + ) + self.assertEqual(variants[0], "What did Alice do with her closest friend?") + self.assertIn("Alice", variants) + self.assertIn("closest friend", variants) + self.assertIn("charity event", variants) + noun_chunks_included = [ + chunk + for chunk in elements.noun_chunks + if chunk in variants and chunk != "Alice" + ] + self.assertLessEqual( + len(noun_chunks_included), retrieve_mod._MAX_NOUN_CHUNKS + ) + self.assertTrue( + any("friend" in v.lower() for v in variants[1:]), + "enumeration decomposition should add relation-focused subqueries", + ) + + +class FormatHelpersTests(unittest.TestCase): + def test_format_includes_happened_at(self): + from src.services.api.controllers import retrieve as retrieve_mod + + n = _node("a", "Alice") + r = _pred("r1", "ATTENDED") + m = _node("e", "Wedding", happened_at="2024-05-01", labels=["EVENT"]) + r2 = _pred("r2", "INVOLVED") + b = _node("b", "Bob") + line = retrieve_mod._format_event_fact(n, r, m, r2, b) + self.assertIn("@2024-05-01", line) + self.assertIn("Wedding", line) + + +class GetContextRetrievalTests(unittest.IsolatedAsyncioTestCase): + async def test_retains_all_event_facts_not_just_first(self): + from src.services.api.controllers import retrieve as retrieve_mod + + facts = [ + ( + _node("a", "Alice"), + _pred("r1", "ATTENDED"), + _node("e1", "Event1", happened_at="2024-01-01", labels=["EVENT"]), + _pred("r2", "WITH"), + _node("b", "Bob"), + ), + ( + _node("a", "Alice"), + _pred("r3", "VISITED"), + _node("e2", "Event2", happened_at="2024-02-01", labels=["EVENT"]), + _pred("r4", "IN"), + _node("c", "Paris", labels=["LOCATION"]), + ), + ( + _node("a", "Alice"), + _pred("r5", "BOUGHT"), + _node("e3", "Event3", labels=["EVENT"]), + _pred("r6", "ITEM"), + _node("d", "Book", labels=["OBJECT"]), + ), + ( + _node("a", "Alice"), + _pred("r7", "MET"), + _node("e4", "Event4", labels=["EVENT"]), + _pred("r8", "WITH"), + _node("f", "Carol"), + ), + ( + _node("a", "Alice"), + _pred("r9", "MOVED"), + _node("e5", "Event5", labels=["EVENT"]), + _pred("r10", "TO"), + _node("g", "London", labels=["LOCATION"]), + ), + ] + + mock_embeddings = MagicMock() + mock_embeddings.embed_text.return_value = Vector( + id="q", embeddings=[0.1, 0.2, 0.3], metadata={} + ) + + mock_vs = MagicMock() + mock_vs.search_nodes.return_value = [ + Vector( + id="n1", + embeddings=[0.1, 0.2, 0.3], + metadata={"uuid": "a", "name": "Alice"}, + distance=0.1, + ) + ] + mock_vs.search_relationships.return_value = [] + + mock_graph = MagicMock() + mock_graph.get_event_centric_neighbors.return_value = facts + + mock_data = MagicMock() + mock_data.get_last_text_chunks.return_value = [] + mock_data.get_last_structured_data.return_value = [] + + elements = ExtractElementsResponse(tokens=[], noun_chunks=[]) + + with ( + patch.object(retrieve_mod, "embeddings_adapter", mock_embeddings), + patch.object(retrieve_mod, "vector_search", mock_vs), + patch.object(retrieve_mod, "graph_adapter", mock_graph), + patch.object(retrieve_mod, "data_adapter", mock_data), + patch.object( + retrieve_mod._entity_extractor, + "extract_elements", + return_value=elements, + ), + ): + response = await retrieve_mod.get_context( + GetContextRequestBody(text="Alice", brain_id="brain-a", max_facts=40) + ) + + self.assertEqual(len(response.triples), 5) + self.assertIn("@2024-01-01", response.text_context) + self.assertIn("Paris", response.text_context) + + async def test_relationship_vector_search_seeds_facts(self): + from src.services.api.controllers import retrieve as retrieve_mod + + fact = ( + _node("tail", "Alice"), + _pred("r1", "ORGANIZED"), + _node("mid", "Fundraiser", happened_at="2023-11-11", labels=["EVENT"]), + _pred("r2", "FOR"), + _node("tip", "Charity", labels=["ORG"]), + ) + + mock_embeddings = MagicMock() + mock_embeddings.embed_text.return_value = Vector( + id="q", embeddings=[0.1, 0.2, 0.3], metadata={} + ) + + mock_vs = MagicMock() + mock_vs.search_nodes.return_value = [] + mock_vs.search_relationships.return_value = [ + Vector( + id="rel1", + embeddings=[0.1, 0.2, 0.3], + metadata={"uuid": "r1", "node_ids": ["tail", "mid"]}, + distance=0.05, + ) + ] + + mock_graph = MagicMock() + mock_graph.get_event_centric_neighbors.return_value = [fact] + + mock_data = MagicMock() + mock_data.get_last_text_chunks.return_value = [] + mock_data.get_last_structured_data.return_value = [] + + elements = ExtractElementsResponse(tokens=[], noun_chunks=[]) + + with ( + patch.object(retrieve_mod, "embeddings_adapter", mock_embeddings), + patch.object(retrieve_mod, "vector_search", mock_vs), + patch.object(retrieve_mod, "graph_adapter", mock_graph), + patch.object(retrieve_mod, "data_adapter", mock_data), + patch.object( + retrieve_mod._entity_extractor, + "extract_elements", + return_value=elements, + ), + ): + response = await retrieve_mod.get_context( + GetContextRequestBody( + text="who organized the fundraiser", + brain_id="brain-a", + ) + ) + + mock_graph.get_event_centric_neighbors.assert_called() + seed_uuids = mock_graph.get_event_centric_neighbors.call_args[0][0] + self.assertIn("tail", seed_uuids) + self.assertIn("mid", seed_uuids) + self.assertEqual(len(response.triples), 1) + self.assertIn("Fundraiser", response.text_context) + + async def test_max_facts_cap_and_dedup(self): + from src.services.api.controllers import retrieve as retrieve_mod + + shared = ( + _node("a", "Alice"), + _pred("r1", "ATTENDED"), + _node("e1", "Event1", labels=["EVENT"]), + _pred("r2", "WITH"), + _node("b", "Bob"), + ) + other = ( + _node("a", "Alice"), + _pred("r3", "VISITED"), + _node("e2", "Event2", labels=["EVENT"]), + _pred("r4", "IN"), + _node("c", "Paris", labels=["LOCATION"]), + ) + + mock_embeddings = MagicMock() + mock_embeddings.embed_text.return_value = Vector( + id="q", embeddings=[0.1, 0.2, 0.3], metadata={} + ) + mock_vs = MagicMock() + mock_vs.search_nodes.return_value = [ + Vector( + id="n1", + embeddings=[0.1, 0.2, 0.3], + metadata={"uuid": "a", "name": "Alice"}, + distance=0.2, + ) + ] + mock_vs.search_relationships.return_value = [] + mock_graph = MagicMock() + mock_graph.get_event_centric_neighbors.return_value = [shared, shared, other] + mock_data = MagicMock() + mock_data.get_last_text_chunks.return_value = [] + mock_data.get_last_structured_data.return_value = [] + elements = ExtractElementsResponse(tokens=[], noun_chunks=[]) + + with ( + patch.object(retrieve_mod, "embeddings_adapter", mock_embeddings), + patch.object(retrieve_mod, "vector_search", mock_vs), + patch.object(retrieve_mod, "graph_adapter", mock_graph), + patch.object(retrieve_mod, "data_adapter", mock_data), + patch.object( + retrieve_mod._entity_extractor, + "extract_elements", + return_value=elements, + ), + ): + response = await retrieve_mod.get_context( + GetContextRequestBody(text="Alice", brain_id="brain-a", max_facts=1) + ) + + self.assertEqual(len(response.triples), 1) + + async def test_context_path_does_not_emit_dossier_lines(self): + from src.services.api.controllers import retrieve as retrieve_mod + + fact = ( + _node("a", "Alice"), + _pred("r1", "KNOWS"), + _node("e1", "Meeting", labels=["EVENT"]), + _pred("r2", "WITH"), + _node("b", "Bob"), + ) + + mock_embeddings = MagicMock() + mock_embeddings.embed_text.return_value = Vector( + id="q", embeddings=[0.1, 0.2, 0.3], metadata={} + ) + mock_vs = MagicMock() + mock_vs.search_nodes.return_value = [ + Vector( + id="n1", + embeddings=[0.1, 0.2, 0.3], + metadata={"uuid": "a", "name": "Alice"}, + distance=0.1, + ) + ] + mock_vs.search_relationships.return_value = [] + mock_graph = MagicMock() + mock_graph.get_event_centric_neighbors.return_value = [fact] + mock_data = MagicMock() + mock_data.get_last_text_chunks.return_value = [] + mock_data.get_last_structured_data.return_value = [] + elements = ExtractElementsResponse( + tokens=[{"text": "Alice", "lemma": "Alice", "pos": "PROPN"}], + noun_chunks=[], + ) + + with ( + patch.object(retrieve_mod, "embeddings_adapter", mock_embeddings), + patch.object(retrieve_mod, "vector_search", mock_vs), + patch.object(retrieve_mod, "graph_adapter", mock_graph), + patch.object(retrieve_mod, "data_adapter", mock_data), + patch.object( + retrieve_mod._entity_extractor, + "extract_elements", + return_value=elements, + ), + patch( + "src.core.search.entity_info.EventSynergyRetriever" + ) as synergy_cls, + ): + response = await retrieve_mod.get_context( + GetContextRequestBody( + text="Where does Alice's friend live?", + brain_id="brain-a", + ) + ) + + synergy_cls.assert_not_called() + self.assertFalse( + any(line.startswith("[dossier:") for line in response.text_context.splitlines()) + ) + self.assertEqual(len(response.triples), 1) + + +class EventSynergyHardeningTests(unittest.TestCase): + def test_explores_top_k_branches_not_only_greedy_best(self): + alice = _node("a", "Alice") + wrong = _node("w", "Wrong") + mid = _node("m", "Bridge") + goal = _node("g", "Goal") + rel_wrong = _pred("rw", "DISTRACTOR") + rel_wrong.properties = {"v_id": "v_wrong"} + rel_bridge = _pred("rb", "VIA") + rel_bridge.properties = {"v_id": "v_bridge"} + rel_goal = _pred("rg", "REACHES") + rel_goal.properties = {"v_id": "v_goal"} + + neighbors = { + "a": [(rel_wrong, wrong), (rel_bridge, mid)], + "m": [(rel_goal, goal)], + "w": [], + "g": [], + } + + embeddings = { + "v_wrong": [1.0, 0.0, 0.0], + "v_bridge": [0.8, 0.2, 0.0], + "v_goal": [0.0, 1.0, 0.0], + } + query_embedding = [0.0, 1.0, 0.0] + + def fake_get_by_ids(ids, store="relationships", brain_id="default"): + vid = ids[0] + return [ + Vector(id=vid, embeddings=embeddings[vid], metadata={}) + ] + + with ( + patch( + "src.core.search.entity_info.graph_adapter.get_neighbors", + side_effect=lambda nodes, brain_id="default", **kwargs: { + nodes[0]: neighbors.get(nodes[0], []) + }, + ), + patch( + "src.core.search.entity_info.vector_store_adapter.get_by_ids", + side_effect=fake_get_by_ids, + ), + patch( + "src.core.search.entity_info.cosine_similarity", + side_effect=lambda a, b: sum(x * y for x, y in zip(a, b)), + ), + ): + retriever = EventSynergyRetriever("brain-a") + path = retriever._recursive_explorer( + "a", + query_embedding, + depth=3, + visited_ids=set(), + work_counter=[0], + branch_factor=3, + ) + + self.assertGreaterEqual(len(path), 2) + hop_names = [hop[0][0].name for hop in path] + self.assertIn("VIA", hop_names) + self.assertIn("REACHES", hop_names) + + def test_work_cap_stops_exploration(self): + nodes = {f"n{i}": _node(f"n{i}", f"N{i}") for i in range(20)} + rels = {} + for i in range(19): + pred = _pred(f"r{i}", f"REL{i}") + pred.properties = {"v_id": f"v{i}"} + rels[f"n{i}"] = [(pred, nodes[f"n{i+1}"])] + rels["n19"] = [] + + with ( + patch( + "src.core.search.entity_info.graph_adapter.get_neighbors", + side_effect=lambda nodes_arg, brain_id="default", **kwargs: { + nodes_arg[0]: rels.get(nodes_arg[0], []) + }, + ), + patch( + "src.core.search.entity_info.vector_store_adapter.get_by_ids", + side_effect=lambda ids, store="relationships", brain_id="default": [ + Vector(id=ids[0], embeddings=[1.0, 0.0], metadata={}) + ], + ), + patch( + "src.core.search.entity_info.cosine_similarity", + return_value=0.5, + ), + patch("src.core.search.entity_info._MAX_EXPLORATION_WORK", 3), + ): + retriever = EventSynergyRetriever("brain-a") + work = [0] + retriever._recursive_explorer( + "n0", + [1.0, 0.0], + depth=20, + visited_ids=set(), + work_counter=work, + branch_factor=1, + ) + self.assertLessEqual(work[0], 3) + + +class ContextPassageModeTests(unittest.TestCase): + def test_search_off_keeps_ilike_lexical_leg(self): + from src.services.api.controllers import retrieve as retrieve_mod + + mock_embeddings = MagicMock() + mock_embeddings.embed_text.return_value = Vector( + id="q", embeddings=[0.1], metadata={} + ) + mock_vs = MagicMock() + mock_vs.search_data.return_value = [ + Vector(id="d", metadata={"resource_id": "dense-1"}, distance=0.2) + ] + ilike_chunk = TextChunk(id="ilike-1", text="ilike hit about license") + mock_data = MagicMock() + mock_data.search.return_value = SimpleNamespace(text_chunks=[ilike_chunk]) + mock_data.get_text_chunks_by_ids.return_value = ( + [TextChunk(id="dense-1", text="dense body")], + [], + ) + + with ( + patch.object(retrieve_mod, "embeddings_adapter", mock_embeddings), + patch.object(retrieve_mod, "vector_search", mock_vs), + patch.object(retrieve_mod, "data_adapter", mock_data), + patch.object(retrieve_mod.config, "search_enabled", False), + patch.object(retrieve_mod.config, "context_passage_mode", "hybrid"), + ): + hits = retrieve_mod._retrieve_passages("license", "brain-a", limit=8) + + mock_data.search.assert_called() + mock_data.search_bm25.assert_not_called() + self.assertTrue(any(item[0] == "ilike-1" for item in hits)) + + def test_search_on_hybrid_uses_bm25(self): + from src.services.api.controllers import retrieve as retrieve_mod + + mock_embeddings = MagicMock() + mock_embeddings.embed_text.return_value = Vector( + id="q", embeddings=[0.1], metadata={} + ) + mock_vs = MagicMock() + mock_vs.search_data.return_value = [ + Vector(id="d", metadata={"resource_id": "dense-1"}, distance=0.2) + ] + bm25_chunk = TextChunk(id="bm25-1", text="bm25 hit about license") + mock_data = MagicMock() + mock_data.search_bm25.return_value = [(bm25_chunk, 5.0)] + mock_data.get_text_chunks_by_ids.return_value = ( + [TextChunk(id="dense-1", text="dense body")], + [], + ) + + with ( + patch.object(retrieve_mod, "embeddings_adapter", mock_embeddings), + patch.object(retrieve_mod, "vector_search", mock_vs), + patch.object(retrieve_mod, "data_adapter", mock_data), + patch.object(retrieve_mod.config, "search_enabled", True), + patch.object(retrieve_mod.config, "context_passage_mode", "hybrid"), + ): + hits = retrieve_mod._retrieve_passages("license", "brain-a", limit=8) + + mock_data.search_bm25.assert_called() + mock_data.search.assert_not_called() + ids = [item[0] for item in hits] + self.assertIn("bm25-1", ids) + self.assertIn("dense-1", ids) + + def test_search_on_ilike_freezes_old_lexical_leg(self): + from src.services.api.controllers import retrieve as retrieve_mod + + mock_embeddings = MagicMock() + mock_embeddings.embed_text.return_value = Vector( + id="q", embeddings=[0.1], metadata={} + ) + mock_vs = MagicMock() + mock_vs.search_data.return_value = [] + ilike_chunk = TextChunk(id="ilike-2", text="legacy ilike") + mock_data = MagicMock() + mock_data.search.return_value = SimpleNamespace(text_chunks=[ilike_chunk]) + + with ( + patch.object(retrieve_mod, "embeddings_adapter", mock_embeddings), + patch.object(retrieve_mod, "vector_search", mock_vs), + patch.object(retrieve_mod, "data_adapter", mock_data), + patch.object(retrieve_mod.config, "search_enabled", True), + patch.object(retrieve_mod.config, "context_passage_mode", "ilike"), + ): + hits = retrieve_mod._retrieve_passages("license", "brain-a", limit=8) + + mock_data.search.assert_called() + mock_data.search_bm25.assert_not_called() + self.assertEqual(hits[0][0], "ilike-2") + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_dates.py b/tests/test_dates.py new file mode 100644 index 0000000000000000000000000000000000000000..eaf1ea94b0c3b421b4eb444d20ed9ed3bec675e8 --- /dev/null +++ b/tests/test_dates.py @@ -0,0 +1,178 @@ +import os +import sys +import unittest +from datetime import datetime, timedelta, timezone + +ENV_DEFAULTS = { + "BRAINPAT_TOKEN": "test-token", + "MODELS_MODE": "local", + "EMBEDDINGS_LOCAL_MODEL": "local-model", + "EMBEDDINGS_SMALL_MODEL": "small-model", + "EMBEDDING_NODES_DIMENSION": "3", + "EMBEDDING_TRIPLETS_DIMENSION": "3", + "EMBEDDING_OBSERVATIONS_DIMENSION": "3", + "EMBEDDING_DATA_DIMENSION": "3", + "EMBEDDING_RELATIONSHIPS_DIMENSION": "3", + "REDIS_HOST": "localhost", + "REDIS_PORT": "6379", + "NEO4J_HOST": "localhost", + "NEO4J_PORT": "7687", + "NEO4J_USERNAME": "neo4j", + "NEO4J_PASSWORD": "password", + "MILVUS_HOST": "localhost", + "MILVUS_PORT": "19530", + "MONGO_CONNECTION_STRING": "mongodb://localhost:27017", + "CELERY_WORKER_CONCURRENCY": "1", + "OLLAMA_HOST": "localhost", + "OLLAMA_PORT": "11434", + "OLLAMA_LLM_SMALL_MODEL": "small", + "OLLAMA_LLM_LARGE_MODEL": "large", + "POSTGRES_HOST": "localhost", + "POSTGRES_PORT": "5432", + "POSTGRES_USERNAME": "postgres", + "POSTGRES_PASSWORD": "postgres", +} +for key, value in ENV_DEFAULTS.items(): + os.environ.setdefault(key, value) + +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from src.utils.dates import ( + normalize_date_string, + parse_date_string, + resolve_relative_date, + to_naive_utc, +) + +LOCOMO_TIMESTAMP = "1:56 pm on 8 May, 2023" + + +class ParseDateStringTests(unittest.TestCase): + def test_locomo_session_timestamp_parses(self): + self.assertEqual( + parse_date_string(LOCOMO_TIMESTAMP), + datetime(2023, 5, 8, 13, 56), + ) + + def test_locomo_morning_timestamp_parses(self): + self.assertEqual( + parse_date_string("10:37 am on 27 June, 2023"), + datetime(2023, 6, 27, 10, 37), + ) + + def test_iso_variants_parse(self): + expected = datetime(2023, 5, 8, 13, 56) + for value in ( + "2023-05-08T13:56:00", + "2023-05-08T13:56:00.000000", + "2023-05-08T13:56:00Z", + "2023-05-08T13:56:00+00:00", + "2023-05-08 13:56:00", + "2023-05-08 13:56:00.000000", + "2023-05-08 13:56:00+00:00", + ): + with self.subTest(value=value): + self.assertEqual(parse_date_string(value), expected) + + def test_offset_aware_input_is_converted_to_naive_utc(self): + parsed = parse_date_string("2023-05-08T13:56:00+02:00") + self.assertEqual(parsed, datetime(2023, 5, 8, 11, 56)) + self.assertIsNone(parsed.tzinfo) + + def test_parsed_value_never_breaks_subtraction(self): + parsed = parse_date_string("2023-05-08T13:56:00-05:00") + self.assertIsInstance(datetime.now() - parsed, timedelta) + + def test_ambiguous_numeric_date_is_day_first(self): + self.assertEqual(parse_date_string("08/05/2023"), datetime(2023, 5, 8)) + self.assertEqual(normalize_date_string("08/05/2023"), "08/05/2023") + + def test_day_first_holds_for_unambiguous_high_day(self): + self.assertEqual(parse_date_string("25/05/2023"), datetime(2023, 5, 25)) + + def test_unparseable_value_returns_none(self): + self.assertIsNone(parse_date_string("sometime last summer")) + + def test_previously_supported_formats_still_parse(self): + expected = datetime(2023, 5, 8) + for value in ("08/05/2023", "2023-05-08", "2023/05/08", "08-05-2023", + "May 8, 2023", "May 8 2023", "8 May 2023"): + with self.subTest(value=value): + self.assertEqual(parse_date_string(value), expected) + + +class NormalizeDateStringTests(unittest.TestCase): + def test_output_format_is_day_first_slashed(self): + self.assertEqual(normalize_date_string(LOCOMO_TIMESTAMP), "08/05/2023") + self.assertEqual(normalize_date_string("2023-05-08T13:56:00Z"), "08/05/2023") + self.assertEqual(normalize_date_string("2023-05-08"), "08/05/2023") + + def test_unparseable_value_passes_through_stripped(self): + self.assertEqual( + normalize_date_string(" sometime last summer "), + "sometime last summer", + ) + + +class ResolveRelativeDateTests(unittest.TestCase): + def test_yesterday_against_locomo_timestamp(self): + self.assertEqual( + resolve_relative_date("yesterday", LOCOMO_TIMESTAMP), + "07/05/2023", + ) + + def test_last_tuesday_against_locomo_timestamp(self): + self.assertEqual( + resolve_relative_date("last Tuesday", LOCOMO_TIMESTAMP), + "02/05/2023", + ) + + def test_days_ago_against_locomo_timestamp(self): + self.assertEqual( + resolve_relative_date("3 days ago", LOCOMO_TIMESTAMP), + "05/05/2023", + ) + + def test_weeks_ago_against_locomo_timestamp(self): + self.assertEqual( + resolve_relative_date("2 weeks ago", LOCOMO_TIMESTAMP), + "24/04/2023", + ) + + def test_unparseable_expression_returns_input_unchanged(self): + self.assertEqual( + resolve_relative_date("a few months ago", LOCOMO_TIMESTAMP), + "a few months ago", + ) + + def test_unparseable_reference_returns_input_unchanged(self): + self.assertEqual( + resolve_relative_date("yesterday", "whenever it was"), + "yesterday", + ) + + def test_next_month_on_century_non_leap_year_does_not_crash(self): + self.assertEqual( + resolve_relative_date("next month", "2100-01-31"), + "28/02/2100", + ) + + def test_last_month_on_century_non_leap_year_does_not_crash(self): + self.assertEqual( + resolve_relative_date("last month", "2100-03-31"), + "28/02/2100", + ) + + +class ToNaiveUtcTests(unittest.TestCase): + def test_naive_value_is_returned_unchanged(self): + value = datetime(2023, 5, 8, 13, 56) + self.assertIs(to_naive_utc(value), value) + + def test_aware_value_is_shifted_to_utc(self): + value = datetime(2023, 5, 8, 13, 56, tzinfo=timezone(timedelta(hours=2))) + self.assertEqual(to_naive_utc(value), datetime(2023, 5, 8, 11, 56)) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_deploy_production_contract.py b/tests/test_deploy_production_contract.py new file mode 100644 index 0000000000000000000000000000000000000000..17201838a461483b31b67d2ed24c5fac508c2062 --- /dev/null +++ b/tests/test_deploy_production_contract.py @@ -0,0 +1,205 @@ +from __future__ import annotations + +import hashlib +import json +import os +import shutil +import subprocess +from pathlib import Path + +import pytest +import yaml + +from deploy.backup_restore import verify_backup +from scripts.check_release_readiness import _check_latency +from scripts.openai_ci_stub import _embedding +from scripts.production_smoke import _latency_summary + + +ROOT = Path(__file__).resolve().parents[1] +DEPLOY = ROOT / "deploy" + + +@pytest.mark.parametrize("profile", ["light", "heavy"]) +def test_only_nginx_publishes_ports(profile): + data = yaml.safe_load( + (DEPLOY / f"docker-compose.{profile}.yaml").read_text(encoding="utf-8") + ) + published = { + service: config["ports"] + for service, config in data["services"].items() + if config.get("ports") + } + assert published == {"nginx": ["80:80", "443:443"]} + assert "version" not in data + + +def _compose_env(profile: str) -> dict[str, str]: + env = { + **os.environ, + "BRAINAPI_IMAGE": "brainapi:test", + "BRAINAPI_ENV_FILE": str(DEPLOY / f"env.{profile}.example"), + "REDIS_PASSWORD": "redis-secret", + } + if profile == "light": + env["POSTGRES_PASSWORD"] = "postgres-secret" + else: + env.update( + { + "NEO4J_PASSWORD": "neo-secret", + "MONGO_PASSWORD": "mongo-secret", + "MINIO_ACCESS_KEY": "minio-access", + "MINIO_SECRET_KEY": "minio-secret", + "MILVUS_TOKEN": "root:milvus-secret", + "MILVUS_ROOT_PASSWORD": "milvus-secret", + } + ) + return env + + +@pytest.mark.skipif(shutil.which("docker") is None, reason="Docker unavailable") +@pytest.mark.parametrize("profile", ["light", "heavy"]) +def test_compose_profiles_render_with_escaped_healthchecks(profile): + result = subprocess.run( + [ + "docker", + "compose", + "-f", + str(DEPLOY / f"docker-compose.{profile}.yaml"), + "config", + "--format", + "json", + ], + env=_compose_env(profile), + text=True, + capture_output=True, + check=True, + ) + config = json.loads(result.stdout) + assert config["services"]["redis"]["healthcheck"]["test"][1].find( + "REDIS_PASSWORD" + ) >= 0 + assert set(config["services"]["nginx"]["ports"][0]) >= {"target", "published"} + + +@pytest.mark.skipif(shutil.which("docker") is None, reason="Docker unavailable") +def test_compose_rejects_missing_required_secret(): + env = _compose_env("light") + env.pop("REDIS_PASSWORD") + result = subprocess.run( + ["docker", "compose", "-f", str(DEPLOY / "docker-compose.light.yaml"), "config", "-q"], + env=env, + text=True, + capture_output=True, + ) + assert result.returncode != 0 + assert "REDIS_PASSWORD is required" in result.stderr + + +def test_nginx_routes_console_api_and_mcp(): + config = (DEPLOY / "nginx" / "conf.d" / "brainapi.conf").read_text( + encoding="utf-8" + ) + assert "location /mcp" in config + assert "location /" in config + assert "brainapi_api" in config + assert "brainapi_mcp" in config + + +def test_backup_manifest_verification_detects_tampering(tmp_path): + artifact = tmp_path / "postgres.dump" + artifact.write_bytes(b"consistent") + manifest = { + "format_version": 1, + "profile": "light", + "checksums": { + artifact.name: hashlib.sha256(artifact.read_bytes()).hexdigest() + }, + } + (tmp_path / "manifest.json").write_text(json.dumps(manifest), encoding="utf-8") + verify_backup(tmp_path, "light") + artifact.write_bytes(b"tampered") + with pytest.raises(RuntimeError, match="Checksum mismatch"): + verify_backup(tmp_path, "light") + + +def test_light_backup_includes_per_brain_postgres_databases(): + source = (DEPLOY / "backup_restore.py").read_text(encoding="utf-8") + assert "postgres-brains.tar.gz" in source + assert "brain_*.dump" in source + + +def test_recommendation_openapi_surface_is_preview(): + from src.services.api.app import app + + schema = app.openapi() + assert schema["paths"]["/retrieve/recommend"]["get"]["x-stability"] == "preview" + assert schema["paths"]["/retrieve/recommend"]["post"]["x-stability"] == "preview" + + +def test_production_smoke_percentiles_are_deterministic(): + assert _latency_summary([1, 2, 3, 4, 5]) == { + "samples": 5, + "p50_ms": 3, + "p95_ms": 4.8, + "p99_ms": 4.96, + } + + +def test_product_state_artifact_never_persists_a_brain_token(): + source = (ROOT / "scripts" / "production_smoke.py").read_text(encoding="utf-8") + state_block = source.split('state = {', 1)[1].split('}', 1)[0] + assert '"brain_token"' not in state_block + + +def test_release_latency_gate_requires_both_profiles(): + result = { + "context": { + "p50_ms": 999, + "p95_ms": 1200, + "p99_ms": 1500, + "online_llm_retrieval_loops": 0, + }, + "search": { + "p50_ms": 199, + "p95_ms": 250, + "p99_ms": 300, + "excludes_embed_query": True, + }, + } + _check_latency({"light": result, "heavy": result}) + with pytest.raises(RuntimeError, match="missing the heavy"): + _check_latency({"light": result}) + + +def test_ci_embedding_stub_is_deterministic_and_normalized(): + first = _embedding("same input", dimensions=8) + assert first == _embedding("same input", dimensions=8) + assert first != _embedding("different input", dimensions=8) + assert sum(value * value for value in first) == pytest.approx(1.0) + + +def test_release_workflow_enforces_artifact_gate_before_publish(): + workflow = (ROOT / ".github" / "workflows" / "release.yaml").read_text( + encoding="utf-8" + ) + assert "check_release_readiness.py gate-artifacts" in workflow + assert "needs: [validate-tag, verify-required-checks]" in workflow + assert 'python scripts/release_version.py "$GITHUB_REF_NAME"' in workflow + assert "steps.tag.outputs.project_version" in workflow + + +def test_mcp_has_only_one_lifespan_definition(): + source = (ROOT / "src" / "services" / "mcp" / "app.py").read_text( + encoding="utf-8" + ) + assert source.count("async def _lifespan") == 1 + + +def test_ci_profiles_enable_dense_search_without_online_llm(): + override = yaml.safe_load((DEPLOY / "docker-compose.ci.yaml").read_text()) + environment = override["x-ci-model-environment"] + assert environment["SEARCH_ENABLED"] == "true" + assert environment["SEARCH_USE_DENSE"] == "true" + assert environment["SEARCH_USE_BM25"] == "false" + assert environment["CONTEXT_PASSAGE_MODE"] == "dense" diff --git a/tests/test_entity_synergies.py b/tests/test_entity_synergies.py new file mode 100644 index 0000000000000000000000000000000000000000..b66da77b32b04f3204f8077f2fcc6ee8f3dc4bd8 --- /dev/null +++ b/tests/test_entity_synergies.py @@ -0,0 +1,138 @@ +import os +import unittest +from unittest.mock import MagicMock, patch + +ENV_DEFAULTS = { + "BRAINPAT_TOKEN": "test-token", + "MODELS_MODE": "local", + "EMBEDDINGS_LOCAL_MODEL": "local-model", + "EMBEDDINGS_SMALL_MODEL": "small-model", + "EMBEDDING_NODES_DIMENSION": "3", + "EMBEDDING_TRIPLETS_DIMENSION": "3", + "EMBEDDING_OBSERVATIONS_DIMENSION": "3", + "EMBEDDING_DATA_DIMENSION": "3", + "EMBEDDING_RELATIONSHIPS_DIMENSION": "3", + "REDIS_HOST": "localhost", + "REDIS_PORT": "6379", + "NEO4J_HOST": "localhost", + "NEO4J_PORT": "7687", + "NEO4J_USERNAME": "neo4j", + "NEO4J_PASSWORD": "password", + "MILVUS_HOST": "localhost", + "MILVUS_PORT": "19530", + "MONGO_CONNECTION_STRING": "mongodb://localhost:27017", + "CELERY_WORKER_CONCURRENCY": "1", + "OLLAMA_HOST": "localhost", + "OLLAMA_PORT": "11434", + "OLLAMA_LLM_SMALL_MODEL": "small", + "OLLAMA_LLM_LARGE_MODEL": "large", + "PIPELINE_MODE": "accurate", +} +for key, value in ENV_DEFAULTS.items(): + os.environ.setdefault(key, value) + + +class PolarityMatchesTests(unittest.TestCase): + def test_same_matching_and_neutral(self): + from src.core.search.entity_sibilings import polarity_matches + + self.assertTrue(polarity_matches("positive", "positive", "same")) + self.assertTrue(polarity_matches(None, None, "same")) + self.assertTrue(polarity_matches("neutral", None, "same")) + self.assertFalse(polarity_matches("positive", "negative", "same")) + + def test_opposite_pos_neg_only(self): + from src.core.search.entity_sibilings import polarity_matches + + self.assertTrue(polarity_matches("positive", "negative", "opposite")) + self.assertTrue(polarity_matches("negative", "positive", "opposite")) + self.assertFalse(polarity_matches("positive", "positive", "opposite")) + self.assertFalse(polarity_matches(None, "negative", "opposite")) + self.assertFalse(polarity_matches("neutral", "positive", "opposite")) + + +class SynergyTopKAndValidityTests(unittest.TestCase): + def test_predicate_validity(self): + from src.constants.kg import Predicate + from src.core.search.entity_sibilings import _predicate_currently_valid + + ok = Predicate.model_construct( + uuid="p1", name="MADE", properties={}, deprecated=False + ) + self.assertTrue(_predicate_currently_valid(ok)) + bad = Predicate.model_construct( + uuid="p2", + name="MADE", + properties={"invalid_at": "2024-01-01"}, + deprecated=False, + ) + self.assertFalse(_predicate_currently_valid(bad)) + dep = Predicate.model_construct( + uuid="p3", name="MADE", properties={}, deprecated=True + ) + self.assertFalse(_predicate_currently_valid(dep)) + + def test_top_k_bounds_results(self): + from src.constants.kg import EntitySynergy, Node + from src.core.search import entity_sibilings as mod + + target = Node.model_construct( + uuid="t1", + name="Widget", + labels=["PRODUCT"], + polarity="positive", + properties={"v_id": "tv"}, + ) + candidates = [ + EntitySynergy( + node=Node.model_construct( + uuid=f"c{i}", + name=f"C{i}", + labels=["PRODUCT"], + polarity="positive", + properties={}, + ), + connected_by=[target], + association_score=float(10 - i), + ) + for i in range(5) + ] + + retriever = mod.EntitySinergyRetriever("recsys-test") + + embed = MagicMock() + embed.embeddings = [0.1, 0.2, 0.3] + vs = MagicMock() + vs.metadata = {"uuid": "t1"} + + with ( + patch.object(mod, "embeddings_adapter") as emb, + patch.object(mod, "vector_search") as vsearch, + patch.object(mod, "graph_adapter") as graph, + patch.object(mod, "vector_store_adapter"), + ): + emb.embed_text.return_value = embed + vsearch.search_nodes.return_value = [vs] + graph.get_by_uuid.return_value = target + graph.get_neighbors.return_value = {"t1": []} + + def fake_retrieve(*args, **kwargs): + top_k = kwargs.get("top_k", 50) + ranked = sorted( + candidates, key=lambda x: x.association_score, reverse=True + ) + return target, ranked[:top_k], [], [] + + with patch.object( + retriever, "retrieve_sibilings", side_effect=fake_retrieve + ): + _, syn, _, _ = retriever.retrieve_sibilings( + "Widget", "same", top_k=2 + ) + self.assertEqual(len(syn), 2) + self.assertEqual(syn[0].node.uuid, "c0") + self.assertEqual(syn[1].node.uuid, "c1") + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_entity_synergy_recency.py b/tests/test_entity_synergy_recency.py new file mode 100644 index 0000000000000000000000000000000000000000..38c3311e3de65ff42244f98b215cbb7c4aed37f5 --- /dev/null +++ b/tests/test_entity_synergy_recency.py @@ -0,0 +1,97 @@ +import os +import sys +import unittest +from datetime import datetime, timedelta, timezone +from unittest.mock import patch + +ENV_DEFAULTS = { + "BRAINPAT_TOKEN": "test-token", + "MODELS_MODE": "local", + "EMBEDDINGS_LOCAL_MODEL": "local-model", + "EMBEDDINGS_SMALL_MODEL": "small-model", + "EMBEDDING_NODES_DIMENSION": "3", + "EMBEDDING_TRIPLETS_DIMENSION": "3", + "EMBEDDING_OBSERVATIONS_DIMENSION": "3", + "EMBEDDING_DATA_DIMENSION": "3", + "EMBEDDING_RELATIONSHIPS_DIMENSION": "3", + "REDIS_HOST": "localhost", + "REDIS_PORT": "6379", + "NEO4J_HOST": "localhost", + "NEO4J_PORT": "7687", + "NEO4J_USERNAME": "neo4j", + "NEO4J_PASSWORD": "password", + "MILVUS_HOST": "localhost", + "MILVUS_PORT": "19530", + "MONGO_CONNECTION_STRING": "mongodb://localhost:27017", + "CELERY_WORKER_CONCURRENCY": "1", + "OLLAMA_HOST": "localhost", + "OLLAMA_PORT": "11434", + "OLLAMA_LLM_SMALL_MODEL": "small", + "OLLAMA_LLM_LARGE_MODEL": "large", + "POSTGRES_HOST": "localhost", + "POSTGRES_PORT": "5432", + "POSTGRES_USERNAME": "postgres", + "POSTGRES_PASSWORD": "postgres", +} +for key, value in ENV_DEFAULTS.items(): + os.environ.setdefault(key, value) + +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from src.constants.kg import Node +from src.core.search.entity_info import _recency_score + + +def _node(happened_at=None): + return Node.model_construct( + uuid="node-1", + name="Dinner with Melanie", + labels=["EVENT"], + happened_at=happened_at, + ) + + +class RecencyScoreTests(unittest.TestCase): + def test_missing_happened_at_is_neutral(self): + self.assertEqual(_recency_score(_node()), 1.0) + + def test_stored_day_first_date_decays(self): + score = _recency_score(_node("08/05/2023")) + self.assertLess(score, 1.0) + self.assertGreater(score, 0.0) + + def test_locomo_timestamp_decays(self): + self.assertEqual( + _recency_score(_node("1:56 pm on 8 May, 2023")), + _recency_score(_node("08/05/2023")), + ) + + def test_older_event_scores_lower_than_newer_event(self): + older = _recency_score(_node("08/05/2013")) + newer = _recency_score(_node("08/05/2023")) + self.assertLess(older, newer) + self.assertLess(newer, 1.0) + + def test_today_is_neutral(self): + today = datetime.now().strftime("%d/%m/%Y") + self.assertEqual(_recency_score(_node(today)), 1.0) + + def test_datetime_happened_at_is_supported(self): + self.assertEqual( + _recency_score(_node(datetime(2023, 5, 8))), + _recency_score(_node("08/05/2023")), + ) + + def test_offset_aware_datetime_does_not_raise(self): + aware = datetime(2023, 5, 8, tzinfo=timezone(timedelta(hours=2))) + self.assertLess(_recency_score(_node(aware)), 1.0) + + def test_unparseable_happened_at_falls_back_to_neutral_and_logs(self): + with patch("builtins.print") as printed: + self.assertEqual(_recency_score(_node("sometime last summer")), 1.0) + self.assertTrue(printed.called) + self.assertIn("unparseable happened_at", printed.call_args[0][0]) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_event_hub_invalidation.py b/tests/test_event_hub_invalidation.py new file mode 100644 index 0000000000000000000000000000000000000000..6eafc4d5aac44e83b4be6c79af2ca75bc67bde26 --- /dev/null +++ b/tests/test_event_hub_invalidation.py @@ -0,0 +1,210 @@ +import os +import sys +import unittest +from unittest.mock import patch + + +ENV_DEFAULTS = { + "BRAINPAT_TOKEN": "test-token", + "MODELS_MODE": "local", + "EMBEDDINGS_LOCAL_MODEL": "local-model", + "EMBEDDINGS_SMALL_MODEL": "small-model", + "EMBEDDING_NODES_DIMENSION": "3", + "EMBEDDING_TRIPLETS_DIMENSION": "3", + "EMBEDDING_OBSERVATIONS_DIMENSION": "3", + "EMBEDDING_DATA_DIMENSION": "3", + "EMBEDDING_RELATIONSHIPS_DIMENSION": "3", + "REDIS_HOST": "localhost", + "REDIS_PORT": "6379", + "NEO4J_HOST": "localhost", + "NEO4J_PORT": "7687", + "NEO4J_USERNAME": "neo4j", + "NEO4J_PASSWORD": "password", + "MILVUS_HOST": "localhost", + "MILVUS_PORT": "19530", + "MONGO_CONNECTION_STRING": "mongodb://localhost:27017", + "CELERY_WORKER_CONCURRENCY": "1", + "OLLAMA_HOST": "localhost", + "OLLAMA_PORT": "11434", + "OLLAMA_LLM_SMALL_MODEL": "small", + "OLLAMA_LLM_LARGE_MODEL": "large", + "PIPELINE_MODE": "accurate", +} +for key, value in ENV_DEFAULTS.items(): + os.environ.setdefault(key, value) + +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from src.constants.agents import ArchitectAgentEntity, ArchitectAgentRelationship +from src.constants.kg import Node, Predicate + + +class _FakeGraphAdapter: + def __init__(self): + self.nodes = {} + self.edges = [] + self.updates = [] + + def add_node(self, uuid, name, node_type): + self.nodes[uuid] = Node(uuid=uuid, name=name, labels=[node_type]) + return self.nodes[uuid] + + def add_edge(self, rel_uuid, tail_uuid, name, tip_uuid): + predicate = Predicate(uuid=rel_uuid, name=name, description=f"desc-{name}") + self.edges.append((tail_uuid, predicate, tip_uuid)) + return predicate + + def predicate(self, rel_uuid): + for _, predicate, _ in self.edges: + if predicate.uuid == rel_uuid: + return predicate + raise KeyError(rel_uuid) + + def get_neighbors( + self, + nodes, + brain_id="default", + same_type_only=False, + limit=None, + of_types=None, + ): + uuids = [node if isinstance(node, str) else node.uuid for node in nodes] + neighbors = {uuid: [] for uuid in uuids} + for tail_uuid, predicate, tip_uuid in self.edges: + if tail_uuid in neighbors: + neighbors[tail_uuid].append( + ( + predicate.model_copy(update={"direction": "out"}), + self.nodes[tip_uuid], + ) + ) + if tip_uuid in neighbors: + neighbors[tip_uuid].append( + ( + predicate.model_copy(update={"direction": "in"}), + self.nodes[tail_uuid], + ) + ) + return neighbors + + def update_properties(self, uuid, kind, brain_id="default", new_properties=None): + self.updates.append((uuid, kind, dict(new_properties or {}))) + predicate = self.predicate(uuid) + predicate.properties = { + **(predicate.properties or {}), + **(new_properties or {}), + } + if (new_properties or {}).get("deprecated"): + predicate.deprecated = True + + +def _entity(uuid, name, entity_type): + return ArchitectAgentEntity(uuid=uuid, name=name, type=entity_type) + + +def _relationship(rel_uuid, tail, name, tip, valid_at=None): + return ArchitectAgentRelationship( + uuid=rel_uuid, + flow_key="flow-1", + tail=tail, + name=name, + description=f"desc-{name}", + tip=tip, + properties={"valid_at": valid_at} if valid_at else {}, + ) + + +class EventHubInvalidationTests(unittest.TestCase): + def test_second_event_by_same_actor_keeps_both_attributions(self): + from src.services.api.controllers import retrieve as retrieve_mod + from src.workers.tasks import ingestion as ingestion_mod + + graph = _FakeGraphAdapter() + graph.add_node("alice", "Alice", "PERSON") + graph.add_node("event-1", "Purchase", "EVENT") + graph.add_node("event-2", "Trip", "EVENT") + graph.add_edge("rel-1", "alice", "MADE", "event-1") + graph.add_edge("rel-2", "alice", "MADE", "event-2") + + relationship = _relationship( + "rel-2", + _entity("alice", "Alice", "PERSON"), + "MADE", + _entity("event-2", "Trip", "EVENT"), + valid_at="02/02/2024", + ) + + with patch.object(ingestion_mod, "graph_adapter", graph): + ingestion_mod._invalidate_superseded_relationships( + relationship, brain_id="brain-a" + ) + + self.assertEqual(graph.updates, []) + self.assertTrue(retrieve_mod._is_currently_valid(graph.predicate("rel-1"))) + self.assertTrue(retrieve_mod._is_currently_valid(graph.predicate("rel-2"))) + + def test_functional_attribute_edge_is_still_superseded(self): + from src.services.api.controllers import retrieve as retrieve_mod + from src.workers.tasks import ingestion as ingestion_mod + + graph = _FakeGraphAdapter() + graph.add_node("alice", "Alice", "PERSON") + graph.add_node("nyc", "New York City", "CITY") + graph.add_node("sf", "San Francisco", "CITY") + graph.add_edge("rel-1", "alice", "LIVES_IN", "nyc") + graph.add_edge("rel-2", "alice", "LIVES_IN", "sf") + + relationship = _relationship( + "rel-2", + _entity("alice", "Alice", "PERSON"), + "LIVES_IN", + _entity("sf", "San Francisco", "CITY"), + valid_at="02/02/2024", + ) + + with patch.object(ingestion_mod, "graph_adapter", graph): + ingestion_mod._invalidate_superseded_relationships( + relationship, brain_id="brain-a" + ) + + self.assertEqual( + graph.updates, + [ + ( + "rel-1", + "relationship", + {"invalid_at": "02/02/2024", "deprecated": True}, + ) + ], + ) + self.assertFalse(retrieve_mod._is_currently_valid(graph.predicate("rel-1"))) + self.assertTrue(retrieve_mod._is_currently_valid(graph.predicate("rel-2"))) + + def test_inbound_edge_on_the_subject_is_not_superseded(self): + from src.workers.tasks import ingestion as ingestion_mod + + graph = _FakeGraphAdapter() + graph.add_node("alice", "Alice", "PERSON") + graph.add_node("bob", "Bob", "PERSON") + graph.add_node("carol", "Carol", "PERSON") + graph.add_edge("rel-1", "bob", "MANAGES", "alice") + graph.add_edge("rel-2", "alice", "MANAGES", "carol") + + relationship = _relationship( + "rel-2", + _entity("alice", "Alice", "PERSON"), + "MANAGES", + _entity("carol", "Carol", "PERSON"), + valid_at="02/02/2024", + ) + + with patch.object(ingestion_mod, "graph_adapter", graph): + ingestion_mod._invalidate_superseded_relationships( + relationship, brain_id="brain-a" + ) + + self.assertEqual(graph.updates, []) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_graph_upsert_contract.py b/tests/test_graph_upsert_contract.py new file mode 100644 index 0000000000000000000000000000000000000000..25017e82bfacab23352b43393ad6aae6c108cd1f --- /dev/null +++ b/tests/test_graph_upsert_contract.py @@ -0,0 +1,248 @@ +import os +import unittest +from unittest.mock import MagicMock, patch + + +ENV_DEFAULTS = { + "BRAINPAT_TOKEN": "test-token", + "MODELS_MODE": "local", + "EMBEDDINGS_LOCAL_MODEL": "local-model", + "EMBEDDINGS_SMALL_MODEL": "small-model", + "EMBEDDING_NODES_DIMENSION": "3", + "EMBEDDING_TRIPLETS_DIMENSION": "3", + "EMBEDDING_OBSERVATIONS_DIMENSION": "3", + "EMBEDDING_DATA_DIMENSION": "3", + "EMBEDDING_RELATIONSHIPS_DIMENSION": "3", + "REDIS_HOST": "localhost", + "REDIS_PORT": "6379", + "NEO4J_HOST": "localhost", + "NEO4J_PORT": "7687", + "NEO4J_USERNAME": "neo4j", + "NEO4J_PASSWORD": "password", + "MILVUS_HOST": "localhost", + "MILVUS_PORT": "19530", + "MONGO_CONNECTION_STRING": "mongodb://localhost:27017", + "CELERY_WORKER_CONCURRENCY": "1", + "OLLAMA_HOST": "localhost", + "OLLAMA_PORT": "11434", + "OLLAMA_LLM_SMALL_MODEL": "small", + "OLLAMA_LLM_LARGE_MODEL": "large", + "PIPELINE_MODE": "accurate", +} +for key, value in ENV_DEFAULTS.items(): + os.environ.setdefault(key, value) + + +class NetworkXUpsertContractTests(unittest.TestCase): + def test_same_uuid_upsert_keeps_one_node_and_preserves_uuid(self): + from src.lib.postgresql.graph_store import _BrainGraph + + brain = _BrainGraph("brain-a") + first = brain.upsert_node( + ["PERSON"], + {"uuid": "alice-1", "name": "Alice"}, + {"uuid": "alice-1", "name": "Alice", "description": "v1"}, + ) + second = brain.upsert_node( + ["PERSON"], + {"uuid": "alice-1", "name": "Alice"}, + {"uuid": "alice-1", "name": "Alice", "description": "v2"}, + ) + self.assertEqual(first, "alice-1") + self.assertEqual(second, "alice-1") + self.assertEqual(len(list(brain.graph.nodes)), 1) + self.assertEqual(brain.node_data("alice-1")["uuid"], "alice-1") + self.assertEqual(brain.node_data("alice-1")["description"], "v1 | v2") + + def test_same_name_different_uuid_does_not_overwrite_existing_uuid(self): + from src.lib.postgresql.graph_store import _BrainGraph + + brain = _BrainGraph("brain-a") + brain.upsert_node( + ["PERSON"], + {"uuid": "alice-1", "name": "Alice"}, + {"uuid": "alice-1", "name": "Alice"}, + ) + brain.upsert_node( + ["PERSON"], + {"uuid": "alice-2", "name": "Alice"}, + {"uuid": "alice-2", "name": "Alice"}, + ) + self.assertEqual(len(list(brain.graph.nodes)), 2) + self.assertEqual(brain.node_data("alice-1")["uuid"], "alice-1") + self.assertEqual(brain.node_data("alice-2")["uuid"], "alice-2") + + def test_relationship_upsert_by_uuid_is_idempotent(self): + from src.lib.postgresql.graph_store import PostgreSQLGraphStore + + store = PostgreSQLGraphStore.__new__(PostgreSQLGraphStore) + from src.lib.postgresql.graph_store import _BrainGraph + + brain = _BrainGraph("brain-a") + brain.upsert_node(["PERSON"], {"uuid": "a"}, {"uuid": "a", "name": "Alice"}) + brain.upsert_node(["PRODUCT"], {"uuid": "b"}, {"uuid": "b", "name": "Widget"}) + store._brains = {"brain-a": brain} + store._brains_lock = __import__("threading").RLock() + store._persist_relationship = MagicMock() + store._persist_node = MagicMock() + + first = store.merge_relationship( + "brain-a", + ["PERSON"], + "Alice", + ["PRODUCT"], + "Widget", + "OWNS", + {"uuid": "rel-1", "description": "v1"}, + subject_uuid="a", + object_uuid="b", + ) + second = store.merge_relationship( + "brain-a", + ["PERSON"], + "Alice", + ["PRODUCT"], + "Widget", + "OWNS", + {"uuid": "rel-1", "description": "v2"}, + subject_uuid="a", + object_uuid="b", + ) + self.assertIsNotNone(first) + self.assertIsNotNone(second) + edges = list(brain.graph.edges(keys=True, data=True)) + self.assertEqual(len(edges), 1) + self.assertEqual(edges[0][2], "rel-1") + self.assertEqual(edges[0][3]["description"], "v2") + + def test_relationship_upsert_creates_missing_endpoint_nodes(self): + from src.lib.postgresql.graph_store import PostgreSQLGraphStore + from src.lib.postgresql.graph_store import _BrainGraph + + store = PostgreSQLGraphStore.__new__(PostgreSQLGraphStore) + brain = _BrainGraph("brain-a") + store._brains = {"brain-a": brain} + store._brains_lock = __import__("threading").RLock() + store._persist_node = MagicMock() + store._persist_relationship = MagicMock() + + result = store.merge_relationship( + "brain-a", + ["PERSON"], + "Alice", + ["PRODUCT"], + "Widget", + "OWNS", + {"uuid": "rel-1", "description": "created"}, + subject_uuid="a", + object_uuid="b", + ) + self.assertIsNotNone(result) + self.assertEqual(set(brain.graph.nodes), {"a", "b"}) + self.assertEqual(len(list(brain.graph.edges(keys=True))), 1) + store._persist_node.assert_called() + store._persist_relationship.assert_called_once() + + def test_normalize_read_sql_rewrites_jsonb_ilike(self): + from src.lib.postgresql.graph_store import normalize_read_sql + + sql = ( + "SELECT uuid, data FROM kg_nodes WHERE data->>'name' ILIKE '%Reminding%' " + "OR data->'description' ILIKE '%remind%' LIMIT 50" + ) + fixed = normalize_read_sql(sql) + self.assertIn("data->>'description' ILIKE", fixed) + self.assertNotIn("data->'description' ILIKE", fixed) + + def test_stale_cache_reloads_when_db_counts_diverge(self): + from src.lib.postgresql.graph_store import PostgreSQLGraphStore + from src.lib.postgresql.graph_store import _BrainGraph + + store = PostgreSQLGraphStore.__new__(PostgreSQLGraphStore) + stale = _BrainGraph("brain-a") + store._brains = {"brain-a": stale} + store._brains_lock = __import__("threading").RLock() + store._db_graph_counts = MagicMock(return_value=(2, 1)) + + fresh = _BrainGraph("brain-a") + fresh.graph.add_node("a", name="Alice", labels=["PERSON"]) + fresh.graph.add_node("b", name="Bob", labels=["PERSON"]) + fresh.graph.add_edge("a", "b", key="r1", rel_type="KNOWS") + store._reload_brain_locked = MagicMock(return_value=fresh) + + loaded = store._load_brain("brain-a") + self.assertIs(loaded, fresh) + store._reload_brain_locked.assert_called_once_with("brain-a") + + +class Neo4jUpsertContractTests(unittest.TestCase): + def test_add_nodes_merges_by_uuid_not_name(self): + from src.constants.kg import Node + from src.lib.neo4j.client import Neo4jClient + + client = Neo4jClient.__new__(Neo4jClient) + client.ensure_database = MagicMock() + queries = [] + + def _capture(query, brain_id): + queries.append(query) + return "ok" + + client._execute_query_with_retry = _capture + client._clean_labels = lambda labels: [str(l).upper() for l in labels] + client._format_property_key = lambda key: key + client._format_value = lambda value: ( + "null" + if value is None + else f"'{value}'" + if isinstance(value, str) + else str(value) + ) + client._clean_property_key = lambda key: key + + node = Node( + uuid="alice-1", + name="Alice", + labels=["PERSON"], + description="v1", + properties={}, + ) + client.add_nodes([node], "brain-a") + self.assertEqual(len(queries), 1) + self.assertIn("MERGE (n:PERSON {uuid: 'alice-1'})", queries[0]) + self.assertNotIn("{name:", queries[0]) + + def test_add_relationship_merges_by_endpoint_and_relationship_uuid(self): + from src.constants.kg import Node, Predicate + from src.lib.neo4j.client import Neo4jClient + + client = Neo4jClient.__new__(Neo4jClient) + client.ensure_database = MagicMock() + client.driver = MagicMock() + client._clean_labels = lambda labels: [str(l).upper() for l in labels] + client._format_value = lambda value: ( + "null" + if value is None + else f"'{value}'" + if isinstance(value, str) + else str(value) + ) + + subject = Node(uuid="a", name="Alice", labels=["PERSON"], properties={}) + obj = Node(uuid="b", name="Widget", labels=["PRODUCT"], properties={}) + predicate = Predicate( + uuid="rel-1", + name="OWNS", + description="owns", + properties={"v_id": "v1"}, + flow_key="flow-1", + ) + client.add_relationship(subject, predicate, obj, "brain-a") + query = client.driver.execute_query.call_args.args[0] + self.assertIn("WHERE a['uuid'] = 'a'", query) + self.assertIn("WHERE b['uuid'] = 'b'", query) + self.assertIn("MERGE (a)-[r:OWNS {uuid: 'rel-1'}]->(b)", query) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_graphrag_upgrade_helpers.py b/tests/test_graphrag_upgrade_helpers.py new file mode 100644 index 0000000000000000000000000000000000000000..60a5d5c45410dfffa448a02a2f24a2da6630b331 --- /dev/null +++ b/tests/test_graphrag_upgrade_helpers.py @@ -0,0 +1,135 @@ +import os +import unittest + +ENV_DEFAULTS = { + "BRAINPAT_TOKEN": "test-token", + "MODELS_MODE": "local", + "EMBEDDINGS_LOCAL_MODEL": "local-model", + "EMBEDDINGS_SMALL_MODEL": "small-model", + "EMBEDDING_NODES_DIMENSION": "3", + "EMBEDDING_TRIPLETS_DIMENSION": "3", + "EMBEDDING_OBSERVATIONS_DIMENSION": "3", + "EMBEDDING_DATA_DIMENSION": "3", + "EMBEDDING_RELATIONSHIPS_DIMENSION": "3", + "REDIS_HOST": "localhost", + "REDIS_PORT": "6379", + "NEO4J_HOST": "localhost", + "NEO4J_PORT": "7687", + "NEO4J_USERNAME": "neo4j", + "NEO4J_PASSWORD": "password", + "MILVUS_HOST": "localhost", + "MILVUS_PORT": "19530", + "MONGO_CONNECTION_STRING": "mongodb://localhost:27017", + "CELERY_WORKER_CONCURRENCY": "1", + "OLLAMA_HOST": "localhost", + "OLLAMA_PORT": "11434", + "OLLAMA_LLM_SMALL_MODEL": "small", + "OLLAMA_LLM_LARGE_MODEL": "large", + "POSTGRES_HOST": "localhost", + "POSTGRES_PORT": "5432", + "POSTGRES_USERNAME": "postgres", + "POSTGRES_PASSWORD": "postgres", +} +for key, value in ENV_DEFAULTS.items(): + os.environ.setdefault(key, value) + +from src.core.saving.identity import merge_source_chunk_ids, stamp_provenance +from src.core.search.fact_filter import ( + filter_relevant_facts, + personalized_pagerank, + reciprocal_rank_fusion, +) +from src.utils.dates import normalize_date_string, resolve_relative_date + + +class ProvenanceHelpersTests(unittest.TestCase): + def test_merge_source_chunk_ids_dedupes(self): + merged = merge_source_chunk_ids( + ["a", "b"], + "b", + ["c"], + None, + "a", + ) + self.assertEqual(merged, ["a", "b", "c"]) + + def test_stamp_provenance_accumulates(self): + props = stamp_provenance( + {"source_chunk_ids": ["chunk-1"]}, + source_chunk_id="chunk-2", + source_timestamp="01/05/2023", + existing_properties={"source_chunk_ids": ["chunk-0"]}, + ) + self.assertEqual(props["source_chunk_ids"], ["chunk-0", "chunk-1", "chunk-2"]) + self.assertEqual(props["source_timestamp"], "01/05/2023") + + +class RelativeDateTests(unittest.TestCase): + def test_normalize_absolute(self): + self.assertEqual(normalize_date_string("2023-05-15"), "15/05/2023") + + def test_resolve_yesterday(self): + self.assertEqual( + resolve_relative_date("yesterday", "15/05/2023"), + "14/05/2023", + ) + + def test_resolve_last_tuesday(self): + # 15/05/2023 is a Monday + self.assertEqual( + resolve_relative_date("last Tuesday", "15/05/2023"), + "09/05/2023", + ) + + +class RrfTests(unittest.TestCase): + def test_rrf_prefers_items_ranked_high_in_multiple_lists(self): + fused = reciprocal_rank_fusion( + [ + ["a", "b", "c"], + ["b", "a", "d"], + ] + ) + ranked = [item for item, _ in fused] + self.assertEqual(ranked[0], "a") + self.assertIn("b", ranked[:2]) + + +class FactFilterTests(unittest.TestCase): + def test_fallback_preserves_all_candidates(self): + keep = filter_relevant_facts( + "What books has Melanie read?", + [f"fact-{i}" for i in range(12)], + llm_adapter=None, + max_keep=4, + ) + self.assertEqual(keep, list(range(12))) + + +class PersonalizedPageRankTests(unittest.TestCase): + def test_ppr_concentrates_on_seed_neighborhood(self): + adjacency = { + "seed": ["a", "b"], + "a": ["seed", "c"], + "b": ["seed"], + "c": ["a"], + "far": ["x"], + "x": ["far"], + } + scores = personalized_pagerank(adjacency, {"seed": 1.0}, iterations=30) + self.assertGreater(scores["seed"], scores["far"]) + self.assertGreater(scores["a"], scores["far"]) + + +class ScoutChunkingTests(unittest.TestCase): + def test_chunk_text_splits_long_input(self): + from src.utils.text_chunking import chunk_text + + text = ("paragraph one.\n\n" * 50) + ("x" * 7000) + chunks = chunk_text(text, max_chars=1000) + self.assertGreater(len(chunks), 1) + self.assertTrue(all(len(c) <= 1000 or c.count("x") > 0 for c in chunks)) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_grounding.py b/tests/test_grounding.py new file mode 100644 index 0000000000000000000000000000000000000000..a91f849f01cc8a87bd04b36d403cab2369d51b17 --- /dev/null +++ b/tests/test_grounding.py @@ -0,0 +1,158 @@ +from types import SimpleNamespace + +from src.core.saving.grounding import ( + align_span, + cheap_janitor_precheck, + decide_relationship_grounding, + endpoints_well_formed, + find_span_offsets, + realign_span_offsets, + relationship_looks_grounded, + triage_relationships_for_janitor, +) + + +def test_align_span_exact_and_fuzzy(): + source = "Alice met Bob at the park on Tuesday afternoon." + score, _ = align_span("Alice met Bob at the park", source) + assert score == 1.0 + score2, window = align_span("Bob at the park on Tuesday", source) + assert score2 >= 0.45 + assert "bob" in window or "park" in window + + +def test_find_span_offsets_exact_and_whitespace(): + source = "On Tuesday Alice paid Bob 12 dollars." + span = "Alice paid Bob 12 dollars" + start, end = find_span_offsets(span, source) + assert source[start:end] == span + + spaced = "Alice paid Bob 12 dollars" + found = find_span_offsets(spaced, source) + assert found is not None + assert "alice" in source[found[0] : found[1]].lower() + + +def test_realign_span_offsets_repairs_mismatch(): + source = "On Tuesday Alice paid Bob 12 dollars." + span = "Alice paid Bob 12 dollars" + ok, start, end, reason = realign_span_offsets( + span=span, + source_text=source, + span_start=0, + span_end=5, + ) + assert ok and reason == "" + assert start is not None and end is not None + assert source[start:end] == span + + +def test_realign_clears_bad_offsets_when_span_grounded(): + source = "Alice hired Bob as a contractor in May." + span = "Alice hired Bob as a contractor" + # Offsets point nowhere useful and span cannot be exact-found if we + # mutate — use a grounded span with impossible offsets that find can fix. + ok, start, end, reason = realign_span_offsets( + span=span, + source_text=source, + span_start=-1, + span_end=9999, + ) + assert ok and reason == "" + assert start is not None + assert source[start:end] == span + + +def test_endpoints_reject_type_named_placeholders(): + good = SimpleNamespace( + name="MET", + tip=SimpleNamespace(name="Bob", type="PERSON"), + tail=SimpleNamespace(name="Alice", type="PERSON"), + ) + bad = SimpleNamespace( + name="PAID", + tip=SimpleNamespace(name="Money", type="MONEY"), + tail=SimpleNamespace(name="Alice", type="PERSON"), + ) + assert endpoints_well_formed(good) + assert not endpoints_well_formed(bad) + + +def test_cheap_janitor_precheck_splits(): + source = "Alice hired Bob as a contractor in May." + grounded = SimpleNamespace( + name="HIRED", + description="Alice hired Bob as a contractor", + properties={}, + tip=SimpleNamespace(name="Bob", type="PERSON", uuid="1"), + tail=SimpleNamespace(name="Alice", type="PERSON", uuid="2"), + ) + ungrounded = SimpleNamespace( + name="HIRED", + description="totally fabricated claim about zebras", + properties={}, + tip=SimpleNamespace(name="Bob", type="PERSON", uuid="1"), + tail=SimpleNamespace(name="Alice", type="PERSON", uuid="2"), + ) + skip, need = cheap_janitor_precheck([grounded, ungrounded], source) + assert grounded in skip + assert ungrounded in need + ok, score, _ = relationship_looks_grounded(grounded, source) + assert ok and score >= 0.45 + + +def test_triage_accept_reject_ambiguous(): + source = "Alice hired Bob as a contractor in May." + accept_rel = SimpleNamespace( + name="HIRED", + description="Alice hired Bob as a contractor", + properties={"source_span": "Alice hired Bob as a contractor"}, + tip=SimpleNamespace(name="Bob", type="PERSON", uuid="1"), + tail=SimpleNamespace(name="Alice", type="PERSON", uuid="2"), + ) + reject_placeholder = SimpleNamespace( + name="HIRED", + description="Alice hired Bob", + properties={}, + tip=SimpleNamespace(name="PERSON", type="PERSON", uuid="1"), + tail=SimpleNamespace(name="Alice", type="PERSON", uuid="2"), + ) + reject_ungrounded = SimpleNamespace( + name="HIRED", + description="zebras invented a spaceship yesterday", + properties={"source_span": "zebras invented a spaceship yesterday"}, + tip=SimpleNamespace(name="Bob", type="PERSON", uuid="1"), + tail=SimpleNamespace(name="Alice", type="PERSON", uuid="2"), + ) + ambiguous = SimpleNamespace( + name="HIRED", + description="", + properties={}, + tip=SimpleNamespace(name="Bob", type="PERSON", uuid="1"), + tail=SimpleNamespace(name="Alice", type="PERSON", uuid="2"), + ) + triage = triage_relationships_for_janitor( + [accept_rel, reject_placeholder, reject_ungrounded, ambiguous], + source, + ) + assert accept_rel in triage.accept + reject_reasons = {d.reason for d in triage.reject} + assert "type_named_placeholder_endpoint" in reject_reasons + assert "source_span_not_grounded" in reject_reasons + assert ambiguous in triage.ambiguous + + +def test_decide_relationship_grounding_accept_sets_span(): + source = "Alice hired Bob as a contractor in May." + rel = SimpleNamespace( + name="HIRED", + description="Alice hired Bob as a contractor", + properties={}, + tip=SimpleNamespace(name="Bob", type="PERSON", uuid="1"), + tail=SimpleNamespace(name="Alice", type="PERSON", uuid="2"), + ) + decision = decide_relationship_grounding(rel, source) + assert decision.decision == "accept" + assert decision.score >= 0.45 + assert rel.properties.get("source_span") + assert "grounding_score" in rel.properties diff --git a/tests/test_halfvec_ann.py b/tests/test_halfvec_ann.py new file mode 100644 index 0000000000000000000000000000000000000000..5ee34c141fd1e60ed0c31f64a4a89a1028e68cb8 --- /dev/null +++ b/tests/test_halfvec_ann.py @@ -0,0 +1,182 @@ +import os +import unittest +from contextlib import contextmanager +from unittest.mock import MagicMock, patch + +ENV_DEFAULTS = { + "BRAINPAT_TOKEN": "test-token", + "MODELS_MODE": "local", + "EMBEDDINGS_LOCAL_MODEL": "local-model", + "EMBEDDINGS_SMALL_MODEL": "small-model", + "EMBEDDING_NODES_DIMENSION": "3", + "EMBEDDING_TRIPLETS_DIMENSION": "3", + "EMBEDDING_OBSERVATIONS_DIMENSION": "3", + "EMBEDDING_DATA_DIMENSION": "3", + "EMBEDDING_RELATIONSHIPS_DIMENSION": "3", + "REDIS_HOST": "localhost", + "REDIS_PORT": "6379", + "NEO4J_HOST": "localhost", + "NEO4J_PORT": "7687", + "NEO4J_USERNAME": "neo4j", + "NEO4J_PASSWORD": "password", + "MILVUS_HOST": "localhost", + "MILVUS_PORT": "19530", + "MONGO_CONNECTION_STRING": "mongodb://localhost:27017", + "CELERY_WORKER_CONCURRENCY": "1", + "OLLAMA_HOST": "localhost", + "OLLAMA_PORT": "11434", + "OLLAMA_LLM_SMALL_MODEL": "small", + "OLLAMA_LLM_LARGE_MODEL": "large", +} +for key, value in ENV_DEFAULTS.items(): + os.environ.setdefault(key, value) + +from src.constants.embeddings import Vector +from src.lib.postgresql import vectors as vectors_mod +from src.utils.vector_search import stable_top_k_vectors + + +def _postgres_available() -> bool: + try: + import psycopg2 + + from src.config import config + + pg = config.postgresql + pg.validate_credentials() + conn = psycopg2.connect( + host=pg.host, + port=pg.port, + user=pg.username, + password=pg.password, + dbname=pg.maintenance_database or "postgres", + connect_timeout=2, + ) + conn.close() + return True + except Exception: + return False + + +class HalfvecAnnTests(unittest.TestCase): + def test_3072_creates_halfvec_hnsw_when_search_on(self): + ddl = vectors_mod._vector_index_ddl( + "vectors_data", 3072, search_enabled=True + ) + self.assertIn("halfvec(3072)", ddl) + self.assertIn("hnsw", ddl) + self.assertIn("halfvec_cosine_ops", ddl) + + def test_3072_skips_index_when_search_off(self): + ddl = vectors_mod._vector_index_ddl( + "vectors_data", 3072, search_enabled=False + ) + self.assertEqual(ddl.strip(), "") + + def test_under_2000_keeps_float32_hnsw(self): + ddl = vectors_mod._vector_index_ddl( + "vectors_data", 1536, search_enabled=True + ) + self.assertIn("vector_cosine_ops", ddl) + self.assertNotIn("halfvec", ddl) + + def test_overfetch_float32_rerank_matches_exact_topk(self): + ann_order = [ + Vector(id="b", metadata={"uuid": "b", "resource_id": "b"}, distance=0.05), + Vector(id="a", metadata={"uuid": "a", "resource_id": "a"}, distance=0.01), + Vector(id="c", metadata={"uuid": "c", "resource_id": "c"}, distance=0.20), + ] + reranked = stable_top_k_vectors(ann_order, 2) + self.assertEqual([v.id for v in reranked], ["a", "b"]) + + def test_search_vectors_orders_by_halfvec_and_reranks_float32(self): + client = vectors_mod.PostgreSQLVectorStoreClient.__new__( + vectors_mod.PostgreSQLVectorStoreClient + ) + executed: list[str] = [] + + class Cursor: + def execute(self, sql, params=None): + executed.append(str(sql)) + + def fetchall(self): + return [ + { + "id": 2, + "uuid": "b", + "metadata": {"resource_id": "b"}, + "distance": 0.2, + }, + { + "id": 1, + "uuid": "a", + "metadata": {"resource_id": "a"}, + "distance": 0.05, + }, + ] + + @contextmanager + def fake_connection(_brain_id): + conn = MagicMock() + cur = Cursor() + + @contextmanager + def fake_cursor(**_kwargs): + yield cur + + conn.cursor.side_effect = lambda **kwargs: fake_cursor() + yield conn + + with ( + patch.object(client, "_ensure_store"), + patch.object(client, "_connection", fake_connection), + patch.object(vectors_mod, "EMBEDDING_STORES_SIZES", {"data": 3072}), + patch.object(vectors_mod.config, "search_enabled", True), + ): + hits = client.search_vectors([0.0] * 3072, "brain", "data", k=1) + + joined = "\n".join(executed) + self.assertIn("halfvec(3072)", joined) + self.assertIn("embeddings <=> %s::vector", joined) + self.assertEqual([h.id for h in hits], ["a"]) + + @unittest.skipUnless(_postgres_available(), "postgres not available") + def test_postgres_creates_halfvec_hnsw_for_3072(self): + import psycopg2.extras + + from src.lib.postgresql._naming import brain_db_name + from src.lib.postgresql._provisioning import borrow, get_brain_pool + + brain_id = "searchbench_halfvec_unit" + sizes = { + "nodes": 3072, + "triplets": 3072, + "observations": 3072, + "data": 3072, + "relationships": 3072, + } + client = vectors_mod.PostgreSQLVectorStoreClient() + with ( + patch.object(vectors_mod.config, "search_enabled", True), + patch.object(vectors_mod, "EMBEDDING_STORES_SIZES", sizes), + ): + client._initialized_stores.clear() + client._ensure_store("data", brain_id) + with borrow(get_brain_pool(brain_id)) as conn: + with conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) as cur: + cur.execute( + """ + SELECT indexdef + FROM pg_indexes + WHERE tablename = 'vectors_data' + AND indexname = 'idx_vectors_data_embeddings_halfvec' + """ + ) + row = cur.fetchone() + self.assertIsNotNone(row) + self.assertIn("halfvec", row["indexdef"]) + self.assertIn("hnsw", row["indexdef"].lower()) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_ingest_cost.py b/tests/test_ingest_cost.py new file mode 100644 index 0000000000000000000000000000000000000000..df8700fe1224152436bc0b38e0b5b812807681c1 --- /dev/null +++ b/tests/test_ingest_cost.py @@ -0,0 +1,95 @@ +from src.core.saving.ingest_cost import ( + SOURCE_TOKENIZER_ID, + IngestCostLedger, + StageCost, + count_source_tokens, + merge_cost_into_status_payload, + record_usage_from_response, + track_stage, +) + + +def test_stage_cost_accumulates(): + stage = StageCost() + stage.add_usage(input_tokens=10, output_tokens=5, cached_tokens=2, calls=1) + stage.add_usage(input_tokens=3, output_tokens=1, calls=1) + assert stage.input_tokens == 13 + assert stage.output_tokens == 6 + assert stage.cached_tokens == 2 + assert stage.calls == 2 + assert stage.total_tokens == 19 + + +def test_ledger_roundtrip_and_merge(): + a = IngestCostLedger() + a.scout.add_usage(input_tokens=100, output_tokens=20, calls=2) + a.janitor_skipped = 3 + payload = a.to_dict() + assert payload["total_llm_tokens"] == 120 + assert payload["stages"]["scout"]["calls"] == 2 + + b = IngestCostLedger.from_dict(payload) + b.architect.add_usage(input_tokens=50, output_tokens=10, calls=1) + a.merge(b) + assert a.scout.input_tokens == 200 + assert a.architect.input_tokens == 50 + assert a.janitor_skipped == 6 + + +def test_track_stage_records_usage_from_response(): + ledger = IngestCostLedger() + with track_stage(ledger, "architect"): + record_usage_from_response( + {"usage_metadata": {"input_tokens": 40, "output_tokens": 8}} + ) + assert ledger.architect.input_tokens == 40 + assert ledger.architect.output_tokens == 8 + assert ledger.architect.calls == 1 + assert ledger.architect.latency_ms >= 0 + + +def test_merge_cost_into_status_payload(): + existing = {"status": "started"} + ledger = IngestCostLedger() + ledger.scout.add_usage(input_tokens=1, output_tokens=1, calls=1) + merged = merge_cost_into_status_payload(existing, ledger) + assert merged["cost"]["stages"]["scout"]["input_tokens"] == 1 + again = merge_cost_into_status_payload( + merged, IngestCostLedger.from_dict(merged["cost"]) + ) + assert again["cost"]["stages"]["scout"]["input_tokens"] == 2 + + +def test_source_token_multiplier_uses_pinned_tokenizer(): + ledger = IngestCostLedger() + text = "Alice hired Bob as a contractor in May." + ledger.set_source_text(text) + tokens, tokenizer_id, estimated = count_source_tokens(text) + assert tokens == ledger.source_tokens + assert tokenizer_id == ledger.source_tokenizer + assert not estimated + assert ledger.source_tokenizer == SOURCE_TOKENIZER_ID + assert ledger.source_chars == len(text) + + ledger.architect.add_usage(input_tokens=90, output_tokens=10, calls=2) + payload = ledger.to_dict() + assert payload["llm_source_multiplier"] == round(100 / float(tokens), 3) + assert payload["source_tokenizer"] == SOURCE_TOKENIZER_ID + assert payload["source_tokens"] == tokens + + +def test_escalate_and_janitor_rates(): + ledger = IngestCostLedger() + ledger.record_architect_unit(escalated=False, schema_calls=1) + ledger.record_architect_unit( + escalated=True, reason="schema_empty_or_all_rejected", schema_calls=2, repair_calls=1 + ) + ledger.janitor_skipped = 7 + ledger.janitor_ran = 3 + payload = ledger.to_dict() + assert payload["architect_units"] == 2 + assert payload["architect_escalations"] == 1 + assert payload["escalate_rate"] == 0.5 + assert payload["janitor_skip_rate"] == 0.7 + assert payload["architect_repair_calls"] == 1 + assert "schema_empty_or_all_rejected" in payload["escalate_reasons"] diff --git a/tests/test_ingestion_identity.py b/tests/test_ingestion_identity.py new file mode 100644 index 0000000000000000000000000000000000000000..724dcaf757b192a15ab3a7c370c45be4a6ad1e22 --- /dev/null +++ b/tests/test_ingestion_identity.py @@ -0,0 +1,228 @@ +import os +import unittest +from unittest.mock import MagicMock, patch + + +ENV_DEFAULTS = { + "BRAINPAT_TOKEN": "test-token", + "MODELS_MODE": "local", + "EMBEDDINGS_LOCAL_MODEL": "local-model", + "EMBEDDINGS_SMALL_MODEL": "small-model", + "EMBEDDING_NODES_DIMENSION": "3", + "EMBEDDING_TRIPLETS_DIMENSION": "3", + "EMBEDDING_OBSERVATIONS_DIMENSION": "3", + "EMBEDDING_DATA_DIMENSION": "3", + "EMBEDDING_RELATIONSHIPS_DIMENSION": "3", + "REDIS_HOST": "localhost", + "REDIS_PORT": "6379", + "NEO4J_HOST": "localhost", + "NEO4J_PORT": "7687", + "NEO4J_USERNAME": "neo4j", + "NEO4J_PASSWORD": "password", + "MILVUS_HOST": "localhost", + "MILVUS_PORT": "19530", + "MONGO_CONNECTION_STRING": "mongodb://localhost:27017", + "CELERY_WORKER_CONCURRENCY": "1", + "OLLAMA_HOST": "localhost", + "OLLAMA_PORT": "11434", + "OLLAMA_LLM_SMALL_MODEL": "small", + "OLLAMA_LLM_LARGE_MODEL": "large", + "PIPELINE_MODE": "accurate", +} +for key, value in ENV_DEFAULTS.items(): + os.environ.setdefault(key, value) + + +def _triple(happened_at="01/01/2024", subject_uuid=None, event_uuid=None): + subject = {"name": "Alice", "type": "PERSON"} + if subject_uuid: + subject["uuid"] = subject_uuid + event = { + "name": "Purchase", + "type": "EVENT", + "happened_at": happened_at, + } + if event_uuid: + event["uuid"] = event_uuid + return { + "subject": subject, + "subj_event": {"name": "MADE"}, + "event": event, + "event_obj": {"name": "TARGETED"}, + "object": {"name": "Widget", "type": "PRODUCT"}, + } + + +class StableIdentityHelpersTests(unittest.TestCase): + def test_stable_node_id_is_deterministic(self): + from src.core.saving.identity import stable_node_id + + first = stable_node_id("Alice", "PERSON") + second = stable_node_id("alice", "person") + self.assertEqual(first, second) + self.assertNotEqual( + stable_node_id("Purchase", "EVENT", "01/01/2024"), + stable_node_id("Purchase", "EVENT", "02/01/2024"), + ) + + def test_supplied_uuid_is_authoritative(self): + from src.core.saving.identity import stable_node_id + + self.assertEqual( + stable_node_id("Alice", "PERSON", supplied_uuid="keep-me"), + "keep-me", + ) + + +class TripleReplayIdentityTests(unittest.TestCase): + def test_identical_structured_conversion_replays_same_ids(self): + from src.core.agents.architect_agent import ingestion_triples_to_relationships + from src.services.api.constants.requests import IngestionTripleSet + + triple = IngestionTripleSet(**_triple()) + first_rels, first_entities = ingestion_triples_to_relationships([triple], []) + second_rels, second_entities = ingestion_triples_to_relationships([triple], []) + + self.assertEqual( + [r.uuid for r in first_rels], + [r.uuid for r in second_rels], + ) + self.assertEqual( + {k: v.uuid for k, v in first_entities.items()}, + {k: v.uuid for k, v in second_entities.items()}, + ) + self.assertEqual(first_rels[0].tip.uuid, first_rels[1].tail.uuid) + + def test_same_named_events_with_different_dates_stay_separate(self): + from src.core.agents.architect_agent import ingestion_triples_to_relationships + from src.services.api.constants.requests import IngestionTripleSet + + first = IngestionTripleSet(**_triple(happened_at="01/01/2024")) + second = IngestionTripleSet(**_triple(happened_at="02/01/2024")) + relationships, entities = ingestion_triples_to_relationships( + [first, second], [] + ) + + event_uuids = { + rel.tip.uuid if rel.name == "MADE" else rel.tail.uuid + for rel in relationships + if rel.name in ("MADE", "TARGETED") + } + purchase_events = [ + entity + for key, entity in entities.items() + if len(key) >= 2 and key[0] == "purchase" and key[1] == "event" + ] + self.assertEqual(len(purchase_events), 2) + self.assertEqual(len({e.uuid for e in purchase_events}), 2) + self.assertEqual(len(event_uuids), 2) + + +class ResolutionIdentityTests(unittest.TestCase): + def test_existing_uuid_is_reused_and_not_overwritten(self): + from src.constants.agents import ArchitectAgentEntity, ArchitectAgentRelationship + from src.constants.kg import Node + from src.workers.tasks import ingestion as ingestion_mod + + existing = Node( + uuid="existing-alice", + name="Alice", + labels=["PERSON"], + properties={}, + ) + alice = ArchitectAgentEntity( + uuid="existing-alice", + name="Alice", + type="PERSON", + ) + widget = ArchitectAgentEntity( + uuid="widget-1", + name="Widget", + type="PRODUCT", + ) + relationships = [ + ArchitectAgentRelationship( + tail=alice, + name="OWNS", + tip=widget, + flow_key="flow-1", + uuid="rel-supplied", + ) + ] + + with patch.object( + ingestion_mod.graph_adapter, + "get_by_uuid", + side_effect=lambda uuid, brain_id="default": ( + existing if uuid == "existing-alice" else None + ), + ), patch.object( + ingestion_mod.graph_adapter, + "get_by_identification_params", + return_value=None, + ), patch.object( + ingestion_mod.vector_store_adapter, + "search_vectors", + return_value=[], + ): + ingestion_mod._resolve_relationship_entities(relationships, "brain-a") + + self.assertEqual(relationships[0].tail.uuid, "existing-alice") + self.assertEqual(relationships[0].uuid, "rel-supplied") + + def test_ambiguous_semantic_matches_do_not_force_merge(self): + from src.constants.agents import ArchitectAgentEntity, ArchitectAgentRelationship + from src.constants.embeddings import Vector + from src.constants.kg import Node + from src.workers.tasks import ingestion as ingestion_mod + + alice = ArchitectAgentEntity(uuid="new-alice", name="Alice", type="PERSON") + widget = ArchitectAgentEntity(uuid="widget-1", name="Widget", type="PRODUCT") + relationships = [ + ArchitectAgentRelationship( + tail=alice, + name="OWNS", + tip=widget, + flow_key="flow-1", + ) + ] + + candidate_a = MagicMock() + candidate_a.id = "va" + candidate_a.metadata = {"uuid": "a1", "labels": ["PERSON"]} + candidate_b = MagicMock() + candidate_b.id = "vb" + candidate_b.metadata = {"uuid": "a2", "labels": ["PERSON"]} + + with patch.object( + ingestion_mod.graph_adapter, "get_by_uuid", return_value=None + ), patch.object( + ingestion_mod.graph_adapter, + "get_by_identification_params", + return_value=None, + ), patch.object( + ingestion_mod.embeddings_adapter, + "embed_text", + return_value=Vector(id="q", embeddings=[1.0, 0.0, 0.0], metadata={}), + ), patch.object( + ingestion_mod.vector_store_adapter, + "search_vectors", + return_value=[candidate_a, candidate_b], + ), patch.object( + ingestion_mod.vector_store_adapter, + "get_by_ids", + side_effect=lambda ids, store="nodes", brain_id="default": [ + Vector(id=ids[0], embeddings=[1.0, 0.0, 0.0], metadata={}) + ], + ), patch.object( + ingestion_mod, + "cosine_similarity", + return_value=0.95, + ): + ingestion_mod._resolve_relationship_entities(relationships, "brain-a") + + self.assertEqual(relationships[0].tail.uuid, "new-alice") + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_ingestion_orchestration_collection.py b/tests/test_ingestion_orchestration_collection.py new file mode 100644 index 0000000000000000000000000000000000000000..e3d0089269f1c03217dd161497d3043d357eb317 --- /dev/null +++ b/tests/test_ingestion_orchestration_collection.py @@ -0,0 +1,130 @@ +import os +import unittest +from unittest.mock import MagicMock, patch + + +ENV_DEFAULTS = { + "BRAINPAT_TOKEN": "test-token", + "MODELS_MODE": "local", + "EMBEDDINGS_LOCAL_MODEL": "local-model", + "EMBEDDINGS_SMALL_MODEL": "small-model", + "EMBEDDING_NODES_DIMENSION": "3", + "EMBEDDING_TRIPLETS_DIMENSION": "3", + "EMBEDDING_OBSERVATIONS_DIMENSION": "3", + "EMBEDDING_DATA_DIMENSION": "3", + "EMBEDDING_RELATIONSHIPS_DIMENSION": "3", + "REDIS_HOST": "localhost", + "REDIS_PORT": "6379", + "NEO4J_HOST": "localhost", + "NEO4J_PORT": "7687", + "NEO4J_USERNAME": "neo4j", + "NEO4J_PASSWORD": "password", + "MILVUS_HOST": "localhost", + "MILVUS_PORT": "19530", + "MONGO_CONNECTION_STRING": "mongodb://localhost:27017", + "CELERY_WORKER_CONCURRENCY": "1", + "OLLAMA_HOST": "localhost", + "OLLAMA_PORT": "11434", + "OLLAMA_LLM_SMALL_MODEL": "small", + "OLLAMA_LLM_LARGE_MODEL": "large", + "PIPELINE_MODE": "accurate", +} +for key, value in ENV_DEFAULTS.items(): + os.environ.setdefault(key, value) + + +class IngestionOrchestrationCollectionTests(unittest.TestCase): + def test_architect_queues_relationships_instead_of_celery_delay(self): + from src.constants.agents import ArchitectAgentEntity, ArchitectAgentRelationship + from src.core.agents.architect_agent import ArchitectAgent + + agent = ArchitectAgent.__new__(ArchitectAgent) + agent.relationships_set = [] + agent.pending_persistence_batches = [] + agent.session_id = "session-1" + + rel = ArchitectAgentRelationship( + uuid="rel-1", + flow_key="flow-1", + name="OWNS", + tail=ArchitectAgentEntity(uuid="a", name="Alice", type="PERSON"), + tip=ArchitectAgentEntity(uuid="b", name="Widget", type="PRODUCT"), + ) + with patch( + "src.workers.tasks.ingestion.process_architect_relationships" + ) as mocked_task: + agent.queue_relationships_for_persistence([rel]) + mocked_task.delay.assert_not_called() + + pending = agent.take_pending_relationships() + self.assertEqual(len(pending), 1) + self.assertEqual(pending[0].uuid, "rel-1") + self.assertEqual(agent.pending_persistence_batches, []) + + def test_enrich_kg_returns_complete_persistence_batch(self): + from src.constants.agents import ArchitectAgentEntity, ArchitectAgentRelationship + from src.core.saving.auto_kg import EnrichmentOrchestrationResult + from src.core.saving import auto_kg + + rel = ArchitectAgentRelationship( + uuid="rel-1", + flow_key="flow-1", + name="OWNS", + description="owns", + tail=ArchitectAgentEntity(uuid="a", name="Alice", type="PERSON"), + tip=ArchitectAgentEntity(uuid="b", name="Widget", type="PRODUCT"), + ) + + scout = MagicMock() + scout.run.return_value = MagicMock(entities=[]) + architect = MagicMock() + architect.session_id = "sess-1" + architect.take_pending_relationships.return_value = [rel] + + with patch.object(auto_kg, "ScoutAgent", return_value=scout), patch.object( + auto_kg, "ArchitectAgent", return_value=architect + ), patch.object(auto_kg, "IngestionManager", return_value=MagicMock()), patch.object( + auto_kg.config, "pipeline_mode", "accurate" + ), patch.object( + auto_kg.config, "run_graph_consolidator", True + ), patch( + "src.workers.tasks.ingestion.consolidate_graph_async" + ) as consolidate_task: + result = auto_kg.enrich_kg_from_input("Alice owns a widget", brain_id="brain-a") + consolidate_task.delay.assert_not_called() + + self.assertIsInstance(result, EnrichmentOrchestrationResult) + self.assertEqual(result.session_id, "sess-1") + self.assertEqual(result.brain_id, "brain-a") + self.assertEqual(len(result.enrichment_relationships), 1) + self.assertTrue(result.should_consolidate) + + def test_create_relationship_tool_does_not_publish_celery_tasks(self): + from src.constants.agents import ArchitectAgentEntity, ArchitectAgentRelationship + from src.core.agents.architect_agent import ArchitectAgent + + agent = ArchitectAgent.__new__(ArchitectAgent) + agent.relationships_set = [] + agent.pending_persistence_batches = [] + agent.session_id = "sess-1" + + output_rels = [ + ArchitectAgentRelationship( + uuid="rel-1", + flow_key="flow-1", + name="OWNS", + tail=ArchitectAgentEntity(uuid="a", name="Alice", type="PERSON"), + tip=ArchitectAgentEntity(uuid="b", name="Widget", type="PRODUCT"), + ) + ] + + with patch( + "src.workers.tasks.ingestion.process_architect_relationships" + ) as mocked_task: + agent.queue_relationships_for_persistence(output_rels) + mocked_task.delay.assert_not_called() + + self.assertEqual(len(agent.take_pending_relationships()), 1) + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_ingestion_standard.py b/tests/test_ingestion_standard.py new file mode 100644 index 0000000000000000000000000000000000000000..be2c2c5358b3ea4179f8264d052f41efde0884fd --- /dev/null +++ b/tests/test_ingestion_standard.py @@ -0,0 +1,263 @@ +import ast +import json +import os +import unittest +from pathlib import Path +from unittest.mock import MagicMock, patch + + +ROOT = Path(__file__).resolve().parent.parent + + +ENV_DEFAULTS = { + "BRAINPAT_TOKEN": "test-token", + "MODELS_MODE": "local", + "EMBEDDINGS_LOCAL_MODEL": "local-model", + "EMBEDDINGS_SMALL_MODEL": "small-model", + "EMBEDDING_NODES_DIMENSION": "3", + "EMBEDDING_TRIPLETS_DIMENSION": "3", + "EMBEDDING_OBSERVATIONS_DIMENSION": "3", + "EMBEDDING_DATA_DIMENSION": "3", + "EMBEDDING_RELATIONSHIPS_DIMENSION": "3", + "REDIS_HOST": "localhost", + "REDIS_PORT": "6379", + "NEO4J_HOST": "localhost", + "NEO4J_PORT": "7687", + "NEO4J_USERNAME": "neo4j", + "NEO4J_PASSWORD": "password", + "MILVUS_HOST": "localhost", + "MILVUS_PORT": "19530", + "MONGO_CONNECTION_STRING": "mongodb://localhost:27017", + "CELERY_WORKER_CONCURRENCY": "1", + "OLLAMA_HOST": "localhost", + "OLLAMA_PORT": "11434", + "OLLAMA_LLM_SMALL_MODEL": "small", + "OLLAMA_LLM_LARGE_MODEL": "large", + "PIPELINE_MODE": "accurate", +} +for key, value in ENV_DEFAULTS.items(): + os.environ.setdefault(key, value) + + +class SourceTextHelperTests(unittest.TestCase): + def test_source_text_from_text_payload(self): + from src.constants.tasks.ingestion import IngestionTaskArgs + from src.workers.tasks.ingestion import source_text_from_payload + + payload = IngestionTaskArgs( + data={"data_type": "text", "text_data": "hello world"}, + brain_id="brain1", + ) + self.assertFalse(payload.skip_enrichment) + skipped = IngestionTaskArgs( + data={"data_type": "text", "text_data": "catalog row"}, + brain_id="brain1", + skip_enrichment=True, + ) + self.assertTrue(skipped.skip_enrichment) + self.assertEqual(source_text_from_payload(payload), "hello world") + + def test_source_text_from_json_payload(self): + from src.constants.tasks.ingestion import IngestionTaskArgs + from src.workers.tasks.ingestion import source_text_from_payload + + payload = IngestionTaskArgs( + data={"data_type": "json", "json_data": {"a": 1, "b": "x"}}, + brain_id="brain1", + ) + self.assertEqual( + source_text_from_payload(payload), + json.dumps({"a": 1, "b": "x"}), + ) + + +class PipelineModeValidationTests(unittest.TestCase): + def test_invalid_pipeline_mode_raises(self): + from src.config import validate_pipeline_mode + + with self.assertRaises(ValueError): + validate_pipeline_mode(None) + with self.assertRaises(ValueError): + validate_pipeline_mode("turbo") + self.assertEqual(validate_pipeline_mode("accurate"), "accurate") + self.assertEqual(validate_pipeline_mode("lightweight"), "lightweight") + + +class ConsolidateGraphAsyncReturnTests(unittest.TestCase): + def test_consolidate_graph_async_has_no_undefined_consolidation_response(self): + source = (ROOT / "src/workers/tasks/ingestion.py").read_text(encoding="utf-8") + tree = ast.parse(source) + for node in ast.walk(tree): + if isinstance(node, ast.Name) and node.id == "consolidation_response": + self.fail( + "consolidation_response is still referenced in ingestion.py" + ) + + +class IngestDataObservationAndEnrichmentTests(unittest.TestCase): + def _make_request(self): + request = MagicMock() + request.id = "task-123" + return request + + def test_json_ingest_passes_serialized_text_to_enrichment_and_observations(self): + from src.constants.data import TextChunk + from src.constants.embeddings import Vector + from src.workers.tasks import ingestion as ingestion_mod + + saved_observations = [] + enrich_calls = [] + observe_calls = [] + + def fake_save_text_chunk(chunk, brain_id="default"): + return chunk + + def fake_save_observations(observations, brain_id="default"): + saved_observations.append({"observations": observations, "brain_id": brain_id}) + return observations + + def fake_observe(*, text, observate_for, context=None): + observe_calls.append( + {"text": text, "observate_for": observate_for, "context": context} + ) + return ["obs-1"] + + def fake_enrich(text, targeting=None, brain_id="default", **kwargs): + from src.core.saving.auto_kg import EnrichmentOrchestrationResult + + enrich_calls.append({"text": text, "brain_id": brain_id}) + return EnrichmentOrchestrationResult( + session_id=None, + ingestion_session_id="ingest-1", + brain_id=brain_id, + enrichment_relationships=[], + should_consolidate=False, + ) + + task = ingestion_mod.ingest_data + request = self._make_request() + args = { + "data": {"data_type": "json", "json_data": {"name": "Alice"}}, + "brain_id": "tenant-a", + "observate_for": ["people"], + } + + with ( + patch.object(ingestion_mod, "data_adapter") as data_adapter, + patch.object(ingestion_mod, "embeddings_adapter") as embeddings_adapter, + patch.object(ingestion_mod, "vector_store_adapter") as vector_store_adapter, + patch.object(ingestion_mod, "cache_adapter"), + patch.object(ingestion_mod, "observations_agent") as observations_agent, + patch.object(ingestion_mod, "enrich_kg_from_input", side_effect=fake_enrich), + patch.object(ingestion_mod.config, "pipeline_mode", "accurate"), + patch.object(ingestion_mod.config, "run_observations", True), + patch.object(ingestion_mod, "set_ingestion_task_status"), + patch("celery.chain") as chain_mock, + ): + chain_mock.return_value.apply_async = MagicMock() + data_adapter.save_text_chunk.side_effect = fake_save_text_chunk + data_adapter.save_observations.side_effect = fake_save_observations + embeddings_adapter.embed_text.return_value = Vector( + id="v1", embeddings=[0.1, 0.2], metadata={} + ) + observations_agent.observe.side_effect = fake_observe + vector_store_adapter.add_vectors.return_value = ["v1"] + + result = task.run.__func__( + type("Bound", (), {"request": request})(), + args, + ) + + expected_text = json.dumps({"name": "Alice"}) + self.assertEqual(result, "task-123") + self.assertEqual(len(observe_calls), 1) + self.assertEqual(observe_calls[0]["text"], expected_text) + self.assertIsNone(observe_calls[0]["context"]) + self.assertEqual(len(saved_observations), 1) + self.assertEqual(saved_observations[0]["brain_id"], "tenant-a") + self.assertEqual(len(enrich_calls), 1) + self.assertEqual(enrich_calls[0]["text"], expected_text) + self.assertEqual(enrich_calls[0]["brain_id"], "tenant-a") + + def test_skip_enrichment_writes_chunk_without_kg(self): + from src.constants.embeddings import Vector + from src.workers.tasks import ingestion as ingestion_mod + + enrich_calls = [] + observe_calls = [] + + def fake_save_text_chunk(chunk, brain_id="default"): + return chunk + + def fake_enrich(*args, **kwargs): + enrich_calls.append(kwargs) + raise AssertionError("enrichment should be skipped") + + def fake_observe(**kwargs): + observe_calls.append(kwargs) + raise AssertionError("observations should be skipped") + + task = ingestion_mod.ingest_data + request = self._make_request() + args = { + "data": {"data_type": "text", "text_data": "DOCID p1. Title: kettle"}, + "brain_id": "searchbenchesci", + "skip_enrichment": True, + } + + with ( + patch.object(ingestion_mod, "data_adapter") as data_adapter, + patch.object(ingestion_mod, "embeddings_adapter") as embeddings_adapter, + patch.object(ingestion_mod, "vector_store_adapter") as vector_store_adapter, + patch.object(ingestion_mod, "cache_adapter"), + patch.object(ingestion_mod, "observations_agent") as observations_agent, + patch.object(ingestion_mod, "enrich_kg_from_input", side_effect=fake_enrich), + patch.object(ingestion_mod.config, "pipeline_mode", "accurate"), + patch.object(ingestion_mod, "set_ingestion_task_status") as set_status, + ): + data_adapter.save_text_chunk.side_effect = fake_save_text_chunk + embeddings_adapter.embed_text.return_value = Vector( + id="v1", embeddings=[0.1, 0.2], metadata={} + ) + observations_agent.observe.side_effect = fake_observe + vector_store_adapter.add_vectors.return_value = ["v1"] + result = task.run.__func__( + type("Bound", (), {"request": request})(), + args, + ) + + self.assertEqual(result, "task-123") + self.assertEqual(observe_calls, []) + self.assertEqual(enrich_calls, []) + self.assertTrue(data_adapter.save_text_chunk.called) + self.assertTrue(embeddings_adapter.embed_text.called) + completed = [ + call + for call in set_status.call_args_list + if call.args and call.args[2] == "completed" + ] + self.assertTrue(completed) + + def test_invalid_pipeline_mode_fails_ingest(self): + from src.workers.tasks import ingestion as ingestion_mod + + task = ingestion_mod.ingest_data + request = self._make_request() + args = { + "data": {"data_type": "text", "text_data": "hello"}, + "brain_id": "tenant-a", + } + + with ( + patch.object(ingestion_mod, "cache_adapter"), + patch.object(ingestion_mod.config, "pipeline_mode", None), + ): + with self.assertRaises(ValueError): + task.run.__func__( + type("Bound", (), {"request": request})(), + args, + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_ingestion_structured_contract.py b/tests/test_ingestion_structured_contract.py new file mode 100644 index 0000000000000000000000000000000000000000..ff55362f73fd20285995096e507b444729e0d0cc --- /dev/null +++ b/tests/test_ingestion_structured_contract.py @@ -0,0 +1,202 @@ +import os +import unittest +from unittest.mock import MagicMock, patch + +from pydantic import ValidationError + + +ENV_DEFAULTS = { + "BRAINPAT_TOKEN": "test-token", + "MODELS_MODE": "local", + "EMBEDDINGS_LOCAL_MODEL": "local-model", + "EMBEDDINGS_SMALL_MODEL": "small-model", + "EMBEDDING_NODES_DIMENSION": "3", + "EMBEDDING_TRIPLETS_DIMENSION": "3", + "EMBEDDING_OBSERVATIONS_DIMENSION": "3", + "EMBEDDING_DATA_DIMENSION": "3", + "EMBEDDING_RELATIONSHIPS_DIMENSION": "3", + "REDIS_HOST": "localhost", + "REDIS_PORT": "6379", + "NEO4J_HOST": "localhost", + "NEO4J_PORT": "7687", + "NEO4J_USERNAME": "neo4j", + "NEO4J_PASSWORD": "password", + "MILVUS_HOST": "localhost", + "MILVUS_PORT": "19530", + "MONGO_CONNECTION_STRING": "mongodb://localhost:27017", + "CELERY_WORKER_CONCURRENCY": "1", + "OLLAMA_HOST": "localhost", + "OLLAMA_PORT": "11434", + "OLLAMA_LLM_SMALL_MODEL": "small", + "OLLAMA_LLM_LARGE_MODEL": "large", + "PIPELINE_MODE": "accurate", +} +for key, value in ENV_DEFAULTS.items(): + os.environ.setdefault(key, value) + + +def _minimal_triple(): + return { + "event": {"name": "Purchase", "type": "EVENT"}, + "event_obj": {"name": "TARGETED"}, + "object": {"name": "Widget", "type": "PRODUCT"}, + } + + +class StructuredRequestValidationTests(unittest.TestCase): + def test_triple_only_without_anchor_or_text_validates(self): + from src.services.api.constants.requests import IngestionStructuredRequestBody + + body = IngestionStructuredRequestBody(data=[_minimal_triple()]) + self.assertIsNone(body.anchor) + self.assertIsNone(body.text) + + def test_direct_has_triple_validates(self): + from src.services.api.constants.requests import IngestionTripleSet + + triple = IngestionTripleSet( + subject={"name": "bed", "type": "ENTITY", "uuid": "0"}, + subj_event={"name": "HAS", "uuid": "rel-1"}, + object={"name": "navy", "type": "ATTR", "uuid": "hub-navy"}, + ) + self.assertIsNone(triple.event) + self.assertIsNone(triple.event_obj) + prefers = IngestionTripleSet( + subject={"name": "u01", "type": "USER", "uuid": "user:u01"}, + subj_event={"name": "PREFERS", "uuid": "rel-pref"}, + object={"name": "70s", "type": "ATTR", "uuid": "hub:attr:70s"}, + ) + self.assertIsNone(prefers.event) + self.assertEqual(prefers.subj_event.name, "PREFERS") + with self.assertRaises(ValidationError): + IngestionTripleSet( + subject={"name": "bed", "type": "ENTITY", "uuid": "0"}, + subj_event={"name": "HAS"}, + event={"name": "HAS", "type": "EVENT"}, + object={"name": "navy", "type": "ATTR"}, + ) + + def test_string_anchor_is_rejected(self): + from src.services.api.constants.requests import IngestionStructuredRequestBody + + with self.assertRaises(ValidationError): + IngestionStructuredRequestBody( + data=[_minimal_triple()], + anchor="John", + ) + + def test_anchor_requires_uuid_or_name_and_type(self): + from src.services.api.constants.requests import IngestionStructuredRequestBody + + with self.assertRaises(ValidationError): + IngestionStructuredRequestBody( + data=[_minimal_triple()], + anchor={"name": "John"}, + ) + with self.assertRaises(ValidationError): + IngestionStructuredRequestBody( + data=[_minimal_triple()], + anchor={"type": "PERSON"}, + ) + + body = IngestionStructuredRequestBody( + data=[_minimal_triple()], + anchor={"uuid": "abc-123"}, + ) + self.assertEqual(body.anchor.uuid, "abc-123") + + body = IngestionStructuredRequestBody( + data=[_minimal_triple()], + anchor={"name": "John", "type": "PERSON"}, + ) + self.assertEqual(body.anchor.name, "John") + + +class StructuredAnchorRuntimeTests(unittest.TestCase): + def test_missing_uuid_anchor_raises_and_marks_failed(self): + from src.workers.tasks import ingestion as ingestion_mod + + request = MagicMock() + request.id = "task-anchor-miss" + cache_writes = [] + + def fake_cache_set(key, value, brain_id="default", expires_in=None): + cache_writes.append({"key": key, "value": value, "brain_id": brain_id}) + return True + + args = { + "data": [_minimal_triple()], + "anchor": {"uuid": "missing-uuid"}, + "brain_id": "tenant-a", + } + + with ( + patch.object(ingestion_mod, "cache_adapter") as cache_adapter, + patch.object(ingestion_mod, "graph_adapter") as graph_adapter, + patch.object(ingestion_mod, "IngestionManager"), + ): + cache_adapter.set.side_effect = fake_cache_set + graph_adapter.get_by_uuid.return_value = None + + with self.assertRaises(ValueError) as ctx: + ingestion_mod.ingest_structured_data.run.__func__( + type("Bound", (), {"request": request})(), + args, + ) + + self.assertIn("missing-uuid", str(ctx.exception)) + failed = [w for w in cache_writes if '"status": "failed"' in w["value"]] + self.assertTrue(failed) + self.assertEqual(failed[-1]["brain_id"], "tenant-a") + graph_adapter.get_by_uuid.assert_called_once_with( + "missing-uuid", brain_id="tenant-a" + ) + + def test_name_type_anchor_lookup_passes_brain_id(self): + from src.constants.kg import Node + from src.workers.tasks import ingestion as ingestion_mod + + request = MagicMock() + request.id = "task-anchor-name" + captured = {} + + def fake_get_by_identification_params(*args, **kwargs): + captured["args"] = args + captured["kwargs"] = kwargs + return Node( + uuid="n1", + name="John", + labels=["PERSON"], + description="", + properties={}, + ) + + args = { + "data": [_minimal_triple()], + "anchor": {"name": "John", "type": "PERSON"}, + "brain_id": "tenant-b", + } + + with ( + patch.object(ingestion_mod, "cache_adapter"), + patch.object(ingestion_mod, "graph_adapter") as graph_adapter, + patch.object(ingestion_mod, "IngestionManager"), + patch.object(ingestion_mod, "data_adapter"), + patch.object(ingestion_mod, "set_ingestion_task_status"), + patch("celery.chain") as chain_mock, + ): + chain_mock.return_value.apply_async = MagicMock() + graph_adapter.get_by_identification_params.side_effect = ( + fake_get_by_identification_params + ) + result = ingestion_mod.ingest_structured_data.run.__func__( + type("Bound", (), {"request": request})(), + args, + ) + + self.assertEqual(result, "task-anchor-name") + self.assertEqual(captured["kwargs"].get("brain_id"), "tenant-b") + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_ingestion_task_lifecycle.py b/tests/test_ingestion_task_lifecycle.py new file mode 100644 index 0000000000000000000000000000000000000000..6b12b40a83be3ced886020bc397234f3852e9cc1 --- /dev/null +++ b/tests/test_ingestion_task_lifecycle.py @@ -0,0 +1,222 @@ +import json +import os +import unittest +from unittest.mock import MagicMock, patch + + +ENV_DEFAULTS = { + "BRAINPAT_TOKEN": "test-token", + "MODELS_MODE": "local", + "EMBEDDINGS_LOCAL_MODEL": "local-model", + "EMBEDDINGS_SMALL_MODEL": "small-model", + "EMBEDDING_NODES_DIMENSION": "3", + "EMBEDDING_TRIPLETS_DIMENSION": "3", + "EMBEDDING_OBSERVATIONS_DIMENSION": "3", + "EMBEDDING_DATA_DIMENSION": "3", + "EMBEDDING_RELATIONSHIPS_DIMENSION": "3", + "REDIS_HOST": "localhost", + "REDIS_PORT": "6379", + "NEO4J_HOST": "localhost", + "NEO4J_PORT": "7687", + "NEO4J_USERNAME": "neo4j", + "NEO4J_PASSWORD": "password", + "MILVUS_HOST": "localhost", + "MILVUS_PORT": "19530", + "MONGO_CONNECTION_STRING": "mongodb://localhost:27017", + "CELERY_WORKER_CONCURRENCY": "1", + "OLLAMA_HOST": "localhost", + "OLLAMA_PORT": "11434", + "OLLAMA_LLM_SMALL_MODEL": "small", + "OLLAMA_LLM_LARGE_MODEL": "large", + "PIPELINE_MODE": "accurate", +} +for key, value in ENV_DEFAULTS.items(): + os.environ.setdefault(key, value) + + +class TaskStatusMonotonicTests(unittest.TestCase): + def test_queued_cannot_overwrite_started_or_terminal(self): + from src.workers.tasks import ingestion as ingestion_mod + + store = {} + + def fake_get(key, brain_id="default"): + return store.get((key, brain_id)) + + def fake_set(key, value, brain_id="default", expires_in=None): + store[(key, brain_id)] = value + return True + + with patch.object(ingestion_mod, "cache_adapter") as cache_adapter: + cache_adapter.get.side_effect = fake_get + cache_adapter.set.side_effect = fake_set + + ingestion_mod.set_ingestion_task_status("t1", "b1", "queued") + ingestion_mod.set_ingestion_task_status("t1", "b1", "started", stage="source") + ingestion_mod.set_ingestion_task_status("t1", "b1", "queued") + started = json.loads(store[("task:t1", "b1")]) + self.assertEqual(started["status"], "started") + + ingestion_mod.set_ingestion_task_status("t1", "b1", "completed") + ingestion_mod.set_ingestion_task_status("t1", "b1", "started") + completed = json.loads(store[("task:t1", "b1")]) + self.assertEqual(completed["status"], "completed") + + def test_finalizer_writes_completed(self): + from src.workers.tasks import ingestion as ingestion_mod + + store = {} + + def fake_get(key, brain_id="default"): + return store.get((key, brain_id)) + + def fake_set(key, value, brain_id="default", expires_in=None): + store[(key, brain_id)] = value + return True + + with patch.object(ingestion_mod, "cache_adapter") as cache_adapter: + cache_adapter.get.side_effect = fake_get + cache_adapter.set.side_effect = fake_set + ingestion_mod.set_ingestion_task_status("t2", "b1", "persisting") + result = ingestion_mod.finalize_ingestion_task.run.__func__( + type("Bound", (), {"request": MagicMock(id="child")})(), + "t2", + "b1", + "completed", + ) + self.assertEqual(result, "t2") + self.assertEqual(json.loads(store[("task:t2", "b1")])["status"], "completed") + + +class IngestApiLifecycleTests(unittest.TestCase): + def test_ingest_records_queued_before_publish_and_returns_202(self): + from fastapi.testclient import TestClient + + events = [] + + def fake_set_status(task_id, brain_id, status, **kwargs): + events.append(("status", status, task_id)) + return {"status": status, "task_id": task_id} + + def fake_apply_async(*args, **kwargs): + events.append(("publish", kwargs.get("task_id"))) + return MagicMock(id=kwargs.get("task_id")) + + with patch( + "src.services.api.routes.ingest.set_ingestion_task_status", + side_effect=fake_set_status, + ), patch( + "src.services.api.routes.ingest.ingest_data_task.apply_async", + side_effect=fake_apply_async, + ): + from src.services.api.routes.ingest import ingest_router + from src.services.api.dependencies import get_brain_id + from fastapi import FastAPI + + app = FastAPI() + app.dependency_overrides[get_brain_id] = lambda: "brain-a" + app.include_router(ingest_router) + client = TestClient(app) + response = client.post( + "/ingest/", + json={ + "data": {"data_type": "text", "text_data": "hello"}, + }, + ) + + self.assertEqual(response.status_code, 202) + body = response.json() + self.assertEqual(body["message"], "Ingestion accepted") + self.assertIn("task_id", body) + self.assertEqual(events[0][0], "status") + self.assertEqual(events[0][1], "queued") + self.assertEqual(events[1][0], "publish") + self.assertEqual(events[0][2], events[1][1]) + + +class TasksNotFoundTests(unittest.TestCase): + def test_unknown_task_returns_not_found(self): + from fastapi import FastAPI + from fastapi.testclient import TestClient + + with patch( + "src.services.api.routes.tasks.cache_adapter" + ) as cache_adapter: + cache_adapter.get_task.return_value = None + from src.services.api.routes.tasks import tasks_router + from src.services.api.dependencies import get_brain_id + from fastapi import FastAPI + + app = FastAPI() + app.dependency_overrides[get_brain_id] = lambda: "brain-a" + app.include_router(tasks_router) + client = TestClient(app) + response = client.get("/tasks/missing-id") + + self.assertEqual(response.status_code, 404) + + +class ParentCompletionChainTests(unittest.TestCase): + def test_parent_does_not_mark_completed_before_chain(self): + from src.core.saving.auto_kg import EnrichmentOrchestrationResult + from src.workers.tasks import ingestion as ingestion_mod + + request = MagicMock() + request.id = "parent-1" + statuses = [] + + def fake_set_status(task_id, brain_id, status, **kwargs): + statuses.append(status) + return {"status": status, "task_id": task_id} + + chained = [] + + class FakeChain: + def __init__(self, *steps): + chained.extend(steps) + + def apply_async(self): + return MagicMock(id="chain-1") + + with patch.object( + ingestion_mod, "set_ingestion_task_status", side_effect=fake_set_status + ), patch.object( + ingestion_mod, + "enrich_kg_from_input", + return_value=EnrichmentOrchestrationResult( + session_id="s1", + ingestion_session_id="i1", + brain_id="brain-a", + enrichment_relationships=[{"uuid": "r1"}], + should_consolidate=False, + ), + ), patch.object(ingestion_mod, "data_adapter") as data_adapter, patch.object( + ingestion_mod, "embeddings_adapter" + ) as embeddings_adapter, patch.object( + ingestion_mod, "vector_store_adapter" + ), patch.object( + ingestion_mod.config, "pipeline_mode", "lightweight" + ), patch( + "celery.chain", FakeChain + ): + from src.constants.embeddings import Vector + + data_adapter.save_text_chunk.side_effect = lambda chunk, brain_id="default": chunk + embeddings_adapter.embed_text.return_value = Vector( + id="v1", embeddings=[0.1], metadata={} + ) + result = ingestion_mod.ingest_data.run.__func__( + type("Bound", (), {"request": request})(), + { + "data": {"data_type": "text", "text_data": "hello"}, + "brain_id": "brain-a", + }, + ) + + self.assertEqual(result, "parent-1") + self.assertNotIn("completed", statuses) + self.assertTrue(chained) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_janitor_outcomes.py b/tests/test_janitor_outcomes.py new file mode 100644 index 0000000000000000000000000000000000000000..e57fdcfc22a13c4258d98b0ce9eaf31e1aacd47c --- /dev/null +++ b/tests/test_janitor_outcomes.py @@ -0,0 +1,268 @@ +from types import SimpleNamespace +from unittest.mock import MagicMock, patch + +import os + +ENV_DEFAULTS = { + "BRAINPAT_TOKEN": "test-token", + "MODELS_MODE": "local", + "PIPELINE_MODE": "accurate", + "EMBEDDINGS_LOCAL_MODEL": "local-model", + "EMBEDDINGS_SMALL_MODEL": "small-model", + "EMBEDDING_NODES_DIMENSION": "3", + "EMBEDDING_TRIPLETS_DIMENSION": "3", + "EMBEDDING_OBSERVATIONS_DIMENSION": "3", + "EMBEDDING_DATA_DIMENSION": "3", + "EMBEDDING_RELATIONSHIPS_DIMENSION": "3", + "REDIS_HOST": "localhost", + "REDIS_PORT": "6379", + "NEO4J_HOST": "localhost", + "NEO4J_PORT": "7687", + "NEO4J_USERNAME": "neo4j", + "NEO4J_PASSWORD": "password", + "MILVUS_HOST": "localhost", + "MILVUS_PORT": "19530", + "MONGO_CONNECTION_STRING": "mongodb://localhost:27017", + "CELERY_WORKER_CONCURRENCY": "1", + "OLLAMA_HOST": "localhost", + "OLLAMA_PORT": "11434", + "OLLAMA_LLM_SMALL_MODEL": "small", + "OLLAMA_LLM_LARGE_MODEL": "large", +} +for key, value in ENV_DEFAULTS.items(): + os.environ.setdefault(key, value) + +from src.constants.agents import ( + ArchitectAgentEntity, + ArchitectAgentRelationship, + AtomicJanitorAgentInputOutput, + AtomicJanitorAgentWrongRelationship, + _ArchitectAgentRelationship, +) +from src.core.agents.architect_agent import ArchitectAgent +from src.core.saving.ingest_cost import IngestCostLedger + + +def _rel(name="MET", grounded=True): + props = {} + if grounded: + props["source_span"] = "Alice met Bob at the park" + return ArchitectAgentRelationship( + flow_key="fk", + uuid="r1", + name=name, + description="Alice met Bob at the park" if grounded else "zebras invented ships", + properties=props, + tail=ArchitectAgentEntity( + uuid="a", name="Alice", type="PERSON", description="", properties={} + ), + tip=ArchitectAgentEntity( + uuid="b", name="Bob", type="PERSON", description="", properties={} + ), + ) + + +def test_janitor_ok_status_keeps_batch(): + agent = ArchitectAgent( + llm_adapter=MagicMock(), + cache_adapter=MagicMock(), + kg=MagicMock(), + vector_store=MagicMock(), + embeddings=MagicMock(), + ingestion_manager=MagicMock(), + ) + # Force ambiguous path: weak/missing span so triage sends to LLM. + rel = _rel(grounded=False) + rel.properties = {} + rel.description = "" + agent.pending_persistence_batches = [[rel]] + ledger = IngestCostLedger() + + mock_janitor = MagicMock() + mock_janitor.run_atomic_janitor.return_value = "OK" + with patch( + "src.core.agents.janitor_agent.JanitorAgent", return_value=mock_janitor + ), patch("src.services.input.agents.llm_small_adapter"), patch( + "src.services.input.agents.graph_adapter" + ) as ga, patch( + "src.services.input.agents.vector_store_adapter" + ), patch( + "src.services.input.agents.embeddings_adapter" + ): + ga.graphdb_description = "db" + agent.run_batched_janitor( + text="Alice met Bob at the park.", + cost_ledger=ledger, + ) + pending = agent.take_pending_relationships() + assert len(pending) == 1 + assert ledger.janitor_ran == 1 + assert ledger.janitor_ambiguous == 1 + + +def test_janitor_reject_status_vetoes_without_persist(): + agent = ArchitectAgent( + llm_adapter=MagicMock(), + cache_adapter=MagicMock(), + kg=MagicMock(), + vector_store=MagicMock(), + embeddings=MagicMock(), + ingestion_manager=MagicMock(), + ) + rel = _rel(grounded=False) + rel.properties = {} + rel.description = "" + agent.pending_persistence_batches = [[rel]] + ledger = IngestCostLedger() + + mock_janitor = MagicMock() + mock_janitor.run_atomic_janitor.return_value = AtomicJanitorAgentInputOutput( + status="REJECT", + veto_reasons=["fabricated_edge"], + ) + with patch( + "src.core.agents.janitor_agent.JanitorAgent", return_value=mock_janitor + ), patch("src.services.input.agents.llm_small_adapter"), patch( + "src.services.input.agents.graph_adapter" + ) as ga, patch( + "src.services.input.agents.vector_store_adapter" + ), patch( + "src.services.input.agents.embeddings_adapter" + ): + ga.graphdb_description = "db" + agent.run_batched_janitor( + text="Alice met Bob at the park.", + cost_ledger=ledger, + ) + assert agent.take_pending_relationships() == [] + assert "fabricated_edge" in ledger.janitor_drop_reasons + + +def test_janitor_parse_failure_does_not_approve(): + agent = ArchitectAgent( + llm_adapter=MagicMock(), + cache_adapter=MagicMock(), + kg=MagicMock(), + vector_store=MagicMock(), + embeddings=MagicMock(), + ingestion_manager=MagicMock(), + ) + rel = _rel(grounded=False) + rel.properties = {} + rel.description = "" + agent.pending_persistence_batches = [[rel]] + ledger = IngestCostLedger() + + mock_janitor = MagicMock() + mock_janitor.run_atomic_janitor.return_value = None + with patch( + "src.core.agents.janitor_agent.JanitorAgent", return_value=mock_janitor + ), patch("src.services.input.agents.llm_small_adapter"), patch( + "src.services.input.agents.graph_adapter" + ) as ga, patch( + "src.services.input.agents.vector_store_adapter" + ), patch( + "src.services.input.agents.embeddings_adapter" + ): + ga.graphdb_description = "db" + agent.run_batched_janitor( + text="Alice met Bob at the park.", + cost_ledger=ledger, + ) + assert agent.take_pending_relationships() == [] + assert "janitor_parse_failure" in ledger.janitor_drop_reasons + + +def test_janitor_wrong_relationship_dropped_fixed_kept(): + agent = ArchitectAgent( + llm_adapter=MagicMock(), + cache_adapter=MagicMock(), + kg=MagicMock(), + vector_store=MagicMock(), + embeddings=MagicMock(), + ingestion_manager=MagicMock(), + ) + bad = _rel(grounded=False) + bad.properties = {} + bad.description = "" + agent.pending_persistence_batches = [[bad]] + ledger = IngestCostLedger() + + fixed = _ArchitectAgentRelationship( + tip=bad.tip, + tail=bad.tail, + name="MET", + description="Alice met Bob at the park", + properties={"source_span": "Alice met Bob at the park"}, + ) + wrong = AtomicJanitorAgentWrongRelationship( + relationship=_ArchitectAgentRelationship( + tip=bad.tip, + tail=bad.tail, + name=bad.name, + description=bad.description or "", + properties={}, + ), + reason="ungrounded", + instructions="drop", + ) + mock_janitor = MagicMock() + mock_janitor.run_atomic_janitor.return_value = AtomicJanitorAgentInputOutput( + status="ERROR", + fixed_relationships=[fixed], + wrong_relationships=[wrong], + ) + with patch( + "src.core.agents.janitor_agent.JanitorAgent", return_value=mock_janitor + ), patch("src.services.input.agents.llm_small_adapter"), patch( + "src.services.input.agents.graph_adapter" + ) as ga, patch( + "src.services.input.agents.vector_store_adapter" + ), patch( + "src.services.input.agents.embeddings_adapter" + ): + ga.graphdb_description = "db" + agent.run_batched_janitor( + text="Alice met Bob at the park.", + cost_ledger=ledger, + ) + pending = agent.take_pending_relationships() + assert len(pending) == 1 + assert pending[0].name == "MET" + assert "ungrounded" in ledger.janitor_drop_reasons + + +def test_deterministic_reject_skips_llm(): + agent = ArchitectAgent( + llm_adapter=MagicMock(), + cache_adapter=MagicMock(), + kg=MagicMock(), + vector_store=MagicMock(), + embeddings=MagicMock(), + ingestion_manager=MagicMock(), + ) + placeholder = ArchitectAgentRelationship( + flow_key="fk", + uuid="r1", + name="MET", + description="Alice met Bob", + properties={"source_span": "Alice met Bob"}, + tail=ArchitectAgentEntity( + uuid="a", name="PERSON", type="PERSON", description="", properties={} + ), + tip=ArchitectAgentEntity( + uuid="b", name="Bob", type="PERSON", description="", properties={} + ), + ) + agent.pending_persistence_batches = [[placeholder]] + ledger = IngestCostLedger() + with patch("src.core.agents.janitor_agent.JanitorAgent") as jan_cls: + agent.run_batched_janitor( + text="Alice met Bob at the park.", + cost_ledger=ledger, + ) + jan_cls.assert_not_called() + assert agent.take_pending_relationships() == [] + assert ledger.janitor_rejected == 1 + assert ledger.janitor_ran == 0 + assert "type_named_placeholder_endpoint" in ledger.janitor_drop_reasons diff --git a/tests/test_kombu_redis_patch.py b/tests/test_kombu_redis_patch.py new file mode 100644 index 0000000000000000000000000000000000000000..40ca0d0240340cc5130206a527fff36ca9f3ffbc --- /dev/null +++ b/tests/test_kombu_redis_patch.py @@ -0,0 +1,162 @@ +from queue import Empty +from unittest.mock import MagicMock + +import pytest +from redis.exceptions import ResponseError + +from src.workers.kombu_redis_patch import apply_kombu_redis_unblocked_patch + + +@pytest.fixture(autouse=True) +def reset_kombu_channel_and_patch_flag(): + from kombu.transport.redis import Channel + + import src.workers.kombu_redis_patch as patch_mod + + saved = Channel._brpop_read + saved_monotonic = patch_mod.time.monotonic + patch_mod._PATCHED = False + patch_mod._UNBLOCKED_WINDOW_SECONDS = 60 + patch_mod._UNBLOCKED_MAX_RETRIES = 20 + patch_mod._UNBLOCKED_WINDOW_STARTED_AT = 0.0 + patch_mod._UNBLOCKED_EVENT_COUNT = 0 + yield + Channel._brpop_read = saved + patch_mod.time.monotonic = saved_monotonic + patch_mod._PATCHED = False + patch_mod._UNBLOCKED_WINDOW_SECONDS = 60 + patch_mod._UNBLOCKED_MAX_RETRIES = 20 + patch_mod._UNBLOCKED_WINDOW_STARTED_AT = 0.0 + patch_mod._UNBLOCKED_EVENT_COUNT = 0 + + +def test_unblocked_response_error_becomes_empty(): + from kombu.transport.redis import Channel + + def fake_brpop(self, **options): + raise ResponseError( + "UNBLOCKED force unblock from blocking operation, " + "instance state changed (master -> replica?)" + ) + + Channel._brpop_read = fake_brpop + apply_kombu_redis_unblocked_patch() + + ch = MagicMock() + ch.client.connection.disconnect = MagicMock() + ch.connection_errors = () + ch.ResponseError = ResponseError + + with pytest.raises(Empty): + Channel._brpop_read(ch) + ch.client.connection.disconnect.assert_called_once() + + +def test_other_response_error_reraised(): + from kombu.transport.redis import Channel + + def fake_brpop(self, **options): + raise ResponseError("WRONGTYPE") + + Channel._brpop_read = fake_brpop + apply_kombu_redis_unblocked_patch() + + ch = MagicMock() + ch.client.connection.disconnect = MagicMock() + ch.connection_errors = () + ch.ResponseError = ResponseError + + with pytest.raises(ResponseError, match="WRONGTYPE"): + Channel._brpop_read(ch) + ch.client.connection.disconnect.assert_not_called() + + +def test_unblocked_repeats_below_threshold_keep_returning_empty(): + from kombu.transport.redis import Channel + + import src.workers.kombu_redis_patch as patch_mod + + def fake_brpop(self, **options): + raise ResponseError( + "UNBLOCKED force unblock from blocking operation, " + "instance state changed (master -> replica?)" + ) + + Channel._brpop_read = fake_brpop + patch_mod._UNBLOCKED_MAX_RETRIES = 3 + apply_kombu_redis_unblocked_patch() + + ch = MagicMock() + ch.client.connection.disconnect = MagicMock() + ch.connection_errors = () + ch.ResponseError = ResponseError + + with pytest.raises(Empty): + Channel._brpop_read(ch) + with pytest.raises(Empty): + Channel._brpop_read(ch) + assert ch.client.connection.disconnect.call_count == 2 + + +def test_unblocked_threshold_breaches_and_reraises_response_error(): + from kombu.transport.redis import Channel + + import src.workers.kombu_redis_patch as patch_mod + + def fake_brpop(self, **options): + raise ResponseError( + "UNBLOCKED force unblock from blocking operation, " + "instance state changed (master -> replica?)" + ) + + Channel._brpop_read = fake_brpop + patch_mod._UNBLOCKED_MAX_RETRIES = 2 + apply_kombu_redis_unblocked_patch() + + ch = MagicMock() + ch.client.connection.disconnect = MagicMock() + ch.connection_errors = () + ch.ResponseError = ResponseError + + with pytest.raises(Empty): + Channel._brpop_read(ch) + with pytest.raises(ResponseError, match="UNBLOCKED"): + Channel._brpop_read(ch) + assert ch.client.connection.disconnect.call_count == 2 + + +def test_unblocked_counter_resets_after_window_expires(): + from kombu.transport.redis import Channel + + import src.workers.kombu_redis_patch as patch_mod + + class MonotonicSequence: + def __init__(self, values): + self._it = iter(values) + + def __call__(self): + return next(self._it) + + def fake_brpop(self, **options): + raise ResponseError( + "UNBLOCKED force unblock from blocking operation, " + "instance state changed (master -> replica?)" + ) + + Channel._brpop_read = fake_brpop + patch_mod._UNBLOCKED_MAX_RETRIES = 2 + patch_mod._UNBLOCKED_WINDOW_SECONDS = 5 + patch_mod.time.monotonic = MonotonicSequence([1.0, 10.0, 10.1]) + apply_kombu_redis_unblocked_patch() + + ch = MagicMock() + ch.client.connection.disconnect = MagicMock() + ch.connection_errors = () + ch.ResponseError = ResponseError + + with pytest.raises(Empty): + Channel._brpop_read(ch) + with pytest.raises(Empty): + Channel._brpop_read(ch) + with pytest.raises(ResponseError, match="UNBLOCKED"): + Channel._brpop_read(ch) diff --git a/tests/test_mcp_oauth_redis_storage.py b/tests/test_mcp_oauth_redis_storage.py new file mode 100644 index 0000000000000000000000000000000000000000..294a46eae6a2d5cfc0207b48a796aa60609527ac --- /dev/null +++ b/tests/test_mcp_oauth_redis_storage.py @@ -0,0 +1,194 @@ +import asyncio +import os +import unittest +from urllib.parse import parse_qs, urlparse + +from pydantic import AnyUrl + +os.environ.setdefault("REDIS_HOST", "localhost") +os.environ.setdefault("REDIS_PORT", "6379") + +from mcp.server.auth.provider import AuthorizationParams +from mcp.shared.auth import OAuthClientInformationFull + +from src.services.mcp.oauth_provider import BrainapiMcpOAuthProvider + + +class FakeRedis: + def __init__(self): + self.values = {} + self.sets = {} + self.expirations = {} + + def set(self, key, value, ex=None): + self.values[key] = value + if ex: + self.expirations[key] = ex + return True + + def get(self, key): + return self.values.get(key) + + def delete(self, *keys): + removed = 0 + for key in keys: + if key in self.values: + removed += 1 + self.values.pop(key, None) + if key in self.sets: + removed += 1 + self.sets.pop(key, None) + self.expirations.pop(key, None) + return removed + + def sadd(self, key, value): + self.sets.setdefault(key, set()).add(value) + return 1 + + def smembers(self, key): + return self.sets.get(key, set()) + + def srem(self, key, value): + if key not in self.sets: + return 0 + before = len(self.sets[key]) + self.sets[key].discard(value) + return before - len(self.sets[key]) + + def expire(self, key, seconds): + self.expirations[key] = seconds + return True + + +def make_provider(redis_client, pat_verifier=lambda _pat: False): + return BrainapiMcpOAuthProvider( + issuer_url="https://brainapi.example", + resource_server_url="https://brainapi.example/mcp", + valid_scopes=["brainapi"], + access_token_ttl_seconds=3600, + refresh_token_ttl_seconds=7200, + auth_code_ttl_seconds=600, + redis_client=redis_client, + pat_verifier=pat_verifier, + ) + + +class BrainapiMcpOAuthProviderRedisTests(unittest.TestCase): + def test_authorization_code_exchange_survives_provider_recreation(self): + redis_client = FakeRedis() + issuer = make_provider(redis_client) + client = OAuthClientInformationFull( + client_id="client-1", + redirect_uris=[AnyUrl("https://claude.ai/api/mcp/auth_callback")], + ) + asyncio.run(issuer.register_client(client)) + + code = issuer.issue_auth_code( + client_id="client-1", + redirect_uri=AnyUrl("https://claude.ai/api/mcp/auth_callback"), + code_challenge="challenge", + scopes=["brainapi"], + resource="https://brainapi.example/mcp", + state="state", + brainpat="brain-pat", + ) + + recreated = make_provider(redis_client) + loaded_client = asyncio.run(recreated.get_client("client-1")) + loaded_code = asyncio.run(recreated.load_authorization_code(loaded_client, code)) + token = asyncio.run(recreated.exchange_authorization_code(loaded_client, loaded_code)) + + self.assertEqual(recreated.get_pat_for_access_token(token.access_token), "brain-pat") + self.assertIsNone(asyncio.run(recreated.load_authorization_code(loaded_client, code))) + self.assertIsNotNone(asyncio.run(recreated.load_access_token(token.access_token))) + + def test_authorization_code_preserves_implicit_redirect_uri(self): + redis_client = FakeRedis() + provider = make_provider(redis_client) + + code = provider.issue_auth_code( + client_id="client-1", + redirect_uri=AnyUrl("https://claude.ai/api/mcp/auth_callback"), + redirect_uri_provided_explicitly=False, + code_challenge="challenge", + scopes=["brainapi"], + resource="https://brainapi.example/mcp", + state="state", + brainpat="brain-pat", + ) + auth_code = asyncio.run(provider.load_authorization_code(None, code)) + + self.assertFalse(auth_code.redirect_uri_provided_explicitly) + + def test_authorize_carries_redirect_uri_explicitness_to_consent(self): + provider = make_provider(FakeRedis()) + client = OAuthClientInformationFull( + client_id="client-1", + redirect_uris=[AnyUrl("https://claude.ai/api/mcp/auth_callback")], + ) + params = AuthorizationParams( + state="state", + scopes=["brainapi"], + code_challenge="challenge", + redirect_uri=AnyUrl("https://claude.ai/api/mcp/auth_callback"), + redirect_uri_provided_explicitly=False, + resource="https://brainapi.example/mcp", + ) + + url = asyncio.run(provider.authorize(client, params)) + query = parse_qs(urlparse(url).query) + + self.assertEqual(query["redirect_uri_provided_explicitly"], ["0"]) + + def test_valid_pat_can_be_used_as_bearer_token(self): + provider = make_provider(FakeRedis(), pat_verifier=lambda pat: pat == "brain-pat") + + access_token = asyncio.run(provider.load_access_token("brain-pat")) + + self.assertIsNotNone(access_token) + self.assertEqual(access_token.token, "brain-pat") + self.assertEqual(access_token.client_id, "brainpat") + self.assertEqual(access_token.scopes, ["brainapi"]) + + def test_invalid_pat_is_not_accepted_as_bearer_token(self): + provider = make_provider(FakeRedis(), pat_verifier=lambda _pat: False) + + self.assertIsNone(asyncio.run(provider.load_access_token("bad-pat"))) + + def test_refresh_exchange_survives_provider_recreation_and_revokes_old_access_token(self): + redis_client = FakeRedis() + provider = make_provider(redis_client) + client = OAuthClientInformationFull( + client_id="client-1", + redirect_uris=[AnyUrl("https://claude.ai/api/mcp/auth_callback")], + ) + asyncio.run(provider.register_client(client)) + code = provider.issue_auth_code( + client_id="client-1", + redirect_uri=AnyUrl("https://claude.ai/api/mcp/auth_callback"), + code_challenge="challenge", + scopes=["brainapi"], + resource="https://brainapi.example/mcp", + state=None, + brainpat="brain-pat", + ) + auth_code = asyncio.run(provider.load_authorization_code(client, code)) + first_token = asyncio.run(provider.exchange_authorization_code(client, auth_code)) + + recreated = make_provider(redis_client) + refresh_token = asyncio.run( + recreated.load_refresh_token(client, first_token.refresh_token) + ) + second_token = asyncio.run( + recreated.exchange_refresh_token(client, refresh_token, ["brainapi"]) + ) + + self.assertIsNone(asyncio.run(recreated.load_access_token(first_token.access_token))) + self.assertEqual( + recreated.get_pat_for_access_token(second_token.access_token), + "brain-pat", + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_mcp_traverse_graph.py b/tests/test_mcp_traverse_graph.py new file mode 100644 index 0000000000000000000000000000000000000000..da3e7e306b7533ef5afbf0162f517835120d76eb --- /dev/null +++ b/tests/test_mcp_traverse_graph.py @@ -0,0 +1,218 @@ +import os +import sys +import unittest + +ENV_DEFAULTS = { + "BRAINPAT_TOKEN": "test-token", + "MODELS_MODE": "local", + "EMBEDDINGS_LOCAL_MODEL": "local-model", + "EMBEDDINGS_SMALL_MODEL": "small-model", + "EMBEDDING_NODES_DIMENSION": "3", + "EMBEDDING_TRIPLETS_DIMENSION": "3", + "EMBEDDING_OBSERVATIONS_DIMENSION": "3", + "EMBEDDING_DATA_DIMENSION": "3", + "EMBEDDING_RELATIONSHIPS_DIMENSION": "3", + "REDIS_HOST": "localhost", + "REDIS_PORT": "6379", + "NEO4J_HOST": "localhost", + "NEO4J_PORT": "7687", + "NEO4J_USERNAME": "neo4j", + "NEO4J_PASSWORD": "password", + "MILVUS_HOST": "localhost", + "MILVUS_PORT": "19530", + "MONGO_CONNECTION_STRING": "mongodb://localhost:27017", + "CELERY_WORKER_CONCURRENCY": "1", + "OLLAMA_HOST": "localhost", + "OLLAMA_PORT": "11434", + "OLLAMA_LLM_SMALL_MODEL": "small", + "OLLAMA_LLM_LARGE_MODEL": "large", +} +for key, value in ENV_DEFAULTS.items(): + os.environ.setdefault(key, value) + +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from src.adapters.graph import GraphAdapter +from src.constants.kg import Node, Predicate +from src.core.search.traverse import flatten_neighborhood, MAX_TRAVERSE_DEPTH, MAX_TRAVERSE_HOPS + + +def _neighbor( + pred_name: str, + direction: str, + node_uuid: str, + node_name: str, + labels: list[str], + nested: list[dict] | None = None, +) -> dict: + return { + "predicate": Predicate(name=pred_name, description="", direction=direction), + "node": Node(uuid=node_uuid, name=node_name, labels=labels), + "neighbors": nested or [], + } + + +class FlattenNeighborhoodTests(unittest.TestCase): + def test_flattens_nested_neighbors(self): + neighborhood = [ + _neighbor( + "KNOWS", + "out", + "b", + "Bob", + ["PERSON"], + nested=[_neighbor("WORKS_AT", "out", "c", "Acme", ["ORGANIZATION"])], + ) + ] + hops, truncated = flatten_neighborhood( + neighborhood, + rel_types=None, + node_labels=None, + direction="both", + limit=50, + ) + self.assertFalse(truncated) + self.assertEqual(len(hops), 2) + self.assertEqual(hops[0]["depth"], 1) + self.assertEqual(hops[0]["node"]["uuid"], "b") + self.assertEqual(hops[1]["depth"], 2) + self.assertEqual(hops[1]["via_uuid"], "b") + self.assertEqual(hops[1]["node"]["uuid"], "c") + + def test_filters_by_rel_type(self): + neighborhood = [ + _neighbor("KNOWS", "out", "b", "Bob", ["PERSON"]), + _neighbor("WORKS_AT", "out", "c", "Acme", ["ORGANIZATION"]), + ] + hops, _ = flatten_neighborhood( + neighborhood, + rel_types=["WORKS_AT"], + node_labels=None, + direction="both", + limit=50, + ) + self.assertEqual(len(hops), 1) + self.assertEqual(hops[0]["predicate"]["name"], "WORKS_AT") + + def test_filters_by_node_labels(self): + neighborhood = [ + _neighbor("KNOWS", "out", "b", "Bob", ["PERSON"]), + _neighbor("LOCATED_IN", "out", "c", "Paris", ["LOCATION"]), + ] + hops, _ = flatten_neighborhood( + neighborhood, + rel_types=None, + node_labels=["LOCATION"], + direction="both", + limit=50, + ) + self.assertEqual(len(hops), 1) + self.assertEqual(hops[0]["node"]["labels"], ["LOCATION"]) + + def test_filters_by_direction(self): + neighborhood = [ + _neighbor("KNOWS", "out", "b", "Bob", ["PERSON"]), + _neighbor("KNOWS", "in", "c", "Carol", ["PERSON"]), + ] + hops, _ = flatten_neighborhood( + neighborhood, + rel_types=None, + node_labels=None, + direction="out", + limit=50, + ) + self.assertEqual(len(hops), 1) + self.assertEqual(hops[0]["direction"], "out") + + def test_respects_limit_and_sets_truncated(self): + neighborhood = [ + _neighbor("REL", "out", f"n{i}", f"Node{i}", ["THING"]) + for i in range(5) + ] + hops, truncated = flatten_neighborhood( + neighborhood, + rel_types=None, + node_labels=None, + direction="both", + limit=2, + ) + self.assertTrue(truncated) + self.assertEqual(len(hops), 2) + + +class GraphAdapterTraverseGraphTests(unittest.TestCase): + def test_traverse_graph_by_uuid(self): + start = Node(uuid="a", name="Alice", labels=["PERSON"]) + neighborhood = [_neighbor("KNOWS", "out", "b", "Bob", ["PERSON"])] + + class FakeGraphClient: + def get_by_uuid(self, uuid, brain_id): + return start if uuid == "a" else None + + def get_neighborhood(self, node, depth, brain_id): + return neighborhood + + adapter = GraphAdapter() + adapter.add_client(FakeGraphClient()) + result = adapter.traverse_graph(start_uuid="a", brain_id="default", depth=2) + self.assertEqual(result["start"]["uuid"], "a") + self.assertEqual(len(result["hops"]), 1) + self.assertFalse(result["truncated"]) + + def test_traverse_graph_by_name_and_labels(self): + start = Node(uuid="a", name="Alice", labels=["PERSON"]) + neighborhood = [] + + class FakeGraphClient: + def get_by_identification_params(self, params, brain_id, entity_types=None): + return start + + def get_neighborhood(self, node, depth, brain_id): + return neighborhood + + adapter = GraphAdapter() + adapter.add_client(FakeGraphClient()) + result = adapter.traverse_graph( + start_name="Alice", + start_labels=["PERSON"], + brain_id="default", + ) + self.assertEqual(result["start"]["name"], "Alice") + + def test_traverse_graph_requires_start_identifiers(self): + adapter = GraphAdapter() + adapter.add_client(type("Fake", (), {})()) + result = adapter.traverse_graph(brain_id="default") + self.assertIn("error", result) + + def test_traverse_graph_clamps_depth_and_limit(self): + start = Node(uuid="a", name="Alice", labels=["PERSON"]) + + class FakeGraphClient: + captured_depth = None + + def get_by_uuid(self, uuid, brain_id): + return start + + def get_neighborhood(self, node, depth, brain_id): + FakeGraphClient.captured_depth = depth + return [] + + adapter = GraphAdapter() + adapter.add_client(FakeGraphClient()) + adapter.traverse_graph( + start_uuid="a", + depth=99, + limit=999, + ) + self.assertEqual(FakeGraphClient.captured_depth, MAX_TRAVERSE_DEPTH) + + +class TraverseConstantsTests(unittest.TestCase): + def test_constants(self): + self.assertEqual(MAX_TRAVERSE_DEPTH, 5) + self.assertEqual(MAX_TRAVERSE_HOPS, 100) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_networkx_event_centric.py b/tests/test_networkx_event_centric.py new file mode 100644 index 0000000000000000000000000000000000000000..1ecd7df2969c88f7977c3df15218ab13b6b5d1db --- /dev/null +++ b/tests/test_networkx_event_centric.py @@ -0,0 +1,127 @@ +import os +import sys +import unittest + +ENV_DEFAULTS = { + "BRAINPAT_TOKEN": "test-token", + "MODELS_MODE": "local", + "EMBEDDINGS_LOCAL_MODEL": "local-model", + "EMBEDDINGS_SMALL_MODEL": "small-model", + "EMBEDDING_NODES_DIMENSION": "3", + "EMBEDDING_TRIPLETS_DIMENSION": "3", + "EMBEDDING_OBSERVATIONS_DIMENSION": "3", + "EMBEDDING_DATA_DIMENSION": "3", + "EMBEDDING_RELATIONSHIPS_DIMENSION": "3", + "REDIS_HOST": "localhost", + "REDIS_PORT": "6379", + "NEO4J_HOST": "localhost", + "NEO4J_PORT": "7687", + "NEO4J_USERNAME": "neo4j", + "NEO4J_PASSWORD": "password", + "MILVUS_HOST": "localhost", + "MILVUS_PORT": "19530", + "MONGO_CONNECTION_STRING": "mongodb://localhost:27017", + "CELERY_WORKER_CONCURRENCY": "1", + "OLLAMA_HOST": "localhost", + "OLLAMA_PORT": "11434", + "OLLAMA_LLM_SMALL_MODEL": "small", + "OLLAMA_LLM_LARGE_MODEL": "large", + "POSTGRES_HOST": "localhost", + "POSTGRES_PORT": "5432", + "POSTGRES_USERNAME": "postgres", + "POSTGRES_PASSWORD": "postgres", +} +for key, value in ENV_DEFAULTS.items(): + os.environ.setdefault(key, value) + +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +try: + from src.lib.postgresql.graph_store import _BrainGraph + from src.lib.postgresql.networkx_client import NetworkXGraphClient + + DEPS_AVAILABLE = True +except ImportError: + _BrainGraph = None + NetworkXGraphClient = None + DEPS_AVAILABLE = False + + +class _FakeStore: + def __init__(self, brain): + self._brain = brain + + def ensure_database(self, brain_id): + return None + + def get_brain(self, brain_id): + return self._brain + + +def _build_brain(): + brain = _BrainGraph("test-brain") + brain.graph.add_node("A", uuid="A", name="Alice", labels=["Person"]) + brain.graph.add_node("B", uuid="B", name="Meeting", labels=["Event"]) + brain.graph.add_node("C", uuid="C", name="Bob", labels=["Person"]) + brain.graph.add_node("D", uuid="D", name="Elsewhere", labels=["Event"]) + brain.graph.add_edge( + "A", "B", key="r1", uuid="r1", rel_type="CAUSED", flow_key="flow1" + ) + brain.graph.add_edge( + "B", "C", key="r2", uuid="r2", rel_type="LED_TO", flow_key="flow1" + ) + brain.graph.add_edge( + "B", "D", key="r3", uuid="r3", rel_type="UNRELATED", flow_key="other" + ) + return brain + + +@unittest.skipUnless(DEPS_AVAILABLE, "postgresql/networkx dependencies unavailable") +class NetworkXEventCentricTests(unittest.TestCase): + def _client(self): + client = object.__new__(NetworkXGraphClient) + client._store = _FakeStore(_build_brain()) + return client + + def test_event_path_records_helper_exists(self): + self.assertTrue(hasattr(NetworkXGraphClient, "_event_path_records")) + self.assertTrue(hasattr(NetworkXGraphClient, "_node_from_brain")) + self.assertTrue(hasattr(NetworkXGraphClient, "_predicate_from_edge")) + + def test_get_event_centric_neighbors_returns_matching_flow_paths(self): + client = self._client() + results = client.get_event_centric_neighbors(["A"], "test-brain") + self.assertEqual(len(results), 1) + n, r1, m, r2, b = results[0] + self.assertEqual(n.uuid, "A") + self.assertEqual(m.uuid, "B") + self.assertEqual(b.uuid, "C") + self.assertEqual(r1.name, "CAUSED") + self.assertEqual(r2.name, "LED_TO") + self.assertEqual(r1.flow_key, "flow1") + self.assertEqual(r2.flow_key, "flow1") + + def test_get_event_centric_neighbors_empty_for_unknown_node(self): + client = self._client() + self.assertEqual(client.get_event_centric_neighbors(["Z"], "test-brain"), []) + + def test_get_event_centric_neighbors_empty_input(self): + client = self._client() + self.assertEqual(client.get_event_centric_neighbors([], "test-brain"), []) + + def test_get_nexts_by_flow_key_returns_next_triple(self): + client = self._client() + res = client.get_nexts_by_flow_key( + [{"predicate_uuid": "r1", "flow_key": "flow1"}], "test-brain" + ) + self.assertIn("r1", res) + triples = res["r1"] + self.assertEqual(len(triples), 1) + m, r2, b = triples[0] + self.assertEqual(m.uuid, "B") + self.assertEqual(r2.name, "LED_TO") + self.assertEqual(b.uuid, "C") + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_phase_b_event_graphrag.py b/tests/test_phase_b_event_graphrag.py new file mode 100644 index 0000000000000000000000000000000000000000..7ee14accffe915446f80d952ac54aef2a7ac859c --- /dev/null +++ b/tests/test_phase_b_event_graphrag.py @@ -0,0 +1,409 @@ +import os +import sys +import time +import unittest + +ENV_DEFAULTS = { + "BRAINPAT_TOKEN": "test-token", + "MODELS_MODE": "local", + "EMBEDDINGS_LOCAL_MODEL": "local-model", + "EMBEDDINGS_SMALL_MODEL": "small-model", + "EMBEDDING_NODES_DIMENSION": "3", + "EMBEDDING_TRIPLETS_DIMENSION": "3", + "EMBEDDING_OBSERVATIONS_DIMENSION": "3", + "EMBEDDING_DATA_DIMENSION": "3", + "EMBEDDING_RELATIONSHIPS_DIMENSION": "3", + "REDIS_HOST": "localhost", + "REDIS_PORT": "6379", + "NEO4J_HOST": "localhost", + "NEO4J_PORT": "7687", + "NEO4J_USERNAME": "neo4j", + "NEO4J_PASSWORD": "password", + "MILVUS_HOST": "localhost", + "MILVUS_PORT": "19530", + "MONGO_CONNECTION_STRING": "mongodb://localhost:27017", + "CELERY_WORKER_CONCURRENCY": "1", + "OLLAMA_HOST": "localhost", + "OLLAMA_PORT": "11434", + "OLLAMA_LLM_SMALL_MODEL": "small", + "OLLAMA_LLM_LARGE_MODEL": "large", + "POSTGRES_HOST": "localhost", + "POSTGRES_PORT": "5432", + "POSTGRES_USERNAME": "postgres", + "POSTGRES_PASSWORD": "postgres", +} +for key, value in ENV_DEFAULTS.items(): + os.environ.setdefault(key, value) + +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from src.constants.kg import Node, Predicate +from src.core.search.fact_filter import personalized_pagerank +from src.services.api.controllers import retrieve as retrieve_mod + + +def _node(uuid: str, name: str, *, labels=None, happened_at=None): + return Node( + uuid=uuid, + name=name, + labels=labels or ["PERSON"], + description=f"desc-{name}", + properties={}, + happened_at=happened_at, + ) + + +def _pred(uuid: str, name: str, *, flow_key: str = "", invalid=False, deprecated=False): + props = {"invalid_at": "01/01/2020"} if invalid else {} + return Predicate( + uuid=uuid, + name=name, + description=f"desc-{name}", + flow_key=flow_key or None, + properties=props, + deprecated=deprecated, + ) + + +def _candidate( + *, + score: float, + r_uuid: str, + r2_uuid: str, + flow_key: str = "flow", + n_uuid: str = "n", + m_uuid: str = "m", + b_uuid: str = "b", + r_name: str = "MADE", + r2_name: str = "TARGETED", + n_labels=None, + m_labels=None, + b_labels=None, + session_ids=None, + happened_at=None, + entity: str = "q", +): + n = _node(n_uuid, n_uuid, labels=n_labels or ["PERSON"]) + r = _pred(r_uuid, r_name, flow_key=flow_key) + m = _node( + m_uuid, + m_uuid, + labels=m_labels or ["EVENT"], + happened_at=happened_at, + ) + r2 = _pred(r2_uuid, r2_name, flow_key=flow_key) + b = _node(b_uuid, b_uuid, labels=b_labels or ["PERSON"]) + return { + "identified_entity": entity, + "triple": (n, r, m, r2, b), + "score": score, + "key": (r_uuid, r2_uuid), + "text": retrieve_mod._format_event_fact(n, r, m, r2, b), + "chunk_ids": [], + "session_ids": list(session_ids or []), + } + + +class DiversificationTests(unittest.TestCase): + def test_coverage_beats_pure_truncation_under_max_facts(self): + ranked = [] + for i in range(8): + ranked.append( + _candidate( + score=0.01 * i, + r_uuid=f"r-hub0-{i}", + r2_uuid=f"r2-hub0-{i}", + flow_key="hub-0", + m_uuid="event-0", + session_ids=["session_1"], + ) + ) + for i in range(4): + ranked.append( + _candidate( + score=0.5 + 0.01 * i, + r_uuid=f"r-hub{i+1}", + r2_uuid=f"r2-hub{i+1}", + flow_key=f"hub-{i+1}", + m_uuid=f"event-{i+1}", + session_ids=[f"session_{i+2}"], + ) + ) + control = ranked[:5] + diversified = retrieve_mod._diversify_facts(ranked, max_facts=5) + control_hubs = {retrieve_mod._event_hub_id(c) for c in control} + control_sessions = {s for c in control for s in c["session_ids"]} + div_hubs = {retrieve_mod._event_hub_id(c) for c in diversified} + div_sessions = {s for c in diversified for s in c["session_ids"]} + self.assertGreaterEqual(len(div_hubs), len(control_hubs)) + self.assertGreater(len(div_sessions), len(control_sessions)) + self.assertEqual(len(diversified), 5) + + def test_diversify_is_deterministic(self): + ranked = [ + _candidate( + score=0.2, + r_uuid=f"r{i}", + r2_uuid=f"x{i}", + flow_key=f"h{i % 3}", + m_uuid=f"e{i % 3}", + session_ids=[f"session_{(i % 4) + 1}"], + ) + for i in range(12) + ] + a = retrieve_mod._diversify_facts(ranked, max_facts=6) + b = retrieve_mod._diversify_facts(list(reversed(ranked)), max_facts=6) + self.assertEqual([c["key"] for c in a], [c["key"] for c in b]) + + def test_diversify_max_facts_zero(self): + ranked = [_candidate(score=0.1, r_uuid="r", r2_uuid="x")] + self.assertEqual(retrieve_mod._diversify_facts(ranked, max_facts=0), []) + + def test_diversify_latency_under_budget(self): + ranked = [ + _candidate( + score=0.001 * i, + r_uuid=f"r{i}", + r2_uuid=f"x{i}", + flow_key=f"h{i % 40}", + m_uuid=f"e{i % 40}", + session_ids=[f"session_{(i % 20) + 1}"], + ) + for i in range(200) + ] + retrieve_mod._diversify_facts(ranked, max_facts=50) + samples = [] + for _ in range(5): + start = time.perf_counter() + out = retrieve_mod._diversify_facts(ranked, max_facts=50) + samples.append((time.perf_counter() - start) * 1000) + self.assertEqual(len(out), 50) + self.assertLess(min(samples), 25.0) + + +class HubCompletenessTests(unittest.TestCase): + def test_spine_scores_above_context_only(self): + spine = _candidate( + score=0.5, + r_uuid="r1", + r2_uuid="r2", + r_name="MADE", + r2_name="TARGETED", + m_uuid="e1", + ) + context = _candidate( + score=0.5, + r_uuid="r3", + r2_uuid="r4", + r_name="RELATED", + r2_name="OCCURRED_WITHIN", + m_uuid="e2", + b_uuid="ctx", + b_labels=["CONTEXT"], + ) + self.assertGreater( + retrieve_mod._hub_completeness_score(spine), + retrieve_mod._hub_completeness_score(context), + ) + + def test_completeness_prefers_full_triangle_when_scores_tied(self): + weak = _candidate( + score=0.4, + r_uuid="rw", + r2_uuid="rw2", + r_name="MENTIONED", + r2_name="NEAR", + m_uuid="ew", + m_labels=["THING"], + ) + strong = _candidate( + score=0.4, + r_uuid="rs", + r2_uuid="rs2", + r_name="MADE", + r2_name="TARGETED", + m_uuid="es", + ) + ranked = retrieve_mod._rank_facts_with_completeness([weak, strong]) + self.assertEqual(ranked[0]["key"], ("rs", "rs2")) + + def test_complementary_legs_kept_under_budget(self): + spine = _candidate( + score=0.1, + r_uuid="r-spine", + r2_uuid="r2-spine", + flow_key="same-hub", + m_uuid="event-same", + r_name="MADE", + r2_name="TARGETED", + session_ids=["session_1"], + ) + context = _candidate( + score=0.11, + r_uuid="r-ctx", + r2_uuid="r2-ctx", + flow_key="same-hub", + m_uuid="event-same", + r_name="MADE", + r2_name="OCCURRED_WITHIN", + b_uuid="place", + b_labels=["LOCATION"], + session_ids=["session_1"], + ) + other = _candidate( + score=0.12, + r_uuid="r-other", + r2_uuid="r2-other", + flow_key="other-hub", + m_uuid="event-other", + session_ids=["session_2"], + ) + noise = [ + _candidate( + score=0.8, + r_uuid=f"rn{i}", + r2_uuid=f"xn{i}", + flow_key="noise-hub", + m_uuid="event-noise", + session_ids=["session_1"], + ) + for i in range(5) + ] + curated = retrieve_mod._diversify_facts( + [spine, context, other] + noise, max_facts=3 + ) + keys = {c["key"] for c in curated} + self.assertIn(("r-spine", "r2-spine"), keys) + self.assertIn(("r-ctx", "r2-ctx"), keys) + + +class TypedPprTests(unittest.TestCase): + def test_weighted_ppr_accepts_edge_weights(self): + adjacency = { + "seed": [("spine", 1.0), ("ctx", 0.2)], + "spine": [("seed", 1.0)], + "ctx": [("seed", 0.2)], + "far": [("x", 1.0)], + "x": [("far", 1.0)], + } + scores = personalized_pagerank(adjacency, {"seed": 1.0}, iterations=40) + self.assertGreater(scores["spine"], scores["ctx"]) + self.assertGreater(scores["seed"], scores["far"]) + + def test_adjacency_skips_invalid_legs(self): + valid_n = _node("n1", "Alice") + valid_r = _pred("r1", "MADE", flow_key="f1") + event = _node("e1", "Party", labels=["EVENT"]) + valid_r2 = _pred("r2", "TARGETED", flow_key="f1") + obj = _node("b1", "Bob") + bad_r = _pred("rb", "MADE", flow_key="f2", invalid=True) + bad_r2 = _pred("rb2", "TARGETED", flow_key="f2") + neighbors = [ + (valid_n, valid_r, event, valid_r2, obj), + (valid_n, bad_r, event, bad_r2, obj), + ] + + class _Graph: + def get_event_centric_neighbors(self, seed_uuids, brain_id="default"): + return neighbors + + original = retrieve_mod.graph_adapter + retrieve_mod.graph_adapter = _Graph() + try: + adj = retrieve_mod._build_adjacency_from_seeds(["n1"], "brain") + finally: + retrieve_mod.graph_adapter = original + self.assertIn("n1", adj) + self.assertIn("e1", adj) + edge_targets = {dst for dst, _w in adj["n1"]} + self.assertIn("e1", edge_targets) + + def test_typed_adjacency_weights_spine_over_context(self): + actor = _node("n1", "Alice") + made = _pred("r1", "MADE", flow_key="f1") + event = _node("e1", "Party", labels=["EVENT"]) + targeted = _pred("r2", "TARGETED", flow_key="f1") + obj = _node("b1", "Bob") + occurred = _pred("r3", "OCCURRED_WITHIN", flow_key="f1") + place = _node("p1", "Park", labels=["LOCATION"]) + neighbors = [ + (actor, made, event, targeted, obj), + (actor, made, event, occurred, place), + ] + + class _Graph: + def get_event_centric_neighbors(self, seed_uuids, brain_id="default"): + return neighbors + + original = retrieve_mod.graph_adapter + retrieve_mod.graph_adapter = _Graph() + try: + adj = retrieve_mod._build_adjacency_from_seeds(["n1", "e1"], "brain") + finally: + retrieve_mod.graph_adapter = original + e_out = {dst: w for dst, w in adj["e1"]} + self.assertGreater(e_out.get("b1", 0), e_out.get("p1", 0)) + + def test_typed_ppr_reorder_deterministic(self): + a = _candidate(score=0.3, r_uuid="ra", r2_uuid="xa", m_uuid="ea", n_uuid="na") + b = _candidate(score=0.3, r_uuid="rb", r2_uuid="xb", m_uuid="eb", n_uuid="nb") + ppr = {"na": 0.2, "ea": 0.5, "nb": 0.2, "eb": 0.5} + forward = sorted([a, b], key=lambda c: retrieve_mod._ppr_rank_key(c, ppr)) + reverse = sorted([b, a], key=lambda c: retrieve_mod._ppr_rank_key(c, ppr)) + self.assertEqual([c["key"] for c in forward], [c["key"] for c in reverse]) + + +class PathFormatTests(unittest.TestCase): + def test_format_labels_actor_event_target(self): + n = _node("a", "Alice") + r = _pred("r1", "MADE") + m = _node("e", "Wedding", happened_at="2024-05-01", labels=["EVENT"]) + r2 = _pred("r2", "TARGETED") + b = _node("b", "Bob") + line = retrieve_mod._format_event_fact(n, r, m, r2, b) + self.assertIn("Actor:", line) + self.assertIn("Event:", line) + self.assertIn("Target:", line) + self.assertIn("@2024-05-01", line) + self.assertNotIn("hub→hub", line.lower()) + + def test_format_labels_context_for_occurred_within(self): + n = _node("a", "Alice") + r = _pred("r1", "MADE") + m = _node("e", "Picnic", labels=["EVENT"]) + r2 = _pred("r2", "OCCURRED_WITHIN") + b = _node("p", "Park", labels=["LOCATION"]) + line = retrieve_mod._format_event_fact(n, r, m, r2, b) + self.assertIn("Context:", line) + self.assertNotIn("Target:", line) + + +class TemporalConflictMetaTests(unittest.TestCase): + def test_recency_prefer_and_emit_conflicts(self): + older = _candidate( + score=0.2, + r_uuid="ro", + r2_uuid="xo", + flow_key="h-old", + m_uuid="e-old", + n_uuid="alice", + happened_at="01/01/2020", + ) + newer = _candidate( + score=0.2, + r_uuid="rn", + r2_uuid="xn", + flow_key="h-new", + m_uuid="e-new", + n_uuid="alice", + happened_at="01/01/2024", + ) + ranked = retrieve_mod._rank_facts_with_completeness([older, newer]) + self.assertEqual(ranked[0]["key"], ("rn", "xn")) + conflicts = retrieve_mod._temporal_conflict_meta([older, newer]) + self.assertTrue(conflicts) + self.assertEqual(conflicts[0]["entity_uuid"], "alice") + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_phase_c_hub_bridges.py b/tests/test_phase_c_hub_bridges.py new file mode 100644 index 0000000000000000000000000000000000000000..0c66865497d5b841f2086b327e3c0592a14bd77b --- /dev/null +++ b/tests/test_phase_c_hub_bridges.py @@ -0,0 +1,780 @@ +import os +import sys +import unittest +from unittest.mock import MagicMock, patch + +ENV_DEFAULTS = { + "BRAINPAT_TOKEN": "test-token", + "MODELS_MODE": "local", + "EMBEDDINGS_LOCAL_MODEL": "local-model", + "EMBEDDINGS_SMALL_MODEL": "small-model", + "EMBEDDING_NODES_DIMENSION": "3", + "EMBEDDING_TRIPLETS_DIMENSION": "3", + "EMBEDDING_OBSERVATIONS_DIMENSION": "3", + "EMBEDDING_DATA_DIMENSION": "3", + "EMBEDDING_RELATIONSHIPS_DIMENSION": "3", + "REDIS_HOST": "localhost", + "REDIS_PORT": "6379", + "NEO4J_HOST": "localhost", + "NEO4J_PORT": "7687", + "NEO4J_USERNAME": "neo4j", + "NEO4J_PASSWORD": "password", + "MILVUS_HOST": "localhost", + "MILVUS_PORT": "19530", + "MONGO_CONNECTION_STRING": "mongodb://localhost:27017", + "CELERY_WORKER_CONCURRENCY": "1", + "OLLAMA_HOST": "localhost", + "OLLAMA_PORT": "11434", + "OLLAMA_LLM_SMALL_MODEL": "small", + "OLLAMA_LLM_LARGE_MODEL": "large", + "POSTGRES_HOST": "localhost", + "POSTGRES_PORT": "5432", + "POSTGRES_USERNAME": "postgres", + "POSTGRES_PASSWORD": "postgres", +} +for key, value in ENV_DEFAULTS.items(): + os.environ.setdefault(key, value) + +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from src.constants.kg import Node, Predicate +from src.core.saving.hub_bridges import ( + HubBridge, + bridges_from_memberships, + canonical_event_pair, + entity_event_memberships, + select_bridge_neighbors, +) +from src.services.api.controllers import retrieve as retrieve_mod + + +def _node(uuid: str, name: str, *, labels=None): + return Node( + uuid=uuid, + name=name, + labels=labels or ["PERSON"], + description=f"desc-{name}", + properties={}, + ) + + +def _pred(uuid: str, name: str, *, flow_key: str = ""): + return Predicate( + uuid=uuid, + name=name, + description=f"desc-{name}", + flow_key=flow_key or None, + properties={}, + ) + + +def _candidate( + *, + score: float, + r_uuid: str, + r2_uuid: str, + flow_key: str, + m_uuid: str, + n_uuid: str = "n", + b_uuid: str = "b", +): + n = _node(n_uuid, n_uuid) + r = _pred(r_uuid, "MADE", flow_key=flow_key) + m = _node(m_uuid, m_uuid, labels=["EVENT"]) + r2 = _pred(r2_uuid, "TARGETED", flow_key=flow_key) + b = _node(b_uuid, b_uuid) + return { + "identified_entity": "q", + "triple": (n, r, m, r2, b), + "score": score, + "key": (r_uuid, r2_uuid), + "text": f"fact-{m_uuid}", + "chunk_ids": [], + } + + +class HubBridgeIndexTests(unittest.TestCase): + def test_canonical_pair_orders_lexicographically(self): + self.assertEqual(canonical_event_pair("b", "a"), ("a", "b")) + + def test_build_bridges_from_shared_actor(self): + rows = [ + ("event-1", "melanie", "Melanie", "MADE"), + ("event-2", "melanie", "Melanie", "MADE"), + ("event-1", "park", "Park", "OCCURRED_WITHIN"), + ("event-2", "park", "Park", "OCCURRED_WITHIN"), + ("event-3", "jon", "Jon", "TARGETED"), + ] + membership = entity_event_memberships(rows) + bridges = bridges_from_memberships(membership) + self.assertEqual(len(bridges), 1) + self.assertEqual(bridges[0].event_a, "event-1") + self.assertEqual(bridges[0].event_b, "event-2") + self.assertEqual(bridges[0].shared_entity, "melanie") + + def test_rebuild_idempotent_membership(self): + rows = [ + ("e2", "ent", "Ent", "MADE"), + ("e1", "ent", "Ent", "TARGETED"), + ("e2", "ent", "Ent", "MADE"), + ] + first = bridges_from_memberships(entity_event_memberships(rows)) + second = bridges_from_memberships(entity_event_memberships(rows)) + self.assertEqual(first, second) + + def test_select_bridge_neighbors_respects_max_and_determinism(self): + bridges = [ + HubBridge("e1", "e2", "a", "A", 1.0), + HubBridge("e1", "e3", "b", "B", 2.0), + HubBridge("e1", "e4", "c", "C", 1.5), + ] + a = select_bridge_neighbors(["e1"], bridges, max_per_hub=2) + b = select_bridge_neighbors(["e1"], bridges, max_per_hub=2) + self.assertEqual(a, b) + self.assertEqual([n for n, _ in a], ["e3", "e4"]) + + def test_prefer_bridge_to_novel_source_session(self): + bridges = [ + HubBridge("e1", "e_same", "shared", "Shared", 2.0), + HubBridge("e1", "e_novel", "shared", "Shared", 1.0), + ] + chosen = select_bridge_neighbors( + ["e1"], + bridges, + max_per_hub=1, + seed_sessions={"session_1"}, + hub_sessions={ + "e_same": {"session_1"}, + "e_novel": {"session_5"}, + }, + ) + self.assertEqual([n for n, _ in chosen], ["e_novel"]) + + def test_novel_session_ranking_is_deterministic(self): + bridges = [ + HubBridge("e1", "e_a", "ent", "Ent", 1.0), + HubBridge("e1", "e_b", "ent", "Ent", 1.0), + HubBridge("e1", "e_c", "ent", "Ent", 1.0), + ] + kwargs = dict( + seed_sessions={"session_1"}, + hub_sessions={ + "e_a": {"session_1"}, + "e_b": {"session_5"}, + "e_c": {"session_1", "session_7"}, + }, + max_per_hub=2, + ) + a = select_bridge_neighbors(["e1"], bridges, **kwargs) + b = select_bridge_neighbors(["e1"], bridges, **kwargs) + self.assertEqual(a, b) + self.assertEqual([n for n, _ in a], ["e_b", "e_c"]) + + +class BridgeExpandTests(unittest.TestCase): + def test_one_bridge_expand_adds_cross_event_candidates(self): + seed = _candidate( + score=0.1, + r_uuid="r1", + r2_uuid="r2", + flow_key="f1", + m_uuid="event-seed", + n_uuid="actor", + b_uuid="obj1", + ) + bridged_n = _node("actor2", "actor2") + bridged_r = _pred("r3", "MADE", flow_key="f2") + bridged_m = _node("event-bridge", "event-bridge", labels=["EVENT"]) + bridged_r2 = _pred("r4", "TARGETED", flow_key="f2") + bridged_b = _node("obj2", "obj2") + bridge = HubBridge( + event_a="event-bridge", + event_b="event-seed", + shared_entity="actor", + shared_entity_name="actor", + weight=1.0, + ) + + mock_adapter = MagicMock() + mock_adapter.get_hub_bridges.return_value = [bridge] + mock_adapter.get_event_hub_facts.return_value = [ + (bridged_n, bridged_r, bridged_m, bridged_r2, bridged_b) + ] + + with patch.object(retrieve_mod, "graph_adapter", mock_adapter): + expanded, paths = retrieve_mod._expand_cross_event_bridges( + [seed], + "brain", + max_per_hub=3, + ) + + self.assertEqual(len(expanded), 2) + hubs = {retrieve_mod._event_hub_id(c) for c in expanded} + self.assertEqual(hubs, {"event-seed", "event-bridge"}) + self.assertEqual(len(paths), 1) + self.assertEqual(sorted(paths[0]["hubs"]), ["event-bridge", "event-seed"]) + + def test_paths_for_curated_attaches_legs(self): + seed = _candidate( + score=0.1, + r_uuid="r1", + r2_uuid="r2", + flow_key="f1", + m_uuid="event-seed", + ) + bridged = _candidate( + score=0.2, + r_uuid="r3", + r2_uuid="r4", + flow_key="f2", + m_uuid="event-bridge", + n_uuid="actor2", + b_uuid="obj2", + ) + path_meta = [ + { + "hubs": ["event-bridge", "event-seed"], + "shared_entity": "actor", + "shared_entity_name": "actor", + "weight": 1.0, + } + ] + kept = retrieve_mod._paths_for_curated(path_meta, [seed, bridged]) + self.assertEqual(len(kept), 1) + self.assertEqual(kept[0]["legs"], ["fact-event-bridge", "fact-event-seed"]) + dropped = retrieve_mod._paths_for_curated(path_meta, [seed]) + self.assertEqual(dropped, []) + + def test_bridge_expand_disabled_when_zero(self): + seed = _candidate( + score=0.1, + r_uuid="r1", + r2_uuid="r2", + flow_key="f1", + m_uuid="event-seed", + ) + expanded, paths = retrieve_mod._expand_cross_event_bridges( + [seed], + "brain", + max_per_hub=0, + ) + self.assertEqual(expanded, [seed]) + self.assertEqual(paths, []) + + def test_diversify_after_expand_respects_max_facts(self): + candidates = [ + _candidate( + score=0.01 * i, + r_uuid=f"r{i}", + r2_uuid=f"rr{i}", + flow_key=f"f{i}", + m_uuid=f"event-{i}", + n_uuid=f"n{i}", + b_uuid=f"b{i}", + ) + for i in range(8) + ] + curated = retrieve_mod._diversify_facts(candidates, max_facts=3) + self.assertEqual(len(curated), 3) + + def test_expand_then_diversify_deterministic(self): + seed = _candidate( + score=0.2, + r_uuid="r1", + r2_uuid="r2", + flow_key="f1", + m_uuid="event-seed", + ) + bridged = [] + for i in range(3): + bridged.append( + ( + _node(f"n{i}", f"n{i}"), + _pred(f"br{i}", "MADE", flow_key=f"bf{i}"), + _node(f"event-b{i}", f"event-b{i}", labels=["EVENT"]), + _pred(f"brr{i}", "TARGETED", flow_key=f"bf{i}"), + _node(f"bb{i}", f"bb{i}"), + ) + ) + bridges = [ + HubBridge("event-b0", "event-seed", "shared", "Shared", 1.0), + HubBridge("event-b1", "event-seed", "shared", "Shared", 1.0), + HubBridge("event-b2", "event-seed", "shared", "Shared", 1.0), + ] + mock_adapter = MagicMock() + mock_adapter.get_hub_bridges.return_value = bridges + mock_adapter.get_event_hub_facts.return_value = bridged + + with patch.object(retrieve_mod, "graph_adapter", mock_adapter): + a, _ = retrieve_mod._expand_cross_event_bridges( + [seed], "brain", max_per_hub=2 + ) + b, _ = retrieve_mod._expand_cross_event_bridges( + [seed], "brain", max_per_hub=2 + ) + self.assertEqual( + [c["key"] for c in a], + [c["key"] for c in b], + ) + curated_a = retrieve_mod._diversify_facts( + retrieve_mod._rank_facts_with_completeness(a), max_facts=2 + ) + curated_b = retrieve_mod._diversify_facts( + retrieve_mod._rank_facts_with_completeness(b), max_facts=2 + ) + self.assertEqual( + [c["key"] for c in curated_a], + [c["key"] for c in curated_b], + ) + self.assertEqual(len(curated_a), 2) + + + def test_expand_selects_by_weight_without_session_pool(self): + seed = _candidate( + score=0.1, + r_uuid="r1", + r2_uuid="r2", + flow_key="f1", + m_uuid="event-seed", + ) + seed["session_ids"] = ["session_1"] + seed["chunk_ids"] = ["chunk-seed"] + + same_n = _node("n-same", "n-same") + same_r = _pred("rs", "MADE", flow_key="fs") + same_m = _node("event-same", "event-same", labels=["EVENT"]) + same_r2 = _pred("rrs", "TARGETED", flow_key="fs") + same_b = _node("b-same", "b-same") + for item in (same_n, same_r, same_m, same_r2, same_b): + item.properties = {"source_chunk_ids": ["chunk-same"]} + + novel_n = _node("n-novel", "n-novel") + novel_r = _pred("rn", "MADE", flow_key="fn") + novel_m = _node("event-novel", "event-novel", labels=["EVENT"]) + novel_r2 = _pred("rrn", "TARGETED", flow_key="fn") + novel_b = _node("b-novel", "b-novel") + for item in (novel_n, novel_r, novel_m, novel_r2, novel_b): + item.properties = {"source_chunk_ids": ["chunk-novel"]} + + bridges = [ + HubBridge("event-same", "event-seed", "shared", "Shared", 2.0), + HubBridge("event-novel", "event-seed", "shared", "Shared", 1.0), + ] + mock_adapter = MagicMock() + mock_adapter.get_hub_bridges.return_value = bridges + mock_adapter.get_event_hub_facts.return_value = [ + (same_n, same_r, same_m, same_r2, same_b), + (novel_n, novel_r, novel_m, novel_r2, novel_b), + ] + + chunk_sessions = { + "chunk-seed": ["session_1"], + "chunk-same": ["session_1"], + "chunk-novel": ["session_5"], + } + + with patch.object(retrieve_mod, "graph_adapter", mock_adapter): + with patch.object( + retrieve_mod, + "_resolve_chunk_sessions", + side_effect=lambda ids, _brain: { + cid: chunk_sessions[cid] + for cid in ids + if cid in chunk_sessions + }, + ): + expanded, _paths = retrieve_mod._expand_cross_event_bridges( + [seed], + "brain", + max_per_hub=1, + ) + + hubs = {retrieve_mod._event_hub_id(c) for c in expanded} + self.assertIn("event-seed", hubs) + self.assertIn("event-same", hubs) + self.assertNotIn("event-novel", hubs) + mock_adapter.get_event_hub_facts.assert_called_once() + fetched = mock_adapter.get_event_hub_facts.call_args[0][0] + self.assertEqual(fetched, ["event-same"]) + + def test_reserved_slots_keep_novel_bridge_session_under_max_facts(self): + seeds = [ + _candidate( + score=0.01 * i, + r_uuid=f"rs{i}", + r2_uuid=f"rrs{i}", + flow_key=f"fs{i}", + m_uuid=f"event-seed-{i}", + n_uuid=f"ns{i}", + b_uuid=f"bs{i}", + ) + for i in range(8) + ] + for seed in seeds: + seed["session_ids"] = ["session_1"] + + bridges = [ + _candidate( + score=5.0 + 0.01 * i, + r_uuid=f"rb{i}", + r2_uuid=f"rrb{i}", + flow_key=f"fb{i}", + m_uuid=f"event-bridge-{i}", + n_uuid=f"nb{i}", + b_uuid=f"bb{i}", + ) + for i in range(3) + ] + for bridge in bridges: + bridge["session_ids"] = ["session_5"] + bridge["bridge"] = { + "from_hub": "event-seed-0", + "to_hub": bridge["triple"][2].uuid, + "shared_entity": "shared", + "shared_entity_name": "Shared", + } + + ranked = seeds + bridges + max_facts = 5 + + pool = [retrieve_mod._prepare_diversify_item(c) for c in ranked] + pool.sort(key=lambda item: (-item["relevance"], item["tie"])) + without_reserve: list[dict] = [] + selected_meta: list[dict] = [] + selected_hubs: set[str] = set() + selected_sessions: set[str] = set() + while len(without_reserve) < max_facts and pool: + idx = retrieve_mod._mmr_pick_index( + pool, selected_meta, selected_hubs, selected_sessions + ) + chosen = pool.pop(idx) + without_reserve.append(chosen["candidate"]) + selected_meta.append(chosen) + selected_hubs.add(chosen["hub"]) + selected_sessions.update(chosen["sessions"]) + without_sessions = { + s for c in without_reserve for s in (c.get("session_ids") or []) + } + self.assertNotIn("session_5", without_sessions) + + curated = retrieve_mod._diversify_facts(ranked, max_facts=max_facts) + self.assertEqual(len(curated), max_facts) + curated_sessions = { + s for c in curated for s in (c.get("session_ids") or []) + } + self.assertIn("session_5", curated_sessions) + self.assertTrue(any(c.get("bridge") for c in curated)) + + again = retrieve_mod._diversify_facts( + list(reversed(ranked)), max_facts=max_facts + ) + self.assertEqual( + [c["key"] for c in curated], + [c["key"] for c in again], + ) + + def test_build_fact_channel_keeps_candidate_session_ids(self): + seed = _candidate( + score=0.1, + r_uuid="r1", + r2_uuid="r2", + flow_key="f1", + m_uuid="event-seed", + ) + seed["session_ids"] = ["session_1"] + seed["chunk_ids"] = ["chunk-missing-from-map"] + bridge = _candidate( + score=5.0, + r_uuid="rb", + r2_uuid="rrb", + flow_key="fb", + m_uuid="event-bridge", + n_uuid="nb", + b_uuid="bb", + ) + bridge["session_ids"] = ["session_5"] + bridge["chunk_ids"] = ["chunk-bridge-missing"] + bridge["bridge"] = { + "from_hub": "event-seed", + "to_hub": "event-bridge", + "shared_entity": "shared", + "shared_entity_name": "Shared", + } + curated = retrieve_mod._diversify_facts([seed, bridge], max_facts=2) + text_lines, triples, graph_sessions = retrieve_mod._build_fact_channel( + curated, chunk_sessions={} + ) + self.assertIn("session_5", graph_sessions) + self.assertTrue(any("session_5" in line for line in text_lines)) + emitted = { + sid + for t in triples + for sid in (t.source_session_ids or []) + } + self.assertIn("session_5", emitted) + + def test_rank_bridge_seed_hubs_total_order_ignores_arrival(self): + candidates = [ + _candidate( + score=0.4, + r_uuid="r1", + r2_uuid="x1", + flow_key="f1", + m_uuid="hub-b", + ), + _candidate( + score=0.1, + r_uuid="r2", + r2_uuid="x2", + flow_key="f2", + m_uuid="hub-a", + ), + _candidate( + score=0.1, + r_uuid="r3", + r2_uuid="x3", + flow_key="f3", + m_uuid="hub-c", + ), + _candidate( + score=0.2, + r_uuid="r4", + r2_uuid="x4", + flow_key="f4", + m_uuid="hub-b", + ), + ] + a = retrieve_mod._rank_bridge_seed_hubs(candidates, cap=3) + b = retrieve_mod._rank_bridge_seed_hubs(list(reversed(candidates)), cap=3) + self.assertEqual(a, b) + self.assertEqual(a, ["hub-a", "hub-c", "hub-b"]) + + def test_expand_marks_reserve_ok_only_for_top_reserve_hubs(self): + seeds = [ + _candidate( + score=0.01 * i, + r_uuid=f"rs{i}", + r2_uuid=f"rrs{i}", + flow_key=f"fs{i}", + m_uuid=f"event-seed-{i:02d}", + n_uuid=f"ns{i}", + b_uuid=f"bs{i}", + ) + for i in range(10) + ] + bridges = [ + HubBridge( + f"event-bridge-{i}", + f"event-seed-{i:02d}", + f"ent-{i}", + f"Ent{i}", + 1.0, + ) + for i in range(10) + ] + facts = [] + for i in range(10): + facts.append( + ( + _node(f"nb{i}", f"nb{i}"), + _pred(f"rb{i}", "MADE", flow_key=f"fb{i}"), + _node( + f"event-bridge-{i}", + f"event-bridge-{i}", + labels=["EVENT"], + ), + _pred(f"rrb{i}", "TARGETED", flow_key=f"fb{i}"), + _node(f"bb{i}", f"bb{i}"), + ) + ) + mock_adapter = MagicMock() + mock_adapter.get_hub_bridges.return_value = bridges + mock_adapter.get_event_hub_facts.return_value = facts + with patch.object(retrieve_mod, "graph_adapter", mock_adapter): + expanded, _ = retrieve_mod._expand_cross_event_bridges( + seeds, "brain", max_per_hub=1 + ) + by_to = { + c["bridge"]["to_hub"]: c["bridge"] + for c in expanded + if c.get("bridge") + } + reserve_cap = retrieve_mod._BRIDGE_RESERVE_HUB_CAP + for i in range(10): + meta = by_to[f"event-bridge-{i}"] + self.assertEqual(meta["reserve_ok"], i < reserve_cap) + + def test_reserved_slots_ignore_unstable_tail_hub_bridges(self): + seeds = [ + _candidate( + score=0.01 * i, + r_uuid=f"rs{i}", + r2_uuid=f"rrs{i}", + flow_key=f"fs{i}", + m_uuid=f"event-seed-{i}", + n_uuid=f"ns{i}", + b_uuid=f"bs{i}", + ) + for i in range(8) + ] + for seed in seeds: + seed["session_ids"] = ["session_1"] + + stable = _candidate( + score=5.0, + r_uuid="rb0", + r2_uuid="rrb0", + flow_key="fb0", + m_uuid="event-bridge-stable", + n_uuid="nb0", + b_uuid="bb0", + ) + stable["session_ids"] = ["session_5"] + stable["bridge"] = { + "from_hub": "event-seed-0", + "to_hub": "event-bridge-stable", + "shared_entity": "shared", + "shared_entity_name": "Shared", + "reserve_ok": True, + } + + jitter_a = _candidate( + score=5.1, + r_uuid="rb1", + r2_uuid="rrb1", + flow_key="fb1", + m_uuid="event-bridge-jitter-a", + n_uuid="nb1", + b_uuid="bb1", + ) + jitter_a["session_ids"] = ["session_9"] + jitter_a["bridge"] = { + "from_hub": "event-seed-9", + "to_hub": "event-bridge-jitter-a", + "shared_entity": "shared", + "shared_entity_name": "Shared", + "reserve_ok": False, + } + + jitter_b = _candidate( + score=5.1, + r_uuid="rb2", + r2_uuid="rrb2", + flow_key="fb2", + m_uuid="event-bridge-jitter-b", + n_uuid="nb2", + b_uuid="bb2", + ) + jitter_b["session_ids"] = ["session_13"] + jitter_b["bridge"] = { + "from_hub": "event-seed-9", + "to_hub": "event-bridge-jitter-b", + "shared_entity": "shared", + "shared_entity_name": "Shared", + "reserve_ok": False, + } + + curated_a = retrieve_mod._diversify_facts( + seeds + [stable, jitter_a], max_facts=5 + ) + curated_b = retrieve_mod._diversify_facts( + seeds + [stable, jitter_b], max_facts=5 + ) + sessions_a = { + s for c in curated_a for s in (c.get("session_ids") or []) + } + sessions_b = { + s for c in curated_b for s in (c.get("session_ids") or []) + } + self.assertEqual(sessions_a, sessions_b) + self.assertIn("session_5", sessions_a) + self.assertNotIn("session_9", sessions_a) + self.assertNotIn("session_13", sessions_b) + + def test_expand_then_diversify_stable_under_tail_seed_jitter(self): + core = [ + _candidate( + score=0.01 * i, + r_uuid=f"rs{i}", + r2_uuid=f"rrs{i}", + flow_key=f"fs{i}", + m_uuid=f"event-seed-{i:02d}", + n_uuid=f"ns{i}", + b_uuid=f"bs{i}", + ) + for i in range(retrieve_mod._BRIDGE_RESERVE_HUB_CAP) + ] + for seed in core: + seed["session_ids"] = ["session_1"] + seed["chunk_ids"] = ["chunk-seed"] + + def run_with_tail(tail_hub: str, novel_hub: str, novel_session: str): + tail = _candidate( + score=0.9, + r_uuid=f"rt-{tail_hub}", + r2_uuid=f"rrt-{tail_hub}", + flow_key=f"ft-{tail_hub}", + m_uuid=tail_hub, + n_uuid=f"nt-{tail_hub}", + b_uuid=f"bt-{tail_hub}", + ) + tail["session_ids"] = ["session_1"] + tail["chunk_ids"] = ["chunk-seed"] + seeds = core + [tail] + bridges = [ + HubBridge("event-bridge-core", "event-seed-00", "ent-c", "C", 1.0), + HubBridge(novel_hub, tail_hub, "ent-t", "T", 1.0), + ] + core_fact = ( + _node("nbc", "nbc"), + _pred("rbc", "MADE", flow_key="fbc"), + _node("event-bridge-core", "event-bridge-core", labels=["EVENT"]), + _pred("rrbc", "TARGETED", flow_key="fbc"), + _node("bbc", "bbc"), + ) + for item in core_fact: + item.properties = {"source_chunk_ids": ["chunk-core"]} + novel_fact = ( + _node("nbn", "nbn"), + _pred("rbn", "MADE", flow_key="fbn"), + _node(novel_hub, novel_hub, labels=["EVENT"]), + _pred("rrbn", "TARGETED", flow_key="fbn"), + _node("bbn", "bbn"), + ) + for item in novel_fact: + item.properties = {"source_chunk_ids": ["chunk-novel"]} + mock_adapter = MagicMock() + mock_adapter.get_hub_bridges.return_value = bridges + mock_adapter.get_event_hub_facts.return_value = [core_fact, novel_fact] + chunk_sessions = { + "chunk-seed": ["session_1"], + "chunk-core": ["session_5"], + "chunk-novel": [novel_session], + } + with patch.object(retrieve_mod, "graph_adapter", mock_adapter): + with patch.object( + retrieve_mod, + "_resolve_chunk_sessions", + side_effect=lambda ids, _brain: { + cid: chunk_sessions[cid] + for cid in ids + if cid in chunk_sessions + }, + ): + expanded, _ = retrieve_mod._expand_cross_event_bridges( + seeds, "brain", max_per_hub=1 + ) + return retrieve_mod._diversify_facts(expanded, max_facts=5) + + a = run_with_tail("event-seed-90", "event-bridge-a", "session_9") + b = run_with_tail("event-seed-91", "event-bridge-b", "session_13") + sessions_a = {s for c in a for s in (c.get("session_ids") or [])} + sessions_b = {s for c in b for s in (c.get("session_ids") or [])} + self.assertEqual(sessions_a, sessions_b) + self.assertIn("session_5", sessions_a) + self.assertNotIn("session_9", sessions_a) + self.assertNotIn("session_13", sessions_b) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_plugin_refactors.py b/tests/test_plugin_refactors.py new file mode 100644 index 0000000000000000000000000000000000000000..173cd025db1ae6ce8d9da520020a740dd4601f6d --- /dev/null +++ b/tests/test_plugin_refactors.py @@ -0,0 +1,145 @@ +import tempfile +import unittest +from pathlib import Path + +from src.core.plugins.catalog import PluginManifestCatalog +from src.core.plugins.entrypoints import PluginEntryPointResolver +from src.core.plugins.loader import PluginLoader +from src.core.plugins.manifest import MANIFEST_FILENAME + + +def write_manifest( + plugin_dir: Path, + *, + name: str, + version: str = "1.0.0", + entry_point: str = "main.py", + priority: int = 100, +) -> None: + plugin_dir.mkdir(parents=True, exist_ok=True) + content = ( + f"name: {name}\n" + f"version: {version}\n" + f"entry_point: \"{entry_point}\"\n" + f"priority: {priority}\n" + ) + (plugin_dir / MANIFEST_FILENAME).write_text(content, encoding="utf-8") + + +class PluginEntryPointResolverTests(unittest.TestCase): + def test_resolver_handles_python_suffix_safely(self): + resolver = PluginEntryPointResolver() + resolved = resolver.resolve("mycopy.py", Path("/tmp/demo-plugin")) + self.assertEqual(resolved, "mycopy") + + def test_resolver_maps_init_to_package(self): + resolver = PluginEntryPointResolver() + self.assertEqual( + resolver.resolve("__init__.py", Path("/tmp/demo-plugin")), + "demo-plugin", + ) + self.assertEqual( + resolver.resolve("pkg/__init__.py", Path("/tmp/demo-plugin")), + "pkg", + ) + + def test_resolver_normalizes_relative_paths(self): + resolver = PluginEntryPointResolver() + resolved = resolver.resolve("./nested/module.py", Path("/tmp/demo-plugin")) + self.assertEqual(resolved, "nested.module") + + +class PluginManifestCatalogTests(unittest.TestCase): + def test_catalog_can_toggle_validation(self): + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + write_manifest(root / "valid", name="valid", entry_point="main.py") + write_manifest(root / "invalid", name="invalid", entry_point="") + + catalog = PluginManifestCatalog(root) + + non_validated = catalog.discover(validate=False) + validated = catalog.discover(validate=True) + + self.assertEqual({m.name for m in non_validated}, {"valid", "invalid"}) + self.assertEqual({m.name for m in validated}, {"valid"}) + + +class PluginLoaderRefactorTests(unittest.TestCase): + class DummyContext: + def __init__(self): + self.loaded = [] + + def test_loader_discovers_valid_plugins_sorted_by_priority(self): + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + write_manifest(root / "slow", name="slow", priority=20, entry_point="main.py") + write_manifest(root / "fast", name="fast", priority=5, entry_point="main.py") + write_manifest(root / "broken", name="broken", priority=1, entry_point="") + + loader = PluginLoader(root, self.DummyContext()) + discovered = loader.discover() + + self.assertEqual([m.name for m in discovered], ["fast", "slow"]) + + def test_loader_uses_entry_point_resolver_for_python_module(self): + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + plugin_dir = root / "mycopy" + write_manifest(plugin_dir, name="mycopy", entry_point="mycopy.py") + (plugin_dir / "mycopy.py").write_text( + "def register(context):\n context.loaded.append('mycopy')\n", + encoding="utf-8", + ) + manifest = loader_manifest = PluginManifestCatalog(root).discover(validate=True)[0] + loader = PluginLoader(root, self.DummyContext()) + + loaded = loader.load(loader_manifest) + + self.assertTrue(loaded) + self.assertEqual(loader.context.loaded, ["mycopy"]) + self.assertIn(manifest.name, loader.loaded_plugins) + + def test_loader_isolates_modules_for_plugins_with_same_entrypoint_name(self): + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + alpha_dir = root / "alpha" + beta_dir = root / "beta" + write_manifest(alpha_dir, name="alpha", entry_point="main.py", priority=1) + write_manifest(beta_dir, name="beta", entry_point="main.py", priority=2) + + (alpha_dir / "main.py").write_text( + "def register(context):\n" + " from routes.shared import marker\n" + " context.loaded.append(marker())\n", + encoding="utf-8", + ) + (beta_dir / "main.py").write_text( + "def register(context):\n" + " from routes.shared import marker\n" + " context.loaded.append(marker())\n", + encoding="utf-8", + ) + + (alpha_dir / "routes").mkdir(parents=True, exist_ok=True) + (beta_dir / "routes").mkdir(parents=True, exist_ok=True) + (alpha_dir / "routes" / "shared.py").write_text( + "def marker():\n" + " return 'alpha'\n", + encoding="utf-8", + ) + (beta_dir / "routes" / "shared.py").write_text( + "def marker():\n" + " return 'beta'\n", + encoding="utf-8", + ) + + loader = PluginLoader(root, self.DummyContext()) + results = loader.load_all() + + self.assertEqual(results, {"alpha": True, "beta": True}) + self.assertEqual(loader.context.loaded, ["alpha", "beta"]) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_postgresql_graph_read_query.py b/tests/test_postgresql_graph_read_query.py new file mode 100644 index 0000000000000000000000000000000000000000..9daa6394ab2084bd0c3af37cd351c80fc699d4b7 --- /dev/null +++ b/tests/test_postgresql_graph_read_query.py @@ -0,0 +1,219 @@ +import json +import os +import sys +import unittest +from unittest.mock import MagicMock, patch + +ENV_DEFAULTS = { + "BRAINPAT_TOKEN": "test-token", + "MODELS_MODE": "local", + "EMBEDDINGS_LOCAL_MODEL": "local-model", + "EMBEDDINGS_SMALL_MODEL": "small-model", + "EMBEDDING_NODES_DIMENSION": "3", + "EMBEDDING_TRIPLETS_DIMENSION": "3", + "EMBEDDING_OBSERVATIONS_DIMENSION": "3", + "EMBEDDING_DATA_DIMENSION": "3", + "EMBEDDING_RELATIONSHIPS_DIMENSION": "3", + "REDIS_HOST": "localhost", + "REDIS_PORT": "6379", + "NEO4J_HOST": "localhost", + "NEO4J_PORT": "7687", + "NEO4J_USERNAME": "neo4j", + "NEO4J_PASSWORD": "password", + "MILVUS_HOST": "localhost", + "MILVUS_PORT": "19530", + "MONGO_CONNECTION_STRING": "mongodb://localhost:27017", + "CELERY_WORKER_CONCURRENCY": "1", + "OLLAMA_HOST": "localhost", + "OLLAMA_PORT": "11434", + "OLLAMA_LLM_SMALL_MODEL": "small", + "OLLAMA_LLM_LARGE_MODEL": "large", + "POSTGRES_HOST": "localhost", + "POSTGRES_PORT": "5432", + "POSTGRES_USERNAME": "postgres", + "POSTGRES_PASSWORD": "postgres", +} +for key, value in ENV_DEFAULTS.items(): + os.environ.setdefault(key, value) + +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +import importlib.util +from pathlib import Path + +_read_query_path = ( + Path(__file__).resolve().parent.parent / "src/lib/postgresql/read_query.py" +) +_read_query_spec = importlib.util.spec_from_file_location( + "brainapi_read_query", _read_query_path +) +_read_query_module = importlib.util.module_from_spec(_read_query_spec) +assert _read_query_spec.loader is not None +_read_query_spec.loader.exec_module(_read_query_module) +validate_read_only_sql = _read_query_module.validate_read_only_sql +ReadQueryValidationError = _read_query_module.ReadQueryValidationError + +from src.adapters.graph import GraphAdapter + +try: + from src.lib.postgresql.graph_store import GraphDatabaseError, PostgreSQLGraphStore +except ImportError: + GraphDatabaseError = Exception + PostgreSQLGraphStore = None + + +class ValidateReadOnlySqlTests(unittest.TestCase): + def test_accepts_select(self): + sql = validate_read_only_sql("SELECT uuid, data FROM kg_nodes LIMIT 5") + self.assertEqual(sql, "SELECT uuid, data FROM kg_nodes LIMIT 5") + + def test_accepts_with_recursive(self): + query = ( + "WITH RECURSIVE walk AS (" + "SELECT source_uuid, target_uuid FROM kg_relationships " + "WHERE source_uuid = 'abc' " + "UNION ALL SELECT r.source_uuid, r.target_uuid " + "FROM walk w JOIN kg_relationships r ON r.source_uuid = w.target_uuid " + "WHERE w.depth < 2) SELECT * FROM walk" + ) + self.assertTrue(validate_read_only_sql(query).startswith("WITH RECURSIVE")) + + def test_accepts_table_shorthand(self): + sql = validate_read_only_sql("TABLE kg_nodes") + self.assertEqual(sql, "TABLE kg_nodes") + + def test_rejects_empty_query(self): + with self.assertRaises(ReadQueryValidationError): + validate_read_only_sql(" ") + + def test_rejects_multiple_statements(self): + with self.assertRaises(ReadQueryValidationError): + validate_read_only_sql("SELECT 1; DELETE FROM kg_nodes") + + def test_rejects_insert(self): + with self.assertRaises(ReadQueryValidationError): + validate_read_only_sql("INSERT INTO kg_nodes VALUES ('x', '{}')") + + def test_rejects_delete(self): + with self.assertRaises(ReadQueryValidationError): + validate_read_only_sql("DELETE FROM kg_nodes") + + def test_rejects_drop(self): + with self.assertRaises(ReadQueryValidationError): + validate_read_only_sql("DROP TABLE kg_nodes") + + def test_rejects_invalid_start_keyword(self): + with self.assertRaises(ReadQueryValidationError): + validate_read_only_sql("UPDATE kg_nodes SET data = '{}'") + + def test_rejects_cypher_match(self): + with self.assertRaises(ReadQueryValidationError) as ctx: + validate_read_only_sql( + "MATCH (n:PERSON) WHERE n.uuid = 'abc' RETURN n" + ) + self.assertIn("Cypher", str(ctx.exception)) + + def test_rejects_explain_cypher(self): + with self.assertRaises(ReadQueryValidationError) as ctx: + validate_read_only_sql( + "EXPLAIN MATCH (n) RETURN n.uuid AS uuid LIMIT 50" + ) + self.assertIn("Cypher", str(ctx.exception)) + + def test_rejects_legacy_nodes_table(self): + with self.assertRaises(ReadQueryValidationError) as ctx: + validate_read_only_sql("SELECT COUNT(*) AS total FROM nodes") + self.assertIn("kg_nodes", str(ctx.exception)) + + def test_rejects_table_nodes_shorthand(self): + with self.assertRaises(ReadQueryValidationError) as ctx: + validate_read_only_sql("TABLE nodes") + self.assertIn("kg_nodes", str(ctx.exception)) + + def test_rejects_incomplete_with_recursive_unbalanced_parens(self): + with self.assertRaises(ReadQueryValidationError) as ctx: + validate_read_only_sql( + "WITH RECURSIVE walk AS (" + "SELECT source_uuid, target_uuid, rel_type, 1 AS depth " + "FROM kg_relationships WHERE source_uuid = 'abc'" + ) + self.assertIn("Incomplete SQL", str(ctx.exception)) + + def test_rejects_recursive_cte_fragment_without_outer_select(self): + with self.assertRaises(ReadQueryValidationError) as ctx: + validate_read_only_sql( + "WITH RECURSIVE walk AS (" + "SELECT source_uuid, target_uuid, rel_type, 1 AS depth " + "FROM kg_relationships WHERE source_uuid = 'abc')" + ) + msg = str(ctx.exception) + self.assertTrue( + "Incomplete WITH RECURSIVE" in msg or "Incomplete SQL" in msg + ) + + def test_rejects_orphaned_union_all_branch(self): + with self.assertRaises(ReadQueryValidationError) as ctx: + validate_read_only_sql( + "SELECT r.source_uuid, r.target_uuid, r.rel_type, w.depth + 1 " + "FROM walk w JOIN kg_relationships r ON r.source_uuid = w.target_uuid " + "WHERE w.depth < 3) SELECT DISTINCT w.source_uuid FROM walk w LIMIT 50" + ) + self.assertIn("Incomplete SQL", str(ctx.exception)) + + +class ExecuteReadQueryTests(unittest.TestCase): + @unittest.skipIf(PostgreSQLGraphStore is None, "postgresql dependencies unavailable") + def test_applies_row_cap_and_truncated_flag(self): + store = PostgreSQLGraphStore() + rows = [{"uuid": f"n{i}", "data": {"name": f"node{i}"}} for i in range(101)] + mock_cursor = MagicMock() + mock_cursor.fetchmany.return_value = rows + mock_cursor.__enter__ = MagicMock(return_value=mock_cursor) + mock_cursor.__exit__ = MagicMock(return_value=False) + + mock_conn = MagicMock() + mock_conn.cursor.return_value = mock_cursor + mock_conn.__enter__ = MagicMock(return_value=mock_conn) + mock_conn.__exit__ = MagicMock(return_value=False) + + with patch.object(store, "_connection", return_value=mock_conn): + with patch.object(store, "_ensure_brain_schema"): + result = store.execute_read_query("default", "SELECT uuid, data FROM kg_nodes") + + self.assertTrue(result["truncated"]) + self.assertEqual(len(result["records"]), 100) + + +class GraphOperationSerializationTests(unittest.TestCase): + def test_execute_operation_serializes_postgresql_read_result(self): + class FakeGraphClient: + def execute_operation(self, operation: str, brain_id: str): + return { + "records": [{"uuid": "abc", "data": {"name": "Alice"}}], + "truncated": False, + } + + adapter = GraphAdapter() + adapter.add_client(FakeGraphClient()) + result = adapter.execute_operation( + "SELECT uuid, data FROM kg_nodes LIMIT 1", + brain_id="test-brain", + ) + payload = json.loads(result) + self.assertFalse(payload["truncated"]) + self.assertEqual(payload["records"][0]["uuid"], "abc") + + def test_execute_operation_returns_error_string_on_validation_failure(self): + class FakeGraphClient: + def execute_operation(self, operation: str, brain_id: str): + raise GraphDatabaseError("Only read-only SELECT queries are allowed") + + adapter = GraphAdapter() + adapter.add_client(FakeGraphClient()) + result = adapter.execute_operation("DELETE FROM kg_nodes", brain_id="default") + self.assertIn("Error executing graph operation", result) + self.assertIn("read-only", result) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_production_hardening.py b/tests/test_production_hardening.py new file mode 100644 index 0000000000000000000000000000000000000000..0aadbfd8985a393aa919970b17ceda7d865c4138 --- /dev/null +++ b/tests/test_production_hardening.py @@ -0,0 +1,123 @@ +from __future__ import annotations + +import os +from unittest.mock import patch + +import pytest +from fastapi import FastAPI +from fastapi.testclient import TestClient + +from src.services.api.errors import install_error_handlers +from src.services.api.middlewares.auth import BrainPATMiddleware +from src.services.api.routes.system import system_router +from src.services.brain_lifecycle import BrainLifecycleResult +from src.workers.redis_url import redis_connection_url + + +def test_malformed_authorization_fails_closed_with_401(): + app = FastAPI() + install_error_handlers(app) + app.add_middleware(BrainPATMiddleware) + + @app.get("/system/test") + async def protected(): + return {"ok": True} + + with patch.dict(os.environ, {"BRAINPAT_TOKEN": "system-token"}): + response = TestClient(app).get( + "/system/test", headers={"Authorization": "malformed"} + ) + + assert response.status_code == 401 + assert response.json()["error"]["code"] == "AUTH_INVALID" + + +@pytest.mark.parametrize( + ("path", "method", "operation"), + [ + ("/system/brains/demo/create-backup", "post", "create-backup"), + ], +) +def test_unfinished_system_routes_return_structured_501(path, method, operation): + app = FastAPI() + install_error_handlers(app) + app.include_router(system_router) + + response = getattr(TestClient(app), method)(path) + + assert response.status_code == 501 + assert response.json()["error"]["code"] == "not_implemented" + assert response.json()["operation"] == operation + + +class _LifecycleStub: + def clear(self, brain_id: str): + return BrainLifecycleResult( + brain_id=brain_id, + operation="clear", + existed=True, + cleared_backends=["data", "graph", "vectors", "cache"], + ) + + def delete(self, brain_id: str): + return BrainLifecycleResult( + brain_id=brain_id, + operation="delete", + existed=True, + cleared_backends=["data", "graph", "vectors", "cache"], + ) + + +def test_brain_lifecycle_routes_use_non_get_destructive_methods(): + app = FastAPI() + install_error_handlers(app) + app.include_router(system_router) + + with patch( + "src.services.api.routes.system.get_brain_lifecycle_service", + return_value=_LifecycleStub(), + ): + client = TestClient(app) + cleared = client.post("/system/brains/demo/clear") + deleted = client.delete("/system/brains/demo") + + assert cleared.status_code == 200 + assert cleared.json()["operation"] == "clear" + assert deleted.status_code == 200 + assert deleted.json()["operation"] == "delete" + + +def test_cors_defaults_and_production_wildcard_guard(): + from src.services.api.app import _cors_allowed_origins + + with patch.dict(os.environ, {"ENV": "production", "CORS_ALLOWED_ORIGINS": ""}): + assert _cors_allowed_origins() == [] + with patch.dict(os.environ, {"ENV": "development", "CORS_ALLOWED_ORIGINS": ""}): + assert _cors_allowed_origins() == ["*"] + with patch.dict( + os.environ, {"ENV": "production", "CORS_ALLOWED_ORIGINS": "*"} + ): + with pytest.raises(RuntimeError, match="development"): + _cors_allowed_origins() + + +def test_plugin_failures_are_fatal_by_default_in_production(): + from src.services.api.app import _enforce_plugin_results + + with patch.dict(os.environ, {"ENV": "production"}, clear=False): + with pytest.raises(RuntimeError, match="demo"): + _enforce_plugin_results({"demo": False}) + with patch.dict( + os.environ, + {"ENV": "production", "PLUGIN_FAILURE_POLICY": "warn"}, + clear=False, + ): + _enforce_plugin_results({"demo": False}) + + +def test_worker_redis_url_propagates_encoded_password(): + assert ( + redis_connection_url("redis", 6379, "secret:/@ value") + == "redis://:secret%3A%2F%40%20value@redis:6379/0" + ) + assert redis_connection_url("redis", "6379", None) == "redis://redis:6379/0" diff --git a/tests/test_quantity_node_identity.py b/tests/test_quantity_node_identity.py new file mode 100644 index 0000000000000000000000000000000000000000..101633fbe95411351b73a2702fd7b912916905d4 --- /dev/null +++ b/tests/test_quantity_node_identity.py @@ -0,0 +1,143 @@ +import json +import os +import re +import unittest + + +ENV_DEFAULTS = { + "BRAINPAT_TOKEN": "test-token", + "MODELS_MODE": "local", + "EMBEDDINGS_LOCAL_MODEL": "local-model", + "EMBEDDINGS_SMALL_MODEL": "small-model", + "EMBEDDING_NODES_DIMENSION": "3", + "EMBEDDING_TRIPLETS_DIMENSION": "3", + "EMBEDDING_OBSERVATIONS_DIMENSION": "3", + "EMBEDDING_DATA_DIMENSION": "3", + "EMBEDDING_RELATIONSHIPS_DIMENSION": "3", + "REDIS_HOST": "localhost", + "REDIS_PORT": "6379", + "NEO4J_HOST": "localhost", + "NEO4J_PORT": "7687", + "NEO4J_USERNAME": "neo4j", + "NEO4J_PASSWORD": "password", + "MILVUS_HOST": "localhost", + "MILVUS_PORT": "19530", + "MONGO_CONNECTION_STRING": "mongodb://localhost:27017", + "CELERY_WORKER_CONCURRENCY": "1", + "OLLAMA_HOST": "localhost", + "OLLAMA_PORT": "11434", + "OLLAMA_LLM_SMALL_MODEL": "small", + "OLLAMA_LLM_LARGE_MODEL": "large", +} +for key, value in ENV_DEFAULTS.items(): + os.environ.setdefault(key, value) + +from src.constants.prompts import architect_agent, janitor_agent, scout_agent +from src.core.saving.identity import stable_node_id + + +_PLACEHOLDER_TYPES = { + "MONEY", + "UNIT", + "UNITS", + "AMOUNT", + "QUANTITY", + "COUNT", + "CURRENCY", + "NUMBER", +} +_ENTITY_PATTERN = re.compile(r"\{\{[^{}]*\"type\"\s*:[^{}]*\}\}") + + +def _prompt_constants(module) -> dict[str, str]: + return { + name: value + for name, value in vars(module).items() + if name.isupper() and isinstance(value, str) + } + + +def _demonstrated_entities(prompt: str) -> list[dict]: + entities: list[dict] = [] + for match in _ENTITY_PATTERN.finditer(prompt): + body = match.group(0).replace("{{", "{").replace("}}", "}") + try: + parsed = json.loads(body) + except json.JSONDecodeError: + continue + if isinstance(parsed, dict) and parsed.get("type") and parsed.get("name"): + entities.append(parsed) + return entities + + +def _shared_hub_entities(prompt: str) -> list[dict]: + shared: list[dict] = [] + for entity in _demonstrated_entities(prompt): + entity_type = str(entity["type"]).strip() + name = str(entity["name"]).strip() + if entity_type.upper() in _PLACEHOLDER_TYPES: + shared.append(entity) + elif name.lower() == entity_type.lower(): + shared.append(entity) + return shared + + +def _fact_node_ids(prompt: str, event_name: str, happened_at: str) -> set[str]: + ids = {stable_node_id(event_name, "EVENT", happened_at)} + for entity in _shared_hub_entities(prompt): + ids.add(stable_node_id(entity["name"], entity["type"])) + return ids + + +class QuantityNodeIdentityTests(unittest.TestCase): + def test_two_monetary_amounts_do_not_share_a_node_id(self): + for module in (scout_agent, architect_agent): + for name, prompt in _prompt_constants(module).items(): + acme = _fact_node_ids(prompt, "Raised", "19/01/2026") + globex = _fact_node_ids(prompt, "Raised", "03/04/2026") + self.assertEqual( + acme & globex, + set(), + f"{module.__name__}.{name} demonstrates a node shared by two " + f"unrelated monetary facts", + ) + + def test_prompts_demonstrate_no_type_named_placeholder_nodes(self): + offenders: list[str] = [] + for module in (scout_agent, architect_agent): + for name, prompt in _prompt_constants(module).items(): + for entity in _shared_hub_entities(prompt): + offenders.append( + f"{module.__name__}.{name}: " + f"{entity['type']}/{entity['name']}" + ) + self.assertEqual(offenders, []) + + def test_prompts_do_not_instruct_unit_entities(self): + offenders: list[str] = [] + for module in (scout_agent, architect_agent): + for name, prompt in _prompt_constants(module).items(): + if re.search(r"\"Unit\".*as an entity", prompt): + offenders.append(f"{module.__name__}.{name}") + if "quantitative units" in prompt: + offenders.append(f"{module.__name__}.{name}") + self.assertEqual(offenders, []) + + def test_janitor_still_prescribes_amount_migration(self): + prompts = "\n".join(_prompt_constants(janitor_agent).values()) + self.assertIn("Amount Migration", prompts) + self.assertIn("amount-nodes", prompts) + + def test_type_named_identity_is_constant_across_facts(self): + self.assertEqual( + stable_node_id("Money", "MONEY"), + stable_node_id("Money", "MONEY"), + ) + self.assertNotEqual( + stable_node_id("Raised", "EVENT", "19/01/2026"), + stable_node_id("Raised", "EVENT", "03/04/2026"), + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_recommend_recsys.py b/tests/test_recommend_recsys.py new file mode 100644 index 0000000000000000000000000000000000000000..148522614ea871b0f9c86b6d5b5730f58e5e6d44 --- /dev/null +++ b/tests/test_recommend_recsys.py @@ -0,0 +1,253 @@ +import os +import sys +import unittest +from datetime import datetime, timedelta +from unittest.mock import MagicMock, patch + +ENV_DEFAULTS = { + "BRAINPAT_TOKEN": "test-token", + "MODELS_MODE": "local", + "EMBEDDINGS_LOCAL_MODEL": "local-model", + "EMBEDDINGS_SMALL_MODEL": "small-model", + "EMBEDDING_NODES_DIMENSION": "3", + "EMBEDDING_TRIPLETS_DIMENSION": "3", + "EMBEDDING_OBSERVATIONS_DIMENSION": "3", + "EMBEDDING_DATA_DIMENSION": "3", + "EMBEDDING_RELATIONSHIPS_DIMENSION": "3", + "REDIS_HOST": "localhost", + "REDIS_PORT": "6379", + "NEO4J_HOST": "localhost", + "NEO4J_PORT": "7687", + "NEO4J_USERNAME": "neo4j", + "NEO4J_PASSWORD": "password", + "MILVUS_HOST": "localhost", + "MILVUS_PORT": "19530", + "MONGO_CONNECTION_STRING": "mongodb://localhost:27017", + "CELERY_WORKER_CONCURRENCY": "1", + "OLLAMA_HOST": "localhost", + "OLLAMA_PORT": "11434", + "OLLAMA_LLM_SMALL_MODEL": "small", + "OLLAMA_LLM_LARGE_MODEL": "large", + "POSTGRES_HOST": "localhost", + "POSTGRES_PORT": "5432", + "POSTGRES_USERNAME": "postgres", + "POSTGRES_PASSWORD": "postgres", +} +for key, value in ENV_DEFAULTS.items(): + os.environ.setdefault(key, value) + +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from src.constants.kg import Node, Predicate +from src.core.search.recommend import ( + DEFAULT_BEHAVIOR_WEIGHTS, + EntityRecommendRetriever, + behavior_weight, + degree_dampen, + recency_decay, + attr_idf, +) + + +def _node(uuid: str, name: str, labels: list[str], happened_at=None) -> Node: + return Node.model_construct( + uuid=uuid, + name=name, + labels=labels, + happened_at=happened_at, + properties={}, + ) + + +def _pred(name: str = "MADE", weight=None) -> Predicate: + props = {"weight": weight} if weight is not None else {} + return Predicate.model_construct( + uuid=f"pred-{name}-{weight}", + name=name, + description=name, + properties=props, + deprecated=False, + ) + + +class BehaviorWeightTests(unittest.TestCase): + def test_purchase_higher_than_view(self): + self.assertEqual(behavior_weight("Purchase"), 1.0) + self.assertEqual(behavior_weight("View"), 0.2) + self.assertEqual(behavior_weight("AddToCart"), 0.5) + + def test_unknown_defaults_to_view_weight(self): + self.assertEqual(behavior_weight("WeirdEvent"), 0.2) + + def test_favorite_between_cart_and_purchase(self): + self.assertEqual(behavior_weight("Favorite"), 0.7) + self.assertEqual(behavior_weight("wishlist"), 0.7) + self.assertEqual(behavior_weight("add_to_favorite"), 0.7) + self.assertGreater(behavior_weight("Favorite"), behavior_weight("AddToCart")) + self.assertLess(behavior_weight("Favorite"), behavior_weight("Purchase")) + + def test_follow_is_half(self): + self.assertEqual(behavior_weight("Follow"), 0.5) + self.assertEqual(behavior_weight("FLW"), 0.5) + self.assertEqual(behavior_weight("follow"), 0.5) + self.assertGreater(behavior_weight("Follow"), behavior_weight("WeirdEvent")) + self.assertLess(behavior_weight("Follow"), behavior_weight("Favorite")) + + def test_custom_table(self): + self.assertEqual(behavior_weight("View", {"view": 0.9}), 0.9) + + +class RecencyDecayTests(unittest.TestCase): + def test_off_when_half_life_none(self): + self.assertEqual(recency_decay("01/01/2020", None), 1.0) + + def test_older_decays_more(self): + now = datetime(2026, 8, 6) + older = recency_decay("01/01/2020", 30.0, now=now) + newer = recency_decay("01/08/2026", 30.0, now=now) + self.assertLess(older, newer) + self.assertLessEqual(newer, 1.0) + + def test_half_life_halves(self): + now = datetime(2026, 8, 6) + past = (now - timedelta(days=30)).strftime("%d/%m/%Y") + score = recency_decay(past, 30.0, now=now) + self.assertAlmostEqual(score, 0.5, places=2) + + +class DegreeDampenTests(unittest.TestCase): + def test_disabled(self): + self.assertEqual(degree_dampen(100, False), 1.0) + + def test_hub_lower_than_rare(self): + self.assertLess(degree_dampen(100, True), degree_dampen(1, True)) + + def test_attr_idf_monotonic(self): + self.assertLess(attr_idf(50), attr_idf(2)) + + +class RecommendExcludeSeenTests(unittest.TestCase): + @patch("src.core.search.recommend.graph_adapter") + @patch("src.core.search.recommend.EntitySinergyRetriever") + def test_exclude_seen_keeps_collaborative_unseen(self, mock_syn_cls, mock_graph): + user = _node("user:u1", "u1", ["USER"]) + peer = _node("user:u2", "u2", ["USER"]) + shared = _node("item:a", "a", ["PRODUCT"]) + fresh = _node("item:b", "b", ["PRODUCT"]) + evt_u1 = _node("evt:1", "Purchase", ["EVENT"], happened_at="01/08/2026") + evt_u2_shared = _node("evt:2", "Purchase", ["EVENT"], happened_at="01/08/2026") + evt_u2_fresh = _node("evt:3", "Purchase", ["EVENT"], happened_at="01/08/2026") + + mock_syn = MagicMock() + mock_syn.retrieve_sibilings.return_value = (user, [], [], []) + mock_syn_cls.return_value = mock_syn + + def get_neighbors(uuids, of_types=None, brain_id="default"): + uid = uuids[0] + if uid == user.uuid: + return {user.uuid: [(_pred("MADE"), evt_u1)]} + if uid == evt_u1.uuid: + return { + evt_u1.uuid: [ + (_pred("TARGETED"), shared), + (_pred("MADE"), user), + ] + } + if uid == shared.uuid: + return { + shared.uuid: [ + (_pred("TARGETED"), evt_u1), + (_pred("TARGETED"), evt_u2_shared), + ] + } + if uid == evt_u2_shared.uuid: + return { + evt_u2_shared.uuid: [ + (_pred("MADE"), peer), + (_pred("TARGETED"), shared), + ] + } + if uid == peer.uuid: + return { + peer.uuid: [ + (_pred("MADE"), evt_u2_shared), + (_pred("MADE"), evt_u2_fresh), + ] + } + if uid == evt_u2_fresh.uuid: + return { + evt_u2_fresh.uuid: [ + (_pred("MADE"), peer), + (_pred("TARGETED"), fresh), + ] + } + return {uid: []} + + mock_graph.get_neighbors.side_effect = get_neighbors + + retriever = EntityRecommendRetriever("demorecsys") + _, recs = retriever.recommend( + "u1", + include_asymmetric=True, + include_multi_interest=False, + include_attribute_pref=False, + exclude_seen=True, + diversify=False, + labels=["PRODUCT"], + top_k=10, + ) + uuids = {r["node"].uuid for r in recs} + self.assertNotIn(shared.uuid, uuids) + self.assertIn(fresh.uuid, uuids) + + @patch("src.core.search.recommend.graph_adapter") + @patch("src.core.search.recommend.EntitySinergyRetriever") + def test_behavior_weights_rank_purchase_above_view( + self, mock_syn_cls, mock_graph + ): + user = _node("user:u1", "u1", ["USER"]) + p_view = _node("item:v", "v", ["PRODUCT"]) + p_buy = _node("item:b", "b", ["PRODUCT"]) + evt_view = _node("evt:v", "View", ["EVENT"], happened_at="01/08/2026") + evt_buy = _node("evt:b", "Purchase", ["EVENT"], happened_at="01/08/2026") + + mock_syn = MagicMock() + mock_syn.retrieve_sibilings.return_value = (user, [], [], []) + mock_syn_cls.return_value = mock_syn + + def get_neighbors(uuids, of_types=None, brain_id="default"): + uid = uuids[0] + if uid == user.uuid: + return { + user.uuid: [ + (_pred("MADE"), evt_view), + (_pred("MADE"), evt_buy), + ] + } + if uid == evt_view.uuid: + return {evt_view.uuid: [(_pred("TARGETED"), p_view)]} + if uid == evt_buy.uuid: + return {evt_buy.uuid: [(_pred("TARGETED"), p_buy)]} + return {uid: []} + + mock_graph.get_neighbors.side_effect = get_neighbors + + retriever = EntityRecommendRetriever("demorecsys") + _, recs = retriever.recommend( + "u1", + include_asymmetric=True, + include_multi_interest=False, + include_attribute_pref=False, + exclude_seen=False, + diversify=False, + labels=["PRODUCT"], + top_k=10, + behavior_weights=DEFAULT_BEHAVIOR_WEIGHTS, + ) + self.assertGreaterEqual(len(recs), 2) + by_uuid = {r["node"].uuid: r["score"] for r in recs} + self.assertGreater(by_uuid[p_buy.uuid], by_uuid[p_view.uuid]) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_recsys_export_edges.py b/tests/test_recsys_export_edges.py new file mode 100644 index 0000000000000000000000000000000000000000..d7a8bf46a3041831e01221076ab47dddab5d0db1 --- /dev/null +++ b/tests/test_recsys_export_edges.py @@ -0,0 +1,57 @@ +import unittest +from types import SimpleNamespace + +from plugins_path_helper import load_export_edges + + +class FakePage: + def __init__(self, results, total): + self.results = results + self.total = total + + +class FakeGraph: + def __init__(self, users, hubs): + self.users = users + self.hubs = hubs + + def search_entities(self, brain_id="default", limit=10, skip=0, node_labels=None, query_text=None): + assert node_labels == ["USER"] + chunk = self.users[skip : skip + limit] + return FakePage(chunk, len(self.users)) + + def get_event_centric_neighbors(self, nodes, brain_id="default"): + return self.hubs + + +class ExportEdgesTests(unittest.TestCase): + def test_export_two_hubs(self): + export_edges = load_export_edges() + u1 = SimpleNamespace(uuid="user:u1", name="u1", labels=["USER"]) + u2 = SimpleNamespace(uuid="user:u2", name="u2", labels=["USER"]) + e1 = SimpleNamespace(uuid="evt:1", name="Purchase", labels=["EVENT"]) + e2 = SimpleNamespace(uuid="evt:2", name="View", labels=["EVENT"]) + p1 = SimpleNamespace(uuid="item:sku-1", name="sku-1", labels=["PRODUCT"]) + p2 = SimpleNamespace(uuid="item:sku-2", name="sku-2", labels=["PRODUCT"]) + pred = SimpleNamespace(name="MADE", uuid="r1") + pred2 = SimpleNamespace(name="TARGETED", uuid="r2") + hubs = [ + (u1, pred, e1, pred2, p1), + (u2, pred, e2, pred2, p2), + ] + graph = FakeGraph([u1, u2], hubs) + edges = export_edges.export_user_item_edges(graph, "demorecsys") + self.assertEqual(len(edges), 2) + by_user = {e["user_id"]: e for e in edges} + self.assertEqual(by_user["u1"]["item_id"], "sku-1") + self.assertEqual(by_user["u1"]["weight"], 3.0) + self.assertEqual(by_user["u2"]["weight"], 1.0) + + def test_refuse_forbidden_brain(self): + export_edges = load_export_edges() + with self.assertRaises(ValueError): + export_edges.export_user_item_edges(FakeGraph([], []), "beam1m1clean") + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_recsys_lightgcn.py b/tests/test_recsys_lightgcn.py new file mode 100644 index 0000000000000000000000000000000000000000..d250ee2e7235af25bd44d9aa0f25a71ba99e18b7 --- /dev/null +++ b/tests/test_recsys_lightgcn.py @@ -0,0 +1,56 @@ +import tempfile +import unittest +from pathlib import Path + + +class LightGCNTrainSmokeTests(unittest.TestCase): + def test_train_and_infer_toy_edges(self): + import sys + + root = Path(__file__).resolve().parents[1] + plugin = root / "plugins" / "recsys-gnn" + if not (plugin / "models" / "lightgcn.py").is_file(): + self.skipTest("optional recsys-gnn plugin is not installed") + if str(plugin) not in sys.path: + sys.path.insert(0, str(plugin)) + + from models.lightgcn import train_and_save + from models.artifacts import load_artifacts + from models.infer import rank_items + + edges = [] + for u in range(5): + for i in range(3): + edges.append( + { + "user_id": f"u{u}", + "item_id": f"sku-{i}", + "user_uuid": f"user:u{u}", + "item_uuid": f"item:sku-{i}", + "user_name": f"u{u}", + "item_name": f"sku-{i}", + "weight": 1.0, + } + ) + with tempfile.TemporaryDirectory() as tmp: + meta = train_and_save( + edges, + brain_id="demorecsys", + epochs=3, + embedding_dim=8, + n_layers=2, + out_dir=Path(tmp), + ) + self.assertEqual(meta["n_users"], 5) + self.assertEqual(meta["n_items"], 3) + arts = load_artifacts("demorecsys", base=Path(tmp)) + self.assertIsNotNone(arts) + self.assertEqual(arts.user_emb.shape, (5, 8)) + self.assertEqual(arts.item_emb.shape, (3, 8)) + out = rank_items(arts, "u0", top_k=2, exclude_seen=False) + self.assertEqual(out["status"], "ok") + self.assertEqual(len(out["items"]), 2) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_recsys_movielens.py b/tests/test_recsys_movielens.py new file mode 100644 index 0000000000000000000000000000000000000000..30c3abd5cacddf0d17be1ab1043b11b13503132d --- /dev/null +++ b/tests/test_recsys_movielens.py @@ -0,0 +1,43 @@ +import tempfile +import unittest +from pathlib import Path + + +class MovieLensNormalizeTests(unittest.TestCase): + def test_filter_and_stats(self): + import sys + + root = Path(__file__).resolve().parents[1] + bench = root / "benchmarks" + if str(bench) not in sys.path: + sys.path.insert(0, str(bench)) + + from recsys.dataset import dataset_stats, filter_interactions, write_jsonl + from recsys.evaluate import load_interactions + + rows = [] + for u in range(5): + for i in range(u + 2): # users 0..4 have 2..6 interactions + rows.append( + { + "user_id": f"mlu{u}", + "item_id": f"mlm{i}", + "behavior": "purchase", + "timestamp": f"2024-01-0{i+1}T00:00:00Z", + } + ) + with tempfile.TemporaryDirectory() as tmp: + path = write_jsonl(rows, Path(tmp) / "x.jsonl") + loaded = load_interactions(path) + self.assertEqual(len(loaded), len(rows)) + filtered = filter_interactions( + loaded, min_interactions=4, max_users=10 + ) + # users 2,3,4 have 4,5,6 ix + self.assertEqual(len({r["user_id"] for r in filtered}), 3) + stats = dataset_stats(filtered) + self.assertEqual(stats["n_users"], 3) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_release_version.py b/tests/test_release_version.py new file mode 100644 index 0000000000000000000000000000000000000000..e5d1f05bbf5ef06111b73d6dc266b38117322b03 --- /dev/null +++ b/tests/test_release_version.py @@ -0,0 +1,33 @@ +from __future__ import annotations + +import pytest + +from scripts.release_version import ReleaseVersion, parse_release_tag + + +@pytest.mark.parametrize( + ("tag", "expected"), + [ + ("v2.19.0", ReleaseVersion("2.19.0", "2.19.0")), + ("v2.19.0-rc.1", ReleaseVersion("2.19.0-rc.1", "2.19.0rc1")), + ("v2.19.0-dev", ReleaseVersion("2.19.0-dev", "2.19.0.dev0")), + ("v2.19.0-dev.3", ReleaseVersion("2.19.0-dev.3", "2.19.0.dev3")), + ], +) +def test_parse_release_tag(tag: str, expected: ReleaseVersion): + assert parse_release_tag(tag) == expected + + +@pytest.mark.parametrize( + "tag", + [ + "2.19.0", + "v2.19", + "v2.19.0-rc", + "v2.19.0-dev.foo", + "v2.19.0-beta.1", + ], +) +def test_parse_release_tag_rejects_unsupported_formats(tag: str): + with pytest.raises(ValueError, match="Unsupported release tag"): + parse_release_tag(tag) diff --git a/tests/test_search_api.py b/tests/test_search_api.py new file mode 100644 index 0000000000000000000000000000000000000000..07feb9367bce165f2f9e70bc1bb1ce61fb6bfe6b --- /dev/null +++ b/tests/test_search_api.py @@ -0,0 +1,393 @@ +import os +import unittest +from unittest.mock import MagicMock, patch + +ENV_DEFAULTS = { + "BRAINPAT_TOKEN": "test-token", + "MODELS_MODE": "local", + "EMBEDDINGS_LOCAL_MODEL": "local-model", + "EMBEDDINGS_SMALL_MODEL": "small-model", + "EMBEDDING_NODES_DIMENSION": "3", + "EMBEDDING_TRIPLETS_DIMENSION": "3", + "EMBEDDING_OBSERVATIONS_DIMENSION": "3", + "EMBEDDING_DATA_DIMENSION": "3", + "EMBEDDING_RELATIONSHIPS_DIMENSION": "3", + "REDIS_HOST": "localhost", + "REDIS_PORT": "6379", + "NEO4J_HOST": "localhost", + "NEO4J_PORT": "7687", + "NEO4J_USERNAME": "neo4j", + "NEO4J_PASSWORD": "password", + "MILVUS_HOST": "localhost", + "MILVUS_PORT": "19530", + "MONGO_CONNECTION_STRING": "mongodb://localhost:27017", + "CELERY_WORKER_CONCURRENCY": "1", + "OLLAMA_HOST": "localhost", + "OLLAMA_PORT": "11434", + "OLLAMA_LLM_SMALL_MODEL": "small", + "OLLAMA_LLM_LARGE_MODEL": "large", +} +for key, value in ENV_DEFAULTS.items(): + os.environ.setdefault(key, value) + +from fastapi import HTTPException + +from src.constants.embeddings import Vector +from src.constants.data import TextChunk +from src.services.api.constants.requests import SearchRequestBody + + +class SearchApiTests(unittest.IsolatedAsyncioTestCase): + async def test_404_when_search_disabled(self): + from src.services.api.controllers import search as search_mod + + with patch.object(search_mod.config, "search_enabled", False): + with self.assertRaises(HTTPException) as ctx: + await search_mod.search(SearchRequestBody(query="license")) + self.assertEqual(ctx.exception.status_code, 404) + + async def test_fused_hits_when_both_channels_on(self): + from src.services.api.controllers import search as search_mod + + dense_vec = Vector( + id="dense-1", + metadata={"resource_id": "chunk-dense"}, + distance=0.1, + ) + bm25_chunk = TextChunk(id="chunk-bm25", text="counseling license renewal") + mock_embeddings = MagicMock() + mock_embeddings.embed_text.return_value = Vector( + id="q", embeddings=[0.1, 0.2], metadata={} + ) + mock_vs = MagicMock() + mock_vs.search_data.return_value = [dense_vec] + mock_data = MagicMock() + mock_data.search_bm25.return_value = [(bm25_chunk, 4.2)] + mock_data.get_text_chunks_by_ids.return_value = ( + [TextChunk(id="chunk-dense", text="dense passage about alice")], + [], + ) + + with ( + patch.object(search_mod.config, "search_enabled", True), + patch.object(search_mod.config, "search_use_dense", True), + patch.object(search_mod.config, "search_use_bm25", True), + patch.object(search_mod.config, "search_fusion", "rrf"), + patch.object(search_mod, "embeddings_adapter", mock_embeddings), + patch.object(search_mod, "vector_search", mock_vs), + patch.object(search_mod, "data_adapter", mock_data), + ): + response = await search_mod.search( + SearchRequestBody(query="license", k=10, profile_stages=True) + ) + + ids = [hit.id for hit in response.hits] + self.assertIn("chunk-dense", ids) + self.assertIn("chunk-bm25", ids) + self.assertTrue(any(hit.scores.rrf for hit in response.hits)) + mock_embeddings.embed_text.assert_called_once_with("license") + self.assertIsNotNone(response.stage_timings) + stage_names = [s["stage"] for s in response.stage_timings.get("stages", [])] + self.assertIn("embed.query", stage_names) + self.assertTrue( + any(name.startswith("search.") for name in stage_names), + stage_names, + ) + + async def test_dense_only_skips_bm25(self): + from src.services.api.controllers import search as search_mod + + mock_embeddings = MagicMock() + mock_embeddings.embed_text.return_value = Vector( + id="q", embeddings=[0.1], metadata={} + ) + mock_vs = MagicMock() + mock_vs.search_data.return_value = [ + Vector(id="d1", metadata={"resource_id": "only-dense"}, distance=0.2) + ] + mock_data = MagicMock() + mock_data.get_text_chunks_by_ids.return_value = ( + [TextChunk(id="only-dense", text="vector hit")], + [], + ) + + with ( + patch.object(search_mod.config, "search_enabled", True), + patch.object(search_mod.config, "search_use_dense", True), + patch.object(search_mod.config, "search_use_bm25", False), + patch.object(search_mod, "embeddings_adapter", mock_embeddings), + patch.object(search_mod, "vector_search", mock_vs), + patch.object(search_mod, "data_adapter", mock_data), + ): + response = await search_mod.search(SearchRequestBody(query="alice", k=5)) + + self.assertEqual([hit.id for hit in response.hits], ["only-dense"]) + mock_data.search_bm25.assert_not_called() + self.assertIsNotNone(response.hits[0].scores.dense) + + async def test_bm25_only_skips_embed(self): + from src.services.api.controllers import search as search_mod + + mock_embeddings = MagicMock() + mock_data = MagicMock() + mock_data.search_bm25.return_value = [ + (TextChunk(id="lex", text="license board"), 3.1) + ] + mock_vs = MagicMock() + + with ( + patch.object(search_mod.config, "search_enabled", True), + patch.object(search_mod.config, "search_use_dense", False), + patch.object(search_mod.config, "search_use_bm25", True), + patch.object(search_mod, "embeddings_adapter", mock_embeddings), + patch.object(search_mod, "vector_search", mock_vs), + patch.object(search_mod, "data_adapter", mock_data), + ): + response = await search_mod.search(SearchRequestBody(query="license", k=5)) + + self.assertEqual([hit.id for hit in response.hits], ["lex"]) + mock_embeddings.embed_text.assert_not_called() + mock_vs.search_data.assert_not_called() + self.assertEqual(response.hits[0].scores.bm25, 3.1) + self.assertIsNone(response.hits[0].node_id) + self.assertEqual(response.node_ids, []) + + async def test_passage_docid_sets_node_id(self): + from src.services.api.controllers import search as search_mod + + mock_embeddings = MagicMock() + mock_data = MagicMock() + mock_data.search_bm25.return_value = [ + ( + TextChunk( + id="chunk-bed", + text="DOCID 0.\nTitle: solid wood platform bed\nClass: Beds", + ), + 5.0, + ) + ] + mock_vs = MagicMock() + + with ( + patch.object(search_mod.config, "search_enabled", True), + patch.object(search_mod.config, "search_use_dense", False), + patch.object(search_mod.config, "search_use_bm25", True), + patch.object(search_mod, "embeddings_adapter", mock_embeddings), + patch.object(search_mod, "vector_search", mock_vs), + patch.object(search_mod, "data_adapter", mock_data), + ): + response = await search_mod.search( + SearchRequestBody(query="platform bed", k=5) + ) + + self.assertEqual([hit.id for hit in response.hits], ["chunk-bed"]) + self.assertEqual(response.hits[0].channel, "passages") + self.assertEqual(response.hits[0].node_id, "0") + self.assertEqual(response.node_ids, ["0"]) + + def test_default_search_mode_is_default(self): + body = SearchRequestBody(query="license") + self.assertEqual(body.mode, "default") + self.assertEqual(body.k, 10) + self.assertIsNone(body.extras) + + async def test_extras_equality_filter_and_facets(self): + from src.services.api.controllers import search as search_mod + from src.core.search.hybrid import ( + facet_counts_from_extras, + hit_matches_extras, + ) + + self.assertTrue(hit_matches_extras({"locale": "it"}, {"locale": "IT"})) + self.assertFalse(hit_matches_extras({"color": "argento"}, {"color": "nope"})) + self.assertEqual( + facet_counts_from_extras( + [ + {"locale": "it", "color": "argento", "brand": "CasaLuce"}, + {"locale": "it", "color": "blu navy", "brand": "AtelierNord"}, + None, + ] + ), + { + "locale": {"it": 2}, + "color": {"argento": 1, "blu navy": 1}, + "brand": {"CasaLuce": 1, "AtelierNord": 1}, + }, + ) + self.assertIsNone(facet_counts_from_extras([None, {}])) + self.assertEqual( + facet_counts_from_extras( + [{"locale": "it", "uuid": "skip-me", "resource_id": "chunk"}] + ), + {"locale": {"it": 1}}, + ) + + mock_data = MagicMock() + mock_data.search_bm25.return_value = [ + ( + TextChunk( + id="it-1", + text="bollitore acciaio", + metadata={"locale": "it", "color": "argento", "brand": "CasaLuce"}, + ), + 3.0, + ), + ( + TextChunk( + id="it-2", + text="divano velluto", + metadata={"locale": "it", "color": "blu navy", "brand": "AtelierNord"}, + ), + 2.0, + ), + ] + mock_data.get_text_chunks_by_ids.return_value = ([], []) + mock_embeddings = MagicMock() + mock_vs = MagicMock() + + with ( + patch.object(search_mod.config, "search_enabled", True), + patch.object(search_mod.config, "search_use_dense", False), + patch.object(search_mod.config, "search_use_bm25", True), + patch.object(search_mod, "embeddings_adapter", mock_embeddings), + patch.object(search_mod, "vector_search", mock_vs), + patch.object(search_mod, "data_adapter", mock_data), + ): + kept = await search_mod.search( + SearchRequestBody(query="divano", k=10, extras={"locale": "it"}) + ) + missed = await search_mod.search( + SearchRequestBody(query="divano", k=10, extras={"color": "nope"}) + ) + unfiltered = await search_mod.search( + SearchRequestBody(query="divano", k=10) + ) + + self.assertEqual({hit.id for hit in kept.hits}, {"it-1", "it-2"}) + self.assertEqual(kept.hits[0].extras["locale"], "it") + self.assertEqual(kept.facets["locale"]["it"], 2) + self.assertEqual(kept.facets["brand"]["CasaLuce"], 1) + self.assertEqual(missed.hits, []) + self.assertIsNone(missed.facets) + self.assertEqual({hit.id for hit in unfiltered.hits}, {"it-1", "it-2"}) + self.assertEqual(unfiltered.hits[0].channel, "passages") + mock_embeddings.embed_text.assert_not_called() + + def test_literal_overlap_and_frozen_head_helpers(self): + from src.core.search.hybrid import ( + frozen_head_merge, + fuse_passage_lists, + literal_overlap_ids, + ) + + ranked = frozen_head_merge( + [f"h{index}" for index in range(12)], + [["extra", "h0"]], + head_k=10, + k=15, + ) + self.assertEqual(ranked[:10], [f"h{index}" for index in range(10)]) + self.assertEqual(ranked[10], "extra") + ids = literal_overlap_ids( + "navy velvet sofa", + { + "hit": "Title: Navy velvet sofa two seats", + "miss": "plastic kettle lid", + "partial": "velvet throw pillow", + }, + k=10, + ) + self.assertEqual(ids[0], "hit") + self.assertIn("partial", ids) + self.assertNotIn("miss", ids) + fused = fuse_passage_lists( + ["d1", "d2"], + ["b1", "d1"], + fusion="cc", + alpha=0.7, + dense_similarities={"d1": 1.0, "d2": 0.2}, + bm25_scores={"b1": 4.0, "d1": 1.0}, + ) + self.assertTrue(fused) + self.assertEqual(fused[0][0], "d1") + + async def test_fusion_alpha_request_overrides_config(self): + from src.services.api.controllers import search as search_mod + + mock_data = MagicMock() + mock_data.search_bm25.return_value = [ + (TextChunk(id="lex", text="license board"), 3.1) + ] + mock_embeddings = MagicMock() + mock_embeddings.embed_text.return_value = Vector( + id="q", embeddings=[0.1], metadata={} + ) + mock_vs = MagicMock() + mock_vs.search_data.return_value = [ + Vector(id="d1", metadata={"resource_id": "dense-1"}, distance=0.1) + ] + mock_data.get_text_chunks_by_ids.return_value = ( + [TextChunk(id="dense-1", text="vector hit")], + [], + ) + + with ( + patch.object(search_mod.config, "search_enabled", True), + patch.object(search_mod.config, "search_use_dense", True), + patch.object(search_mod.config, "search_use_bm25", True), + patch.object(search_mod.config, "search_fusion", "rrf"), + patch.object(search_mod.config, "search_fusion_alpha", 0.1), + patch.object(search_mod.config, "search_literal_fill", False), + patch.object(search_mod, "embeddings_adapter", mock_embeddings), + patch.object(search_mod, "vector_search", mock_vs), + patch.object(search_mod, "data_adapter", mock_data), + ): + response = await search_mod.search( + SearchRequestBody( + query="license", + k=5, + fusion="cc", + fusion_alpha=0.9, + ) + ) + + ids = [hit.id for hit in response.hits] + self.assertIn("dense-1", ids) + self.assertTrue(any(hit.scores.cc is not None for hit in response.hits)) + + async def test_literal_fill_uses_frozen_head(self): + from src.services.api.controllers import search as search_mod + + mock_data = MagicMock() + mock_data.search_bm25.return_value = [ + (TextChunk(id=f"h{index}", text=f"t{index}"), float(20 - index)) + for index in range(12) + ] + mock_data.get_text_chunks.return_value = ( + [TextChunk(id="title-hit", text="navy velvet sofa")], + 1, + ) + mock_embeddings = MagicMock() + mock_vs = MagicMock() + + with ( + patch.object(search_mod.config, "search_enabled", True), + patch.object(search_mod.config, "search_use_dense", False), + patch.object(search_mod.config, "search_use_bm25", True), + patch.object(search_mod.config, "search_literal_fill", True), + patch.object(search_mod, "embeddings_adapter", mock_embeddings), + patch.object(search_mod, "vector_search", mock_vs), + patch.object(search_mod, "data_adapter", mock_data), + ): + response = await search_mod.search( + SearchRequestBody(query="navy velvet sofa", k=15) + ) + + ids = [hit.id for hit in response.hits] + self.assertEqual(ids[:10], [f"h{index}" for index in range(10)]) + self.assertIn("title-hit", ids[10:]) + self.assertEqual(response.channel_lists["literal"], ["title-hit"]) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_search_bench.py b/tests/test_search_bench.py new file mode 100644 index 0000000000000000000000000000000000000000..7c04ac711301e86b6bd8f8807ddd21067295bda8 --- /dev/null +++ b/tests/test_search_bench.py @@ -0,0 +1,1691 @@ +import json +import tempfile +import unittest +from pathlib import Path +from unittest.mock import patch + + +class SearchBenchTests(unittest.TestCase): + @classmethod + def setUpClass(cls): + import sys + + root = Path(__file__).resolve().parents[1] + bench = root / "benchmarks" + if str(bench) not in sys.path: + sys.path.insert(0, str(bench)) + + def require_local_artifact(self, path: Path) -> Path: + if not path.is_file(): + self.skipTest(f"optional local benchmark artifact is absent: {path}") + return path + + def test_brain_id_guard(self): + from search.config import DEFAULT_BRAIN_ID, validate_brain_id + + self.assertEqual(validate_brain_id("searchbenchsmoke"), "searchbenchsmoke") + self.assertEqual(validate_brain_id(DEFAULT_BRAIN_ID), DEFAULT_BRAIN_ID) + self.assertEqual(validate_brain_id("searchbench1"), "searchbench1") + self.assertEqual(validate_brain_id("searchbenchjdslice"), "searchbenchjdslice") + for bad in ( + "locomoconv26", + "locomoconv26clean", + "beam1m1clean", + "beam100k1", + "demorecsys", + "lme-s-smoke", + "default", + "search", + ): + with self.assertRaises(SystemExit): + validate_brain_id(bad) + + def test_metrics_ranking_and_latency(self): + from search.metrics import ( + aggregate_query_metrics, + mrr, + ndcg_at_k, + percentile, + recall_at_k, + retrieve_latency_ms, + ) + + ranked = ["a", "b", "gold", "d"] + gold = {"gold"} + self.assertEqual(recall_at_k(ranked, gold, 2), 0.0) + self.assertEqual(recall_at_k(ranked, gold, 3), 1.0) + self.assertEqual(mrr(ranked, gold), 1.0 / 3) + self.assertGreater(ndcg_at_k(ranked, gold, 10), 0.0) + self.assertLess(ndcg_at_k(ranked, gold, 10), 1.0) + self.assertEqual(ndcg_at_k(["gold"], gold, 10), 1.0) + self.assertGreater( + ndcg_at_k( + ["exact", "sub"], + {"exact", "sub"}, + 10, + grades={"exact": 1.0, "sub": 0.1}, + ), + ndcg_at_k( + ["sub", "exact"], + {"exact", "sub"}, + 10, + grades={"exact": 1.0, "sub": 0.1}, + ), + ) + wands_gold = {"exact", "partial"} + wands_grades = {"exact": 1.0, "partial": 0.5} + self.assertGreater( + ndcg_at_k(["exact", "partial"], wands_gold, 10, grades=wands_grades), + ndcg_at_k(["partial", "exact"], wands_gold, 10, grades=wands_grades), + ) + self.assertAlmostEqual( + ndcg_at_k(["exact", "partial"], wands_gold, 10, grades=wands_grades), + ndcg_at_k( + ["exact", "partial"], + wands_gold, + 10, + grades={"exact": 2.0, "partial": 1.0}, + ), + ) + import math + + from search.metrics import _dcg + + self.assertAlmostEqual( + _dcg([1.0, 0.5], 2), + 1.0 / math.log2(2) + 0.5 / math.log2(3), + ) + self.assertEqual(recall_at_k(["exact", "other"], wands_gold, 10), 0.5) + self.assertEqual(percentile([10.0, 20.0, 30.0], 50), 20.0) + + retrieve, embed = retrieve_latency_ms( + { + "stages": [ + {"stage": "embed.query", "wall_ms": 80.0}, + {"stage": "search.retrieve", "wall_ms": 12.5}, + ] + }, + 200.0, + ) + self.assertEqual(retrieve, 12.5) + self.assertEqual(embed, 80.0) + retrieve_fallback, embed_fallback = retrieve_latency_ms( + {"stages": [{"stage": "embed.query", "wall_ms": 40.0}]}, + 90.0, + ) + self.assertEqual(retrieve_fallback, 50.0) + self.assertEqual(embed_fallback, 40.0) + + rows = [ + { + "slice": "keyword", + "metrics": { + "recall@5": 1.0, + "recall@10": 1.0, + "recall@20": 1.0, + "ndcg@10": 1.0, + "ndcg@20": 1.0, + "ndcg": 1.0, + "mrr": 1.0, + }, + "retrieve_ms": 10.0, + "embed_ms": 50.0, + "client_wall_ms": 70.0, + }, + { + "slice": "paraphrase", + "metrics": { + "recall@5": 0.0, + "recall@10": 0.0, + "recall@20": 1.0, + "ndcg@10": 0.0, + "ndcg@20": 0.5, + "ndcg": 0.5, + "mrr": 0.0, + }, + "retrieve_ms": 30.0, + "embed_ms": 50.0, + "client_wall_ms": 90.0, + }, + ] + metrics = aggregate_query_metrics(rows) + self.assertEqual(metrics["recall@10"], 0.5) + self.assertEqual(metrics["ndcg@10"], 0.5) + self.assertEqual(metrics["ndcg@20"], 0.75) + self.assertEqual(metrics["by_slice"]["keyword"]["recall@10"], 1.0) + self.assertEqual(metrics["p50_retrieve_ms"], 20.0) + + from search.metrics import ndcg_at_k + + ranked_pool = ["exact", "irr", "sub", "other"] + grades_pool = {"exact": 1.0, "sub": 0.1, "irr": 0.0} + gold_pool = {"exact", "sub"} + manual = ndcg_at_k(ranked_pool, gold_pool, 20, grades=grades_pool) + self.assertGreater(manual, 0.0) + self.assertEqual( + ndcg_at_k(ranked_pool, gold_pool, 20, grades=grades_pool), + ndcg_at_k(ranked_pool, gold_pool, 4, grades=grades_pool), + ) + + def test_doc_marker_mapping(self): + from search.dataset import dataset_stats, load_records, map_doc_ids_to_chunks + + root = Path(__file__).resolve().parents[1] + fixture = self.require_local_artifact( + root / "benchmarks" / "data" / "search_toy.jsonl" + ) + rows = load_records(fixture) + stats = dataset_stats(rows) + self.assertGreaterEqual(stats["n_docs"], 6) + self.assertGreaterEqual(stats["n_queries"], 8) + self.assertIn("keyword", stats["slices"]) + self.assertIn("paraphrase", stats["slices"]) + mapping = map_doc_ids_to_chunks( + [{"doc_id": "license-alice"}], + [ + {"id": "c1", "text": "DOCID license-alice. Alice completed her license."}, + {"id": "c2", "text": "unrelated"}, + ], + ) + self.assertEqual(mapping["license-alice"], {"c1"}) + + def test_gold_matches_chunk_or_node_uuid(self): + from search.evaluate import gold_hit_grades, gold_hit_ids + from search.metrics import ndcg_at_k + + query = {"gold_doc_ids": ["sku-1"], "gold_grades": {"sku-1": 1.0}} + mapped = {"sku-1": {"chunk-1"}} + gold = gold_hit_ids(query, mapped) + self.assertEqual(gold, {"sku-1"}) + grades = gold_hit_grades(query, mapped) + self.assertEqual(ndcg_at_k(["sku-1"], gold, 10, grades=grades), 1.0) + from search.evaluate import canonicalize_hit_ids, invert_doc_chunks + + ranked_chunk = canonicalize_hit_ids(["chunk-1"], invert_doc_chunks(mapped)) + self.assertEqual(ndcg_at_k(ranked_chunk, gold, 10, grades=grades), 1.0) + self.assertEqual(ndcg_at_k(["other"], gold, 10, grades=grades), 0.0) + + def test_wands_recall_gold_is_exact_and_partial(self): + from search.evaluate import gold_hit_ids + from search.metrics import recall_at_k + + query = { + "gold_doc_ids": ["exact", "partial"], + "gold_grades": {"exact": 1.0, "partial": 0.5}, + } + gold = gold_hit_ids(query, {}) + self.assertEqual(gold, {"exact", "partial"}) + self.assertEqual(recall_at_k(["exact", "irr"], gold, 10), 0.5) + self.assertEqual(recall_at_k(["exact", "partial"], gold, 10), 1.0) + unlabeled = gold_hit_ids( + { + "gold_doc_ids": ["exact"], + "gold_grades": {"exact": 1.0, "partial": 0.5}, + }, + {}, + ) + self.assertEqual(unlabeled, {"exact", "partial"}) + irr_excluded = gold_hit_ids( + { + "gold_doc_ids": ["exact"], + "gold_grades": {"exact": 1.0, "irr": 0.0}, + }, + {}, + ) + self.assertEqual(irr_excluded, {"exact"}) + + def test_replay_fusion_compact_hub_drop(self): + from search.replay_fusion import replay + + root = Path(__file__).resolve().parents[1] + graph_path = ( + root / "benchmarks" / "runs" / "search-esci-slice-allcols" / "eval.json" + ) + passages_path = ( + root / "benchmarks" / "runs" / "search-esci-slice-passages" / "eval.json" + ) + if not graph_path.exists() or not passages_path.exists(): + self.skipTest("missing paired ESCI slice eval.json files") + result = replay( + json.loads(graph_path.read_text(encoding="utf-8")), + json.loads(passages_path.read_text(encoding="utf-8")), + ) + self.assertEqual(result["n_queries"], 11) + self.assertAlmostEqual(result["hub_drop"]["recall@10"], 0.4758, places=3) + self.assertAlmostEqual(result["hub_drop"]["mrr"], 0.8485, places=3) + + def test_unique_doc_counts_and_channel_lists(self): + from search.evaluate import channel_id_lists, unique_doc_counts + + raw = ["chunk-1", "sku-1", "chunk-2", "hub:attr:red"] + mapping = {"chunk-1": "sku-1", "chunk-2": "sku-2"} + n_raw, n_canon = unique_doc_counts(raw, mapping, k=20) + self.assertEqual(n_raw, 4) + self.assertEqual(n_canon, 3) + lists = channel_id_lists( + { + "channel_lists": { + "dense": ["chunk-1"], + "bm25": ["chunk-2"], + "entities": ["sku-1"], + "communities": ["sku-3"], + } + }, + [{"id": "chunk-1", "channel": "passages", "doc_id": "sku-1"}], + ) + self.assertEqual(lists["dense_ids"], ["chunk-1"]) + self.assertEqual(lists["entity_ids"], ["sku-1"]) + self.assertEqual(lists["community_ids"], ["sku-3"]) + self.assertEqual(lists["passage_ids"], ["chunk-1"]) + + def test_replay_collapse_and_gated_arms(self): + from search.replay_fusion import pick_offline_winner, replay_offline + + gold = [f"d{i}" for i in range(1, 21)] + grades = {doc: 1.0 for doc in gold} + passages_hits = [ + {"id": f"chunk-{item}", "channel": "passages", "doc_id": item} + for item in gold + ] + passages = { + "queries": [ + { + "qid": "q1", + "gold_doc_ids": gold, + "gold_grades": grades, + "hit_ids": gold, + "dense_ids": [f"chunk-{item}" for item in gold], + "bm25_ids": [f"chunk-{item}" for item in gold], + "passage_ids": [f"chunk-{item}" for item in gold], + "hits": passages_hits, + "metrics": { + "ndcg@10": 1.0, + "recall@10": 0.5, + "recall@20": 1.0, + "mrr": 1.0, + }, + } + ] + } + fused_raw = [f"chunk-{item}" for item in gold[:8]] + gold[:8] + gold[8:12] + graph = { + "queries": [ + { + "qid": "q1", + "gold_doc_ids": gold, + "gold_grades": grades, + "hit_ids": gold[:12], + "entity_ids": gold[:5], + "community_ids": ["d21"], + "hits": [ + { + "id": hid, + "channel": "passages" if hid.startswith("chunk-") else "entities", + "doc_id": hid.replace("chunk-", ""), + } + for hid in fused_raw + ], + "metrics": { + "ndcg@10": 0.6, + "recall@10": 0.4, + "recall@20": 0.6, + "mrr": 1.0, + }, + } + ] + } + result = replay_offline(graph, passages) + self.assertEqual(result["n_queries"], 1) + self.assertGreaterEqual( + result["arms"]["collapse-rrf"]["recall@20"], + result["arms"]["passages"]["recall@20"] - 1e-9, + ) + self.assertEqual(result["arms"]["passages"]["unique_docs@20"], 20.0) + self.assertEqual(result["arms"]["confirmation"]["unique_docs@20"], 20.0) + self.assertEqual(result["arms"]["expansion-n10"]["unique_docs@20"], 20.0) + winner = pick_offline_winner( + { + "passages": { + "ndcg@10": 0.758, + "recall@20": 0.847, + }, + "collapse-rrf": { + "ndcg@10": 0.640, + "recall@20": 0.847, + }, + "expansion-n10": { + "ndcg@10": 0.758, + "recall@20": 0.847, + }, + } + ) + self.assertEqual(winner, "expansion-n10") + self.assertEqual( + pick_offline_winner( + { + "passages": {"ndcg@10": 0.758, "recall@20": 0.847}, + "collapse-rrf": {"ndcg@10": 0.640, "recall@20": 0.847}, + "expansion-n10": {"ndcg@10": 0.758, "recall@20": 0.815}, + } + ), + "G08", + ) + self.assertEqual( + pick_offline_winner( + { + "passages": {"ndcg@10": 0.758, "recall@20": 0.847}, + "collapse-rrf": {"ndcg@10": 0.640, "recall@20": 0.847}, + } + ), + "G08", + ) + + def test_score_search_dumps_channel_and_doc_id(self): + from search.client import TimedResult + from search.evaluate import score_search_result + from search.metrics import ndcg_at_k, recall_at_k + + result = TimedResult( + data={ + "hits": [ + {"id": "chunk-1", "channel": "passages", "node_id": "sku-1"}, + {"id": "sku-1", "channel": "entities", "node_id": "sku-1"}, + {"id": "hub:attr:red", "channel": "communities"}, + ], + "stage_timings": { + "stages": [{"stage": "search.retrieve", "wall_ms": 11.0}] + }, + }, + latency_ms=40.0, + ) + gold = {"sku-1"} + grades = {"sku-1": 1.0} + scored = score_search_result( + result, + gold=gold, + ks=(5, 10, 20, 50), + grades=grades, + chunk_to_doc={"chunk-1": "sku-1"}, + ) + self.assertEqual(scored["hit_ids"], ["sku-1", "hub:attr:red"]) + self.assertEqual( + scored["hits"], + [ + { + "id": "chunk-1", + "channel": "passages", + "doc_id": "sku-1", + "node_id": "sku-1", + }, + { + "id": "sku-1", + "channel": "entities", + "doc_id": "sku-1", + "node_id": "sku-1", + }, + { + "id": "hub:attr:red", + "channel": "communities", + "doc_id": "hub:attr:red", + "node_id": None, + }, + ], + ) + self.assertEqual( + scored["metrics"]["recall@10"], + recall_at_k(scored["hit_ids"], gold, 10), + ) + self.assertEqual( + scored["metrics"]["ndcg@10"], + ndcg_at_k(scored["hit_ids"], gold, 10, grades=grades), + ) + self.assertEqual( + [row["doc_id"] for row in scored["hits"][:1]], + scored["hit_ids"][:1], + ) + self.assertEqual(scored["n_unique_docs_raw"], 3) + self.assertEqual(scored["n_unique_docs_canonical"], 2) + self.assertEqual(scored["unique_docs_k"], 50) + self.assertEqual(scored["n_unique_docs_retrieve_canonical"], 2) + self.assertIn("ndcg@50", scored["metrics"]) + self.assertEqual(scored["entity_ids"], ["sku-1"]) + self.assertEqual(scored["community_ids"], ["hub:attr:red"]) + self.assertEqual(scored["passage_ids"], ["chunk-1"]) + + def test_rank_pool_filters_and_keeps_irrelevant_grades(self): + from search.client import TimedResult + from search.evaluate import ( + candidate_pool_grades, + candidate_pool_ids, + filter_ranked_to_pool, + score_search_result, + ) + from search.metrics import ndcg_at_k + + query = { + "gold_doc_ids": ["p-e"], + "gold_grades": {"p-e": 1.0, "p-s": 0.1}, + "candidate_doc_ids": ["p-e", "p-s", "p-i"], + "candidate_grades": {"p-e": 1.0, "p-s": 0.1, "p-i": 0.0}, + } + self.assertEqual(candidate_pool_ids(query), ["p-e", "p-s", "p-i"]) + self.assertEqual(candidate_pool_grades(query)["p-i"], 0.0) + self.assertEqual( + filter_ranked_to_pool(["p-x", "p-e", "p-i", "p-y"], query["candidate_doc_ids"]), + ["p-e", "p-i"], + ) + result = TimedResult( + data={ + "hits": [ + {"id": "outside", "channel": "passages"}, + {"id": "p-e", "channel": "passages"}, + {"id": "p-i", "channel": "passages"}, + ], + "stage_timings": { + "stages": [{"stage": "search.retrieve", "wall_ms": 9.0}] + }, + }, + latency_ms=12.0, + ) + scored = score_search_result( + result, + gold={"p-e", "p-s"}, + ks=(5, 10, 20), + grades=candidate_pool_grades(query), + pool_ids=candidate_pool_ids(query), + ) + self.assertEqual(scored["hit_ids"], ["p-e", "p-i"]) + expected = ndcg_at_k( + ["p-e", "p-i"], + {"p-e", "p-s"}, + 20, + grades=candidate_pool_grades(query), + ) + self.assertEqual(scored["metrics"]["ndcg@20"], expected) + + def test_rank_pool_ce_uses_injected_predict(self): + import sys + + root = Path(__file__).resolve().parents[1] + plugin = root / "plugins" / "search-rerank" + self.require_local_artifact(plugin / "rerank.py") + if str(plugin) not in sys.path: + sys.path.insert(0, str(plugin)) + import rerank as rerank_mod + + from search.rank_pool import run_ce_on_pool + + def predict(pairs): + scores = [] + for _, text in pairs: + if "gold" in text: + scores.append(2.0) + elif "partial" in text: + scores.append(1.0) + else: + scores.append(0.0) + return scores + + rerank_mod.set_predict(predict) + try: + rows = [ + { + "type": "doc", + "doc_id": "irr", + "text": "DOCID irr. Title: other", + }, + { + "type": "doc", + "doc_id": "gold", + "text": "DOCID gold. Title: gold kettle", + }, + { + "type": "doc", + "doc_id": "sub", + "text": "DOCID sub. Title: partial kettle", + }, + { + "type": "query", + "qid": "esci-1", + "query": "kettle", + "gold_doc_ids": ["gold", "sub"], + "gold_grades": {"gold": 1.0, "sub": 0.1}, + "candidate_doc_ids": ["irr", "gold", "sub"], + "candidate_grades": {"irr": 0.0, "gold": 1.0, "sub": 0.1}, + "slice": "esci-us", + }, + ] + result = run_ce_on_pool(rows, dataset_name="toy-pool.jsonl") + finally: + rerank_mod.set_predict(None) + self.assertEqual(result["status"], "ok") + self.assertTrue(result["rank_pool"]) + self.assertTrue(result["rank_pool_ce"]) + self.assertEqual(result["queries"][0]["hit_ids"][0], "gold") + self.assertGreater(result["metrics"]["ndcg@20"], 0.0) + + def test_ce_on_retrieved_reorders_hits_only(self): + import sys + + root = Path(__file__).resolve().parents[1] + plugin = root / "plugins" / "search-rerank" + self.require_local_artifact(plugin / "rerank.py") + if str(plugin) not in sys.path: + sys.path.insert(0, str(plugin)) + import rerank as rerank_mod + + from search.rerank_retrieved import run_ce_on_retrieved + + def predict(pairs): + scores = [] + for _, text in pairs: + if "gold" in text: + scores.append(2.0) + elif "partial" in text: + scores.append(1.0) + else: + scores.append(0.0) + return scores + + rerank_mod.set_predict(predict) + try: + rows = [ + { + "type": "doc", + "doc_id": "irr", + "text": "DOCID irr. Title: other", + }, + { + "type": "doc", + "doc_id": "gold", + "text": "DOCID gold. Title: gold kettle", + }, + { + "type": "doc", + "doc_id": "sub", + "text": "DOCID sub. Title: partial kettle", + }, + { + "type": "query", + "qid": "esci-1", + "query": "kettle", + "gold_doc_ids": ["gold", "sub"], + "gold_grades": {"gold": 1.0, "sub": 0.1}, + "slice": "esci-us", + }, + ] + eval_result = { + "k": 50, + "ks": [5, 10, 20, 50], + "rank_pool": False, + "fusion": "rrf", + "channels": ["passages"], + "n_docs": 3, + "queries": [ + { + "qid": "esci-1", + "query": "kettle", + "slice": "esci-us", + "gold_doc_ids": ["gold", "sub"], + "gold_grades": {"gold": 1.0, "sub": 0.1}, + "hit_ids": ["irr", "sub", "gold"], + "metrics": {}, + "retrieve_ms": 12.0, + "pool_coverage": None, + } + ], + } + result = run_ce_on_retrieved( + eval_result, + rows, + dataset_name="toy-retrieved.jsonl", + ) + finally: + rerank_mod.set_predict(None) + self.assertEqual(result["status"], "ok") + self.assertFalse(result["rank_pool_ce"]) + self.assertEqual(result["rerank"], "harness:cross-encoder") + self.assertEqual(result["queries"][0]["hit_ids"][0], "gold") + self.assertEqual(set(result["queries"][0]["hit_ids"]), {"irr", "sub", "gold"}) + self.assertGreater(result["metrics"]["ndcg@10"], 0.0) + + def test_electronics_marker_for_esci72_style_titles(self): + from search.miss import looks_like_electronics + + self.assertTrue(looks_like_electronics("Apple iPad Air 2, 16 GB")) + self.assertFalse(looks_like_electronics("Price Tags Without String")) + + def test_miss_strata_classifies_head_tail_and_total_miss(self): + from search.miss_strata import classify_eval, rewrite_query + + self.assertIsNone(rewrite_query("esci-72", "$100 things that are not electronics")) + self.assertIsNone(rewrite_query("esci-177", "p_num: integer not null i_num: integer")) + rewritten = rewrite_query( + "esci-113", + ""tortillas without interesterified soybean oil”", + ) + self.assertIsNotNone(rewritten) + self.assertNotIn(""", rewritten) + + eval_result = { + "k": 50, + "queries": [ + { + "qid": "q-head", + "query": "red mug", + "hit_ids": ["g1", "x"] + [f"n{i}" for i in range(48)], + "gold_doc_ids": ["g1", "g2"], + "gold_grades": {"g1": 1.0, "g2": 0.1}, + "metrics": {"recall@10": 0.5, "recall@50": 0.5}, + }, + { + "qid": "q-tail", + "query": "blue mug", + "hit_ids": ["n0"] * 10 + ["g2"] + [f"n{i}" for i in range(39)], + "gold_doc_ids": ["g2"], + "gold_grades": {"g2": 1.0}, + "metrics": {"recall@10": 0.0, "recall@50": 1.0}, + }, + { + "qid": "esci-113", + "query": ""tortillas without interesterified soybean oil”", + "hit_ids": [f"n{i}" for i in range(50)], + "gold_doc_ids": ["g3"], + "gold_grades": {"g3": 1.0}, + "metrics": {"recall@10": 0.0, "recall@50": 0.0}, + }, + ], + } + rows = [ + {"type": "doc", "doc_id": "g1", "text": "Title: red mug"}, + {"type": "doc", "doc_id": "g2", "text": "Title: blue mug"}, + {"type": "doc", "doc_id": "g3", "text": "Title: tortillas"}, + { + "type": "query", + "qid": "q-head", + "query": "red mug", + "gold_doc_ids": ["g1", "g2"], + "gold_grades": {"g1": 1.0, "g2": 0.1}, + }, + { + "type": "query", + "qid": "q-tail", + "query": "blue mug", + "gold_doc_ids": ["g2"], + "gold_grades": {"g2": 1.0}, + }, + { + "type": "query", + "qid": "esci-113", + "query": ""tortillas without interesterified soybean oil”", + "gold_doc_ids": ["g3"], + "gold_grades": {"g3": 1.0}, + }, + ] + taxonomy = classify_eval(eval_result, rows, k=50) + self.assertEqual(taxonomy["n_queries"], 3) + self.assertEqual(taxonomy["n_gold"], 4) + self.assertEqual(taxonomy["stratum_counts"]["head-ok"], 1) + self.assertEqual(taxonomy["stratum_counts"]["rank-too-low"], 1) + self.assertEqual(taxonomy["stratum_counts"]["total-miss"], 1) + self.assertTrue(taxonomy["run_query_side"]) + self.assertEqual(taxonomy["rewritable_qids"], ["esci-113"]) + + def test_spell_normalize_nfkc_and_accents(self): + from search.miss_strata import normalize_spelling, write_spell_jsonl + + self.assertEqual(normalize_spelling("niños!!!"), "ninos") + self.assertEqual(normalize_spelling(" sofa, azul. "), "sofa azul") + self.assertEqual(normalize_spelling("caf\u00e9"), "cafe") + with tempfile.TemporaryDirectory() as tmp: + dest = Path(tmp) / "spell.jsonl" + summary = write_spell_jsonl( + [ + {"type": "doc", "doc_id": "d1", "text": "sofa"}, + {"type": "query", "qid": "q1", "query": "sofá azul!!"}, + ], + dest, + ) + self.assertEqual(summary["n_queries_changed"], 1) + rows = dest.read_text(encoding="utf-8").strip().splitlines() + query = json.loads(rows[1]) + self.assertEqual(query["query"], "sofa azul") + self.assertEqual(query["query_original"], "sofá azul!!") + + def test_doc_meta_keys_copies_generic_catalog_fields(self): + from search.evaluate import doc_meta_keys + + self.assertEqual( + doc_meta_keys( + { + "doc_id": "it-bollitore", + "brand": "CasaLuce", + "color": "argento", + "locale": "it", + "title": "ignored", + } + ), + {"brand": "CasaLuce", "color": "argento", "locale": "it"}, + ) + self.assertIsNone(doc_meta_keys({"doc_id": "x", "text": "only text"})) + + def test_dense_holdout_excludes_eval_qids(self): + from search.finetune_esci_ce import held_out_query_ids + + holdout_path = self.require_local_artifact( + Path(__file__).resolve().parents[1] + / "benchmarks" + / "data" + / "search_esci_74.jsonl" + ) + holdout = held_out_query_ids(holdout_path) + self.assertIn("72", holdout) + self.assertIn("esci-72", holdout) + self.assertGreater(len(holdout), 74) + + def test_local_dense_scores_injected_ranks(self): + from search.local_dense import evaluate_dense + + queries = [ + { + "qid": "q1", + "query": "red mug", + "slice": "toy", + "gold_doc_ids": ["gold"], + "gold_grades": {"gold": 1.0, "sub": 0.1}, + } + ] + ranked = {"q1": ["gold", "sub", "irr"]} + metrics, per_query = evaluate_dense( + ranked, queries, ks=(5, 10, 20, 50), encode_ms=10.0 + ) + self.assertEqual(per_query[0]["hit_ids"][0], "gold") + self.assertEqual(metrics["recall@10"], 1.0) + self.assertGreater(metrics["ndcg@10"], 0.0) + + def test_exhaustive_ce_ranks_exact_above_irrelevant(self): + from search.rank_corpus import PROTOCOL, run_exhaustive_ce + + rows = [ + {"type": "doc", "doc_id": "irr", "text": "DOCID irr. Title: other"}, + {"type": "doc", "doc_id": "exact", "text": "DOCID exact. Title: gold kettle"}, + {"type": "doc", "doc_id": "maybe", "text": "DOCID maybe. Title: similar kettle"}, + { + "type": "query", + "qid": "esci-toy", + "query": "kettle", + "gold_doc_ids": ["exact"], + "gold_grades": {"exact": 1.0, "maybe": 0.1}, + "slice": "toy", + }, + ] + + def predict(pairs): + out = [] + for _, text in pairs: + if "gold kettle" in text: + out.append([0.95, 0.03, 0.01, 0.01]) + elif "similar" in text: + out.append([0.05, 0.80, 0.10, 0.05]) + else: + out.append([0.01, 0.02, 0.07, 0.90]) + return out + + result = run_exhaustive_ce( + rows, + predict=predict, + model_name="toy-4class", + dataset_name="toy-corpus.jsonl", + k=3, + ks=(5, 10, 20, 50), + ) + self.assertEqual(result["status"], "ok") + self.assertEqual(result["protocol"], PROTOCOL) + self.assertFalse(result["rank_pool"]) + self.assertEqual(result["queries"][0]["hit_ids"][0], "exact") + self.assertLess(result["queries"][0]["hit_ids"].index("exact"), result["queries"][0]["hit_ids"].index("irr")) + self.assertGreater(result["metrics"]["ndcg@10"], 0.0) + + def test_list_overlap_counts_sidecar_unique_golds(self): + from search.list_overlap import run_union, summarize_overlap, unique_golds + + gold = {"g1", "g2", "g3"} + extra = unique_golds( + ["g2", "noise", "g3"], + ["g1", "g2", "other"], + gold, + k=50, + ) + self.assertEqual(extra, ["g3"]) + passages = { + "run_id": "passages", + "queries": [ + { + "qid": "q1", + "hit_ids": ["g1", "g2", "p1"], + "gold_doc_ids": ["g1", "g2", "g3"], + "gold_grades": {"g1": 1.0, "g2": 1.0, "g3": 0.1}, + } + ], + } + bge = { + "run_id": "bge", + "queries": [{"qid": "q1", "hit_ids": ["g3", "g1", "b1"]}], + } + summary = summarize_overlap(passages, {"bge": bge}, k=50) + self.assertEqual(summary["runs"]["bge"]["unique_gold_hits"], 1) + rows = [ + {"type": "doc", "doc_id": "g1", "text": "g1"}, + {"type": "doc", "doc_id": "g2", "text": "g2"}, + {"type": "doc", "doc_id": "g3", "text": "g3"}, + { + "type": "query", + "qid": "q1", + "query": "mug", + "gold_doc_ids": ["g1", "g2", "g3"], + "gold_grades": {"g1": 1.0, "g2": 1.0, "g3": 0.1}, + }, + ] + union = run_union( + [passages, bge], + rows, + dataset_name="toy.jsonl", + k=50, + ks=(5, 10, 20, 50), + run_names=["passages", "bge"], + ) + self.assertEqual(union["channels"], ["harness-union"]) + self.assertIn("g3", union["queries"][0]["hit_ids"][:10]) + self.assertGreaterEqual(union["metrics"]["recall@10"], 1.0) + + def test_cascade_freezes_head_and_injects_tail_golds(self): + from search.list_overlap import CASCADE_PROTOCOL, cascade_frozen_head, run_cascade + + passages = [f"h{index}" for index in range(10)] + ["tail_gold", "irr"] + gold = {"h0", "tail_gold", "extra"} + ranked = cascade_frozen_head( + passages, + [["extra", "noise", "h0"]], + gold, + head_k=10, + k=50, + ) + self.assertEqual(ranked[:10], passages[:10]) + self.assertIn("extra", ranked[10:]) + self.assertNotIn("extra", ranked[:10]) + self.assertIn("tail_gold", ranked) + rows = [ + {"type": "doc", "doc_id": doc_id, "text": doc_id} + for doc_id in [*passages, "extra"] + ] + rows.append( + { + "type": "query", + "qid": "q1", + "query": "mug", + "gold_doc_ids": ["h0", "tail_gold", "extra"], + "gold_grades": {"h0": 1.0, "tail_gold": 1.0, "extra": 0.1}, + } + ) + result = run_cascade( + {"run_id": "passages", "queries": [{"qid": "q1", "hit_ids": passages}]}, + [{"run_id": "bge", "queries": [{"qid": "q1", "hit_ids": ["extra", "noise"]}]}], + rows, + dataset_name="toy.jsonl", + k=50, + head_k=10, + ks=(5, 10, 20, 50), + run_names=["passages", "bge"], + ) + self.assertEqual(result["protocol"], CASCADE_PROTOCOL) + self.assertEqual(result["channels"], ["harness-cascade"]) + self.assertEqual(result["queries"][0]["hit_ids"][:10], passages[:10]) + self.assertIn("extra", result["queries"][0]["hit_ids"][10:]) + self.assertEqual(result["metrics"]["recall@10"], 1.0 / 3.0) + + def test_frozen_head_merge_matches_harness_cascade(self): + from src.core.search.hybrid import frozen_head_merge + from search.list_overlap import cascade_frozen_head + + passages = [f"h{index}" for index in range(10)] + ["tail_gold", "irr"] + sidecars = [["extra", "noise", "h0"]] + gold = {"h0", "tail_gold", "extra"} + ranked = frozen_head_merge( + passages, + sidecars, + head_k=10, + k=50, + prefer_ids=gold, + ) + self.assertEqual( + ranked, + cascade_frozen_head(passages, sidecars, gold, head_k=10, k=50), + ) + live = frozen_head_merge(passages, sidecars, head_k=10, k=50) + self.assertEqual(live[:10], passages[:10]) + self.assertIn("extra", live[10:]) + self.assertIn("noise", live[10:]) + + def test_cascade_replay_esci74_stored_lists(self): + from search.dataset import load_records + from search.list_overlap import load_eval_run, run_cascade + + root = Path(__file__).resolve().parents[1] + runs = root / "benchmarks" / "runs" + passages_path = runs / "search-esci-74-passages-k50" / "eval.json" + bge_path = runs / "search-esci-74-bge-base-k50" / "eval.json" + colbert_path = runs / "search-esci-74-colbert-k50" / "eval.json" + dataset = root / "benchmarks" / "data" / "search_esci_74.jsonl" + if not all( + path.exists() + for path in (passages_path, bge_path, colbert_path, dataset) + ): + self.skipTest("stored US ESCI n=74 evals not present") + passages = load_eval_run(passages_path.parent) + bge = load_eval_run(bge_path.parent) + colbert = load_eval_run(colbert_path.parent) + rows = load_records(dataset) + result = run_cascade( + passages, + [bge, colbert], + rows, + dataset_name="search_esci_74.jsonl", + k=50, + head_k=10, + ks=(5, 10, 20, 50), + run_names=["passages", "bge", "colbert"], + ) + metrics = result["metrics"] + self.assertAlmostEqual(metrics["ndcg@10"], 0.500, places=3) + self.assertAlmostEqual(metrics["recall@10"], 0.379, places=3) + self.assertAlmostEqual(metrics["recall@50"], 0.889, places=3) + + def test_ltr_head_promotes_title_overlap_on_held_out_query(self): + from search.ltr_head import ( + example_from_eval_row, + features_for_doc, + fit_pairwise, + rerank_ids, + run_ltr_head, + ) + + feats = features_for_doc( + "navy velvet sofa", + "hit", + rrf_ids=["miss", "hit"], + bm25_ids=["hit"], + dense_ids=["miss"], + doc={"title": "Navy velvet sofa two seats", "brand": "AtelierNord"}, + ) + self.assertGreater(feats[3], 0.5) + self.assertEqual(feats[4], 0.0) + train_row = { + "qid": "q-train", + "query": "navy velvet sofa", + "hit_ids": ["irr", "gold"], + "bm25_ids": ["gold"], + "dense_ids": ["irr"], + "hits": [ + {"id": "irr", "doc_id": "irr"}, + {"id": "gold", "doc_id": "gold"}, + ], + "gold_doc_ids": ["gold"], + "gold_grades": {"gold": 1.0}, + } + test_row = { + "qid": "q-test", + "query": "navy velvet sofa", + "hit_ids": ["irr", "gold"], + "bm25_ids": ["gold"], + "dense_ids": ["irr"], + "hits": [ + {"id": "irr", "doc_id": "irr"}, + {"id": "gold", "doc_id": "gold"}, + ], + "gold_doc_ids": ["gold"], + "gold_grades": {"gold": 1.0}, + } + docs = { + "gold": {"doc_id": "gold", "title": "Navy velvet sofa"}, + "irr": {"doc_id": "irr", "title": "plastic kettle lid"}, + } + train_ex = example_from_eval_row(train_row, docs, k=10) + test_ex = example_from_eval_row(test_row, docs, k=10) + self.assertIsNotNone(train_ex) + self.assertIsNotNone(test_ex) + weights = fit_pairwise([train_ex], epochs=20, seed=0) + ranked = rerank_ids(test_ex, weights) + self.assertEqual(ranked[0], "gold") + result = run_ltr_head( + {"run_id": "toy", "queries": [train_row, test_row]}, + [ + {"type": "doc", "doc_id": "gold", "title": "Navy velvet sofa", "text": "Title: Navy velvet sofa"}, + {"type": "doc", "doc_id": "irr", "title": "plastic kettle lid", "text": "Title: plastic kettle lid"}, + { + "type": "query", + "qid": "q-train", + "query": "navy velvet sofa", + "gold_doc_ids": ["gold"], + "gold_grades": {"gold": 1.0}, + }, + { + "type": "query", + "qid": "q-test", + "query": "navy velvet sofa", + "gold_doc_ids": ["gold"], + "gold_grades": {"gold": 1.0}, + }, + ], + dataset_name="toy.jsonl", + k=10, + ks=(5, 10), + n_folds=2, + ) + self.assertEqual(result["protocol"], "ltr-head-cv") + self.assertGreaterEqual(result["metrics"]["recall@10"], 1.0) + + def test_ltr_pair_policy_skips_unlabeled_for_other_query_neg(self): + from search.ltr_head import ( + PAIR_OTHER_QUERY_NEG, + PAIR_UNLABELED_ZERO, + collect_pairs, + example_from_eval_row, + features_for_doc, + other_query_gold_ids, + ) + + row_a = { + "qid": "q-a", + "query": "navy sofa", + "hit_ids": ["gold-a", "gold-b", "unlabeled"], + "hits": [ + {"id": "gold-a", "doc_id": "gold-a"}, + {"id": "gold-b", "doc_id": "gold-b"}, + {"id": "unlabeled", "doc_id": "unlabeled"}, + ], + "gold_doc_ids": ["gold-a"], + "gold_grades": {"gold-a": 1.0}, + } + row_b = { + "qid": "q-b", + "query": "steel kettle", + "hit_ids": ["gold-b", "gold-a", "unlabeled"], + "hits": [ + {"id": "gold-b", "doc_id": "gold-b"}, + {"id": "gold-a", "doc_id": "gold-a"}, + {"id": "unlabeled", "doc_id": "unlabeled"}, + ], + "gold_doc_ids": ["gold-b"], + "gold_grades": {"gold-b": 1.0}, + } + docs = { + "gold-a": {"doc_id": "gold-a", "title": "Navy sofa"}, + "gold-b": {"doc_id": "gold-b", "title": "Steel kettle"}, + "unlabeled": {"doc_id": "unlabeled", "title": "random cable"}, + } + ex_a = example_from_eval_row(row_a, docs, k=10) + ex_b = example_from_eval_row(row_b, docs, k=10) + other = other_query_gold_ids([ex_a, ex_b], "q-a") + self.assertEqual(other, {"gold-b"}) + zero_pairs = collect_pairs(ex_a, pair_policy=PAIR_UNLABELED_ZERO) + neg_pairs = collect_pairs( + ex_a, pair_policy=PAIR_OTHER_QUERY_NEG, other_gold=other + ) + zero_docs = {(ex_a["ids"][hi], ex_a["ids"][lo]) for hi, lo in zero_pairs} + neg_docs = {(ex_a["ids"][hi], ex_a["ids"][lo]) for hi, lo in neg_pairs} + self.assertIn(("gold-a", "unlabeled"), zero_docs) + self.assertNotIn(("gold-a", "unlabeled"), neg_docs) + self.assertIn(("gold-a", "gold-b"), neg_docs) + feats = features_for_doc( + "navy sofa", + "gold-a", + rrf_ids=["gold-a"], + bm25_ids=["gold-a"], + dense_ids=["gold-a"], + doc={"title": "Navy sofa"}, + ce_gain=0.42, + ) + self.assertEqual(len(feats), 7) + self.assertAlmostEqual(float(feats[-1]), 0.42) + from search.ltr_head import rank_train_group + + grouped = rank_train_group( + ex_a, pair_policy=PAIR_OTHER_QUERY_NEG, other_gold=other + ) + self.assertIsNotNone(grouped) + _, labels = grouped + self.assertEqual(len(labels), 2) + + def test_ltr_lightgbm_promotes_high_ce_gain_on_held_out_query(self): + from search.ltr_head import ( + PAIR_OTHER_QUERY_NEG, + example_from_eval_row, + fit_lightgbm, + rerank_ids_model, + ) + + n_queries = 8 + docs: dict[str, dict[str, str]] = {} + rows: list[dict] = [] + for index in range(n_queries): + gold = f"gold-{index}" + other = f"gold-{(index + 1) % n_queries}" + miss = f"miss-{index}" + docs[gold] = {"doc_id": gold, "title": f"Navy sofa {index}"} + docs[miss] = {"doc_id": miss, "title": f"plastic lid {index}"} + rows.append( + { + "qid": f"q-{index}", + "query": "navy sofa", + "hit_ids": [miss, other, gold], + "bm25_ids": [gold], + "dense_ids": [miss], + "hits": [ + {"id": miss, "doc_id": miss}, + {"id": other, "doc_id": other}, + {"id": gold, "doc_id": gold}, + ], + "gold_doc_ids": [gold], + "gold_grades": {gold: 1.0}, + "ce": {gold: 0.95, other: 0.05, miss: 0.01}, + } + ) + examples = [ + example_from_eval_row( + row, docs, k=10, ce_scores=row["ce"] + ) + for row in rows + ] + self.assertTrue(all(item is not None for item in examples)) + model = fit_lightgbm( + examples[:-1], + pair_policy=PAIR_OTHER_QUERY_NEG, + min_data_in_leaf=1, + ) + ranked = rerank_ids_model(examples[-1], model) + self.assertEqual(ranked[0], "gold-7") + self.assertGreater(float(sum(model.feature_importances_)), 0.0) + + def test_ltr_apply_trains_on_other_run_not_eval_qid(self): + from search.ltr_head import PROTOCOL_APPLY, run_ltr_head + + train_docs = [ + {"type": "doc", "doc_id": "gold-t", "title": "Navy velvet sofa", "text": "Title: Navy velvet sofa"}, + {"type": "doc", "doc_id": "irr-t", "title": "plastic lid", "text": "Title: plastic lid"}, + ] + eval_docs = [ + {"type": "doc", "doc_id": "gold-e", "title": "Navy velvet sofa", "text": "Title: Navy velvet sofa"}, + {"type": "doc", "doc_id": "irr-e", "title": "plastic lid", "text": "Title: plastic lid"}, + ] + train_queries = [ + { + "type": "query", + "qid": "q-train", + "query": "navy velvet sofa", + "gold_doc_ids": ["gold-t"], + "gold_grades": {"gold-t": 1.0}, + } + ] + eval_queries = [ + { + "type": "query", + "qid": "q-eval", + "query": "navy velvet sofa", + "gold_doc_ids": ["gold-e"], + "gold_grades": {"gold-e": 1.0}, + } + ] + train_row = { + "qid": "q-train", + "query": "navy velvet sofa", + "hit_ids": ["irr-t", "gold-t"], + "bm25_ids": ["gold-t"], + "dense_ids": ["irr-t"], + "hits": [ + {"id": "irr-t", "doc_id": "irr-t"}, + {"id": "gold-t", "doc_id": "gold-t"}, + ], + "gold_doc_ids": ["gold-t"], + "gold_grades": {"gold-t": 1.0}, + } + eval_row = { + "qid": "q-eval", + "query": "navy velvet sofa", + "hit_ids": ["irr-e", "gold-e"], + "bm25_ids": ["gold-e"], + "dense_ids": ["irr-e"], + "hits": [ + {"id": "irr-e", "doc_id": "irr-e"}, + {"id": "gold-e", "doc_id": "gold-e"}, + ], + "gold_doc_ids": ["gold-e"], + "gold_grades": {"gold-e": 1.0}, + } + result = run_ltr_head( + {"run_id": "toy-eval", "queries": [eval_row]}, + eval_docs + eval_queries, + dataset_name="toy-eval.jsonl", + k=10, + ks=(5, 10), + n_folds=2, + train_eval_result={"run_id": "toy-train", "queries": [train_row]}, + train_rows=train_docs + train_queries, + train_source_run="toy-train", + ) + self.assertEqual(result["protocol"], PROTOCOL_APPLY) + self.assertEqual(result["ltr_train_run"], "toy-train") + self.assertEqual(result["ltr_n_train_queries"], 1) + self.assertEqual(result["queries"][0]["qid"], "q-eval") + self.assertEqual(result["queries"][0]["hit_ids"][0], "gold-e") + self.assertGreaterEqual(result["metrics"]["recall@10"], 1.0) + + def test_ltr_apply_lightgbm_trains_on_other_run_not_eval_qid(self): + from search.ltr_head import HEAD_LIGHTGBM, PROTOCOL_APPLY, run_ltr_head + + train_docs: list[dict] = [] + train_queries: list[dict] = [] + train_eval_rows: list[dict] = [] + for index in range(8): + gold = f"gold-t-{index}" + miss = f"miss-t-{index}" + train_docs.extend( + [ + { + "type": "doc", + "doc_id": gold, + "title": f"Navy velvet sofa {index}", + "text": f"Title: Navy velvet sofa {index}", + }, + { + "type": "doc", + "doc_id": miss, + "title": f"plastic lid {index}", + "text": f"Title: plastic lid {index}", + }, + ] + ) + qid = f"q-train-{index}" + train_queries.append( + { + "type": "query", + "qid": qid, + "query": "navy velvet sofa", + "gold_doc_ids": [gold], + "gold_grades": {gold: 1.0}, + } + ) + train_eval_rows.append( + { + "qid": qid, + "query": "navy velvet sofa", + "hit_ids": [miss, gold], + "bm25_ids": [gold], + "dense_ids": [miss], + "hits": [ + {"id": miss, "doc_id": miss}, + {"id": gold, "doc_id": gold}, + ], + "gold_doc_ids": [gold], + "gold_grades": {gold: 1.0}, + } + ) + eval_docs = [ + { + "type": "doc", + "doc_id": "gold-e", + "title": "Navy velvet sofa", + "text": "Title: Navy velvet sofa", + }, + { + "type": "doc", + "doc_id": "irr-e", + "title": "plastic lid", + "text": "Title: plastic lid", + }, + ] + eval_queries = [ + { + "type": "query", + "qid": "q-eval", + "query": "navy velvet sofa", + "gold_doc_ids": ["gold-e"], + "gold_grades": {"gold-e": 1.0}, + } + ] + eval_row = { + "qid": "q-eval", + "query": "navy velvet sofa", + "hit_ids": ["irr-e", "gold-e"], + "bm25_ids": ["gold-e"], + "dense_ids": ["irr-e"], + "hits": [ + {"id": "irr-e", "doc_id": "irr-e"}, + {"id": "gold-e", "doc_id": "gold-e"}, + ], + "gold_doc_ids": ["gold-e"], + "gold_grades": {"gold-e": 1.0}, + } + result = run_ltr_head( + {"run_id": "toy-eval", "queries": [eval_row]}, + eval_docs + eval_queries, + dataset_name="toy-eval.jsonl", + k=10, + ks=(5, 10), + n_folds=2, + ltr_head=HEAD_LIGHTGBM, + train_eval_result={"run_id": "toy-train", "queries": train_eval_rows}, + train_rows=train_docs + train_queries, + train_source_run="toy-train", + ) + self.assertEqual(result["protocol"], PROTOCOL_APPLY) + self.assertEqual(result["ltr_train_run"], "toy-train") + self.assertEqual(result["ltr_n_train_queries"], 8) + self.assertEqual(result["rerank"], "ltr-lightgbm") + self.assertEqual(result["queries"][0]["qid"], "q-eval") + self.assertNotEqual(result["queries"][0]["qid"], "q-train-0") + + def test_export_hybrid_lists_holdout_unlabeled_is_i(self): + from search.export_hybrid_lists import ( + PROTECTED_OUT_NAMES, + export_hybrid_lists, + rows_from_eval, + ) + + self.assertIn("esci_retrieved_lists.jsonl", PROTECTED_OUT_NAMES) + eval_result = { + "queries": [ + { + "qid": "esci-hold", + "query": "navy sofa", + "hit_ids": ["gold-h", "miss-h"], + "hits": [ + {"id": "gold-h", "doc_id": "gold-h"}, + {"id": "miss-h", "doc_id": "miss-h"}, + ], + "gold_grades": {"gold-h": 1.0}, + }, + { + "qid": "esci-ok", + "query": "navy sofa", + "hit_ids": ["gold-ok", "miss-ok"], + "hits": [ + {"id": "gold-ok", "doc_id": "gold-ok"}, + {"id": "miss-ok", "doc_id": "miss-ok"}, + ], + "gold_grades": {"gold-ok": 1.0}, + }, + ] + } + docs = [ + { + "type": "doc", + "doc_id": "gold-ok", + "title": "Navy sofa", + "text": "Title: Navy sofa", + }, + { + "type": "doc", + "doc_id": "miss-ok", + "title": "plastic lid", + "text": "Title: plastic lid", + }, + ] + queries = [ + { + "type": "query", + "qid": "esci-ok", + "query": "navy sofa", + "gold_doc_ids": ["gold-ok"], + "gold_grades": {"gold-ok": 1.0}, + }, + { + "type": "query", + "qid": "esci-hold", + "query": "navy sofa", + "gold_doc_ids": ["gold-h"], + "gold_grades": {"gold-h": 1.0}, + }, + ] + rows = rows_from_eval( + eval_result, + docs=docs, + queries=queries, + holdout={"esci-hold", "hold"}, + k=50, + ) + qids = {row["query_id"] for row in rows} + self.assertNotIn("esci-hold", qids) + self.assertIn("esci-ok", qids) + by_pid = {row["product_id"]: row["label"] for row in rows} + self.assertEqual(by_pid["gold-ok"], "E") + self.assertEqual(by_pid["miss-ok"], "I") + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + dataset = root / "train.jsonl" + holdout_path = root / "holdout.jsonl" + out = root / "hybrid.jsonl" + dataset.write_text( + json.dumps(docs[0]) + + "\n" + + json.dumps(docs[1]) + + "\n" + + json.dumps(queries[0]) + + "\n", + encoding="utf-8", + ) + holdout_path.write_text( + json.dumps( + { + "type": "query", + "qid": "esci-hold", + "query": "navy sofa", + "gold_doc_ids": ["gold-h"], + } + ) + + "\n", + encoding="utf-8", + ) + meta = export_hybrid_lists( + eval_result=eval_result, + dataset_path=dataset, + holdout_path=holdout_path, + out_path=out, + k=50, + ) + self.assertEqual(meta["source"], "hybrid-k50") + self.assertEqual(meta["n_queries"], 1) + with self.assertRaises(ValueError): + export_hybrid_lists( + eval_result=eval_result, + dataset_path=dataset, + holdout_path=holdout_path, + out_path=root / "esci_retrieved_lists.jsonl", + k=50, + ) + + def test_retrieved_lists_unlabeled_is_irrelevant(self): + from search.finetune_esci_4class import held_out_query_ids + from search.mine_retrieved_lists import ( + is_held_out, + labeled_hits, + mine_from_groups, + select_groups, + ) + from search.pool_first_stage import Bm25Index, rank_docs, tokenize + + docs = [ + ("B001", tokenize("red ceramic mug")), + ("B002", tokenize("blue dinner plate")), + ("B003", tokenize("red mug gift set")), + ] + index = Bm25Index(docs) + qrels = {"B001": "E", "B003": "S"} + scores = index.scores(tokenize("red ceramic mug")) + ranked = rank_docs(scores, ["B001", "B002", "B003"]) + hits = labeled_hits(ranked, qrels, k=3) + by_id = dict(hits) + self.assertEqual(by_id["B001"], "E") + self.assertEqual(by_id["B003"], "S") + self.assertEqual(by_id["B002"], "I") + self.assertTrue(any(label == "I" for _, label in hits)) + + groups = { + "q1": { + "query": "red ceramic mug", + "qrels": {"B001": "E", "B003": "S"}, + } + } + passages = { + "B001": "Title: red ceramic mug", + "B002": "Title: blue dinner plate", + "B003": "Title: red mug gift set", + } + rows = mine_from_groups(groups, passages, k=3) + labels = {row["product_id"]: row["label"] for row in rows} + self.assertEqual(labels["B001"], "E") + self.assertEqual(labels["B002"], "I") + + holdout_path = self.require_local_artifact( + Path(__file__).resolve().parents[1] + / "benchmarks" + / "data" + / "search_esci_74.jsonl" + ) + holdout = held_out_query_ids(holdout_path) + self.assertTrue(is_held_out("72", holdout)) + selected = select_groups( + { + "72": {"query": "not electronics", "qrels": {"x": "E"}}, + "esci-113": {"query": "html", "qrels": {"y": "E"}}, + "900001": {"query": "ok", "qrels": {"z": "E"}}, + }, + holdout=holdout, + max_queries=10, + seed=1, + ) + self.assertNotIn("72", selected) + self.assertNotIn("esci-113", selected) + self.assertIn("900001", selected) + + def test_local_colbert_fake_encoder_ranks_match(self): + from search.local_colbert import PLUGIN_DIR, run_local_colbert + + import sys + + self.require_local_artifact(PLUGIN_DIR / "encode.py") + if str(PLUGIN_DIR) not in sys.path: + sys.path.insert(0, str(PLUGIN_DIR)) + import encode as colbert_encode + + def fake_tokens(text: str): + blob = (text or "").lower() + if "match" in blob or blob == "q": + return [[1.0, 0.0]] + return [[0.0, 1.0]] + + colbert_encode.set_encoder(fake_tokens) + try: + rows = [ + {"type": "doc", "doc_id": "hit", "text": "match"}, + {"type": "doc", "doc_id": "miss", "text": "other"}, + { + "type": "query", + "qid": "q1", + "query": "q", + "gold_doc_ids": ["hit"], + "gold_grades": {"hit": 1.0}, + }, + ] + result = run_local_colbert( + rows, + dataset_name="toy", + k=2, + ks=(5, 10, 20, 50), + brain_id="harness-local-colbert", + ) + self.assertEqual(result["channels"], ["harness-colbert"]) + self.assertEqual(result["queries"][0]["hit_ids"][0], "hit") + finally: + colbert_encode.set_encoder(None) + + def test_failed_report_skips_ledger(self): + from search.report import entry_from_report, update_reports_json + + failed = { + "status": "failed", + "run_id": "search-fail", + "brain_id": "searchbenchsmoke", + "dataset": "search_toy.jsonl", + "n_queries": 8, + "ndcg@10": 0.9, + } + self.assertIsNone(entry_from_report(failed)) + empty_ok = { + "status": "ok", + "run_id": "search-empty", + "n_queries": 0, + } + self.assertIsNone(entry_from_report(empty_ok)) + + ledger = { + "schema_version": 2, + "benchmarks": { + "locomo": {"name": "LoCoMo", "leaderboard": [{"run_id": "keep-me"}]}, + "recsys": {"name": "RecSys", "leaderboard": [{"run_id": "keep-recsys"}]}, + "search": {"name": "Search (hybrid BM25 + dense)", "leaderboard": []}, + }, + } + with tempfile.TemporaryDirectory() as tmp: + path = Path(tmp) / "REPORTS.json" + path.write_text(json.dumps(ledger), encoding="utf-8") + with patch("search.report.REPORTS_PATH", path): + update_reports_json(failed) + after = json.loads(path.read_text(encoding="utf-8")) + self.assertEqual( + after["benchmarks"]["locomo"]["leaderboard"][0]["run_id"], + "keep-me", + ) + self.assertEqual( + after["benchmarks"]["recsys"]["leaderboard"][0]["run_id"], + "keep-recsys", + ) + self.assertEqual(after["benchmarks"]["search"]["leaderboard"], []) + + ok = { + "status": "ok", + "run_id": "search-ok", + "brain_id": "searchbenchsmoke", + "dataset": "search_toy.jsonl", + "fusion": "rrf", + "n_queries": 8, + "ndcg@10": 0.5, + "recall@10": 0.6, + "mrr": 0.4, + "p50_retrieve_ms": 12.0, + "p95_retrieve_ms": 30.0, + "git_sha": "abc", + "recorded_at": "2026-08-18T00:00:00+00:00", + } + with patch("search.report.REPORTS_PATH", path): + update_reports_json(ok) + after_ok = json.loads(path.read_text(encoding="utf-8")) + self.assertEqual( + after_ok["benchmarks"]["locomo"]["leaderboard"][0]["run_id"], + "keep-me", + ) + self.assertEqual(len(after_ok["benchmarks"]["search"]["leaderboard"]), 1) + self.assertEqual( + after_ok["benchmarks"]["search"]["leaderboard"][0]["run_id"], + "search-ok", + ) + + def test_cli_refuses_memory_brain(self): + from search.cli import main + + with self.assertRaises(SystemExit) as ctx: + main(["--brain", "locomoconv26", "dataset-stats"]) + self.assertIn("Refusing brain_id", str(ctx.exception)) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_search_bm25.py b/tests/test_search_bm25.py new file mode 100644 index 0000000000000000000000000000000000000000..65040a0e4d0158d85412ad6647fe4348cdaff837 --- /dev/null +++ b/tests/test_search_bm25.py @@ -0,0 +1,256 @@ +import os +import unittest +from unittest.mock import MagicMock, patch + +ENV_DEFAULTS = { + "BRAINPAT_TOKEN": "test-token", + "MODELS_MODE": "local", + "EMBEDDINGS_LOCAL_MODEL": "local-model", + "EMBEDDINGS_SMALL_MODEL": "small-model", + "EMBEDDING_NODES_DIMENSION": "3", + "EMBEDDING_TRIPLETS_DIMENSION": "3", + "EMBEDDING_OBSERVATIONS_DIMENSION": "3", + "EMBEDDING_DATA_DIMENSION": "3", + "EMBEDDING_RELATIONSHIPS_DIMENSION": "3", + "REDIS_HOST": "localhost", + "REDIS_PORT": "6379", + "NEO4J_HOST": "localhost", + "NEO4J_PORT": "7687", + "NEO4J_USERNAME": "neo4j", + "NEO4J_PASSWORD": "password", + "MILVUS_HOST": "localhost", + "MILVUS_PORT": "19530", + "MONGO_CONNECTION_STRING": "mongodb://localhost:27017", + "CELERY_WORKER_CONCURRENCY": "1", + "OLLAMA_HOST": "localhost", + "OLLAMA_PORT": "11434", + "OLLAMA_LLM_SMALL_MODEL": "small", + "OLLAMA_LLM_LARGE_MODEL": "large", +} +for key, value in ENV_DEFAULTS.items(): + os.environ.setdefault(key, value) + +from src.core.search.bm25 import okapi_bm25_document +from src.constants.data import TextChunk +from src.lib.postgresql import data as pgdata + + +def _postgres_available() -> bool: + try: + import psycopg2 + + pg = pgdata.config.postgresql + pg.validate_credentials() + conn = psycopg2.connect( + host=pg.host, + port=pg.port, + user=pg.username, + password=pg.password, + dbname=pg.maintenance_database or "postgres", + connect_timeout=2, + ) + conn.close() + return True + except Exception: + return False + + +class OkapiBm25Tests(unittest.TestCase): + def test_term_overlap_ranks_above_non_overlap(self): + overlap = okapi_bm25_document( + {"license": 2.0}, + dl=8, + avgdl=8, + n_docs=2, + dfs={"license": 1}, + ) + non_overlap = okapi_bm25_document( + {}, + dl=8, + avgdl=8, + n_docs=2, + dfs={"license": 1}, + ) + self.assertGreater(overlap, non_overlap) + self.assertEqual(non_overlap, 0.0) + + def test_higher_tf_ranks_higher(self): + high = okapi_bm25_document( + {"license": 3.0}, + dl=10, + avgdl=10, + n_docs=4, + dfs={"license": 2}, + ) + low = okapi_bm25_document( + {"license": 1.0}, + dl=10, + avgdl=10, + n_docs=4, + dfs={"license": 2}, + ) + self.assertGreater(high, low) + + def test_search_ddl_is_gated_off_base_schema(self): + self.assertNotIn("search_tsv", pgdata._BRAIN_DDL) + self.assertIn("search_tsv", pgdata._SEARCH_DDL) + self.assertIn("gin", pgdata._SEARCH_DDL.lower()) + self.assertIn("to_tsvector", pgdata._SEARCH_DDL) + self.assertIn("to_tsvector('english'", pgdata._SEARCH_DDL) + self.assertNotIn("to_tsvector('italian'", pgdata._SEARCH_DDL) + self.assertNotIn("to_tsvector('spanish'", pgdata._SEARCH_DDL) + self.assertNotIn("search_tsv_alt", pgdata._SEARCH_DDL) + alt = pgdata._search_alt_ddl("italian") + self.assertIn("to_tsvector('italian'", alt) + self.assertIn("search_tsv_alt", alt) + with self.assertRaises(ValueError): + pgdata._search_alt_ddl("english") + + def test_english_bm25_sql_keeps_plainto_and(self): + sql = pgdata._bm25_sql("english", "search_tsv", "search_len") + self.assertIn("plainto_tsquery('english'", sql) + self.assertIn("search_tsv", sql) + self.assertNotIn("search_tsv_alt", sql) + self.assertNotIn("WHERE TRUE", sql) + + def test_alt_bm25_sql_matches_any_lexeme(self): + sql = pgdata._bm25_sql("italian", "search_tsv_alt", "search_len_alt") + self.assertNotIn("plainto_tsquery", sql) + self.assertIn("WHERE TRUE", sql) + self.assertIn("search_tsv_alt", sql) + self.assertIn("search_len_alt", sql) + and_sql = pgdata._bm25_sql( + "italian", "search_tsv_alt", "search_len_alt", query_match="and" + ) + self.assertIn("plainto_tsquery('italian'", and_sql) + + def test_searchbenchesci74_stays_english_and_path(self): + with ( + patch.object( + pgdata.config, "search_fts_brains", frozenset({"searchbenchitsmoke"}) + ), + patch.object(pgdata.config, "search_fts_regconfig", "italian"), + ): + self.assertIsNone( + pgdata.config.search_fts_regconfig_for_brain("searchbenchesci74") + ) + self.assertIsNone( + pgdata.config.search_fts_regconfig_for_brain("locomoconv26") + ) + + def test_node_search_schema_skipped_when_search_disabled(self): + from src.lib.postgresql.graph_store import PostgreSQLGraphStore + + store = PostgreSQLGraphStore.__new__(PostgreSQLGraphStore) + store._schema_ready = {"brain-a"} + store._search_ready_brains = set() + store._schema_lock = MagicMock() + with patch.object(pgdata.config, "search_enabled", False): + store._ensure_brain_schema("brain-a") + self.assertEqual(store._search_ready_brains, set()) + store._schema_lock.assert_not_called() + + def test_search_schema_skipped_when_search_disabled(self): + client = pgdata.PostgreSQLDataClient.__new__(pgdata.PostgreSQLDataClient) + client._initialized_brains = {"brain-a"} + client._search_ready_brains = set() + client._search_alt_brains = set() + client._lock = MagicMock() + with patch.object(pgdata.config, "search_enabled", False): + client._ensure_brain_schema("brain-a") + self.assertEqual(client._search_ready_brains, set()) + + @unittest.skipUnless(_postgres_available(), "postgres not available") + def test_postgres_bm25_ranks_term_overlap_first(self): + brain_id = "searchbench_bm25_unit" + with patch.object(pgdata.config, "search_enabled", True): + client = pgdata.PostgreSQLDataClient() + overlap = TextChunk( + id="overlap", + text="alice counseling license renewal board", + ) + other = TextChunk( + id="other", + text="weather forecast sunny tomorrow picnic", + ) + client.save_text_chunk(overlap, brain_id) + client.save_text_chunk(other, brain_id) + ranked = client.search_bm25("counseling license", brain_id, limit=10) + ids = [chunk.id for chunk, _score in ranked] + self.assertTrue(ids, "expected at least one BM25 hit") + self.assertEqual(ids[0], "overlap") + self.assertNotIn("other", ids) + + def test_fts_regconfig_only_allowlisted_searchbench(self): + with ( + patch.object( + pgdata.config, + "search_fts_brains", + frozenset({"locomoconv26", "searchbenchitsmoke", "demorecsys"}), + ), + patch.object(pgdata.config, "search_fts_regconfig", "italian"), + ): + self.assertEqual( + pgdata.config.search_fts_regconfig_for_brain("searchbenchitsmoke"), + "italian", + ) + self.assertIsNone( + pgdata.config.search_fts_regconfig_for_brain("searchbenchescies") + ) + self.assertIsNone(pgdata.config.search_fts_regconfig_for_brain("locomoconv26")) + self.assertIsNone(pgdata.config.search_fts_regconfig_for_brain("demorecsys")) + self.assertIsNone(pgdata.config.search_fts_regconfig_for_brain("beam1m1clean")) + + def test_alt_ddl_not_executed_for_memory_brain(self): + client = pgdata.PostgreSQLDataClient.__new__(pgdata.PostgreSQLDataClient) + client._initialized_brains = {"locomoconv26"} + client._search_ready_brains = {"locomoconv26"} + client._search_alt_brains = set() + client._lock = MagicMock() + with ( + patch.object(pgdata.config, "search_enabled", True), + patch.object( + pgdata.config, "search_fts_regconfig_for_brain", return_value=None + ), + patch.object(pgdata, "ensure_brain_database") as ensure_db, + ): + client._ensure_brain_schema("locomoconv26") + ensure_db.assert_not_called() + self.assertEqual(client._search_alt_brains, set()) + + def test_alt_ddl_executed_for_allowlisted_searchbench(self): + from contextlib import contextmanager + + client = pgdata.PostgreSQLDataClient.__new__(pgdata.PostgreSQLDataClient) + client._initialized_brains = {"searchbenchitsmoke"} + client._search_ready_brains = {"searchbenchitsmoke"} + client._search_alt_brains = set() + client._lock = MagicMock() + cur = MagicMock() + conn = MagicMock() + conn.cursor.return_value.__enter__.return_value = cur + conn.cursor.return_value.__exit__.return_value = False + + @contextmanager + def fake_borrow(_pool): + yield conn + + with ( + patch.object(pgdata.config, "search_enabled", True), + patch.object( + pgdata.config, "search_fts_regconfig_for_brain", return_value="italian" + ), + patch.object(pgdata, "ensure_brain_database"), + patch.object(pgdata, "get_brain_pool"), + patch.object(pgdata, "borrow", fake_borrow), + ): + client._ensure_brain_schema("searchbenchitsmoke") + executed = " ".join(str(call.args[0]) for call in cur.execute.call_args_list) + self.assertIn("search_tsv_alt", executed) + self.assertIn("to_tsvector('italian'", executed) + self.assertNotIn("to_tsvector('english'", executed) + self.assertIn("searchbenchitsmoke", client._search_alt_brains) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_search_catalog.py b/tests/test_search_catalog.py new file mode 100644 index 0000000000000000000000000000000000000000..62d22bdfc1f78ec0c3f222fc0ccda46206ae069d --- /dev/null +++ b/tests/test_search_catalog.py @@ -0,0 +1,687 @@ +import tempfile +import unittest +from pathlib import Path +import os + +ENV_DEFAULTS = { + "BRAINPAT_TOKEN": "test-token", + "MODELS_MODE": "local", + "EMBEDDINGS_LOCAL_MODEL": "local-model", + "EMBEDDINGS_SMALL_MODEL": "small-model", + "EMBEDDING_NODES_DIMENSION": "3", + "EMBEDDING_TRIPLETS_DIMENSION": "3", + "EMBEDDING_OBSERVATIONS_DIMENSION": "3", + "EMBEDDING_DATA_DIMENSION": "3", + "EMBEDDING_RELATIONSHIPS_DIMENSION": "3", + "REDIS_HOST": "localhost", + "REDIS_PORT": "6379", + "NEO4J_HOST": "localhost", + "NEO4J_PORT": "7687", + "NEO4J_USERNAME": "neo4j", + "NEO4J_PASSWORD": "password", + "MILVUS_HOST": "localhost", + "MILVUS_PORT": "19530", + "MONGO_CONNECTION_STRING": "mongodb://localhost:27017", + "CELERY_WORKER_CONCURRENCY": "1", + "OLLAMA_HOST": "localhost", + "OLLAMA_PORT": "11434", + "OLLAMA_LLM_SMALL_MODEL": "small", + "OLLAMA_LLM_LARGE_MODEL": "large", +} +for key, value in ENV_DEFAULTS.items(): + os.environ.setdefault(key, value) + + +class SearchCatalogTests(unittest.TestCase): + @classmethod + def setUpClass(cls): + import sys + + root = Path(__file__).resolve().parents[1] + bench = root / "benchmarks" + if str(root) not in sys.path: + sys.path.insert(0, str(root)) + if str(bench) not in sys.path: + sys.path.insert(0, str(bench)) + + def test_format_product_text_includes_marker(self): + from search.catalog import format_product_text + from search.dataset import doc_marker + + text = format_product_text( + "B07ABC", + title="Blue kettle", + description="1.5 liter stovetop kettle", + extras=(("Brand", "Acme"), ("Color", "blue")), + ) + self.assertIn(doc_marker("B07ABC"), text) + self.assertIn("Title: Blue kettle", text) + self.assertIn("Brand: Acme", text) + + def test_select_catalog_keeps_distractors_and_grades(self): + from search.catalog import ESCI_GAINS, _docs_and_queries, _select_catalog + + queries, needed = _select_catalog( + [ + { + "query_id": "10", + "query": "blue kettle", + "product_id": "p-exact", + "esci_label": "E", + }, + { + "query_id": "10", + "query": "blue kettle", + "product_id": "p-sub", + "esci_label": "S", + }, + { + "query_id": "10", + "query": "blue kettle", + "product_id": "p-irr", + "esci_label": "I", + }, + ], + max_queries=8, + max_docs=20, + candidates_per_query=40, + slice_name="esci-us", + dataset="esci", + gains=ESCI_GAINS, + label_key="esci_label", + query_text_key="query", + ) + self.assertEqual(needed, {"p-exact", "p-sub", "p-irr"}) + self.assertEqual(len(queries), 1) + self.assertEqual(queries[0]["gold_grades"]["p-exact"], 1.0) + self.assertEqual(queries[0]["gold_grades"]["p-sub"], 0.1) + self.assertNotIn("p-irr", queries[0]["gold_grades"]) + self.assertEqual(queries[0]["candidate_grades"]["p-irr"], 0.0) + self.assertEqual( + queries[0]["candidate_doc_ids"], + ["p-exact", "p-sub", "p-irr"], + ) + rows = _docs_and_queries( + queries, + products={ + "p-exact": {"product_title": "Blue kettle", "product_description": ""}, + "p-sub": {"product_title": "Teapot", "product_description": ""}, + "p-irr": {"product_title": "Lamp", "product_description": ""}, + }, + doc_ids=needed, + dataset="esci", + title_key="product_title", + description_key="product_description", + ) + docs = [row for row in rows if row["type"] == "doc"] + self.assertEqual({row["doc_id"] for row in docs}, needed) + self.assertEqual(docs[0]["title"], "Blue kettle") + + def test_select_catalog_skips_holdout_qids(self): + from search.catalog import ESCI_GAINS, _select_catalog + + queries, needed = _select_catalog( + [ + { + "query_id": "72", + "query": "held out kettle", + "product_id": "p-hold", + "esci_label": "E", + }, + { + "query_id": "99", + "query": "train sofa", + "product_id": "p-train", + "esci_label": "E", + }, + ], + max_queries=8, + max_docs=20, + candidates_per_query=40, + slice_name="esci-us", + dataset="esci", + gains=ESCI_GAINS, + label_key="esci_label", + query_text_key="query", + holdout_qids={"esci-72", "72"}, + ) + self.assertEqual([row["qid"] for row in queries], ["esci-99"]) + self.assertEqual(needed, {"p-train"}) + + def test_wands_gains_select_exact_and_partial_not_irrelevant(self): + from search.catalog import WANDS_GAINS, _select_catalog + + self.assertEqual( + WANDS_GAINS, + {"Exact": 1.0, "Partial": 0.5, "Irrelevant": 0.0}, + ) + queries, needed = _select_catalog( + [ + { + "query_id": "1", + "query": "velvet sofa", + "query_class": "Sofas", + "product_id": "p-exact", + "label": "Exact", + }, + { + "query_id": "1", + "query": "velvet sofa", + "query_class": "Sofas", + "product_id": "p-partial", + "label": "Partial", + }, + { + "query_id": "1", + "query": "velvet sofa", + "query_class": "Sofas", + "product_id": "p-irr", + "label": "Irrelevant", + }, + ], + max_queries=8, + max_docs=20, + candidates_per_query=40, + slice_name="wands", + dataset="wands", + gains=WANDS_GAINS, + label_key="label", + query_text_key="query", + query_slice_key="query_class", + ) + self.assertEqual(needed, {"p-exact", "p-partial", "p-irr"}) + self.assertEqual(len(queries), 1) + self.assertEqual(queries[0]["qid"], "wands-1") + self.assertEqual(queries[0]["gold_grades"]["p-exact"], 1.0) + self.assertEqual(queries[0]["gold_grades"]["p-partial"], 0.5) + self.assertNotIn("p-irr", queries[0]["gold_grades"]) + self.assertEqual(queries[0]["gold_doc_ids"], ["p-exact", "p-partial"]) + self.assertEqual(queries[0]["candidate_grades"]["p-irr"], 0.0) + + def test_frozen_wands_jsonl_blocks_overwrite(self): + from search.catalog import DATA_DIR, catalog_overwrite_blocked + + self.assertTrue( + catalog_overwrite_blocked(DATA_DIR / "search_esci_74.jsonl") + ) + wands = DATA_DIR / "search_wands.jsonl" + if not wands.exists(): + self.skipTest("optional frozen WANDS dataset is absent") + self.assertTrue(wands.exists() and wands.stat().st_size > 0) + self.assertTrue(catalog_overwrite_blocked(wands)) + self.assertFalse( + catalog_overwrite_blocked(DATA_DIR / "search_esci.jsonl") + ) + + def test_frozen_structured_brains_exclude_wandsgraph(self): + from search.config import FROZEN_STRUCTURED_BRAINS + + self.assertIn("searchbenchwands", FROZEN_STRUCTURED_BRAINS) + self.assertIn("searchbenchesci74", FROZEN_STRUCTURED_BRAINS) + self.assertNotIn("searchbenchwandsgraph", FROZEN_STRUCTURED_BRAINS) + self.assertNotIn("searchbenchjdslice", FROZEN_STRUCTURED_BRAINS) + + def test_evaluate_refuses_frozen_structured_ingest(self): + from search.evaluate import evaluate_search + + class _Client: + class _Settings: + brain_id = "searchbenchwands" + + settings = _Settings() + + with self.assertRaises(SystemExit): + evaluate_search(_Client(), [], ingest_graph=True) + with self.assertRaises(SystemExit): + evaluate_search( + _Client(), + [], + interactions=[{"user_id": "u", "item_id": "0"}], + ) + + def test_wands_feature_parse_and_catalog_triples(self): + from search.mapping import ( + docs_to_triples, + entity_uuid, + parse_feature_string, + ) + + parsed = parse_feature_string( + "|Color:Navy|Material:Velvet|InStock:yes|Finish:|" + ) + self.assertEqual(parsed, [("Color", "Navy"), ("Material", "Velvet")]) + docs = [ + { + "doc_id": f"d{i}", + "title": f"Item {i}", + "class": "sofas" if i == 1 else "", + "brand": "Acme" if i == 2 else "", + "features": "|Color:Navy|" if i == 3 else "", + "dataset": "wands", + } + for i in range(1, 6) + ] + triples = docs_to_triples(docs) + uuids = {row["subject"]["uuid"] for row in triples} + self.assertEqual(uuids, {entity_uuid(f"d{i}") for i in range(1, 6)}) + self.assertTrue(all(row.get("event") is None for row in triples)) + self.assertTrue(all(row["subj_event"]["name"] == "HAS" for row in triples)) + self.assertTrue(all("event_obj" not in row or row.get("event_obj") is None for row in triples)) + objects = {(row["object"]["type"], row["object"]["name"]) for row in triples} + self.assertIn(("CLASS", "sofas"), objects) + self.assertIn(("ATTR", "Acme"), objects) + self.assertIn(("ATTR", "Navy"), objects) + self.assertNotIn("demorecsys", str(triples)) + + def test_catalog_graph_sofas_modern_has_search_text(self): + from search.mapping import doc_to_triples, parse_feature_string, split_hierarchy + + parsed = parse_feature_string("dsprimaryproductstyle : modern|color : navy") + self.assertIn(("dsprimaryproductstyle", "modern"), parsed) + self.assertEqual(split_hierarchy("Furniture > Living Room > Sofas"), ["Furniture", "Living Room", "Sofas"]) + triples = doc_to_triples( + { + "doc_id": "sofa-1", + "title": "Velvet sofa", + "description": "A low modern sofa for small rooms", + "class": "sofas", + "hierarchy": "Furniture > Living Room > Sofas", + "features": "style:modern|color:navy", + "price": "899", + "dataset": "wands", + } + ) + objects = {(row["object"]["type"], row["object"]["name"]) for row in triples} + self.assertIn(("CLASS", "sofas"), objects) + self.assertIn(("ATTR", "modern"), objects) + self.assertIn(("TYPE", "Furniture"), objects) + subject = triples[0]["subject"] + self.assertIn("modern sofa", subject["properties"]["search_text"].lower()) + self.assertIn("sofas", subject["properties"]["search_text"].lower()) + self.assertIn("modern", subject["properties"]["search_text"].lower()) + self.assertIn("navy", subject["properties"]["search_text"].lower()) + self.assertEqual(subject["properties"]["price"], "899") + hub_texts = {row["object"]["properties"]["search_text"] for row in triples} + self.assertTrue(any("modern" in text.lower() for text in hub_texts)) + self.assertNotIn(("ATTR", "899"), objects) + self.assertTrue(all(row.get("event") is None for row in triples)) + self.assertTrue(all(row["subj_event"]["name"] == "HAS" for row in triples)) + self.assertTrue( + all("dsprimaryproductstyle" not in str(row.get("subj_event") or {}) for row in triples) + ) + + def test_entity_search_text_prefers_catalog_blob(self): + from search.mapping import doc_id_from_text, doc_to_triples, node_id_from_passage_text + + blob = ( + "DOCID wands-bed.\n" + "Title: solid wood platform bed\n" + "Class: Beds\n" + "Features: dsprimaryproductstyle : modern|woodspecies : rubberwood" + ) + triples = doc_to_triples( + { + "doc_id": "wands-bed", + "title": "solid wood platform bed", + "class": "Beds", + "features": "dsprimaryproductstyle : modern|woodspecies : rubberwood", + "text": blob, + } + ) + subject = triples[0]["subject"] + self.assertEqual(subject["properties"]["search_text"], blob) + self.assertIn("rubberwood", subject["properties"]["search_text"]) + self.assertIsNone(triples[0].get("event")) + self.assertEqual(triples[0]["subj_event"]["name"], "HAS") + self.assertNotIn("rubberwood", triples[0]["subj_event"]["name"]) + self.assertEqual(doc_id_from_text(blob), "wands-bed") + self.assertEqual(node_id_from_passage_text(blob), "wands-bed") + self.assertIsNone(node_id_from_passage_text("no marker here")) + + def test_catalog_has_is_direct_edge(self): + from search.mapping import doc_to_triples, is_static_has_triple + + triples = doc_to_triples( + { + "doc_id": "0", + "title": "solid wood platform bed", + "class": "Beds", + "features": "woodspecies : rubberwood", + } + ) + self.assertTrue(triples) + self.assertTrue(all(is_static_has_triple(row) for row in triples)) + self.assertTrue(all(row.get("event") is None for row in triples)) + kinds = {(row["object"]["type"], row["object"]["name"]) for row in triples} + self.assertIn(("CLASS", "Beds"), kinds) + self.assertIn(("ATTR", "rubberwood"), kinds) + + def test_entity_backfill_refuses_frozen_brains(self): + from search.backfill_entities import ( + apply_entity_text_backfill, + backfill_rows_from_docs, + refuse_entity_backfill, + ) + + self.assertEqual(refuse_entity_backfill("searchbenchwandsgraph"), "searchbenchwandsgraph") + with self.assertRaises(SystemExit): + refuse_entity_backfill("searchbenchwands") + with self.assertRaises(SystemExit): + refuse_entity_backfill("searchbenchesci74") + rows = backfill_rows_from_docs( + [ + { + "doc_id": "0", + "title": "solid wood platform bed", + "class": "Beds", + "features": "dsprimaryproductstyle : modern|woodspecies : rubberwood", + "text": "DOCID 0.\nTitle: solid wood platform bed\nFeatures: woodspecies : rubberwood", + } + ] + ) + self.assertEqual(rows[0]["uuid"], "0") + self.assertIn("rubberwood", rows[0]["search_text"]) + with self.assertRaises(SystemExit): + apply_entity_text_backfill( + brain_id="searchbenchwands", + rows=rows, + graph=None, + embeddings=None, + vector_store=None, + ) + + def test_apply_entity_backfill_updates_entity_not_event(self): + from types import SimpleNamespace + from unittest.mock import MagicMock + + from search.backfill_entities import apply_entity_text_backfill + from src.constants.embeddings import Vector + + node = SimpleNamespace( + uuid="0", + name="bed", + labels=["ENTITY"], + properties={"search_text": "bed", "v_id": "old-v"}, + ) + graph = MagicMock() + graph.get_by_uuid.return_value = node + embeddings = MagicMock() + embeddings.embed_text.return_value = Vector( + id="new", embeddings=[0.1], metadata={} + ) + store = MagicMock() + store.add_vectors.return_value = ["vid-1"] + summary = apply_entity_text_backfill( + brain_id="searchbenchwandsgraph", + rows=[{"uuid": "0", "name": "bed", "search_text": "rubberwood modern bed"}], + graph=graph, + embeddings=embeddings, + vector_store=store, + is_item=lambda uuid, labels: "ENTITY" in (labels or []), + ) + self.assertEqual(summary["updated"], 1) + self.assertEqual(summary["missing"], 0) + written = [ + call.kwargs.get("new_properties") or {} + for call in graph.update_node.call_args_list + ] + self.assertTrue(any("rubberwood" in str(props.get("search_text") or "") for props in written)) + event = SimpleNamespace( + uuid="evt-1", + name="HAS", + labels=["EVENT"], + properties={}, + ) + graph.get_by_uuid.return_value = event + skipped = apply_entity_text_backfill( + brain_id="searchbenchwandsgraph", + rows=[{"uuid": "evt-1", "name": "HAS", "search_text": "should not copy"}], + graph=graph, + embeddings=embeddings, + vector_store=store, + is_item=lambda uuid, labels: "ENTITY" in (labels or []), + ) + self.assertEqual(skipped["updated"], 0) + self.assertEqual(skipped["skipped"], 1) + + def test_wandsgraph_node_join_calls_neighbors(self): + from search.client import TimedResult + from search.evaluate import assert_wandsgraph_node_join + + class _Client: + class _Settings: + brain_id = "searchbenchwandsgraph" + + settings = _Settings() + + def get_neighbors(self, uuid, limit=5): + self.seen = uuid + return TimedResult(data={"count": 4}, latency_ms=1.0, status_code=200) + + client = _Client() + out = assert_wandsgraph_node_join( + client, + { + "queries": [ + { + "qid": "q1", + "hits": [ + { + "id": "chunk-1", + "doc_id": "0", + "node_id": "0", + "channel": "passages", + } + ], + } + ] + }, + ) + self.assertEqual(out["node_id"], "0") + self.assertEqual(out["status_code"], 200) + self.assertEqual(client.seen, "0") + skipped = assert_wandsgraph_node_join( + type("C", (), {"settings": type("S", (), {"brain_id": "searchbenchsmoke"})()})(), + {"queries": []}, + ) + self.assertTrue(skipped["skipped"]) + + def test_interaction_triples_have_happened_at(self): + from search.mapping import interactions_to_triples, load_interaction_rows + + rows = [ + { + "user_id": "u01", + "item_id": "sku-101", + "behavior": "view", + "timestamp": "2024-01-02T10:00:00Z", + "category": "phones", + "brand": "Acme", + }, + { + "user_id": "u01", + "item_id": "sku-102", + "behavior": "purchase", + "timestamp": "2024-01-05T12:00:00Z", + }, + { + "user_id": "u02", + "item_id": "sku-201", + "behavior": "view", + "timestamp": "2024-01-02T09:00:00Z", + }, + ] + triples = interactions_to_triples(rows) + events = [ + row["event"] + for row in triples + if row.get("event") and row["event"]["name"] != "HAS" + ] + self.assertGreaterEqual(len(events), 3) + self.assertTrue(all(event.get("happened_at") for event in events)) + self.assertTrue(all(event["type"] == "EVENT" for event in events)) + self.assertTrue(all(row["object"]["uuid"] in {"sku-101", "sku-102", "sku-201"} or row["object"]["type"] != "ENTITY" for row in triples)) + entity_ids = { + row["object"]["uuid"] + for row in triples + if row["object"]["type"] == "ENTITY" + } + self.assertEqual(entity_ids, {"sku-101", "sku-102", "sku-201"}) + self.assertNotIn("PRODUCT", str(triples)) + self.assertNotIn("Categorized", str(triples)) + self.assertNotIn("demorecsys", str(triples)) + root = Path(__file__).resolve().parents[1] + toy = root / "benchmarks" / "data" / "recsys_toy.jsonl" + if toy.exists(): + loaded = load_interaction_rows(toy)[:3] + self.assertEqual(len(loaded), 3) + self.assertEqual(loaded[0]["item_id"], "sku-101") + + def test_interaction_options_emit_prefers_not_catalog_has(self): + from search.mapping import hub_uuid, interaction_to_triples + + triples = interaction_to_triples( + { + "user_id": "u01", + "item_id": "sku-101", + "behavior": "view", + "timestamp": "2024-01-02T10:00:00Z", + "color": "navy", + "options": {"style": "70s", "color": "olive"}, + }, + seq=1, + ) + prefers = [ + row + for row in triples + if (row.get("subj_event") or {}).get("name") == "PREFERS" + ] + self.assertEqual(len(prefers), 2) + self.assertTrue(all(row.get("event") is None for row in prefers)) + self.assertTrue(all(row["subject"]["uuid"] == "user:u01" for row in prefers)) + style_hub = hub_uuid("attr", "70s") + olive_hub = hub_uuid("attr", "olive") + prefers_tips = {row["object"]["uuid"] for row in prefers} + self.assertEqual(prefers_tips, {style_hub, olive_hub}) + has_tips = { + row["object"]["uuid"] + for row in triples + if (row.get("subj_event") or {}).get("name") == "HAS" + } + self.assertIn(hub_uuid("attr", "navy"), has_tips) + self.assertNotIn(style_hub, has_tips) + self.assertNotIn(olive_hub, has_tips) + + def test_personalize_smoke_fixture_emits_prefers(self): + from search.mapping import hub_uuid, load_interaction_rows, interactions_to_triples + + root = Path(__file__).resolve().parents[1] + fixture = root / "benchmarks" / "data" / "search_personalize_smoke.jsonl" + if not fixture.exists(): + self.skipTest("optional personalization smoke fixture is absent") + rows = load_interaction_rows(fixture) + self.assertEqual(len(rows), 4) + triples = interactions_to_triples(rows) + prefers = [ + row + for row in triples + if (row.get("subj_event") or {}).get("name") == "PREFERS" + ] + self.assertGreaterEqual(len(prefers), 1) + self.assertIn(hub_uuid("attr", "70s"), {row["object"]["uuid"] for row in prefers}) + self.assertTrue(any(row["event"]["name"] == "Favorite" for row in triples if row.get("event"))) + + def test_cli_unknown_catalog(self): + from search.cli import main + + self.assertEqual(main(["download", "--name", "not-a-dataset"]), 1) + + def test_write_roundtrip(self): + from search.dataset import dataset_stats, load_records, write_records + + rows = [ + {"type": "doc", "doc_id": "a", "text": "DOCID a. Title: kettle"}, + { + "type": "query", + "qid": "q1", + "query": "kettle", + "gold_doc_ids": ["a"], + "gold_grades": {"a": 1.0}, + "slice": "esci-us", + }, + ] + with tempfile.TemporaryDirectory() as tmp: + path = Path(tmp) / "search_esci.jsonl" + write_records(rows, path) + loaded = load_records(path) + stats = dataset_stats(loaded) + self.assertEqual(stats["n_docs"], 1) + self.assertEqual(stats["n_queries"], 1) + self.assertTrue(stats["graded"]) + + def test_esci_locale_paths_keep_us_default(self): + from search.catalog import ESCI_JSONL, catalog_jsonl_path, normalize_esci_locale + + self.assertEqual(catalog_jsonl_path("esci"), ESCI_JSONL) + self.assertEqual(catalog_jsonl_path("esci", locale="us"), ESCI_JSONL) + self.assertEqual(catalog_jsonl_path("esci", locale="US"), ESCI_JSONL) + es_path = catalog_jsonl_path("esci", locale="es") + jp_path = catalog_jsonl_path("esci", locale="jp") + self.assertEqual(es_path.name, "search_esci_es.jsonl") + self.assertEqual(jp_path.name, "search_esci_jp.jsonl") + self.assertNotEqual(es_path, ESCI_JSONL) + self.assertNotEqual(jp_path, ESCI_JSONL) + self.assertEqual(normalize_esci_locale("es"), "es") + + def test_esci_locale_rejects_italian(self): + from search.catalog import catalog_jsonl_path, normalize_esci_locale + from search.cli import main + + with self.assertRaises(ValueError) as ctx: + normalize_esci_locale("it") + self.assertIn("no Italian", str(ctx.exception)) + with self.assertRaises(ValueError): + catalog_jsonl_path("esci", locale="it") + self.assertEqual(main(["download", "--name", "esci", "--locale", "it"]), 1) + + def test_italian_smoke_is_not_esci(self): + from search.dataset import dataset_stats, load_records + + root = Path(__file__).resolve().parents[1] + path = root / "benchmarks" / "data" / "search_italian_smoke.jsonl" + if not path.exists(): + self.skipTest("optional Italian smoke fixture is absent") + rows = load_records(path) + stats = dataset_stats(rows) + self.assertGreaterEqual(stats["n_docs"], 3) + self.assertGreaterEqual(stats["n_queries"], 3) + self.assertEqual(stats["slices"], {"italian-smoke": stats["n_queries"]}) + for row in rows: + self.assertNotEqual(row.get("dataset"), "esci") + self.assertNotIn("esci-", str(row.get("qid") or "")) + self.assertNotIn("esci-", str(row.get("slice") or "")) + + def test_italian_inflect_is_not_esci(self): + from search.dataset import dataset_stats, load_records + + root = Path(__file__).resolve().parents[1] + path = root / "benchmarks" / "data" / "search_italian_smoke_inflect.jsonl" + if not path.exists(): + self.skipTest("optional Italian inflection fixture is absent") + rows = load_records(path) + stats = dataset_stats(rows) + self.assertGreaterEqual(stats["n_docs"], 3) + self.assertGreaterEqual(stats["n_queries"], 3) + self.assertEqual(stats["slices"], {"italian-smoke-inflect": stats["n_queries"]}) + queries = [row["query"] for row in rows if row.get("type") == "query"] + self.assertIn("bollitori acciaio", queries) + self.assertIn("divani velluto", queries) + self.assertIn("caffettiere alluminio", queries) + for row in rows: + self.assertNotEqual(row.get("dataset"), "esci") + self.assertNotIn("esci-", str(row.get("qid") or "")) + self.assertNotIn("esci-", str(row.get("slice") or "")) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_search_config.py b/tests/test_search_config.py new file mode 100644 index 0000000000000000000000000000000000000000..872372f092583509050b084d5c7921af879e3c7f --- /dev/null +++ b/tests/test_search_config.py @@ -0,0 +1,145 @@ +import os +import unittest +from unittest.mock import patch + +ENV_DEFAULTS = { + "BRAINPAT_TOKEN": "test-token", + "MODELS_MODE": "local", + "EMBEDDINGS_LOCAL_MODEL": "local-model", + "EMBEDDINGS_SMALL_MODEL": "small-model", + "EMBEDDING_NODES_DIMENSION": "3", + "EMBEDDING_TRIPLETS_DIMENSION": "3", + "EMBEDDING_OBSERVATIONS_DIMENSION": "3", + "EMBEDDING_DATA_DIMENSION": "3", + "EMBEDDING_RELATIONSHIPS_DIMENSION": "3", + "REDIS_HOST": "localhost", + "REDIS_PORT": "6379", + "NEO4J_HOST": "localhost", + "NEO4J_PORT": "7687", + "NEO4J_USERNAME": "neo4j", + "NEO4J_PASSWORD": "password", + "MILVUS_HOST": "localhost", + "MILVUS_PORT": "19530", + "MONGO_CONNECTION_STRING": "mongodb://localhost:27017", + "CELERY_WORKER_CONCURRENCY": "1", + "OLLAMA_HOST": "localhost", + "OLLAMA_PORT": "11434", + "OLLAMA_LLM_SMALL_MODEL": "small", + "OLLAMA_LLM_LARGE_MODEL": "large", + "PIPELINE_MODE": "accurate", +} +for key, value in ENV_DEFAULTS.items(): + os.environ.setdefault(key, value) + +from src.config import ( + validate_context_passage_mode, + validate_search_config, + validate_search_fusion, +) + + +class SearchConfigValidationTests(unittest.TestCase): + def test_defaults_are_search_off_and_fused(self): + self.assertEqual(validate_search_fusion(None), "rrf") + self.assertEqual(validate_context_passage_mode(None), "hybrid") + validate_search_config( + enabled=False, + use_dense=False, + use_bm25=False, + data_db="mongo", + bm25_k1=1.2, + bm25_b=0.75, + ) + + def test_bm25_requires_postgres_data_db(self): + with self.assertRaises(ValueError) as ctx: + validate_search_config( + enabled=True, + use_dense=True, + use_bm25=True, + data_db="mongo", + bm25_k1=1.2, + bm25_b=0.75, + ) + self.assertIn("DATA_DB=postgresql", str(ctx.exception)) + + def test_dense_only_search_supports_non_postgres_data_db(self): + validate_search_config( + enabled=True, + use_dense=True, + use_bm25=False, + data_db="mongo", + bm25_k1=1.2, + bm25_b=0.75, + ) + + def test_enabled_requires_at_least_one_channel(self): + with self.assertRaises(ValueError) as ctx: + validate_search_config( + enabled=True, + use_dense=False, + use_bm25=False, + data_db="postgresql", + bm25_k1=1.2, + bm25_b=0.75, + ) + self.assertIn("SEARCH_USE_DENSE", str(ctx.exception)) + + def test_invalid_fusion_and_passage_mode(self): + with self.assertRaises(ValueError): + validate_search_fusion("weighted") + with self.assertRaises(ValueError): + validate_context_passage_mode("splade") + + def test_config_reads_search_defaults(self): + env = { + "BRAINPAT_TOKEN": "test-token", + "MODELS_MODE": "local", + "EMBEDDINGS_LOCAL_MODEL": "local-model", + "EMBEDDINGS_SMALL_MODEL": "small-model", + "EMBEDDING_NODES_DIMENSION": "3", + "EMBEDDING_TRIPLETS_DIMENSION": "3", + "EMBEDDING_OBSERVATIONS_DIMENSION": "3", + "EMBEDDING_DATA_DIMENSION": "3", + "EMBEDDING_RELATIONSHIPS_DIMENSION": "3", + "REDIS_HOST": "localhost", + "REDIS_PORT": "6379", + "NEO4J_HOST": "localhost", + "NEO4J_PORT": "7687", + "NEO4J_USERNAME": "neo4j", + "NEO4J_PASSWORD": "password", + "MILVUS_HOST": "localhost", + "MILVUS_PORT": "19530", + "MONGO_CONNECTION_STRING": "mongodb://localhost:27017", + "CELERY_WORKER_CONCURRENCY": "1", + "OLLAMA_HOST": "localhost", + "OLLAMA_PORT": "11434", + "OLLAMA_LLM_SMALL_MODEL": "small", + "OLLAMA_LLM_LARGE_MODEL": "large", + "DATA_DB": "postgresql", + } + with patch.dict(os.environ, env, clear=False): + for key in ( + "SEARCH_ENABLED", + "SEARCH_USE_DENSE", + "SEARCH_USE_BM25", + "SEARCH_FUSION", + "SEARCH_LITERAL_FILL", + "CONTEXT_PASSAGE_MODE", + ): + os.environ.pop(key, None) + from src.config import Config + + cfg = Config() + self.assertFalse(cfg.search_enabled) + self.assertTrue(cfg.search_use_dense) + self.assertTrue(cfg.search_use_bm25) + self.assertEqual(cfg.search_fusion, "rrf") + self.assertEqual(cfg.search_bm25_k1, 1.2) + self.assertEqual(cfg.search_bm25_b, 0.75) + self.assertEqual(cfg.context_passage_mode, "hybrid") + self.assertFalse(cfg.search_literal_fill) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_search_esci_4class.py b/tests/test_search_esci_4class.py new file mode 100644 index 0000000000000000000000000000000000000000..ffdbf1f8d04bdc3172d7a8e3d6308869056b871a --- /dev/null +++ b/tests/test_search_esci_4class.py @@ -0,0 +1,156 @@ +import unittest +from pathlib import Path + + +class SearchEsci4ClassTests(unittest.TestCase): + @classmethod + def setUpClass(cls): + import sys + + root = Path(__file__).resolve().parents[1] + bench = root / "benchmarks" + if str(bench) not in sys.path: + sys.path.insert(0, str(bench)) + + def test_weighted_scores_match_esci_gains(self): + from search.finetune_esci_4class import weighted_scores + + exact = weighted_scores([[1.0, 0.0, 0.0, 0.0]]) + sub = weighted_scores([[0.0, 1.0, 0.0, 0.0]]) + comp = weighted_scores([[0.0, 0.0, 1.0, 0.0]]) + irr = weighted_scores([[0.0, 0.0, 0.0, 1.0]]) + self.assertEqual(exact, [1.0]) + self.assertEqual(sub, [0.1]) + self.assertEqual(comp, [0.01]) + self.assertEqual(irr, [0.0]) + mixed = weighted_scores([[0.5, 0.5, 0.0, 0.0]]) + self.assertAlmostEqual(mixed[0], 0.55) + + def test_rank_doc_ids_orders_by_weighted_score(self): + from search.finetune_esci_4class import rank_doc_ids, weighted_scores + + doc_ids = ["irr", "exact", "sub"] + scores = weighted_scores( + [ + [0.05, 0.05, 0.1, 0.8], + [0.9, 0.05, 0.05, 0.0], + [0.1, 0.8, 0.05, 0.05], + ] + ) + self.assertEqual(rank_doc_ids(doc_ids, scores), ["exact", "sub", "irr"]) + + def test_class_weights_upweight_rare_labels(self): + from search.finetune_esci_4class import class_weights + + weights = class_weights([80, 10, 5, 5]) + self.assertGreater(weights[2], weights[0]) + self.assertGreater(weights[3], weights[0]) + + def test_held_out_query_ids_strip_esci_prefix(self): + import json + import tempfile + + from search.finetune_esci_4class import held_out_query_ids + + with tempfile.TemporaryDirectory() as tmp: + path = Path(tmp) / "slice.jsonl" + path.write_text( + json.dumps( + { + "type": "doc", + "doc_id": "p1", + "text": "DOCID p1.\nTitle: kettle", + } + ) + + "\n" + + json.dumps( + { + "type": "query", + "qid": "esci-99", + "query": "kettle", + "gold_doc_ids": ["p1"], + } + ) + + "\n", + encoding="utf-8", + ) + holdout = held_out_query_ids(path) + self.assertIn("esci-99", holdout) + self.assertIn("99", holdout) + + def test_run_4class_on_pool_keeps_irrelevant_and_ranks_exact_first(self): + from search.rank_pool_4class import run_4class_on_pool + + def predict(pairs): + scores = [] + for _, text in pairs: + if "gold kettle" in text: + scores.append([0.85, 0.1, 0.05, 0.0]) + elif "partial kettle" in text: + scores.append([0.1, 0.8, 0.05, 0.05]) + else: + scores.append([0.05, 0.05, 0.1, 0.8]) + return scores + + rows = [ + {"type": "doc", "doc_id": "irr", "text": "DOCID irr. Title: lamp"}, + { + "type": "doc", + "doc_id": "gold", + "text": "DOCID gold. Title: gold kettle", + }, + { + "type": "doc", + "doc_id": "sub", + "text": "DOCID sub. Title: partial kettle", + }, + { + "type": "query", + "qid": "esci-1", + "query": "kettle", + "gold_doc_ids": ["gold", "sub"], + "gold_grades": {"gold": 1.0, "sub": 0.1}, + "candidate_doc_ids": ["irr", "gold", "sub"], + "candidate_grades": {"irr": 0.0, "gold": 1.0, "sub": 0.1}, + "slice": "esci-us", + }, + ] + result = run_4class_on_pool( + rows, + dataset_name="toy.jsonl", + predict=predict, + model_name="toy-4class", + brain_id="searchbenchesci74", + ) + self.assertEqual(result["status"], "ok") + self.assertEqual(result["n_queries"], 1) + self.assertTrue(result["rank_pool"]) + ranked = result["queries"][0]["hit_ids"] + self.assertEqual(ranked, ["gold", "sub", "irr"]) + self.assertEqual(ranked[-1], "irr") + self.assertGreater(result["metrics"]["ndcg@20"], 0.9) + + def test_persist_tokenizer_max_length_writes_train_length(self): + import tempfile + from types import SimpleNamespace + + from search.finetune_esci_4class import persist_tokenizer_max_length + + class FakeTokenizer: + def __init__(self): + self.model_max_length = 10**18 + self.saved = None + + def save_pretrained(self, path): + self.saved = path + + tokenizer = FakeTokenizer() + model = SimpleNamespace(max_length=192, tokenizer=tokenizer) + with tempfile.TemporaryDirectory() as tmp: + persist_tokenizer_max_length(model, Path(tmp)) + self.assertEqual(tokenizer.model_max_length, 192) + self.assertEqual(tokenizer.saved, tmp) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_search_graph.py b/tests/test_search_graph.py new file mode 100644 index 0000000000000000000000000000000000000000..e1bf35d30355edfc016ca90edbb22d6ed39088d2 --- /dev/null +++ b/tests/test_search_graph.py @@ -0,0 +1,713 @@ +import os +import unittest +from unittest.mock import MagicMock, patch + +ENV_DEFAULTS = { + "BRAINPAT_TOKEN": "test-token", + "MODELS_MODE": "local", + "EMBEDDINGS_LOCAL_MODEL": "local-model", + "EMBEDDINGS_SMALL_MODEL": "small-model", + "EMBEDDING_NODES_DIMENSION": "3", + "EMBEDDING_TRIPLETS_DIMENSION": "3", + "EMBEDDING_OBSERVATIONS_DIMENSION": "3", + "EMBEDDING_DATA_DIMENSION": "3", + "EMBEDDING_RELATIONSHIPS_DIMENSION": "3", + "REDIS_HOST": "localhost", + "REDIS_PORT": "6379", + "NEO4J_HOST": "localhost", + "NEO4J_PORT": "7687", + "NEO4J_USERNAME": "neo4j", + "NEO4J_PASSWORD": "password", + "MILVUS_HOST": "localhost", + "MILVUS_PORT": "19530", + "MONGO_CONNECTION_STRING": "mongodb://localhost:27017", + "CELERY_WORKER_CONCURRENCY": "1", + "OLLAMA_HOST": "localhost", + "OLLAMA_PORT": "11434", + "OLLAMA_LLM_SMALL_MODEL": "small", + "OLLAMA_LLM_LARGE_MODEL": "large", +} +for key, value in ENV_DEFAULTS.items(): + os.environ.setdefault(key, value) + +from fastapi import HTTPException + +from src.constants.embeddings import Vector +from src.constants.kg import Node, SearchEntitiesResult +from src.core.search.graph_channels import ( + DEFAULT_COMMUNITY_LABELS, + collect_community_hits, + collect_entity_hits, + collect_event_hits, + expand_neighbor_hits, +) +from src.core.search.hybrid import fuse_passage_lists +from src.services.api.constants.requests import SearchHit, SearchRequestBody, SearchResponse + + +def _node( + uuid: str, + name: str, + labels: list[str], + happened_at: str | None = None, + description: str | None = None, + properties: dict | None = None, +) -> Node: + props = dict(properties or {}) + if happened_at: + props["happened_at"] = happened_at + return Node( + uuid=uuid, + name=name, + labels=labels, + happened_at=happened_at, + description=description, + properties=props, + ) + + +class FakeGraph: + def __init__(self, nodes: list[Node], neighbors: dict[str, list[Node]] | None = None): + self.nodes = {node.uuid: node for node in nodes} + self.neighbors = neighbors or {} + self.neighbor_calls = 0 + + def search_entities( + self, + brain_id: str = "default", + limit: int = 10, + skip: int = 0, + node_labels: list[str] | None = None, + query_text: str | None = None, + ) -> SearchEntitiesResult: + matched: list[Node] = [] + want = {str(item).upper() for item in (node_labels or [])} + needle = (query_text or "").lower() + for node in self.nodes.values(): + have = {str(item).upper() for item in node.labels} + if want and not (have & want): + continue + if needle and needle not in node.name.lower(): + continue + matched.append(node) + page = matched[skip : skip + limit] + return SearchEntitiesResult(results=page, total=len(matched)) + + def search_nodes_bm25( + self, + query_text: str, + brain_id: str = "default", + limit: int = 10, + node_labels: list[str] | None = None, + node_uuids: list[str] | None = None, + ): + needle = (query_text or "").lower() + want = {str(item).upper() for item in (node_labels or [])} + allowed = {str(item) for item in (node_uuids or [])} or None + scored: list[tuple[Node, float]] = [] + for node in self.nodes.values(): + if allowed is not None and node.uuid not in allowed: + continue + have = {str(item).upper() for item in node.labels} + if want and not (have & want): + continue + props = node.properties or {} + text = " ".join( + [ + node.name or "", + node.description or "", + str(props.get("search_text") or ""), + ] + ).lower() + score = 0.0 + if needle and needle in text: + score += 5.0 + for token in needle.split(): + if token and token in text: + score += 1.0 + if score: + scored.append((node, score)) + scored.sort(key=lambda item: (-item[1], item[0].uuid)) + return scored[:limit] + + def get_by_uuids(self, uuids: list[str], brain_id: str = "default") -> list[Node]: + return [self.nodes[item] for item in uuids if item in self.nodes] + + def get_by_uuid(self, uuid: str, brain_id: str = "default") -> Node: + return self.nodes[uuid] + + def get_neighbors(self, nodes, brain_id: str = "default", **kwargs): + self.neighbor_calls += 1 + keys = [] + for item in nodes: + keys.append(item if isinstance(item, str) else item.uuid) + out = {} + for key in keys: + out[key] = [(None, node) for node in self.neighbors.get(key, [])] + return out + + def get_event_centric_neighbors(self, nodes, brain_id: str = "default"): + return [] + + +class SearchGraphChannelTests(unittest.IsolatedAsyncioTestCase): + def test_contract_has_graph_fields_not_catalog_enums(self): + body = SearchRequestBody(query="sofa") + self.assertEqual(body.channels, ["passages"]) + self.assertEqual(body.expand, "none") + self.assertEqual(body.mode, "default") + self.assertIsNone(body.node_labels) + fields = set(SearchRequestBody.model_fields) + hit_fields = set(SearchHit.model_fields) + for banned in ( + "product_id", + "product_features", + "sku", + "category", + "brand", + "user_id", + ): + self.assertNotIn(banned, fields) + self.assertNotIn(banned, hit_fields) + self.assertIn("labels", hit_fields) + self.assertIn("extras", hit_fields) + self.assertIn("node_id", hit_fields) + self.assertIn("node_ids", set(SearchResponse.model_fields)) + self.assertIn("extras", fields) + self.assertEqual(list(DEFAULT_COMMUNITY_LABELS), ["TYPE", "CLASS", "TOPIC"]) + + async def test_unknown_channel_is_400(self): + from src.services.api.controllers import search as search_mod + + with patch.object(search_mod.config, "search_enabled", True): + with self.assertRaises(HTTPException) as ctx: + await search_mod.search( + SearchRequestBody(query="sofa", channels=["foo"]) + ) + self.assertEqual(ctx.exception.status_code, 400) + self.assertIn("Unknown search channel", str(ctx.exception.detail)) + + async def test_entities_empty_graph_is_200(self): + from src.services.api.controllers import search as search_mod + + mock_embeddings = MagicMock() + mock_embeddings.embed_text.return_value = Vector( + id="q", embeddings=[0.1], metadata={} + ) + mock_vs = MagicMock() + mock_vs.search_nodes.return_value = [] + graph = FakeGraph([]) + + with ( + patch.object(search_mod.config, "search_enabled", True), + patch.object(search_mod.config, "search_use_dense", False), + patch.object(search_mod.config, "search_use_bm25", True), + patch.object(search_mod, "embeddings_adapter", mock_embeddings), + patch.object(search_mod, "vector_search", mock_vs), + patch.object(search_mod, "graph_adapter", graph), + patch.object(search_mod, "data_adapter", MagicMock()), + ): + response = await search_mod.search( + SearchRequestBody(query="sofa", channels=["entities"], k=5) + ) + + self.assertEqual(response.hits, []) + + async def test_seeded_entity_name_in_top_k(self): + from src.services.api.controllers import search as search_mod + + graph = FakeGraph([_node("sku-1", "navy velvet sofa", ["ENTITY"])]) + mock_embeddings = MagicMock() + mock_embeddings.embed_text.return_value = Vector( + id="q", embeddings=[0.2], metadata={} + ) + mock_vs = MagicMock() + mock_vs.search_nodes.return_value = [] + + with ( + patch.object(search_mod.config, "search_enabled", True), + patch.object(search_mod.config, "search_use_dense", False), + patch.object(search_mod.config, "search_use_bm25", True), + patch.object(search_mod, "embeddings_adapter", mock_embeddings), + patch.object(search_mod, "vector_search", mock_vs), + patch.object(search_mod, "graph_adapter", graph), + patch.object(search_mod, "data_adapter", MagicMock()), + ): + response = await search_mod.search( + SearchRequestBody(query="velvet sofa", channels=["entities"], k=5) + ) + + self.assertEqual([hit.id for hit in response.hits], ["sku-1"]) + self.assertEqual(response.hits[0].channel, "entities") + self.assertIn("ENTITY", response.hits[0].labels) + self.assertEqual(response.hits[0].node_id, "sku-1") + self.assertEqual(response.node_ids, ["sku-1"]) + + async def test_node_labels_mismatch_is_empty_not_400(self): + from src.services.api.controllers import search as search_mod + + graph = FakeGraph([_node("sku-1", "navy velvet sofa", ["ENTITY"])]) + mock_embeddings = MagicMock() + mock_embeddings.embed_text.return_value = Vector( + id="q", embeddings=[0.2], metadata={} + ) + mock_vs = MagicMock() + mock_vs.search_nodes.return_value = [] + + with ( + patch.object(search_mod.config, "search_enabled", True), + patch.object(search_mod, "embeddings_adapter", mock_embeddings), + patch.object(search_mod, "vector_search", mock_vs), + patch.object(search_mod, "graph_adapter", graph), + patch.object(search_mod, "data_adapter", MagicMock()), + ): + response = await search_mod.search( + SearchRequestBody( + query="velvet sofa", + channels=["entities"], + node_labels=["EVENT"], + k=5, + ) + ) + + self.assertEqual(response.hits, []) + + async def test_default_passages_does_not_touch_graph(self): + from src.services.api.controllers import search as search_mod + from src.constants.data import TextChunk + + mock_data = MagicMock() + mock_data.search_bm25.return_value = [ + (TextChunk(id="lex", text="license board"), 3.1) + ] + mock_graph = MagicMock() + mock_embeddings = MagicMock() + mock_vs = MagicMock() + + with ( + patch.object(search_mod.config, "search_enabled", True), + patch.object(search_mod.config, "search_use_dense", False), + patch.object(search_mod.config, "search_use_bm25", True), + patch.object(search_mod, "embeddings_adapter", mock_embeddings), + patch.object(search_mod, "vector_search", mock_vs), + patch.object(search_mod, "graph_adapter", mock_graph), + patch.object(search_mod, "data_adapter", mock_data), + ): + response = await search_mod.search(SearchRequestBody(query="license", k=5)) + + self.assertEqual([hit.id for hit in response.hits], ["lex"]) + self.assertEqual(response.hits[0].channel, "passages") + mock_graph.search_entities.assert_not_called() + mock_embeddings.embed_text.assert_not_called() + + async def test_events_empty_ok(self): + from src.services.api.controllers import search as search_mod + + graph = FakeGraph([_node("sku-1", "navy velvet sofa", ["ENTITY"])]) + mock_embeddings = MagicMock() + mock_embeddings.embed_text.return_value = Vector( + id="q", embeddings=[0.2], metadata={} + ) + mock_vs = MagicMock() + mock_vs.search_nodes.return_value = [] + + with ( + patch.object(search_mod.config, "search_enabled", True), + patch.object(search_mod, "embeddings_adapter", mock_embeddings), + patch.object(search_mod, "vector_search", mock_vs), + patch.object(search_mod, "graph_adapter", graph), + patch.object(search_mod, "data_adapter", MagicMock()), + ): + response = await search_mod.search( + SearchRequestBody(query="view", channels=["events"], k=5) + ) + + self.assertEqual(response.hits, []) + + async def test_events_with_happened_at(self): + from src.services.api.controllers import search as search_mod + + graph = FakeGraph( + [ + _node( + "evt-1", + "View", + ["EVENT"], + happened_at="2024-01-05T12:00:00Z", + ) + ] + ) + mock_embeddings = MagicMock() + mock_embeddings.embed_text.return_value = Vector( + id="q", embeddings=[0.2], metadata={} + ) + mock_vs = MagicMock() + mock_vs.search_nodes.return_value = [] + + with ( + patch.object(search_mod.config, "search_enabled", True), + patch.object(search_mod, "embeddings_adapter", mock_embeddings), + patch.object(search_mod, "vector_search", mock_vs), + patch.object(search_mod, "graph_adapter", graph), + patch.object(search_mod, "data_adapter", MagicMock()), + ): + response = await search_mod.search( + SearchRequestBody(query="View", channels=["events"], k=5) + ) + + self.assertEqual([hit.id for hit in response.hits], ["evt-1"]) + self.assertEqual(response.hits[0].channel, "events") + self.assertEqual(response.hits[0].extras["happened_at"], "2024-01-05T12:00:00Z") + + def test_community_hub_returns_members_with_fanout_cap(self): + hub = _node("class-sofas", "sofas", ["CLASS"]) + members = [ + _node("e1", "velvet sofa", ["ENTITY"]), + _node("e2", "leather sofa", ["ENTITY"]), + _node("e3", "loveseat", ["ENTITY"]), + ] + unrelated = _node("e4", "lamp", ["ENTITY"]) + graph = FakeGraph( + [hub, *members, unrelated], + neighbors={"class-sofas": members}, + ) + hits = collect_community_hits( + query="sofas", + brain_id="searchbenchsmoke", + k=10, + graph=graph, + fanout=2, + ) + ids = [hit.id for hit in hits] + self.assertTrue(set(ids).issubset({"e1", "e2", "e3"})) + self.assertNotIn("e4", ids) + self.assertNotIn("class-sofas", ids) + self.assertEqual(len(ids), 2) + self.assertTrue(all(hit.channel == "communities" for hit in hits)) + + def test_expand_neighbors_default_off(self): + seed = _node("sku-1", "velvet sofa", ["ENTITY"]) + neighbor = _node("class-sofas", "sofas", ["CLASS"]) + graph = FakeGraph([seed, neighbor], neighbors={"sku-1": [neighbor]}) + seeds = collect_entity_hits( + query="velvet sofa", + brain_id="searchbenchsmoke", + k=5, + graph=graph, + ) + self.assertEqual(graph.neighbor_calls, 0) + expanded = expand_neighbor_hits( + seeds, + brain_id="searchbenchsmoke", + k=5, + graph=graph, + fanout=10, + ) + self.assertEqual([hit.id for hit in expanded], ["class-sofas"]) + self.assertEqual(expanded[0].channel, "neighbors") + + async def test_expand_omitted_skips_hops(self): + from src.services.api.controllers import search as search_mod + + seed = _node("sku-1", "velvet sofa", ["ENTITY"]) + neighbor = _node("n1", "pillow", ["ENTITY"]) + graph = FakeGraph([seed, neighbor], neighbors={"sku-1": [neighbor]}) + mock_embeddings = MagicMock() + mock_embeddings.embed_text.return_value = Vector( + id="q", embeddings=[0.2], metadata={} + ) + mock_vs = MagicMock() + mock_vs.search_nodes.return_value = [] + + with ( + patch.object(search_mod.config, "search_enabled", True), + patch.object(search_mod, "embeddings_adapter", mock_embeddings), + patch.object(search_mod, "vector_search", mock_vs), + patch.object(search_mod, "graph_adapter", graph), + patch.object(search_mod, "data_adapter", MagicMock()), + ): + response = await search_mod.search( + SearchRequestBody(query="velvet sofa", channels=["entities"], k=5) + ) + + self.assertEqual([hit.id for hit in response.hits], ["sku-1"]) + self.assertEqual(graph.neighbor_calls, 0) + + async def test_expand_neighbors_adds_hop(self): + from src.services.api.controllers import search as search_mod + + seed = _node("sku-1", "velvet sofa", ["ENTITY"]) + neighbor = _node("n1", "pillow", ["ENTITY"]) + graph = FakeGraph([seed, neighbor], neighbors={"sku-1": [neighbor]}) + mock_embeddings = MagicMock() + mock_embeddings.embed_text.return_value = Vector( + id="q", embeddings=[0.2], metadata={} + ) + mock_vs = MagicMock() + mock_vs.search_nodes.return_value = [] + + with ( + patch.object(search_mod.config, "search_enabled", True), + patch.object(search_mod, "embeddings_adapter", mock_embeddings), + patch.object(search_mod, "vector_search", mock_vs), + patch.object(search_mod, "graph_adapter", graph), + patch.object(search_mod, "data_adapter", MagicMock()), + ): + response = await search_mod.search( + SearchRequestBody( + query="velvet sofa", + channels=["entities"], + expand="neighbors", + k=5, + ) + ) + + ids = [hit.id for hit in response.hits] + self.assertIn("sku-1", ids) + self.assertIn("n1", ids) + self.assertTrue(any(hit.channel == "neighbors" for hit in response.hits)) + + def test_fuse_mixed_chunk_and_node_ids(self): + fused = fuse_passage_lists( + ["chunk-1"], + [], + extra_id_lists=[["sku-1"]], + ) + ids = [item for item, _ in fused] + self.assertEqual(set(ids), {"chunk-1", "sku-1"}) + + async def test_fused_graph_lists_drop_hub_ids(self): + from src.constants.data import TextChunk + from src.services.api.controllers import search as search_mod + + hub = _node("hub:attr:red", "red", ["ATTR"]) + product = _node("sku-1", "red velvet sofa", ["ENTITY"]) + graph = FakeGraph([hub, product], neighbors={"hub:attr:red": [product]}) + mock_data = MagicMock() + mock_data.search_bm25.return_value = [ + (TextChunk(id="chunk-1", text="red velvet sofa"), 2.0) + ] + mock_embeddings = MagicMock() + mock_embeddings.embed_text.return_value = Vector( + id="q", embeddings=[0.2], metadata={} + ) + mock_vs = MagicMock() + mock_vs.search_nodes.return_value = [] + + with ( + patch.object(search_mod.config, "search_enabled", True), + patch.object(search_mod.config, "search_use_dense", False), + patch.object(search_mod.config, "search_use_bm25", True), + patch.object(search_mod, "embeddings_adapter", mock_embeddings), + patch.object(search_mod, "vector_search", mock_vs), + patch.object(search_mod, "graph_adapter", graph), + patch.object(search_mod, "data_adapter", mock_data), + ): + response = await search_mod.search( + SearchRequestBody( + query="red sofa", + channels=["passages", "entities", "communities"], + k=10, + ) + ) + + ids = [hit.id for hit in response.hits] + self.assertNotIn("hub:attr:red", ids) + self.assertIn("sku-1", ids) + self.assertIn("chunk-1", ids) + self.assertEqual(response.channel_lists["bm25"], ["chunk-1"]) + self.assertIn("sku-1", response.channel_lists["entities"]) + self.assertNotIn("hub:attr:red", response.channel_lists["entities"]) + self.assertNotIn("hub:attr:red", response.channel_lists["communities"]) + + def test_entities_drop_attr_hubs(self): + hub = _node("hub:attr:red", "red", ["ATTR"]) + product = _node("sku-1", "red velvet sofa", ["ENTITY"]) + graph = FakeGraph([hub, product]) + hits = collect_entity_hits( + query="red sofa", + brain_id="searchbenchsmoke", + k=5, + graph=graph, + ) + ids = [hit.id for hit in hits] + self.assertEqual(ids, ["sku-1"]) + self.assertTrue(all(hit.channel == "entities" for hit in hits)) + + def test_community_skips_attr_member_hubs(self): + hub = _node("class-sofas", "sofas", ["CLASS"]) + product = _node("e1", "velvet sofa", ["ENTITY"]) + attr = _node("hub:attr:red", "red", ["ATTR"]) + graph = FakeGraph( + [hub, product, attr], + neighbors={"class-sofas": [product, attr]}, + ) + hits = collect_community_hits( + query="sofas", + brain_id="searchbenchsmoke", + k=10, + graph=graph, + fanout=5, + ) + ids = [hit.id for hit in hits] + self.assertEqual(ids, ["e1"]) + self.assertNotIn("hub:attr:red", ids) + + def test_community_attr_hub_returns_products(self): + hub = _node("hub:attr:red", "red", ["ATTR"]) + product = _node("e1", "red sofa", ["ENTITY"]) + graph = FakeGraph([hub, product], neighbors={"hub:attr:red": [product]}) + hits = collect_community_hits( + query="red sofa", + brain_id="searchbenchsmoke", + k=10, + graph=graph, + fanout=5, + ) + self.assertEqual([hit.id for hit in hits], ["e1"]) + self.assertEqual(hits[0].channel, "communities") + + def test_search_controller_does_not_import_retrieve_assembler(self): + from pathlib import Path + + root = Path(__file__).resolve().parents[1] + search_text = ( + root / "src" / "services" / "api" / "controllers" / "search.py" + ).read_text(encoding="utf-8") + retrieve_text = ( + root / "src" / "services" / "api" / "controllers" / "retrieve.py" + ).read_text(encoding="utf-8") + self.assertNotIn("controllers.retrieve", search_text) + self.assertNotIn("kg_topic_sessions", search_text) + self.assertNotIn("graph_channels", retrieve_text) + schema = ( + root / "src" / "services" / "api" / "constants" / "requests.py" + ).read_text(encoding="utf-8") + self.assertNotIn("product_id", schema.split("class SearchRequestBody")[1].split("class SearchHitScores")[0]) + + +class SearchGraphHelperTests(unittest.TestCase): + def test_entity_ann_and_name_match(self): + named = _node("sku-1", "navy velvet sofa", ["ENTITY"]) + vector_only = _node("sku-2", "other item", ["ENTITY"]) + graph = FakeGraph([named, vector_only]) + vs = MagicMock() + vs.search_nodes.return_value = [ + Vector(id="v2", metadata={"uuid": "sku-2", "name": "other item"}, distance=0.1) + ] + hits = collect_entity_hits( + query="velvet sofa", + brain_id="searchbenchsmoke", + k=5, + graph=graph, + vector_search=vs, + query_vector=[0.1, 0.2], + ) + ids = [hit.id for hit in hits] + self.assertIn("sku-1", ids) + self.assertIn("sku-2", ids) + + def test_events_rank_recent_higher(self): + older = _node("evt-old", "View", ["EVENT"], happened_at="2020-01-01T00:00:00Z") + newer = _node("evt-new", "View", ["EVENT"], happened_at="2026-08-01T00:00:00Z") + graph = FakeGraph([older, newer]) + hits = collect_event_hits( + query="View", + brain_id="searchbenchsmoke", + k=5, + graph=graph, + ) + self.assertEqual(hits[0].id, "evt-new") + self.assertGreater(hits[0].score, hits[1].score) + + def test_entity_bm25_matches_description_only(self): + named = _node("sku-1", "catalog item", ["ENTITY"]) + described = _node( + "sku-2", + "item two", + ["ENTITY"], + description="navy velvet ottoman for small rooms", + properties={"search_text": "item two navy velvet ottoman for small rooms"}, + ) + graph = FakeGraph([named, described]) + hits = collect_entity_hits( + query="ottoman", + brain_id="searchbenchsmoke", + k=5, + graph=graph, + ) + ids = [hit.id for hit in hits] + self.assertIn("sku-2", ids) + self.assertNotIn("sku-1", ids) + + def test_community_intersects_class_and_attr_hubs(self): + class_hub = _node("hub:class:sofas", "sofas", ["CLASS"]) + attr_hub = _node("hub:attr:modern", "modern", ["ATTR"]) + both = _node("e1", "velvet sofa", ["ENTITY"]) + class_only = _node("e2", "leather sofa", ["ENTITY"]) + graph = FakeGraph( + [class_hub, attr_hub, both, class_only], + neighbors={ + "hub:class:sofas": [both, class_only], + "hub:attr:modern": [both], + }, + ) + hits = collect_community_hits( + query="modern sofas", + brain_id="searchbenchsmoke", + k=10, + graph=graph, + fanout=5, + ) + ids = [hit.id for hit in hits] + self.assertEqual(ids, ["e1"]) + self.assertNotIn("e2", ids) + self.assertNotIn("hub:class:sofas", ids) + + def test_community_union_when_single_hub_kind(self): + class_hub = _node("hub:class:sofas", "sofas", ["CLASS"]) + a = _node("e1", "velvet sofa", ["ENTITY"]) + b = _node("e2", "leather sofa", ["ENTITY"]) + graph = FakeGraph( + [class_hub, a, b], + neighbors={"hub:class:sofas": [a, b]}, + ) + hits = collect_community_hits( + query="sofas", + brain_id="searchbenchsmoke", + k=10, + graph=graph, + fanout=5, + ) + self.assertEqual(set(hit.id for hit in hits), {"e1", "e2"}) + + def test_community_hybrid_ranks_by_node_bm25_not_degree(self): + class_hub = _node("hub:class:sofas", "sofas", ["CLASS"]) + attr_hub = _node("hub:attr:modern", "modern", ["ATTR"]) + weak = _node("e1", "sofa", ["ENTITY"], description="basic") + strong = _node( + "e2", + "plush seating", + ["ENTITY"], + description="modern sofas extra plush", + properties={"search_text": "plush seating modern sofas extra plush"}, + ) + graph = FakeGraph( + [class_hub, attr_hub, weak, strong], + neighbors={ + "hub:class:sofas": [weak, strong], + "hub:attr:modern": [weak, strong], + }, + ) + hits = collect_community_hits( + query="modern sofas", + brain_id="searchbenchsmoke", + k=10, + graph=graph, + fanout=5, + ) + self.assertEqual([hit.id for hit in hits], ["e2", "e1"]) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_search_jdsearch.py b/tests/test_search_jdsearch.py new file mode 100644 index 0000000000000000000000000000000000000000..e2a415a94e80c4f25bbd1b36d8f2039eda7c0422 --- /dev/null +++ b/tests/test_search_jdsearch.py @@ -0,0 +1,312 @@ +import tempfile +import unittest +from pathlib import Path +from unittest.mock import MagicMock + +FIXTURE_DIR = ( + Path(__file__).resolve().parents[1] / "tests" / "fixtures" / "jdsearch" +) + + +class SearchJdsearchTests(unittest.TestCase): + @classmethod + def setUpClass(cls): + import sys + + root = Path(__file__).resolve().parents[1] + bench = root / "benchmarks" + if str(root) not in sys.path: + sys.path.insert(0, str(root)) + if str(bench) not in sys.path: + sys.path.insert(0, str(bench)) + + def _fixture_paths(self) -> tuple[Path, Path]: + product = FIXTURE_DIR / "product_meta_data.txt" + behavior = FIXTURE_DIR / "user_behavior_data.txt" + self.assertTrue(product.exists(), product) + self.assertTrue(behavior.exists(), behavior) + return product, behavior + + def test_term_and_list_separators(self): + from search.jdsearch import TERM_SEP, join_terms, split_list + + self.assertEqual(TERM_SEP, "\030") + self.assertEqual(join_terms(f"111{TERM_SEP}222"), "111 222") + self.assertEqual(split_list("100_200_999"), ["100", "200", "999"]) + self.assertEqual(split_list(""), []) + + def test_stats_histogram_and_truncated_count(self): + from search.jdsearch import PAPER_N_PRODUCTS, collect_jdsearch_stats + + product, behavior = self._fixture_paths() + stats = collect_jdsearch_stats( + product_path=product, + behavior_path=behavior, + extract=False, + ) + self.assertEqual(stats["n_behavior_rows"], 2) + self.assertEqual(stats["n_products_seen"], 2) + self.assertFalse(stats["truncated"]) + self.assertIn("1", stats["label_histogram"]) + self.assertEqual(stats["label_scheme"], "graded") + self.assertGreater(stats["history_type_counts"].get("CLICK", 0), 0) + self.assertGreater(stats["history_type_counts"].get("FLW", 0), 0) + self.assertIn("999", {"999"}) + self.assertGreater(stats["n_wids_missing_meta"], 0) + + truncated = collect_jdsearch_stats( + product_path=product, + behavior_path=behavior, + extract=False, + max_behavior_rows=1, + max_product_rows=1, + ) + self.assertTrue(truncated["truncated"]) + self.assertEqual(truncated["n_behavior_rows"], 1) + self.assertEqual(truncated["n_products_seen"], 1) + self.assertNotEqual(truncated["n_products_seen"], PAPER_N_PRODUCTS) + self.assertLess(truncated["n_products_seen"], 12_000_000) + + def test_prepare_rows_caps_target_and_gold(self): + from search.catalog import FROZEN_JSONL_IF_EXISTS + from search.dataset import dataset_stats, load_records, split_corpus + from search.jdsearch import ( + jdsearch_interactions_path, + prepare_jdsearch_bundle, + prepare_jdsearch_rows, + ) + + product, behavior = self._fixture_paths() + wands = ( + Path(__file__).resolve().parents[1] + / "benchmarks" + / "data" + / "search_wands.jsonl" + ) + wands_mtime = wands.stat().st_mtime if wands.exists() else None + rows, interactions, stats = prepare_jdsearch_rows( + max_queries=80, + max_docs=2000, + product_path=product, + behavior_path=behavior, + extract=False, + ) + docs, queries = split_corpus(rows) + self.assertLessEqual(len(queries), 80) + self.assertLessEqual(len(docs), 2000) + self.assertEqual(stats["label_scheme"], "graded") + self.assertTrue(queries) + for query in queries: + self.assertTrue(str(query.get("target") or "").startswith("jd-u")) + self.assertTrue(query.get("gold_doc_ids")) + self.assertTrue(query.get("gold_grades")) + self.assertTrue(query.get("candidate_doc_ids")) + self.assertTrue(any(row.get("behavior") == "follow" for row in interactions)) + self.assertTrue(any(row.get("behavior") == "click" for row in interactions)) + qids = {query["qid"] for query in queries} + self.assertTrue(qids) + for query in queries: + self.assertNotIn("999", query["gold_doc_ids"]) + self.assertIn("search_jdsearch.jsonl", FROZEN_JSONL_IF_EXISTS) + with tempfile.TemporaryDirectory() as tmp: + dest = Path(tmp) / "search_jdsearch.jsonl" + written = prepare_jdsearch_bundle( + dest, + max_queries=80, + max_docs=2000, + product_path=product, + behavior_path=behavior, + extract=False, + ) + self.assertEqual(written, dest) + loaded = load_records(dest) + stats = dataset_stats(loaded) + self.assertLessEqual(stats["n_queries"], 80) + self.assertLessEqual(stats["n_docs"], 2000) + _, loaded_queries = split_corpus(loaded) + self.assertTrue(all(q.get("target") and q.get("gold_doc_ids") for q in loaded_queries)) + inter_path = jdsearch_interactions_path(dest) + self.assertTrue(inter_path.exists()) + self.assertFalse((Path(tmp) / "search_wands.jsonl").exists()) + self.assertFalse((Path(tmp) / "search_esci_74.jsonl").exists()) + if wands_mtime is not None: + self.assertEqual(wands.stat().st_mtime, wands_mtime) + + def test_prepare_drops_all_missing_gold(self): + from search.jdsearch import prepare_jdsearch_rows + + product, behavior = self._fixture_paths() + with tempfile.TemporaryDirectory() as tmp: + extra_behavior = Path(tmp) / "user_behavior_data.txt" + extra_behavior.write_text( + behavior.read_text(encoding="utf-8") + + "onlymissing\t999\t1.0\t-1\t\t\t0\n", + encoding="utf-8", + ) + rows, _interactions, _stats = prepare_jdsearch_rows( + product_path=product, + behavior_path=extra_behavior, + extract=False, + ) + queries = [row for row in rows if row.get("type") == "query"] + self.assertTrue(all("999" not in (q.get("gold_doc_ids") or []) for q in queries)) + self.assertTrue(all(q.get("gold_doc_ids") for q in queries)) + + def test_follow_event_not_has(self): + import importlib.util + + from search.mapping import HAS_EVENT_NAME, interaction_to_triples + + triples = interaction_to_triples( + [ + { + "user_id": "jd-u0", + "item_id": "100", + "behavior": "follow", + "timestamp": "2022-10-16T00:00:00Z", + } + ] + ) + events = [ + row["event"]["name"] + for row in triples + if isinstance(row.get("event"), dict) and row["event"].get("name") + ] + self.assertIn("Follow", events) + self.assertNotIn(HAS_EVENT_NAME, events) + mapping_path = ( + Path(__file__).resolve().parents[1] + / "plugins" + / "features-rec" + / "models" + / "mapping.py" + ) + if not mapping_path.is_file(): + self.skipTest("optional features-rec plugin is not installed") + spec = importlib.util.spec_from_file_location( + "features_rec_mapping_jdsearch", mapping_path + ) + self.assertIsNotNone(spec) + self.assertIsNotNone(spec.loader) + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) + self.assertEqual(mod.normalize_behavior("FLW"), ("Follow", "TARGETED")) + self.assertEqual(mod.behavior_weight("FLW"), 0.5) + self.assertEqual(mod.behavior_weight("follow"), 0.5) + + def test_evaluate_sends_target_only_when_personalized(self): + from search.client import TimedResult + from search.evaluate import evaluate_search + from src.services.api.constants.requests import SearchRequestBody + + fields = set(SearchRequestBody.model_fields) + self.assertIn("target", fields) + self.assertNotIn("user_id", fields) + + class _Client: + class _Settings: + brain_id = "searchbenchjdslice" + + settings = _Settings() + + def __init__(self): + self.calls = [] + + def search(self, query, **kwargs): + self.calls.append({"query": query, **kwargs}) + return TimedResult(data={"hits": []}, latency_ms=1.0) + + def list_text_chunks(self, **kwargs): + skip = int(kwargs.get("skip") or 0) + if skip: + return TimedResult(data={"data": [], "total": 1}, latency_ms=1.0) + return TimedResult( + data={ + "data": [{"id": "c1", "text": "DOCID 100. 111 222"}], + "total": 1, + }, + latency_ms=1.0, + ) + + rows = [ + { + "type": "doc", + "doc_id": "100", + "text": "DOCID 100. 111 222", + }, + { + "type": "query", + "qid": "jdsearch-0", + "query": "111 222", + "target": "jd-u0", + "gold_doc_ids": ["100"], + "gold_grades": {"100": 1.0}, + "candidate_doc_ids": ["100"], + }, + ] + client = _Client() + evaluate_search(client, rows, skip_ingest=True, personalize=True) + self.assertEqual(client.calls[0].get("target"), "jd-u0") + client.calls.clear() + evaluate_search(client, rows, skip_ingest=True, personalize=False) + self.assertFalse(client.calls[0].get("target")) + client.calls.clear() + evaluate_search( + client, + [{**rows[1], "target": ""}, rows[0]] + if False + else [ + rows[0], + {**rows[1], "target": ""}, + ], + skip_ingest=True, + personalize=True, + ) + self.assertFalse(client.calls[0].get("target")) + + def test_client_search_body_omits_target_by_default(self): + from search.client import BrainAPIClient, TimedResult + from search.config import Settings + + captured = {} + + class _Stub(BrainAPIClient): + def __post_init__(self): + self._client = MagicMock() + + def _request(self, method, path, *, json=None, params=None): + captured["json"] = json + return TimedResult(data={"hits": []}, latency_ms=1.0) + + settings = MagicMock(spec=Settings) + settings.brain_id = "searchbenchjdslice" + settings.brainapi_url = "http://127.0.0.1" + settings.brainpat_token = "token" + settings.require_brainapi = lambda: None + client = _Stub(settings) + client.search("111 222") + self.assertNotIn("target", captured["json"]) + client.search("111 222", target="jd-u0") + self.assertEqual(captured["json"]["target"], "jd-u0") + self.assertNotIn("user_id", captured["json"]) + + def test_bm25_tokens_present_in_doc_text(self): + from search.jdsearch import prepare_jdsearch_rows + + product, behavior = self._fixture_paths() + rows, _interactions, _stats = prepare_jdsearch_rows( + product_path=product, + behavior_path=behavior, + extract=False, + ) + docs = [row for row in rows if row.get("type") == "doc"] + queries = [row for row in rows if row.get("type") == "query"] + self.assertTrue(docs) + blob = " ".join(str(doc.get("text") or "") for doc in docs) + self.assertIn("DOCID 100.", blob) + nonempty = 0 + for query in queries: + tokens = str(query.get("query") or "").split() + if any(token and token in blob for token in tokens): + nonempty += 1 + self.assertGreater(nonempty, 0) diff --git a/tests/test_search_personalize.py b/tests/test_search_personalize.py new file mode 100644 index 0000000000000000000000000000000000000000..e0ebc7bfaec3f4d9d960961d0963c21d915e87dc --- /dev/null +++ b/tests/test_search_personalize.py @@ -0,0 +1,324 @@ +import os +import unittest +from unittest.mock import MagicMock, patch + +ENV_DEFAULTS = { + "BRAINPAT_TOKEN": "test-token", + "MODELS_MODE": "local", + "EMBEDDINGS_LOCAL_MODEL": "local-model", + "EMBEDDINGS_SMALL_MODEL": "small-model", + "EMBEDDING_NODES_DIMENSION": "3", + "EMBEDDING_TRIPLETS_DIMENSION": "3", + "EMBEDDING_OBSERVATIONS_DIMENSION": "3", + "EMBEDDING_DATA_DIMENSION": "3", + "EMBEDDING_RELATIONSHIPS_DIMENSION": "3", + "REDIS_HOST": "localhost", + "REDIS_PORT": "6379", + "NEO4J_HOST": "localhost", + "NEO4J_PORT": "7687", + "NEO4J_USERNAME": "neo4j", + "NEO4J_PASSWORD": "password", + "MILVUS_HOST": "localhost", + "MILVUS_PORT": "19530", + "MONGO_CONNECTION_STRING": "mongodb://localhost:27017", + "CELERY_WORKER_CONCURRENCY": "1", + "OLLAMA_HOST": "localhost", + "OLLAMA_PORT": "11434", + "OLLAMA_LLM_SMALL_MODEL": "small", + "OLLAMA_LLM_LARGE_MODEL": "large", +} +for key, value in ENV_DEFAULTS.items(): + os.environ.setdefault(key, value) + +from src.constants.data import TextChunk +from src.constants.kg import Node, Predicate +from src.core.search.personalize import ( + blend_ranked, + personalize_ranked_ids, + query_personalize_lambda, + score_nodes_for_user, + user_pref_weights, +) +from src.services.api.constants.requests import SearchRequestBody + + +def _node(uuid: str, name: str, labels: list[str], happened_at: str | None = None) -> Node: + return Node( + uuid=uuid, + name=name, + labels=labels, + happened_at=happened_at, + properties={}, + ) + + +def _pred(name: str, amount: float | None = None, weight: float | None = None) -> Predicate: + props = {} + if weight is not None: + props["weight"] = weight + return Predicate( + uuid=f"rel:{name}:{id(name)}", + name=name, + description="", + amount=amount, + properties=props, + ) + + +class FakeGraph: + def __init__(self): + self.nodes: dict[str, Node] = {} + self.edges: dict[str, list[tuple[Predicate, Node]]] = {} + + def add(self, node: Node) -> Node: + self.nodes[node.uuid] = node + self.edges.setdefault(node.uuid, []) + return node + + def link(self, tail: str, pred: Predicate, tip: str) -> None: + self.edges.setdefault(tail, []).append((pred, self.nodes[tip])) + + def get_by_uuids(self, uuids, brain_id="default"): + return [self.nodes[uid] for uid in uuids if uid in self.nodes] + + def get_by_uuid(self, uuid, brain_id="default"): + return self.nodes.get(uuid) + + def get_neighbors(self, nodes, brain_id="default", of_types=None, **kwargs): + out = {} + for item in nodes: + uid = item if isinstance(item, str) else item.uuid + neigh = list(self.edges.get(uid, [])) + if of_types: + wanted = {label.upper() for label in of_types} + neigh = [ + (pred, node) + for pred, node in neigh + if wanted & {label.upper() for label in (node.labels or [])} + ] + out[uid] = neigh + return out + + +def _style_graph() -> tuple[FakeGraph, Node]: + graph = FakeGraph() + user = graph.add(_node("user:u01", "u01", ["USER"])) + recent = graph.add( + _node("evt-recent", "View", ["EVENT"], happened_at="08/20/2026") + ) + old = graph.add(_node("evt-old", "View", ["EVENT"], happened_at="08/21/2025")) + seventies = graph.add(_node("70s-item", "lamp 70s", ["ENTITY"])) + modern = graph.add(_node("modern-item", "lamp modern", ["ENTITY"])) + attr_70s = graph.add(_node("hub:attr:70s", "70s", ["ATTR", "STYLE"])) + attr_modern = graph.add(_node("hub:attr:modern", "modern", ["ATTR", "STYLE"])) + graph.link("user:u01", _pred("MADE"), "evt-recent") + graph.link("evt-recent", _pred("TARGETED"), "70s-item") + graph.link("70s-item", _pred("HAS"), "hub:attr:70s") + graph.link("user:u01", _pred("MADE"), "evt-old") + graph.link("evt-old", _pred("TARGETED"), "modern-item") + graph.link("modern-item", _pred("HAS"), "hub:attr:modern") + graph.link( + "user:u01", + _pred("PREFERS", amount=0.4, weight=0.4), + "hub:attr:70s", + ) + return graph, user + + +class QueryLambdaTests(unittest.TestCase): + def test_generic_one_token(self): + self.assertAlmostEqual(query_personalize_lambda("lamp"), 0.85) + + def test_two_and_three_tokens(self): + self.assertAlmostEqual(query_personalize_lambda("oak table"), 0.5) + self.assertAlmostEqual(query_personalize_lambda("oak dining table"), 0.25) + + def test_four_plus_tokens(self): + self.assertAlmostEqual(query_personalize_lambda("oak dining table set"), 0.1) + + def test_digits_are_specific(self): + self.assertEqual(query_personalize_lambda("oak dining table 180cm"), 0.0) + self.assertEqual(query_personalize_lambda("sku 7468"), 0.0) + + +class BlendTests(unittest.TestCase): + def test_zero_lambda_preserves_order(self): + ids = ["a", "b", "c"] + ordered, _ = blend_ranked(ids, {"a": 3, "b": 2, "c": 1}, {"c": 9}, 0.0) + self.assertEqual(ordered, ids) + + def test_all_zero_prefs_preserve_order(self): + ids = ["a", "b", "c"] + ordered, _ = blend_ranked(ids, {"a": 3, "b": 2, "c": 1}, {}, 0.9) + self.assertEqual(ordered, ids) + + def test_high_lambda_promotes_overlap_without_dropping(self): + ids = ["a", "b", "c"] + ordered, _ = blend_ranked( + ids, {"a": 3, "b": 2, "c": 1}, {"a": 0, "b": 0, "c": 10}, 1.0 + ) + self.assertEqual(ordered[0], "c") + self.assertEqual(set(ordered), set(ids)) + + +class PrefVectorTests(unittest.TestCase): + def test_recent_70s_outranks_old_modern_and_combines_prefers(self): + graph, user = _style_graph() + with patch("src.core.search.personalize.graph_adapter", graph): + weights = user_pref_weights(user, "brain") + self.assertGreater(weights["hub:attr:70s"], weights["hub:attr:modern"]) + self.assertGreater(weights["hub:attr:70s"], 0.4) + + def test_missing_user_is_empty(self): + graph = FakeGraph() + with patch("src.core.search.personalize.graph_adapter", graph): + self.assertEqual(user_pref_weights(None, "brain"), {}) + self.assertEqual( + user_pref_weights(_node("missing", "x", ["USER"]), "brain"), + {}, + ) + + def test_score_nodes_overlap_only(self): + graph, user = _style_graph() + with patch("src.core.search.personalize.graph_adapter", graph): + prefs = user_pref_weights(user, "brain") + scores = score_nodes_for_user( + ["70s-item", "modern-item", "ghost"], prefs, "brain" + ) + self.assertGreater(scores["70s-item"], scores["modern-item"]) + self.assertEqual(scores["ghost"], 0.0) + + +class SearchPersonalizeTests(unittest.IsolatedAsyncioTestCase): + async def _search(self, body: SearchRequestBody, chunks: list): + from src.services.api.controllers import search as search_mod + + mock_data = MagicMock() + mock_data.search_bm25.return_value = chunks + mock_data.get_text_chunks_by_ids.return_value = ( + [item[0] for item in chunks], + None, + ) + with ( + patch.object(search_mod.config, "search_enabled", True), + patch.object(search_mod.config, "search_use_dense", False), + patch.object(search_mod.config, "search_use_bm25", True), + patch.object(search_mod.config, "search_literal_fill", False), + patch.object(search_mod, "embeddings_adapter", MagicMock()), + patch.object(search_mod, "vector_search", MagicMock()), + patch.object(search_mod, "data_adapter", mock_data), + ): + return await search_mod.search(body) + + def test_schema_allows_target_not_user_id(self): + body = SearchRequestBody(query="lamp", target="u01") + self.assertEqual(body.target, "u01") + self.assertEqual(body.channels, ["passages"]) + fields = set(SearchRequestBody.model_fields) + self.assertIn("target", fields) + for banned in ("product_id", "sku", "brand", "user_id"): + self.assertNotIn(banned, fields) + + async def test_omitted_target_keeps_retrieve_order(self): + chunks = [ + (TextChunk(id="first", text="DOCID 70s-item. lamp"), 9.0), + (TextChunk(id="second", text="DOCID modern-item. lamp"), 8.0), + (TextChunk(id="third", text="no join"), 7.0), + ] + response = await self._search(SearchRequestBody(query="lamp", k=3), chunks) + self.assertEqual([hit.id for hit in response.hits], ["first", "second", "third"]) + self.assertTrue(all(hit.scores.personalize is None for hit in response.hits)) + + async def test_target_promotes_70s_on_generic_query(self): + graph, _ = _style_graph() + chunks = [ + (TextChunk(id="first", text="DOCID modern-item. lamp"), 9.0), + (TextChunk(id="second", text="DOCID 70s-item. lamp"), 8.0), + (TextChunk(id="third", text="no join lamp"), 7.0), + ] + with patch("src.core.search.personalize.graph_adapter", graph): + response = await self._search( + SearchRequestBody(query="lamp", k=3, target="u01"), + chunks, + ) + ids = [hit.id for hit in response.hits] + self.assertEqual(ids[0], "second") + self.assertIn("third", ids) + self.assertEqual(response.hits[0].node_id, "70s-item") + self.assertIsNotNone(response.hits[0].scores.personalize) + + async def test_specific_query_does_not_reorder(self): + graph, _ = _style_graph() + chunks = [ + (TextChunk(id="first", text="DOCID modern-item. oak"), 9.0), + (TextChunk(id="second", text="DOCID 70s-item. oak"), 8.0), + ] + with patch("src.core.search.personalize.graph_adapter", graph): + response = await self._search( + SearchRequestBody( + query="oak dining table 180cm", k=2, target="u01" + ), + chunks, + ) + self.assertEqual([hit.id for hit in response.hits], ["first", "second"]) + + async def test_extras_and_survives_personalize(self): + graph, _ = _style_graph() + chunks = [ + ( + TextChunk( + id="first", + text="DOCID modern-item. lamp", + metadata={"color": "navy"}, + ), + 9.0, + ), + ( + TextChunk( + id="second", + text="DOCID 70s-item. lamp", + metadata={"color": "olive"}, + ), + 8.0, + ), + ( + TextChunk( + id="third", + text="DOCID other-item. lamp", + metadata={"color": "navy"}, + ), + 7.0, + ), + ] + with patch("src.core.search.personalize.graph_adapter", graph): + response = await self._search( + SearchRequestBody( + query="lamp", + k=3, + target="u01", + extras={"color": "navy"}, + ), + chunks, + ) + ids = [hit.id for hit in response.hits] + self.assertNotIn("second", ids) + self.assertEqual(set(ids), {"first", "third"}) + self.assertTrue( + all((hit.extras or {}).get("color") == "navy" for hit in response.hits) + ) + self.assertTrue( + all((hit.extras or {}).get("style") != "70s" for hit in response.hits) + ) + + def test_personalize_never_filters_ids(self): + graph, _ = _style_graph() + with patch("src.core.search.personalize.graph_adapter", graph): + ordered, _ = personalize_ranked_ids( + query="lamp", + ranked_ids=["a", "b"], + retrieve_scores={"a": 2, "b": 1}, + node_id_by_hit={"a": "modern-item", "b": "70s-item"}, + target="u01", + brain_id="brain", + ) + self.assertEqual(set(ordered), {"a", "b"}) diff --git a/tests/test_search_plugins.py b/tests/test_search_plugins.py new file mode 100644 index 0000000000000000000000000000000000000000..eaa99337895f9d604445aaec35d06f29b51d9796 --- /dev/null +++ b/tests/test_search_plugins.py @@ -0,0 +1,455 @@ +import os +import sys +import unittest +from pathlib import Path +from unittest.mock import MagicMock, patch + +ENV_DEFAULTS = { + "BRAINPAT_TOKEN": "test-token", + "MODELS_MODE": "local", + "EMBEDDINGS_LOCAL_MODEL": "local-model", + "EMBEDDINGS_SMALL_MODEL": "small-model", + "EMBEDDING_NODES_DIMENSION": "3", + "EMBEDDING_TRIPLETS_DIMENSION": "3", + "EMBEDDING_OBSERVATIONS_DIMENSION": "3", + "EMBEDDING_DATA_DIMENSION": "3", + "EMBEDDING_RELATIONSHIPS_DIMENSION": "3", + "REDIS_HOST": "localhost", + "REDIS_PORT": "6379", + "NEO4J_HOST": "localhost", + "NEO4J_PORT": "7687", + "NEO4J_USERNAME": "neo4j", + "NEO4J_PASSWORD": "password", + "MILVUS_HOST": "localhost", + "MILVUS_PORT": "19530", + "MONGO_CONNECTION_STRING": "mongodb://localhost:27017", + "CELERY_WORKER_CONCURRENCY": "1", + "OLLAMA_HOST": "localhost", + "OLLAMA_PORT": "11434", + "OLLAMA_LLM_SMALL_MODEL": "small", + "OLLAMA_LLM_LARGE_MODEL": "large", +} +for key, value in ENV_DEFAULTS.items(): + os.environ.setdefault(key, value) + +from fastapi import HTTPException + +from src.constants.data import TextChunk +from src.core.search.hooks import ( + register_search_reranker, + register_search_retriever, + reset_search_plugins, +) +from src.services.api.constants.requests import SearchRequestBody + +ROOT = Path(__file__).resolve().parents[1] + + +class SearchPluginHookTests(unittest.IsolatedAsyncioTestCase): + def setUp(self): + reset_search_plugins() + + def tearDown(self): + reset_search_plugins() + + async def test_unknown_rerank_plugin_is_400(self): + from src.services.api.controllers import search as search_mod + + with patch.object(search_mod.config, "search_enabled", True): + with self.assertRaises(HTTPException) as ctx: + await search_mod.search( + SearchRequestBody( + query="license", + rerank="plugin:missing-ce", + ) + ) + self.assertEqual(ctx.exception.status_code, 400) + self.assertIn("Unknown search rerank plugin", str(ctx.exception.detail)) + + async def test_unknown_retriever_plugin_is_400(self): + from src.services.api.controllers import search as search_mod + + with patch.object(search_mod.config, "search_enabled", True): + with self.assertRaises(HTTPException) as ctx: + await search_mod.search( + SearchRequestBody( + query="license", + channels=["plugin:splade"], + ) + ) + self.assertEqual(ctx.exception.status_code, 400) + self.assertIn("Unknown search retriever plugin", str(ctx.exception.detail)) + + async def test_linear_rerank_is_400_not_silent(self): + from src.services.api.controllers import search as search_mod + + with patch.object(search_mod.config, "search_enabled", True): + with self.assertRaises(HTTPException) as ctx: + await search_mod.search( + SearchRequestBody(query="license", rerank="linear") + ) + self.assertEqual(ctx.exception.status_code, 400) + + async def test_cross_encoder_reranks_top_hits(self): + from src.services.api.controllers import search as search_mod + + def reverse_rerank(query, candidates, k): + ordered = list(reversed(candidates)) + for index, item in enumerate(ordered): + item["score"] = 10 - index + return ordered[:k] + + register_search_reranker("cross-encoder", reverse_rerank) + mock_data = MagicMock() + mock_data.search_bm25.return_value = [ + (TextChunk(id="first", text="alpha"), 9.0), + (TextChunk(id="second", text="beta"), 8.0), + (TextChunk(id="third", text="gamma"), 7.0), + ] + mock_embeddings = MagicMock() + mock_vs = MagicMock() + + with ( + patch.object(search_mod.config, "search_enabled", True), + patch.object(search_mod.config, "search_use_dense", False), + patch.object(search_mod.config, "search_use_bm25", True), + patch.object(search_mod, "embeddings_adapter", mock_embeddings), + patch.object(search_mod, "vector_search", mock_vs), + patch.object(search_mod, "data_adapter", mock_data), + ): + response = await search_mod.search( + SearchRequestBody( + query="license", + k=3, + rerank="plugin:cross-encoder", + ) + ) + + self.assertEqual([hit.id for hit in response.hits], ["third", "second", "first"]) + self.assertEqual(response.hits[0].scores.rerank, 10) + mock_embeddings.embed_text.assert_not_called() + + def test_catalog_retrieve_and_rerank_caps(self): + from src.core.search.hooks import ( + CATALOG_RERANK_MAX_K, + RERANK_MAX_K, + rerank_max_k_for_mode, + retrieve_k_for_mode, + ) + + self.assertEqual(RERANK_MAX_K, 10) + self.assertEqual(CATALOG_RERANK_MAX_K, 50) + self.assertEqual(retrieve_k_for_mode("default", 10), 10) + self.assertEqual(retrieve_k_for_mode("catalog", 10), 50) + self.assertEqual(retrieve_k_for_mode("catalog", 50), 50) + self.assertEqual(retrieve_k_for_mode("catalog", 200), 200) + self.assertEqual(rerank_max_k_for_mode("default"), 10) + self.assertEqual(rerank_max_k_for_mode("catalog"), 50) + + async def test_default_reranks_at_most_ten(self): + from src.core.search.hooks import RERANK_MAX_K + from src.services.api.controllers import search as search_mod + + seen: list[tuple[int, int]] = [] + + def capture_rerank(query, candidates, k): + seen.append((len(candidates), k)) + return list(candidates)[:k] + + register_search_reranker("cross-encoder", capture_rerank) + mock_data = MagicMock() + mock_data.search_bm25.return_value = [ + (TextChunk(id=f"h{index}", text=f"t{index}"), float(60 - index)) + for index in range(50) + ] + mock_embeddings = MagicMock() + mock_vs = MagicMock() + + with ( + patch.object(search_mod.config, "search_enabled", True), + patch.object(search_mod.config, "search_use_dense", False), + patch.object(search_mod.config, "search_use_bm25", True), + patch.object(search_mod, "embeddings_adapter", mock_embeddings), + patch.object(search_mod, "vector_search", mock_vs), + patch.object(search_mod, "data_adapter", mock_data), + ): + response = await search_mod.search( + SearchRequestBody( + query="license", + k=50, + rerank="plugin:cross-encoder", + ) + ) + + self.assertEqual(RERANK_MAX_K, 10) + self.assertEqual(seen, [(10, 10)]) + self.assertEqual(len(response.hits), 50) + mock_data.search_bm25.assert_called_with("license", "default", limit=50) + + async def test_catalog_mode_reranks_fifty_when_k_fifty(self): + from src.core.search.hooks import CATALOG_RERANK_MAX_K + from src.services.api.controllers import search as search_mod + + seen: list[tuple[int, int]] = [] + + def capture_rerank(query, candidates, k): + seen.append((len(candidates), k)) + ordered = list(reversed(candidates)) + return ordered[:k] + + register_search_reranker("cross-encoder", capture_rerank) + mock_data = MagicMock() + mock_data.search_bm25.return_value = [ + (TextChunk(id=f"h{index}", text=f"t{index}"), float(60 - index)) + for index in range(50) + ] + mock_embeddings = MagicMock() + mock_vs = MagicMock() + + with ( + patch.object(search_mod.config, "search_enabled", True), + patch.object(search_mod.config, "search_use_dense", False), + patch.object(search_mod.config, "search_use_bm25", True), + patch.object(search_mod, "embeddings_adapter", mock_embeddings), + patch.object(search_mod, "vector_search", mock_vs), + patch.object(search_mod, "data_adapter", mock_data), + ): + response = await search_mod.search( + SearchRequestBody( + query="license", + k=50, + mode="catalog", + rerank="plugin:cross-encoder", + ) + ) + + self.assertEqual(CATALOG_RERANK_MAX_K, 50) + self.assertEqual(seen, [(50, 50)]) + self.assertEqual(len(response.hits), 50) + self.assertEqual(response.hits[0].id, "h49") + mock_data.search_bm25.assert_called_with("license", "default", limit=50) + + async def test_catalog_mode_retrieves_fifty_when_k_ten(self): + from src.services.api.controllers import search as search_mod + + seen: list[tuple[int, int]] = [] + + def capture_rerank(query, candidates, k): + seen.append((len(candidates), k)) + return list(candidates)[:k] + + register_search_reranker("cross-encoder", capture_rerank) + mock_data = MagicMock() + mock_data.search_bm25.return_value = [ + (TextChunk(id=f"h{index}", text=f"t{index}"), float(60 - index)) + for index in range(50) + ] + mock_embeddings = MagicMock() + mock_vs = MagicMock() + + with ( + patch.object(search_mod.config, "search_enabled", True), + patch.object(search_mod.config, "search_use_dense", False), + patch.object(search_mod.config, "search_use_bm25", True), + patch.object(search_mod, "embeddings_adapter", mock_embeddings), + patch.object(search_mod, "vector_search", mock_vs), + patch.object(search_mod, "data_adapter", mock_data), + ): + response = await search_mod.search( + SearchRequestBody( + query="license", + k=10, + mode="catalog", + rerank="plugin:cross-encoder", + ) + ) + + self.assertEqual(seen, [(50, 50)]) + self.assertEqual(len(response.hits), 10) + mock_data.search_bm25.assert_called_with("license", "default", limit=50) + + async def test_splade_channel_fuses_without_silent_bm25_fallback(self): + from src.services.api.controllers import search as search_mod + + def fake_splade(query, brain_id, k): + return ["splade-hit"], {"splade-hit": 4.2}, {"splade-hit": "sparse hit"} + + register_search_retriever("splade", fake_splade) + mock_data = MagicMock() + mock_embeddings = MagicMock() + mock_vs = MagicMock() + + with ( + patch.object(search_mod.config, "search_enabled", True), + patch.object(search_mod.config, "search_use_dense", False), + patch.object(search_mod.config, "search_use_bm25", True), + patch.object(search_mod, "embeddings_adapter", mock_embeddings), + patch.object(search_mod, "vector_search", mock_vs), + patch.object(search_mod, "data_adapter", mock_data), + ): + response = await search_mod.search( + SearchRequestBody( + query="license", + k=5, + channels=["plugin:splade"], + ) + ) + + self.assertEqual([hit.id for hit in response.hits], ["splade-hit"]) + self.assertEqual(response.hits[0].scores.plugin["splade"], 4.2) + mock_data.search_bm25.assert_not_called() + + async def test_plugin_lists_fill_frozen_tail_not_head(self): + from src.services.api.controllers import search as search_mod + + def fake_splade(query, brain_id, k): + return ( + ["plugin-extra"], + {"plugin-extra": 9.9}, + {"plugin-extra": "sparse extra"}, + ) + + register_search_retriever("splade", fake_splade) + mock_data = MagicMock() + mock_data.search_bm25.return_value = [ + (TextChunk(id=f"h{index}", text=f"t{index}"), float(20 - index)) + for index in range(12) + ] + mock_embeddings = MagicMock() + mock_vs = MagicMock() + + with ( + patch.object(search_mod.config, "search_enabled", True), + patch.object(search_mod.config, "search_use_dense", False), + patch.object(search_mod.config, "search_use_bm25", True), + patch.object(search_mod.config, "search_literal_fill", False), + patch.object(search_mod, "embeddings_adapter", mock_embeddings), + patch.object(search_mod, "vector_search", mock_vs), + patch.object(search_mod, "data_adapter", mock_data), + ): + response = await search_mod.search( + SearchRequestBody( + query="license", + k=15, + channels=["passages", "plugin:splade"], + ) + ) + + ids = [hit.id for hit in response.hits] + self.assertEqual(ids[:10], [f"h{index}" for index in range(10)]) + self.assertIn("plugin-extra", ids[10:]) + self.assertNotIn("plugin-extra", ids[:10]) + + def test_context_controller_does_not_import_search_hooks(self): + retrieve = ROOT / "src" / "services" / "api" / "controllers" / "retrieve.py" + text = retrieve.read_text(encoding="utf-8") + self.assertNotIn("src.core.search.hooks", text) + self.assertNotIn("src.core.search.graph_channels", text) + self.assertNotIn("register_search_reranker", text) + self.assertNotIn("plugin:cross-encoder", text) + + +class SearchPluginPackageTests(unittest.TestCase): + def setUp(self): + reset_search_plugins() + self._extra_paths: list[str] = [] + self._extra_modules: list[str] = [] + + def tearDown(self): + reset_search_plugins() + for name in self._extra_modules: + sys.modules.pop(name, None) + for path in self._extra_paths: + if path in sys.path: + sys.path.remove(path) + + def _load_plugin_dir(self, name: str): + plugin_path = ROOT / "plugins" / name + if not (plugin_path / "main.py").is_file(): + self.skipTest(f"optional search plugin is not installed: {name}") + plugin_dir = str(plugin_path) + sys.path.insert(0, plugin_dir) + self._extra_paths.append(plugin_dir) + return plugin_dir + + def test_cross_encoder_plugin_reranks_with_injected_predict(self): + self._load_plugin_dir("search-rerank") + import rerank as ce + + self._extra_modules.extend(["rerank", "main", "routes"]) + ce.set_predict(lambda pairs: [float(len(text)) for _, text in pairs]) + ranked = ce.rerank( + "q", + [ + {"id": "short", "text": "ab", "score": 1}, + {"id": "long", "text": "abcdefghij", "score": 1}, + ], + 10, + ) + self.assertEqual([row["id"] for row in ranked], ["long", "short"]) + from main import register as register_ce + + class Ctx: + _app = None + + def register_search_reranker(self, name, fn): + register_search_reranker(name, fn) + + def register_search_retriever(self, name, fn): + register_search_retriever(name, fn) + + register_ce(Ctx()) + from src.core.search.hooks import get_search_reranker + + self.assertIsNotNone(get_search_reranker("cross-encoder")) + + def test_splade_index_and_retrieve_with_fake_encoder(self): + self._load_plugin_dir("search-splade") + import encode as splade_encode + import index as splade_index + + self._extra_modules.extend(["encode", "index", "routes", "main"]) + splade_encode.set_encoder( + lambda text: {token: 1.0 for token in text.split() if token} + ) + splade_index.index_chunks( + "searchbenchsmoke", + [ + {"id": "a", "text": "alice license portland"}, + {"id": "b", "text": "piano thursday"}, + ], + ) + ids, scores, texts = splade_index.retrieve( + "alice license", "searchbenchsmoke", 2 + ) + self.assertEqual(ids[0], "a") + self.assertGreater(scores["a"], scores.get("b", 0)) + self.assertIn("alice", texts["a"]) + + def test_colbert_maxsim_retrieve_with_fake_encoder(self): + self._load_plugin_dir("search-colbert") + import encode as colbert_encode + import index as colbert_index + + self._extra_modules.extend(["encode", "index", "routes", "main"]) + + def fake_tokens(text: str): + table = { + "q": [[1.0, 0.0]], + "match": [[1.0, 0.0]], + "other": [[0.0, 1.0]], + } + return table.get(text, [[0.0, 0.0]]) + + colbert_encode.set_encoder(fake_tokens) + colbert_index.index_chunks( + "searchbenchsmoke", + [{"id": "hit", "text": "match"}, {"id": "miss", "text": "other"}], + ) + ids, scores, _ = colbert_index.retrieve("q", "searchbenchsmoke", 2) + self.assertEqual(ids[0], "hit") + self.assertGreater(scores["hit"], scores["miss"]) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_spacy_fixes_verification.py b/tests/test_spacy_fixes_verification.py new file mode 100644 index 0000000000000000000000000000000000000000..8704fadcaf8187fd0bc73510ff05957df1d31c4e --- /dev/null +++ b/tests/test_spacy_fixes_verification.py @@ -0,0 +1,154 @@ +import ast +import os +import unittest +from pathlib import Path +from unittest.mock import MagicMock, patch + +ROOT = Path(__file__).resolve().parent.parent + +ENV_DEFAULTS = { + "BRAINPAT_TOKEN": "test-token", + "MODELS_MODE": "local", + "EMBEDDINGS_LOCAL_MODEL": "local-model", + "EMBEDDINGS_SMALL_MODEL": "small-model", + "EMBEDDING_NODES_DIMENSION": "3", + "EMBEDDING_TRIPLETS_DIMENSION": "3", + "EMBEDDING_OBSERVATIONS_DIMENSION": "3", + "EMBEDDING_DATA_DIMENSION": "3", + "EMBEDDING_RELATIONSHIPS_DIMENSION": "3", + "REDIS_HOST": "localhost", + "REDIS_PORT": "6379", + "NEO4J_HOST": "localhost", + "NEO4J_PORT": "7687", + "NEO4J_USERNAME": "neo4j", + "NEO4J_PASSWORD": "password", + "MILVUS_HOST": "localhost", + "MILVUS_PORT": "19530", + "MONGO_CONNECTION_STRING": "mongodb://localhost", + "CELERY_WORKER_CONCURRENCY": "1", + "OLLAMA_HOST": "localhost", + "OLLAMA_PORT": "11434", + "OLLAMA_LLM_SMALL_MODEL": "small", + "OLLAMA_LLM_LARGE_MODEL": "large", +} +for _k, _v in ENV_DEFAULTS.items(): + os.environ.setdefault(_k, _v) + + +class TestDockerfileSpacyModels(unittest.TestCase): + def test_builder_installs_models_via_preload_script(self): + dockerfile = (ROOT / "Dockerfile").read_text() + preload = (ROOT / "scripts" / "preload_docker_models.py").read_text() + self.assertIn("preload_docker_models.py", dockerfile) + self.assertIn("SPACY_MODEL_NAMES", preload) + self.assertIn("spacy download", preload) + self.assertIn("src.constants.spacy_models", preload) + + def test_preload_detects_wrapped_huggingface_rate_limits(self): + import scripts.preload_docker_models as preload + + class FakeResponse: + status_code = 429 + headers = {"Retry-After": "7"} + + class HubError(Exception): + def __init__(self): + super().__init__("429 Client Error: Too Many Requests") + self.response = FakeResponse() + + wrapped = OSError( + "We couldn't connect to 'https://huggingface.co' to load the files" + ) + wrapped.__cause__ = HubError() + + self.assertTrue(preload._is_rate_limited(wrapped)) + self.assertTrue(preload._is_transient_hub_error(wrapped)) + self.assertEqual(preload._retry_after_seconds(wrapped), 7.0) + + def test_chinese_uses_web_pipeline_name(self): + from src.constants.spacy_models import SPACY_MODEL_NAMES + + self.assertEqual(SPACY_MODEL_NAMES["zh"], "zh_core_web_sm") + + def test_no_languages_without_spacy_3_8_pipelines(self): + from src.constants.spacy_models import SPACY_MODEL_NAMES + + for code in ( + "ar", + "bn", + "fil", + "hi", + "id", + "km", + "lo", + "ml", + "ms", + "my", + "pa", + "ta", + "te", + "th", + "tr", + "vi", + ): + self.assertNotIn(code, SPACY_MODEL_NAMES) + + +class TestFastAPIRedirectSlashes(unittest.TestCase): + def test_app_disables_trailing_slash_redirect(self): + src = (ROOT / "src" / "services" / "api" / "app.py").read_text() + tree = ast.parse(src) + found = False + for node in ast.walk(tree): + if not isinstance(node, ast.Call): + continue + func = node.func + if not isinstance(func, ast.Name) or func.id != "FastAPI": + continue + for kw in node.keywords: + if kw.arg == "redirect_slashes" and isinstance( + kw.value, ast.Constant + ): + if kw.value.value is False: + found = True + self.assertTrue( + found, "FastAPI(...) should pass redirect_slashes=False" + ) + + +class TestSpacyManagerDownloadBehavior(unittest.TestCase): + @patch("src.utils.nlp.spacy.subprocess.run") + @patch("src.utils.nlp.spacy.spacy.load") + def test_failed_download_raises_oserror_not_sysexit( + self, mock_load, mock_run + ): + mock_load.side_effect = OSError("missing") + mock_run.return_value = MagicMock(returncode=1, stderr="nope", stdout="") + + from src.utils.nlp.spacy import SpacyManager + + mgr = SpacyManager() + with self.assertRaises(OSError) as ctx: + mgr.get_model("it") + self.assertIn("it_core_news_sm", str(ctx.exception)) + mock_run.assert_called_once() + + @patch("src.utils.nlp.spacy.subprocess.run") + @patch("src.utils.nlp.spacy.spacy.load") + def test_successful_download_then_load( + self, mock_load, mock_run + ): + nlp = object() + mock_load.side_effect = [OSError("missing"), nlp] + mock_run.return_value = MagicMock(returncode=0, stderr="", stdout="") + + from src.utils.nlp.spacy import SpacyManager + + mgr = SpacyManager() + out = mgr.get_model("en") + self.assertIs(out, nlp) + self.assertEqual(mock_load.call_count, 2) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_stage_latency_profiler.py b/tests/test_stage_latency_profiler.py new file mode 100644 index 0000000000000000000000000000000000000000..88d3a029f24df5b3c28b945ff859c829de99f05c --- /dev/null +++ b/tests/test_stage_latency_profiler.py @@ -0,0 +1,476 @@ +import asyncio +import contextlib +import os +import sys +import threading +import time +import unittest +from unittest.mock import MagicMock, patch + + +ENV_DEFAULTS = { + "BRAINPAT_TOKEN": "test-token", + "MODELS_MODE": "local", + "EMBEDDINGS_LOCAL_MODEL": "local-model", + "EMBEDDINGS_SMALL_MODEL": "small-model", + "EMBEDDING_NODES_DIMENSION": "3", + "EMBEDDING_TRIPLETS_DIMENSION": "3", + "EMBEDDING_OBSERVATIONS_DIMENSION": "3", + "EMBEDDING_DATA_DIMENSION": "3", + "EMBEDDING_RELATIONSHIPS_DIMENSION": "3", + "REDIS_HOST": "localhost", + "REDIS_PORT": "6379", + "NEO4J_HOST": "localhost", + "NEO4J_PORT": "7687", + "NEO4J_USERNAME": "neo4j", + "NEO4J_PASSWORD": "password", + "MILVUS_HOST": "localhost", + "MILVUS_PORT": "19530", + "MONGO_CONNECTION_STRING": "mongodb://localhost:27017", + "CELERY_WORKER_CONCURRENCY": "1", + "OLLAMA_HOST": "localhost", + "OLLAMA_PORT": "11434", + "OLLAMA_LLM_SMALL_MODEL": "small", + "OLLAMA_LLM_LARGE_MODEL": "large", + "POSTGRES_HOST": "localhost", + "POSTGRES_PORT": "5432", + "POSTGRES_USERNAME": "postgres", + "POSTGRES_PASSWORD": "postgres", +} +for key, value in ENV_DEFAULTS.items(): + os.environ.setdefault(key, value) + +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from src.constants.embeddings import Vector +from src.constants.kg import Node, Predicate +from src.core.search import entity_info as entity_info_mod +from src.core.search.entity_info import EventSynergyRetriever +from src.lib.tracing.profiler import ( + STAGE_PROFILER_ENV_FLAG, + profile_request, + profile_stage, + stage_profiling_enabled, +) +from src.services.api.constants.requests import GetContextRequestBody +from src.utils.nlp.ner import ExtractElementsResponse + + +def _node(uuid: str, name: str, *, happened_at: str | None = None, labels=None): + return Node( + uuid=uuid, + name=name, + labels=labels or ["PERSON"], + description=f"desc-{name}", + happened_at=happened_at, + ) + + +def _pred(uuid: str, name: str, *, v_id: str | None = None): + predicate = Predicate(uuid=uuid, name=name, description=f"desc-{name}") + if v_id is not None: + predicate.properties = {"v_id": v_id} + return predicate + + +def _stages_by_name(report: dict) -> dict[str, dict]: + return {stage["stage"]: stage for stage in report["stages"]} + + +def _facts(count: int): + return [ + ( + _node("a", "Alice"), + _pred(f"r{i}a", f"ATTENDED_{i}"), + _node( + f"e{i}", + f"Event{i}", + happened_at=f"2024-0{i + 1}-01", + labels=["EVENT"], + ), + _pred(f"r{i}b", f"WITH_{i}"), + _node(f"b{i}", f"Person{i}"), + ) + for i in range(count) + ] + + +@contextlib.contextmanager +def _stubbed_adapters(retrieve_mod, facts, extra=()): + mock_embeddings = MagicMock() + mock_embeddings.embed_text.return_value = Vector( + id="q", embeddings=[0.1, 0.2, 0.3], metadata={} + ) + mock_vs = MagicMock() + mock_vs.search_nodes.return_value = [ + Vector( + id="n1", + embeddings=[0.1, 0.2, 0.3], + metadata={"uuid": "a", "name": "Alice"}, + distance=0.1, + ) + ] + mock_vs.search_relationships.return_value = [] + mock_vs.search_data.return_value = [] + mock_graph = MagicMock() + mock_graph.get_event_centric_neighbors.return_value = facts + mock_data = MagicMock() + mock_data.get_last_text_chunks.return_value = [] + mock_data.get_last_structured_data.return_value = [] + mock_data.search.return_value = MagicMock(text_chunks=[]) + mock_data.get_text_chunks_by_ids.return_value = ([], []) + + with contextlib.ExitStack() as stack: + stack.enter_context( + patch.object(retrieve_mod, "embeddings_adapter", mock_embeddings) + ) + stack.enter_context(patch.object(retrieve_mod, "vector_search", mock_vs)) + stack.enter_context(patch.object(retrieve_mod, "graph_adapter", mock_graph)) + stack.enter_context(patch.object(retrieve_mod, "data_adapter", mock_data)) + stack.enter_context( + patch.object( + retrieve_mod._entity_extractor, + "extract_elements", + return_value=ExtractElementsResponse( + tokens=[{"text": "Alice", "lemma": "Alice", "pos": "PROPN"}], + noun_chunks=[], + ), + ) + ) + for context in extra: + stack.enter_context(context) + yield mock_graph + + +class StageProfilerMechanismTests(unittest.TestCase): + def test_spans_are_not_recorded_unless_a_profile_is_open(self): + with profile_stage("orphan") as detail: + detail["ignored"] = 1 + self.assertFalse(stage_profiling_enabled()) + with profile_request("noop", enabled=False) as profiler: + self.assertIsNone(profiler) + with profile_stage("still-off"): + pass + + def test_environment_flag_enables_profiling_without_a_request_flag(self): + with patch.dict(os.environ, {STAGE_PROFILER_ENV_FLAG: "true"}): + self.assertTrue(stage_profiling_enabled()) + with profile_request("env", enabled=False, publish=False) as profiler: + self.assertIsNotNone(profiler) + with profile_stage("worked"): + pass + self.assertIn("worked", _stages_by_name(profiler.last_report)) + + def test_nested_spans_carry_their_parent_and_detail(self): + with profile_request("root", enabled=True, publish=False) as profiler: + with profile_stage("outer"): + with profile_stage("inner", calls_expected=1) as detail: + detail["rows"] = 7 + + stages = _stages_by_name(profiler.last_report) + self.assertEqual(stages["outer"]["parent"], "root") + self.assertEqual(stages["inner"]["parent"], "outer") + self.assertEqual(stages["inner"]["detail"]["rows"], 7) + + def test_repeated_serial_spans_aggregate_without_overlap(self): + with profile_request("root", enabled=True, publish=False) as profiler: + for _ in range(3): + with profile_stage("serial"): + time.sleep(0.01) + + stage = _stages_by_name(profiler.last_report)["serial"] + self.assertEqual(stage["calls"], 3) + self.assertEqual(stage["overlap_ms"], 0.0) + self.assertAlmostEqual(stage["wall_ms"], stage["wall_sum_ms"], delta=1.0) + + def test_concurrent_spans_report_overlap_instead_of_a_false_sum(self): + async def scenario(): + with profile_request("root", enabled=True, publish=False) as profiler: + with profile_stage("fanout", blocking=False): + await asyncio.gather( + *[ + asyncio.to_thread(_sleep_span, "branch", 0.05) + for _ in range(4) + ] + ) + return profiler.last_report + + report = asyncio.run(scenario()) + stages = _stages_by_name(report) + branch = stages["branch"] + self.assertEqual(branch["calls"], 4) + self.assertEqual(branch["parent"], "fanout") + self.assertGreater(branch["threads"], 1) + self.assertGreater(branch["overlap_ms"], 0.0) + self.assertLess(branch["wall_ms"], branch["wall_sum_ms"]) + self.assertLessEqual(branch["wall_ms"], stages["fanout"]["wall_ms"] + 1.0) + + def test_off_loop_work_is_excluded_from_the_event_loop_budget(self): + async def scenario(): + with profile_request("root", enabled=True, publish=False) as profiler: + with profile_stage("on_loop"): + _spin(0.03) + await asyncio.to_thread(_sleep_span, "off_loop", 0.05) + return profiler.last_report + + report = asyncio.run(scenario()) + stages = _stages_by_name(report) + self.assertTrue(stages["on_loop"]["on_loop"]) + self.assertFalse(stages["off_loop"]["on_loop"]) + self.assertGreaterEqual(report["loop_blocked_ms"], 25.0) + self.assertLess(report["loop_blocked_ms"], stages["off_loop"]["wall_ms"] + 40.0) + + def test_io_wait_is_separated_from_python_cpu_time(self): + with profile_request("root", enabled=True, publish=False) as profiler: + with profile_stage("waiting"): + time.sleep(0.05) + with profile_stage("computing"): + _spin(0.05) + + stages = _stages_by_name(profiler.last_report) + self.assertGreater(stages["waiting"]["io_wait_ms"], 30.0) + self.assertLess(stages["waiting"]["cpu_ms"], 20.0) + self.assertGreater(stages["computing"]["cpu_ms"], 20.0) + + def test_awaiting_spans_do_not_claim_cpu_time(self): + async def scenario(): + with profile_request("root", enabled=True, publish=False) as profiler: + with profile_stage("awaits", blocking=False): + await asyncio.sleep(0.01) + return profiler.last_report + + stage = _stages_by_name(asyncio.run(scenario()))["awaits"] + self.assertIsNone(stage["cpu_ms"]) + self.assertIsNone(stage["io_wait_ms"]) + + def test_a_raising_body_still_records_the_span(self): + with self.assertRaises(RuntimeError): + with profile_request("root", enabled=True, publish=False) as profiler: + with profile_stage("boom"): + raise RuntimeError("boom") + + self.assertIn("boom", _stages_by_name(profiler.last_report)) + + +class GetContextStageCoverageTests(unittest.IsolatedAsyncioTestCase): + async def test_no_timings_are_attached_unless_the_caller_asks(self): + from src.services.api.controllers import retrieve as retrieve_mod + + with _stubbed_adapters(retrieve_mod, _facts(3)): + response = await retrieve_mod.get_context( + GetContextRequestBody(text="Alice", brain_id="brain-a") + ) + + self.assertIsNone(response.stage_timings) + + async def test_every_documented_stage_is_measured(self): + from src.services.api.controllers import retrieve as retrieve_mod + + with _stubbed_adapters( + retrieve_mod, + _facts(3), + extra=[ + patch.object( + retrieve_mod, + "_build_adjacency_from_seeds", + MagicMock(return_value={"a": ["e0"], "e0": ["a"]}), + ), + patch.object( + retrieve_mod, + "_context_looks_insufficient", + MagicMock(return_value=True), + ), + ], + ): + response = await retrieve_mod.get_context( + GetContextRequestBody( + text="Which books did Alice read?", + brain_id="brain-a", + sufficiency_retry=True, + profile_stages=True, + ) + ) + + report = response.stage_timings + self.assertIsNotNone(report) + stages = _stages_by_name(report) + for expected in ( + "nlp.extract_elements", + "nlp.query_variants", + "retrieval.fanout", + "facts.variant", + "facts.seed_search", + "embed.query", + "vector.search_nodes", + "vector.search_relationships", + "graph.event_neighbors", + "passages.collect", + "passages.retrieve", + "vector.search_data", + "data.keyword_search", + "historical.context", + "facts.dedup_rank", + "ppr", + "ppr.adjacency", + "ppr.iterations", + "fact_filter", + "context.assemble", + "sufficiency.check", + "sufficiency.retry", + "context.render", + ): + self.assertIn(expected, stages, f"{expected} was not measured") + self.assertFalse(any(name.startswith("dossiers") for name in stages)) + self.assertGreater(report["total_ms"], 0.0) + self.assertGreaterEqual(report["span_count"], len(stages)) + + async def test_the_concurrent_fanout_is_reported_as_overlapping(self): + from src.services.api.controllers import retrieve as retrieve_mod + + with _stubbed_adapters(retrieve_mod, _facts(2)): + response = await retrieve_mod.get_context( + GetContextRequestBody( + text="What did Alice and Bob do together?", + brain_id="brain-a", + profile_stages=True, + ) + ) + + stages = _stages_by_name(response.stage_timings) + fanout = stages["retrieval.fanout"] + variants = stages["facts.variant"] + self.assertGreater(variants["calls"], 1) + self.assertLessEqual(variants["wall_ms"], fanout["wall_ms"] + 1.0) + self.assertLessEqual(fanout["wall_ms"], response.stage_timings["total_ms"]) + + async def test_ppr_stages_are_attributed_to_the_event_loop(self): + from src.services.api.controllers import retrieve as retrieve_mod + + with _stubbed_adapters( + retrieve_mod, + _facts(3), + extra=[ + patch.object( + retrieve_mod, + "_build_adjacency_from_seeds", + MagicMock(return_value={"a": ["e0"], "e0": ["a"]}), + ) + ], + ): + response = await retrieve_mod.get_context( + GetContextRequestBody( + text="Alice", brain_id="brain-a", profile_stages=True + ) + ) + + stages = _stages_by_name(response.stage_timings) + self.assertTrue(stages["ppr.adjacency"]["on_loop"]) + self.assertTrue(stages["ppr.iterations"]["on_loop"]) + self.assertTrue(stages["nlp.extract_elements"]["on_loop"]) + self.assertFalse(stages["facts.variant"]["on_loop"]) + self.assertEqual(stages["ppr.iterations"]["detail"]["iterations"], 20) + + async def test_the_report_is_published_as_a_latency_trace_event(self): + from src.lib.tracing.events import TraceEventType + from src.lib.tracing.tracker import tracer + from src.services.api.controllers import retrieve as retrieve_mod + + tracer.queue.drain() + with _stubbed_adapters(retrieve_mod, _facts(1)): + await retrieve_mod.get_context( + GetContextRequestBody( + text="Alice", brain_id="brain-a", profile_stages=True + ) + ) + + events = [ + event + for event in tracer.queue.drain() + if event.event_type is TraceEventType.LATENCY + ] + self.assertEqual(len(events), 1) + self.assertEqual(events[0].name, "retrieve.context.stages") + self.assertIn("stages", events[0].metadata) + + +class SpacyStagePassesTests(unittest.TestCase): + def test_the_duplicate_process_doc_pass_is_visible_in_the_report(self): + from src.utils.nlp import ner as ner_mod + + with ( + patch.object(ner_mod, "langid_detect", return_value=("en", 1.0)), + patch.object( + ner_mod._entity_extractor.spacy_manager, + "get_model", + return_value=MagicMock(return_value=object()), + ), + patch.object( + ner_mod.MultiLangEntityExtractor, + "_process_doc", + return_value={"tokens": [], "noun_chunks": []}, + ), + ): + with profile_request("nlp", enabled=True, publish=False) as profiler: + ner_mod._entity_extractor.extract_elements("Alice read a book") + + stages = _stages_by_name(profiler.last_report) + self.assertEqual(stages["nlp.parse"]["calls"], 1) + self.assertEqual(stages["nlp.process_doc"]["calls"], 2) + self.assertTrue(stages["nlp.process_doc"]["on_loop"]) + + +class DossierWalkStageTests(unittest.TestCase): + def test_one_vector_fetch_per_edge_is_counted(self): + nodes = {name: _node(name, name.upper()) for name in ("a", "b", "c", "d")} + rels = { + "a": [ + (_pred("r1", "TO_B", v_id="v1"), nodes["b"]), + (_pred("r2", "TO_C", v_id="v2"), nodes["c"]), + ], + "b": [(_pred("r3", "TO_D", v_id="v3"), nodes["d"])], + "c": [], + "d": [], + } + edges = sum(len(v) for v in rels.values()) + + graph = MagicMock() + graph.get_neighbors.side_effect = lambda ids, brain_id="default", **kwargs: { + ids[0]: rels.get(ids[0], []) + } + vectors = MagicMock() + vectors.get_by_ids.side_effect = lambda ids, **kwargs: [ + Vector(id=ids[0], embeddings=[1.0, 0.0], metadata={}) + ] + + with ( + patch.object(entity_info_mod, "graph_adapter", graph), + patch.object(entity_info_mod, "vector_store_adapter", vectors), + patch.object(entity_info_mod, "cosine_similarity", return_value=0.5), + ): + retriever = EventSynergyRetriever("brain-a") + with profile_request("dossier", enabled=True, publish=False) as profiler: + with profile_stage("dossiers.explore"): + retriever._recursive_explorer( + "a", [1.0, 0.0], depth=3, visited_ids=set(), work_counter=[0] + ) + + stages = _stages_by_name(profiler.last_report) + fetches = stages["dossiers.edge_vector_fetch"] + self.assertEqual(fetches["calls"], edges) + self.assertEqual(fetches["parent"], "dossiers.explore") + self.assertEqual(stages["dossiers.get_neighbors"]["parent"], "dossiers.explore") + + +def _sleep_span(name: str, seconds: float) -> None: + with profile_stage(name): + time.sleep(seconds) + + +def _spin(seconds: float) -> None: + deadline = time.perf_counter() + seconds + while time.perf_counter() < deadline: + threading.get_ident() + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_static_hardening_refactors.py b/tests/test_static_hardening_refactors.py new file mode 100644 index 0000000000000000000000000000000000000000..c5a0d68dbad4c95a3f69d840ff68847ba253de72 --- /dev/null +++ b/tests/test_static_hardening_refactors.py @@ -0,0 +1,204 @@ +import ast +from pathlib import Path +import unittest + + +ROOT = Path(__file__).resolve().parent.parent + + +def read_source(relative_path: str) -> str: + return (ROOT / relative_path).read_text(encoding="utf-8") + + +def parse_source(relative_path: str) -> ast.Module: + return ast.parse(read_source(relative_path)) + + +def get_function_default( + relative_path: str, function_name: str, arg_name: str +) -> ast.AST | None: + tree = parse_source(relative_path) + for node in ast.walk(tree): + if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)): + if node.name != function_name: + continue + args = node.args.args + defaults = node.args.defaults + default_offset = len(args) - len(defaults) + for index, arg in enumerate(args): + if arg.arg == arg_name and index >= default_offset: + return defaults[index - default_offset] + return None + raise AssertionError(f"Function {function_name} not found in {relative_path}") + + +class MutableDefaultGuardTests(unittest.TestCase): + def test_graph_update_properties_uses_none_defaults(self): + new_properties_default = get_function_default( + "src/adapters/graph.py", "update_properties", "new_properties" + ) + properties_to_remove_default = get_function_default( + "src/adapters/graph.py", "update_properties", "properties_to_remove" + ) + self.assertIsInstance(new_properties_default, ast.Constant) + self.assertIsNone(new_properties_default.value) + self.assertIsInstance(properties_to_remove_default, ast.Constant) + self.assertIsNone(properties_to_remove_default.value) + + def test_cleanup_strip_properties_uses_none_default(self): + pop_also_default = get_function_default( + "src/utils/cleanup.py", "strip_properties", "pop_also" + ) + self.assertIsInstance(pop_also_default, ast.Constant) + self.assertIsNone(pop_also_default.value) + + def test_entities_get_entity_status_uses_none_default(self): + types_default = get_function_default( + "src/services/api/controllers/entities.py", "get_entity_status", "types" + ) + self.assertIsInstance(types_default, ast.Constant) + self.assertIsNone(types_default.value) + + def test_routes_get_entity_status_uses_none_default(self): + types_default = get_function_default( + "src/services/api/routes/retrieve.py", "get_entity_status", "types" + ) + self.assertIsInstance(types_default, ast.Constant) + self.assertIsNone(types_default.value) + + +class GraphReductionArchitectureTests(unittest.TestCase): + def setUp(self): + self.source = read_source("src/adapters/graph.py") + self.tree = parse_source("src/adapters/graph.py") + + def test_strategy_classes_exist(self): + class_names = { + node.name for node in ast.walk(self.tree) if isinstance(node, ast.ClassDef) + } + self.assertIn("NeighborVectorReductionStrategy", class_names) + self.assertIn("SimilarityOnlyReductionStrategy", class_names) + self.assertIn("DescriptionAwareReductionStrategy", class_names) + self.assertIn("NeighborVectorReductionStrategyFactory", class_names) + + def test_graph_adapter_init_accepts_reduction_factory(self): + self.assertIn( + "reduction_strategy_factory: Optional[NeighborVectorReductionStrategyFactory] = None", + self.source, + ) + self.assertIn( + "self._reduction_strategy_factory =", + self.source, + ) + + def test_reduce_neighbor_vectors_uses_strategy_factory(self): + self.assertIn("if not vectors_with_desc or not averaged_vector:", self.source) + self.assertIn( + "strategy = self._reduction_strategy_factory.create(description)", + self.source, + ) + self.assertIn( + "or len(embeddings) != len(averaged_vector)", + self.source, + ) + + def test_get_2nd_degree_hops_uses_averaging_helper(self): + self.assertIn("averaged_vector = self._average_embeddings(vs)", self.source) + self.assertIn("if len(averaged_vector) == 0:", self.source) + self.assertIn("all_filtered_fd_uuids: set[str] = set()", self.source) + + +class EntityStatusHardeningTests(unittest.TestCase): + def test_entity_status_skips_missing_graph_rows(self): + source = read_source("src/services/api/controllers/entities.py") + self.assertIn( + "nodes = graph_adapter.get_by_uuids([target_node_id], brain_id=brain_id)", + source, + ) + self.assertIn("if len(nodes) == 0:", source) + self.assertIn("continue", source) + + +class VectorSearchFacadeArchitectureTests(unittest.TestCase): + def test_vector_search_facade_file_exists_with_store_helpers(self): + source = read_source("src/utils/vector_search.py") + self.assertIn("class VectorSearchFacade", source) + self.assertIn("def search_nodes(", source) + self.assertIn("def search_triplets(", source) + self.assertIn("def search_relationships(", source) + self.assertIn("def search_data(", source) + self.assertIn("store=store", source) + self.assertIn("brain_id=brain_id", source) + + def test_retrieve_data_uses_facade_methods_not_positional_calls(self): + source = read_source("src/services/api/controllers/retrieve.py") + self.assertIn("vector_search = VectorSearchFacade(vector_store_adapter)", source) + self.assertIn("vector_search.search_data(", source) + self.assertIn("vector_search.search_triplets(", source) + self.assertIn("k=limit", source) + self.assertNotIn( + 'vector_store_adapter.search_vectors(\n text_embeddings.embeddings, "data", brain_id, limit', + source, + ) + self.assertNotIn( + 'vector_store_adapter.search_vectors(\n text_embeddings.embeddings, "triplets", brain_id, limit', + source, + ) + + def test_key_modules_use_facade_or_keyword_safe_search(self): + expected_snippets = { + "src/services/api/controllers/entities.py": [ + "VectorSearchFacade", + "vector_search.search_nodes(", + ], + "src/services/api/controllers/kg.py": [ + "VectorSearchFacade", + "vector_search.search_nodes(", + ], + "src/services/mcp/main.py": [ + "VectorSearchFacade", + "vector_search.search_nodes(", + ], + "src/core/search/entity_context.py": [ + "VectorSearchFacade", + "vector_search.search_nodes(", + ], + "src/core/search/entity_info.py": [ + "VectorSearchFacade", + "vector_search.search_nodes(", + ], + "src/core/search/entity_sibilings.py": [ + "VectorSearchFacade", + "vector_search.search_nodes(", + ], + "src/core/agents/tools/kg_agent/KGAgentSearchGraphTool.py": [ + 'store="triplets"', + 'store="nodes"', + "brain_id=self.brain_id", + ], + "src/core/agents/tools/kg_agent/KGAgentAddTripletsTool.py": [ + 'store="nodes"', + "brain_id=self.brain_id", + "k=5", + ], + } + for relative_path, snippets in expected_snippets.items(): + source = read_source(relative_path) + for snippet in snippets: + self.assertIn(snippet, source) + + +class JanitorToolDeduplicationTests(unittest.TestCase): + def test_janitor_tools_init_exports_single_graph_read_tool(self): + source = read_source("src/core/agents/tools/janitor_agent/__init__.py") + self.assertIn("JanitorAgentExecuteGraphReadOperationTool", source) + self.assertNotIn("JanitorAgentExecuteGraphOperationTool", source) + + def test_janitor_agent_imports_single_graph_read_tool(self): + source = read_source("src/core/agents/janitor_agent.py") + self.assertIn("JanitorAgentExecuteGraphReadOperationTool", source) + self.assertNotIn("JanitorAgentExecuteGraphOperationTool", source) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_structured_triple_persistence.py b/tests/test_structured_triple_persistence.py new file mode 100644 index 0000000000000000000000000000000000000000..d4ca4c3067c8a9b849fcb6eec56aa9125db9e844 --- /dev/null +++ b/tests/test_structured_triple_persistence.py @@ -0,0 +1,381 @@ +import os +import unittest +from unittest.mock import MagicMock, patch + + +ENV_DEFAULTS = { + "BRAINPAT_TOKEN": "test-token", + "MODELS_MODE": "local", + "EMBEDDINGS_LOCAL_MODEL": "local-model", + "EMBEDDINGS_SMALL_MODEL": "small-model", + "EMBEDDING_NODES_DIMENSION": "3", + "EMBEDDING_TRIPLETS_DIMENSION": "3", + "EMBEDDING_OBSERVATIONS_DIMENSION": "3", + "EMBEDDING_DATA_DIMENSION": "3", + "EMBEDDING_RELATIONSHIPS_DIMENSION": "3", + "REDIS_HOST": "localhost", + "REDIS_PORT": "6379", + "NEO4J_HOST": "localhost", + "NEO4J_PORT": "7687", + "NEO4J_USERNAME": "neo4j", + "NEO4J_PASSWORD": "password", + "MILVUS_HOST": "localhost", + "MILVUS_PORT": "19530", + "MONGO_CONNECTION_STRING": "mongodb://localhost:27017", + "CELERY_WORKER_CONCURRENCY": "1", + "OLLAMA_HOST": "localhost", + "OLLAMA_PORT": "11434", + "OLLAMA_LLM_SMALL_MODEL": "small", + "OLLAMA_LLM_LARGE_MODEL": "large", + "PIPELINE_MODE": "accurate", +} +for key, value in ENV_DEFAULTS.items(): + os.environ.setdefault(key, value) + + +def _immediate_chain_factory(persist_calls): + class ImmediateChain: + def __init__(self, *steps): + self.steps = steps + + def apply_async(self, *args, **kwargs): + for step in self.steps: + task_name = getattr(step, "task", None) or getattr(step, "name", "") + args_ = getattr(step, "args", ()) or () + if "process_architect_relationships" in str(task_name) and args_: + persist_calls.append(args_[0]) + return MagicMock(id="chain") + + return ImmediateChain + + + +def _full_triple(): + return { + "subject": {"name": "Alice", "type": "PERSON", "uuid": "sub-1"}, + "subj_event": {"name": "MADE", "uuid": "rel-1"}, + "event": { + "name": "Purchase", + "type": "EVENT", + "uuid": "evt-1", + "happened_at": "01/01/2024", + }, + "event_obj": {"name": "TARGETED", "uuid": "rel-2", "amount": 3}, + "object": {"name": "Widget", "type": "PRODUCT", "uuid": "obj-1"}, + } + + +class TripleConversionTests(unittest.TestCase): + def test_full_triple_produces_subject_event_and_event_object_edges(self): + from src.core.agents.architect_agent import ingestion_triples_to_relationships + from src.services.api.constants.requests import IngestionTripleSet + + triple = IngestionTripleSet(**_full_triple()) + relationships, entities = ingestion_triples_to_relationships([triple], []) + + self.assertEqual(len(relationships), 2) + self.assertEqual(relationships[0].tail.uuid, "sub-1") + self.assertEqual(relationships[0].name, "MADE") + self.assertEqual(relationships[0].tip.uuid, "evt-1") + self.assertEqual(relationships[0].tip.happened_at, "01/01/2024") + self.assertEqual(relationships[1].tail.uuid, "evt-1") + self.assertEqual(relationships[1].name, "TARGETED") + self.assertEqual(relationships[1].tip.uuid, "obj-1") + self.assertEqual(relationships[1].amount, 3) + self.assertIn(("alice", "person"), entities) + self.assertIn(("purchase", "event", "01/01/2024"), entities) + + def test_direct_has_is_one_edge_not_event(self): + from src.core.agents.architect_agent import ingestion_triples_to_relationships + from src.services.api.constants.requests import IngestionTripleSet + + triple = IngestionTripleSet( + subject={"name": "bed", "type": "ENTITY", "uuid": "0"}, + subj_event={"name": "HAS", "uuid": "rel-has-1"}, + object={"name": "rubberwood", "type": "ATTR", "uuid": "hub-attr-rubberwood"}, + ) + relationships, entities = ingestion_triples_to_relationships([triple], []) + self.assertEqual(len(relationships), 1) + self.assertEqual(relationships[0].tail.uuid, "0") + self.assertEqual(relationships[0].name, "HAS") + self.assertEqual(relationships[0].tip.uuid, "hub-attr-rubberwood") + self.assertEqual(relationships[0].tip.type, "ATTR") + self.assertNotIn(("has", "event", ""), entities) + self.assertNotIn(("has", "event"), entities) + + def test_has_wrapper_without_happened_at_collapses(self): + from src.core.agents.architect_agent import ingestion_triples_to_relationships + from src.services.api.constants.requests import IngestionTripleSet + + triple = IngestionTripleSet( + subject={"name": "bed", "type": "ENTITY", "uuid": "0"}, + subj_event={"name": "HAS", "uuid": "rel-1"}, + event={"name": "HAS", "type": "EVENT", "uuid": "evt-has"}, + event_obj={"name": "HAS", "uuid": "rel-2"}, + object={"name": "modern", "type": "ATTR", "uuid": "hub-modern"}, + ) + relationships, entities = ingestion_triples_to_relationships([triple], []) + self.assertEqual(len(relationships), 1) + self.assertEqual(relationships[0].tail.uuid, "0") + self.assertEqual(relationships[0].tip.uuid, "hub-modern") + self.assertEqual(relationships[0].name, "HAS") + self.assertTrue( + all(getattr(rel.tail, "type", "").upper() != "EVENT" for rel in relationships) + ) + self.assertTrue( + all(getattr(rel.tip, "type", "").upper() != "EVENT" for rel in relationships) + ) + + def test_has_event_with_happened_at_stays_two_edges(self): + from src.core.agents.architect_agent import ingestion_triples_to_relationships + from src.services.api.constants.requests import IngestionTripleSet + + triple = IngestionTripleSet( + subject={"name": "u1", "type": "USER", "uuid": "user-1"}, + subj_event={"name": "HAS", "uuid": "rel-1"}, + event={ + "name": "HAS", + "type": "EVENT", + "uuid": "evt-dated", + "happened_at": "01/02/2024", + }, + event_obj={"name": "HAS", "uuid": "rel-2"}, + object={"name": "sku-1", "type": "ENTITY", "uuid": "sku-1"}, + ) + relationships, _ = ingestion_triples_to_relationships([triple], []) + self.assertEqual(len(relationships), 2) + self.assertEqual(relationships[0].tip.uuid, "evt-dated") + self.assertEqual(relationships[1].tail.uuid, "evt-dated") + + def test_direct_prefers_is_one_edge(self): + from src.core.agents.architect_agent import ingestion_triples_to_relationships + from src.services.api.constants.requests import IngestionTripleSet + + triple = IngestionTripleSet( + subject={"name": "u01", "type": "USER", "uuid": "user:u01"}, + subj_event={ + "name": "PREFERS", + "uuid": "rel-pref-1", + "amount": 0.2, + "properties": {"facet": "style", "value": "70s"}, + }, + object={"name": "70s", "type": "ATTR", "uuid": "hub:attr:70s"}, + ) + relationships, entities = ingestion_triples_to_relationships([triple], []) + self.assertEqual(len(relationships), 1) + self.assertEqual(relationships[0].tail.uuid, "user:u01") + self.assertEqual(relationships[0].name, "PREFERS") + self.assertEqual(relationships[0].tip.uuid, "hub:attr:70s") + self.assertEqual(relationships[0].amount, 0.2) + self.assertNotIn(("prefers", "event", ""), entities) + self.assertNotIn(("prefers", "event"), entities) + + +class StructuredTriplePersistenceTests(unittest.TestCase): + def test_triple_only_persists_without_llm(self): + from src.workers.tasks import ingestion as ingestion_mod + + request = MagicMock() + request.id = "task-struct-1" + persist_calls = [] + saved_structured = [] + scout_called = [] + architect_called = [] + + args = { + "data": [_full_triple()], + "brain_id": "tenant-a", + "text": None, + "mode": "deterministic", + } + + with ( + patch.object(ingestion_mod, "cache_adapter"), + patch.object(ingestion_mod, "graph_adapter"), + patch.object(ingestion_mod, "IngestionManager"), + patch.object(ingestion_mod, "data_adapter") as data_adapter, + patch.object(ingestion_mod, "ScoutAgent") as ScoutAgent, + patch.object(ingestion_mod, "ArchitectAgent") as ArchitectAgent, + patch.object(ingestion_mod, "KGAgent") as KGAgent, + patch("celery.chain", _immediate_chain_factory(persist_calls)), + patch.object(ingestion_mod, "set_ingestion_task_status"), + patch.object(ingestion_mod, "finalize_ingestion_task"), + ): + data_adapter.save_structured_data.side_effect = ( + lambda data, brain_id="default": saved_structured.append( + {"data": data, "brain_id": brain_id} + ) + or data + ) + ScoutAgent.side_effect = lambda **kwargs: scout_called.append(True) + ArchitectAgent.side_effect = lambda **kwargs: architect_called.append(True) + KGAgent.side_effect = lambda **kwargs: (_ for _ in ()).throw( + AssertionError("KGAgent must not be constructed in deterministic mode") + ) + + result = ingestion_mod.ingest_structured_data.run.__func__( + type("Bound", (), {"request": request})(), + args, + ) + + self.assertEqual(result, "task-struct-1") + self.assertEqual(len(persist_calls), 1) + relationships = persist_calls[0]["relationships"] + self.assertEqual(len(relationships), 2) + self.assertEqual(relationships[0]["name"], "MADE") + self.assertEqual(relationships[1]["name"], "TARGETED") + self.assertEqual( + relationships[0]["properties"].get("source"), "structured_deterministic" + ) + self.assertEqual(persist_calls[0]["brain_id"], "tenant-a") + self.assertEqual(len(saved_structured), 1) + self.assertEqual(saved_structured[0]["brain_id"], "tenant-a") + self.assertEqual(saved_structured[0]["data"].id, "task-struct-1") + self.assertEqual(scout_called, []) + self.assertEqual(architect_called, []) + + def test_deterministic_ignores_text_for_llm(self): + from src.workers.tasks import ingestion as ingestion_mod + + request = MagicMock() + request.id = "task-struct-det-text" + persist_calls = [] + scout_called = [] + + args = { + "data": [_full_triple()], + "brain_id": "recsys-demo", + "text": "should not trigger scout", + "mode": "deterministic", + } + + with ( + patch.object(ingestion_mod, "cache_adapter"), + patch.object(ingestion_mod, "graph_adapter"), + patch.object(ingestion_mod, "IngestionManager"), + patch.object(ingestion_mod, "data_adapter"), + patch.object(ingestion_mod, "ScoutAgent") as ScoutAgent, + patch.object(ingestion_mod, "ArchitectAgent"), + patch.object(ingestion_mod, "KGAgent"), + patch("celery.chain", _immediate_chain_factory(persist_calls)), + patch.object(ingestion_mod, "set_ingestion_task_status"), + patch.object(ingestion_mod, "finalize_ingestion_task"), + ): + ScoutAgent.side_effect = lambda **kwargs: scout_called.append(True) + ingestion_mod.ingest_structured_data.run.__func__( + type("Bound", (), {"request": request})(), + args, + ) + + self.assertEqual(scout_called, []) + self.assertEqual(len(persist_calls), 1) + + def test_deterministic_fails_closed_on_unresolved_name_anchor(self): + from src.workers.tasks import ingestion as ingestion_mod + + request = MagicMock() + request.id = "task-struct-anchor-fail" + graph = MagicMock() + graph.get_by_identification_params.return_value = None + + args = { + "data": [_full_triple()], + "brain_id": "recsys-demo", + "mode": "deterministic", + "anchor": {"name": "Alice", "type": "PERSON"}, + } + + with ( + patch.object(ingestion_mod, "cache_adapter"), + patch.object(ingestion_mod, "graph_adapter", graph), + patch.object(ingestion_mod, "IngestionManager"), + patch.object(ingestion_mod, "data_adapter"), + patch.object(ingestion_mod, "KGAgent") as KGAgent, + patch.object(ingestion_mod, "set_ingestion_task_status"), + ): + KGAgent.side_effect = lambda **kwargs: (_ for _ in ()).throw( + AssertionError("KGAgent must not run in deterministic mode") + ) + with self.assertRaises(ValueError) as ctx: + ingestion_mod.ingest_structured_data.run.__func__( + type("Bound", (), {"request": request})(), + args, + ) + self.assertIn("deterministic mode", str(ctx.exception)) + + def test_text_enrichment_does_not_repersist_submitted_triples(self): + from src.core.agents.architect_agent import ArchitectAgentResponse + from src.core.agents.scout_agent import ScoutAgentResponse + from src.workers.tasks import ingestion as ingestion_mod + + request = MagicMock() + request.id = "task-struct-2" + persist_calls = [] + run_structured_kwargs = [] + + args = { + "data": [_full_triple()], + "brain_id": "tenant-a", + "text": "Alice bought a widget", + "mode": "hybrid", + } + + scout = MagicMock() + scout.run_structured.return_value = ScoutAgentResponse(entities=[]) + architect = MagicMock() + architect.session_id = "sess-enrich" + architect.take_pending_relationships.return_value = [] + architect.run_structured.side_effect = ( + lambda **kwargs: run_structured_kwargs.append(kwargs) + or ArchitectAgentResponse(new_nodes=[], relationships=[]) + ) + + with ( + patch.object(ingestion_mod, "cache_adapter"), + patch.object(ingestion_mod, "graph_adapter"), + patch.object(ingestion_mod, "IngestionManager"), + patch.object(ingestion_mod, "data_adapter"), + patch.object(ingestion_mod, "ScoutAgent", return_value=scout), + patch.object(ingestion_mod, "ArchitectAgent", return_value=architect), + patch("celery.chain", _immediate_chain_factory(persist_calls)), + patch.object(ingestion_mod, "set_ingestion_task_status"), + patch.object(ingestion_mod, "finalize_ingestion_task"), + ): + ingestion_mod.ingest_structured_data.run.__func__( + type("Bound", (), {"request": request})(), + args, + ) + + self.assertEqual(len(persist_calls), 1) + self.assertEqual(len(run_structured_kwargs), 1) + self.assertFalse(run_structured_kwargs[0]["persist_submitted"]) + for rel in persist_calls[0]["relationships"]: + self.assertEqual(rel["properties"].get("source"), "structured_deterministic") + + def test_resolved_mode_inference(self): + from src.services.api.constants.requests import ( + IngestionStructuredRequestBody, + IngestionTripleSet, + ) + + triple = IngestionTripleSet(**_full_triple()) + self.assertEqual( + IngestionStructuredRequestBody(data=[triple]).resolved_mode(), + "deterministic", + ) + self.assertEqual( + IngestionStructuredRequestBody( + data=[triple], text="x" + ).resolved_mode(), + "hybrid", + ) + self.assertEqual( + IngestionStructuredRequestBody( + data=[triple], text="x", mode="deterministic" + ).resolved_mode(), + "deterministic", + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_synergy_metrics.py b/tests/test_synergy_metrics.py new file mode 100644 index 0000000000000000000000000000000000000000..430c42dc267c406a14819d1538169537cf598169 --- /dev/null +++ b/tests/test_synergy_metrics.py @@ -0,0 +1,74 @@ +import os +import unittest + +ENV_DEFAULTS = { + "BRAINPAT_TOKEN": "test-token", + "MODELS_MODE": "local", + "EMBEDDINGS_LOCAL_MODEL": "local-model", + "EMBEDDINGS_SMALL_MODEL": "small-model", + "EMBEDDING_NODES_DIMENSION": "3", + "EMBEDDING_TRIPLETS_DIMENSION": "3", + "EMBEDDING_OBSERVATIONS_DIMENSION": "3", + "EMBEDDING_DATA_DIMENSION": "3", + "EMBEDDING_RELATIONSHIPS_DIMENSION": "3", + "REDIS_HOST": "localhost", + "REDIS_PORT": "6379", + "NEO4J_HOST": "localhost", + "NEO4J_PORT": "7687", + "NEO4J_USERNAME": "neo4j", + "NEO4J_PASSWORD": "password", + "MILVUS_HOST": "localhost", + "MILVUS_PORT": "19530", + "MONGO_CONNECTION_STRING": "mongodb://localhost:27017", + "CELERY_WORKER_CONCURRENCY": "1", + "OLLAMA_HOST": "localhost", + "OLLAMA_PORT": "11434", + "OLLAMA_LLM_SMALL_MODEL": "small", + "OLLAMA_LLM_LARGE_MODEL": "large", +} +for key, value in ENV_DEFAULTS.items(): + os.environ.setdefault(key, value) + + +class SynergyMetricsTests(unittest.TestCase): + def test_intra_list_diversity_and_coverage(self): + from src.core.search.synergy_metrics import ( + intra_list_diversity, + popularity_stratum, + summarize_recommendation_list, + type_coverage, + unexpectedness_by_graph_distance, + ) + + labels = [["PRODUCT", "PHONE"], ["PRODUCT", "CASE"], ["CATEGORY"]] + self.assertGreater(intra_list_diversity(labels), 0.0) + self.assertEqual(type_coverage(labels), 4) + self.assertGreater(unexpectedness_by_graph_distance([1, 2, 4]), 0.0) + self.assertEqual(popularity_stratum(50), "head") + self.assertEqual(popularity_stratum(1), "tail") + summary = summarize_recommendation_list(labels, [1, 2, 3]) + self.assertEqual(summary["n"], 3) + self.assertIn("intra_list_diversity", summary) + + +class RecommendMmrTests(unittest.TestCase): + def test_mmr_prefers_type_diversity(self): + from src.constants.kg import Node + from src.core.search.recommend import _mmr_diversify + + a = Node.model_construct(uuid="a", name="A", labels=["PHONE"], properties={}) + b = Node.model_construct(uuid="b", name="B", labels=["PHONE"], properties={}) + c = Node.model_construct(uuid="c", name="C", labels=["CASE"], properties={}) + items = [ + (a, 1.0, [], "synergy"), + (b, 0.99, [], "synergy"), + (c, 0.5, [], "synergy"), + ] + out = _mmr_diversify(items, top_k=2, lambda_mult=0.5) + self.assertEqual(len(out), 2) + self.assertEqual(out[0][0].uuid, "a") + self.assertEqual(out[1][0].uuid, "c") + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_topic_hyperedges.py b/tests/test_topic_hyperedges.py new file mode 100644 index 0000000000000000000000000000000000000000..b2ec7085af35caec737caef8bf65cd3fe9a35bc9 --- /dev/null +++ b/tests/test_topic_hyperedges.py @@ -0,0 +1,58 @@ +from __future__ import annotations + +import unittest + +from src.core.saving.topic_hyperedges import ( + cluster_sessions_into_topics, + lexical_topic_score, + rrf_fuse, + select_topics_and_sessions, + session_entity_sets, +) + + +class TopicHyperedgesTests(unittest.TestCase): + def test_session_entity_sets_and_cluster(self) -> None: + rows = [ + ("session_1", "e1", "pottery"), + ("session_1", "e2", "Melanie"), + ("session_2", "e3", "pottery workshop"), + ("session_2", "e2", "Melanie"), + ("session_3", "e4", "counseling"), + ("session_3", "e5", "Caroline"), + ] + entities = session_entity_sets(rows) + topics = cluster_sessions_into_topics(entities, merge_threshold=0.2) + self.assertGreaterEqual(len(topics), 2) + session_ids = {t.session_id for t in topics} + self.assertEqual(session_ids, {"session_1", "session_2", "session_3"}) + coverage = len(session_ids) / 3 + self.assertGreaterEqual(coverage, 0.8) + + def test_select_topics_boosts_query_overlap(self) -> None: + from src.core.saving.topic_hyperedges import TopicSession + + memberships = [ + TopicSession("topic:a", "pottery camping", "session_1"), + TopicSession("topic:a", "pottery camping", "session_2"), + TopicSession("topic:b", "counseling career", "session_3"), + ] + topics, sessions = select_topics_and_sessions( + "What pottery workshops did Melanie attend?", + memberships, + seed_sessions=["session_1"], + k_topics=2, + k_sessions=5, + ) + self.assertTrue(topics) + self.assertIn("session_1", sessions) + self.assertGreater(lexical_topic_score("pottery", "pottery camping"), 0.0) + + def test_rrf_fuse(self) -> None: + fused = rrf_fuse([["a", "b", "c"], ["b", "a", "d"]]) + self.assertEqual(fused[0], "a") + self.assertIn("b", fused[:2]) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_tracing_runtime_monitor.py b/tests/test_tracing_runtime_monitor.py new file mode 100644 index 0000000000000000000000000000000000000000..6934c2ff6a4a954ebfb4d37021790e6a6f730b0d --- /dev/null +++ b/tests/test_tracing_runtime_monitor.py @@ -0,0 +1,88 @@ +import asyncio +import unittest +from unittest.mock import patch + +from src.lib.tracing import LocalTraceQueue, TraceEventType, TraceTracker +from src.lib.tracing import runtime as tracing_runtime +from src.lib.tracing.runtime import HealthProbe, RuntimeMonitor + + +class RuntimeMonitorTests(unittest.TestCase): + def setUp(self): + self.original_tracer = tracing_runtime.tracer + self.tracker = TraceTracker(queue=LocalTraceQueue(), enabled=True) + tracing_runtime.tracer = self.tracker + + def tearDown(self): + tracing_runtime.tracer = self.original_tracer + tracing_runtime._monitors.clear() + + def test_runtime_monitor_records_heartbeat_resources_and_shutdown(self): + monitor = RuntimeMonitor( + service_name="test-service", + probes=[], + resource_sampler=lambda: {"rss_kb": 12}, + ) + + monitor.heartbeat() + monitor.sample_resources() + monitor.stop() + + events = self.tracker.queue.drain() + self.assertEqual(events[0].event_type, TraceEventType.HEARTBEAT) + self.assertEqual(events[1].event_type, TraceEventType.RESOURCE_SAMPLE) + self.assertEqual(events[1].metadata["rss_kb"], 12) + self.assertEqual(events[2].event_type, TraceEventType.PROCESS) + self.assertEqual(events[2].name, "process.stopped") + + def test_health_check_records_success_and_downtime(self): + monitor = RuntimeMonitor( + service_name="test-service", + probes=[ + HealthProbe("up", "127.0.0.1", 1), + HealthProbe("down", "127.0.0.1", 2), + ], + ) + + with patch.object(tracing_runtime, "probe_tcp", side_effect=[True, False]): + monitor.check_health() + + events = self.tracker.queue.drain() + self.assertEqual(events[0].event_type, TraceEventType.HEALTH_CHECK) + self.assertEqual(events[0].name, "health.up") + self.assertEqual(events[1].event_type, TraceEventType.DOWNTIME) + self.assertEqual(events[1].name, "health.down.down") + + def test_unhandled_exception_hook_records_exception(self): + monitor = RuntimeMonitor(service_name="test-service", probes=[]) + exc = RuntimeError("boom") + + monitor.record_unhandled_exception(exc, exc.__traceback__, "unit-test") + + events = self.tracker.queue.drain() + self.assertEqual(len(events), 1) + self.assertEqual(events[0].event_type, TraceEventType.EXCEPTION) + self.assertEqual(events[0].severity.value, "critical") + self.assertEqual(events[0].operation, "unit-test") + + def test_asyncio_exception_handler_records_context(self): + async def run_test(): + loop = asyncio.get_running_loop() + previous_handler = loop.get_exception_handler() + loop.set_exception_handler(lambda _loop, _context: None) + monitor = RuntimeMonitor(service_name="test-service", probes=[]) + monitor.install_asyncio_exception_handler(loop) + try: + loop.call_exception_handler({"message": "async problem"}) + finally: + loop.set_exception_handler(previous_handler) + + asyncio.run(run_test()) + + events = self.tracker.queue.drain() + self.assertEqual(events[0].event_type, TraceEventType.ERROR) + self.assertEqual(events[0].name, "runtime.asyncio.unhandled_error") + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_tracing_sla_tracker.py b/tests/test_tracing_sla_tracker.py new file mode 100644 index 0000000000000000000000000000000000000000..612ef5c6a156598da1153f9d258766a9dff61bc1 --- /dev/null +++ b/tests/test_tracing_sla_tracker.py @@ -0,0 +1,167 @@ +import asyncio +from unittest.mock import patch +import unittest + +from starlette.applications import Starlette +from starlette.responses import JSONResponse, PlainTextResponse +from starlette.middleware import Middleware +from starlette.routing import Route +from starlette.testclient import TestClient + +from src.lib.tracing import ( + LocalTraceQueue, + TraceEvent, + TraceEventType, + TraceSeverity, + TraceSubscriber, + TraceTracker, +) +from src.core.agents.core.invoke_loop import run_invoke_loop +from src.lib.tracing.middleware import TraceMiddleware +from src.lib.tracing import middleware as tracing_middleware +from src.lib.tracing import workers as tracing_workers + + +class MemorySubscriber(TraceSubscriber): + def __init__(self): + self.events = [] + + def handle(self, event: TraceEvent) -> None: + self.events.append(event) + + +class TraceTrackerTests(unittest.TestCase): + def test_publish_records_event_with_tenant_and_trace_context(self): + tracker = TraceTracker(queue=LocalTraceQueue(), enabled=True) + tokens = tracker.set_context(trace_id="trace-1", tenant_id="tenant-1") + try: + event = tracker.error( + "operation.failed", + service="api", + operation="GET /demo", + message="failed", + ) + finally: + tracker.reset_context(tokens) + + self.assertIsNotNone(event) + drained = tracker.queue.drain() + self.assertEqual(len(drained), 1) + self.assertEqual(drained[0].event_type, TraceEventType.ERROR) + self.assertEqual(drained[0].severity, TraceSeverity.ERROR) + self.assertEqual(drained[0].tenant_id, "tenant-1") + self.assertEqual(drained[0].trace_id, "trace-1") + self.assertEqual(drained[0].service, "api") + self.assertEqual(drained[0].operation, "GET /demo") + + def test_span_records_exception_and_sla_breach(self): + tracker = TraceTracker( + queue=LocalTraceQueue(), + enabled=True, + slow_operation_ms=0, + ) + + with self.assertRaises(ValueError): + with tracker.span("failing-span", tenant_id="brain1"): + raise ValueError("boom") + + events = tracker.queue.drain() + self.assertEqual([event.event_type for event in events], [ + TraceEventType.EXCEPTION, + TraceEventType.SLA_BREACH, + ]) + self.assertEqual(events[0].error_type, "ValueError") + self.assertIn("boom", events[0].message) + self.assertEqual(events[0].tenant_id, "brain1") + self.assertIsNotNone(events[0].stack_trace) + + def test_track_loop_records_expensive_loop_when_threshold_is_met(self): + tracker = TraceTracker( + queue=LocalTraceQueue(), + enabled=True, + expensive_loop_iterations=3, + ) + + self.assertEqual(list(tracker.track_loop("loop", range(3))), [0, 1, 2]) + + events = tracker.queue.drain() + self.assertEqual(len(events), 1) + self.assertEqual(events[0].event_type, TraceEventType.EXPENSIVE_LOOP) + self.assertEqual(events[0].metadata["iterations"], 3) + + def test_dispatch_once_sends_event_to_subscriber(self): + async def run_test(): + tracker = TraceTracker(queue=LocalTraceQueue(), enabled=True) + subscriber = MemorySubscriber() + tracker.subscribe(subscriber) + event = tracker.error("error") + + dispatched = await tracker.dispatch_once() + + self.assertIs(dispatched, event) + self.assertEqual(subscriber.events, [event]) + + asyncio.run(run_test()) + + +class TraceMiddlewareTests(unittest.TestCase): + def setUp(self): + self.original_tracer = tracing_middleware.tracer + self.tracker = TraceTracker(queue=LocalTraceQueue(), enabled=True) + tracing_middleware.tracer = self.tracker + + def tearDown(self): + tracing_middleware.tracer = self.original_tracer + + def test_middleware_records_server_errors_and_tenant_id(self): + async def error_route(_request): + return JSONResponse({"detail": "bad"}, status_code=503) + + app = Starlette( + routes=[Route("/bad", error_route)], + middleware=[ + Middleware( + TraceMiddleware, + service_name="test-api", + slow_request_ms=100000, + ) + ], + ) + + response = TestClient(app).get("/bad", headers={"X-Brain-ID": "tenant-a"}) + + self.assertEqual(response.status_code, 503) + events = self.tracker.queue.drain() + self.assertEqual(len(events), 1) + self.assertEqual(events[0].event_type, TraceEventType.ERROR) + self.assertEqual(events[0].tenant_id, "tenant-a") + self.assertEqual(events[0].service, "test-api") + self.assertEqual(events[0].status_code, 503) + + def test_middleware_records_sla_breach(self): + async def ok_route(_request): + return PlainTextResponse("ok") + + app = Starlette( + routes=[Route("/ok", ok_route)], + middleware=[ + Middleware( + TraceMiddleware, + service_name="test-api", + slow_request_ms=0, + ) + ], + ) + + response = TestClient(app).get("/ok?brain_id=tenant-b") + + self.assertEqual(response.status_code, 200) + events = self.tracker.queue.drain() + self.assertEqual(len(events), 1) + self.assertEqual(events[0].event_type, TraceEventType.SLA_BREACH) + self.assertEqual(events[0].tenant_id, "tenant-b") + self.assertEqual(events[0].operation, "GET /ok") + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_tracing_workers_and_agents.py b/tests/test_tracing_workers_and_agents.py new file mode 100644 index 0000000000000000000000000000000000000000..4913fa89611103489f4af8c51a4c21bc62033a17 --- /dev/null +++ b/tests/test_tracing_workers_and_agents.py @@ -0,0 +1,140 @@ +import os +import unittest +from unittest.mock import patch + +from src.lib.tracing import LocalTraceQueue, TraceEventType, TraceTracker +from src.lib.tracing import workers as tracing_workers + + +class WorkerTracingTests(unittest.TestCase): + def setUp(self): + self.original_tracer = tracing_workers.tracer + self.tracker = TraceTracker(queue=LocalTraceQueue(), enabled=True) + tracing_workers.tracer = self.tracker + + def tearDown(self): + tracing_workers.tracer = self.original_tracer + tracing_workers._task_starts.clear() + tracing_workers._task_context_tokens.clear() + + def test_worker_signals_record_start_sla_and_failure(self): + tracing_workers._trace_task_prerun( + sender=type("Task", (), {"name": "demo.task"})(), + task_id="task-1", + args=({"brain_id": "tenant-1"},), + kwargs={}, + ) + tracing_workers._task_starts["task-1"] -= 2 + tracing_workers._trace_task_failure( + sender=type("Task", (), {"name": "demo.task"})(), + task_id="task-1", + exception=RuntimeError("boom"), + args=({"brain_id": "tenant-1"},), + kwargs={}, + ) + tracing_workers._trace_task_postrun( + sender=type("Task", (), {"name": "demo.task"})(), + task_id="task-1", + args=({"brain_id": "tenant-1"},), + kwargs={}, + state="FAILURE", + ) + + events = self.tracker.queue.drain() + self.assertEqual(events[0].event_type, TraceEventType.LATENCY) + self.assertEqual(events[0].tenant_id, "tenant-1") + self.assertEqual(events[1].event_type, TraceEventType.EXCEPTION) + self.assertEqual(events[1].error_type, "RuntimeError") + self.assertEqual(events[2].event_type, TraceEventType.SLA_BREACH) + self.assertEqual(events[2].operation, "demo.task") + + +class AgentLoopTracingTests(unittest.TestCase): + def test_agent_loop_records_model_exception(self): + from src.core.agents.core import invoke_loop + + tracker = TraceTracker(queue=LocalTraceQueue(), enabled=True) + + class FailingModel: + def invoke(self, *_args, **_kwargs): + raise RuntimeError("model down") + + class Agent: + class Tool: + name = "demo_tool" + description = "Demo tool" + args_schema = {} + + tools = [Tool()] + output_schema = None + model = FailingModel() + thinking = False + _tools_bound = False + system_prompt = "system" + debug = False + + def _model_requires_thought_signatures(self): + return False + + with patch.object(invoke_loop, "tracer", tracker): + with self.assertRaises(RuntimeError): + invoke_loop.run_invoke_loop( + Agent(), + [{"role": "user", "content": "hello"}], + {"metadata": {"brain_id": "tenant-2", "agent": "test-agent"}}, + ) + + events = tracker.queue.drain() + event_types = [event.event_type for event in events] + self.assertIn(TraceEventType.EXCEPTION, event_types) + self.assertTrue( + any(event.name == "agent.model.invoke.failed" for event in events) + ) + self.assertTrue(any(event.name == "agent.invoke_loop" for event in events)) + self.assertTrue(all( + event.tenant_id == "tenant-2" for event in events if event.tenant_id + )) + + def test_agent_loop_records_outer_loop_threshold(self): + from src.core.agents.core import invoke_loop + + tracker = TraceTracker(queue=LocalTraceQueue(), enabled=True) + + class SimpleModel: + def invoke(self, *_args, **_kwargs): + return {"content": "done"} + + class Agent: + tools = [] + output_schema = None + model = SimpleModel() + thinking = False + _tools_bound = False + system_prompt = "system" + debug = False + _get_effective_output_schema = None + + def _model_requires_thought_signatures(self): + return False + + with patch.object(invoke_loop, "tracer", tracker): + with patch.dict(os.environ, {"TRACE_AGENT_LOOP_ITERATIONS": "1"}): + result = invoke_loop.run_invoke_loop( + Agent(), + [{"role": "user", "content": "hello"}], + {"metadata": {"brain_id": "tenant-3", "agent": "test-agent"}}, + ) + + self.assertGreaterEqual(len(result["messages"]), 1) + events = tracker.queue.drain() + self.assertTrue( + any( + event.event_type == TraceEventType.EXPENSIVE_LOOP + and event.name == "agent.invoke_loop.outer_loop" + for event in events + ) + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_vector_similarity_contract.py b/tests/test_vector_similarity_contract.py new file mode 100644 index 0000000000000000000000000000000000000000..05595831c2ac6deb46964a08cc44111df833586f --- /dev/null +++ b/tests/test_vector_similarity_contract.py @@ -0,0 +1,118 @@ +import os +import unittest +from types import SimpleNamespace +from unittest.mock import MagicMock + + +ENV_DEFAULTS = { + "BRAINPAT_TOKEN": "test-token", + "MODELS_MODE": "local", + "EMBEDDINGS_LOCAL_MODEL": "local-model", + "EMBEDDINGS_SMALL_MODEL": "small-model", + "EMBEDDING_NODES_DIMENSION": "3", + "EMBEDDING_TRIPLETS_DIMENSION": "3", + "EMBEDDING_OBSERVATIONS_DIMENSION": "3", + "EMBEDDING_DATA_DIMENSION": "3", + "EMBEDDING_RELATIONSHIPS_DIMENSION": "3", + "REDIS_HOST": "localhost", + "REDIS_PORT": "6379", + "NEO4J_HOST": "localhost", + "NEO4J_PORT": "7687", + "NEO4J_USERNAME": "neo4j", + "NEO4J_PASSWORD": "password", + "MILVUS_HOST": "localhost", + "MILVUS_PORT": "19530", + "MONGO_CONNECTION_STRING": "mongodb://localhost:27017", + "CELERY_WORKER_CONCURRENCY": "1", + "OLLAMA_HOST": "localhost", + "OLLAMA_PORT": "11434", + "OLLAMA_LLM_SMALL_MODEL": "small", + "OLLAMA_LLM_LARGE_MODEL": "large", + "PIPELINE_MODE": "accurate", +} +for key, value in ENV_DEFAULTS.items(): + os.environ.setdefault(key, value) + + +class VectorSimilarityContractTests(unittest.TestCase): + def test_adapter_sorts_nearest_first_by_cosine_distance(self): + from src.adapters.embeddings import VectorStoreAdapter + from src.constants.embeddings import Vector + + store = MagicMock() + store.search_vectors.return_value = [ + Vector(id="far", metadata={}, distance=0.8), + Vector(id="near", metadata={}, distance=0.05), + Vector(id="mid", metadata={}, distance=0.3), + ] + adapter = VectorStoreAdapter() + adapter.add_client(store) + + results = adapter.search_vectors([1.0, 0.0, 0.0], brain_id="b", store="nodes", k=3) + self.assertEqual([v.id for v in results], ["near", "mid", "far"]) + + def test_adapter_equal_distance_tiebreaks_by_uuid(self): + from src.adapters.embeddings import VectorStoreAdapter + from src.constants.embeddings import Vector + + store = MagicMock() + store.search_vectors.return_value = [ + Vector(id="2", metadata={"uuid": "z"}, distance=0.1), + Vector(id="1", metadata={"uuid": "a"}, distance=0.1), + ] + adapter = VectorStoreAdapter() + adapter.add_client(store) + results = adapter.search_vectors([1.0], brain_id="b", store="nodes", k=2) + self.assertEqual([v.metadata["uuid"] for v in results], ["a", "z"]) + + def test_milvus_converts_similarity_score_to_cosine_distance(self): + from src.constants.embeddings import Vector + from src.lib.milvus.client import MilvusClient + + client = MilvusClient.__new__(MilvusClient) + milvus = MagicMock() + milvus.search.return_value = [ + [ + { + "id": 1, + "distance": 0.95, + "entity": {"uuid": "near"}, + }, + { + "id": 2, + "distance": 0.2, + "entity": {"uuid": "far"}, + }, + ] + ] + client._get_client = MagicMock(return_value=milvus) + client._ensure_store = MagicMock() + + results = MilvusClient.search_vectors( + client, [1.0, 0.0, 0.0], "brain-a", "nodes", k=2 + ) + self.assertEqual(results[0].id, "1") + self.assertAlmostEqual(results[0].distance, 0.05) + self.assertEqual(results[1].id, "2") + self.assertAlmostEqual(results[1].distance, 0.8) + self.assertTrue(isinstance(results[0], Vector)) + + def test_relationship_dedup_skips_self_and_uses_distance_threshold(self): + from src.workers.tasks.ingestion import ( + nearest_existing_vector, + should_dedup_relationship, + ) + + self_hit = SimpleNamespace(id="v-self", distance=0.0, metadata={"uuid": "self"}) + near = SimpleNamespace(id="v-near", distance=0.05, metadata={"uuid": "near"}) + far = SimpleNamespace(id="v-far", distance=0.5, metadata={"uuid": "far"}) + + nearest = nearest_existing_vector([self_hit, near, far], "v-self") + self.assertEqual(nearest.id, "v-near") + self.assertTrue(should_dedup_relationship(nearest)) + self.assertFalse(should_dedup_relationship(far)) + self.assertIsNone(nearest_existing_vector([self_hit], "v-self")) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_workspace_facade.py b/tests/test_workspace_facade.py new file mode 100644 index 0000000000000000000000000000000000000000..b949c1923f19b2da41c3d2c4c480600188132643 --- /dev/null +++ b/tests/test_workspace_facade.py @@ -0,0 +1,265 @@ +import json + +from fastapi import FastAPI +from fastapi.testclient import TestClient + +from src.constants.data import Brain, Workspace +from src.services.api.middlewares.auth import BrainPATMiddleware +from src.services.api.middlewares.brains import BrainMiddleware +from src.services.api.routes.workspace_api import workspace_api_router + + +class FacadeData: + def __init__(self): + self.brains = { + "alpha": Brain(name_key="alpha", pat="alpha-secret"), + "beta": Brain(name_key="beta", pat="beta-secret"), + } + self.workspaces = { + "alpha": Workspace(brain_id="alpha", slug="alpha", display_name="Alpha"), + "beta": Workspace(brain_id="beta", slug="beta", display_name="Beta"), + "archived": Workspace( + brain_id="alpha", + slug="archived", + display_name="Archived", + archived=True, + ), + } + + def get_workspace(self, slug): + return self.workspaces.get(slug) + + def get_brain(self, name_key=None, **_kwargs): + return self.brains.get(name_key) + + def get_brain_by_pat(self, pat): + return next((brain for brain in self.brains.values() if brain.pat == pat), None) + + +class FacadeCache: + def __init__(self): + self.tasks = { + ("alpha", "alpha-task"): json.dumps( + { + "status": "completed", + "result": "ALPHA_ONLY_FACT", + "brain_id": "alpha", + } + ), + ("beta", "beta-task"): json.dumps( + { + "status": "completed", + "result": "BETA_ONLY_FACT", + "brain_id": "beta", + } + ), + # Simulate a corrupt/migrated record placed in the wrong Redis + # namespace. The persisted scope must still prevent disclosure. + ("beta", "misfiled-alpha-task"): json.dumps( + { + "status": "completed", + "result": "ALPHA_MISFILED_FACT", + "brain_id": "alpha", + } + ), + } + + def get(self, key, brain_id="default"): + if brain_id == "system" and key.startswith("brain:"): + return key.removeprefix("brain:") + if brain_id == "system" and key.startswith("brainpat:"): + target = key.removeprefix("brainpat:") + return f"{target}-secret" + return None + + def get_task(self, task_id, brain_id="default"): + return self.tasks.get((brain_id, task_id)) + + def get_task_keys(self, brain_id="default"): + return [f"{brain_id}:task:{task}" for scope, task in self.tasks if scope == brain_id] + + +def facade_client(monkeypatch): + from src.services.api.middlewares import auth, brains + from src.services.api.routes import ingest, meta, tasks, workspace_api + + data = FacadeData() + cache = FacadeCache() + monkeypatch.setenv("BRAINPAT_TOKEN", "system-secret") + monkeypatch.setattr(brains, "data_adapter", data) + monkeypatch.setattr(brains, "cache_adapter", cache) + monkeypatch.setattr(auth, "data_adapter", data) + monkeypatch.setattr(auth, "cache_adapter", cache) + monkeypatch.setattr(workspace_api, "data_adapter", data) + monkeypatch.setattr(tasks, "cache_adapter", cache) + + async def labels(brain_id): + return [f"{brain_id.upper()}_ONLY_FACT"] + + monkeypatch.setattr(workspace_api, "get_entities_labels_controller", labels) + monkeypatch.setattr(meta, "get_entities_labels_controller", labels) + app = FastAPI() + app.add_middleware(BrainPATMiddleware) + app.add_middleware(BrainMiddleware) + app.include_router(meta.meta_router) + app.include_router(workspace_api_router) + return TestClient(app), ingest + + +def test_hosted_scope_authorization_and_failures(monkeypatch): + client, _ = facade_client(monkeypatch) + system = {"Authorization": "Bearer system-secret"} + + discovery = client.get("/brains/alpha/api", headers=system) + assert discovery.status_code == 200 + assert discovery.json()["links"] == { + "native_api": "http://testserver/brains/alpha/api", + "native_openapi": "http://testserver/brains/alpha/api/openapi.json", + "openapi": "http://testserver/brains/alpha/api/openapi.json", + "console": "http://testserver/console/w/alpha/", + "agent_api": "http://testserver/brains/alpha/agent", + "agent_openapi": "http://testserver/brains/alpha/agent/openapi.json", + "mcp": "http://testserver/brains/alpha/mcp", + } + assert client.get("/brains/beta/api", headers=system).status_code == 200 + assert client.get( + "/brains/alpha/api", headers={"Authorization": "Bearer alpha-secret"} + ).status_code == 200 + assert client.get( + "/brains/beta/api", headers={"Authorization": "Bearer alpha-secret"} + ).status_code == 403 + assert client.get( + "/brains/beta/api", headers={"Authorization": "Bearer invalid-secret"} + ).status_code == 401 + assert client.get( + "/brains/alpha/api", + headers={**system, "X-Brain-ID": "beta"}, + ).status_code == 409 + query_conflict = client.get( + "/brains/alpha/api/meta/entity-labels?brain_id=beta", headers=system + ) + assert query_conflict.status_code == 409 + assert query_conflict.json()["error"]["code"] == "BRAIN_SCOPE_CONFLICT" + assert client.get("/brains/archived/api", headers=system).status_code == 410 + assert client.get("/brains/missing/api", headers=system).status_code == 404 + + +def test_hosted_data_and_task_isolation(monkeypatch): + client, _ = facade_client(monkeypatch) + headers = {"Authorization": "Bearer system-secret"} + + alpha = client.get("/brains/alpha/api/meta/entity-labels", headers=headers) + beta = client.get("/brains/beta/api/meta/entity-labels", headers=headers) + assert alpha.json() == ["ALPHA_ONLY_FACT"] + assert beta.json() == ["BETA_ONLY_FACT"] + + own_task = client.get("/brains/alpha/api/tasks/alpha-task", headers=headers) + cross_task = client.get("/brains/beta/api/tasks/alpha-task", headers=headers) + misfiled_task = client.get( + "/brains/beta/api/tasks/misfiled-alpha-task", headers=headers + ) + beta_tasks = client.get("/brains/beta/api/tasks/", headers=headers) + assert own_task.status_code == 200 + assert own_task.json()["result"] == "ALPHA_ONLY_FACT" + assert own_task.json()["brain_id"] == "alpha" + assert cross_task.status_code == 404 + assert misfiled_task.status_code == 404 + assert beta_tasks.status_code == 200 + assert [task["id"] for task in beta_tasks.json()["tasks"]] == ["beta-task"] + + +def test_hosted_ingest_uses_canonical_body_scope(monkeypatch): + client, ingest = facade_client(monkeypatch) + headers = {"Authorization": "Bearer system-secret"} + queued = [] + statuses = [] + monkeypatch.setattr( + ingest.ingest_data_task, + "apply_async", + lambda *, args, task_id: queued.append((args[0], task_id)), + ) + monkeypatch.setattr( + ingest, + "set_ingestion_task_status", + lambda task_id, brain_id, status, **kwargs: statuses.append( + (task_id, brain_id, status, kwargs) + ), + ) + + response = client.post( + "/brains/alpha/api/ingest/", + headers=headers, + json={"data": {"data_type": "text", "text_data": "ALPHA_ONLY_FACT"}}, + ) + assert response.status_code == 202 + assert queued[0][0]["brain_id"] == "alpha" + assert statuses[0][1:3] == ("alpha", "queued") + + conflict = client.post( + "/brains/alpha/api/ingest/", + headers=headers, + json={ + "brain_id": "beta", + "data": {"data_type": "text", "text_data": "BETA_ONLY_FACT"}, + }, + ) + assert conflict.status_code == 409 + assert conflict.json()["error"]["code"] == "BRAIN_SCOPE_CONFLICT" + assert len(queued) == 1 + + +def test_legacy_header_scope_remains_supported(monkeypatch): + client, _ = facade_client(monkeypatch) + response = client.get( + "/meta/entity-labels", + headers={ + "Authorization": "Bearer system-secret", + "X-Brain-ID": "beta", + }, + ) + assert response.status_code == 200 + assert response.json() == ["BETA_ONLY_FACT"] + + +def test_hosted_ai_neighbors_uses_canonical_workspace_scope(monkeypatch): + from src.constants.kg import Node + from src.services.api.constants.requests import RetrieveNeighborsRequestResponse + from src.services.api.routes import retrieve + + client, _ = facade_client(monkeypatch) + calls = [] + + async def ai_neighbors(identification_params, looking_for, limit, brain_id): + calls.append(brain_id) + return RetrieveNeighborsRequestResponse( + main_node=Node(labels=["Workspace"], name=brain_id), + neighbors=[], + ) + + monkeypatch.setattr( + retrieve, "retrieve_neighbors_ai_mode_controller", ai_neighbors + ) + headers = {"Authorization": "Bearer system-secret"} + body = { + "identification_params": {"name": "customer", "entity_types": []}, + "looking_for": ["preferences"], + "limit": 5, + } + + alpha = client.post( + "/brains/alpha/api/retrieve/entities/neighbors/ai-mode", + headers=headers, + json=body, + ) + beta = client.post( + "/brains/beta/api/retrieve/entities/neighbors/ai-mode", + headers=headers, + json=body, + ) + + assert alpha.status_code == 200 + assert beta.status_code == 200 + assert alpha.json()["main_node"]["name"] == "alpha" + assert beta.json()["main_node"]["name"] == "beta" + assert calls == ["alpha", "beta"] + assert "default" not in calls diff --git a/tests/test_workspace_mcp.py b/tests/test_workspace_mcp.py new file mode 100644 index 0000000000000000000000000000000000000000..7fd5c71592b3108176ec154c95f39d964aca4876 --- /dev/null +++ b/tests/test_workspace_mcp.py @@ -0,0 +1,210 @@ +import asyncio +import json + +import pytest + +from src.services.brain_context import BrainContext, brain_context_var + + +def test_workspace_mcp_tools_are_registered(): + from src.services.mcp.main import mcp, workspace_mcp + + names = set(workspace_mcp._tool_manager._tools) + assert { + "memory_search", + "memory_context", + "memory_store", + "memory_observe", + "memory_neighbors", + "memory_entity", + "memory_task_status", + } <= names + assert "search_memory" not in names + assert "traverse_graph" not in names + assert "list_brains" not in names + assert "memory_search" not in set(mcp._tool_manager._tools) + + +def test_workspace_mcp_semantic_tools_receive_canonical_scope(monkeypatch): + from src.services.mcp import main + + calls = [] + + async def search(query, limit, brain_id): + calls.append(("search", brain_id)) + return [{"id": brain_id}] + + async def context(query, max_tokens, brain_id): + calls.append(("context", brain_id)) + return {"context": query, "sources": []} + + async def store(**kwargs): + calls.append(("store", kwargs["brain_id"])) + return {"task_id": "alpha-task"} + + async def task(task_id, brain_id): + calls.append(("task", brain_id)) + if task_id != f"{brain_id}-task": + from fastapi import HTTPException + + raise HTTPException(status_code=404, detail="Task not found") + return { + "task_id": task_id, + "status": "queued", + "brain_id": brain_id, + "workspace": "untrusted-task-payload", + } + + monkeypatch.setattr(main, "agent_memory_search", search) + monkeypatch.setattr(main, "agent_memory_context", context) + monkeypatch.setattr(main, "agent_memory_store", store) + monkeypatch.setattr(main, "agent_memory_task_status", task) + token = brain_context_var.set(BrainContext("alpha", "alpha", "brain")) + try: + assert asyncio.run(main.memory_search("q"))["results"][0]["id"] == "alpha" + assert asyncio.run(main.memory_context("q"))["workspace"] == "alpha" + assert asyncio.run(main.memory_store("fact"))["task_id"] == "alpha-task" + task_result = asyncio.run(main.memory_task_status("alpha-task")) + assert task_result["brain_id"] == "alpha" + assert task_result["workspace"] == "alpha" + finally: + brain_context_var.reset(token) + assert calls == [ + ("search", "alpha"), + ("context", "alpha"), + ("store", "alpha"), + ("task", "alpha"), + ] + + beta_token = brain_context_var.set(BrainContext("beta", "beta", "brain")) + try: + from fastapi import HTTPException + + with pytest.raises(HTTPException) as exc: + asyncio.run(main.memory_task_status("alpha-task")) + assert exc.value.status_code == 404 + finally: + brain_context_var.reset(beta_token) + + +def test_workspace_mcp_tools_reject_unscoped_direct_use(): + from src.services.mcp.main import memory_search + + with pytest.raises(ValueError, match="workspace MCP endpoint"): + asyncio.run(memory_search("q")) + + +def test_workspace_mcp_transport_resolves_path_and_enforces_pat(monkeypatch): + from src.services.mcp import app as mcp_app + from tests.test_workspace_facade import FacadeData + + data = FacadeData() + monkeypatch.setattr(mcp_app, "data_adapter", data) + + async def downstream(scope, _receive, send): + context = brain_context_var.get() + body = f"{scope['path']}:{context.brain_id}".encode() + await send({"type": "http.response.start", "status": 200, "headers": []}) + await send({"type": "http.response.body", "body": body}) + + async def invoke(path, pat): + messages = [] + middleware = mcp_app.AuthContextMiddleware(downstream) + await middleware( + { + "type": "http", + "method": "POST", + "path": path, + "headers": [(b"authorization", f"Bearer {pat}".encode())], + }, + lambda: None, + messages.append, + ) + return messages + + monkeypatch.setattr( + mcp_app, + "guard_brainpat", + lambda pat: True + if pat == "system-secret" + else ("alpha" if pat == "alpha-secret" else False), + ) + allowed = asyncio.run(invoke("/brains/alpha/mcp", "alpha-secret")) + assert allowed[0]["status"] == 200 + assert allowed[1]["body"] == b"/workspace/mcp:alpha" + forbidden = asyncio.run(invoke("/brains/beta/mcp", "alpha-secret")) + assert forbidden[0]["status"] == 403 + invalid = asyncio.run(invoke("/brains/alpha/mcp", "invalid")) + assert invalid[0]["status"] == 401 + archived = asyncio.run(invoke("/brains/archived/mcp", "system-secret")) + assert archived[0]["status"] == 410 + missing = asyncio.run(invoke("/brains/missing/mcp", "system-secret")) + assert missing[0]["status"] == 404 + + +def _mcp_response(response): + content_type = response.headers.get("content-type", "") + if "application/json" in content_type: + return response.json() + for line in response.text.splitlines(): + if line.startswith("data: "): + return json.loads(line.removeprefix("data: ")) + raise AssertionError(f"MCP response contained no JSON payload: {response.text}") + + +def test_workspace_mcp_real_transport_initialize_and_tool_list(monkeypatch): + """Exercise the mounted FastMCP ASGI app through its public workspace URL.""" + from starlette.testclient import TestClient + + from src.services.mcp import app as mcp_app + from tests.test_workspace_facade import FacadeData + + monkeypatch.setattr(mcp_app, "data_adapter", FacadeData()) + monkeypatch.setattr( + mcp_app, + "guard_brainpat", + lambda pat: "alpha" if pat == "alpha-secret" else False, + ) + headers = { + "Authorization": "Bearer alpha-secret", + "Content-Type": "application/json", + "Accept": "application/json, text/event-stream", + } + + with TestClient(mcp_app.app) as client: + initialized = client.post( + "/brains/alpha/mcp", + headers=headers, + json={ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "protocolVersion": "2025-03-26", + "capabilities": {}, + "clientInfo": {"name": "workspace-test", "version": "1"}, + }, + }, + ) + assert initialized.status_code == 200 + assert _mcp_response(initialized)["result"]["serverInfo"]["name"] == ( + "brainapi-workspace-memory" + ) + + listed = client.post( + "/brains/alpha/mcp", + headers=headers, + json={"jsonrpc": "2.0", "id": 2, "method": "tools/list", "params": {}}, + ) + assert listed.status_code == 200 + names = {tool["name"] for tool in _mcp_response(listed)["result"]["tools"]} + + assert { + "memory_search", + "memory_context", + "memory_store", + "memory_neighbors", + "memory_entity", + "memory_task_status", + } <= names + assert "list_brains" not in names diff --git a/tests/test_workspace_plugin_contract.py b/tests/test_workspace_plugin_contract.py new file mode 100644 index 0000000000000000000000000000000000000000..ca56e89584ebec2d1ad3b3781071894460d79c42 --- /dev/null +++ b/tests/test_workspace_plugin_contract.py @@ -0,0 +1,139 @@ +import pytest +from fastapi import APIRouter, Depends, FastAPI + +from src.core.plugins.context import PluginContext +from src.services.api.dependencies import get_brain_id + + +def context_for(app=None, mcp=None, workspace_mcp=None): + return PluginContext( + adapters=None, + prompts=None, + config=None, + app=app, + mcp=mcp, + workspace_mcp=workspace_mcp, + ) + + +def test_workspace_plugin_route_requires_explicit_brain_scope(): + app = FastAPI() + router = APIRouter() + + @router.get("/unsafe") + async def unsafe(): + return {} + + with pytest.raises(ValueError, match="must declare"): + context_for(app=app).register_workspace_route(router, capability="demo") + assert not any("/plugins/demo" in route.path for route in app.routes) + + +def test_workspace_plugin_route_registration_and_collision(): + app = FastAPI() + router = APIRouter() + + @router.get("/safe") + async def safe(brain_id: str = Depends(get_brain_id)): + return {"brain_id": brain_id} + + context = context_for(app=app) + context.register_workspace_route(router, capability="demo") + assert any( + route.path == "/brains/{workspace_slug}/api/plugins/demo/safe" + for route in app.routes + ) + with pytest.raises(ValueError, match="collision"): + context.register_workspace_route(router, capability="demo") + + +def test_workspace_plugin_route_receives_authoritative_brain(monkeypatch): + from fastapi.testclient import TestClient + from src.services.api.middlewares import auth, brains + from src.services.api.middlewares.auth import BrainPATMiddleware + from src.services.api.middlewares.brains import BrainMiddleware + from tests.test_workspace_facade import FacadeCache, FacadeData + + app = FastAPI() + router = APIRouter() + + @router.get("/scope") + async def scope(brain_id: str = Depends(get_brain_id)): + return {"brain_id": brain_id} + + context_for(app=app).register_workspace_route(router, capability="demo") + data, cache = FacadeData(), FacadeCache() + monkeypatch.setenv("BRAINPAT_TOKEN", "system-secret") + monkeypatch.setattr(brains, "data_adapter", data) + monkeypatch.setattr(brains, "cache_adapter", cache) + monkeypatch.setattr(auth, "data_adapter", data) + monkeypatch.setattr(auth, "cache_adapter", cache) + app.add_middleware(BrainPATMiddleware) + app.add_middleware(BrainMiddleware) + client = TestClient(app) + headers = {"Authorization": "Bearer system-secret"} + path = "/brains/alpha/api/plugins/demo/scope" + assert client.get(path, headers=headers).json() == {"brain_id": "alpha"} + assert client.get( + path, headers={**headers, "X-Brain-ID": "beta"} + ).status_code == 409 + + +class FakeMCP: + def __init__(self): + self.tools = {} + + def tool(self, name=None, **_kwargs): + def decorate(fn): + self.tools[name or fn.__name__] = fn + return fn + + return decorate + + +def test_workspace_mcp_tool_requires_and_receives_context(): + from src.services.brain_context import BrainContext, brain_context_var + + mcp = FakeMCP() + context = context_for(mcp=mcp) + + def unsafe(): + return None + + with pytest.raises(ValueError, match="brain_context"): + context.register_workspace_mcp_tool(unsafe) + + async def safe(value: str, brain_context): + return f"{brain_context.brain_id}:{value}" + + context.register_workspace_mcp_tool(safe, name="safe") + import inspect + + assert "brain_context" not in inspect.signature(mcp.tools["safe"]).parameters + token = brain_context_var.set(BrainContext("alpha", "alpha", "brain")) + try: + import asyncio + + assert asyncio.run(mcp.tools["safe"]("value")) == "alpha:value" + finally: + brain_context_var.reset(token) + + +def test_ordinary_mcp_plugin_is_not_exposed_to_workspace_registry(): + legacy_mcp = FakeMCP() + workspace_mcp = FakeMCP() + context = context_for(mcp=legacy_mcp, workspace_mcp=workspace_mcp) + + def ordinary(): + return "legacy" + + async def workspace_safe(brain_context): + return brain_context.brain_id + + context.register_mcp_tool(ordinary) + context.register_workspace_mcp_tool(workspace_safe) + + assert "ordinary" in legacy_mcp.tools + assert "ordinary" not in workspace_mcp.tools + assert "workspace_safe" in workspace_mcp.tools + assert "workspace_safe" not in legacy_mcp.tools diff --git a/tests/test_workspaces.py b/tests/test_workspaces.py new file mode 100644 index 0000000000000000000000000000000000000000..178f46d75c66beb4a5c246eee72947c86ac0481b --- /dev/null +++ b/tests/test_workspaces.py @@ -0,0 +1,161 @@ +from src.constants.data import Brain, Workspace +from src.services.workspaces import ( + WorkspaceConflictError, + WorkspaceAccessDeniedError, + WorkspaceError, + WorkspaceScopeConflictError, + WorkspaceService, + resolve_workspace_scope, + authorize_brain_pat, + validate_slug, + workspace_payload, + public_base_url, +) + + +class MemoryRepository: + def __init__(self, brains=()): + self.brains = {brain.name_key: brain for brain in brains} + self.workspaces = {} + + def get_brains_list(self): + return list(self.brains.values()) + + def get_brain(self, name_key): + return self.brains.get(name_key) + + def create_brain(self, name_key): + if name_key in self.brains: + raise ValueError("duplicate brain") + brain = Brain(name_key=name_key) + self.brains[name_key] = brain + return brain + + def get_workspaces(self): + return list(self.workspaces.values()) + + def get_workspace(self, slug): + return next((item for item in self.workspaces.values() if item.slug == slug), None) + + def get_workspace_by_brain_id(self, brain_id): + return self.workspaces.get(brain_id) + + def create_workspace(self, workspace): + if self.get_workspace(workspace.slug) or workspace.brain_id in self.workspaces: + raise WorkspaceConflictError("duplicate workspace") + self.workspaces[workspace.brain_id] = workspace + return workspace + + def update_workspace(self, workspace): + self.workspaces[workspace.brain_id] = workspace + return workspace + + +def test_slug_validation(): + assert validate_slug("aux-research") == "aux-research" + for invalid in ("Aux", "two words", "-alpha", "alpha-", "a" * 64): + try: + validate_slug(invalid) + except WorkspaceError: + pass + else: + raise AssertionError(f"accepted invalid slug {invalid!r}") + + +def test_duplicate_slug_and_brain_id_are_rejected(): + repository = MemoryRepository([Brain(name_key="alpha"), Brain(name_key="beta")]) + repository.create_workspace(Workspace(brain_id="alpha", slug="alpha", display_name="Alpha")) + for duplicate in ( + Workspace(brain_id="beta", slug="alpha", display_name="Same slug"), + Workspace(brain_id="alpha", slug="other", display_name="Same brain"), + ): + try: + repository.create_workspace(duplicate) + except WorkspaceConflictError: + pass + else: + raise AssertionError("duplicate workspace was accepted") + + +def test_bootstrap_existing_brains_is_idempotent(): + repository = MemoryRepository([Brain(name_key="Alpha Brain"), Brain(name_key="beta")]) + service = WorkspaceService(repository) + first = service.bootstrap() + second = service.bootstrap() + assert [(item.brain_id, item.slug) for item in first] == [ + ("Alpha Brain", "alpha-brain"), + ("beta", "beta"), + ] + assert [item.id for item in first] == [item.id for item in second] + assert len(repository.workspaces) == 2 + + +def test_generated_endpoint_urls_normalize_public_base_url(): + workspace = Workspace(brain_id="alpha", slug="alpha", display_name="Alpha") + payload = workspace_payload(workspace, "https://host.example") + assert payload["api_base_url"] == "https://host.example/brains/alpha/api" + assert payload["console_url"] == "https://host.example/console/w/alpha/" + assert "pat" not in payload + assert public_base_url("https://host.example///") == "https://host.example" + + +def test_workspace_url_is_authoritative(): + alpha = Workspace(brain_id="alpha", slug="alpha", display_name="Alpha") + assert resolve_workspace_scope(alpha) == "alpha" + assert resolve_workspace_scope(alpha, header_brain_id="alpha", body_brain_id="alpha") == "alpha" + try: + resolve_workspace_scope(alpha, header_brain_id="beta") + except WorkspaceScopeConflictError: + pass + else: + raise AssertionError("conflicting X-Brain-ID was accepted") + + +def test_create_workspace_uses_same_brain_registry(): + repository = MemoryRepository() + workspace = WorkspaceService(repository).create( + slug="aux-research", display_name="Aux Research", description="Research" + ) + assert repository.get_brain("aux-research") is not None + assert workspace.brain_id == "aux-research" + + +def test_system_and_per_brain_pat_authorization(): + authorize_brain_pat( + "system-secret", system_pat="system-secret", brain_pat="alpha-secret", workspace_scoped=True + ) + authorize_brain_pat( + "alpha-secret", system_pat="system-secret", brain_pat="alpha-secret", workspace_scoped=True + ) + try: + authorize_brain_pat( + "alpha-secret", system_pat="system-secret", brain_pat="beta-secret", workspace_scoped=True + ) + except WorkspaceAccessDeniedError: + pass + else: + raise AssertionError("alpha PAT accessed beta") + + +def test_hosted_facade_reuses_existing_route_handlers(): + from src.services.api.routes import workspace_api + from src.services.api.routes.ingest import ingest_data + from src.services.api.routes.retrieve import get_context + from src.services.api.routes.tasks import get_task + + endpoints = {route.endpoint for route in workspace_api.workspace_api_router.routes} + assert ingest_data in endpoints + assert get_context in endpoints + assert get_task in endpoints + + +def test_workspace_openapi_excludes_global_and_system_routes(): + from src.services.api.routes.workspace_api import _workspace_openapi_template + + paths = _workspace_openapi_template()["paths"] + assert "/ingest/" in paths + assert "/retrieve/context" in paths + assert "/tasks/{task_id}" in paths + assert "/meta/entity-labels" in paths + assert "/meta/login-info" not in paths + assert not any(path.startswith("/system") for path in paths) diff --git a/tui/AGENTS.md b/tui/AGENTS.md new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/tui/CLAUDE.md b/tui/CLAUDE.md new file mode 100644 index 0000000000000000000000000000000000000000..43c994c2d3617f947bcb5adf1933e21dabe46bb5 --- /dev/null +++ b/tui/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md diff --git a/tui/README.md b/tui/README.md new file mode 100644 index 0000000000000000000000000000000000000000..22c283fb05293b561888ccd59149a7f8a76f2d94 --- /dev/null +++ b/tui/README.md @@ -0,0 +1,130 @@ +# brainapi + +Interactive installer and runtime CLI for [BrainAPI](https://github.com/Lumen-Labs/brainapi2). + +## Install + +```bash +npm install -g brainapi-tui +``` + +This puts a `brainapi` binary on your `$PATH`. + +## Quick start + +```bash +brainapi init # clone the project, set up a Python venv, configure services +brainapi start # start docker compose services + run the API +brainapi doctor # check Python, Docker, Ollama, and configured services +``` + +### Non-interactive / hybrid init + +Any value you pass as a flag skips that wizard prompt; everything else is still asked. + +```bash +brainapi init \ + --vector-db=postgresql \ + --data-db=postgresql \ + --graph-db=networkx \ + --models-mode=remote \ + --llm-small-provider=openai \ + --llm-large-provider=openai \ + --embeddings-provider=openai \ + --openai-api-key=sk-... \ + --llm-small=gpt-4o-mini \ + --llm-large=gpt-4o \ + --brainpat-token=brainpat_... \ + --no-plugins \ + --no-start-services +``` + +`brainapi --init ...` is an alias for `brainapi init ...`. + +| Flag group | Examples | +| --- | --- | +| Defaults / DBs | `--defaults`, `--vector-db`, `--data-db`, `--graph-db` | +| Pipeline / runtime | `--ocr-mode`, `--services-runtime`, `--start-services`, `--no-start-services` | +| Models | `--models-mode`, `--llm-small-provider`, `--llm-large-provider`, `--embeddings-provider`, `--llm-small`, `--llm-large`, `--embedding-model`, `--embedding-dimensions` | +| Ollama | `--ollama-host`, `--ollama-port`, `--ollama-small`, `--ollama-large`, `--ollama-embeddings` | +| GCP | `--gcp-credentials`, `--gcp-project`, `--gcp-small`, `--gcp-large`, `--gcp-embedding` | +| Azure | `--azure-endpoint`, `--azure-api-version`, `--azure-key`, `--azure-small`, `--azure-large`, `--azure-embedding-endpoint`, `--azure-embedding-key`, `--azure-embedding` | +| OpenAI / Anthropic / DeepSeek | `--openai-api-key`, `--openai-base-url`, `--openai-small`, `--openai-large`, `--openai-embedding`, `--anthropic-api-key`, `--anthropic-small`, `--anthropic-large`, `--deepseek-api-key`, `--deepseek-small`, `--deepseek-large` | +| Bedrock | `--aws-region`, `--aws-access-key-id`, `--aws-secret-access-key`, `--aws-session-token`, `--bedrock-small`, `--bedrock-large`, `--bedrock-embedding` | +| Connections | `--redis-host`, `--redis-port`, `--postgres-*`, `--neo4j-*`, `--milvus-*`, `--mongo-*` | +| Auth / plugins | `--brainpat-token`, `--plugin ` (repeatable), `--no-plugins` | + +If any config flag is present (or `--defaults`), the “Use default settings?” question is skipped. + +## Commands + +| Command | Description | +| ----------------- | -------------------------------------------------------------------------------------------------- | +| `brainapi init` | Full bootstrap: clone the repo, create a venv, install deps, and run setup (flags skip prompts). | +| `brainapi start` | Start docker compose containers for the chosen services and launch the API. | +| `brainapi config` | Re-run the interactive flow and rewrite `.env`. | +| `brainapi doctor` | Check that Python, Docker, Ollama, GCP credentials, and configured services are reachable. | +| `brainapi update` | `git pull` the project and `pip install -e .` again. | +| `brainapi plugins install ` | Install a plugin from the registry (e.g. `brainapi plugins install chatbot`). | +| `brainapi plugins list` | List installed plugins (`--remote` to browse the registry). | +| `brainapi plugins uninstall ` | Remove an installed plugin. | +| `brainapi reset` | Purge Celery queues and/or wipe Redis + Postgres brain state (interactive). | +| `brainapi reset --brain -y` | Wipe one brain's Redis keys and DROP its `brain_*` Postgres database. | +| `brainapi reset --all -y` | Redis `FLUSHDB` + DROP all `brain_*` databases + clear `data_brains`. | +| `brainapi reset --queues-only -y` | Purge Celery queue keys only. | + +### `brainapi start` options + +| Option | Description | +| ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | +| `--pipeline accurate\|lightweight` | Set `PIPELINE_MODE` in `~/.brainapi/source/.env` before start (accurate = full pipeline; lightweight = faster ingestion). | + +## Where things live + +- Source: `~/.brainapi/source/` +- Python venv: `~/.brainapi/source/.venv/` +- Env file: `~/.brainapi/source/.env` +- Install state: `~/.brainapi/state.json` + +## Configuration + +The interactive flow asks (any matching CLI flag skips that prompt): + +1. Use default settings? (NetworkX + Postgres + pgvector + remote GCP Vertex) +2. Otherwise: Vector DB → Data DB → Graph DB → Models mode +3. For `remote` models mode: pick provider, then provide GCP credentials, project, and model names +4. For `local` models mode: probe Ollama, wait for it to start, then verify pulled models +5. Search: enable `/retrieve/search`? If yes, retrieval = both fused (default) / dense only / BM25 only. Requires `DATA_DB=postgresql`. Search p50 SLO is < 200 ms excluding embed RTT. +6. Connection details for only the services you actually selected +7. `BRAINPAT_TOKEN` (generate or paste) +8. Optionally start the docker compose containers now + +If Python (>=3.11) or Docker is missing, the TUI walks you through installing them with platform-aware commands and retries detection automatically. + +## Development + +```bash +cd tui +npm install +npm run dev # builds in watch mode +node dist/index.js init # try it +``` + +### Publishing + +```bash +npm version +npm publish +``` + +`prepublishOnly` runs the build automatically so the published tarball always contains `dist/`. Only the `dist/`, `README.md`, and `LICENSE` files are included (see the `files` field in `package.json`). + +If someone runs any command before `brainapi init` (e.g. `brainapi doctor` or `brainapi start`), the CLI prints a notice and runs `init` first. + +## Environment overrides + +| Variable | Default | Description | +| ------------------- | --------------------------------------------- | --------------------------------- | +| `BRAINAPI_REPO_URL` | `https://github.com/Lumen-Labs/brainapi2.git` | The git repo cloned on `init`. | +| `BRAINAPI_HOME` | `$HOME/.brainapi` | Where the source and state live. | +| `BRAINAPI_BRANCH` | `main` | Branch to checkout after cloning. | diff --git a/tui/package-lock.json b/tui/package-lock.json new file mode 100644 index 0000000000000000000000000000000000000000..3ff6c981b78746aa75dacf4880a27a22bee01883 --- /dev/null +++ b/tui/package-lock.json @@ -0,0 +1,1877 @@ +{ + "name": "brainapi-tui", + "version": "0.3.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "brainapi-tui", + "version": "0.3.0", + "license": "MIT", + "dependencies": { + "@clack/prompts": "^0.7.0", + "execa": "^9.4.0", + "picocolors": "^1.1.0", + "simple-git": "^3.27.0" + }, + "bin": { + "brainapi": "dist/index.js" + }, + "devDependencies": { + "@types/node": "^22.7.4", + "tsup": "^8.3.0", + "typescript": "^5.6.2" + }, + "engines": { + "node": ">=18.17" + } + }, + "node_modules/@clack/core": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@clack/core/-/core-0.3.5.tgz", + "integrity": "sha512-5cfhQNH+1VQ2xLQlmzXMqUoiaH0lRBq9/CLW9lTyMbuKLC3+xEK01tHVvyut++mLOn5urSHmkm6I0Lg9MaJSTQ==", + "license": "MIT", + "dependencies": { + "picocolors": "^1.0.0", + "sisteransi": "^1.0.5" + } + }, + "node_modules/@clack/prompts": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-0.7.0.tgz", + "integrity": "sha512-0MhX9/B4iL6Re04jPrttDm+BsP8y6mS7byuv0BvXgdXhbV5PdlsHt55dvNsuBCPZ7xq1oTAOOuotR9NFbQyMSA==", + "bundleDependencies": [ + "is-unicode-supported" + ], + "license": "MIT", + "dependencies": { + "@clack/core": "^0.3.3", + "is-unicode-supported": "*", + "picocolors": "^1.0.0", + "sisteransi": "^1.0.5" + } + }, + "node_modules/@clack/prompts/node_modules/is-unicode-supported": { + "version": "1.3.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@kwsites/file-exists": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@kwsites/file-exists/-/file-exists-1.1.1.tgz", + "integrity": "sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==", + "license": "MIT", + "dependencies": { + "debug": "^4.1.1" + } + }, + "node_modules/@kwsites/promise-deferred": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@kwsites/promise-deferred/-/promise-deferred-1.1.1.tgz", + "integrity": "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==", + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.4.tgz", + "integrity": "sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.4.tgz", + "integrity": "sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.4.tgz", + "integrity": "sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.4.tgz", + "integrity": "sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.4.tgz", + "integrity": "sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.4.tgz", + "integrity": "sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.4.tgz", + "integrity": "sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.4.tgz", + "integrity": "sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.4.tgz", + "integrity": "sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.4.tgz", + "integrity": "sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.4.tgz", + "integrity": "sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.4.tgz", + "integrity": "sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.4.tgz", + "integrity": "sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.4.tgz", + "integrity": "sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.4.tgz", + "integrity": "sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.4.tgz", + "integrity": "sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.4.tgz", + "integrity": "sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.4.tgz", + "integrity": "sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.4.tgz", + "integrity": "sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.4.tgz", + "integrity": "sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.4.tgz", + "integrity": "sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.4.tgz", + "integrity": "sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.4.tgz", + "integrity": "sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.4.tgz", + "integrity": "sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.4.tgz", + "integrity": "sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@sec-ant/readable-stream": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", + "integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==", + "license": "MIT" + }, + "node_modules/@simple-git/args-pathspec": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@simple-git/args-pathspec/-/args-pathspec-1.0.3.tgz", + "integrity": "sha512-ngJMaHlsWDTfjyq9F3VIQ8b7NXbBLq5j9i5bJ6XLYtD6qlDXT7fdKY2KscWWUF8t18xx052Y/PUO1K1TRc9yKA==", + "license": "MIT" + }, + "node_modules/@simple-git/argv-parser": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@simple-git/argv-parser/-/argv-parser-1.1.1.tgz", + "integrity": "sha512-Q9lBcfQ+VQCpQqGJFHe5yooOS5hGdLFFbJ5R+R5aDsnkPCahtn1hSkMcORX65J2Z5lxSkD0lQorMsncuBQxYUw==", + "license": "MIT", + "dependencies": { + "@simple-git/args-pathspec": "^1.0.3" + } + }, + "node_modules/@sindresorhus/merge-streams": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz", + "integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.19.19", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.19.tgz", + "integrity": "sha512-dyh/xO2Fh5bYrfWaaqGrRQQGkNdmYw6AmaAUvYeUMNTWQtvb796ikLdmTchRmOlOiIJ1TDXfWgVx1QkUlQ6Hew==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/bundle-require": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/bundle-require/-/bundle-require-5.1.0.tgz", + "integrity": "sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "load-tsconfig": "^0.2.3" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "peerDependencies": { + "esbuild": ">=0.18" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, + "node_modules/execa": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-9.6.1.tgz", + "integrity": "sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==", + "license": "MIT", + "dependencies": { + "@sindresorhus/merge-streams": "^4.0.0", + "cross-spawn": "^7.0.6", + "figures": "^6.1.0", + "get-stream": "^9.0.0", + "human-signals": "^8.0.1", + "is-plain-obj": "^4.1.0", + "is-stream": "^4.0.1", + "npm-run-path": "^6.0.0", + "pretty-ms": "^9.2.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^4.0.0", + "yoctocolors": "^2.1.1" + }, + "engines": { + "node": "^18.19.0 || >=20.5.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/figures": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz", + "integrity": "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==", + "license": "MIT", + "dependencies": { + "is-unicode-supported": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/fix-dts-default-cjs-exports": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fix-dts-default-cjs-exports/-/fix-dts-default-cjs-exports-1.0.1.tgz", + "integrity": "sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "magic-string": "^0.30.17", + "mlly": "^1.7.4", + "rollup": "^4.34.8" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-stream": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz", + "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==", + "license": "MIT", + "dependencies": { + "@sec-ant/readable-stream": "^0.4.1", + "is-stream": "^4.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/human-signals": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-8.0.1.tgz", + "integrity": "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-stream": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz", + "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/joycon": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", + "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/load-tsconfig": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/load-tsconfig/-/load-tsconfig-0.2.5.tgz", + "integrity": "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/mlly": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz", + "integrity": "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.16.0", + "pathe": "^2.0.3", + "pkg-types": "^1.3.1", + "ufo": "^1.6.3" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/npm-run-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-6.0.0.tgz", + "integrity": "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==", + "license": "MIT", + "dependencies": { + "path-key": "^4.0.0", + "unicorn-magic": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/parse-ms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz", + "integrity": "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-types": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/pretty-ms": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.3.0.tgz", + "integrity": "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==", + "license": "MIT", + "dependencies": { + "parse-ms": "^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/rollup": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.4.tgz", + "integrity": "sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.4", + "@rollup/rollup-android-arm64": "4.60.4", + "@rollup/rollup-darwin-arm64": "4.60.4", + "@rollup/rollup-darwin-x64": "4.60.4", + "@rollup/rollup-freebsd-arm64": "4.60.4", + "@rollup/rollup-freebsd-x64": "4.60.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.4", + "@rollup/rollup-linux-arm-musleabihf": "4.60.4", + "@rollup/rollup-linux-arm64-gnu": "4.60.4", + "@rollup/rollup-linux-arm64-musl": "4.60.4", + "@rollup/rollup-linux-loong64-gnu": "4.60.4", + "@rollup/rollup-linux-loong64-musl": "4.60.4", + "@rollup/rollup-linux-ppc64-gnu": "4.60.4", + "@rollup/rollup-linux-ppc64-musl": "4.60.4", + "@rollup/rollup-linux-riscv64-gnu": "4.60.4", + "@rollup/rollup-linux-riscv64-musl": "4.60.4", + "@rollup/rollup-linux-s390x-gnu": "4.60.4", + "@rollup/rollup-linux-x64-gnu": "4.60.4", + "@rollup/rollup-linux-x64-musl": "4.60.4", + "@rollup/rollup-openbsd-x64": "4.60.4", + "@rollup/rollup-openharmony-arm64": "4.60.4", + "@rollup/rollup-win32-arm64-msvc": "4.60.4", + "@rollup/rollup-win32-ia32-msvc": "4.60.4", + "@rollup/rollup-win32-x64-gnu": "4.60.4", + "@rollup/rollup-win32-x64-msvc": "4.60.4", + "fsevents": "~2.3.2" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/simple-git": { + "version": "3.36.0", + "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.36.0.tgz", + "integrity": "sha512-cGQjLjK8bxJw4QuYT7gxHw3/IouVESbhahSsHrX97MzCL1gu2u7oy38W6L2ZIGECEfIBG4BabsWDPjBxJENv9Q==", + "license": "MIT", + "dependencies": { + "@kwsites/file-exists": "^1.1.1", + "@kwsites/promise-deferred": "^1.1.1", + "@simple-git/args-pathspec": "^1.0.3", + "@simple-git/argv-parser": "^1.1.0", + "debug": "^4.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/steveukx/git-js?sponsor=1" + } + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "license": "MIT" + }, + "node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "node_modules/strip-final-newline": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-4.0.0.tgz", + "integrity": "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/tsup": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/tsup/-/tsup-8.5.1.tgz", + "integrity": "sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing==", + "dev": true, + "license": "MIT", + "dependencies": { + "bundle-require": "^5.1.0", + "cac": "^6.7.14", + "chokidar": "^4.0.3", + "consola": "^3.4.0", + "debug": "^4.4.0", + "esbuild": "^0.27.0", + "fix-dts-default-cjs-exports": "^1.0.0", + "joycon": "^3.1.1", + "picocolors": "^1.1.1", + "postcss-load-config": "^6.0.1", + "resolve-from": "^5.0.0", + "rollup": "^4.34.8", + "source-map": "^0.7.6", + "sucrase": "^3.35.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.11", + "tree-kill": "^1.2.2" + }, + "bin": { + "tsup": "dist/cli-default.js", + "tsup-node": "dist/cli-node.js" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@microsoft/api-extractor": "^7.36.0", + "@swc/core": "^1", + "postcss": "^8.4.12", + "typescript": ">=4.5.0" + }, + "peerDependenciesMeta": { + "@microsoft/api-extractor": { + "optional": true + }, + "@swc/core": { + "optional": true + }, + "postcss": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/ufo": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.4.tgz", + "integrity": "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unicorn-magic": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz", + "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/yoctocolors": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.2.tgz", + "integrity": "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/tui/package.json b/tui/package.json new file mode 100644 index 0000000000000000000000000000000000000000..87cd227bebe80b122a48d3250ee33ebf74968920 --- /dev/null +++ b/tui/package.json @@ -0,0 +1,42 @@ +{ + "name": "brainapi-tui", + "version": "0.4.0", + "description": "Interactive installer and runtime CLI for BrainAPI.", + "license": "MIT", + "type": "module", + "bin": { + "brainapi": "dist/index.js" + }, + "files": [ + "dist", + "README.md", + "LICENSE" + ], + "engines": { + "node": ">=18.17" + }, + "scripts": { + "sync:install-extras": "node scripts/sync-install-extras.mjs", + "prebuild": "npm run sync:install-extras", + "build": "tsup", + "predev": "npm run sync:install-extras", + "dev": "tsup --watch", + "typecheck": "npm run sync:install-extras && tsc --noEmit", + "start": "node dist/index.js", + "prepublishOnly": "npm run build" + }, + "dependencies": { + "@clack/prompts": "^0.7.0", + "execa": "^9.4.0", + "picocolors": "^1.1.0", + "simple-git": "^3.27.0" + }, + "devDependencies": { + "@types/node": "^22.7.4", + "tsup": "^8.3.0", + "typescript": "^5.6.2" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/tui/scripts/sync-install-extras.mjs b/tui/scripts/sync-install-extras.mjs new file mode 100644 index 0000000000000000000000000000000000000000..48391fd5312fb708ced8c9fcfb61183477ec1bfe --- /dev/null +++ b/tui/scripts/sync-install-extras.mjs @@ -0,0 +1,19 @@ +#!/usr/bin/env node +import { mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +const here = dirname(fileURLToPath(import.meta.url)); +const repoRoot = resolve(here, "..", ".."); +const sourcePath = resolve(repoRoot, "scripts", "install_extras.py"); +const targetPath = resolve(here, "..", "src", "embedded", "install-extras-script.ts"); + +const script = readFileSync(sourcePath, "utf8"); +mkdirSync(dirname(targetPath), { recursive: true }); +const out = `// Auto-generated from scripts/install_extras.py — do not edit by hand. +// Regenerate with: npm run sync:install-extras (also runs as prebuild). + +export const INSTALL_EXTRAS_SCRIPT = ${JSON.stringify(script)}; +`; +writeFileSync(targetPath, out, "utf8"); +console.log(`Synced ${sourcePath} → ${targetPath} (${script.length} bytes)`); diff --git a/tui/src/commands/config.ts b/tui/src/commands/config.ts new file mode 100644 index 0000000000000000000000000000000000000000..9feb22e9764eec97122295f63524b1074a732550 --- /dev/null +++ b/tui/src/commands/config.ts @@ -0,0 +1,439 @@ +import * as p from "@clack/prompts"; +import pc from "picocolors"; +import { readState, updateState } from "../lib/state.js"; +import { askUseDefaults, DEFAULT_DBS } from "../flows/defaults.js"; +import { askDatabases } from "../flows/services.js"; +import { askModels } from "../flows/models.js"; +import { + askPipeline, + ocrToExtras, + postgresBackendExtras, +} from "../flows/pipeline.js"; +import { askConnections } from "../flows/connections.js"; +import { askAuth } from "../flows/auth.js"; +import { askSearch } from "../flows/search.js"; +import { askServicesRuntime } from "../flows/services-runtime.js"; +import { + runSetupWizard, + toInitChoices, + type SetupDraft, +} from "../flows/setup-wizard.js"; +import { askPluginUninstall, askPlugins } from "../flows/plugins.js"; +import { writeEnvFromChoices } from "../lib/write-env.js"; +import { installExtras, missingExtras } from "../lib/python.js"; +import { askText, isPromptBack, pickOne } from "../lib/prompts.js"; +import { + installLocalPlugin, + installRegistryPlugin, + listInstalledPlugins, + uninstallPlugin, +} from "../lib/plugins.js"; +import type { InitChoices, InstallState } from "../types.js"; + +const MENU_BACK_HINT = "Return to configuration menu"; + +type ConfigMenuSection = + | "defaults" + | "databases" + | "runtime" + | "models" + | "pipeline" + | "search" + | "connections" + | "auth" + | "plugins" + | "wizard" + | "save" + | "exit"; + +interface ConfigDraft extends SetupDraft {} + +function draftSummary(draft: ConfigDraft): string { + const lines: string[] = []; + if (draft.dbs) { + lines.push( + `Databases: ${draft.dbs.vectorDb} / ${draft.dbs.dataDb} / ${draft.dbs.graphDb}`, + ); + } else { + lines.push("Databases: not configured"); + } + lines.push( + draft.servicesRuntime + ? `Services runtime: ${draft.servicesRuntime}` + : "Services runtime: not configured", + ); + lines.push(draft.models ? "Models: configured" : "Models: not configured"); + lines.push( + draft.pipeline + ? `Pipeline OCR: ${draft.pipeline.ocrMode}` + : "Pipeline OCR: not configured", + ); + if (draft.search) { + lines.push( + draft.search.enabled + ? `Search: enabled (${draft.search.retrieval})` + : "Search: disabled", + ); + } else { + lines.push("Search: not configured (default off)"); + } + lines.push( + draft.connections ? "Connections: configured" : "Connections: not configured", + ); + lines.push( + draft.auth?.brainpatToken ? "Auth token: set" : "Auth token: not configured", + ); + lines.push(`Plugins selected: ${draft.plugins?.length ?? 0}`); + return lines.join("\n"); +} + +function hasCompleteDraft(draft: ConfigDraft): boolean { + return Boolean( + draft.dbs && + draft.servicesRuntime && + draft.models && + draft.pipeline && + draft.connections && + draft.auth?.brainpatToken, + ); +} + +async function configMainMenu(): Promise { + return pickOne({ + message: "What would you like to configure?", + options: [ + { + value: "defaults", + label: "Apply default stack", + hint: "NetworkX + Postgres + remote LLM", + }, + { value: "databases", label: "Databases" }, + { value: "runtime", label: "Services runtime" }, + { value: "models", label: "Models (LLM & embeddings)" }, + { value: "pipeline", label: "Pipeline (OCR)" }, + { value: "search", label: "Search (BM25 + dense)" }, + { value: "connections", label: "Connection details" }, + { value: "auth", label: "Authentication token" }, + { value: "plugins", label: "Plugins (install/uninstall)" }, + { + value: "wizard", + label: "Configure all (step-by-step)", + hint: "Full flow with back between steps", + }, + { value: "save", label: "Save & apply changes" }, + { value: "exit", label: "Exit without saving" }, + ], + }); +} + +async function applyDefaultsSection(draft: ConfigDraft): Promise { + const useDefaults = await askUseDefaults({ + allowBack: true, + backHint: MENU_BACK_HINT, + }); + if (isPromptBack(useDefaults) || !useDefaults) { + return; + } + draft.usedDefaults = true; + draft.dbs = DEFAULT_DBS; + draft.pipeline = { ocrMode: "docparser" }; + const models = await askModels({ prechosenMode: "remote" }); + if (!isPromptBack(models)) { + draft.models = models; + } + p.log.success("Default stack applied to draft."); +} + +async function configureDatabases(draft: ConfigDraft): Promise { + const dbs = await askDatabases({ + allowBack: true, + backHint: MENU_BACK_HINT, + initial: draft.dbs, + }); + if (isPromptBack(dbs)) { + return; + } + draft.dbs = dbs; + draft.usedDefaults = false; + p.log.success("Databases updated."); +} + +async function configureRuntime(draft: ConfigDraft): Promise { + if (!draft.dbs) { + p.log.warn("Configure databases first."); + return; + } + const runtime = await askServicesRuntime(draft.dbs, { + allowBack: true, + backHint: MENU_BACK_HINT, + initialValue: draft.servicesRuntime, + }); + if (isPromptBack(runtime)) { + return; + } + draft.servicesRuntime = runtime; + p.log.success("Services runtime updated."); +} + +async function configureModels(draft: ConfigDraft): Promise { + const models = await askModels({ + allowBack: true, + backHint: MENU_BACK_HINT, + initialMode: draft.models?.mode, + }); + if (isPromptBack(models)) { + return; + } + draft.models = models; + p.log.success("Models updated."); +} + +async function configurePipeline(draft: ConfigDraft): Promise { + const pipeline = await askPipeline({ + allowBack: true, + backHint: MENU_BACK_HINT, + initialOcrMode: draft.pipeline?.ocrMode, + }); + if (isPromptBack(pipeline)) { + return; + } + draft.pipeline = pipeline; + draft.usedDefaults = false; + p.log.success("Pipeline updated."); +} + +async function configureSearch(draft: ConfigDraft): Promise { + const search = await askSearch({ + allowBack: true, + backHint: MENU_BACK_HINT, + initial: draft.search, + dataDb: draft.dbs?.dataDb, + }); + if (isPromptBack(search)) { + return; + } + draft.search = search; + p.log.success("Search updated."); +} + +async function configureConnections(draft: ConfigDraft): Promise { + if (!draft.dbs) { + p.log.warn("Configure databases first."); + return; + } + const connections = await askConnections(draft.dbs); + draft.connections = connections; + p.log.success("Connections updated."); +} + +async function configureAuth(draft: ConfigDraft): Promise { + const auth = await askAuth({ + allowBack: true, + backHint: MENU_BACK_HINT, + }); + if (isPromptBack(auth)) { + return; + } + draft.auth = auth; + p.log.success("Authentication token updated."); +} + +async function configurePlugins(draft: ConfigDraft): Promise { + const action = await pickOne<"add_search" | "install_path" | "uninstall" | "list" | "back">({ + message: "Plugin management", + options: [ + { value: "add_search", label: "Search and add plugin (local/registry)" }, + { value: "install_path", label: "Install local plugin by path" }, + { value: "uninstall", label: "Uninstall installed plugin" }, + { value: "list", label: "List installed plugins" }, + { value: "back", label: "Back" }, + ], + initialValue: "add_search", + }); + if (action === "back") return; + if (action === "list") { + const installed = await listInstalledPlugins(); + if (installed.length === 0) { + p.log.info("No installed plugins."); + return; + } + p.note( + installed.map((plugin) => `${plugin.name} (${plugin.version})`).join("\n"), + "Installed plugins", + ); + return; + } + if (action === "install_path") { + const pluginPath = await askText({ + message: "Enter local plugin directory path", + placeholder: "/path/to/plugin", + }); + if (!pluginPath.trim()) { + p.log.warn("No path provided."); + return; + } + const installedName = await installLocalPlugin(pluginPath.trim()); + const existing = draft.plugins ?? []; + draft.plugins = [ + ...existing.filter((plugin) => plugin.name !== installedName), + { name: installedName, source: "local", path: pluginPath.trim() }, + ]; + p.log.success(`Installed local plugin ${installedName}.`); + return; + } + if (action === "uninstall") { + const selected = await askPluginUninstall({ allowBack: true, backHint: MENU_BACK_HINT }); + if (selected === null || isPromptBack(selected)) { + return; + } + await uninstallPlugin(selected); + draft.plugins = (draft.plugins ?? []).filter((plugin) => plugin.name !== selected); + p.log.success(`Uninstalled plugin ${selected}.`); + return; + } + const selectedPlugins = await askPlugins({ + allowBack: true, + backHint: MENU_BACK_HINT, + initial: draft.plugins ?? [], + }); + if (isPromptBack(selectedPlugins)) { + return; + } + for (const plugin of selectedPlugins) { + if (plugin.source === "local") { + if (!plugin.path) continue; + await installLocalPlugin(plugin.path); + p.log.success(`Installed local plugin ${plugin.name}.`); + continue; + } + await installRegistryPlugin(plugin.name, plugin.version); + p.log.success(`Installed registry plugin ${plugin.name}.`); + } + draft.plugins = selectedPlugins; +} + +async function runConfigWizard(draft: ConfigDraft): Promise { + const completed = await runSetupWizard(draft, { + firstStepBack: "return", + firstStepBackHint: MENU_BACK_HINT, + stepBackHint: "Previous step", + }); + if (completed) { + p.log.success("Full configuration complete."); + } +} + +async function saveDraft(draft: ConfigDraft): Promise { + const statePatch: Partial = {}; + if (draft.servicesRuntime) { + statePatch.servicesRuntime = draft.servicesRuntime; + } + if (draft.plugins) { + statePatch.installedPlugins = draft.plugins.map((plugin) => plugin.name); + } + if (Object.keys(statePatch).length > 0) { + await updateState(statePatch); + } + + if (!hasCompleteDraft(draft)) { + p.log.success("Changes saved."); + p.log.warn( + "Draft is partial, so .env was not rewritten. Use Configure all to fully regenerate it.", + ); + p.outro(pc.green("Partial configuration applied.")); + return true; + } + + const choices = toInitChoices(draft); + await writeEnvFromChoices(choices); + await updateState({ + envWritten: true, + servicesRuntime: choices.servicesRuntime, + installedPlugins: choices.plugins.map((plugin) => plugin.name), + }); + + const extras = [ + ...ocrToExtras(choices.pipeline.ocrMode), + ...postgresBackendExtras(choices.dbs), + ]; + const missingExtrasList = await missingExtras(extras); + if (missingExtrasList.length > 0) { + p.log.info(`Installing missing extras: ${missingExtrasList.join(", ")}`); + await installExtras(missingExtrasList); + p.log.success("Extras installed."); + } + + p.outro(pc.green(".env rewritten.")); + return true; +} + +export async function runConfig(): Promise { + p.intro(pc.bgCyan(pc.black(" brainapi ")) + " " + pc.dim("config")); + + const state = await readState(); + if (!state || !state.cloned) { + p.cancel( + "No brainapi install detected. Run " + pc.cyan("brainapi init") + " first.", + ); + process.exit(1); + } + + const draft: ConfigDraft = { + servicesRuntime: state.servicesRuntime ?? undefined, + plugins: (state.installedPlugins ?? []).map((name) => ({ + name, + source: "local", + })), + }; + + p.note( + "Pick a section to update, or use step-by-step mode. Select Back on any prompt to go back.", + "Configuration", + ); + + while (true) { + p.note(draftSummary(draft), "Current draft"); + const section = await configMainMenu(); + + switch (section) { + case "exit": + p.cancel("No changes saved."); + return; + case "save": + if (await saveDraft(draft)) { + return; + } + break; + case "defaults": + await applyDefaultsSection(draft); + break; + case "databases": + await configureDatabases(draft); + break; + case "runtime": + await configureRuntime(draft); + break; + case "models": + await configureModels(draft); + break; + case "pipeline": + await configurePipeline(draft); + break; + case "search": + await configureSearch(draft); + break; + case "connections": + await configureConnections(draft); + break; + case "auth": + await configureAuth(draft); + break; + case "plugins": + await configurePlugins(draft); + break; + case "wizard": + await runConfigWizard(draft); + break; + } + } +} diff --git a/tui/src/commands/doctor.ts b/tui/src/commands/doctor.ts new file mode 100644 index 0000000000000000000000000000000000000000..118944a352d571563b0d5147f415daeb46e3cc0c --- /dev/null +++ b/tui/src/commands/doctor.ts @@ -0,0 +1,181 @@ +import { access } from "node:fs/promises"; +import * as p from "@clack/prompts"; +import pc from "picocolors"; +import { detectPython } from "../lib/python.js"; +import { detectDocker } from "../lib/docker.js"; +import { tcpProbe } from "../lib/net.js"; +import { isOllamaRunning } from "../lib/ollama.js"; +import { readEnvFile } from "../lib/env.js"; +import { envFilePath, venvPath } from "../lib/paths.js"; +import { readState } from "../lib/state.js"; + +interface CheckResult { + name: string; + ok: boolean | "warn"; + detail?: string; +} + +function getValue( + env: Awaited>, + key: string, +): string | undefined { + const idx = env.keyIndex.get(key); + if (idx === undefined) return undefined; + const raw = (env.lines[idx] ?? "").split("=").slice(1).join("="); + return raw.trim().replace(/^"|"$/g, ""); +} + +async function fileExists(path: string): Promise { + try { + await access(path); + return true; + } catch { + return false; + } +} + +function statusGlyph(ok: boolean | "warn"): string { + if (ok === true) return pc.green("OK"); + if (ok === "warn") return pc.yellow("WARN"); + return pc.red("FAIL"); +} + +export async function runDoctor(): Promise { + p.intro(pc.bgCyan(pc.black(" brainapi ")) + " " + pc.dim("doctor")); + + const results: CheckResult[] = []; + + const py = await detectPython(); + results.push({ + name: "Python >= 3.11", + ok: py !== null, + detail: py ? `${py.bin} (${py.version.join(".")})` : "Not found on PATH", + }); + + results.push({ + name: "Python venv", + ok: await fileExists(venvPath()), + detail: venvPath(), + }); + + const state = await readState(); + results.push({ + name: "Install state", + ok: state !== null, + detail: state ? "~/.brainapi/state.json present" : "missing", + }); + + let env: Awaited> | null = null; + if (await fileExists(envFilePath())) { + env = await readEnvFile(envFilePath()); + results.push({ name: ".env", ok: true, detail: envFilePath() }); + } else { + results.push({ name: ".env", ok: false, detail: "missing — run brainapi init" }); + } + + const runtime = state?.servicesRuntime ?? "docker"; + if (runtime === "docker") { + const dockerState = await detectDocker(); + results.push({ + name: "Docker", + ok: dockerState === "ok" ? true : dockerState === "daemon_down" ? "warn" : false, + detail: + dockerState === "ok" + ? "installed and daemon up" + : dockerState === "daemon_down" + ? "installed but daemon not running" + : "not installed", + }); + } else { + results.push({ + name: "Docker", + ok: "warn", + detail: "not required (services runtime = manual)", + }); + } + + if (env) { + const modelsMode = getValue(env, "MODELS_MODE"); + if (modelsMode === "local") { + const host = getValue(env, "OLLAMA_HOST") ?? "localhost"; + const port = Number(getValue(env, "OLLAMA_PORT") ?? "11434"); + const ok = await isOllamaRunning({ host, port }); + results.push({ + name: "Ollama", + ok, + detail: `http://${host}:${port}`, + }); + } else if (modelsMode === "remote") { + const credPath = getValue(env, "GCP_CREDENTIALS_PATH"); + results.push({ + name: "GCP credentials", + ok: credPath ? await fileExists(credPath) : false, + detail: credPath ?? "GCP_CREDENTIALS_PATH not set", + }); + } + + const services: Array<[string, string, string]> = [ + ["Redis", getValue(env, "REDIS_HOST") ?? "localhost", getValue(env, "REDIS_PORT") ?? "6379"], + ]; + if ( + getValue(env, "VECTOR_DB") === "postgresql" || + getValue(env, "DATA_DB") === "postgresql" || + getValue(env, "GRAPH_DB") === "networkx" + ) { + services.push([ + "PostgreSQL", + getValue(env, "POSTGRES_HOST") ?? "localhost", + getValue(env, "POSTGRES_PORT") ?? "5432", + ]); + } + if (getValue(env, "GRAPH_DB") === "neo4j") { + services.push([ + "Neo4j", + getValue(env, "NEO4J_HOST") ?? "localhost", + getValue(env, "NEO4J_PORT") ?? "7687", + ]); + } + if (getValue(env, "VECTOR_DB") === "milvus") { + services.push([ + "Milvus", + getValue(env, "MILVUS_HOST") ?? "localhost", + getValue(env, "MILVUS_PORT") ?? "19530", + ]); + } + if (getValue(env, "DATA_DB") === "mongo") { + services.push([ + "Mongo", + getValue(env, "MONGO_HOST") ?? "localhost", + getValue(env, "MONGO_PORT") ?? "27017", + ]); + } + if (getValue(env, "CELERY_BACKEND") === "rabbitmq") { + services.push([ + "RabbitMQ", + getValue(env, "RABBITMQ_HOST") ?? "localhost", + getValue(env, "RABBITMQ_PORT") ?? "5672", + ]); + } + + for (const [name, host, portRaw] of services) { + const port = Number(portRaw); + const ok = await tcpProbe({ host, port }); + results.push({ + name, + ok, + detail: `${host}:${port}`, + }); + } + } + + const lines = results.map((r) => `${statusGlyph(r.ok)} ${r.name}${r.detail ? pc.dim(` — ${r.detail}`) : ""}`); + p.note(lines.join("\n"), "Diagnostics"); + + const fails = results.filter((r) => r.ok === false).length; + if (fails === 0) { + p.outro(pc.green("All checks passed.")); + } else { + p.outro(pc.red(`${fails} check(s) failed.`)); + process.exit(1); + } +} diff --git a/tui/src/commands/init.ts b/tui/src/commands/init.ts new file mode 100644 index 0000000000000000000000000000000000000000..172bf87d20d77b5f4208365b4b070b87e0f6ccf6 --- /dev/null +++ b/tui/src/commands/init.ts @@ -0,0 +1,278 @@ +import * as p from "@clack/prompts"; +import pc from "picocolors"; +import { sourcePath } from "../lib/paths.js"; +import { + defaultState, + ensureStateDir, + readState, + updateState, + writeState, +} from "../lib/state.js"; +import { ensureRepo, isClonedRepo } from "../lib/git.js"; +import { ensurePython } from "../lib/python-recovery.js"; +import { + createVenv, + installDeps, + venvExists, +} from "../lib/python.js"; +import { runSetupWizard, toInitChoices, type SetupDraft } from "../flows/setup-wizard.js"; +import { + ocrToExtras, + postgresBackendExtras, +} from "../flows/pipeline.js"; +import { writeEnvFromChoices } from "../lib/write-env.js"; +import { ensureDocker } from "../lib/docker-recovery.js"; +import { bringUpServices, bringUpServicesManually } from "../lib/bring-up.js"; +import { targetFromConnections } from "../lib/service-target.js"; +import { askConfirm } from "../lib/prompts.js"; +import { installLocalPlugin, installRegistryPlugin } from "../lib/plugins.js"; +import { + hasConfigOverrides, + type InitFlagOverrides, +} from "../lib/init-flags.js"; +import type { + Connections, + DbChoices, + InitChoices, + PluginChoice, + ServicesRuntime, +} from "../types.js"; +import { type ServiceName } from "../constants.js"; + +export interface InitOptions { + repoUrl?: string; + branch?: string; + force?: boolean; + flags?: InitFlagOverrides; +} + +function intro(): void { + p.intro(pc.bgCyan(pc.black(" brainapi ")) + " " + pc.dim("init")); +} + +function outro(): void { + p.outro( + `${pc.green("Setup complete.")} Try ${pc.cyan("brainapi start")} when you're ready.`, + ); +} + +function pickedServices(dbs: DbChoices): ServiceName[] { + const set = new Set(["redis"]); + if ( + dbs.vectorDb === "postgresql" || + dbs.dataDb === "postgresql" || + dbs.graphDb === "networkx" + ) { + set.add("postgresql"); + } + if (dbs.graphDb === "neo4j") set.add("neo4j"); + if (dbs.vectorDb === "milvus") set.add("milvus"); + if (dbs.dataDb === "mongo") set.add("mongo"); + return [...set]; +} + + +async function clonePhase(opts: InitOptions): Promise { + if (await isClonedRepo()) { + p.log.info("Repository already cloned — pulling latest"); + } + const spinner = p.spinner(); + spinner.start("Cloning brainapi source"); + try { + const state = await readState(); + const repoUrl = opts.repoUrl ?? state?.repoUrl ?? defaultState().repoUrl; + const branch = opts.branch ?? state?.branch ?? defaultState().branch; + const result = await ensureRepo({ repoUrl, branch }); + spinner.stop( + result === "cloned" + ? `Cloned to ${pc.cyan(sourcePath())}` + : `Updated ${pc.cyan(sourcePath())}`, + ); + } catch (err) { + spinner.stop(pc.red("Clone failed.")); + throw err; + } +} + +async function ensureVenv(pythonBin: string): Promise { + const spinner = p.spinner(); + if (await venvExists()) { + p.log.info("Virtualenv already exists — skipping creation"); + return; + } + spinner.start("Creating Python venv (.venv)"); + try { + await createVenv(pythonBin); + spinner.stop("Virtualenv created"); + } catch (err) { + spinner.stop(pc.red("Failed to create venv")); + throw err; + } +} + +async function installPhase(extras: string[]): Promise { + if (extras.length === 0) { + p.log.info("Installing Python base dependencies"); + } else { + p.log.info( + `Installing Python base dependencies + extras (${extras.join(", ")})`, + ); + } + await installDeps({ extras }); + p.log.success("Dependencies installed"); +} + +async function configurePhase(flags?: InitFlagOverrides): Promise { + const draft: SetupDraft = {}; + await runSetupWizard(draft, { + firstStepBack: "cancel", + firstStepBackHint: "Cancel setup", + stepBackHint: "Previous step", + flags, + }); + return toInitChoices(draft); +} + +async function servicesPhase( + dbs: DbChoices, + connections: Connections, + runtime: ServicesRuntime, + startServices?: boolean, +): Promise { + const services = pickedServices(dbs); + const items = services.map((service) => ({ + service, + target: targetFromConnections(service, connections), + })); + + if (runtime === "manual") { + p.log.step("Checking backing services (manual runtime)"); + await bringUpServicesManually(items); + await updateState({ + containersStarted: false, + selectedServices: services, + servicesRuntime: "manual", + }); + return; + } + + let startNow: boolean; + if (startServices !== undefined) { + startNow = startServices; + p.log.info( + startNow + ? "Starting docker compose containers (--start-services)." + : "Skipping docker compose containers (--no-start-services).", + ); + } else { + startNow = await askConfirm({ + message: "Start docker compose containers now for the services you picked?", + initialValue: true, + }); + } + if (!startNow) { + p.log.info( + "Skipping. Start them later with " + + pc.cyan("brainapi start") + + " or " + + pc.cyan("docker compose -f src/lib//docker-compose.yaml up -d"), + ); + await updateState({ + selectedServices: services, + servicesRuntime: "docker", + }); + return; + } + + const ready = await ensureDocker(); + if (ready === "cancelled") { + p.log.warn( + "Docker is not available — setup is otherwise complete. Re-run " + + pc.cyan("brainapi start") + + " once Docker is up.", + ); + return; + } + + await bringUpServices(items); + await updateState({ + containersStarted: true, + selectedServices: services, + servicesRuntime: "docker", + }); +} + +async function pluginsPhase(plugins: PluginChoice[]): Promise { + if (plugins.length === 0) { + await updateState({ installedPlugins: [] }); + return; + } + const installed: string[] = []; + for (const plugin of plugins) { + if (plugin.source === "local") { + if (!plugin.path) { + p.log.warn(`Skipping local plugin '${plugin.name}' because path is missing.`); + continue; + } + await installLocalPlugin(plugin.path); + p.log.success(`Installed local plugin ${plugin.name}`); + installed.push(plugin.name); + continue; + } + await installRegistryPlugin(plugin.name, plugin.version); + p.log.success(`Installed registry plugin ${plugin.name}`); + installed.push(plugin.name); + } + await updateState({ installedPlugins: installed }); +} + +export async function runInit(opts: InitOptions = {}): Promise { + intro(); + await ensureStateDir(); + const existing = await readState(); + if (existing && !opts.force) { + p.log.info( + "Resuming from previous install state at " + pc.cyan("~/.brainapi/state.json"), + ); + } else if (!existing) { + await writeState(defaultState()); + } + + if (opts.flags && hasConfigOverrides(opts.flags)) { + p.log.info("Using CLI flags for setup — skipping prompts for provided values."); + } + + let python = await ensurePython(); + await clonePhase(opts); + + const recheck = await ensurePython(); + if (recheck.bin !== python.bin) { + p.log.info(`Using ${recheck.bin} (${recheck.version.join(".")})`); + } + python = recheck; + + await ensureVenv(python.bin); + + const choices = await configurePhase(opts.flags); + const extras = [ + ...ocrToExtras(choices.pipeline.ocrMode), + ...postgresBackendExtras(choices.dbs), + ]; + + await installPhase(extras); + + await pluginsPhase(choices.plugins); + + await writeEnvFromChoices(choices); + await updateState({ envWritten: true }); + p.log.success(".env written"); + + await servicesPhase( + choices.dbs, + choices.connections, + choices.servicesRuntime, + opts.flags?.startServices, + ); + + outro(); +} diff --git a/tui/src/commands/plugins.ts b/tui/src/commands/plugins.ts new file mode 100644 index 0000000000000000000000000000000000000000..fa9ee0b33ad56369cf0886a614a5378d955c3f60 --- /dev/null +++ b/tui/src/commands/plugins.ts @@ -0,0 +1,80 @@ +import * as p from "@clack/prompts"; +import pc from "picocolors"; +import { installRegistryPlugin, runPluginCli, uninstallPlugin } from "../lib/plugins.js"; +import { readState } from "../lib/state.js"; + +export type PluginsSubcommand = "install" | "uninstall" | "list" | "info" | "update"; + +export interface PluginsOptions { + subcommand?: PluginsSubcommand; + name?: string; + version?: string; + remote?: boolean; +} + +async function ensureInstalled(): Promise { + const state = await readState(); + if (!state || !state.envWritten) { + p.cancel( + "No brainapi install detected. Run " + pc.cyan("brainapi init") + " first.", + ); + process.exit(1); + } +} + +export async function runPlugins(opts: PluginsOptions): Promise { + await ensureInstalled(); + + const subcommand = opts.subcommand; + if (!subcommand) { + p.log.error( + "Usage: brainapi plugins [name] [--version ] [--remote]", + ); + process.exit(1); + } + + p.intro(pc.bgCyan(pc.black(" brainapi ")) + " " + pc.dim(`plugins ${subcommand}`)); + + switch (subcommand) { + case "install": { + if (!opts.name) { + p.log.error("Plugin name is required. Example: brainapi plugins install chatbot"); + process.exit(1); + } + await installRegistryPlugin(opts.name, opts.version); + p.outro(`Installed ${opts.name}`); + return; + } + case "uninstall": { + if (!opts.name) { + p.log.error("Plugin name is required. Example: brainapi plugins uninstall chatbot"); + process.exit(1); + } + await uninstallPlugin(opts.name); + p.outro(`Uninstalled ${opts.name}`); + return; + } + case "list": { + const args = ["list"]; + if (opts.remote) args.push("--remote"); + await runPluginCli(args); + return; + } + case "info": { + if (!opts.name) { + p.log.error("Plugin name is required. Example: brainapi plugins info chatbot"); + process.exit(1); + } + await runPluginCli(["info", opts.name]); + return; + } + case "update": { + if (!opts.name) { + p.log.error("Plugin name is required. Example: brainapi plugins update chatbot"); + process.exit(1); + } + await runPluginCli(["update", opts.name]); + return; + } + } +} diff --git a/tui/src/commands/reset.ts b/tui/src/commands/reset.ts new file mode 100644 index 0000000000000000000000000000000000000000..c7a57e6b69ce7dced68db34941e53f9090802bb7 --- /dev/null +++ b/tui/src/commands/reset.ts @@ -0,0 +1,220 @@ +import * as p from "@clack/prompts"; +import pc from "picocolors"; +import { readEnvFile, getEnvValue } from "../lib/env.js"; +import { envFilePath } from "../lib/paths.js"; +import { readState } from "../lib/state.js"; +import { ENV_KEYS } from "../constants.js"; +import { + buildResetPlan, + brainDbName, + executeReset, + listBrainDatabases, + type ResetScope, +} from "../lib/reset-state.js"; + +export interface ResetCommandOptions { + brain?: string; + all?: boolean; + queuesOnly?: boolean; + redisOnly?: boolean; + dbOnly?: boolean; + yes?: boolean; +} + +async function ensureInstalled(): Promise { + const state = await readState(); + if (!state || !state.envWritten) { + p.cancel( + "No brainapi install detected. Run " + pc.cyan("brainapi init") + " first.", + ); + process.exit(1); + } +} + +function summarizePlan( + scope: ResetScope, + redisOnly: boolean, + dbOnly: boolean, +): string[] { + const lines: string[] = []; + if (scope.kind === "queues") { + lines.push("Purge Celery queues in Redis (leave brain data intact)"); + } else if (scope.kind === "brain") { + lines.push(`Reset brain ${pc.cyan(scope.brainId)}`); + if (!dbOnly) { + lines.push(`- Redis: delete ${scope.brainId}:* keys + purge Celery queues`); + } + if (!redisOnly) { + lines.push( + `- Postgres: DROP ${brainDbName(scope.brainId)} + registry row`, + ); + } + } else { + lines.push("Reset ALL brains"); + if (!dbOnly) lines.push("- Redis: FLUSHDB"); + if (!redisOnly) { + lines.push("- Postgres: DROP every brain_* database + clear data_brains"); + } + } + lines.push(""); + lines.push( + pc.yellow("Stop `brainapi start` first so workers are not holding DB connections."), + ); + return lines; +} + +async function resolveScope( + opts: ResetCommandOptions, +): Promise { + if (opts.queuesOnly) return { kind: "queues" }; + if (opts.all) return { kind: "all" }; + if (opts.brain) { + const brainId = opts.brain.trim(); + if (!brainId) { + throw new Error("Brain id cannot be empty."); + } + return { kind: "brain", brainId }; + } + + const mode = await p.select({ + message: "What should be cleared?", + options: [ + { + value: "brain", + label: "One brain", + hint: "Redis keys + DROP that brain's Postgres DB", + }, + { + value: "all", + label: "All brains", + hint: "Redis FLUSHDB + DROP all brain_* databases", + }, + { + value: "queues", + label: "Celery queues only", + hint: "Purge pending/running queue keys in Redis", + }, + ], + }); + if (p.isCancel(mode)) { + p.cancel("Reset cancelled."); + process.exit(0); + } + + if (mode === "queues") return { kind: "queues" }; + if (mode === "all") return { kind: "all" }; + + const env = await readEnvFile(envFilePath()); + let hint = "e.g. locomoconv26"; + try { + const brains = await listBrainDatabases(env); + if (brains.length > 0) { + hint = brains.map((b) => b.brainId).slice(0, 8).join(", "); + if (brains.length > 8) hint += ", …"; + } + } catch { + // listing is optional for the prompt + } + + const brain = await p.text({ + message: "Brain id to reset", + placeholder: hint, + validate: (value) => { + if (!value.trim()) return "Brain id is required"; + if (!/^[A-Za-z0-9_-]+$/.test(value.trim())) { + return "Use alphanumeric brain ids (hyphen/underscore ok)"; + } + }, + }); + if (p.isCancel(brain)) { + p.cancel("Reset cancelled."); + process.exit(0); + } + return { kind: "brain", brainId: String(brain).trim() }; +} + +export async function runReset(opts: ResetCommandOptions = {}): Promise { + await ensureInstalled(); + p.intro(pc.bgCyan(pc.black(" brainapi ")) + " " + pc.dim("reset")); + + if (opts.redisOnly && opts.dbOnly) { + p.log.error("Use only one of --redis-only / --db-only."); + process.exit(1); + } + const exclusive = [opts.brain, opts.all, opts.queuesOnly].filter(Boolean).length; + if (exclusive > 1) { + p.log.error("Use only one of --brain / --all / --queues-only."); + process.exit(1); + } + + const env = await readEnvFile(envFilePath()); + const scope = await resolveScope(opts); + const plan = buildResetPlan(env, scope); + + p.note(summarizePlan(scope, Boolean(opts.redisOnly), Boolean(opts.dbOnly)).join("\n"), "Plan"); + + if (plan.postgres) { + p.log.info( + `Postgres ${plan.postgres.host}:${plan.postgres.port} (system db ${plan.postgres.systemDatabase})`, + ); + } + p.log.info(`Redis ${plan.redisHost}:${plan.redisPort}`); + + const celeryBackend = getEnvValue(env, ENV_KEYS.celeryBackend) ?? "redis"; + if (celeryBackend !== "redis" && !opts.dbOnly) { + p.log.error( + `reset supports CELERY_BACKEND=redis only (current: ${celeryBackend}). Use --db-only to wipe Postgres.`, + ); + process.exit(1); + } + + if (!opts.yes) { + const ok = await p.confirm({ + message: "This is destructive. Continue?", + initialValue: false, + }); + if (p.isCancel(ok) || !ok) { + p.cancel("Reset cancelled."); + process.exit(0); + } + } + + const spinner = p.spinner(); + spinner.start("Clearing state…"); + try { + const result = await executeReset(env, scope, { + yes: opts.yes, + redisOnly: opts.redisOnly, + dbOnly: opts.dbOnly, + }); + spinner.stop("Done."); + + const lines: string[] = []; + if (result.redisDeletedKeys === -1) { + lines.push("Redis: FLUSHDB completed"); + } else if (!opts.dbOnly) { + lines.push(`Redis: deleted ~${result.redisDeletedKeys} key(s)`); + } + if (result.databasesDropped.length > 0) { + lines.push(`Postgres: dropped ${result.databasesDropped.join(", ")}`); + } + if (result.registryRowsDeleted > 0) { + lines.push(`Registry: removed ${result.registryRowsDeleted} data_brains row(s)`); + } + for (const warning of result.warnings) { + lines.push(pc.yellow(warning)); + } + if (lines.length > 0) { + p.note(lines.join("\n"), "Result"); + } + p.outro( + pc.green("State cleared.") + + " Restart with " + + pc.cyan("brainapi start") + + " when ready.", + ); + } catch (err) { + spinner.stop("Failed."); + throw err; + } +} diff --git a/tui/src/commands/start.ts b/tui/src/commands/start.ts new file mode 100644 index 0000000000000000000000000000000000000000..ac1cfceaa7e8834036f35ef9db850c98965353d3 --- /dev/null +++ b/tui/src/commands/start.ts @@ -0,0 +1,228 @@ +import * as p from "@clack/prompts"; +import pc from "picocolors"; +import { readState } from "../lib/state.js"; +import { ensureDocker } from "../lib/docker-recovery.js"; +import { bringUpServices, bringUpServicesManually } from "../lib/bring-up.js"; +import { targetFromEnv } from "../lib/service-target.js"; +import { envFileToProcessEnv, readEnvFile, setPipelineMode } from "../lib/env.js"; +import { syncDevSourceIfAvailable } from "../lib/sync-source.js"; +import { envFilePath, sourcePath, venvBin } from "../lib/paths.js"; +import { + installDeps, + missingRequiredPythonDeps, +} from "../lib/python.js"; +import { runParallel } from "../lib/parallel.js"; +import { ensureConsoleBuilt } from "../lib/console-build.js"; +import { + API_DEFAULT_PORT, + MCP_DEFAULT_PORT, + SERVICE_COMPOSE_FILES, + type ServiceName, +} from "../constants.js"; +import type { PipelineMode, ServicesRuntime } from "../types.js"; + +function getEnv( + env: Awaited>, + key: string, +): string | undefined { + const idx = env.keyIndex.get(key); + if (idx === undefined) return undefined; + const raw = (env.lines[idx] ?? "").split("=").slice(1).join("="); + return raw.trim().replace(/^"|"$/g, ""); +} + +function pickedFromEnv(env: Awaited>): ServiceName[] { + const services = new Set(["redis"]); + const vector = getEnv(env, "VECTOR_DB"); + const data = getEnv(env, "DATA_DB"); + const graph = getEnv(env, "GRAPH_DB"); + if (vector === "postgresql" || data === "postgresql" || graph === "networkx") { + services.add("postgresql"); + } + if (graph === "neo4j") services.add("neo4j"); + if (vector === "milvus") services.add("milvus"); + if (data === "mongo") services.add("mongo"); + if (getEnv(env, "CELERY_BACKEND") === "rabbitmq") services.add("rabbitmq"); + return [...services].filter((s) => s in SERVICE_COMPOSE_FILES); +} + +export interface StartOptions { + api?: boolean; + mcp?: boolean; + worker?: boolean; + services?: boolean; + pipelineMode?: PipelineMode; +} + +export async function runStart(opts: StartOptions = {}): Promise { + const wantApi = opts.api !== false; + const wantMcp = opts.mcp !== false; + const wantWorker = opts.worker !== false; + const wantServices = opts.services !== false; + + p.intro(pc.bgCyan(pc.black(" brainapi ")) + " " + pc.dim("start")); + + await syncDevSourceIfAvailable(); + + const state = await readState(); + if (!state || !state.envWritten) { + p.cancel( + "No brainapi install detected. Run " + pc.cyan("brainapi init") + " first.", + ); + process.exit(1); + } + + if (opts.pipelineMode) { + await setPipelineMode(opts.pipelineMode); + p.log.info(`PIPELINE_MODE set to ${pc.cyan(opts.pipelineMode)}`); + } + + const env = await readEnvFile(envFilePath()); + + if (await missingRequiredPythonDeps()) { + p.log.info("Installing missing Python dependencies for current .env..."); + try { + await installDeps({ skipBase: true }); + p.log.success("Python dependencies installed."); + } catch (err) { + p.log.error( + `Failed to install Python dependencies: ${ + err instanceof Error ? err.message : String(err) + }`, + ); + } + } + + if (wantServices) { + const services = pickedFromEnv(env); + const runtime: ServicesRuntime = state.servicesRuntime ?? "docker"; + const getter = (key: string) => getEnv(env, key); + const items = services.map((service) => ({ + service, + target: targetFromEnv(service, getter), + })); + + if (runtime === "manual") { + p.log.step("Checking backing services (manual runtime)"); + await bringUpServicesManually(items); + } else { + const ready = await ensureDocker(); + if (ready === "cancelled") { + p.cancel("Docker is not available. Aborting start."); + process.exit(1); + } + await bringUpServices(items); + } + } else { + p.log.info("Skipping backing services (--no-services)."); + } + + if (!wantApi && !wantMcp && !wantWorker) { + p.outro(pc.green("Services up. Skipping API/MCP/worker.")); + return; + } + + if (wantApi) { + await ensureConsoleBuilt(); + } + + const processEnv = envFileToProcessEnv(env); + if (!processEnv.ENV) { + processEnv.ENV = "development"; + } + if (!processEnv.CONSOLE_ENABLED) { + processEnv.CONSOLE_ENABLED = "true"; + } + const jobs = buildJobs({ wantApi, wantMcp, wantWorker, processEnv }); + + p.note( + jobs + .map((j) => `${pc.bold(j.name.padEnd(7))} ${pc.dim(`${j.bin} ${j.args.join(" ")}`)}`) + .join("\n") + + (wantApi + ? `\n\n${pc.cyan("Local console:")} http://localhost:${API_DEFAULT_PORT}/console` + : ""), + "Starting (Ctrl-C to stop everything)", + ); + + const results = await runParallel(jobs); + const failed = results.filter((r) => r.code !== 0 && r.signal === null); + if (failed.length > 0) { + p.outro( + pc.red( + `Exited: ${failed.map((f) => `${f.name}=${f.code}`).join(", ")}`, + ), + ); + process.exit(1); + } + p.outro(pc.green("All processes exited cleanly.")); +} + +function buildJobs(opts: { + wantApi: boolean; + wantMcp: boolean; + wantWorker: boolean; + processEnv: NodeJS.ProcessEnv; +}): Array<{ + name: string; + bin: string; + args: string[]; + cwd: string; + env: NodeJS.ProcessEnv; +}> { + const jobs: Array<{ + name: string; + bin: string; + args: string[]; + cwd: string; + env: NodeJS.ProcessEnv; + }> = []; + const cwd = sourcePath(); + + if (opts.wantApi) { + jobs.push({ + name: "api", + bin: venvBin("uvicorn"), + args: [ + "src.services.api.app:app", + "--host", + "0.0.0.0", + "--port", + String(API_DEFAULT_PORT), + ], + cwd, + env: opts.processEnv, + }); + } + if (opts.wantMcp) { + jobs.push({ + name: "mcp", + bin: venvBin("uvicorn"), + args: [ + "src.services.mcp.app:app", + "--host", + "0.0.0.0", + "--port", + String(MCP_DEFAULT_PORT), + ], + cwd, + env: opts.processEnv, + }); + } + if (opts.wantWorker) { + jobs.push({ + name: "worker", + bin: venvBin("celery"), + args: [ + "-A", + "src.workers.app", + "worker", + "--loglevel=info", + "--pool=threads", + ], + cwd, + env: opts.processEnv, + }); + } + return jobs; +} diff --git a/tui/src/commands/update.ts b/tui/src/commands/update.ts new file mode 100644 index 0000000000000000000000000000000000000000..0377137cb448ba4f7bfb5b61680488cd0a4eb7c0 --- /dev/null +++ b/tui/src/commands/update.ts @@ -0,0 +1,74 @@ +import * as p from "@clack/prompts"; +import pc from "picocolors"; +import { envFilePath, sourcePath } from "../lib/paths.js"; +import { readState } from "../lib/state.js"; +import { pullRepo } from "../lib/git.js"; +import { installDeps } from "../lib/python.js"; +import { readEnvFile } from "../lib/env.js"; +import { syncDevSourceIfAvailable } from "../lib/sync-source.js"; + +function getEnv( + env: Awaited>, + key: string, +): string | undefined { + const idx = env.keyIndex.get(key); + if (idx === undefined) return undefined; + const raw = (env.lines[idx] ?? "").split("=").slice(1).join("="); + return raw.trim().replace(/^"|"$/g, ""); +} + +async function extrasFromEnv(): Promise { + try { + const env = await readEnvFile(envFilePath()); + const ocr = getEnv(env, "OCR_MODE"); + const extras = ocr === "docling" ? ["docling-ocr"] : []; + const vector = getEnv(env, "VECTOR_DB"); + const data = getEnv(env, "DATA_DB"); + const graph = getEnv(env, "GRAPH_DB"); + if ( + vector === "postgresql" || + data === "postgresql" || + graph === "networkx" + ) { + extras.push("postgresql-backend"); + } + return extras; + } catch { + return []; + } +} + +export async function runUpdate(): Promise { + p.intro(pc.bgCyan(pc.black(" brainapi ")) + " " + pc.dim("update")); + + const state = await readState(); + if (!state || !state.cloned) { + p.cancel( + "No brainapi install detected. Run " + pc.cyan("brainapi init") + " first.", + ); + process.exit(1); + } + + const branch = state.branch || "main"; + const spinner = p.spinner(); + spinner.start(`Updating source from origin/${branch} in ${sourcePath()}`); + try { + await pullRepo(branch); + spinner.stop("Source updated"); + } catch (err) { + spinner.stop(pc.red("Source update failed")); + throw err; + } + + await syncDevSourceIfAvailable(); + + const extras = await extrasFromEnv(); + if (extras.length === 0) { + p.log.step("Reinstalling Python base dependencies"); + } else { + p.log.step(`Reinstalling Python base + extras (${extras.join(", ")})`); + } + await installDeps({ extras }); + + p.outro(pc.green("brainapi is up to date.")); +} diff --git a/tui/src/constants.ts b/tui/src/constants.ts new file mode 100644 index 0000000000000000000000000000000000000000..e3ebcb65c664d755c03911504583613509b6ee3a --- /dev/null +++ b/tui/src/constants.ts @@ -0,0 +1,261 @@ +export const DEFAULT_REPO_URL = "https://github.com/Lumen-Labs/brainapi2.git"; +export const DEFAULT_BRANCH = "main"; +export const MIN_PYTHON_VERSION: [number, number] = [3, 11]; +export const PYTHON_CANDIDATES = [ + "python3.13", + "python3.12", + "python3.11", + "python3", + "python", +]; + +export const OLLAMA_DEFAULT_HOST = "localhost"; +export const OLLAMA_DEFAULT_PORT = 11434; +export const OLLAMA_DEFAULT_SMALL_MODEL = "hf.co/unsloth/Qwen3-14B-GGUF:Q4_K_M"; +export const OLLAMA_DEFAULT_LARGE_MODEL = "hf.co/unsloth/Qwen3-14B-GGUF:Q4_K_M"; + +export const GCP_DEFAULT_SMALL_MODEL = "gemini-3-flash-preview"; +export const GCP_DEFAULT_LARGE_MODEL = "gemini-3-flash-preview"; +export const GCP_DEFAULT_EMBEDDING_MODEL = "text-embedding-005"; + +export const AZURE_DEFAULT_LARGE_MODEL = "gpt-4o"; +export const AZURE_DEFAULT_SMALL_MODEL = "gpt-4o-mini"; +export const AZURE_DEFAULT_LARGE_API_VERSION = "2024-12-01-preview"; +export const AZURE_DEFAULT_EMBEDDING_MODEL = "text-embedding-3-large"; + +export const OPENAI_DEFAULT_SMALL_MODEL = "gpt-4o-mini"; +export const OPENAI_DEFAULT_LARGE_MODEL = "gpt-4o"; +export const OPENAI_DEFAULT_EMBEDDING_MODEL = "text-embedding-3-large"; +export const ANTHROPIC_DEFAULT_SMALL_MODEL = "claude-3-5-haiku-latest"; +export const ANTHROPIC_DEFAULT_LARGE_MODEL = "claude-sonnet-4-20250514"; +export const DEEPSEEK_DEFAULT_SMALL_MODEL = "deepseek-v4-flash"; +export const DEEPSEEK_DEFAULT_LARGE_MODEL = "deepseek-v4-pro"; + +export const KNOWN_EMBEDDING_MODEL_DIMENSIONS: Record = { + "nomic-embed-text": 768, + "mxbai-embed-large": 1024, + "all-minilm": 384, + "text-embedding-3-large": 3072, + "text-embedding-3-small": 1536, + "text-embedding-ada-002": 1536, + "text-embedding-005": 768, + "text-embedding-004": 768, + "amazon.titan-embed-text-v2:0": 1024, + "amazon.titan-embed-text-v1": 1536, + "intfloat/e5-small": 384, + "paraphrase-multilingual-minilm-l12-v2": 384, +}; + +export function lookupEmbeddingDimensions(model: string): number | undefined { + const normalized = model.trim().toLowerCase(); + if (normalized in KNOWN_EMBEDDING_MODEL_DIMENSIONS) { + return KNOWN_EMBEDDING_MODEL_DIMENSIONS[normalized]; + } + const entries = Object.entries(KNOWN_EMBEDDING_MODEL_DIMENSIONS).sort( + (a, b) => b[0].length - a[0].length, + ); + for (const [key, dimensions] of entries) { + if (normalized.includes(key.toLowerCase())) { + return dimensions; + } + } + return undefined; +} + +export const OPENAI_EMBEDDING_DIMENSIONS = { + nodesDimension: 3072, + tripletsDimension: 3072, + observationsDimension: 3072, + dataDimension: 3072, + relationshipsDimension: 3072, +} as const; + +export const OPENAI_EMBEDDING_SMALL_DIMENSIONS = { + nodesDimension: 1536, + tripletsDimension: 1536, + observationsDimension: 1536, + dataDimension: 1536, + relationshipsDimension: 1536, +} as const; + +export const BEDROCK_DEFAULT_REGION = "us-east-1"; +export const BEDROCK_DEFAULT_SMALL_MODEL = "us.anthropic.claude-3-5-haiku-20241022-v1:0"; +export const BEDROCK_DEFAULT_LARGE_MODEL = "us.anthropic.claude-3-7-sonnet-20250219-v1:0"; +export const BEDROCK_DEFAULT_EMBEDDING_MODEL = "amazon.titan-embed-text-v2:0"; + +export const EMBEDDINGS_DEFAULTS = { + smallModel: "paraphrase-multilingual-MiniLM-L12-v2", + localModel: "nomic-embed-text:latest", + nodesDimension: 3072, + tripletsDimension: 3072, + observationsDimension: 3072, + dataDimension: 3072, + relationshipsDimension: 3072, +} as const; + +export const OLLAMA_EMBEDDING_DIMENSIONS = { + nodesDimension: 768, + tripletsDimension: 768, + observationsDimension: 768, + dataDimension: 768, + relationshipsDimension: 768, +} as const; + +export const SERVICE_DEFAULTS = { + redis: { host: "localhost", port: 6379 }, + postgresql: { + host: "localhost", + port: 5432, + username: "postgres", + password: "password", + systemDatabase: "brainapi", + maintenanceDatabase: "postgres", + }, + neo4j: { + host: "localhost", + port: 7687, + username: "neo4j", + password: "your_password", + }, + milvus: { + host: "localhost", + port: 19530, + }, + mongo: { + host: "localhost", + port: 27017, + username: "root", + password: "password", + }, + rabbitmq: { + host: "localhost", + port: 5672, + username: "kalo", + password: "kalo", + }, +} as const; + +export const ENV_KEYS = { + graphDb: "GRAPH_DB", + dataDb: "DATA_DB", + vectorDb: "VECTOR_DB", + modelsMode: "MODELS_MODE", + brainpatToken: "BRAINPAT_TOKEN", + pipelineMode: "PIPELINE_MODE", + ocrMode: "OCR_MODE", + agenticArchitecture: "AGENTIC_ARCHITECTURE", + runGraphConsolidator: "RUN_GRAPH_CONSOLIDATOR", + + redisHost: "REDIS_HOST", + redisPort: "REDIS_PORT", + celeryBackend: "CELERY_BACKEND", + celeryWorkerConcurrency: "CELERY_WORKER_CONCURRENCY", + celeryUnblockedWindowSeconds: "CELERY_UNBLOCKED_WINDOW_SECONDS", + celeryUnblockedMaxRetries: "CELERY_UNBLOCKED_MAX_RETRIES", + celeryQueueHealthThreshold: "CELERY_QUEUE_HEALTH_THRESHOLD", + + postgresHost: "POSTGRES_HOST", + postgresPort: "POSTGRES_PORT", + postgresUsername: "POSTGRES_USERNAME", + postgresPassword: "POSTGRES_PASSWORD", + postgresSystemDatabase: "POSTGRES_SYSTEM_DATABASE", + postgresMaintenanceDatabase: "POSTGRES_MAINTENANCE_DATABASE", + + neo4jHost: "NEO4J_HOST", + neo4jPort: "NEO4J_PORT", + neo4jUsername: "NEO4J_USERNAME", + neo4jPassword: "NEO4J_PASSWORD", + + milvusHost: "MILVUS_HOST", + milvusPort: "MILVUS_PORT", + milvusUri: "MILVUS_URI", + milvusToken: "MILVUS_TOKEN", + + mongoHost: "MONGO_HOST", + mongoPort: "MONGO_PORT", + mongoUsername: "MONGO_USERNAME", + mongoPassword: "MONGO_PASSWORD", + mongoConnectionString: "MONGO_CONNECTION_STRING", + mongoSystemDatabase: "MONGO_SYSTEM_DATABASE", + + ollamaHost: "OLLAMA_HOST", + ollamaPort: "OLLAMA_PORT", + ollamaSmallModel: "OLLAMA_LLM_SMALL_MODEL", + ollamaLargeModel: "OLLAMA_LLM_LARGE_MODEL", + + gcpProjectId: "GCP_PROJECT_ID", + gcpCredentialsPath: "GCP_CREDENTIALS_PATH", + llmSmallProvider: "LLM_SMALL_PROVIDER", + llmLargeProvider: "LLM_LARGE_PROVIDER", + embeddingsProvider: "EMBEDDINGS_PROVIDER", + gcpExtraSmallLlmModel: "GCP_EXTRA_SMALL_LLM_MODEL", + gcpSmallLlmModel: "GCP_SMALL_LLM_MODEL", + gcpLargeLlmModel: "GCP_LARGE_LLM_MODEL", + gcpEmbeddingModel: "GCP_EMBEDDING_MODEL", + + azureSmallLlmModel: "AZURE_SMALL_LLM_MODEL", + azureLargeLlmModel: "AZURE_LARGE_LLM_MODEL", + azureLlmApiVersion: "AZURE_LLM_API_VERSION", + azureLlmEndpoint: "AZURE_LLM_ENDPOINT", + azureLlmSubscriptionKey: "AZURE_LLM_SUBSCRIPTION_KEY", + azureLargeLlmApiVersion: "AZURE_LARGE_LLM_API_VERSION", + azureLargeLlmEndpoint: "AZURE_LARGE_LLM_ENDPOINT", + azureLargeLlmSubscriptionKey: "AZURE_LARGE_LLM_SUBSCRIPTION_KEY", + azureEmbeddingModel: "AZURE_EMBEDDING_MODEL", + azureEmbeddingFullEndpoint: "AZURE_EMBEDDING_FULL_ENDPOINT", + azureEmbeddingKey: "AZURE_EMBEDDING_KEY", + + openaiApiKey: "OPENAI_API_KEY", + openaiBaseUrl: "OPENAI_BASE_URL", + openaiSmallLlmModel: "OPENAI_SMALL_LLM_MODEL", + openaiLargeLlmModel: "OPENAI_LARGE_LLM_MODEL", + openaiEmbeddingModel: "OPENAI_EMBEDDING_MODEL", + anthropicApiKey: "ANTHROPIC_API_KEY", + anthropicSmallLlmModel: "ANTHROPIC_SMALL_LLM_MODEL", + anthropicLargeLlmModel: "ANTHROPIC_LARGE_LLM_MODEL", + deepseekApiKey: "DEEPSEEK_API_KEY", + deepseekSmallLlmModel: "DEEPSEEK_SMALL_LLM_MODEL", + deepseekLargeLlmModel: "DEEPSEEK_LARGE_LLM_MODEL", + + bedrockRegion: "BEDROCK_REGION", + bedrockAccessKeyId: "BEDROCK_ACCESS_KEY_ID", + bedrockSecretAccessKey: "BEDROCK_SECRET_ACCESS_KEY", + bedrockSessionToken: "BEDROCK_SESSION_TOKEN", + bedrockSmallLlmModel: "BEDROCK_SMALL_LLM_MODEL", + bedrockLargeLlmModel: "BEDROCK_LARGE_LLM_MODEL", + bedrockEmbeddingModel: "BEDROCK_EMBEDDING_MODEL", + + embeddingsFullEndpoint: "EMBEDDINGS_FULL_ENDPOINT", + embeddingsKey: "EMBEDDINGS_KEY", + + embeddingsSmallModel: "EMBEDDINGS_SMALL_MODEL", + embeddingsLocalModel: "EMBEDDINGS_LOCAL_MODEL", + embeddingNodesDimension: "EMBEDDING_NODES_DIMENSION", + embeddingTripletsDimension: "EMBEDDING_TRIPLETS_DIMENSION", + embeddingObservationsDimension: "EMBEDDING_OBSERVATIONS_DIMENSION", + embeddingDataDimension: "EMBEDDING_DATA_DIMENSION", + embeddingRelationshipsDimension: "EMBEDDING_RELATIONSHIPS_DIMENSION", + enabledPlugins: "ENABLED_PLUGINS", + searchEnabled: "SEARCH_ENABLED", + searchUseDense: "SEARCH_USE_DENSE", + searchUseBm25: "SEARCH_USE_BM25", + searchFusion: "SEARCH_FUSION", + searchFusionAlpha: "SEARCH_FUSION_ALPHA", + searchBm25K1: "SEARCH_BM25_K1", + searchBm25B: "SEARCH_BM25_B", + contextPassageMode: "CONTEXT_PASSAGE_MODE", +} as const; + +export const SERVICE_COMPOSE_FILES = { + redis: "src/lib/redis/docker-compose.yaml", + postgresql: "src/lib/postgresql/docker-compose.yaml", + neo4j: "src/lib/neo4j/docker-compose.yaml", + milvus: "src/lib/milvus/docker-compose.yaml", + mongo: "src/lib/mongo/docker-compose.yaml", + rabbitmq: "src/lib/rabbitmq/docker-compose.yaml", +} as const; + +export type ServiceName = keyof typeof SERVICE_COMPOSE_FILES; + +export const API_DEFAULT_PORT = 8000; +export const MCP_DEFAULT_PORT = 8001; diff --git a/tui/src/embedded/install-extras-script.ts b/tui/src/embedded/install-extras-script.ts new file mode 100644 index 0000000000000000000000000000000000000000..a2a8fedcc155f594484378323535c84950a683fd --- /dev/null +++ b/tui/src/embedded/install-extras-script.ts @@ -0,0 +1,4 @@ +// Auto-generated from scripts/install_extras.py — do not edit by hand. +// Regenerate with: npm run sync:install-extras (also runs as prebuild). + +export const INSTALL_EXTRAS_SCRIPT = "\"\"\"\nInstall only the pyproject.toml optional-dependency groups that the active\n.env requires.\n\nUsage (no TUI required):\n\n python scripts/install_extras.py # auto-detect from .env\n python scripts/install_extras.py docling-ocr # force a specific group\n python scripts/install_extras.py --all # install every optional group\n python scripts/install_extras.py --dry-run # print pip command, don't run\n python scripts/install_extras.py --check # exit non-zero if missing\n\nReads ./.env (or $BRAINAPI_DOTENV) for OCR_MODE and any future toggles, then\ncalls `python -m pip install` against the active interpreter with the matching\nspecs from pyproject.toml's [project.optional-dependencies].\n\nSafe to re-run: pip is a no-op when the specs are already satisfied.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport argparse\nimport importlib.util\nimport os\nimport re\nimport subprocess\nimport sys\nfrom pathlib import Path\n\ntry:\n import tomllib\nexcept ModuleNotFoundError:\n import tomli as tomllib # type: ignore[no-redef]\n\n\nREPO_ROOT = Path(__file__).resolve().parent.parent\nPYPROJECT_PATH = REPO_ROOT / \"pyproject.toml\"\nDEFAULT_ENV_PATH = Path(os.environ.get(\"BRAINAPI_DOTENV\", REPO_ROOT / \".env\"))\n\n\ndef parse_dotenv(path: Path) -> dict[str, str]:\n if not path.exists():\n return {}\n result: dict[str, str] = {}\n for raw in path.read_text(encoding=\"utf-8\").splitlines():\n line = raw.strip()\n if not line or line.startswith(\"#\") or \"=\" not in line:\n continue\n key, _, value = line.partition(\"=\")\n result[key.strip()] = value.strip().strip('\"').strip(\"'\")\n return result\n\n\ndef _load_pyproject() -> dict:\n if not PYPROJECT_PATH.exists():\n return {}\n with PYPROJECT_PATH.open(\"rb\") as fh:\n return tomllib.load(fh)\n\n\ndef _poetry_dep_to_spec(name: str, value) -> str | None:\n if name == \"python\":\n return None\n if isinstance(value, str):\n return name if value == \"*\" else f\"{name}{value}\"\n if isinstance(value, dict):\n version = value.get(\"version\", \"\")\n extras = value.get(\"extras\", []) or []\n head = name + (f\"[{','.join(extras)}]\" if extras else \"\")\n if not version or version == \"*\":\n return head\n return f\"{head}{version}\"\n return None\n\n\ndef load_base_deps() -> list[str]:\n data = _load_pyproject()\n project_deps = data.get(\"project\", {}).get(\"dependencies\", []) or []\n if project_deps:\n return list(project_deps)\n poetry_deps = (\n data.get(\"tool\", {}).get(\"poetry\", {}).get(\"dependencies\", {}) or {}\n )\n out: list[str] = []\n for name, value in poetry_deps.items():\n spec = _poetry_dep_to_spec(name, value)\n if spec:\n out.append(spec)\n return out\n\n\ndef load_optional_groups() -> dict[str, list[str]]:\n data = _load_pyproject()\n project_optional = (\n data.get(\"project\", {}).get(\"optional-dependencies\", {}) or {}\n )\n if project_optional:\n return {k: list(v) for k, v in project_optional.items()}\n poetry_groups = data.get(\"tool\", {}).get(\"poetry\", {}).get(\"group\", {}) or {}\n out: dict[str, list[str]] = {}\n for group_name, group_value in poetry_groups.items():\n if not isinstance(group_value, dict):\n continue\n deps = group_value.get(\"dependencies\", {}) or {}\n specs: list[str] = []\n for name, value in deps.items():\n spec = _poetry_dep_to_spec(name, value)\n if spec:\n specs.append(spec)\n if specs:\n out[group_name] = specs\n return out\n\n\nPOSTGRESQL_BACKEND_SPECS = [\n \"psycopg2-binary>=2.9.6,<3.0.0\",\n \"pgvector>=0.3.0,<1.0.0\",\n \"networkx>=3.0,<4.0.0\",\n]\n\n\ndef uses_postgresql_backend(env: dict[str, str]) -> bool:\n vector = env.get(\"VECTOR_DB\", \"milvus\").lower()\n data = env.get(\"DATA_DB\", \"mongo\").lower()\n graph = env.get(\"GRAPH_DB\", \"neo4j\").lower()\n return vector == \"postgresql\" or data == \"postgresql\" or graph == \"networkx\"\n\n\ndef required_groups(env: dict[str, str]) -> list[str]:\n groups: list[str] = []\n if env.get(\"OCR_MODE\", \"docparser\").lower() == \"docling\":\n groups.append(\"docling-ocr\")\n if uses_postgresql_backend(env):\n groups.append(\"postgresql-backend\")\n return groups\n\n\ndef required_backend_specs(env: dict[str, str]) -> list[str]:\n if not uses_postgresql_backend(env):\n return []\n optional = load_optional_groups()\n if \"postgresql-backend\" in optional:\n return []\n return list(POSTGRESQL_BACKEND_SPECS)\n\n\ndef package_name_from_spec(spec: str) -> str:\n spec = spec.strip()\n match = re.match(r\"^([A-Za-z0-9][A-Za-z0-9._-]*)\", spec)\n if match:\n return match.group(1)\n return spec\n\n\ndef package_installed(name: str) -> bool:\n pkg = package_name_from_spec(name)\n try:\n from importlib.metadata import PackageNotFoundError, version\n\n version(pkg)\n return True\n except PackageNotFoundError:\n pass\n except Exception:\n pass\n import_names = {pkg.replace(\"-\", \"_\"), pkg}\n if pkg == \"psycopg2-binary\":\n import_names.add(\"psycopg2\")\n return any(importlib.util.find_spec(c) is not None for c in import_names)\n\n\ndef filter_missing(specs: list[str]) -> list[str]:\n return [s for s in specs if not package_installed(package_name_from_spec(s))]\n\n\ndef install_specs(specs: list[str], dry_run: bool) -> int:\n if not specs:\n return 0\n cmd = [sys.executable, \"-m\", \"pip\", \"install\", *specs]\n print(f\"$ {' '.join(cmd)}\")\n if dry_run:\n return 0\n return subprocess.call(cmd)\n\n\ndef main(argv: list[str]) -> int:\n parser = argparse.ArgumentParser(description=__doc__)\n parser.add_argument(\n \"groups\",\n nargs=\"*\",\n help=\"Optional groups to install. Omit to auto-detect from .env.\",\n )\n parser.add_argument(\n \"--env-file\",\n default=str(DEFAULT_ENV_PATH),\n help=\"Path to .env (default: ./.env, or $BRAINAPI_DOTENV).\",\n )\n parser.add_argument(\n \"--dry-run\", action=\"store_true\",\n help=\"Print the pip command without running it.\",\n )\n parser.add_argument(\n \"--check\", action=\"store_true\",\n help=\"Exit non-zero if any required extras are missing; do not install.\",\n )\n parser.add_argument(\n \"--all\", action=\"store_true\",\n help=\"Install every optional group declared in pyproject.toml.\",\n )\n parser.add_argument(\n \"--include-base\", action=\"store_true\",\n help=\"Also install [project].dependencies / [tool.poetry.dependencies] base deps.\",\n )\n parser.add_argument(\n \"--upgrade-pip\", action=\"store_true\",\n help=\"Run `pip install --upgrade pip` before installing anything.\",\n )\n args = parser.parse_args(argv)\n\n optional_groups = load_optional_groups()\n\n if args.all:\n wanted = list(optional_groups.keys())\n elif args.groups:\n wanted = args.groups\n else:\n env = parse_dotenv(Path(args.env_file))\n wanted = required_groups(env)\n if wanted:\n print(f\"Auto-detected required groups from {args.env_file}: {wanted}\")\n elif not args.include_base:\n print(\n f\"No optional groups required by {args.env_file} \"\n f\"(OCR_MODE={env.get('OCR_MODE', 'docparser')}).\",\n )\n\n unknown = [g for g in wanted if g not in optional_groups]\n known = [g for g in wanted if g in optional_groups]\n if unknown:\n print(\n f\"Skipping optional groups not declared in pyproject.toml: {unknown}\",\n file=sys.stderr,\n )\n if optional_groups:\n print(\n f\"Available optional groups: {list(optional_groups.keys())}\",\n file=sys.stderr,\n )\n else:\n print(\n \"No optional-dependency groups are declared in pyproject.toml. \"\n \"These packages may already be part of base dependencies — \"\n \"continuing without them.\",\n file=sys.stderr,\n )\n\n wanted = known\n extra_specs: list[str] = []\n for g in wanted:\n extra_specs.extend(optional_groups[g])\n\n env = parse_dotenv(Path(args.env_file))\n backend_specs = (\n required_backend_specs(env) if not args.groups and not args.all else []\n )\n\n if args.check:\n missing = filter_missing([*extra_specs, *backend_specs])\n if missing:\n print(f\"Missing extras: {missing}\", file=sys.stderr)\n hint_groups = wanted or (\n [\"postgresql-backend\"] if backend_specs else []\n )\n if hint_groups:\n print(\n f\"Install them with: {sys.executable} \"\n f\"scripts/install_extras.py {' '.join(hint_groups)}\",\n file=sys.stderr,\n )\n return 1\n print(\"All required extras are installed.\")\n return 0\n\n base_specs = load_base_deps() if args.include_base else []\n all_specs = [*base_specs, *extra_specs, *backend_specs]\n\n if not all_specs:\n return 0\n\n if args.upgrade_pip and not args.dry_run:\n subprocess.call([sys.executable, \"-m\", \"pip\", \"install\", \"--upgrade\", \"pip\"])\n\n return install_specs(all_specs, args.dry_run)\n\n\nif __name__ == \"__main__\":\n sys.exit(main(sys.argv[1:]))\n"; diff --git a/tui/src/flows/auth.ts b/tui/src/flows/auth.ts new file mode 100644 index 0000000000000000000000000000000000000000..4ff44113bf09f78e17b4c04cdb9de5158f3dd1fb --- /dev/null +++ b/tui/src/flows/auth.ts @@ -0,0 +1,66 @@ +import { randomBytes } from "node:crypto"; +import * as p from "@clack/prompts"; +import pc from "picocolors"; +import { askPassword, isPromptBack, pickOne, type PromptBack } from "../lib/prompts.js"; +import type { AuthChoices } from "../types.js"; + +function generateToken(): string { + return `brainpat_${randomBytes(24).toString("hex")}`; +} + +export async function askAuth(opts?: { + allowBack?: false; + prechosenToken?: string; +}): Promise; +export async function askAuth(opts: { + allowBack: true; + backHint?: string; + prechosenToken?: string; +}): Promise; +export async function askAuth(opts?: { + allowBack?: boolean; + backHint?: string; + prechosenToken?: string; +}): Promise { + if (opts?.prechosenToken) { + return { brainpatToken: opts.prechosenToken }; + } + + p.log.step("Authentication token"); + + const action = opts?.allowBack + ? await pickOne<"generate" | "paste">({ + message: "BRAINPAT_TOKEN (used by the API for personal auth)", + options: [ + { value: "generate", label: "Generate one for me" }, + { value: "paste", label: "Paste my own" }, + ], + initialValue: "generate", + allowBack: true, + backHint: opts.backHint, + }) + : await pickOne<"generate" | "paste">({ + message: "BRAINPAT_TOKEN (used by the API for personal auth)", + options: [ + { value: "generate", label: "Generate one for me" }, + { value: "paste", label: "Paste my own" }, + ], + initialValue: "generate", + }); + if (isPromptBack(action)) { + return action; + } + + if (action === "generate") { + const token = generateToken(); + p.note(token, "Generated token (saved to .env)"); + p.log.info(pc.dim("Keep this somewhere safe — it grants API access.")); + return { brainpatToken: token }; + } + + const pasted = await askPassword({ + message: "Paste your BRAINPAT_TOKEN", + validate: (value) => (value.trim().length === 0 ? "Token is required" : undefined), + }); + return { brainpatToken: pasted.trim() }; +} diff --git a/tui/src/flows/connections.ts b/tui/src/flows/connections.ts new file mode 100644 index 0000000000000000000000000000000000000000..9a1f5d473bb23dafb0ae7109489c35a6ff844115 --- /dev/null +++ b/tui/src/flows/connections.ts @@ -0,0 +1,297 @@ +import * as p from "@clack/prompts"; +import { SERVICE_DEFAULTS } from "../constants.js"; +import type { InitFlagOverrides } from "../lib/init-flags.js"; +import { askPassword, askText, pickOne } from "../lib/prompts.js"; +import type { + Connections, + DbChoices, + MilvusConnection, + MongoConnection, + Neo4jConnection, + PostgresConnection, + RedisConnection, +} from "../types.js"; + +function portValidator(value: string): string | undefined { + const n = Number(value); + if (!Number.isInteger(n) || n <= 0 || n > 65535) + return "Port must be 1-65535"; + return undefined; +} + +async function askRedis(flags?: InitFlagOverrides): Promise { + p.log.step("Redis (required for cache + Celery)"); + let host = flags?.redisHost; + if (host === undefined) { + const raw = await askText({ + message: "Redis host", + placeholder: SERVICE_DEFAULTS.redis.host, + defaultValue: SERVICE_DEFAULTS.redis.host, + }); + host = raw.trim() || SERVICE_DEFAULTS.redis.host; + } + let port = flags?.redisPort; + if (port === undefined) { + const raw = await askText({ + message: "Redis port", + placeholder: String(SERVICE_DEFAULTS.redis.port), + defaultValue: String(SERVICE_DEFAULTS.redis.port), + validate: portValidator, + }); + port = Number(raw) || SERVICE_DEFAULTS.redis.port; + } + return { host, port }; +} + +async function askPostgres(flags?: InitFlagOverrides): Promise { + p.log.step("PostgreSQL"); + p.note( + [ + "BrainAPI uses a single Postgres database to host all brains.", + "Each brain is isolated logically via a `brain_id` column, not via separate databases.", + "Press Enter to accept the default name.", + ].join("\n"), + "Heads up" + ); + let host = flags?.postgresHost; + if (host === undefined) { + const raw = await askText({ + message: "Postgres host", + placeholder: SERVICE_DEFAULTS.postgresql.host, + defaultValue: SERVICE_DEFAULTS.postgresql.host, + }); + host = raw.trim() || SERVICE_DEFAULTS.postgresql.host; + } + let port = flags?.postgresPort; + if (port === undefined) { + const raw = await askText({ + message: "Postgres port", + placeholder: String(SERVICE_DEFAULTS.postgresql.port), + defaultValue: String(SERVICE_DEFAULTS.postgresql.port), + validate: portValidator, + }); + port = Number(raw) || SERVICE_DEFAULTS.postgresql.port; + } + let username = flags?.postgresUsername; + if (username === undefined) { + const raw = await askText({ + message: "Postgres username", + placeholder: SERVICE_DEFAULTS.postgresql.username, + defaultValue: SERVICE_DEFAULTS.postgresql.username, + }); + username = raw.trim() || SERVICE_DEFAULTS.postgresql.username; + } + let password = flags?.postgresPassword; + if (password === undefined) { + password = await askPassword({ + message: "Postgres password (default is 'password')", + validate: (value) => + value.length === 0 ? "Password is required" : undefined, + }); + } + let systemDatabase = flags?.postgresSystemDb; + if (systemDatabase === undefined) { + const raw = await askText({ + message: `System database (brains registry, default ${SERVICE_DEFAULTS.postgresql.systemDatabase})`, + placeholder: SERVICE_DEFAULTS.postgresql.systemDatabase, + defaultValue: SERVICE_DEFAULTS.postgresql.systemDatabase, + }); + systemDatabase = + raw.trim() || SERVICE_DEFAULTS.postgresql.systemDatabase; + } + let maintenanceDatabase = flags?.postgresMaintenanceDb; + if (maintenanceDatabase === undefined) { + const raw = await askText({ + message: `Maintenance database (for CREATE DATABASE, default ${SERVICE_DEFAULTS.postgresql.maintenanceDatabase})`, + placeholder: SERVICE_DEFAULTS.postgresql.maintenanceDatabase, + defaultValue: SERVICE_DEFAULTS.postgresql.maintenanceDatabase, + }); + maintenanceDatabase = + raw.trim() || SERVICE_DEFAULTS.postgresql.maintenanceDatabase; + } + return { + host, + port, + username, + password, + systemDatabase, + maintenanceDatabase, + }; +} + +async function askNeo4j(flags?: InitFlagOverrides): Promise { + p.log.step("Neo4j"); + let host = flags?.neo4jHost; + if (host === undefined) { + const raw = await askText({ + message: "Neo4j host", + placeholder: SERVICE_DEFAULTS.neo4j.host, + defaultValue: SERVICE_DEFAULTS.neo4j.host, + }); + host = raw.trim() || SERVICE_DEFAULTS.neo4j.host; + } + let port = flags?.neo4jPort; + if (port === undefined) { + const raw = await askText({ + message: "Neo4j Bolt port", + placeholder: String(SERVICE_DEFAULTS.neo4j.port), + defaultValue: String(SERVICE_DEFAULTS.neo4j.port), + validate: portValidator, + }); + port = Number(raw) || SERVICE_DEFAULTS.neo4j.port; + } + let username = flags?.neo4jUsername; + if (username === undefined) { + const raw = await askText({ + message: "Neo4j username", + placeholder: SERVICE_DEFAULTS.neo4j.username, + defaultValue: SERVICE_DEFAULTS.neo4j.username, + }); + username = raw.trim() || SERVICE_DEFAULTS.neo4j.username; + } + let password = flags?.neo4jPassword; + if (password === undefined) { + password = await askPassword({ + message: "Neo4j password (default is 'your_password')", + validate: (value) => + value.length === 0 ? "Password is required" : undefined, + }); + } + return { host, port, username, password }; +} + +async function askMilvus(flags?: InitFlagOverrides): Promise { + p.log.step("Milvus"); + + let target = flags?.milvusDeployment; + if (flags?.milvusUri || flags?.milvusToken) { + target = "managed"; + } + if (target === undefined) { + target = await pickOne<"local" | "managed">({ + message: "Milvus deployment", + options: [ + { value: "local", label: "Local docker compose" }, + { value: "managed", label: "Managed (Zilliz Cloud) — uri + token" }, + ], + initialValue: "local", + }); + } + + if (target === "managed") { + let uri = flags?.milvusUri; + if (uri === undefined) { + uri = ( + await askText({ + message: "Milvus URI", + placeholder: "https://your-cluster.api.gcp-us-west1.zillizcloud.com", + validate: (value) => + value.trim().length === 0 ? "URI is required" : undefined, + }) + ).trim(); + } + let token = flags?.milvusToken; + if (token === undefined) { + token = await askPassword({ + message: "Milvus token", + validate: (value) => + value.length === 0 ? "Token is required" : undefined, + }); + } + return { + host: flags?.milvusHost ?? SERVICE_DEFAULTS.milvus.host, + port: flags?.milvusPort ?? SERVICE_DEFAULTS.milvus.port, + uri, + token, + }; + } + + let host = flags?.milvusHost; + if (host === undefined) { + const raw = await askText({ + message: "Milvus host", + placeholder: SERVICE_DEFAULTS.milvus.host, + defaultValue: SERVICE_DEFAULTS.milvus.host, + }); + host = raw.trim() || SERVICE_DEFAULTS.milvus.host; + } + let port = flags?.milvusPort; + if (port === undefined) { + const raw = await askText({ + message: "Milvus port", + placeholder: String(SERVICE_DEFAULTS.milvus.port), + defaultValue: String(SERVICE_DEFAULTS.milvus.port), + validate: portValidator, + }); + port = Number(raw) || SERVICE_DEFAULTS.milvus.port; + } + return { host, port }; +} + +async function askMongo(flags?: InitFlagOverrides): Promise { + p.log.step("MongoDB"); + let host = flags?.mongoHost; + if (host === undefined) { + const raw = await askText({ + message: "Mongo host", + placeholder: SERVICE_DEFAULTS.mongo.host, + defaultValue: SERVICE_DEFAULTS.mongo.host, + }); + host = raw.trim() || SERVICE_DEFAULTS.mongo.host; + } + let port = flags?.mongoPort; + if (port === undefined) { + const raw = await askText({ + message: "Mongo port", + placeholder: String(SERVICE_DEFAULTS.mongo.port), + defaultValue: String(SERVICE_DEFAULTS.mongo.port), + validate: portValidator, + }); + port = Number(raw) || SERVICE_DEFAULTS.mongo.port; + } + let username = flags?.mongoUsername; + if (username === undefined) { + const raw = await askText({ + message: "Mongo username", + placeholder: SERVICE_DEFAULTS.mongo.username, + defaultValue: SERVICE_DEFAULTS.mongo.username, + }); + username = raw.trim() || SERVICE_DEFAULTS.mongo.username; + } + let password = flags?.mongoPassword; + if (password === undefined) { + password = await askPassword({ + message: "Mongo password (default is 'password')", + validate: (value) => + value.length === 0 ? "Password is required" : undefined, + }); + } + return { host, port, username, password }; +} + +export async function askConnections( + dbs: DbChoices, + flags?: InitFlagOverrides, +): Promise { + const redis = await askRedis(flags); + const result: Connections = { redis }; + + const needsPostgres = + dbs.vectorDb === "postgresql" || + dbs.dataDb === "postgresql" || + dbs.graphDb === "networkx"; + if (needsPostgres) { + result.postgresql = await askPostgres(flags); + } + if (dbs.graphDb === "neo4j") { + result.neo4j = await askNeo4j(flags); + } + if (dbs.vectorDb === "milvus") { + result.milvus = await askMilvus(flags); + } + if (dbs.dataDb === "mongo") { + result.mongo = await askMongo(flags); + } + + return result; +} diff --git a/tui/src/flows/defaults.ts b/tui/src/flows/defaults.ts new file mode 100644 index 0000000000000000000000000000000000000000..87877472af9e74b4c20c1f9c2757e0ee518c7c35 --- /dev/null +++ b/tui/src/flows/defaults.ts @@ -0,0 +1,78 @@ +import * as p from "@clack/prompts"; +import pc from "picocolors"; +import { + askConfirm, + askConfirmOrBack, + isPromptBack, + pickOne, + type PromptBack, +} from "../lib/prompts.js"; +import type { DbChoices } from "../types.js"; + +export async function askUseDefaults(opts?: { + allowBack?: false; +}): Promise; +export async function askUseDefaults(opts: { + allowBack: true; + backHint?: string; +}): Promise; +export async function askUseDefaults(opts?: { + allowBack?: boolean; + backHint?: string; +}): Promise { + const choice = opts?.allowBack + ? await pickOne<"defaults" | "custom">({ + message: "How would you like to configure BrainAPI?", + options: [ + { value: "defaults", label: "Use default settings", hint: "Postgres + pgvector + NetworkX + remote LLM" }, + { value: "custom", label: "Configure each option manually" }, + ], + initialValue: "defaults", + allowBack: true, + backHint: opts.backHint, + }) + : await pickOne<"defaults" | "custom">({ + message: "How would you like to configure BrainAPI?", + options: [ + { value: "defaults", label: "Use default settings", hint: "Postgres + pgvector + NetworkX + remote LLM" }, + { value: "custom", label: "Configure each option manually" }, + ], + initialValue: "defaults", + }); + + if (isPromptBack(choice)) { + return choice; + } + + if (choice === "custom") return false; + + p.note( + [ + "Stack: NetworkX (on Postgres) + pgvector + Postgres data layer + remote LLM.", + "Backing services: " + pc.cyan("Redis") + " + " + pc.cyan("Postgres") + ".", + "", + "You'll still be asked how to run those services (Docker or manual),", + "for the remote LLM provider, GCP credentials, and a BRAINPAT_TOKEN.", + ].join("\n"), + "Defaults", + ); + + if (opts?.allowBack) { + return askConfirmOrBack({ + message: "Continue with defaults?", + initialValue: true, + backHint: opts.backHint, + }); + } + + return askConfirm({ + message: "Continue with defaults?", + initialValue: true, + }); +} + +export const DEFAULT_DBS: DbChoices = { + vectorDb: "postgresql", + dataDb: "postgresql", + graphDb: "networkx", +}; diff --git a/tui/src/flows/embedding-dimensions.ts b/tui/src/flows/embedding-dimensions.ts new file mode 100644 index 0000000000000000000000000000000000000000..d37cae5d783dd4e184c36b61568b166ee0a572bc --- /dev/null +++ b/tui/src/flows/embedding-dimensions.ts @@ -0,0 +1,151 @@ +import * as p from "@clack/prompts"; +import { + AZURE_DEFAULT_EMBEDDING_MODEL, + BEDROCK_DEFAULT_EMBEDDING_MODEL, + EMBEDDINGS_DEFAULTS, + GCP_DEFAULT_EMBEDDING_MODEL, + KNOWN_EMBEDDING_MODEL_DIMENSIONS, + lookupEmbeddingDimensions, + OPENAI_DEFAULT_EMBEDDING_MODEL, +} from "../constants.js"; +import { + askConfirm, + askConfirmOrBack, + askText, + isPromptBack, + type PromptBack, +} from "../lib/prompts.js"; +import type { EmbeddingDimensions, ModelsChoices, ModelProvider } from "../types.js"; + +function dimensionsFromSize(size: number): EmbeddingDimensions { + return { + nodesDimension: size, + tripletsDimension: size, + observationsDimension: size, + dataDimension: size, + relationshipsDimension: size, + }; +} + +async function askDimensionSize(initial?: number): Promise { + const raw = await askText({ + message: "Embedding vector dimensions", + placeholder: initial ? String(initial) : "3072", + defaultValue: initial ? String(initial) : "3072", + validate: (value) => { + const n = Number(value.trim()); + if (!Number.isInteger(n) || n <= 0) { + return "Enter a positive whole number"; + } + return undefined; + }, + }); + const n = Number(raw.trim()); + if (!Number.isInteger(n) || n <= 0) { + throw new Error("Invalid embedding dimensions"); + } + return n; +} + +export async function askEmbeddingDimensions( + embeddingModel: string, + opts?: { + allowBack?: boolean; + backHint?: string; + initial?: EmbeddingDimensions; + prechosenSize?: number; + }, +): Promise { + if (opts?.prechosenSize !== undefined) { + return dimensionsFromSize(opts.prechosenSize); + } + + p.log.step("Embedding dimensions"); + const known = lookupEmbeddingDimensions(embeddingModel); + const initialSize = + opts?.initial?.nodesDimension ?? known ?? EMBEDDINGS_DEFAULTS.nodesDimension; + + if (known !== undefined) { + p.note( + [ + `Model: ${embeddingModel}`, + `Known output size: ${known} dimensions`, + "", + "All vector stores (nodes, triplets, observations, data, relationships)", + "must use the same dimension as your embedding model.", + ].join("\n"), + "Embedding dimensions", + ); + + if (opts?.allowBack) { + const picked = await askConfirmOrBack({ + message: `Use ${known} dimensions for all embedding vector stores?`, + initialValue: true, + backHint: opts.backHint, + }); + if (isPromptBack(picked)) { + return picked; + } + if (picked) { + return dimensionsFromSize(known); + } + } else { + const useKnown = await askConfirm({ + message: `Use ${known} dimensions for all embedding vector stores?`, + initialValue: true, + }); + if (useKnown) { + return dimensionsFromSize(known); + } + } + } else { + p.note( + [ + `Model: ${embeddingModel}`, + "This model is not in the built-in dimension map.", + "Enter the embedding output size from your provider docs.", + ].join("\n"), + "Embedding dimensions", + ); + } + + const size = await askDimensionSize(initialSize); + return dimensionsFromSize(size); +} + +export function resolveActiveEmbeddingModel(models: ModelsChoices): string { + const provider: ModelProvider = models.embeddingsProvider; + switch (provider) { + case "ollama": + return models.ollama?.embeddingsLocalModel ?? EMBEDDINGS_DEFAULTS.localModel; + case "openai": + return models.openai?.embeddingModel ?? OPENAI_DEFAULT_EMBEDDING_MODEL; + case "azure": + return models.azure?.embeddingModel ?? AZURE_DEFAULT_EMBEDDING_MODEL; + case "gcp_vertex": + return models.gcp?.embeddingModel ?? GCP_DEFAULT_EMBEDDING_MODEL; + case "amazon_bedrock": + return models.bedrock?.embeddingModel ?? BEDROCK_DEFAULT_EMBEDDING_MODEL; + default: + return EMBEDDINGS_DEFAULTS.localModel; + } +} + +export async function attachEmbeddingDimensions( + models: ModelsChoices, + opts?: { + allowBack?: boolean; + backHint?: string; + initial?: EmbeddingDimensions; + prechosenSize?: number; + }, +): Promise { + const embeddingModel = resolveActiveEmbeddingModel(models); + const dimensions = await askEmbeddingDimensions(embeddingModel, opts); + if (isPromptBack(dimensions)) { + return dimensions; + } + return { ...models, embeddingDimensions: dimensions }; +} + +export { KNOWN_EMBEDDING_MODEL_DIMENSIONS }; diff --git a/tui/src/flows/models.ts b/tui/src/flows/models.ts new file mode 100644 index 0000000000000000000000000000000000000000..4db22594b5259a47afee1934b53cd8957dda0fea --- /dev/null +++ b/tui/src/flows/models.ts @@ -0,0 +1,803 @@ +import { readFile, stat } from "node:fs/promises"; +import path from "node:path"; +import { homedir } from "node:os"; +import * as p from "@clack/prompts"; +import pc from "picocolors"; +import { + ANTHROPIC_DEFAULT_LARGE_MODEL, + ANTHROPIC_DEFAULT_SMALL_MODEL, + AZURE_DEFAULT_EMBEDDING_MODEL, + AZURE_DEFAULT_SMALL_MODEL, + AZURE_DEFAULT_LARGE_API_VERSION, + AZURE_DEFAULT_LARGE_MODEL, + BEDROCK_DEFAULT_EMBEDDING_MODEL, + BEDROCK_DEFAULT_LARGE_MODEL, + BEDROCK_DEFAULT_REGION, + BEDROCK_DEFAULT_SMALL_MODEL, + DEEPSEEK_DEFAULT_LARGE_MODEL, + DEEPSEEK_DEFAULT_SMALL_MODEL, + GCP_DEFAULT_EMBEDDING_MODEL, + GCP_DEFAULT_LARGE_MODEL, + GCP_DEFAULT_SMALL_MODEL, + OPENAI_DEFAULT_EMBEDDING_MODEL, + OPENAI_DEFAULT_LARGE_MODEL, + OPENAI_DEFAULT_SMALL_MODEL, +} from "../constants.js"; +import type { InitFlagOverrides } from "../lib/init-flags.js"; +import { + askPassword, + askText, + isPromptBack, + pickOne, + type PromptBack, +} from "../lib/prompts.js"; +import type { + AzureChoices, + BedrockChoices, + DeepSeekChoices, + GcpChoices, + ModelProvider, + ModelsChoices, + ModelsMode, + AnthropicChoices, + OpenAIChoices, +} from "../types.js"; +import { askOllama } from "./ollama.js"; +import { attachEmbeddingDimensions } from "./embedding-dimensions.js"; + +function expandHome(filePath: string): string { + if (filePath.startsWith("~")) { + return path.join(homedir(), filePath.slice(1)); + } + return filePath; +} + +interface GcpCredentialsCheck { + error?: string; + projectId?: string; +} + +async function inspectGcpCredentials(filePath: string): Promise { + const expanded = expandHome(filePath.trim()); + if (!expanded) return { error: "Path is required" }; + try { + const info = await stat(expanded); + if (!info.isFile()) return { error: "Path is not a file" }; + const raw = await readFile(expanded, "utf8"); + const parsed = JSON.parse(raw) as { project_id?: unknown }; + const projectId = + typeof parsed.project_id === "string" && parsed.project_id.trim().length > 0 + ? parsed.project_id.trim() + : undefined; + return { projectId }; + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + return { error: `Could not read credentials JSON: ${message}` }; + } +} + +function pickModel( + providerSpecific: string | undefined, + generic: string | undefined, + fallback: string, +): string { + return providerSpecific ?? generic ?? fallback; +} + +async function askGcp( + opts: { + needLlm: boolean; + needEmbeddings: boolean; + }, + flags?: InitFlagOverrides, +): Promise { + p.log.step( + opts.needLlm && opts.needEmbeddings + ? "Configure GCP Vertex" + : opts.needEmbeddings + ? "Configure GCP Vertex embeddings" + : "Configure GCP Vertex LLMs", + ); + + let credentialsPath = flags?.gcpCredentials + ? expandHome(flags.gcpCredentials) + : ""; + let projectIdFromFile: string | undefined; + if (credentialsPath) { + const result = await inspectGcpCredentials(credentialsPath); + if (result.error) { + throw new Error(`--gcp-credentials: ${result.error}`); + } + projectIdFromFile = result.projectId; + } else { + while (true) { + const raw = await askText({ + message: "Path to GCP service-account credentials JSON", + placeholder: "~/.config/gcloud/brainapi.json", + }); + const result = await inspectGcpCredentials(raw); + if (!result.error) { + credentialsPath = expandHome(raw.trim()); + projectIdFromFile = result.projectId; + break; + } + p.log.error(result.error); + } + } + + let projectId: string; + if (flags?.gcpProject) { + projectId = flags.gcpProject; + } else if (projectIdFromFile) { + projectId = projectIdFromFile; + p.log.info(`Using GCP project ${pc.cyan(projectId)} (from credentials file)`); + } else { + p.log.warn( + "No project_id field found in the credentials JSON — please enter it manually.", + ); + const entered = await askText({ + message: "GCP project id", + placeholder: "my-gcp-project", + validate: (value) => (value.trim().length === 0 ? "Project id is required" : undefined), + }); + projectId = entered.trim(); + } + + let smallLlmModel = pickModel(flags?.gcpSmall, flags?.llmSmall, GCP_DEFAULT_SMALL_MODEL); + let largeLlmModel = pickModel(flags?.gcpLarge, flags?.llmLarge, GCP_DEFAULT_LARGE_MODEL); + if (opts.needLlm) { + if (flags?.gcpSmall === undefined && flags?.llmSmall === undefined) { + const smallModel = await askText({ + message: "GCP small LLM model", + placeholder: GCP_DEFAULT_SMALL_MODEL, + defaultValue: GCP_DEFAULT_SMALL_MODEL, + }); + smallLlmModel = smallModel.trim() || GCP_DEFAULT_SMALL_MODEL; + } + if (flags?.gcpLarge === undefined && flags?.llmLarge === undefined) { + const largeModel = await askText({ + message: "GCP large LLM model", + placeholder: GCP_DEFAULT_LARGE_MODEL, + defaultValue: GCP_DEFAULT_LARGE_MODEL, + }); + largeLlmModel = largeModel.trim() || GCP_DEFAULT_LARGE_MODEL; + } + } + + let embeddingModel = pickModel( + flags?.gcpEmbedding, + flags?.embeddingModel, + GCP_DEFAULT_EMBEDDING_MODEL, + ); + if ( + opts.needEmbeddings && + flags?.gcpEmbedding === undefined && + flags?.embeddingModel === undefined + ) { + const embedding = await askText({ + message: "GCP embedding model", + placeholder: GCP_DEFAULT_EMBEDDING_MODEL, + defaultValue: GCP_DEFAULT_EMBEDDING_MODEL, + }); + embeddingModel = embedding.trim() || GCP_DEFAULT_EMBEDDING_MODEL; + } + + return { + credentialsPath, + projectId, + smallLlmModel, + largeLlmModel, + embeddingModel, + }; +} + +async function askAzure( + opts: { + needLlm: boolean; + needEmbeddings: boolean; + }, + flags?: InitFlagOverrides, +): Promise { + p.log.step( + opts.needLlm && opts.needEmbeddings + ? "Configure Azure OpenAI" + : opts.needEmbeddings + ? "Configure Azure OpenAI embeddings" + : "Configure Azure OpenAI LLMs", + ); + + let llmEndpoint = flags?.azureEndpoint ?? ""; + let llmApiVersion = flags?.azureApiVersion ?? AZURE_DEFAULT_LARGE_API_VERSION; + let llmSubscriptionKey = flags?.azureKey ?? ""; + let smallLlmModel = pickModel(flags?.azureSmall, flags?.llmSmall, AZURE_DEFAULT_SMALL_MODEL); + let largeLlmModel = pickModel(flags?.azureLarge, flags?.llmLarge, AZURE_DEFAULT_LARGE_MODEL); + if (opts.needLlm) { + if (flags?.azureEndpoint === undefined) { + llmEndpoint = ( + await askText({ + message: "Azure LLM endpoint", + placeholder: "https://yourproject.openai.azure.com", + validate: (value) => + value.trim().length === 0 ? "Endpoint is required" : undefined, + }) + ).trim(); + } + if (flags?.azureApiVersion === undefined) { + llmApiVersion = + ( + await askText({ + message: "Azure LLM API version", + placeholder: AZURE_DEFAULT_LARGE_API_VERSION, + defaultValue: AZURE_DEFAULT_LARGE_API_VERSION, + }) + ).trim() || AZURE_DEFAULT_LARGE_API_VERSION; + } + if (flags?.azureKey === undefined) { + llmSubscriptionKey = ( + await askPassword({ + message: "Azure LLM subscription key", + validate: (value) => + value.trim().length === 0 ? "API key is required" : undefined, + }) + ).trim(); + } + if (flags?.azureSmall === undefined && flags?.llmSmall === undefined) { + smallLlmModel = + ( + await askText({ + message: "Azure small LLM deployment/model", + placeholder: AZURE_DEFAULT_SMALL_MODEL, + defaultValue: AZURE_DEFAULT_SMALL_MODEL, + }) + ).trim() || AZURE_DEFAULT_SMALL_MODEL; + } + if (flags?.azureLarge === undefined && flags?.llmLarge === undefined) { + largeLlmModel = + ( + await askText({ + message: "Azure large LLM deployment/model", + placeholder: AZURE_DEFAULT_LARGE_MODEL, + defaultValue: AZURE_DEFAULT_LARGE_MODEL, + }) + ).trim() || AZURE_DEFAULT_LARGE_MODEL; + } + } + + let embeddingEndpoint = flags?.azureEmbeddingEndpoint ?? ""; + let embeddingKey = flags?.azureEmbeddingKey ?? ""; + let embeddingModel = pickModel( + flags?.azureEmbedding, + flags?.embeddingModel, + AZURE_DEFAULT_EMBEDDING_MODEL, + ); + if (opts.needEmbeddings) { + if (flags?.azureEmbeddingEndpoint === undefined) { + embeddingEndpoint = ( + await askText({ + message: "Azure embeddings endpoint URL", + placeholder: + "https://yourproject.openai.azure.com/openai/deployments/text-embedding-3-large/embeddings?api-version=2023-05-15", + validate: (value) => + value.trim().length === 0 ? "Endpoint URL is required" : undefined, + }) + ).trim(); + } + if (flags?.azureEmbeddingKey === undefined) { + embeddingKey = ( + await askPassword({ + message: "Azure embeddings API key", + validate: (value) => + value.trim().length === 0 ? "API key is required" : undefined, + }) + ).trim(); + } + if (flags?.azureEmbedding === undefined && flags?.embeddingModel === undefined) { + embeddingModel = + ( + await askText({ + message: "Azure embedding model/deployment", + placeholder: AZURE_DEFAULT_EMBEDDING_MODEL, + defaultValue: AZURE_DEFAULT_EMBEDDING_MODEL, + }) + ).trim() || AZURE_DEFAULT_EMBEDDING_MODEL; + } + } + + return { + smallLlmModel, + largeLlmModel, + llmApiVersion, + llmEndpoint, + llmSubscriptionKey, + embeddingEndpoint, + embeddingKey, + embeddingModel, + }; +} + +async function askBedrock( + opts: { + needLlm: boolean; + needEmbeddings: boolean; + }, + flags?: InitFlagOverrides, +): Promise { + p.log.step( + opts.needLlm && opts.needEmbeddings + ? "Configure Amazon Bedrock" + : opts.needEmbeddings + ? "Configure Amazon Bedrock embeddings" + : "Configure Amazon Bedrock LLMs", + ); + + let region = flags?.awsRegion ?? BEDROCK_DEFAULT_REGION; + if (flags?.awsRegion === undefined) { + const raw = await askText({ + message: "AWS region", + placeholder: BEDROCK_DEFAULT_REGION, + defaultValue: BEDROCK_DEFAULT_REGION, + }); + region = raw.trim() || BEDROCK_DEFAULT_REGION; + } + + let accessKeyId = flags?.awsAccessKeyId ?? ""; + if (flags?.awsAccessKeyId === undefined) { + accessKeyId = ( + await askText({ + message: "AWS access key id", + validate: (value) => + value.trim().length === 0 ? "Access key id is required" : undefined, + }) + ).trim(); + } + + let secretAccessKey = flags?.awsSecretAccessKey ?? ""; + if (flags?.awsSecretAccessKey === undefined) { + secretAccessKey = ( + await askPassword({ + message: "AWS secret access key", + validate: (value) => + value.trim().length === 0 ? "Secret access key is required" : undefined, + }) + ).trim(); + } + + let sessionToken = flags?.awsSessionToken; + if (flags?.awsSessionToken === undefined) { + const raw = await askPassword({ + message: "AWS session token (optional)", + validate: () => undefined, + }); + sessionToken = raw.trim() || undefined; + } + + let smallLlmModel = pickModel( + flags?.bedrockSmall, + flags?.llmSmall, + BEDROCK_DEFAULT_SMALL_MODEL, + ); + let largeLlmModel = pickModel( + flags?.bedrockLarge, + flags?.llmLarge, + BEDROCK_DEFAULT_LARGE_MODEL, + ); + if (opts.needLlm) { + if (flags?.bedrockSmall === undefined && flags?.llmSmall === undefined) { + smallLlmModel = + ( + await askText({ + message: "Bedrock small LLM model id", + placeholder: BEDROCK_DEFAULT_SMALL_MODEL, + defaultValue: BEDROCK_DEFAULT_SMALL_MODEL, + }) + ).trim() || BEDROCK_DEFAULT_SMALL_MODEL; + } + if (flags?.bedrockLarge === undefined && flags?.llmLarge === undefined) { + largeLlmModel = + ( + await askText({ + message: "Bedrock large LLM model id", + placeholder: BEDROCK_DEFAULT_LARGE_MODEL, + defaultValue: BEDROCK_DEFAULT_LARGE_MODEL, + }) + ).trim() || BEDROCK_DEFAULT_LARGE_MODEL; + } + } + + let embeddingModel = pickModel( + flags?.bedrockEmbedding, + flags?.embeddingModel, + BEDROCK_DEFAULT_EMBEDDING_MODEL, + ); + if ( + opts.needEmbeddings && + flags?.bedrockEmbedding === undefined && + flags?.embeddingModel === undefined + ) { + embeddingModel = + ( + await askText({ + message: "Bedrock embedding model id", + placeholder: BEDROCK_DEFAULT_EMBEDDING_MODEL, + defaultValue: BEDROCK_DEFAULT_EMBEDDING_MODEL, + }) + ).trim() || BEDROCK_DEFAULT_EMBEDDING_MODEL; + } + + return { + region, + accessKeyId, + secretAccessKey, + sessionToken, + smallLlmModel, + largeLlmModel, + embeddingModel, + }; +} + +async function askOpenAI( + opts: { + needLlm: boolean; + needEmbeddings: boolean; + }, + flags?: InitFlagOverrides, +): Promise { + p.log.step( + opts.needLlm && opts.needEmbeddings + ? "Configure OpenAI" + : opts.needEmbeddings + ? "Configure OpenAI embeddings" + : "Configure OpenAI LLMs", + ); + + let apiKey = flags?.openaiApiKey ?? ""; + if (flags?.openaiApiKey === undefined) { + apiKey = ( + await askPassword({ + message: "OpenAI API key", + validate: (value) => + value.trim().length === 0 ? "API key is required" : undefined, + }) + ).trim(); + } + + let baseUrl = flags?.openaiBaseUrl; + if (flags?.openaiBaseUrl === undefined) { + const raw = await askText({ + message: "OpenAI base URL (optional)", + placeholder: "https://api.openai.com/v1", + defaultValue: "", + }); + baseUrl = raw.trim() || undefined; + } + + let smallLlmModel = pickModel( + flags?.openaiSmall, + flags?.llmSmall, + OPENAI_DEFAULT_SMALL_MODEL, + ); + let largeLlmModel = pickModel( + flags?.openaiLarge, + flags?.llmLarge, + OPENAI_DEFAULT_LARGE_MODEL, + ); + if (opts.needLlm) { + if (flags?.openaiSmall === undefined && flags?.llmSmall === undefined) { + const small = await askText({ + message: "OpenAI small LLM model", + placeholder: OPENAI_DEFAULT_SMALL_MODEL, + defaultValue: OPENAI_DEFAULT_SMALL_MODEL, + }); + smallLlmModel = small.trim() || OPENAI_DEFAULT_SMALL_MODEL; + } + if (flags?.openaiLarge === undefined && flags?.llmLarge === undefined) { + const large = await askText({ + message: "OpenAI large LLM model", + placeholder: OPENAI_DEFAULT_LARGE_MODEL, + defaultValue: OPENAI_DEFAULT_LARGE_MODEL, + }); + largeLlmModel = large.trim() || OPENAI_DEFAULT_LARGE_MODEL; + } + } + + let embeddingModel = pickModel( + flags?.openaiEmbedding, + flags?.embeddingModel, + OPENAI_DEFAULT_EMBEDDING_MODEL, + ); + if ( + opts.needEmbeddings && + flags?.openaiEmbedding === undefined && + flags?.embeddingModel === undefined + ) { + const embedding = await askText({ + message: "OpenAI embedding model", + placeholder: OPENAI_DEFAULT_EMBEDDING_MODEL, + defaultValue: OPENAI_DEFAULT_EMBEDDING_MODEL, + }); + embeddingModel = embedding.trim() || OPENAI_DEFAULT_EMBEDDING_MODEL; + } + + return { + apiKey, + baseUrl, + smallLlmModel, + largeLlmModel, + embeddingModel, + }; +} + +async function askAnthropic(flags?: InitFlagOverrides): Promise { + p.log.step("Configure Anthropic"); + let apiKey = flags?.anthropicApiKey ?? ""; + if (flags?.anthropicApiKey === undefined) { + apiKey = ( + await askPassword({ + message: "Anthropic API key", + validate: (value) => + value.trim().length === 0 ? "API key is required" : undefined, + }) + ).trim(); + } + let smallLlmModel = pickModel( + flags?.anthropicSmall, + flags?.llmSmall, + ANTHROPIC_DEFAULT_SMALL_MODEL, + ); + let largeLlmModel = pickModel( + flags?.anthropicLarge, + flags?.llmLarge, + ANTHROPIC_DEFAULT_LARGE_MODEL, + ); + if (flags?.anthropicSmall === undefined && flags?.llmSmall === undefined) { + const raw = await askText({ + message: "Anthropic small LLM model", + placeholder: ANTHROPIC_DEFAULT_SMALL_MODEL, + defaultValue: ANTHROPIC_DEFAULT_SMALL_MODEL, + }); + smallLlmModel = raw.trim() || ANTHROPIC_DEFAULT_SMALL_MODEL; + } + if (flags?.anthropicLarge === undefined && flags?.llmLarge === undefined) { + const raw = await askText({ + message: "Anthropic large LLM model", + placeholder: ANTHROPIC_DEFAULT_LARGE_MODEL, + defaultValue: ANTHROPIC_DEFAULT_LARGE_MODEL, + }); + largeLlmModel = raw.trim() || ANTHROPIC_DEFAULT_LARGE_MODEL; + } + return { apiKey, smallLlmModel, largeLlmModel }; +} + +async function askDeepSeek(flags?: InitFlagOverrides): Promise { + p.log.step("Configure DeepSeek"); + let apiKey = flags?.deepseekApiKey ?? ""; + if (flags?.deepseekApiKey === undefined) { + apiKey = ( + await askPassword({ + message: "DeepSeek API key", + validate: (value) => + value.trim().length === 0 ? "API key is required" : undefined, + }) + ).trim(); + } + let smallLlmModel = pickModel( + flags?.deepseekSmall, + flags?.llmSmall, + DEEPSEEK_DEFAULT_SMALL_MODEL, + ); + let largeLlmModel = pickModel( + flags?.deepseekLarge, + flags?.llmLarge, + DEEPSEEK_DEFAULT_LARGE_MODEL, + ); + if (flags?.deepseekSmall === undefined && flags?.llmSmall === undefined) { + const raw = await askText({ + message: "DeepSeek small LLM model", + placeholder: DEEPSEEK_DEFAULT_SMALL_MODEL, + defaultValue: DEEPSEEK_DEFAULT_SMALL_MODEL, + }); + smallLlmModel = raw.trim() || DEEPSEEK_DEFAULT_SMALL_MODEL; + } + if (flags?.deepseekLarge === undefined && flags?.llmLarge === undefined) { + const raw = await askText({ + message: "DeepSeek large LLM model", + placeholder: DEEPSEEK_DEFAULT_LARGE_MODEL, + defaultValue: DEEPSEEK_DEFAULT_LARGE_MODEL, + }); + largeLlmModel = raw.trim() || DEEPSEEK_DEFAULT_LARGE_MODEL; + } + return { apiKey, smallLlmModel, largeLlmModel }; +} + +async function askLlmProvider( + message: string, + initialValue: ModelProvider, +): Promise { + return pickOne({ + message, + options: [ + { value: "ollama", label: "Ollama" }, + { value: "openai", label: "OpenAI" }, + { value: "anthropic", label: "Anthropic" }, + { value: "deepseek", label: "DeepSeek" }, + { value: "azure", label: "Azure OpenAI" }, + { value: "gcp_vertex", label: "Google Cloud — Vertex AI" }, + { value: "amazon_bedrock", label: "Amazon Bedrock" }, + ], + initialValue, + }); +} + +async function askEmbeddingsProvider( + message: string, + initialValue: Exclude, +): Promise> { + return pickOne>({ + message, + options: [ + { value: "ollama", label: "Ollama" }, + { value: "openai", label: "OpenAI" }, + { value: "azure", label: "Azure OpenAI" }, + { value: "gcp_vertex", label: "Google Cloud — Vertex AI" }, + { value: "amazon_bedrock", label: "Amazon Bedrock" }, + ], + initialValue, + }); +} + +export async function askModels(options?: { + prechosenMode?: ModelsMode; + allowBack?: false; + initialMode?: ModelsMode; + flags?: InitFlagOverrides; +}): Promise; +export async function askModels(options: { + prechosenMode?: ModelsMode; + allowBack: true; + backHint?: string; + initialMode?: ModelsMode; + flags?: InitFlagOverrides; +}): Promise; +export async function askModels(options?: { + prechosenMode?: ModelsMode; + allowBack?: boolean; + backHint?: string; + initialMode?: ModelsMode; + flags?: InitFlagOverrides; +}): Promise { + const flags = options?.flags; + let mode = options?.prechosenMode ?? flags?.modelsMode; + if (!mode) { + const picked = options?.allowBack + ? await pickOne({ + message: "Models mode", + options: [ + { value: "remote", label: "Remote (cloud provider)" }, + { value: "local", label: "Local (Ollama)" }, + ], + initialValue: options.initialMode ?? "remote", + allowBack: true, + backHint: options.backHint, + }) + : await pickOne({ + message: "Models mode", + options: [ + { value: "remote", label: "Remote (cloud provider)" }, + { value: "local", label: "Local (Ollama)" }, + ], + initialValue: options?.initialMode ?? "remote", + }); + if (isPromptBack(picked)) { + return picked; + } + mode = picked; + } + + if (mode === "local") { + const ollama = await askOllama(flags); + return attachEmbeddingDimensions( + { + mode, + llmSmallProvider: "ollama", + llmLargeProvider: "ollama", + embeddingsProvider: "ollama", + ollama, + }, + { + allowBack: options?.allowBack, + backHint: options?.backHint, + prechosenSize: flags?.embeddingDimensions, + }, + ); + } + + const llmSmallProvider = + flags?.llmSmallProvider ?? + (await askLlmProvider("Small LLM provider", "gcp_vertex")); + const llmLargeProvider = + flags?.llmLargeProvider ?? + (await askLlmProvider("Large LLM provider", "azure")); + const embeddingsProvider = + flags?.embeddingsProvider ?? + (await askEmbeddingsProvider("Embeddings provider", "azure")); + + const providers = new Set([llmSmallProvider, llmLargeProvider, embeddingsProvider]); + + let ollama = undefined; + let gcp = undefined; + let azure = undefined; + let openai = undefined; + let anthropic = undefined; + let deepseek = undefined; + let bedrock = undefined; + + if (providers.has("ollama")) { + ollama = await askOllama(flags); + } + if (providers.has("gcp_vertex")) { + gcp = await askGcp( + { + needLlm: + llmSmallProvider === "gcp_vertex" || llmLargeProvider === "gcp_vertex", + needEmbeddings: embeddingsProvider === "gcp_vertex", + }, + flags, + ); + } + if (providers.has("azure")) { + azure = await askAzure( + { + needLlm: llmSmallProvider === "azure" || llmLargeProvider === "azure", + needEmbeddings: embeddingsProvider === "azure", + }, + flags, + ); + } + if (providers.has("openai")) { + openai = await askOpenAI( + { + needLlm: llmSmallProvider === "openai" || llmLargeProvider === "openai", + needEmbeddings: embeddingsProvider === "openai", + }, + flags, + ); + } + if (providers.has("anthropic")) { + anthropic = await askAnthropic(flags); + } + if (providers.has("deepseek")) { + deepseek = await askDeepSeek(flags); + } + if (providers.has("amazon_bedrock")) { + bedrock = await askBedrock( + { + needLlm: + llmSmallProvider === "amazon_bedrock" || + llmLargeProvider === "amazon_bedrock", + needEmbeddings: embeddingsProvider === "amazon_bedrock", + }, + flags, + ); + } + + return attachEmbeddingDimensions( + { + mode, + llmSmallProvider, + llmLargeProvider, + embeddingsProvider, + ollama, + gcp, + azure, + openai, + anthropic, + deepseek, + bedrock, + }, + { + allowBack: options?.allowBack, + backHint: options?.backHint, + prechosenSize: flags?.embeddingDimensions, + }, + ); +} diff --git a/tui/src/flows/ollama.ts b/tui/src/flows/ollama.ts new file mode 100644 index 0000000000000000000000000000000000000000..e4a07f28dd9c8bb0e275fb717d366d7e2691f661 --- /dev/null +++ b/tui/src/flows/ollama.ts @@ -0,0 +1,201 @@ +import { access } from "node:fs/promises"; +import * as p from "@clack/prompts"; +import pc from "picocolors"; +import { + OLLAMA_DEFAULT_HOST, + OLLAMA_DEFAULT_LARGE_MODEL, + OLLAMA_DEFAULT_PORT, + OLLAMA_DEFAULT_SMALL_MODEL, + EMBEDDINGS_DEFAULTS, +} from "../constants.js"; +import type { InitFlagOverrides } from "../lib/init-flags.js"; +import { + hasOllamaModel, + isOllamaRunning, + pullOllamaModel, + type OllamaTarget, +} from "../lib/ollama.js"; +import { askConfirm, askText, pickOne } from "../lib/prompts.js"; +import type { OllamaChoices } from "../types.js"; + +async function waitForRunning(target: OllamaTarget): Promise { + while (!(await isOllamaRunning(target))) { + p.log.warn( + `Ollama did not respond at ${pc.cyan(`http://${target.host}:${target.port}`)}.`, + ); + p.note( + [ + "Start Ollama in a separate terminal:", + " ollama serve", + "Or launch the Ollama desktop app.", + ].join("\n"), + "Ollama not running", + ); + await askConfirm({ + message: "Press enter when Ollama is running", + initialValue: true, + }); + } + p.log.success("Ollama is up."); +} + +type MissingModelAction = "pull" | "change"; + +async function waitForModel( + target: OllamaTarget, + initialModelName: string, + role: "small" | "large", +): Promise { + let modelName = initialModelName; + while (!(await hasOllamaModel(target, modelName))) { + p.log.warn(`Model ${pc.cyan(modelName)} is not available locally.`); + + const action = await pickOne({ + message: `How do you want to provide the ${role} model?`, + options: [ + { + value: "pull", + label: "Pull this model now", + hint: `runs: ollama pull ${modelName}`, + }, + { + value: "change", + label: "Use a different model name", + }, + ], + initialValue: "pull", + }); + + if (action === "pull") { + const spinner = p.spinner(); + spinner.start(`Pulling ${modelName}`); + const result = await pullOllamaModel(modelName); + if (result.ok) { + spinner.stop(`Pulled ${pc.cyan(modelName)}`); + if (await hasOllamaModel(target, modelName)) { + continue; + } + p.log.warn( + `Pull finished but ${pc.cyan(modelName)} is still not listed by Ollama. Check the tag name.`, + ); + continue; + } + spinner.stop(pc.red(`Failed to pull ${modelName}`)); + p.log.error(result.error ?? `ollama pull exited with code ${result.exitCode}`); + continue; + } + + const nextModel = await askText({ + message: `${role[0]!.toUpperCase()}${role.slice(1)} model name (Ollama tag)`, + placeholder: modelName, + defaultValue: modelName, + validate: (value) => + value.trim().length === 0 ? "Model name is required" : undefined, + }); + modelName = nextModel.trim(); + } + p.log.success(`Model ${pc.cyan(modelName)} is available.`); + return modelName; +} + +async function askEmbeddingsLocal(defaultModel: string): Promise { + const raw = await askText({ + message: "Local sentence-transformers embeddings model", + placeholder: defaultModel, + defaultValue: defaultModel, + }); + return raw.trim() || defaultModel; +} + +export async function askOllama( + flags?: InitFlagOverrides, +): Promise { + p.log.step("Configure Ollama"); + + let host = flags?.ollamaHost; + if (host === undefined) { + const hostRaw = await askText({ + message: "Ollama host", + placeholder: OLLAMA_DEFAULT_HOST, + defaultValue: OLLAMA_DEFAULT_HOST, + }); + host = hostRaw.trim() || OLLAMA_DEFAULT_HOST; + } + + let port = flags?.ollamaPort; + if (port === undefined) { + const portRaw = await askText({ + message: "Ollama port", + placeholder: String(OLLAMA_DEFAULT_PORT), + defaultValue: String(OLLAMA_DEFAULT_PORT), + validate: (value) => { + const n = Number(value); + if (!Number.isInteger(n) || n <= 0 || n > 65535) return "Port must be 1-65535"; + return undefined; + }, + }); + port = Number(portRaw) || OLLAMA_DEFAULT_PORT; + } + + const target: OllamaTarget = { host, port }; + + await waitForRunning(target); + + let smallModel = + flags?.ollamaSmall ?? flags?.llmSmall ?? OLLAMA_DEFAULT_SMALL_MODEL; + if ( + flags?.ollamaSmall === undefined && + flags?.llmSmall === undefined + ) { + const smallModelRaw = await askText({ + message: "Small LLM model name (Ollama tag)", + placeholder: OLLAMA_DEFAULT_SMALL_MODEL, + defaultValue: OLLAMA_DEFAULT_SMALL_MODEL, + }); + smallModel = smallModelRaw.trim() || OLLAMA_DEFAULT_SMALL_MODEL; + } + smallModel = await waitForModel(target, smallModel, "small"); + + let largeModel = + flags?.ollamaLarge ?? flags?.llmLarge ?? OLLAMA_DEFAULT_LARGE_MODEL; + if ( + flags?.ollamaLarge === undefined && + flags?.llmLarge === undefined + ) { + const largeModelRaw = await askText({ + message: "Large LLM model name (Ollama tag)", + placeholder: OLLAMA_DEFAULT_LARGE_MODEL, + defaultValue: OLLAMA_DEFAULT_LARGE_MODEL, + }); + largeModel = largeModelRaw.trim() || OLLAMA_DEFAULT_LARGE_MODEL; + } + largeModel = await waitForModel(target, largeModel, "large"); + + let embeddingsLocalModel = + flags?.ollamaEmbeddings ?? + flags?.embeddingModel ?? + EMBEDDINGS_DEFAULTS.localModel; + if ( + flags?.ollamaEmbeddings === undefined && + flags?.embeddingModel === undefined + ) { + embeddingsLocalModel = await askEmbeddingsLocal(EMBEDDINGS_DEFAULTS.localModel); + } + + return { + host: target.host, + port: target.port, + smallModel, + largeModel, + embeddingsLocalModel, + }; +} + +export async function isFileReadable(filePath: string): Promise { + try { + await access(filePath); + return true; + } catch { + return false; + } +} diff --git a/tui/src/flows/pipeline.ts b/tui/src/flows/pipeline.ts new file mode 100644 index 0000000000000000000000000000000000000000..54241e11cd1c6fdbeab21feaddc309f017635f51 --- /dev/null +++ b/tui/src/flows/pipeline.ts @@ -0,0 +1,92 @@ +import * as p from "@clack/prompts"; +import pc from "picocolors"; +import { isPromptBack, pickOne, type PromptBack } from "../lib/prompts.js"; +import type { OcrMode, PipelineChoices } from "../types.js"; + +export async function askPipeline(options?: { + prechosenOcrMode?: OcrMode; + allowBack?: false; + initialOcrMode?: OcrMode; +}): Promise; +export async function askPipeline(options: { + prechosenOcrMode?: OcrMode; + allowBack: true; + backHint?: string; + initialOcrMode?: OcrMode; +}): Promise; +export async function askPipeline(options?: { + prechosenOcrMode?: OcrMode; + allowBack?: boolean; + backHint?: string; + initialOcrMode?: OcrMode; +}): Promise { + if (options?.prechosenOcrMode) { + return { ocrMode: options.prechosenOcrMode }; + } + + p.log.step("Document ingestion"); + const ocrMode = options?.allowBack + ? await pickOne({ + message: "OCR engine for document parsing", + options: [ + { + value: "docparser", + label: "Remote DocParser (default)", + hint: "lightweight install, calls Lumen DocParser API", + }, + { + value: "docling", + label: "Local Docling", + hint: "adds ~2 GB ML deps (docling, accelerate)", + }, + ], + initialValue: options.initialOcrMode ?? "docparser", + allowBack: true, + backHint: options.backHint, + }) + : await pickOne({ + message: "OCR engine for document parsing", + options: [ + { + value: "docparser", + label: "Remote DocParser (default)", + hint: "lightweight install, calls Lumen DocParser API", + }, + { + value: "docling", + label: "Local Docling", + hint: "adds ~2 GB ML deps (docling, accelerate)", + }, + ], + initialValue: options?.initialOcrMode ?? "docparser", + }); + if (isPromptBack(ocrMode)) { + return ocrMode; + } + + if (ocrMode === "docling") { + p.log.info( + pc.dim( + "Docling and accelerate will be installed into the venv after this flow.", + ), + ); + } + + return { ocrMode }; +} + +export function ocrToExtras(ocrMode: OcrMode): string[] { + return ocrMode === "docling" ? ["docling-ocr"] : []; +} + +export function postgresBackendExtras(dbs: { + vectorDb: string; + dataDb: string; + graphDb: string; +}): string[] { + const needs = + dbs.vectorDb === "postgresql" || + dbs.dataDb === "postgresql" || + dbs.graphDb === "networkx"; + return needs ? ["postgresql-backend"] : []; +} diff --git a/tui/src/flows/plugins.ts b/tui/src/flows/plugins.ts new file mode 100644 index 0000000000000000000000000000000000000000..a1aaf91b0c13b09254c673a704183c2dac7edfea --- /dev/null +++ b/tui/src/flows/plugins.ts @@ -0,0 +1,138 @@ +import * as p from "@clack/prompts"; +import { askConfirm, askText, isPromptBack, pickOne, PROMPT_BACK } from "../lib/prompts.js"; +import type { PluginChoice, PluginSource } from "../types.js"; +import { listInstalledPlugins, searchPlugins } from "../lib/plugins.js"; + +interface AskPluginsOptions { + allowBack?: boolean; + backHint?: string; + initial?: PluginChoice[]; + /** When set, skip the interactive plugin search UI. */ + prechosen?: PluginChoice[]; + skip?: boolean; +} + +function pluginLabel(plugin: { + name: string; + version: string; + source: PluginSource; + description: string; +}): string { + const sourceTag = plugin.source === "registry" ? "registry" : "local"; + return `${plugin.name} (${sourceTag})`; +} + +export async function askPlugins( + opts: AskPluginsOptions = {}, +): Promise { + if (opts.skip || opts.prechosen !== undefined) { + return opts.prechosen ?? []; + } + + const selections = new Map(); + for (const plugin of opts.initial ?? []) { + selections.set(`${plugin.source}:${plugin.name}:${plugin.path ?? ""}`, plugin); + } + + while (true) { + const query = await askText({ + message: "Search plugin by name (or leave empty to continue)", + placeholder: "chatbot", + }); + if (!query.trim()) { + if (opts.allowBack && selections.size === 0) { + const shouldGoBack = await pickOne<"continue" | "back">({ + message: "No plugins selected. Continue or go back?", + options: [ + { value: "continue", label: "Continue" }, + { value: "back", label: "Back", hint: opts.backHint }, + ], + initialValue: "continue", + }); + if (shouldGoBack === "back") { + return PROMPT_BACK; + } + } + return [...selections.values()]; + } + + const results = await searchPlugins(query); + if (results.length === 0) { + p.log.warn(`No plugin found for "${query}".`); + } else { + const choice = await pickOne({ + message: "Select a plugin to add", + options: results.map((plugin, index) => ({ + value: String(index), + label: pluginLabel(plugin), + hint: plugin.description || `${plugin.version}`, + })), + allowBack: true, + backHint: opts.backHint, + }); + if (isPromptBack(choice)) { + if (opts.allowBack) { + return PROMPT_BACK; + } + continue; + } + const selected = results[Number(choice)]; + if (!selected) { + p.log.warn("Invalid plugin selection."); + continue; + } + const confirmed = await askConfirm({ + message: `Add ${selected.name} from ${selected.source}?`, + initialValue: true, + }); + if (confirmed) { + const key = `${selected.source}:${selected.name}:${selected.path ?? ""}`; + selections.set(key, { + name: selected.name, + source: selected.source, + version: selected.version, + path: selected.path, + }); + p.log.success(`Added ${selected.name}.`); + } + } + + const addMore = await askConfirm({ + message: "Add another plugin?", + initialValue: true, + }); + if (!addMore) { + return [...selections.values()]; + } + } +} + +export async function askPluginUninstall( + opts: { allowBack?: boolean; backHint?: string } = {}, +): Promise { + const installed = await listInstalledPlugins(); + if (installed.length === 0) { + p.log.info("No installed plugins found."); + return null; + } + const options = installed.map((plugin) => ({ + value: plugin.name, + label: plugin.name, + hint: plugin.version, + })); + const choice = opts.allowBack + ? await pickOne({ + message: "Select plugin to uninstall", + options, + allowBack: true, + backHint: opts.backHint, + }) + : await pickOne({ + message: "Select plugin to uninstall", + options, + }); + if (isPromptBack(choice)) { + return PROMPT_BACK; + } + return choice; +} diff --git a/tui/src/flows/search.ts b/tui/src/flows/search.ts new file mode 100644 index 0000000000000000000000000000000000000000..ba6b453478627679138d5c49ba586a9ed4e15a8a --- /dev/null +++ b/tui/src/flows/search.ts @@ -0,0 +1,117 @@ +import * as p from "@clack/prompts"; +import pc from "picocolors"; +import { isPromptBack, pickOne, type PromptBack } from "../lib/prompts.js"; +import type { SearchChoices, SearchRetrieval } from "../types.js"; + +const DEFAULT_SEARCH: SearchChoices = { + enabled: false, + retrieval: "hybrid", +}; + +export function defaultSearchChoices(): SearchChoices { + return { ...DEFAULT_SEARCH }; +} + +export async function askSearch(options?: { + allowBack?: false; + initial?: SearchChoices; + dataDb?: string; +}): Promise; +export async function askSearch(options: { + allowBack: true; + backHint?: string; + initial?: SearchChoices; + dataDb?: string; +}): Promise; +export async function askSearch(options?: { + allowBack?: boolean; + backHint?: string; + initial?: SearchChoices; + dataDb?: string; +}): Promise { + p.log.step("Search"); + if (options?.dataDb && options.dataDb !== "postgresql") { + p.log.info( + pc.dim( + "Hybrid search needs DATA_DB=postgresql. Leaving SEARCH_ENABLED=false.", + ), + ); + return { enabled: false, retrieval: "hybrid" }; + } + + const enabledChoice = options?.allowBack + ? await pickOne<"yes" | "no">({ + message: "Enable hybrid search API (/retrieve/search)?", + options: [ + { + value: "no", + label: "No (default)", + hint: "memory path unchanged; search routes return 404", + }, + { + value: "yes", + label: "Yes", + hint: "BM25 + dense ANN; fused by default; p50 < 200 ms ex-embed", + }, + ], + initialValue: options.initial?.enabled ? "yes" : "no", + allowBack: true, + backHint: options.backHint, + }) + : await pickOne<"yes" | "no">({ + message: "Enable hybrid search API (/retrieve/search)?", + options: [ + { + value: "no", + label: "No (default)", + hint: "memory path unchanged; search routes return 404", + }, + { + value: "yes", + label: "Yes", + hint: "BM25 + dense ANN; fused by default; p50 < 200 ms ex-embed", + }, + ], + initialValue: options?.initial?.enabled ? "yes" : "no", + }); + if (isPromptBack(enabledChoice)) { + return enabledChoice; + } + if (enabledChoice === "no") { + return { enabled: false, retrieval: "hybrid" }; + } + + const retrieval = options?.allowBack + ? await pickOne({ + message: "Search retrieval", + options: [ + { + value: "hybrid", + label: "Both fused (default)", + hint: "parallel BM25 ∪ dense, RRF", + }, + { value: "dense", label: "Dense only" }, + { value: "bm25", label: "BM25 only" }, + ], + initialValue: options.initial?.retrieval ?? "hybrid", + allowBack: true, + backHint: options.backHint, + }) + : await pickOne({ + message: "Search retrieval", + options: [ + { + value: "hybrid", + label: "Both fused (default)", + hint: "parallel BM25 ∪ dense, RRF", + }, + { value: "dense", label: "Dense only" }, + { value: "bm25", label: "BM25 only" }, + ], + initialValue: options?.initial?.retrieval ?? "hybrid", + }); + if (isPromptBack(retrieval)) { + return retrieval; + } + return { enabled: true, retrieval }; +} diff --git a/tui/src/flows/services-runtime.ts b/tui/src/flows/services-runtime.ts new file mode 100644 index 0000000000000000000000000000000000000000..356096bfc998799a55f79f244b85c8b73b1205c0 --- /dev/null +++ b/tui/src/flows/services-runtime.ts @@ -0,0 +1,101 @@ +import * as p from "@clack/prompts"; +import pc from "picocolors"; +import { isPromptBack, pickOne, type PromptBack } from "../lib/prompts.js"; +import type { DbChoices, ServicesRuntime } from "../types.js"; + +function nonRedisPicked(dbs: DbChoices): string[] { + const picked: string[] = []; + if ( + dbs.vectorDb === "postgresql" || + dbs.dataDb === "postgresql" || + dbs.graphDb === "networkx" + ) { + picked.push("PostgreSQL"); + } + if (dbs.graphDb === "neo4j") picked.push("Neo4j"); + if (dbs.vectorDb === "milvus") picked.push("Milvus"); + if (dbs.dataDb === "mongo") picked.push("MongoDB"); + return picked; +} + +export async function askServicesRuntime( + dbs: DbChoices, + opts?: { + allowBack?: false; + initialValue?: ServicesRuntime; + prechosen?: ServicesRuntime; + }, +): Promise; +export async function askServicesRuntime( + dbs: DbChoices, + opts: { + allowBack: true; + backHint?: string; + initialValue?: ServicesRuntime; + prechosen?: ServicesRuntime; + }, +): Promise; +export async function askServicesRuntime( + dbs: DbChoices, + opts?: { + allowBack?: boolean; + backHint?: string; + initialValue?: ServicesRuntime; + prechosen?: ServicesRuntime; + }, +): Promise { + if (opts?.prechosen) { + return opts.prechosen; + } + + const sidecars = ["Redis", ...nonRedisPicked(dbs)]; + p.note( + [ + `BrainAPI needs these backing services for your setup:`, + ` ${pc.cyan(sidecars.join(", "))}`, + ``, + `You can let BrainAPI manage them via Docker, or run them yourself.`, + ].join("\n"), + "Backing services", + ); + + const picked = opts?.allowBack + ? await pickOne({ + message: "How do you want to run them?", + options: [ + { + value: "docker", + label: "Docker Compose (recommended)", + hint: "isolated, reproducible, one extra dependency", + }, + { + value: "manual", + label: "Manual — I'll install/run them myself", + hint: "no Docker dependency; the TUI just probes for reachability", + }, + ], + initialValue: opts.initialValue ?? "docker", + allowBack: true, + backHint: opts.backHint, + }) + : await pickOne({ + message: "How do you want to run them?", + options: [ + { + value: "docker", + label: "Docker Compose (recommended)", + hint: "isolated, reproducible, one extra dependency", + }, + { + value: "manual", + label: "Manual — I'll install/run them myself", + hint: "no Docker dependency; the TUI just probes for reachability", + }, + ], + initialValue: opts?.initialValue ?? "docker", + }); + if (isPromptBack(picked)) { + return picked; + } + return picked; +} diff --git a/tui/src/flows/services.ts b/tui/src/flows/services.ts new file mode 100644 index 0000000000000000000000000000000000000000..f6b6c91e053c87eb8ae40c4d6f68467f9ab0cdfd --- /dev/null +++ b/tui/src/flows/services.ts @@ -0,0 +1,104 @@ +import * as p from "@clack/prompts"; +import { isPromptBack, pickOne, type PromptBack } from "../lib/prompts.js"; +import type { DbChoices, VectorDb, DataDb, GraphDb } from "../types.js"; + +export async function askDatabases(opts?: { + allowBack?: false; + initial?: Partial; + prechosen?: Partial; +}): Promise; +export async function askDatabases(opts: { + allowBack: true; + backHint?: string; + initial?: Partial; + prechosen?: Partial; +}): Promise; +export async function askDatabases(opts?: { + allowBack?: boolean; + backHint?: string; + initial?: Partial; + prechosen?: Partial; +}): Promise { + p.log.step("Pick your databases"); + + let vectorDb: VectorDb = opts?.prechosen?.vectorDb ?? opts?.initial?.vectorDb ?? "postgresql"; + let dataDb: DataDb = opts?.prechosen?.dataDb ?? opts?.initial?.dataDb ?? "postgresql"; + let graphDb: GraphDb = opts?.prechosen?.graphDb ?? opts?.initial?.graphDb ?? "networkx"; + let step = 0; + + while (step < 3) { + if (step === 0) { + if (opts?.prechosen?.vectorDb) { + step = 1; + continue; + } + const picked = opts?.allowBack + ? await pickOne<"postgresql" | "milvus">({ + message: "Vector database", + options: [ + { value: "postgresql", label: "PostgreSQL + pgvector (default)", hint: "self-hosted, lightweight" }, + { value: "milvus", label: "Milvus", hint: "dedicated vector DB" }, + ], + initialValue: vectorDb, + allowBack: true, + backHint: opts.backHint, + }) + : await pickOne<"postgresql" | "milvus">({ + message: "Vector database", + options: [ + { value: "postgresql", label: "PostgreSQL + pgvector (default)", hint: "self-hosted, lightweight" }, + { value: "milvus", label: "Milvus", hint: "dedicated vector DB" }, + ], + initialValue: vectorDb, + }); + if (isPromptBack(picked)) return picked; + vectorDb = picked; + step = 1; + continue; + } + if (step === 1) { + if (opts?.prechosen?.dataDb) { + step = 2; + continue; + } + const picked = await pickOne<"postgresql" | "mongo">({ + message: "Data database (text chunks, observations, structured data)", + options: [ + { value: "postgresql", label: "PostgreSQL (default)", hint: "reuses the same Postgres if picked above" }, + { value: "mongo", label: "MongoDB" }, + ], + initialValue: dataDb, + allowBack: true, + backHint: "Previous question", + }); + if (isPromptBack(picked)) { + step = 0; + continue; + } + dataDb = picked; + step = 2; + continue; + } + if (opts?.prechosen?.graphDb) { + return { vectorDb, dataDb, graphDb }; + } + const picked = await pickOne<"networkx" | "neo4j">({ + message: "Graph database", + options: [ + { value: "networkx", label: "NetworkX on PostgreSQL (default)", hint: "no extra DB server" }, + { value: "neo4j", label: "Neo4j", hint: "dedicated graph DB" }, + ], + initialValue: graphDb, + allowBack: true, + backHint: "Previous question", + }); + if (isPromptBack(picked)) { + step = 1; + continue; + } + graphDb = picked; + return { vectorDb, dataDb, graphDb }; + } + + return { vectorDb, dataDb, graphDb }; +} diff --git a/tui/src/flows/setup-wizard.ts b/tui/src/flows/setup-wizard.ts new file mode 100644 index 0000000000000000000000000000000000000000..cd5d63f021233e11b472a7f04244b27b1795f71b --- /dev/null +++ b/tui/src/flows/setup-wizard.ts @@ -0,0 +1,317 @@ +import * as p from "@clack/prompts"; +import { askUseDefaults, DEFAULT_DBS } from "./defaults.js"; +import { askDatabases } from "./services.js"; +import { askModels } from "./models.js"; +import { askPipeline } from "./pipeline.js"; +import { askSearch, defaultSearchChoices } from "./search.js"; +import { askConnections } from "./connections.js"; +import { askAuth } from "./auth.js"; +import { askServicesRuntime } from "./services-runtime.js"; +import { askPlugins } from "./plugins.js"; +import { isPromptBack } from "../lib/prompts.js"; +import { + hasConfigOverrides, + type InitFlagOverrides, +} from "../lib/init-flags.js"; +import type { + AuthChoices, + Connections, + DbChoices, + InitChoices, + ModelsChoices, + PluginChoice, + PipelineChoices, + SearchChoices, + ServicesRuntime, +} from "../types.js"; + +export interface SetupDraft { + dbs?: DbChoices; + servicesRuntime?: ServicesRuntime; + models?: ModelsChoices; + pipeline?: PipelineChoices; + search?: SearchChoices; + connections?: Connections; + auth?: AuthChoices; + plugins?: PluginChoice[]; + usedDefaults?: boolean; +} + +export interface RunSetupWizardOptions { + firstStepBack?: "return" | "cancel"; + stepBackHint?: string; + firstStepBackHint?: string; + flags?: InitFlagOverrides; +} + +export function toInitChoices(draft: SetupDraft): InitChoices { + return { + dbs: draft.dbs!, + servicesRuntime: draft.servicesRuntime!, + models: draft.models!, + pipeline: draft.pipeline!, + search: draft.search ?? defaultSearchChoices(), + connections: draft.connections!, + auth: draft.auth!, + plugins: draft.plugins ?? [], + usedDefaults: draft.usedDefaults ?? false, + }; +} + +function seedDraftFromFlags( + draft: SetupDraft, + flags: InitFlagOverrides | undefined, +): void { + if (!flags) return; + + if (flags.defaults) { + draft.usedDefaults = true; + draft.dbs = { ...DEFAULT_DBS }; + draft.pipeline = { ocrMode: flags.ocrMode ?? "docparser" }; + } + + if ( + flags.vectorDb !== undefined || + flags.dataDb !== undefined || + flags.graphDb !== undefined + ) { + draft.dbs = { + vectorDb: flags.vectorDb ?? draft.dbs?.vectorDb ?? DEFAULT_DBS.vectorDb, + dataDb: flags.dataDb ?? draft.dbs?.dataDb ?? DEFAULT_DBS.dataDb, + graphDb: flags.graphDb ?? draft.dbs?.graphDb ?? DEFAULT_DBS.graphDb, + }; + } + + if (flags.ocrMode) { + draft.pipeline = { ocrMode: flags.ocrMode }; + } + + if (flags.servicesRuntime) { + draft.servicesRuntime = flags.servicesRuntime; + } + + if (flags.brainpatToken) { + draft.auth = { brainpatToken: flags.brainpatToken }; + } + + if (flags.noPlugins) { + draft.plugins = []; + } else if (flags.plugins) { + draft.plugins = flags.plugins; + } +} + +export async function runSetupWizard( + draft: SetupDraft, + opts: RunSetupWizardOptions = {}, +): Promise { + const flags = opts.flags; + seedDraftFromFlags(draft, flags); + + const stepBackHint = opts.stepBackHint ?? "Previous step"; + const firstStepBackHint = opts.firstStepBackHint ?? stepBackHint; + let step = 0; + + const skipUseDefaults = + Boolean(flags?.defaults) || (flags !== undefined && hasConfigOverrides(flags)); + + while (step >= 0) { + if (step === 0) { + if (skipUseDefaults) { + if (flags?.defaults) { + draft.usedDefaults = true; + draft.dbs = draft.dbs ?? { ...DEFAULT_DBS }; + draft.pipeline = draft.pipeline ?? { ocrMode: "docparser" }; + step = 2; + continue; + } + draft.usedDefaults = false; + step = 1; + continue; + } + + const useDefaults = await askUseDefaults({ + allowBack: true, + backHint: firstStepBackHint, + }); + if (isPromptBack(useDefaults)) { + if (opts.firstStepBack === "cancel") { + p.cancel("Setup cancelled."); + process.exit(0); + } + return false; + } + draft.usedDefaults = useDefaults; + if (useDefaults) { + draft.dbs = DEFAULT_DBS; + draft.pipeline = { ocrMode: "docparser" }; + step = 2; + continue; + } + step = 1; + continue; + } + + if (step === 1) { + const allDbsPrechosen = + flags?.vectorDb !== undefined && + flags?.dataDb !== undefined && + flags?.graphDb !== undefined; + if (allDbsPrechosen && draft.dbs) { + step = 2; + continue; + } + const dbs = await askDatabases({ + allowBack: true, + backHint: stepBackHint, + initial: draft.dbs, + prechosen: flags + ? { + vectorDb: flags.vectorDb, + dataDb: flags.dataDb, + graphDb: flags.graphDb, + } + : undefined, + }); + if (isPromptBack(dbs)) { + step = 0; + continue; + } + draft.dbs = dbs; + step = 2; + continue; + } + + if (step === 2) { + if (!draft.dbs) { + step = 1; + continue; + } + if (flags?.servicesRuntime) { + draft.servicesRuntime = flags.servicesRuntime; + step = 3; + continue; + } + const runtime = await askServicesRuntime(draft.dbs, { + allowBack: true, + backHint: stepBackHint, + initialValue: draft.servicesRuntime, + prechosen: flags?.servicesRuntime, + }); + if (isPromptBack(runtime)) { + step = draft.usedDefaults ? 0 : 1; + continue; + } + draft.servicesRuntime = runtime; + step = 3; + continue; + } + + if (step === 3) { + const models = draft.usedDefaults + ? await askModels({ + prechosenMode: flags?.modelsMode ?? "remote", + initialMode: draft.models?.mode, + flags, + }) + : await askModels({ + allowBack: true, + backHint: stepBackHint, + initialMode: draft.models?.mode, + prechosenMode: flags?.modelsMode, + flags, + }); + if (isPromptBack(models)) { + step = 2; + continue; + } + draft.models = models; + step = draft.usedDefaults ? 5 : 4; + continue; + } + + if (step === 4) { + const pipeline = await askPipeline({ + allowBack: true, + backHint: stepBackHint, + initialOcrMode: draft.pipeline?.ocrMode, + prechosenOcrMode: flags?.ocrMode, + }); + if (isPromptBack(pipeline)) { + step = 3; + continue; + } + draft.pipeline = pipeline; + step = 5; + continue; + } + + if (step === 5) { + const search = await askSearch({ + allowBack: true, + backHint: stepBackHint, + initial: draft.search, + dataDb: draft.dbs?.dataDb, + }); + if (isPromptBack(search)) { + step = draft.usedDefaults ? 3 : 4; + continue; + } + draft.search = search; + step = 6; + continue; + } + + if (step === 6) { + if (!draft.dbs) { + step = draft.usedDefaults ? 0 : 1; + continue; + } + draft.connections = await askConnections(draft.dbs, flags); + step = 7; + continue; + } + + if (step === 7) { + if (flags?.brainpatToken) { + draft.auth = { brainpatToken: flags.brainpatToken }; + step = 8; + continue; + } + const auth = await askAuth({ + allowBack: true, + backHint: stepBackHint, + prechosenToken: flags?.brainpatToken, + }); + if (isPromptBack(auth)) { + step = 6; + continue; + } + draft.auth = auth; + step = 8; + continue; + } + + if (step === 8) { + if (flags?.noPlugins || flags?.plugins !== undefined) { + draft.plugins = flags.noPlugins ? [] : (flags.plugins ?? []); + return true; + } + const plugins = await askPlugins({ + allowBack: true, + backHint: stepBackHint, + initial: draft.plugins, + prechosen: flags?.plugins, + skip: flags?.noPlugins, + }); + if (isPromptBack(plugins)) { + step = 7; + continue; + } + draft.plugins = plugins; + return true; + } + } + + return false; +} diff --git a/tui/src/index.ts b/tui/src/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..048c57407405170ff78de44199bfd0f37251903a --- /dev/null +++ b/tui/src/index.ts @@ -0,0 +1,336 @@ +import * as p from "@clack/prompts"; +import pc from "picocolors"; +import { runInit } from "./commands/init.js"; +import { runStart } from "./commands/start.js"; +import { runConfig } from "./commands/config.js"; +import { runDoctor } from "./commands/doctor.js"; +import { runUpdate } from "./commands/update.js"; +import { runPlugins, type PluginsOptions, type PluginsSubcommand } from "./commands/plugins.js"; +import { runReset, type ResetCommandOptions } from "./commands/reset.js"; +import { readState } from "./lib/state.js"; +import { + tryApplyInitFlag, + type InitFlagOverrides, +} from "./lib/init-flags.js"; +import { isPipelineMode, type PipelineMode } from "./types.js"; + +type Command = + | "init" + | "start" + | "config" + | "doctor" + | "update" + | "plugins" + | "reset" + | "help"; + +interface ParsedArgs { + command: Command; + repo?: string; + branch?: string; + force?: boolean; + noApi?: boolean; + noMcp?: boolean; + noWorker?: boolean; + noServices?: boolean; + only?: Array<"api" | "mcp" | "worker" | "services">; + pipeline?: PipelineMode; + invalidPipeline?: string; + plugins?: PluginsOptions; + reset?: ResetCommandOptions; + initFlags?: InitFlagOverrides; + initFlagError?: string; + unknown: string[]; +} + +const PLUGIN_SUBCOMMANDS = new Set([ + "install", + "uninstall", + "list", + "info", + "update", +]); + +function isPluginsSubcommand(value: string): value is PluginsSubcommand { + return PLUGIN_SUBCOMMANDS.has(value as PluginsSubcommand); +} + +function parseArgs(argv: string[]): ParsedArgs { + const args = [...argv]; + let command: Command = "help"; + const unknown: string[] = []; + const out: ParsedArgs = { command, unknown }; + let initFlags: InitFlagOverrides | undefined; + + while (args.length > 0) { + const arg = args.shift()!; + if (arg === "--repo" || arg === "-r") { + out.repo = args.shift(); + } else if (arg === "--branch" || arg === "-b") { + out.branch = args.shift(); + } else if (arg === "--force" || arg === "-f") { + out.force = true; + } else if (arg === "--no-api") { + out.noApi = true; + } else if (arg === "--no-mcp") { + out.noMcp = true; + } else if (arg === "--no-worker") { + out.noWorker = true; + } else if (arg === "--no-services") { + out.noServices = true; + } else if (arg === "--pipeline") { + const raw = (args.shift() ?? "").trim().toLowerCase(); + if (isPipelineMode(raw)) { + out.pipeline = raw; + } else { + out.invalidPipeline = raw || "(missing)"; + } + } else if (arg === "--only") { + const value = args.shift() ?? ""; + const parts = value + .split(",") + .map((s) => s.trim().toLowerCase()) + .filter((s) => + s === "api" || s === "mcp" || s === "worker" || s === "services", + ) as Array<"api" | "mcp" | "worker" | "services">; + if (parts.length === 0) { + unknown.push(`--only ${value}`); + } else { + out.only = parts; + } + } else if (arg === "--help" || arg === "-h") { + out.command = "help"; + } else if (arg === "--version" || arg === "-v") { + printVersion(); + process.exit(0); + } else if (arg === "--init") { + out.command = "init"; + } else if (out.command === "help" && !arg.startsWith("-")) { + switch (arg) { + case "init": + case "start": + case "config": + case "doctor": + case "update": + case "plugins": + case "reset": + case "help": + out.command = arg; + break; + default: + unknown.push(arg); + } + } else if (out.command === "init") { + if (!initFlags) initFlags = {}; + const result = tryApplyInitFlag(arg, args, initFlags); + if (result.kind === "error") { + out.initFlagError = result.error; + return out; + } + if (result.kind === "unknown") { + unknown.push(arg); + } + } else if (out.command === "reset") { + if (!out.reset) out.reset = {}; + const reset = out.reset; + if (arg === "--brain") { + reset.brain = args.shift(); + } else if (arg === "--all") { + reset.all = true; + } else if (arg === "--queues-only") { + reset.queuesOnly = true; + } else if (arg === "--redis-only") { + reset.redisOnly = true; + } else if (arg === "--db-only") { + reset.dbOnly = true; + } else if (arg === "--yes" || arg === "-y") { + reset.yes = true; + } else { + unknown.push(arg); + } + } else if (out.command === "plugins") { + if (!out.plugins) out.plugins = {}; + const plugins = out.plugins; + if (!plugins.subcommand && !arg.startsWith("-")) { + if (isPluginsSubcommand(arg)) { + plugins.subcommand = arg; + } else { + unknown.push(arg); + } + } else if (arg === "--version") { + plugins.version = args.shift(); + } else if (arg === "--remote" || arg === "-r") { + plugins.remote = true; + } else if ( + plugins.subcommand && + plugins.subcommand !== "list" && + !plugins.name && + !arg.startsWith("-") + ) { + plugins.name = arg; + } else { + unknown.push(arg); + } + } else { + unknown.push(arg); + } + } + + if (initFlags && Object.keys(initFlags).length > 0) { + out.initFlags = initFlags; + } + return out; +} + +function printHelp(): void { + const lines = [ + pc.bold("brainapi") + " — interactive installer for BrainAPI", + "", + pc.dim("Usage:") + " brainapi [options]", + "", + pc.bold("Commands:"), + " init Clone repo, set up venv, run interactive config, optional containers", + " start [...] Start backing services + API + MCP + celery worker (Ctrl-C to stop all)", + " config Re-run the interactive flow and rewrite .env", + " doctor Check Python, Docker, Ollama, GCP credentials, services", + " update Fetch latest source + reinstall Python dependencies", + " plugins Manage plugins (install, uninstall, list, info, update)", + " reset [...] Purge Celery queues and/or wipe Redis + Postgres brain state", + " help Show this message", + "", + pc.bold("Options:"), + " -r, --repo Git repo to clone (default $BRAINAPI_REPO_URL or upstream)", + " -b, --branch Branch to checkout (default $BRAINAPI_BRANCH or 'main')", + " -f, --force Re-run init even if state exists", + " --init Alias for the init command", + " --no-services For 'start': skip bringing backing services up", + " --no-api For 'start': skip the API server", + " --no-mcp For 'start': skip the MCP server", + " --no-worker For 'start': skip the celery worker", + " --only api,mcp,worker For 'start': run only the listed processes (shortcut)", + " --pipeline accurate|lightweight Set PIPELINE_MODE in ~/.brainapi/source/.env before start", + " --brain For 'reset': wipe one brain (Redis keys + DROP brain_* DB)", + " --all For 'reset': Redis FLUSHDB + DROP all brain_* databases", + " --queues-only For 'reset': purge Celery queue keys only", + " --redis-only For 'reset': skip Postgres", + " --db-only For 'reset': skip Redis", + " -y, --yes For 'reset': skip confirmation prompt", + " -v, --version Print the CLI version", + " -h, --help Show this message", + "", + pc.bold("Init flags:") + " " + pc.dim("(provided values skip that wizard prompt)"), + " --defaults Use default DB stack (Postgres + pgvector + NetworkX)", + " --vector-db|--data-db|--graph-db|--ocr-mode|--services-runtime", + " --models-mode|--llm-*-provider|--llm-small|--llm-large|--embedding-model", + " --start-services|--no-start-services|--brainpat-token|--plugin|--no-plugins", + " …plus connection + provider credential flags (see README)", + "", + pc.dim("Environment overrides: BRAINAPI_HOME, BRAINAPI_REPO_URL, BRAINAPI_BRANCH"), + ]; + console.log(lines.join("\n")); +} + +function printVersion(): void { + console.log("brainapi tui CLI"); +} + +async function autoInitIfNeeded(command: Command): Promise { + if (command === "help" || command === "init") return command; + const state = await readState(); + if (state) return command; + p.log.info( + "No brainapi install found yet — running " + pc.cyan("brainapi init") + " first.", + ); + return "init"; +} + +async function main(): Promise { + const parsed = parseArgs(process.argv.slice(2)); + if (parsed.initFlagError) { + p.log.error(parsed.initFlagError); + process.exit(1); + } + if (parsed.invalidPipeline !== undefined) { + p.log.error( + `Invalid --pipeline value "${parsed.invalidPipeline}". Use accurate or lightweight.`, + ); + process.exit(1); + } + if (parsed.unknown.length > 0 && parsed.command !== "help") { + p.log.warn("Unknown argument(s): " + parsed.unknown.join(", ")); + } + + let cmd = parsed.command; + if (cmd === "help") { + if (parsed.unknown.length === 0 && process.argv.slice(2).length === 0) { + cmd = await autoInitIfNeeded("init"); + } else { + printHelp(); + return; + } + } else { + cmd = await autoInitIfNeeded(cmd); + } + + try { + switch (cmd) { + case "init": + await runInit({ + repoUrl: parsed.repo, + branch: parsed.branch, + force: parsed.force, + flags: parsed.initFlags, + }); + return; + case "start": { + const startOpts = parsed.only + ? { + api: parsed.only.includes("api"), + mcp: parsed.only.includes("mcp"), + worker: parsed.only.includes("worker"), + services: parsed.only.includes("services") || true, + } + : { + api: !parsed.noApi, + mcp: !parsed.noMcp, + worker: !parsed.noWorker, + services: !parsed.noServices, + }; + if (parsed.only) { + startOpts.services = parsed.only.includes("services"); + } + await runStart({ + ...startOpts, + pipelineMode: parsed.pipeline, + }); + return; + } + case "config": + await runConfig(); + return; + case "doctor": + await runDoctor(); + return; + case "update": + await runUpdate(); + return; + case "plugins": + await runPlugins(parsed.plugins ?? {}); + return; + case "reset": + await runReset(parsed.reset ?? {}); + return; + case "help": + printHelp(); + return; + } + } catch (err) { + p.log.error(err instanceof Error ? err.message : String(err)); + process.exit(1); + } +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/tui/src/lib/bring-up.ts b/tui/src/lib/bring-up.ts new file mode 100644 index 0000000000000000000000000000000000000000..c3769c3104e0c38db013fe8c25b5ed3e9dc70d90 --- /dev/null +++ b/tui/src/lib/bring-up.ts @@ -0,0 +1,165 @@ +import * as p from "@clack/prompts"; +import pc from "picocolors"; +import { composeFileExists, composeFileFor, composeUp } from "./docker.js"; +import { tcpProbe } from "./net.js"; +import { nativeRecipeFor } from "./native-install.js"; +import { detectCapabilities } from "./platform.js"; +import type { ServiceName } from "../constants.js"; +import type { ServiceTarget } from "./service-target.js"; + +export interface BringUpInput { + service: ServiceName; + target: ServiceTarget | null; +} + +export async function bringUpServices(items: BringUpInput[]): Promise { + for (const { service, target } of items) { + if (target?.managed) { + p.log.info( + `${service}: managed deployment configured — skipping local docker compose up`, + ); + continue; + } + + if (!(await composeFileExists(service))) { + p.log.warn( + `${service}: compose file not found at ${pc.cyan( + composeFileFor(service), + )}. Skipping container start.`, + ); + p.log.info( + `Fix: push ${pc.cyan( + `src/lib/${service}/docker-compose.yaml`, + )} to your remote, then re-run ${pc.cyan( + "brainapi update", + )}. Or copy it manually into ${pc.cyan("~/.brainapi/source/")}.`, + ); + continue; + } + + if (target && (await tcpProbe({ host: target.host, port: target.port, timeoutMs: 750 }))) { + p.log.info( + `${service} already reachable on ${target.host}:${target.port} — skipping ${pc.cyan( + "docker compose up", + )} (using existing service).`, + ); + continue; + } + + const spinner = p.spinner(); + spinner.start(`docker compose up -d ${service}`); + try { + await composeUp(service); + spinner.stop(`${service} up`); + } catch (err) { + spinner.stop(pc.red(`${service} failed to start`)); + p.log.error(err instanceof Error ? err.message : String(err)); + } + } +} + +const DEFAULT_WAIT_SECONDS = 60; +const PROBE_INTERVAL_MS = 1500; + +export async function bringUpServicesManually( + items: BringUpInput[], +): Promise { + const caps = await detectCapabilities(); + + for (const { service, target } of items) { + if (target?.managed) { + p.log.info( + `${service}: managed deployment configured — using ${pc.cyan( + `${target.host}:${target.port}`, + )}`, + ); + continue; + } + + if (!target) { + p.log.warn(`${service}: no connection details configured. Skipping.`); + continue; + } + + if (await tcpProbe({ host: target.host, port: target.port, timeoutMs: 750 })) { + p.log.success( + `${service} reachable on ${pc.cyan(`${target.host}:${target.port}`)}`, + ); + continue; + } + + p.log.warn( + `${service} is not running on ${pc.cyan(`${target.host}:${target.port}`)}.`, + ); + printNativeRecipe(service, caps); + + const ok = await waitForReachable( + service, + target.host, + target.port, + DEFAULT_WAIT_SECONDS, + ); + if (!ok) { + p.log.warn( + `${service} still not reachable after ${DEFAULT_WAIT_SECONDS}s — continuing anyway. ` + + `BrainAPI will fail at runtime until you start ${pc.cyan(service)} on ${pc.cyan( + `${target.host}:${target.port}`, + )}.`, + ); + } + } +} + +function printNativeRecipe( + service: ServiceName, + caps: Awaited>, +): void { + const recipe = nativeRecipeFor(service, caps); + if (!recipe) { + p.log.info( + `No native install recipe known for ${pc.cyan( + service, + )} on your platform. Please install it manually.`, + ); + return; + } + if (recipe.unsupported) { + p.log.warn(`${service}: ${recipe.unsupported}`); + if (recipe.docsUrl) p.log.info(`Docs: ${pc.cyan(recipe.docsUrl)}`); + return; + } + const lines: string[] = [`Install ${pc.bold(service)}:`]; + for (const cmd of recipe.install) lines.push(` ${pc.cyan(cmd)}`); + if (recipe.start.length > 0) { + lines.push("", "Start it:"); + for (const cmd of recipe.start) lines.push(` ${pc.cyan(cmd)}`); + } + if (recipe.postInstall && recipe.postInstall.length > 0) { + lines.push("", "Post-install:"); + for (const cmd of recipe.postInstall) lines.push(` ${pc.dim(cmd)}`); + } + if (recipe.docsUrl) { + lines.push("", `Docs: ${pc.cyan(recipe.docsUrl)}`); + } + p.note(lines.join("\n"), `${service} — manual install`); +} + +async function waitForReachable( + service: ServiceName, + host: string, + port: number, + totalSeconds: number, +): Promise { + const spinner = p.spinner(); + spinner.start(`Waiting for ${service} to become reachable on ${host}:${port}`); + const deadline = Date.now() + totalSeconds * 1000; + while (Date.now() < deadline) { + if (await tcpProbe({ host, port, timeoutMs: 750 })) { + spinner.stop(`${service} reachable on ${host}:${port}`); + return true; + } + await new Promise((resolve) => setTimeout(resolve, PROBE_INTERVAL_MS)); + } + spinner.stop(pc.yellow(`${service} not reachable after ${totalSeconds}s`)); + return false; +} diff --git a/tui/src/lib/console-build.ts b/tui/src/lib/console-build.ts new file mode 100644 index 0000000000000000000000000000000000000000..7910a7cb903527b19aa85f95f967e35405556eb9 --- /dev/null +++ b/tui/src/lib/console-build.ts @@ -0,0 +1,44 @@ +import { access } from "node:fs/promises"; +import path from "node:path"; +import * as p from "@clack/prompts"; +import pc from "picocolors"; +import { sourcePath } from "./paths.js"; +import { runInherit } from "./exec.js"; + +const CONSOLE_DIST = path.join(sourcePath(), "console", "dist", "index.html"); + +export async function consoleDistExists(): Promise { + try { + await access(CONSOLE_DIST); + return true; + } catch { + return false; + } +} + +export async function ensureConsoleBuilt(): Promise { + if (await consoleDistExists()) { + return; + } + const consoleDir = path.join(sourcePath(), "console"); + const spinner = p.spinner(); + spinner.start("Building local console (first run)"); + try { + const install = await runInherit("npm", ["install"], { cwd: consoleDir }); + if (!install.ok) { + throw new Error("npm install failed in console/"); + } + const build = await runInherit("npm", ["run", "build"], { cwd: consoleDir }); + if (!build.ok) { + throw new Error("npm run build failed in console/"); + } + spinner.stop("Local console built"); + } catch (err) { + spinner.stop(pc.red("Console build failed")); + p.log.warn( + `Could not build console UI: ${ + err instanceof Error ? err.message : String(err) + }. Run ${pc.cyan("make build-console")} manually.`, + ); + } +} diff --git a/tui/src/lib/docker-recovery.ts b/tui/src/lib/docker-recovery.ts new file mode 100644 index 0000000000000000000000000000000000000000..20bba3c58a2e9740352d37068b30a0e21cceb5e9 --- /dev/null +++ b/tui/src/lib/docker-recovery.ts @@ -0,0 +1,251 @@ +import { access } from "node:fs/promises"; +import * as p from "@clack/prompts"; +import pc from "picocolors"; +import { runInherit } from "./exec.js"; +import { detectDocker, waitForDaemon, type DockerState } from "./docker.js"; +import { detectCapabilities } from "./platform.js"; +import { askConfirm, pickOne } from "./prompts.js"; + +interface DockerOption { + value: string; + label: string; + command?: { bin: string; args: string[] }; + manualInstructions?: string; + openUrl?: string; +} + +const MAX_RETRIES = 3; + +async function openUrl(url: string): Promise { + const cmd = process.platform === "darwin" + ? { bin: "open", args: [url] } + : process.platform === "win32" + ? { bin: "cmd", args: ["/c", "start", "", url] } + : { bin: "xdg-open", args: [url] }; + await runInherit(cmd.bin, cmd.args); +} + +async function dockerDesktopExistsOnMac(): Promise { + try { + await access("/Applications/Docker.app"); + return true; + } catch { + return false; + } +} + +async function buildOptions(): Promise { + const caps = await detectCapabilities(); + const options: DockerOption[] = []; + + if (caps.platform === "darwin") { + if (caps.hasBrew) { + options.push({ + value: "brew-cask", + label: "Install Docker Desktop via Homebrew Cask (brew install --cask docker)", + command: { bin: "brew", args: ["install", "--cask", "docker"] }, + }); + } + options.push({ + value: "docker-download-mac", + label: "Open Docker Desktop download page", + openUrl: "https://www.docker.com/products/docker-desktop/", + }); + } else if (caps.platform === "linux") { + for (const pm of caps.linuxPackageManagers) { + const args = pm === "pacman" + ? ["sudo", pm, "-S", "--noconfirm", "docker", "docker-compose"] + : pm === "apk" + ? ["sudo", pm, "add", "docker", "docker-compose"] + : pm === "zypper" + ? ["sudo", pm, "install", "-y", "docker", "docker-compose"] + : ["sudo", pm, "install", "-y", "docker.io", "docker-compose-plugin"]; + options.push({ + value: `pm-${pm}`, + label: `Install via ${pm} (${args.slice(1).join(" ")})`, + command: { bin: args[0]!, args: args.slice(1) }, + }); + } + options.push({ + value: "docker-script", + label: "Install via Docker's convenience script (curl -fsSL https://get.docker.com | sh)", + manualInstructions: + "curl -fsSL https://get.docker.com | sh\nsudo usermod -aG docker $USER\nnewgrp docker", + }); + } else if (caps.platform === "win32") { + if (caps.hasWinget) { + options.push({ + value: "winget", + label: "Install via winget (winget install Docker.DockerDesktop)", + command: { bin: "winget", args: ["install", "Docker.DockerDesktop"] }, + }); + } + if (caps.hasChoco) { + options.push({ + value: "choco", + label: "Install via Chocolatey (choco install docker-desktop)", + command: { bin: "choco", args: ["install", "docker-desktop", "-y"] }, + }); + } + options.push({ + value: "docker-download-win", + label: "Open Docker Desktop download page", + openUrl: "https://www.docker.com/products/docker-desktop/", + }); + } + + options.push({ + value: "manual", + label: "I'll install it manually — wait and retry", + }); + options.push({ + value: "cancel", + label: "Cancel — skip container start", + }); + + return options; +} + +function manualInstructionsFor(): string { + if (process.platform === "darwin") { + return [ + "# Option A: Homebrew Cask", + "brew install --cask docker", + "", + "# Option B: Download installer", + "open https://www.docker.com/products/docker-desktop/", + ].join("\n"); + } + if (process.platform === "linux") { + return [ + "# Debian/Ubuntu", + "sudo apt-get install -y docker.io docker-compose-plugin", + "", + "# Fedora/RHEL", + "sudo dnf install -y docker docker-compose", + "", + "# Add yourself to the docker group:", + "sudo usermod -aG docker $USER", + "newgrp docker", + "", + "# Or convenience script:", + "curl -fsSL https://get.docker.com | sh", + ].join("\n"); + } + return [ + "winget install Docker.DockerDesktop", + "", + "# Or download:", + "https://www.docker.com/products/docker-desktop/", + ].join("\n"); +} + +async function handleMissing(): Promise<"installed" | "cancelled"> { + let attempt = 0; + while (attempt < MAX_RETRIES) { + attempt += 1; + p.log.warn( + attempt === 1 + ? "Docker (or the compose plugin) was not found on your PATH." + : `Still no Docker detected (attempt ${attempt}/${MAX_RETRIES}).`, + ); + const options = await buildOptions(); + const choice = await pickOne({ + message: "How would you like to install Docker?", + options: options.map((o) => ({ value: o.value, label: o.label })), + }); + if (choice === "cancel") return "cancelled"; + + const selected = options.find((o) => o.value === choice); + if (!selected) continue; + + if (selected.openUrl) { + await openUrl(selected.openUrl); + } else if (selected.command) { + const cmdStr = `${selected.command.bin} ${selected.command.args.join(" ")}`; + p.note(cmdStr, "Command to run"); + const confirmRun = await askConfirm({ + message: "Run this for me now?", + initialValue: true, + }); + if (confirmRun) { + await runInherit(selected.command.bin, selected.command.args); + } + } else if (selected.value === "manual") { + p.note(manualInstructionsFor(), "Manual install — copy and run"); + } else if (selected.manualInstructions) { + p.note(selected.manualInstructions, "Copy and run"); + } + + await askConfirm({ + message: "Press enter when Docker is installed", + initialValue: true, + }); + + const state = await detectDocker(); + if (state === "ok") return "installed"; + if (state === "daemon_down") { + const recovered = await handleDaemonDown(); + if (recovered === "ok") return "installed"; + if (recovered === "cancelled") return "cancelled"; + } + } + return "cancelled"; +} + +async function handleDaemonDown(): Promise<"ok" | "cancelled"> { + p.log.warn("Docker is installed but the daemon is not responding."); + + if (process.platform === "darwin" && (await dockerDesktopExistsOnMac())) { + const launch = await askConfirm({ + message: "Launch Docker Desktop now?", + initialValue: true, + }); + if (launch) { + await runInherit("open", ["-a", "Docker"]); + } + } else if (process.platform === "linux") { + p.note( + "sudo systemctl start docker", + "Try starting the daemon", + ); + } + + const spinner = p.spinner(); + spinner.start("Waiting for Docker daemon (up to 60s)"); + const ok = await waitForDaemon(60_000); + if (ok) { + spinner.stop(pc.green("Docker daemon is up.")); + return "ok"; + } + spinner.stop(pc.yellow("Docker daemon did not start in time.")); + const retry = await askConfirm({ + message: "Start Docker manually and press enter to retry — or cancel.", + initialValue: true, + }); + if (!retry) return "cancelled"; + const state = await detectDocker(); + return state === "ok" ? "ok" : "cancelled"; +} + +export async function ensureDocker(): Promise<"ready" | "cancelled"> { + const state = await detectDocker(); + if (state === "ok") return "ready"; + if (state === "missing") { + const result = await handleMissing(); + return result === "installed" ? "ready" : "cancelled"; + } + const result = await handleDaemonDown(); + return result === "ok" ? "ready" : "cancelled"; +} + +export function describeDockerState(state: DockerState): string { + switch (state) { + case "ok": + return "Docker is installed and the daemon is running"; + case "missing": + return "Docker is not installed (or compose plugin missing)"; + case "daemon_down": + return "Docker is installed but the daemon is not responding"; + } +} diff --git a/tui/src/lib/docker.ts b/tui/src/lib/docker.ts new file mode 100644 index 0000000000000000000000000000000000000000..241e8123a331fa5b6a34250b7a7fb1ef77b0950d --- /dev/null +++ b/tui/src/lib/docker.ts @@ -0,0 +1,65 @@ +import { access } from "node:fs/promises"; +import path from "node:path"; +import { sourcePath } from "./paths.js"; +import { runInherit, runQuiet } from "./exec.js"; +import { + SERVICE_COMPOSE_FILES, + type ServiceName, +} from "../constants.js"; + +export type DockerState = "ok" | "missing" | "daemon_down"; + +export async function detectDocker(): Promise { + const cli = await runQuiet("docker", ["--version"]); + if (!cli.ok) return "missing"; + const compose = await runQuiet("docker", ["compose", "version"]); + if (!compose.ok) { + const legacy = await runQuiet("docker-compose", ["--version"]); + if (!legacy.ok) return "missing"; + } + const info = await runQuiet("docker", ["info"]); + if (!info.ok) return "daemon_down"; + return "ok"; +} + +export function composeFileFor(service: ServiceName): string { + return path.join(sourcePath(), SERVICE_COMPOSE_FILES[service]); +} + +export async function composeFileExists(service: ServiceName): Promise { + try { + await access(composeFileFor(service)); + return true; + } catch { + return false; + } +} + +export async function composeUp(service: ServiceName): Promise { + const file = composeFileFor(service); + const result = await runInherit("docker", [ + "compose", + "-f", + file, + "up", + "-d", + ]); + if (!result.ok) { + throw new Error(`docker compose up failed for ${service}`); + } +} + +export async function composeDown(service: ServiceName): Promise { + const file = composeFileFor(service); + await runInherit("docker", ["compose", "-f", file, "down"]); +} + +export async function waitForDaemon(timeoutMs = 60_000): Promise { + const start = Date.now(); + while (Date.now() - start < timeoutMs) { + const info = await runQuiet("docker", ["info"]); + if (info.ok) return true; + await new Promise((resolve) => setTimeout(resolve, 2000)); + } + return false; +} diff --git a/tui/src/lib/env.ts b/tui/src/lib/env.ts new file mode 100644 index 0000000000000000000000000000000000000000..1666abb4c90b72e24f7623c8c8d4cd6551af6753 --- /dev/null +++ b/tui/src/lib/env.ts @@ -0,0 +1,141 @@ +import { readFile, writeFile, access } from "node:fs/promises"; +import path from "node:path"; +import { ENV_KEYS } from "../constants.js"; +import type { PipelineMode } from "../types.js"; +import { envFilePath, sourcePath } from "./paths.js"; + +const QUOTE_NEEDED = /[\s#"'=]/; + +function escapeValue(value: string): string { + if (value === "") return '""'; + if (!QUOTE_NEEDED.test(value)) return value; + return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`; +} + +function parseLine( + line: string, +): { key: string; rawValue: string } | null { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith("#")) return null; + const idx = line.indexOf("="); + if (idx < 0) return null; + return { + key: line.slice(0, idx).trim(), + rawValue: line.slice(idx + 1), + }; +} + +export interface EnvFile { + /** Original line content in file order. */ + lines: string[]; + /** Map of key -> index into `lines` for quick lookup/update. */ + keyIndex: Map; +} + +export async function readEnvFile(filePath: string): Promise { + let text = ""; + try { + text = await readFile(filePath, "utf8"); + } catch (err) { + if ((err as NodeJS.ErrnoException).code !== "ENOENT") throw err; + } + const lines = text.length === 0 ? [] : text.split(/\r?\n/); + if (lines.length > 0 && lines[lines.length - 1] === "") lines.pop(); + const keyIndex = new Map(); + lines.forEach((line, i) => { + const parsed = parseLine(line); + if (parsed) keyIndex.set(parsed.key, i); + }); + return { lines, keyIndex }; +} + +export function setEnvValue(env: EnvFile, key: string, value: string): void { + const newLine = `${key}=${escapeValue(value)}`; + const existing = env.keyIndex.get(key); + if (existing !== undefined) { + env.lines[existing] = newLine; + } else { + env.lines.push(newLine); + env.keyIndex.set(key, env.lines.length - 1); + } +} + +export function removeEnvValue(env: EnvFile, key: string): void { + const existing = env.keyIndex.get(key); + if (existing === undefined) return; + env.lines.splice(existing, 1); + env.keyIndex.clear(); + env.lines.forEach((line, i) => { + const parsed = parseLine(line); + if (parsed) env.keyIndex.set(parsed.key, i); + }); +} + +export function applyEnvValues( + env: EnvFile, + values: Record, +): void { + for (const [key, value] of Object.entries(values)) { + if (value === undefined) continue; + setEnvValue(env, key, String(value)); + } +} + +export async function writeEnvFile( + filePath: string, + env: EnvFile, +): Promise { + const content = env.lines.join("\n") + "\n"; + await writeFile(filePath, content, "utf8"); +} + +export async function loadOrSeedEnv(): Promise { + const targetPath = envFilePath(); + try { + await access(targetPath); + return await readEnvFile(targetPath); + } catch { + const examplePath = path.join(sourcePath(), ".env.example"); + return await readEnvFile(examplePath); + } +} + +export async function saveEnv(env: EnvFile): Promise { + await writeEnvFile(envFilePath(), env); +} + +export async function setPipelineMode(mode: PipelineMode): Promise { + const targetPath = envFilePath(); + const env = await readEnvFile(targetPath); + setEnvValue(env, ENV_KEYS.pipelineMode, mode); + await writeEnvFile(targetPath, env); +} + +export function getEnvValue(env: EnvFile, key: string): string | undefined { + const idx = env.keyIndex.get(key); + if (idx === undefined) return undefined; + const raw = (env.lines[idx] ?? "").split("=").slice(1).join("="); + return raw.trim().replace(/^"|"$/g, ""); +} + +export function resolvedModelsMode(env: EnvFile): "local" | "remote" { + const small = getEnvValue(env, "LLM_SMALL_PROVIDER"); + const large = getEnvValue(env, "LLM_LARGE_PROVIDER"); + const embeddings = getEnvValue(env, "EMBEDDINGS_PROVIDER"); + if (small === "ollama" && large === "ollama" && embeddings === "ollama") { + return "local"; + } + const mode = getEnvValue(env, "MODELS_MODE"); + if (mode === "local" || mode === "remote") return mode; + return "remote"; +} + +export function envFileToProcessEnv(env: EnvFile): NodeJS.ProcessEnv { + const out: Record = {}; + for (const [key, idx] of env.keyIndex.entries()) { + const value = getEnvValue(env, key); + if (value !== undefined) out[key] = value; + } + out.MODELS_MODE = resolvedModelsMode(env); + return { ...process.env, ...out }; +} diff --git a/tui/src/lib/exec.ts b/tui/src/lib/exec.ts new file mode 100644 index 0000000000000000000000000000000000000000..247c686451ac0933e5c740b8043d2876f0ed3f0f --- /dev/null +++ b/tui/src/lib/exec.ts @@ -0,0 +1,72 @@ +import { execa, type ExecaError, type Options } from "execa"; + +export type RunResult = { + exitCode: number; + stdout: string; + stderr: string; + ok: boolean; +}; + +export async function runQuiet( + bin: string, + args: string[] = [], + options: Options = {}, +): Promise { + try { + const result = await execa(bin, args, { + reject: false, + stdout: "pipe", + stderr: "pipe", + ...options, + }); + return { + exitCode: result.exitCode ?? 1, + stdout: result.stdout?.toString() ?? "", + stderr: result.stderr?.toString() ?? "", + ok: (result.exitCode ?? 1) === 0, + }; + } catch (err) { + const ee = err as ExecaError; + return { + exitCode: typeof ee.exitCode === "number" ? ee.exitCode : 1, + stdout: ee.stdout?.toString() ?? "", + stderr: ee.stderr?.toString() ?? (ee.message ?? ""), + ok: false, + }; + } +} + +export async function runInherit( + bin: string, + args: string[] = [], + options: Options = {}, +): Promise { + try { + const result = await execa(bin, args, { + reject: false, + stdout: "inherit", + stderr: "inherit", + ...options, + }); + return { + exitCode: result.exitCode ?? 1, + stdout: "", + stderr: "", + ok: (result.exitCode ?? 1) === 0, + }; + } catch (err) { + const ee = err as ExecaError; + return { + exitCode: typeof ee.exitCode === "number" ? ee.exitCode : 1, + stdout: "", + stderr: ee.message ?? "", + ok: false, + }; + } +} + +export async function which(bin: string): Promise { + const finder = process.platform === "win32" ? "where" : "which"; + const result = await runQuiet(finder, [bin]); + return result.ok && result.stdout.trim().length > 0; +} diff --git a/tui/src/lib/git.ts b/tui/src/lib/git.ts new file mode 100644 index 0000000000000000000000000000000000000000..b7f8e1c4a1f0e5a83f4efbf0f496b8ce44960cdf --- /dev/null +++ b/tui/src/lib/git.ts @@ -0,0 +1,55 @@ +import { access, mkdir } from "node:fs/promises"; +import path from "node:path"; +import { simpleGit } from "simple-git"; +import { brainapiHome, sourcePath } from "./paths.js"; +import { updateState } from "./state.js"; + +export interface CloneOptions { + repoUrl: string; + branch: string; + depth?: number; + onProgress?: (line: string) => void; +} + +export async function isClonedRepo(): Promise { + try { + await access(path.join(sourcePath(), ".git")); + return true; + } catch { + return false; + } +} + +export async function cloneRepo(opts: CloneOptions): Promise { + await mkdir(brainapiHome(), { recursive: true }); + const target = sourcePath(); + const git = simpleGit({ + progress: ({ method, stage, progress }) => { + opts.onProgress?.(`${method} ${stage} ${progress}%`); + }, + }); + await git.clone(opts.repoUrl, target, [ + "--branch", + opts.branch, + ...(opts.depth ? ["--depth", String(opts.depth)] : []), + ]); + await updateState({ cloned: true, repoUrl: opts.repoUrl, branch: opts.branch }); +} + +export async function pullRepo(branch: string): Promise { + const git = simpleGit(sourcePath()); + const remoteRef = `origin/${branch}`; + await git.fetch("origin", branch); + await git.clean("f", ["-d", "-e", ".env", "-e", ".venv"]); + await git.checkout(["-B", branch, "-f"]); + await git.reset(["--hard", remoteRef]); +} + +export async function ensureRepo(opts: CloneOptions): Promise<"cloned" | "pulled"> { + if (await isClonedRepo()) { + await pullRepo(opts.branch); + return "pulled"; + } + await cloneRepo(opts); + return "cloned"; +} diff --git a/tui/src/lib/init-flags.ts b/tui/src/lib/init-flags.ts new file mode 100644 index 0000000000000000000000000000000000000000..4e9c899153193d5dd8e9aa9d723ab8d84e2015bf --- /dev/null +++ b/tui/src/lib/init-flags.ts @@ -0,0 +1,507 @@ +import type { + DataDb, + GraphDb, + ModelProvider, + ModelsMode, + OcrMode, + PluginChoice, + ServicesRuntime, + VectorDb, +} from "../types.js"; + +export interface InitFlagOverrides { + defaults?: boolean; + vectorDb?: VectorDb; + dataDb?: DataDb; + graphDb?: GraphDb; + ocrMode?: OcrMode; + servicesRuntime?: ServicesRuntime; + startServices?: boolean; + + modelsMode?: ModelsMode; + llmSmallProvider?: ModelProvider; + llmLargeProvider?: ModelProvider; + embeddingsProvider?: Exclude; + llmSmall?: string; + llmLarge?: string; + embeddingModel?: string; + embeddingDimensions?: number; + + ollamaHost?: string; + ollamaPort?: number; + ollamaSmall?: string; + ollamaLarge?: string; + ollamaEmbeddings?: string; + + gcpCredentials?: string; + gcpProject?: string; + gcpSmall?: string; + gcpLarge?: string; + gcpEmbedding?: string; + + azureEndpoint?: string; + azureApiVersion?: string; + azureKey?: string; + azureSmall?: string; + azureLarge?: string; + azureEmbeddingEndpoint?: string; + azureEmbeddingKey?: string; + azureEmbedding?: string; + + openaiApiKey?: string; + openaiBaseUrl?: string; + openaiSmall?: string; + openaiLarge?: string; + openaiEmbedding?: string; + + anthropicApiKey?: string; + anthropicSmall?: string; + anthropicLarge?: string; + + deepseekApiKey?: string; + deepseekSmall?: string; + deepseekLarge?: string; + + awsRegion?: string; + awsAccessKeyId?: string; + awsSecretAccessKey?: string; + awsSessionToken?: string; + bedrockSmall?: string; + bedrockLarge?: string; + bedrockEmbedding?: string; + + redisHost?: string; + redisPort?: number; + postgresHost?: string; + postgresPort?: number; + postgresUsername?: string; + postgresPassword?: string; + postgresSystemDb?: string; + postgresMaintenanceDb?: string; + neo4jHost?: string; + neo4jPort?: number; + neo4jUsername?: string; + neo4jPassword?: string; + milvusDeployment?: "local" | "managed"; + milvusHost?: string; + milvusPort?: number; + milvusUri?: string; + milvusToken?: string; + mongoHost?: string; + mongoPort?: number; + mongoUsername?: string; + mongoPassword?: string; + + brainpatToken?: string; + plugins?: PluginChoice[]; + noPlugins?: boolean; +} + +const VECTOR_DBS = new Set(["postgresql", "milvus"]); +const DATA_DBS = new Set(["postgresql", "mongo"]); +const GRAPH_DBS = new Set(["networkx", "neo4j"]); +const OCR_MODES = new Set(["docparser", "docling"]); +const RUNTIMES = new Set(["docker", "manual"]); +const MODELS_MODES = new Set(["local", "remote"]); +const LLM_PROVIDERS = new Set([ + "ollama", + "openai", + "anthropic", + "deepseek", + "azure", + "gcp_vertex", + "amazon_bedrock", +]); +const EMBEDDING_PROVIDERS = new Set>([ + "ollama", + "openai", + "azure", + "gcp_vertex", + "amazon_bedrock", +]); + +function splitFlag(arg: string): { name: string; inline?: string } { + const eq = arg.indexOf("="); + if (eq === -1) return { name: arg }; + return { name: arg.slice(0, eq), inline: arg.slice(eq + 1) }; +} + +function takeValue( + inline: string | undefined, + args: string[], + flag: string, +): { ok: true; value: string } | { ok: false; error: string } { + if (inline !== undefined) { + if (inline.length === 0) { + return { ok: false, error: `${flag} requires a value` }; + } + return { ok: true, value: inline }; + } + const next = args.shift(); + if (next === undefined || next.startsWith("-")) { + if (next !== undefined) args.unshift(next); + return { ok: false, error: `${flag} requires a value` }; + } + return { ok: true, value: next }; +} + +function parsePort( + raw: string, + flag: string, +): { ok: true; value: number } | { ok: false; error: string } { + const n = Number(raw); + if (!Number.isInteger(n) || n <= 0 || n > 65535) { + return { ok: false, error: `${flag} must be an integer port 1-65535` }; + } + return { ok: true, value: n }; +} + +function parsePositiveInt( + raw: string, + flag: string, +): { ok: true; value: number } | { ok: false; error: string } { + const n = Number(raw); + if (!Number.isInteger(n) || n <= 0) { + return { ok: false, error: `${flag} must be a positive integer` }; + } + return { ok: true, value: n }; +} + +function parsePluginSpec(raw: string): PluginChoice { + const at = raw.lastIndexOf("@"); + if (at > 0) { + return { + name: raw.slice(0, at), + source: "registry", + version: raw.slice(at + 1), + }; + } + return { name: raw, source: "registry" }; +} + +/** True when any wizard-config flag is present (skips “use defaults?”). */ +export function hasConfigOverrides(flags: InitFlagOverrides): boolean { + const { + defaults: _defaults, + startServices: _start, + ...rest + } = flags; + return Object.values(rest).some((v) => v !== undefined); +} + +export type ApplyInitFlagResult = + | { kind: "applied" } + | { kind: "unknown" } + | { kind: "error"; error: string }; + +/** Apply a single init flag. Mutates `flags` and may shift `args` for the value. */ +export function tryApplyInitFlag( + raw: string, + args: string[], + flags: InitFlagOverrides, +): ApplyInitFlagResult { + if (!raw.startsWith("-")) return { kind: "unknown" }; + + const { name, inline } = splitFlag(raw); + + const setString = (key: keyof InitFlagOverrides, flagName: string): ApplyInitFlagResult => { + const taken = takeValue(inline, args, flagName); + if (!taken.ok) return { kind: "error", error: taken.error }; + (flags as Record)[key] = taken.value; + return { kind: "applied" }; + }; + + const setPort = (key: keyof InitFlagOverrides, flagName: string): ApplyInitFlagResult => { + const taken = takeValue(inline, args, flagName); + if (!taken.ok) return { kind: "error", error: taken.error }; + const port = parsePort(taken.value, flagName); + if (!port.ok) return { kind: "error", error: port.error }; + (flags as Record)[key] = port.value; + return { kind: "applied" }; + }; + + switch (name) { + case "--defaults": + flags.defaults = true; + return { kind: "applied" }; + case "--vector-db": { + const taken = takeValue(inline, args, name); + if (!taken.ok) return { kind: "error", error: taken.error }; + const v = taken.value.toLowerCase() as VectorDb; + if (!VECTOR_DBS.has(v)) { + return { + kind: "error", + error: `Invalid --vector-db "${taken.value}". Use postgresql or milvus.`, + }; + } + flags.vectorDb = v; + return { kind: "applied" }; + } + case "--data-db": { + const taken = takeValue(inline, args, name); + if (!taken.ok) return { kind: "error", error: taken.error }; + const v = taken.value.toLowerCase() as DataDb; + if (!DATA_DBS.has(v)) { + return { + kind: "error", + error: `Invalid --data-db "${taken.value}". Use postgresql or mongo.`, + }; + } + flags.dataDb = v; + return { kind: "applied" }; + } + case "--graph-db": { + const taken = takeValue(inline, args, name); + if (!taken.ok) return { kind: "error", error: taken.error }; + const v = taken.value.toLowerCase() as GraphDb; + if (!GRAPH_DBS.has(v)) { + return { + kind: "error", + error: `Invalid --graph-db "${taken.value}". Use networkx or neo4j.`, + }; + } + flags.graphDb = v; + return { kind: "applied" }; + } + case "--ocr-mode": { + const taken = takeValue(inline, args, name); + if (!taken.ok) return { kind: "error", error: taken.error }; + const v = taken.value.toLowerCase() as OcrMode; + if (!OCR_MODES.has(v)) { + return { + kind: "error", + error: `Invalid --ocr-mode "${taken.value}". Use docparser or docling.`, + }; + } + flags.ocrMode = v; + return { kind: "applied" }; + } + case "--services-runtime": { + const taken = takeValue(inline, args, name); + if (!taken.ok) return { kind: "error", error: taken.error }; + const v = taken.value.toLowerCase() as ServicesRuntime; + if (!RUNTIMES.has(v)) { + return { + kind: "error", + error: `Invalid --services-runtime "${taken.value}". Use docker or manual.`, + }; + } + flags.servicesRuntime = v; + return { kind: "applied" }; + } + case "--start-services": + flags.startServices = true; + return { kind: "applied" }; + case "--no-start-services": + flags.startServices = false; + return { kind: "applied" }; + case "--models-mode": { + const taken = takeValue(inline, args, name); + if (!taken.ok) return { kind: "error", error: taken.error }; + const v = taken.value.toLowerCase() as ModelsMode; + if (!MODELS_MODES.has(v)) { + return { + kind: "error", + error: `Invalid --models-mode "${taken.value}". Use local or remote.`, + }; + } + flags.modelsMode = v; + return { kind: "applied" }; + } + case "--llm-small-provider": { + const taken = takeValue(inline, args, name); + if (!taken.ok) return { kind: "error", error: taken.error }; + const v = taken.value.toLowerCase() as ModelProvider; + if (!LLM_PROVIDERS.has(v)) { + return { + kind: "error", + error: `Invalid --llm-small-provider "${taken.value}".`, + }; + } + flags.llmSmallProvider = v; + return { kind: "applied" }; + } + case "--llm-large-provider": { + const taken = takeValue(inline, args, name); + if (!taken.ok) return { kind: "error", error: taken.error }; + const v = taken.value.toLowerCase() as ModelProvider; + if (!LLM_PROVIDERS.has(v)) { + return { + kind: "error", + error: `Invalid --llm-large-provider "${taken.value}".`, + }; + } + flags.llmLargeProvider = v; + return { kind: "applied" }; + } + case "--embeddings-provider": { + const taken = takeValue(inline, args, name); + if (!taken.ok) return { kind: "error", error: taken.error }; + const v = taken.value.toLowerCase() as Exclude< + ModelProvider, + "anthropic" | "deepseek" + >; + if (!EMBEDDING_PROVIDERS.has(v)) { + return { + kind: "error", + error: `Invalid --embeddings-provider "${taken.value}".`, + }; + } + flags.embeddingsProvider = v; + return { kind: "applied" }; + } + case "--llm-small": + return setString("llmSmall", name); + case "--llm-large": + return setString("llmLarge", name); + case "--embedding-model": + return setString("embeddingModel", name); + case "--embedding-dimensions": { + const taken = takeValue(inline, args, name); + if (!taken.ok) return { kind: "error", error: taken.error }; + const n = parsePositiveInt(taken.value, name); + if (!n.ok) return { kind: "error", error: n.error }; + flags.embeddingDimensions = n.value; + return { kind: "applied" }; + } + case "--ollama-host": + return setString("ollamaHost", name); + case "--ollama-port": + return setPort("ollamaPort", name); + case "--ollama-small": + return setString("ollamaSmall", name); + case "--ollama-large": + return setString("ollamaLarge", name); + case "--ollama-embeddings": + return setString("ollamaEmbeddings", name); + case "--gcp-credentials": + return setString("gcpCredentials", name); + case "--gcp-project": + return setString("gcpProject", name); + case "--gcp-small": + return setString("gcpSmall", name); + case "--gcp-large": + return setString("gcpLarge", name); + case "--gcp-embedding": + return setString("gcpEmbedding", name); + case "--azure-endpoint": + return setString("azureEndpoint", name); + case "--azure-api-version": + return setString("azureApiVersion", name); + case "--azure-key": + return setString("azureKey", name); + case "--azure-small": + return setString("azureSmall", name); + case "--azure-large": + return setString("azureLarge", name); + case "--azure-embedding-endpoint": + return setString("azureEmbeddingEndpoint", name); + case "--azure-embedding-key": + return setString("azureEmbeddingKey", name); + case "--azure-embedding": + return setString("azureEmbedding", name); + case "--openai-api-key": + return setString("openaiApiKey", name); + case "--openai-base-url": + return setString("openaiBaseUrl", name); + case "--openai-small": + return setString("openaiSmall", name); + case "--openai-large": + return setString("openaiLarge", name); + case "--openai-embedding": + return setString("openaiEmbedding", name); + case "--anthropic-api-key": + return setString("anthropicApiKey", name); + case "--anthropic-small": + return setString("anthropicSmall", name); + case "--anthropic-large": + return setString("anthropicLarge", name); + case "--deepseek-api-key": + return setString("deepseekApiKey", name); + case "--deepseek-small": + return setString("deepseekSmall", name); + case "--deepseek-large": + return setString("deepseekLarge", name); + case "--aws-region": + return setString("awsRegion", name); + case "--aws-access-key-id": + return setString("awsAccessKeyId", name); + case "--aws-secret-access-key": + return setString("awsSecretAccessKey", name); + case "--aws-session-token": + return setString("awsSessionToken", name); + case "--bedrock-small": + return setString("bedrockSmall", name); + case "--bedrock-large": + return setString("bedrockLarge", name); + case "--bedrock-embedding": + return setString("bedrockEmbedding", name); + case "--redis-host": + return setString("redisHost", name); + case "--redis-port": + return setPort("redisPort", name); + case "--postgres-host": + return setString("postgresHost", name); + case "--postgres-port": + return setPort("postgresPort", name); + case "--postgres-username": + return setString("postgresUsername", name); + case "--postgres-password": + return setString("postgresPassword", name); + case "--postgres-system-db": + return setString("postgresSystemDb", name); + case "--postgres-maintenance-db": + return setString("postgresMaintenanceDb", name); + case "--neo4j-host": + return setString("neo4jHost", name); + case "--neo4j-port": + return setPort("neo4jPort", name); + case "--neo4j-username": + return setString("neo4jUsername", name); + case "--neo4j-password": + return setString("neo4jPassword", name); + case "--milvus-deployment": { + const taken = takeValue(inline, args, name); + if (!taken.ok) return { kind: "error", error: taken.error }; + const v = taken.value.toLowerCase(); + if (v !== "local" && v !== "managed") { + return { + kind: "error", + error: `Invalid --milvus-deployment "${taken.value}". Use local or managed.`, + }; + } + flags.milvusDeployment = v; + return { kind: "applied" }; + } + case "--milvus-host": + return setString("milvusHost", name); + case "--milvus-port": + return setPort("milvusPort", name); + case "--milvus-uri": + return setString("milvusUri", name); + case "--milvus-token": + return setString("milvusToken", name); + case "--mongo-host": + return setString("mongoHost", name); + case "--mongo-port": + return setPort("mongoPort", name); + case "--mongo-username": + return setString("mongoUsername", name); + case "--mongo-password": + return setString("mongoPassword", name); + case "--brainpat-token": + return setString("brainpatToken", name); + case "--plugin": { + const taken = takeValue(inline, args, name); + if (!taken.ok) return { kind: "error", error: taken.error }; + if (!flags.plugins) flags.plugins = []; + flags.plugins.push(parsePluginSpec(taken.value)); + return { kind: "applied" }; + } + case "--no-plugins": + flags.noPlugins = true; + return { kind: "applied" }; + default: + return { kind: "unknown" }; + } +} diff --git a/tui/src/lib/native-install.ts b/tui/src/lib/native-install.ts new file mode 100644 index 0000000000000000000000000000000000000000..e70483e80df4bf2542d9289e4c247601c953e0e6 --- /dev/null +++ b/tui/src/lib/native-install.ts @@ -0,0 +1,279 @@ +import type { ServiceName } from "../constants.js"; +import type { PlatformCapabilities, LinuxPackageManager } from "./platform.js"; + +export interface NativeRecipe { + install: string[]; + start: string[]; + postInstall?: string[]; + docsUrl?: string; + unsupported?: string; +} + +function brewRecipe(service: ServiceName): NativeRecipe | undefined { + switch (service) { + case "redis": + return { + install: ["brew install redis"], + start: ["brew services start redis"], + docsUrl: "https://redis.io/docs/install/install-redis/install-redis-on-mac-os/", + }; + case "postgresql": + return { + install: ["brew install postgresql@16", "brew install pgvector"], + start: ["brew services start postgresql@16"], + postInstall: [ + "createdb \"$USER\" 2>/dev/null || true", + "psql -d postgres -c \"ALTER USER \\\"$USER\\\" WITH SUPERUSER;\"", + "echo \"Set POSTGRES_USERNAME / POSTGRES_PASSWORD in .env to match this role.\"", + ], + docsUrl: "https://github.com/pgvector/pgvector#installation", + }; + case "neo4j": + return { + install: ["brew install neo4j"], + start: ["brew services start neo4j"], + postInstall: [ + "# Default credentials are neo4j/neo4j — you'll be prompted to set a new password", + "# at first login: http://localhost:7474", + ], + docsUrl: "https://neo4j.com/docs/operations-manual/current/installation/osx/", + }; + case "mongo": + return { + install: [ + "brew tap mongodb/brew", + "brew install mongodb-community", + ], + start: ["brew services start mongodb/brew/mongodb-community"], + docsUrl: "https://www.mongodb.com/docs/manual/tutorial/install-mongodb-on-os-x/", + }; + case "rabbitmq": + return { + install: ["brew install rabbitmq"], + start: ["brew services start rabbitmq"], + postInstall: [ + "# Default guest user only works on localhost; the BrainAPI broker URL", + "# expects kalo/kalo — create it with:", + "rabbitmqctl add_user kalo kalo", + "rabbitmqctl set_user_tags kalo administrator", + "rabbitmqctl set_permissions -p / kalo \".*\" \".*\" \".*\"", + ], + docsUrl: "https://www.rabbitmq.com/install-homebrew.html", + }; + case "milvus": + return undefined; + default: + return undefined; + } +} + +function aptRecipe(service: ServiceName): NativeRecipe | undefined { + switch (service) { + case "redis": + return { + install: ["sudo apt-get update", "sudo apt-get install -y redis-server"], + start: ["sudo systemctl enable --now redis-server"], + }; + case "postgresql": + return { + install: [ + "sudo apt-get update", + "sudo apt-get install -y postgresql-16 postgresql-16-pgvector", + ], + start: ["sudo systemctl enable --now postgresql"], + postInstall: [ + "sudo -u postgres createuser --superuser \"$USER\"", + "sudo -u postgres createdb \"$USER\"", + "# Then update POSTGRES_USERNAME / POSTGRES_PASSWORD in .env", + ], + docsUrl: "https://www.postgresql.org/download/linux/ubuntu/", + }; + case "neo4j": + return { + install: [ + "wget -O - https://debian.neo4j.com/neotechnology.gpg.key | sudo gpg --dearmor -o /etc/apt/keyrings/neotechnology.gpg", + "echo 'deb [signed-by=/etc/apt/keyrings/neotechnology.gpg] https://debian.neo4j.com stable latest' | sudo tee /etc/apt/sources.list.d/neo4j.list", + "sudo apt-get update && sudo apt-get install -y neo4j", + ], + start: ["sudo systemctl enable --now neo4j"], + docsUrl: "https://neo4j.com/docs/operations-manual/current/installation/linux/debian/", + }; + case "mongo": + return { + install: [ + "curl -fsSL https://www.mongodb.org/static/pgp/server-7.0.asc | sudo gpg -o /usr/share/keyrings/mongodb-server-7.0.gpg --dearmor", + "echo 'deb [arch=amd64,arm64 signed-by=/usr/share/keyrings/mongodb-server-7.0.gpg] https://repo.mongodb.org/apt/ubuntu jammy/mongodb-org/7.0 multiverse' | sudo tee /etc/apt/sources.list.d/mongodb-org-7.0.list", + "sudo apt-get update && sudo apt-get install -y mongodb-org", + ], + start: ["sudo systemctl enable --now mongod"], + docsUrl: "https://www.mongodb.com/docs/manual/tutorial/install-mongodb-on-ubuntu/", + }; + case "rabbitmq": + return { + install: ["sudo apt-get update", "sudo apt-get install -y rabbitmq-server"], + start: ["sudo systemctl enable --now rabbitmq-server"], + postInstall: [ + "sudo rabbitmqctl add_user kalo kalo", + "sudo rabbitmqctl set_user_tags kalo administrator", + "sudo rabbitmqctl set_permissions -p / kalo \".*\" \".*\" \".*\"", + ], + }; + case "milvus": + return undefined; + default: + return undefined; + } +} + +function dnfRecipe(service: ServiceName): NativeRecipe | undefined { + switch (service) { + case "redis": + return { + install: ["sudo dnf install -y redis"], + start: ["sudo systemctl enable --now redis"], + }; + case "postgresql": + return { + install: [ + "sudo dnf install -y postgresql-server postgresql-contrib pgvector", + "sudo /usr/bin/postgresql-setup --initdb", + ], + start: ["sudo systemctl enable --now postgresql"], + docsUrl: "https://www.postgresql.org/download/linux/redhat/", + }; + case "neo4j": + return { + install: [ + "sudo rpm --import https://debian.neo4j.com/neotechnology.gpg.key", + "sudo dnf install -y neo4j", + ], + start: ["sudo systemctl enable --now neo4j"], + }; + case "mongo": + return { + install: [ + "# Add MongoDB official repo first — see docs", + "sudo dnf install -y mongodb-org", + ], + start: ["sudo systemctl enable --now mongod"], + docsUrl: "https://www.mongodb.com/docs/manual/tutorial/install-mongodb-on-red-hat/", + }; + case "rabbitmq": + return { + install: ["sudo dnf install -y rabbitmq-server"], + start: ["sudo systemctl enable --now rabbitmq-server"], + postInstall: [ + "sudo rabbitmqctl add_user kalo kalo", + "sudo rabbitmqctl set_user_tags kalo administrator", + "sudo rabbitmqctl set_permissions -p / kalo \".*\" \".*\" \".*\"", + ], + }; + case "milvus": + return undefined; + default: + return undefined; + } +} + +function genericLinuxRecipe( + service: ServiceName, + pm: LinuxPackageManager, +): NativeRecipe | undefined { + if (pm === "apt-get") return aptRecipe(service); + if (pm === "dnf") return dnfRecipe(service); + if (pm === "pacman") { + switch (service) { + case "redis": + return { + install: ["sudo pacman -S --noconfirm redis"], + start: ["sudo systemctl enable --now redis"], + }; + case "postgresql": + return { + install: [ + "sudo pacman -S --noconfirm postgresql", + "# pgvector is available from the AUR: paru -S pgvector", + ], + start: [ + "sudo -iu postgres initdb -D /var/lib/postgres/data", + "sudo systemctl enable --now postgresql", + ], + }; + default: + return undefined; + } + } + return undefined; +} + +function windowsRecipe(service: ServiceName): NativeRecipe | undefined { + switch (service) { + case "redis": + return { + install: [ + "# Native Redis on Windows is no longer maintained.", + "# Use Memurai (commercial), WSL, or Docker instead.", + ], + start: [], + unsupported: "no first-party Windows build", + docsUrl: "https://www.memurai.com/get-memurai", + }; + case "postgresql": + return { + install: ["winget install PostgreSQL.PostgreSQL"], + start: ["# postgresql-x64 service is auto-started by the installer"], + docsUrl: "https://www.postgresql.org/download/windows/", + }; + case "neo4j": + return { + install: ["winget install Neo4j.Neo4jDesktop"], + start: [], + docsUrl: "https://neo4j.com/download/", + }; + case "mongo": + return { + install: ["winget install MongoDB.Server"], + start: ["# MongoDB service is auto-started by the installer"], + }; + case "rabbitmq": + return { + install: ["winget install RabbitMQ.RabbitMQ"], + start: ["# RabbitMQ service is auto-started by the installer"], + docsUrl: "https://www.rabbitmq.com/install-windows.html", + }; + case "milvus": + return undefined; + default: + return undefined; + } +} + +export function nativeRecipeFor( + service: ServiceName, + caps: PlatformCapabilities, +): NativeRecipe | undefined { + if (service === "milvus") { + return { + install: [], + start: [], + unsupported: + "Milvus has no native install path — use Docker or Zilliz Cloud (managed).", + docsUrl: "https://milvus.io/docs/install_standalone-docker.md", + }; + } + if (caps.platform === "darwin" && caps.hasBrew) { + const recipe = brewRecipe(service); + if (recipe) return recipe; + } + if (caps.platform === "linux") { + for (const pm of caps.linuxPackageManagers) { + const recipe = genericLinuxRecipe(service, pm); + if (recipe) return recipe; + } + } + if (caps.platform === "win32") { + const recipe = windowsRecipe(service); + if (recipe) return recipe; + } + return undefined; +} diff --git a/tui/src/lib/net.ts b/tui/src/lib/net.ts new file mode 100644 index 0000000000000000000000000000000000000000..ad7dd669375660b61ec190d89a09f2f94d80ea84 --- /dev/null +++ b/tui/src/lib/net.ts @@ -0,0 +1,55 @@ +import net from "node:net"; + +export interface TcpProbeOptions { + host: string; + port: number; + timeoutMs?: number; +} + +export async function tcpProbe({ + host, + port, + timeoutMs = 1500, +}: TcpProbeOptions): Promise { + return new Promise((resolve) => { + const socket = new net.Socket(); + let settled = false; + const finish = (ok: boolean) => { + if (settled) return; + settled = true; + socket.destroy(); + resolve(ok); + }; + socket.setTimeout(timeoutMs); + socket.once("connect", () => finish(true)); + socket.once("timeout", () => finish(false)); + socket.once("error", () => finish(false)); + socket.connect(port, host); + }); +} + +export interface HttpProbeOptions { + url: string; + timeoutMs?: number; + expectStatus?: number; +} + +export async function httpProbe({ + url, + timeoutMs = 2000, + expectStatus, +}: HttpProbeOptions): Promise<{ ok: boolean; status?: number; body?: string }> { + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), timeoutMs); + try { + const response = await fetch(url, { signal: controller.signal }); + const body = await response.text(); + const status = response.status; + const matches = expectStatus !== undefined ? status === expectStatus : status < 500; + return { ok: matches, status, body }; + } catch { + return { ok: false }; + } finally { + clearTimeout(timer); + } +} diff --git a/tui/src/lib/ollama.ts b/tui/src/lib/ollama.ts new file mode 100644 index 0000000000000000000000000000000000000000..3e8235795a44754ff30da72aad871964746658c0 --- /dev/null +++ b/tui/src/lib/ollama.ts @@ -0,0 +1,67 @@ +import { httpProbe } from "./net.js"; +import { runInherit, which } from "./exec.js"; + +export interface OllamaTarget { + host: string; + port: number; +} + +export interface OllamaPullResult { + ok: boolean; + exitCode: number; + error?: string; +} + +export async function pullOllamaModel(modelName: string): Promise { + const bin = (await which("ollama")) ? "ollama" : null; + if (!bin) { + return { + ok: false, + exitCode: 127, + error: "ollama CLI not found on PATH", + }; + } + const result = await runInherit(bin, ["pull", modelName]); + if (result.ok) { + return { ok: true, exitCode: result.exitCode }; + } + return { + ok: false, + exitCode: result.exitCode, + error: result.stderr || `ollama pull exited with code ${result.exitCode}`, + }; +} + +export async function isOllamaRunning(target: OllamaTarget): Promise { + const result = await httpProbe({ + url: `http://${target.host}:${target.port}/api/tags`, + expectStatus: 200, + }); + return result.ok; +} + +export async function listOllamaModels(target: OllamaTarget): Promise { + const result = await httpProbe({ + url: `http://${target.host}:${target.port}/api/tags`, + expectStatus: 200, + }); + if (!result.ok || !result.body) return []; + try { + const json = JSON.parse(result.body) as { models?: Array<{ name?: string }> }; + return (json.models ?? []) + .map((m) => m.name?.toString() ?? "") + .filter((name) => name.length > 0); + } catch { + return []; + } +} + +export async function hasOllamaModel( + target: OllamaTarget, + modelName: string, +): Promise { + const models = await listOllamaModels(target); + if (models.includes(modelName)) return true; + const base = modelName.split(":")[0]; + return base !== undefined && models.some((m) => m.split(":")[0] === base); +} diff --git a/tui/src/lib/parallel.ts b/tui/src/lib/parallel.ts new file mode 100644 index 0000000000000000000000000000000000000000..8b4bbc8e4cadc8a4f1eb23bf6b3987d64d8e5700 --- /dev/null +++ b/tui/src/lib/parallel.ts @@ -0,0 +1,103 @@ +import { spawn, type ChildProcess } from "node:child_process"; +import pc from "picocolors"; + +export interface ParallelJob { + name: string; + bin: string; + args: string[]; + cwd?: string; + env?: NodeJS.ProcessEnv; + color?: (s: string) => string; +} + +export interface ParallelResult { + name: string; + code: number | null; + signal: NodeJS.Signals | null; +} + +const COLORS: Array<(s: string) => string> = [ + pc.cyan, + pc.magenta, + pc.yellow, + pc.green, + pc.blue, + pc.red, +]; + +function makePrefixer(prefix: string): (chunk: Buffer | string) => string { + let pending = ""; + return (chunk: Buffer | string): string => { + const text = pending + (typeof chunk === "string" ? chunk : chunk.toString("utf8")); + const lines = text.split(/\r?\n/); + pending = lines.pop() ?? ""; + if (lines.length === 0) return ""; + return lines.map((line) => `${prefix} ${line}`).join("\n") + "\n"; + }; +} + +export async function runParallel(jobs: ParallelJob[]): Promise { + if (jobs.length === 0) return []; + + const labelWidth = Math.max(...jobs.map((j) => j.name.length)); + const children: Array<{ name: string; child: ChildProcess }> = []; + + const cleanup = (signal: NodeJS.Signals): void => { + for (const { child } of children) { + if (!child.killed) { + try { + child.kill(signal); + } catch { + } + } + } + }; + + const onSigint = (): void => cleanup("SIGINT"); + const onSigterm = (): void => cleanup("SIGTERM"); + process.on("SIGINT", onSigint); + process.on("SIGTERM", onSigterm); + + try { + const results = await Promise.all( + jobs.map((job, idx) => { + const color = job.color ?? COLORS[idx % COLORS.length]!; + const label = job.name.padEnd(labelWidth); + const prefix = color(`[${label}]`); + + const child = spawn(job.bin, job.args, { + cwd: job.cwd, + env: { ...process.env, ...job.env }, + stdio: ["ignore", "pipe", "pipe"], + }); + children.push({ name: job.name, child }); + + const outPrefixer = makePrefixer(prefix); + const errPrefixer = makePrefixer(prefix); + child.stdout?.on("data", (chunk) => { + const out = outPrefixer(chunk); + if (out) process.stdout.write(out); + }); + child.stderr?.on("data", (chunk) => { + const out = errPrefixer(chunk); + if (out) process.stderr.write(out); + }); + + return new Promise((resolve) => { + child.on("exit", (code, signal) => { + resolve({ name: job.name, code, signal }); + }); + child.on("error", (err) => { + process.stderr.write(`${prefix} failed to spawn: ${err.message}\n`); + resolve({ name: job.name, code: 127, signal: null }); + }); + }); + }), + ); + + return results; + } finally { + process.off("SIGINT", onSigint); + process.off("SIGTERM", onSigterm); + } +} diff --git a/tui/src/lib/paths.ts b/tui/src/lib/paths.ts new file mode 100644 index 0000000000000000000000000000000000000000..c82b0b24ea5fc130e3e9be98fc3db207ae466fc6 --- /dev/null +++ b/tui/src/lib/paths.ts @@ -0,0 +1,38 @@ +import { homedir, platform as osPlatform } from "node:os"; +import path from "node:path"; +import type { Platform } from "../types.js"; + +export function brainapiHome(): string { + return process.env.BRAINAPI_HOME ?? path.join(homedir(), ".brainapi"); +} + +export function sourcePath(): string { + return path.join(brainapiHome(), "source"); +} + +export function venvPath(): string { + return path.join(sourcePath(), ".venv"); +} + +export function envFilePath(): string { + return path.join(sourcePath(), ".env"); +} + +export function stateFilePath(): string { + return path.join(brainapiHome(), "state.json"); +} + +export function venvBin(name: string): string { + if (currentPlatform() === "win32") { + return path.join(venvPath(), "Scripts", `${name}.exe`); + } + return path.join(venvPath(), "bin", name); +} + +export function currentPlatform(): Platform { + const p = osPlatform(); + if (p === "darwin" || p === "linux" || p === "win32") { + return p; + } + return "linux"; +} diff --git a/tui/src/lib/platform.ts b/tui/src/lib/platform.ts new file mode 100644 index 0000000000000000000000000000000000000000..c82746e5c2820558374839a78fc0c040de78fda5 --- /dev/null +++ b/tui/src/lib/platform.ts @@ -0,0 +1,46 @@ +import { currentPlatform } from "./paths.js"; +import { which } from "./exec.js"; +import type { Platform } from "../types.js"; + +export type LinuxPackageManager = "apt-get" | "dnf" | "pacman" | "apk" | "zypper"; + +export interface PlatformCapabilities { + platform: Platform; + hasBrew: boolean; + hasPyenv: boolean; + hasWinget: boolean; + hasChoco: boolean; + linuxPackageManagers: LinuxPackageManager[]; +} + +const LINUX_PMS: LinuxPackageManager[] = [ + "apt-get", + "dnf", + "pacman", + "apk", + "zypper", +]; + +export async function detectCapabilities(): Promise { + const platform = currentPlatform(); + const [hasBrew, hasPyenv, hasWinget, hasChoco] = await Promise.all([ + platform === "darwin" || platform === "linux" ? which("brew") : Promise.resolve(false), + which("pyenv"), + platform === "win32" ? which("winget") : Promise.resolve(false), + platform === "win32" ? which("choco") : Promise.resolve(false), + ]); + const linuxPackageManagers: LinuxPackageManager[] = []; + if (platform === "linux") { + for (const pm of LINUX_PMS) { + if (await which(pm)) linuxPackageManagers.push(pm); + } + } + return { + platform, + hasBrew, + hasPyenv, + hasWinget, + hasChoco, + linuxPackageManagers, + }; +} diff --git a/tui/src/lib/plugins.ts b/tui/src/lib/plugins.ts new file mode 100644 index 0000000000000000000000000000000000000000..f67ac4f1c0c3b8cd5cf1b79ef22689b8a14ef992 --- /dev/null +++ b/tui/src/lib/plugins.ts @@ -0,0 +1,180 @@ +import { access, cp } from "node:fs/promises"; +import path from "node:path"; +import { runInherit, runQuiet } from "./exec.js"; +import { sourcePath, venvBin } from "./paths.js"; +import type { PluginSource } from "../types.js"; + +export interface PluginSearchResult { + name: string; + version: string; + description: string; + source: PluginSource; + path?: string; +} + +interface LocalPluginManifest { + name: string; + version: string; + path: string; + description: string; +} + +function pluginScript(script: string): string[] { + return ["-c", script]; +} + +async function runPluginJson(script: string): Promise { + const result = await runQuiet(venvBin("python"), pluginScript(script), { + cwd: sourcePath(), + }); + if (!result.ok) { + const details = result.stderr.trim() || result.stdout.trim() || "unknown error"; + throw new Error(details); + } + const text = result.stdout.trim(); + if (!text) { + throw new Error("Plugin command returned empty output"); + } + return JSON.parse(text) as T; +} + +export async function listInstalledPlugins(): Promise { + const script = [ + "import json", + "from src.core.plugins.manager import PluginManager", + "manager = PluginManager(plugins_dir=__import__('pathlib').Path('plugins'), registry_url=None)", + "plugins = manager.list_installed()", + "print(json.dumps([{'name': p.name, 'version': p.version, 'description': p.description or ''} for p in plugins]))", + ].join(";"); + const installed = await runPluginJson>(script); + return installed.map((plugin) => ({ ...plugin, source: "local" })); +} + +async function listRegistryPlugins(): Promise { + const script = [ + "import json", + "from src.core.plugins.manager import PluginManager", + "registry_url = __import__('os').getenv('PLUGIN_REGISTRY_URL', 'https://registry.brain-api.dev')", + "manager = PluginManager(plugins_dir=__import__('pathlib').Path('plugins'), registry_url=registry_url)", + "plugins = manager.list_available()", + "print(json.dumps([{'name': p.name, 'version': p.version, 'description': p.description or ''} for p in plugins]))", + ].join(";"); + const available = await runPluginJson>(script); + return available.map((plugin) => ({ ...plugin, source: "registry" })); +} + +async function listWorkspacePlugins(): Promise { + const script = [ + "import json", + "from pathlib import Path", + "from src.core.plugins.manifest import MANIFEST_FILENAME, parse_manifest", + "base = Path.cwd() / 'plugins'", + "results = []", + "if base.exists():", + " for child in sorted(base.iterdir()):", + " manifest_path = child / MANIFEST_FILENAME", + " if not manifest_path.exists():", + " continue", + " try:", + " m = parse_manifest(manifest_path)", + " except Exception:", + " continue", + " results.append({'name': m.name, 'version': m.version, 'description': m.description or '', 'path': str(child.resolve())})", + "print(json.dumps(results))", + ].join("\n"); + return runPluginJson(script); +} + +export async function searchPlugins(query: string): Promise { + const normalized = (query ?? "").trim().toLowerCase(); + if (!normalized) return []; + const [localInstalled, localWorkspace, registry] = await Promise.all([ + listInstalledPlugins().catch(() => []), + listWorkspacePlugins().catch(() => []), + listRegistryPlugins().catch(() => []), + ]); + const workspaceCandidates: PluginSearchResult[] = localWorkspace.map((plugin) => ({ + name: plugin.name, + version: plugin.version, + description: plugin.description ?? "", + source: "local", + path: plugin.path, + })); + const merged = [...localInstalled, ...workspaceCandidates, ...registry]; + const dedup = new Map(); + for (const candidate of merged) { + if (!candidate?.name) continue; + const key = `${candidate.source}:${candidate.name}:${candidate.path ?? ""}`; + if (!dedup.has(key)) { + dedup.set(key, { + ...candidate, + description: candidate.description ?? "", + version: candidate.version ?? "0.0.0", + }); + } + } + return [...dedup.values()].filter((plugin) => + plugin.name.toLowerCase().includes(normalized) || + plugin.description.toLowerCase().includes(normalized), + ); +} + +export async function runPluginCli(args: string[]): Promise { + const result = await runInherit( + venvBin("python"), + ["-m", "src.core.plugins.cli", "plugins", ...args], + { cwd: sourcePath() }, + ); + if (!result.ok) { + throw new Error("Plugin command failed"); + } +} + +export async function installRegistryPlugin(name: string, version?: string): Promise { + const args = ["install", name]; + if (version && version.trim()) { + args.push("--version", version.trim()); + } + try { + await runPluginCli(args); + } catch { + throw new Error(`Failed to install registry plugin '${name}'`); + } +} + +export async function uninstallPlugin(name: string): Promise { + try { + await runPluginCli(["uninstall", name]); + } catch { + throw new Error(`Failed to uninstall plugin '${name}'`); + } +} + +async function resolveLocalPluginName(pluginPath: string): Promise { + const quotedPath = JSON.stringify(pluginPath); + const script = [ + "from pathlib import Path", + "from src.core.plugins.manifest import MANIFEST_FILENAME, parse_manifest", + `p = Path(${quotedPath})`, + "manifest = p / MANIFEST_FILENAME", + "if not manifest.exists():", + " raise SystemExit('plugin.yaml not found')", + "parsed = parse_manifest(manifest)", + "print(parsed.name)", + ].join("\n"); + const result = await runQuiet(venvBin("python"), pluginScript(script), { cwd: sourcePath() }); + if (!result.ok) { + throw new Error(result.stderr.trim() || result.stdout.trim() || "Invalid local plugin"); + } + const name = result.stdout.trim(); + if (!name) throw new Error("Invalid local plugin manifest"); + return name; +} + +export async function installLocalPlugin(pluginPath: string): Promise { + await access(pluginPath); + const name = await resolveLocalPluginName(pluginPath); + const targetPath = path.join(sourcePath(), "plugins", name); + await cp(pluginPath, targetPath, { recursive: true, force: true }); + return name; +} diff --git a/tui/src/lib/prompts.ts b/tui/src/lib/prompts.ts new file mode 100644 index 0000000000000000000000000000000000000000..8cfbaff20fc037927d887277d2ae493990898ec5 --- /dev/null +++ b/tui/src/lib/prompts.ts @@ -0,0 +1,127 @@ +import * as p from "@clack/prompts"; + +export const PROMPT_BACK = "__back__" as const; +export type PromptBack = typeof PROMPT_BACK; + +export function isPromptBack(value: unknown): value is PromptBack { + return value === PROMPT_BACK; +} + +export interface PickOption { + value: T; + label: string; + hint?: string; +} + +type PickOneBase = { + message: string; + options: PickOption[]; + initialValue?: T; +}; + +export async function pickOne( + opts: PickOneBase & { allowBack?: false }, +): Promise; +export async function pickOne( + opts: PickOneBase & { + allowBack: true; + backLabel?: string; + backHint?: string; + }, +): Promise; +export async function pickOne( + opts: PickOneBase & { + allowBack?: boolean; + backLabel?: string; + backHint?: string; + }, +): Promise { + const options = opts.allowBack + ? [ + { + value: PROMPT_BACK as T, + label: opts.backLabel ?? "Back", + hint: opts.backHint, + }, + ...opts.options, + ] + : opts.options; + const result = await (p.select as unknown as (o: { + message: string; + options: PickOption[]; + initialValue?: T; + }) => Promise)({ + message: opts.message, + options, + initialValue: opts.initialValue, + }); + if (p.isCancel(result)) { + p.cancel("Setup cancelled."); + process.exit(1); + } + if (isPromptBack(result)) { + return PROMPT_BACK; + } + return result as T; +} + +export async function askConfirmOrBack(opts: { + message: string; + initialValue?: boolean; + backHint?: string; +}): Promise { + const initial = opts.initialValue === false ? "no" : "yes"; + const choice = await pickOne<"yes" | "no">({ + message: opts.message, + allowBack: true, + backHint: opts.backHint, + initialValue: initial, + options: [ + { value: "yes", label: "Yes" }, + { value: "no", label: "No" }, + ], + }); + if (isPromptBack(choice)) { + return PROMPT_BACK; + } + return choice === "yes"; +} + +export async function askConfirm(opts: { + message: string; + initialValue?: boolean; +}): Promise { + const result = await p.confirm(opts); + if (p.isCancel(result)) { + p.cancel("Setup cancelled."); + process.exit(1); + } + return result as boolean; +} + +export async function askText(opts: { + message: string; + placeholder?: string; + defaultValue?: string; + initialValue?: string; + validate?: (value: string) => string | undefined; +}): Promise { + const result = await p.text(opts); + if (p.isCancel(result)) { + p.cancel("Setup cancelled."); + process.exit(1); + } + return typeof result === "string" ? result : ""; +} + +export async function askPassword(opts: { + message: string; + validate?: (value: string) => string | undefined; +}): Promise { + const result = await p.password(opts); + if (p.isCancel(result)) { + p.cancel("Setup cancelled."); + process.exit(1); + } + return result as string; +} diff --git a/tui/src/lib/python-recovery.ts b/tui/src/lib/python-recovery.ts new file mode 100644 index 0000000000000000000000000000000000000000..a1dc2688974bf547fa3900c7ee9afbd827aed384 --- /dev/null +++ b/tui/src/lib/python-recovery.ts @@ -0,0 +1,215 @@ +import * as p from "@clack/prompts"; +import pc from "picocolors"; +import { runInherit } from "./exec.js"; +import { detectCapabilities } from "./platform.js"; +import { detectPython, type DetectedPython } from "./python.js"; +import { askConfirm, pickOne } from "./prompts.js"; + +interface PythonOption { + value: string; + label: string; + command?: { bin: string; args: string[] }; + manualInstructions?: string; + openUrl?: string; +} + +const MAX_RETRIES = 3; + +async function openUrl(url: string): Promise { + const cmd = process.platform === "darwin" + ? { bin: "open", args: [url] } + : process.platform === "win32" + ? { bin: "cmd", args: ["/c", "start", "", url] } + : { bin: "xdg-open", args: [url] }; + await runInherit(cmd.bin, cmd.args); +} + +async function buildOptions(): Promise { + const caps = await detectCapabilities(); + const options: PythonOption[] = []; + + if (caps.platform === "darwin") { + if (caps.hasBrew) { + options.push({ + value: "brew", + label: "Install via Homebrew (brew install python@3.12)", + command: { bin: "brew", args: ["install", "python@3.12"] }, + }); + } + options.push({ + value: "pyenv-mac", + label: caps.hasPyenv + ? "Install via pyenv (pyenv install 3.12)" + : "Install pyenv + Python 3.12 (recommended for multiple versions)", + manualInstructions: caps.hasPyenv + ? "pyenv install 3.12.7\npyenv global 3.12.7" + : "brew install pyenv\npyenv install 3.12.7\npyenv global 3.12.7", + }); + options.push({ + value: "python-org-mac", + label: "Open python.org downloads in browser", + openUrl: "https://www.python.org/downloads/macos/", + }); + } else if (caps.platform === "linux") { + for (const pm of caps.linuxPackageManagers) { + const installArgs = pm === "pacman" + ? ["sudo", pm, "-S", "--noconfirm", "python"] + : pm === "apk" + ? ["sudo", pm, "add", "python3", "py3-pip"] + : ["sudo", pm, "install", "-y", "python3.12", "python3.12-venv", "python3-pip"]; + options.push({ + value: `pm-${pm}`, + label: `Install via ${pm} (${installArgs.slice(1).join(" ")})`, + command: { bin: installArgs[0]!, args: installArgs.slice(1) }, + }); + } + if (caps.hasPyenv) { + options.push({ + value: "pyenv-linux", + label: "Install via pyenv (pyenv install 3.12)", + manualInstructions: "pyenv install 3.12.7\npyenv global 3.12.7", + }); + } + options.push({ + value: "python-org-linux", + label: "Open python.org downloads in browser", + openUrl: "https://www.python.org/downloads/source/", + }); + } else if (caps.platform === "win32") { + if (caps.hasWinget) { + options.push({ + value: "winget", + label: "Install via winget (winget install Python.Python.3.12)", + command: { bin: "winget", args: ["install", "Python.Python.3.12"] }, + }); + } + if (caps.hasChoco) { + options.push({ + value: "choco", + label: "Install via Chocolatey (choco install python --version=3.12)", + command: { bin: "choco", args: ["install", "python", "--version=3.12"] }, + }); + } + options.push({ + value: "python-org-win", + label: "Open python.org downloads in browser", + openUrl: "https://www.python.org/downloads/windows/", + }); + } + + options.push({ + value: "manual", + label: "I'll install it manually — wait and retry", + }); + options.push({ + value: "cancel", + label: "Cancel — I'll re-run `brainapi init` later", + }); + + return options; +} + +function manualInstructionsFor(): string { + if (process.platform === "darwin") { + return [ + "# Option A: Homebrew", + "brew install python@3.12", + "", + "# Option B: pyenv (recommended if you juggle Python versions)", + "brew install pyenv", + "pyenv install 3.12.7", + "pyenv global 3.12.7", + "", + "# Option C: Download installer", + "open https://www.python.org/downloads/macos/", + ].join("\n"); + } + if (process.platform === "linux") { + return [ + "# Debian/Ubuntu", + "sudo apt-get update && sudo apt-get install -y python3.12 python3.12-venv python3-pip", + "", + "# Fedora/RHEL", + "sudo dnf install -y python3.12", + "", + "# Arch", + "sudo pacman -S python", + "", + "# Or via pyenv", + "curl https://pyenv.run | bash", + "pyenv install 3.12.7 && pyenv global 3.12.7", + ].join("\n"); + } + return [ + "winget install Python.Python.3.12", + "", + "# Or download:", + "https://www.python.org/downloads/windows/", + ].join("\n"); +} + +export async function recoverPython(): Promise { + let attempt = 0; + while (attempt < MAX_RETRIES) { + attempt += 1; + p.log.warn( + attempt === 1 + ? "Python 3.11+ was not found on your PATH." + : `Still no Python 3.11+ detected (attempt ${attempt}/${MAX_RETRIES}).`, + ); + const options = await buildOptions(); + const choice = await pickOne({ + message: "How would you like to install Python?", + options: options.map((o) => ({ value: o.value, label: o.label })), + }); + const selected = options.find((o) => o.value === choice); + if (!selected) continue; + + if (selected.value === "cancel") { + p.cancel("Setup cancelled. Re-run `brainapi init` once Python is installed."); + process.exit(1); + } + + if (selected.openUrl) { + await openUrl(selected.openUrl); + } else if (selected.command) { + const cmdStr = `${selected.command.bin} ${selected.command.args.join(" ")}`; + p.note(cmdStr, "Command to run"); + const confirmRun = await askConfirm({ + message: "Run this for me now?", + initialValue: true, + }); + if (confirmRun) { + await runInherit(selected.command.bin, selected.command.args); + } + } else if (selected.value === "manual") { + p.note(manualInstructionsFor(), "Manual install — copy and run"); + } else if (selected.manualInstructions) { + p.note(selected.manualInstructions, "Copy and run"); + } + + await askConfirm({ + message: "Press enter when Python is installed and on your PATH", + initialValue: true, + }); + + const detected = await detectPython(); + if (detected) { + p.log.success( + `Found ${pc.cyan(detected.bin)} (Python ${detected.version.join(".")})`, + ); + return detected; + } + } + + p.cancel( + "Could not detect Python 3.11+ after several attempts. Open a fresh shell so PATH refreshes, then re-run `brainapi init`. Any cloned source is preserved.", + ); + process.exit(1); +} + +export async function ensurePython(): Promise { + const detected = await detectPython(); + if (detected) return detected; + return recoverPython(); +} diff --git a/tui/src/lib/python.ts b/tui/src/lib/python.ts new file mode 100644 index 0000000000000000000000000000000000000000..6e100460e5f1a226f09f10c7c784d24c090c7872 --- /dev/null +++ b/tui/src/lib/python.ts @@ -0,0 +1,130 @@ +import { access, mkdir, readFile, writeFile } from "node:fs/promises"; +import path from "node:path"; +import { + MIN_PYTHON_VERSION, + PYTHON_CANDIDATES, +} from "../constants.js"; +import { sourcePath, venvBin, venvPath } from "./paths.js"; +import { runQuiet, runInherit } from "./exec.js"; +import { updateState } from "./state.js"; +import { INSTALL_EXTRAS_SCRIPT as EMBEDDED_INSTALL_EXTRAS_SCRIPT } from "../embedded/install-extras-script.js"; + +const INSTALL_EXTRAS_SCRIPT_REL = "scripts/install_extras.py"; + +async function ensureInstallExtrasScript(): Promise { + const target = path.join(sourcePath(), INSTALL_EXTRAS_SCRIPT_REL); + let existing: string | null = null; + try { + existing = await readFile(target, "utf8"); + } catch { + existing = null; + } + if (existing === EMBEDDED_INSTALL_EXTRAS_SCRIPT) return; + await mkdir(path.dirname(target), { recursive: true }); + await writeFile(target, EMBEDDED_INSTALL_EXTRAS_SCRIPT, "utf8"); +} + +export interface DetectedPython { + bin: string; + version: [number, number, number]; +} + +function meetsMinimum(version: [number, number, number]): boolean { + const [major, minor] = version; + const [minMajor, minMinor] = MIN_PYTHON_VERSION; + if (major > minMajor) return true; + if (major < minMajor) return false; + return minor >= minMinor; +} + +export async function detectPython(): Promise { + for (const candidate of PYTHON_CANDIDATES) { + const result = await runQuiet(candidate, ["--version"]); + if (!result.ok) continue; + const output = result.stdout || result.stderr; + const match = output.match(/Python (\d+)\.(\d+)\.(\d+)/); + if (!match) continue; + const major = Number(match[1]); + const minor = Number(match[2]); + const patch = Number(match[3]); + const version: [number, number, number] = [major, minor, patch]; + if (meetsMinimum(version)) { + return { bin: candidate, version }; + } + } + return null; +} + +export async function venvExists(): Promise { + try { + await access(path.join(venvPath(), "pyvenv.cfg")); + return true; + } catch { + return false; + } +} + +export async function createVenv(pythonBin: string): Promise { + const result = await runInherit(pythonBin, ["-m", "venv", venvPath()]); + if (!result.ok) { + throw new Error(`python -m venv failed with exit code ${result.exitCode}`); + } + await updateState({ venvCreated: true }); +} + +export interface InstallDepsOptions { + extras?: string[]; + skipBase?: boolean; +} + +function scriptArgs(opts: InstallDepsOptions, withUpgrade: boolean): string[] { + const args: string[] = [INSTALL_EXTRAS_SCRIPT_REL]; + if (!opts.skipBase) args.push("--include-base"); + if (withUpgrade) args.push("--upgrade-pip"); + for (const extra of opts.extras ?? []) { + args.push(extra); + } + return args; +} + +export async function installDeps(options: InstallDepsOptions = {}): Promise { + await ensureInstallExtrasScript(); + const pythonBin = venvBin("python"); + const args = scriptArgs(options, true); + const result = await runInherit(pythonBin, args, { cwd: sourcePath() }); + if (!result.ok) { + throw new Error(`scripts/install_extras.py failed with exit code ${result.exitCode}`); + } + if (!options.skipBase) { + await updateState({ depsInstalled: true }); + } +} + +export async function installExtras(extras: string[]): Promise { + if (extras.length === 0) return; + await installDeps({ extras, skipBase: true }); +} + +export async function missingExtras(extras: string[]): Promise { + if (extras.length === 0) return []; + await ensureInstallExtrasScript(); + const pythonBin = venvBin("python"); + const result = await runQuiet( + pythonBin, + [INSTALL_EXTRAS_SCRIPT_REL, "--check", ...extras], + { cwd: sourcePath() }, + ); + if (result.ok) return []; + return [...extras]; +} + +export async function missingRequiredPythonDeps(): Promise { + await ensureInstallExtrasScript(); + const pythonBin = venvBin("python"); + const result = await runQuiet( + pythonBin, + [INSTALL_EXTRAS_SCRIPT_REL, "--check"], + { cwd: sourcePath() }, + ); + return !result.ok; +} diff --git a/tui/src/lib/reset-state.ts b/tui/src/lib/reset-state.ts new file mode 100644 index 0000000000000000000000000000000000000000..8222b244ee32d59c456e123a17da61aeec486503 --- /dev/null +++ b/tui/src/lib/reset-state.ts @@ -0,0 +1,481 @@ +import { createHash } from "node:crypto"; +import { ENV_KEYS, SERVICE_DEFAULTS } from "../constants.js"; +import type { EnvFile } from "./env.js"; +import { getEnvValue } from "./env.js"; +import { runQuiet } from "./exec.js"; +import { which } from "./exec.js"; + +const CELERY_QUEUE_KEYS = [ + "ingest_data", + "process_architect_relationships", + "ingest_structured_data", + "consolidate_graph", + "finalize_ingestion", + "ingest_file", + "unacked", + "unacked_index", + "unacked_mutex", +] as const; + +const BRAIN_DB_PREFIX = "brain_"; +const MAX_DBNAME_LEN = 63; +const HASH_SUFFIX_LEN = 12; + +export type ResetScope = + | { kind: "queues" } + | { kind: "brain"; brainId: string } + | { kind: "all" }; + +export interface ResetOptions { + yes?: boolean; + redisOnly?: boolean; + dbOnly?: boolean; +} + +export interface ResetPlan { + scope: ResetScope; + redisHost: string; + redisPort: number; + postgres?: { + host: string; + port: number; + username: string; + password: string; + systemDatabase: string; + maintenanceDatabase: string; + }; + brainDatabases: string[]; + redisActions: string[]; + dbActions: string[]; +} + +export interface ResetResult { + redisDeletedKeys: number; + databasesDropped: string[]; + registryRowsDeleted: number; + warnings: string[]; +} + +function requireEnv(env: EnvFile, key: string, fallback?: string): string { + return getEnvValue(env, key) ?? fallback ?? ""; +} + +export function brainDbName(brainId: string): string { + if (!brainId.trim()) { + throw new Error("brain_id must be a non-empty string"); + } + const sanitized = + brainId + .toLowerCase() + .replace(/[^a-z0-9_]+/g, "_") + .replace(/^_+|_+$/g, "") || "x"; + const candidate = `${BRAIN_DB_PREFIX}${sanitized}`; + if (candidate.length <= MAX_DBNAME_LEN) return candidate; + + const digest = createHash("sha256") + .update(brainId) + .digest("hex") + .slice(0, HASH_SUFFIX_LEN); + const headBudget = MAX_DBNAME_LEN - BRAIN_DB_PREFIX.length - digest.length - 1; + const head = sanitized.slice(0, headBudget).replace(/_+$/g, ""); + return `${BRAIN_DB_PREFIX}${head}_${digest}`; +} + +export function brainIdFromDbName(dbName: string): string | null { + if (!dbName.startsWith(BRAIN_DB_PREFIX)) return null; + const suffix = dbName.slice(BRAIN_DB_PREFIX.length); + return suffix || null; +} + +export function usesPostgres(env: EnvFile): boolean { + const graph = getEnvValue(env, ENV_KEYS.graphDb); + const data = getEnvValue(env, ENV_KEYS.dataDb); + const vector = getEnvValue(env, ENV_KEYS.vectorDb); + return ( + graph === "networkx" || data === "postgresql" || vector === "postgresql" + ); +} + +export function buildResetPlan(env: EnvFile, scope: ResetScope): ResetPlan { + const redisHost = + requireEnv(env, ENV_KEYS.redisHost, SERVICE_DEFAULTS.redis.host) || + SERVICE_DEFAULTS.redis.host; + const redisPort = Number( + requireEnv(env, ENV_KEYS.redisPort, String(SERVICE_DEFAULTS.redis.port)) || + SERVICE_DEFAULTS.redis.port, + ); + + const plan: ResetPlan = { + scope, + redisHost, + redisPort, + brainDatabases: [], + redisActions: [], + dbActions: [], + }; + + if (scope.kind === "queues") { + plan.redisActions.push("Purge Celery queue keys and unacked buffers"); + } else if (scope.kind === "brain") { + plan.redisActions.push(`Delete Redis keys for brain "${scope.brainId}"`); + plan.redisActions.push("Purge Celery queue keys and unacked buffers"); + plan.brainDatabases = [brainDbName(scope.brainId)]; + plan.dbActions.push(`DROP DATABASE ${plan.brainDatabases[0]}`); + plan.dbActions.push( + `DELETE FROM data_brains WHERE name_key/id = "${scope.brainId}"`, + ); + } else { + plan.redisActions.push("FLUSHDB (clear entire Redis database)"); + plan.dbActions.push("DROP all brain_* databases"); + plan.dbActions.push("DELETE all rows from data_brains"); + } + + if (usesPostgres(env)) { + plan.postgres = { + host: + requireEnv(env, ENV_KEYS.postgresHost, SERVICE_DEFAULTS.postgresql.host) || + SERVICE_DEFAULTS.postgresql.host, + port: Number( + requireEnv( + env, + ENV_KEYS.postgresPort, + String(SERVICE_DEFAULTS.postgresql.port), + ) || SERVICE_DEFAULTS.postgresql.port, + ), + username: + requireEnv( + env, + ENV_KEYS.postgresUsername, + SERVICE_DEFAULTS.postgresql.username, + ) || SERVICE_DEFAULTS.postgresql.username, + password: + requireEnv( + env, + ENV_KEYS.postgresPassword, + SERVICE_DEFAULTS.postgresql.password, + ) || SERVICE_DEFAULTS.postgresql.password, + systemDatabase: + requireEnv(env, ENV_KEYS.postgresSystemDatabase, "brainapi") || + "brainapi", + maintenanceDatabase: + requireEnv(env, ENV_KEYS.postgresMaintenanceDatabase, "postgres") || + "postgres", + }; + } else { + plan.dbActions = []; + plan.brainDatabases = []; + } + + return plan; +} + +async function ensureBin(bin: string): Promise { + if (!(await which(bin))) { + throw new Error( + `Required binary "${bin}" was not found on PATH. Install it or add it to PATH.`, + ); + } +} + +async function redisCli( + host: string, + port: number, + args: string[], +): Promise<{ ok: boolean; stdout: string; stderr: string }> { + return runQuiet("redis-cli", ["-h", host, "-p", String(port), ...args]); +} + +async function purgeCeleryQueues( + host: string, + port: number, +): Promise { + const result = await redisCli(host, port, ["DEL", ...CELERY_QUEUE_KEYS]); + if (!result.ok) { + throw new Error( + `Failed to purge Celery queues: ${result.stderr || result.stdout}`, + ); + } + const n = Number(result.stdout.trim() || "0"); + return Number.isFinite(n) ? n : 0; +} + +async function deleteKeysByPattern( + host: string, + port: number, + pattern: string, +): Promise { + const scanned = await redisCli(host, port, ["--scan", "--pattern", pattern]); + if (!scanned.ok) { + throw new Error( + `Failed to scan Redis keys (${pattern}): ${scanned.stderr || scanned.stdout}`, + ); + } + const keys = scanned.stdout + .split(/\r?\n/) + .map((k) => k.trim()) + .filter(Boolean); + if (keys.length === 0) return 0; + + let deleted = 0; + const chunkSize = 100; + for (let i = 0; i < keys.length; i += chunkSize) { + const chunk = keys.slice(i, i + chunkSize); + const result = await redisCli(host, port, ["DEL", ...chunk]); + if (!result.ok) { + throw new Error( + `Failed to delete Redis keys: ${result.stderr || result.stdout}`, + ); + } + deleted += Number(result.stdout.trim() || "0") || chunk.length; + } + return deleted; +} + +async function psql( + pg: NonNullable, + database: string, + sql: string, +): Promise<{ ok: boolean; stdout: string; stderr: string }> { + return runQuiet( + "psql", + [ + "-h", + pg.host, + "-p", + String(pg.port), + "-U", + pg.username, + "-d", + database, + "-v", + "ON_ERROR_STOP=1", + "-t", + "-A", + "-c", + sql, + ], + { + env: { + ...process.env, + PGPASSWORD: pg.password, + }, + }, + ); +} + +export async function listBrainDatabases( + env: EnvFile, +): Promise> { + if (!usesPostgres(env)) return []; + await ensureBin("psql"); + const plan = buildResetPlan(env, { kind: "all" }); + if (!plan.postgres) return []; + + const result = await psql( + plan.postgres, + plan.postgres.maintenanceDatabase, + `SELECT datname FROM pg_database WHERE datname LIKE 'brain\\_%' ESCAPE '\\' AND datname <> '${plan.postgres.systemDatabase.replace(/'/g, "''")}' ORDER BY 1;`, + ); + if (!result.ok) { + throw new Error( + `Failed to list brain databases: ${result.stderr || result.stdout}`, + ); + } + return result.stdout + .split(/\r?\n/) + .map((line) => line.trim()) + .filter(Boolean) + .map((database) => ({ + database, + brainId: brainIdFromDbName(database) ?? database, + })); +} + +async function dropBrainDatabase( + pg: NonNullable, + database: string, +): Promise { + if (!/^[a-z0-9_]+$/.test(database)) { + throw new Error(`Refusing to drop unsafe database name: ${database}`); + } + if (database === pg.systemDatabase || database === pg.maintenanceDatabase) { + throw new Error( + `Refusing to drop protected database "${database}" (system/maintenance).`, + ); + } + if (!database.startsWith(BRAIN_DB_PREFIX) || database === "brainapi") { + throw new Error( + `Refusing to drop non-brain database "${database}". Expected prefix ${BRAIN_DB_PREFIX}.`, + ); + } + const terminate = await psql( + pg, + pg.maintenanceDatabase, + `SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = '${database}' AND pid <> pg_backend_pid();`, + ); + if (!terminate.ok) { + throw new Error( + `Failed to terminate connections to ${database}: ${terminate.stderr || terminate.stdout}`, + ); + } + const drop = await psql( + pg, + pg.maintenanceDatabase, + `DROP DATABASE IF EXISTS ${database};`, + ); + if (!drop.ok) { + throw new Error( + `Failed to drop ${database}: ${drop.stderr || drop.stdout}`, + ); + } +} + +async function ensureSystemDatabase( + pg: NonNullable, +): Promise { + if (!/^[a-z0-9_]+$/.test(pg.systemDatabase)) { + throw new Error(`Unsafe system database name: ${pg.systemDatabase}`); + } + const result = await psql( + pg, + pg.maintenanceDatabase, + `SELECT 1 FROM pg_database WHERE datname = '${pg.systemDatabase}';`, + ); + if (!result.ok) { + throw new Error( + `Failed to check system database: ${result.stderr || result.stdout}`, + ); + } + if (result.stdout.trim()) return; + const created = await psql( + pg, + pg.maintenanceDatabase, + `CREATE DATABASE ${pg.systemDatabase};`, + ); + if (!created.ok) { + throw new Error( + `Failed to recreate system database ${pg.systemDatabase}: ${created.stderr || created.stdout}`, + ); + } +} + +async function deleteBrainRegistryRows( + pg: NonNullable, + brainId: string | null, +): Promise { + const sql = + brainId === null + ? "DELETE FROM data_brains RETURNING id;" + : `DELETE FROM data_brains WHERE name_key = '${brainId.replace(/'/g, "''")}' OR id = '${brainId.replace(/'/g, "''")}' RETURNING id;`; + const result = await psql(pg, pg.systemDatabase, sql); + if (!result.ok) { + // Table may not exist yet on fresh installs. + if (/data_brains/i.test(result.stderr) && /does not exist/i.test(result.stderr)) { + return 0; + } + throw new Error( + `Failed to clean data_brains registry: ${result.stderr || result.stdout}`, + ); + } + return result.stdout + .split(/\r?\n/) + .map((line) => line.trim()) + .filter(Boolean).length; +} + +export async function executeReset( + env: EnvFile, + scope: ResetScope, + options: ResetOptions = {}, +): Promise { + const celeryBackend = getEnvValue(env, ENV_KEYS.celeryBackend) ?? "redis"; + if (celeryBackend !== "redis" && !options.dbOnly) { + throw new Error( + `reset currently supports CELERY_BACKEND=redis only (got "${celeryBackend}").`, + ); + } + + const plan = buildResetPlan(env, scope); + const warnings: string[] = []; + let redisDeletedKeys = 0; + const databasesDropped: string[] = []; + let registryRowsDeleted = 0; + + const doRedis = !options.dbOnly; + const doDb = !options.redisOnly && Boolean(plan.postgres) && scope.kind !== "queues"; + + if (doRedis) { + await ensureBin("redis-cli"); + if (scope.kind === "all") { + const flush = await redisCli(plan.redisHost, plan.redisPort, ["FLUSHDB"]); + if (!flush.ok) { + throw new Error(`Redis FLUSHDB failed: ${flush.stderr || flush.stdout}`); + } + redisDeletedKeys = -1; // unknown count + } else { + if (scope.kind === "brain") { + redisDeletedKeys += await deleteKeysByPattern( + plan.redisHost, + plan.redisPort, + `${scope.brainId}:*`, + ); + const systemKey = await redisCli(plan.redisHost, plan.redisPort, [ + "DEL", + `system:brain:${scope.brainId}`, + ]); + if (systemKey.ok) { + redisDeletedKeys += Number(systemKey.stdout.trim() || "0") || 0; + } + } + redisDeletedKeys += await purgeCeleryQueues(plan.redisHost, plan.redisPort); + } + } + + if (doDb && plan.postgres) { + await ensureBin("psql"); + await ensureSystemDatabase(plan.postgres); + let targets = plan.brainDatabases; + if (scope.kind === "all") { + const listed = await listBrainDatabases(env); + targets = listed.map((row) => row.database); + } + for (const database of targets) { + if ( + database === plan.postgres.systemDatabase || + database === plan.postgres.maintenanceDatabase || + database === "brainapi" + ) { + warnings.push(`Skipped protected database "${database}".`); + continue; + } + await dropBrainDatabase(plan.postgres, database); + databasesDropped.push(database); + } + if (scope.kind === "brain") { + registryRowsDeleted = await deleteBrainRegistryRows( + plan.postgres, + scope.brainId, + ); + } else if (scope.kind === "all") { + registryRowsDeleted = await deleteBrainRegistryRows(plan.postgres, null); + } + } else if (!options.redisOnly && !plan.postgres && scope.kind !== "queues") { + warnings.push( + "Postgres is not configured for this install (GRAPH_DB/DATA_DB/VECTOR_DB); skipped database wipe.", + ); + } + + if (options.redisOnly) { + warnings.push("Skipped Postgres (--redis-only)."); + } + if (options.dbOnly) { + warnings.push("Skipped Redis (--db-only)."); + } + + return { + redisDeletedKeys, + databasesDropped, + registryRowsDeleted, + warnings, + }; +} diff --git a/tui/src/lib/service-target.ts b/tui/src/lib/service-target.ts new file mode 100644 index 0000000000000000000000000000000000000000..872de1be1680683dc4d67cd4adfb596a9b5be4b5 --- /dev/null +++ b/tui/src/lib/service-target.ts @@ -0,0 +1,110 @@ +import type { Connections } from "../types.js"; +import type { ServiceName } from "../constants.js"; +import { SERVICE_DEFAULTS } from "../constants.js"; + +export interface ServiceTarget { + host: string; + port: number; + managed?: boolean; +} + +export function targetFromConnections( + service: ServiceName, + connections: Connections, +): ServiceTarget | null { + switch (service) { + case "redis": + return { + host: connections.redis.host, + port: connections.redis.port, + }; + case "postgresql": + if (!connections.postgresql) return null; + return { + host: connections.postgresql.host, + port: connections.postgresql.port, + }; + case "neo4j": + if (!connections.neo4j) return null; + return { + host: connections.neo4j.host, + port: connections.neo4j.port, + }; + case "milvus": + if (!connections.milvus) return null; + if (connections.milvus.uri) { + return { + host: connections.milvus.host, + port: connections.milvus.port, + managed: true, + }; + } + return { + host: connections.milvus.host, + port: connections.milvus.port, + }; + case "mongo": + if (!connections.mongo) return null; + return { + host: connections.mongo.host, + port: connections.mongo.port, + }; + case "rabbitmq": + return { + host: SERVICE_DEFAULTS.rabbitmq.host, + port: SERVICE_DEFAULTS.rabbitmq.port, + }; + default: + return null; + } +} + +export function targetFromEnv( + service: ServiceName, + getter: (key: string) => string | undefined, +): ServiceTarget | null { + const defaults = SERVICE_DEFAULTS; + switch (service) { + case "redis": + return { + host: getter("REDIS_HOST") ?? defaults.redis.host, + port: Number(getter("REDIS_PORT") ?? defaults.redis.port), + }; + case "postgresql": + return { + host: getter("POSTGRES_HOST") ?? defaults.postgresql.host, + port: Number(getter("POSTGRES_PORT") ?? defaults.postgresql.port), + }; + case "neo4j": + return { + host: getter("NEO4J_HOST") ?? defaults.neo4j.host, + port: Number(getter("NEO4J_PORT") ?? defaults.neo4j.port), + }; + case "milvus": { + const uri = getter("MILVUS_URI"); + if (uri && uri.length > 0) { + return { + host: getter("MILVUS_HOST") ?? defaults.milvus.host, + port: Number(getter("MILVUS_PORT") ?? defaults.milvus.port), + managed: true, + }; + } + return { + host: getter("MILVUS_HOST") ?? defaults.milvus.host, + port: Number(getter("MILVUS_PORT") ?? defaults.milvus.port), + }; + } + case "mongo": + return { + host: getter("MONGO_HOST") ?? defaults.mongo.host, + port: Number(getter("MONGO_PORT") ?? defaults.mongo.port), + }; + case "rabbitmq": + return { + host: getter("RABBITMQ_HOST") ?? defaults.rabbitmq.host, + port: Number(getter("RABBITMQ_PORT") ?? defaults.rabbitmq.port), + }; + default: + return null; + } +} diff --git a/tui/src/lib/state.ts b/tui/src/lib/state.ts new file mode 100644 index 0000000000000000000000000000000000000000..97f6180200af382148e69a36117a31d070b4e448 --- /dev/null +++ b/tui/src/lib/state.ts @@ -0,0 +1,50 @@ +import { mkdir, readFile, writeFile } from "node:fs/promises"; +import path from "node:path"; +import { brainapiHome, sourcePath, stateFilePath } from "./paths.js"; +import { DEFAULT_BRANCH, DEFAULT_REPO_URL } from "../constants.js"; +import type { InstallState } from "../types.js"; + +export function defaultState(): InstallState { + return { + cloned: false, + venvCreated: false, + depsInstalled: false, + envWritten: false, + containersStarted: false, + selectedServices: null, + installedPlugins: null, + servicesRuntime: null, + sourcePath: sourcePath(), + repoUrl: process.env.BRAINAPI_REPO_URL ?? DEFAULT_REPO_URL, + branch: process.env.BRAINAPI_BRANCH ?? DEFAULT_BRANCH, + }; +} + +export async function readState(): Promise { + try { + const raw = await readFile(stateFilePath(), "utf8"); + const parsed = JSON.parse(raw) as Partial; + return { ...defaultState(), ...parsed }; + } catch (err) { + if ((err as NodeJS.ErrnoException).code === "ENOENT") return null; + throw err; + } +} + +export async function writeState(state: InstallState): Promise { + await mkdir(path.dirname(stateFilePath()), { recursive: true }); + await writeFile(stateFilePath(), JSON.stringify(state, null, 2), "utf8"); +} + +export async function updateState( + patch: Partial, +): Promise { + const current = (await readState()) ?? defaultState(); + const next = { ...current, ...patch }; + await writeState(next); + return next; +} + +export async function ensureStateDir(): Promise { + await mkdir(brainapiHome(), { recursive: true }); +} diff --git a/tui/src/lib/sync-source.ts b/tui/src/lib/sync-source.ts new file mode 100644 index 0000000000000000000000000000000000000000..fb95e8e5b6b96ba073b6098fcbcaa827681a864e --- /dev/null +++ b/tui/src/lib/sync-source.ts @@ -0,0 +1,73 @@ +import { access, cp } from "node:fs/promises"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import * as p from "@clack/prompts"; +import pc from "picocolors"; +import { sourcePath } from "./paths.js"; + +const SYNC_PATHS = ["src", "console"] as const; + +const SKIP_DIR_NAMES = new Set([ + "node_modules", + ".venv", + "__pycache__", + ".git", + ".turbo", + ".next", +]); + +function shouldSkipSyncEntry(srcPath: string): boolean { + return SKIP_DIR_NAMES.has(path.basename(srcPath)); +} + +async function pathExists(target: string): Promise { + try { + await access(target); + return true; + } catch { + return false; + } +} + +export async function devWorkspaceRoot(): Promise { + const fromEnv = process.env.BRAINAPI_DEV_SOURCE?.trim(); + if (fromEnv && (await pathExists(path.join(fromEnv, "src", "config.py")))) { + return fromEnv; + } + + let dir = path.dirname(fileURLToPath(import.meta.url)); + for (let i = 0; i < 6; i++) { + if (await pathExists(path.join(dir, "src", "config.py"))) { + return dir; + } + const parent = path.dirname(dir); + if (parent === dir) break; + dir = parent; + } + return null; +} + +export async function syncDevSourceIfAvailable(): Promise { + const root = await devWorkspaceRoot(); + if (!root) return false; + + const destRoot = sourcePath(); + let synced = false; + for (const rel of SYNC_PATHS) { + const src = path.join(root, rel); + const dest = path.join(destRoot, rel); + if (!(await pathExists(src))) continue; + await cp(src, dest, { + recursive: true, + force: true, + filter: (source) => !shouldSkipSyncEntry(source), + }); + synced = true; + } + if (synced) { + p.log.info( + `Synced Python sources from ${pc.cyan(root)} → ${pc.cyan(destRoot)}`, + ); + } + return synced; +} diff --git a/tui/src/lib/write-env.ts b/tui/src/lib/write-env.ts new file mode 100644 index 0000000000000000000000000000000000000000..216137eb2f6b5e00aa00b966819860461f5e40b8 --- /dev/null +++ b/tui/src/lib/write-env.ts @@ -0,0 +1,318 @@ +import { + ANTHROPIC_DEFAULT_LARGE_MODEL, + ANTHROPIC_DEFAULT_SMALL_MODEL, + AZURE_DEFAULT_EMBEDDING_MODEL, + AZURE_DEFAULT_LARGE_API_VERSION, + AZURE_DEFAULT_LARGE_MODEL, + AZURE_DEFAULT_SMALL_MODEL, + BEDROCK_DEFAULT_EMBEDDING_MODEL, + BEDROCK_DEFAULT_LARGE_MODEL, + BEDROCK_DEFAULT_REGION, + BEDROCK_DEFAULT_SMALL_MODEL, + DEEPSEEK_DEFAULT_LARGE_MODEL, + DEEPSEEK_DEFAULT_SMALL_MODEL, + EMBEDDINGS_DEFAULTS, + OPENAI_DEFAULT_EMBEDDING_MODEL, + OPENAI_DEFAULT_LARGE_MODEL, + OPENAI_DEFAULT_SMALL_MODEL, + OLLAMA_EMBEDDING_DIMENSIONS, + ENV_KEYS, + GCP_DEFAULT_EMBEDDING_MODEL, + GCP_DEFAULT_LARGE_MODEL, + GCP_DEFAULT_SMALL_MODEL, + OLLAMA_DEFAULT_HOST, + OLLAMA_DEFAULT_LARGE_MODEL, + OLLAMA_DEFAULT_PORT, + OLLAMA_DEFAULT_SMALL_MODEL, + SERVICE_DEFAULTS, +} from "../constants.js"; +import { applyEnvValues, loadOrSeedEnv, removeEnvValue, saveEnv } from "./env.js"; +import type { InitChoices } from "../types.js"; +import { resolveActiveEmbeddingModel } from "../flows/embedding-dimensions.js"; +import { lookupEmbeddingDimensions } from "../constants.js"; + +const AZURE_ENDPOINT_PLACEHOLDER = "https://example.openai.azure.com"; +const AZURE_EMBEDDING_PLACEHOLDER = + "https://example.openai.azure.com/openai/deployments/text-embedding-3-large/embeddings?api-version=2023-05-15"; +const AZURE_KEY_PLACEHOLDER = "azure-key-placeholder"; +const OPENAI_KEY_PLACEHOLDER = "openai-api-key-placeholder"; +const ANTHROPIC_KEY_PLACEHOLDER = "anthropic-api-key-placeholder"; +const DEEPSEEK_KEY_PLACEHOLDER = "deepseek-api-key-placeholder"; + +function resolveModelsMode(choices: InitChoices): "local" | "remote" { + const { llmSmallProvider, llmLargeProvider, embeddingsProvider } = choices.models; + if ( + llmSmallProvider === "ollama" && + llmLargeProvider === "ollama" && + embeddingsProvider === "ollama" + ) { + return "local"; + } + return choices.models.mode; +} + +export async function writeEnvFromChoices(choices: InitChoices): Promise { + const env = await loadOrSeedEnv(); + const values: Record = {}; + + values[ENV_KEYS.graphDb] = choices.dbs.graphDb; + values[ENV_KEYS.dataDb] = choices.dbs.dataDb; + values[ENV_KEYS.vectorDb] = choices.dbs.vectorDb; + values[ENV_KEYS.modelsMode] = resolveModelsMode(choices); + values[ENV_KEYS.ocrMode] = choices.pipeline.ocrMode; + values[ENV_KEYS.pipelineMode] = "accurate"; + values[ENV_KEYS.agenticArchitecture] = "custom"; + values[ENV_KEYS.runGraphConsolidator] = "true"; + values[ENV_KEYS.celeryWorkerConcurrency] = 4; + + applySearchValues(values, choices); + + values[ENV_KEYS.brainpatToken] = choices.auth.brainpatToken; + values[ENV_KEYS.enabledPlugins] = choices.plugins.map((plugin) => plugin.name).join(","); + values["ENV"] = "development"; + values["CONSOLE_ENABLED"] = "true"; + + values[ENV_KEYS.redisHost] = choices.connections.redis.host; + values[ENV_KEYS.redisPort] = choices.connections.redis.port; + + applyPostgresValues(values, choices); + applyNeo4jValues(values, choices); + applyMilvusValues(values, choices, env); + applyMongoValues(values, choices); + applyOllamaValues(values, choices); + applyRemoteModelValues(values, choices); + applyEmbeddingDefaults(values, choices); + + applyEnvValues(env, values); + await saveEnv(env); +} + +function applySearchValues( + values: Record, + choices: InitChoices, +): void { + const search = choices.search ?? { enabled: false, retrieval: "hybrid" }; + const enabled = search.enabled; + const retrieval = search.retrieval ?? "hybrid"; + values[ENV_KEYS.searchEnabled] = enabled ? "true" : "false"; + values[ENV_KEYS.searchUseDense] = + !enabled || retrieval === "hybrid" || retrieval === "dense" ? "true" : "false"; + values[ENV_KEYS.searchUseBm25] = + !enabled || retrieval === "hybrid" || retrieval === "bm25" ? "true" : "false"; + values[ENV_KEYS.searchFusion] = "rrf"; + values[ENV_KEYS.searchFusionAlpha] = "0.5"; + values[ENV_KEYS.searchBm25K1] = "1.2"; + values[ENV_KEYS.searchBm25B] = "0.75"; + values[ENV_KEYS.contextPassageMode] = enabled ? "hybrid" : "hybrid"; +} + +function applyPostgresValues( + values: Record, + choices: InitChoices, +): void { + const pg = choices.connections.postgresql; + const defaults = SERVICE_DEFAULTS.postgresql; + values[ENV_KEYS.postgresHost] = pg?.host ?? defaults.host; + values[ENV_KEYS.postgresPort] = pg?.port ?? defaults.port; + values[ENV_KEYS.postgresUsername] = pg?.username ?? defaults.username; + values[ENV_KEYS.postgresPassword] = pg?.password ?? defaults.password; + values[ENV_KEYS.postgresSystemDatabase] = + pg?.systemDatabase ?? defaults.systemDatabase; + values[ENV_KEYS.postgresMaintenanceDatabase] = + pg?.maintenanceDatabase ?? defaults.maintenanceDatabase; +} + +function applyNeo4jValues( + values: Record, + choices: InitChoices, +): void { + const neo = choices.connections.neo4j; + const defaults = SERVICE_DEFAULTS.neo4j; + values[ENV_KEYS.neo4jHost] = neo?.host ?? defaults.host; + values[ENV_KEYS.neo4jPort] = neo?.port ?? defaults.port; + values[ENV_KEYS.neo4jUsername] = neo?.username ?? defaults.username; + values[ENV_KEYS.neo4jPassword] = neo?.password ?? defaults.password; +} + +function applyMilvusValues( + values: Record, + choices: InitChoices, + env: Awaited>, +): void { + const mv = choices.connections.milvus; + const defaults = SERVICE_DEFAULTS.milvus; + values[ENV_KEYS.milvusHost] = mv?.host ?? defaults.host; + values[ENV_KEYS.milvusPort] = mv?.port ?? defaults.port; + if (mv?.uri) { + values[ENV_KEYS.milvusUri] = mv.uri; + } else { + removeEnvValue(env, ENV_KEYS.milvusUri); + } + if (mv?.token) { + values[ENV_KEYS.milvusToken] = mv.token; + } else { + removeEnvValue(env, ENV_KEYS.milvusToken); + } +} + +function applyMongoValues( + values: Record, + choices: InitChoices, +): void { + const mg = choices.connections.mongo; + const defaults = SERVICE_DEFAULTS.mongo; + values[ENV_KEYS.mongoHost] = mg?.host ?? defaults.host; + values[ENV_KEYS.mongoPort] = mg?.port ?? defaults.port; + values[ENV_KEYS.mongoUsername] = mg?.username ?? defaults.username; + values[ENV_KEYS.mongoPassword] = mg?.password ?? defaults.password; + values[ENV_KEYS.mongoSystemDatabase] = "system"; +} + +function applyOllamaValues( + values: Record, + choices: InitChoices, +): void { + const usesOllama = + choices.models.llmSmallProvider === "ollama" || + choices.models.llmLargeProvider === "ollama" || + choices.models.embeddingsProvider === "ollama"; + if (usesOllama && choices.models.ollama) { + const ol = choices.models.ollama; + values[ENV_KEYS.ollamaHost] = ol.host; + values[ENV_KEYS.ollamaPort] = ol.port; + values[ENV_KEYS.ollamaSmallModel] = ol.smallModel; + values[ENV_KEYS.ollamaLargeModel] = ol.largeModel; + values[ENV_KEYS.embeddingsLocalModel] = ol.embeddingsLocalModel; + return; + } + values[ENV_KEYS.ollamaHost] = OLLAMA_DEFAULT_HOST; + values[ENV_KEYS.ollamaPort] = OLLAMA_DEFAULT_PORT; + values[ENV_KEYS.ollamaSmallModel] = OLLAMA_DEFAULT_SMALL_MODEL; + values[ENV_KEYS.ollamaLargeModel] = OLLAMA_DEFAULT_LARGE_MODEL; +} + +function applyRemoteModelValues( + values: Record, + choices: InitChoices, +): void { + const gcp = choices.models.gcp; + const azure = choices.models.azure; + const openai = choices.models.openai; + const anthropic = choices.models.anthropic; + const deepseek = choices.models.deepseek; + const bedrock = choices.models.bedrock; + values[ENV_KEYS.llmSmallProvider] = choices.models.llmSmallProvider; + values[ENV_KEYS.llmLargeProvider] = choices.models.llmLargeProvider; + values[ENV_KEYS.embeddingsProvider] = choices.models.embeddingsProvider; + values[ENV_KEYS.gcpCredentialsPath] = gcp?.credentialsPath ?? "gcp_credentials.json"; + values[ENV_KEYS.gcpProjectId] = gcp?.projectId ?? "your-project-id"; + values[ENV_KEYS.gcpSmallLlmModel] = gcp?.smallLlmModel ?? GCP_DEFAULT_SMALL_MODEL; + values[ENV_KEYS.gcpExtraSmallLlmModel] = + gcp?.smallLlmModel ?? GCP_DEFAULT_SMALL_MODEL; + values[ENV_KEYS.gcpLargeLlmModel] = gcp?.largeLlmModel ?? GCP_DEFAULT_LARGE_MODEL; + values[ENV_KEYS.gcpEmbeddingModel] = gcp?.embeddingModel ?? GCP_DEFAULT_EMBEDDING_MODEL; + + values[ENV_KEYS.azureSmallLlmModel] = + azure?.smallLlmModel ?? AZURE_DEFAULT_SMALL_MODEL; + values[ENV_KEYS.azureLargeLlmModel] = + azure?.largeLlmModel ?? AZURE_DEFAULT_LARGE_MODEL; + values[ENV_KEYS.azureLlmApiVersion] = + azure?.llmApiVersion ?? AZURE_DEFAULT_LARGE_API_VERSION; + values[ENV_KEYS.azureLlmEndpoint] = + azure?.llmEndpoint ?? AZURE_ENDPOINT_PLACEHOLDER; + values[ENV_KEYS.azureLlmSubscriptionKey] = + azure?.llmSubscriptionKey ?? AZURE_KEY_PLACEHOLDER; + values[ENV_KEYS.azureLargeLlmApiVersion] = + azure?.llmApiVersion ?? AZURE_DEFAULT_LARGE_API_VERSION; + values[ENV_KEYS.azureLargeLlmEndpoint] = + azure?.llmEndpoint ?? AZURE_ENDPOINT_PLACEHOLDER; + values[ENV_KEYS.azureLargeLlmSubscriptionKey] = + azure?.llmSubscriptionKey ?? AZURE_KEY_PLACEHOLDER; + values[ENV_KEYS.azureEmbeddingModel] = + azure?.embeddingModel ?? AZURE_DEFAULT_EMBEDDING_MODEL; + values[ENV_KEYS.azureEmbeddingFullEndpoint] = + azure?.embeddingEndpoint ?? AZURE_EMBEDDING_PLACEHOLDER; + values[ENV_KEYS.azureEmbeddingKey] = + azure?.embeddingKey ?? AZURE_KEY_PLACEHOLDER; + + values[ENV_KEYS.openaiApiKey] = openai?.apiKey ?? OPENAI_KEY_PLACEHOLDER; + values[ENV_KEYS.openaiSmallLlmModel] = + openai?.smallLlmModel ?? OPENAI_DEFAULT_SMALL_MODEL; + values[ENV_KEYS.openaiLargeLlmModel] = + openai?.largeLlmModel ?? OPENAI_DEFAULT_LARGE_MODEL; + values[ENV_KEYS.openaiEmbeddingModel] = + openai?.embeddingModel ?? OPENAI_DEFAULT_EMBEDDING_MODEL; + if (openai?.baseUrl) { + values[ENV_KEYS.openaiBaseUrl] = openai.baseUrl; + } else { + values[ENV_KEYS.openaiBaseUrl] = ""; + } + values[ENV_KEYS.anthropicApiKey] = anthropic?.apiKey ?? ANTHROPIC_KEY_PLACEHOLDER; + values[ENV_KEYS.anthropicSmallLlmModel] = + anthropic?.smallLlmModel ?? ANTHROPIC_DEFAULT_SMALL_MODEL; + values[ENV_KEYS.anthropicLargeLlmModel] = + anthropic?.largeLlmModel ?? ANTHROPIC_DEFAULT_LARGE_MODEL; + values[ENV_KEYS.deepseekApiKey] = deepseek?.apiKey ?? DEEPSEEK_KEY_PLACEHOLDER; + values[ENV_KEYS.deepseekSmallLlmModel] = + deepseek?.smallLlmModel ?? DEEPSEEK_DEFAULT_SMALL_MODEL; + values[ENV_KEYS.deepseekLargeLlmModel] = + deepseek?.largeLlmModel ?? DEEPSEEK_DEFAULT_LARGE_MODEL; + values[ENV_KEYS.bedrockAccessKeyId] = bedrock?.accessKeyId ?? "your-access-key-id"; + values[ENV_KEYS.bedrockSecretAccessKey] = + bedrock?.secretAccessKey ?? "your-secret-access-key"; + values[ENV_KEYS.bedrockSessionToken] = bedrock?.sessionToken ?? ""; + values[ENV_KEYS.bedrockSmallLlmModel] = + bedrock?.smallLlmModel ?? BEDROCK_DEFAULT_SMALL_MODEL; + values[ENV_KEYS.bedrockLargeLlmModel] = + bedrock?.largeLlmModel ?? BEDROCK_DEFAULT_LARGE_MODEL; + values[ENV_KEYS.bedrockEmbeddingModel] = + bedrock?.embeddingModel ?? BEDROCK_DEFAULT_EMBEDDING_MODEL; +} + +function fallbackEmbeddingDimensions(choices: InitChoices) { + const embeddingModel = resolveActiveEmbeddingModel(choices.models); + const known = lookupEmbeddingDimensions(embeddingModel); + if (known !== undefined) { + return { + nodesDimension: known, + tripletsDimension: known, + observationsDimension: known, + dataDimension: known, + relationshipsDimension: known, + }; + } + if (choices.models.embeddingsProvider === "ollama") { + return OLLAMA_EMBEDDING_DIMENSIONS; + } + return EMBEDDINGS_DEFAULTS; +} + +function applyEmbeddingDefaults( + values: Record, + choices: InitChoices, +): void { + values[ENV_KEYS.embeddingsSmallModel] = EMBEDDINGS_DEFAULTS.smallModel; + values[ENV_KEYS.embeddingsLocalModel] = + values[ENV_KEYS.embeddingsLocalModel] ?? EMBEDDINGS_DEFAULTS.localModel; + + const embeddingDims = choices.models.embeddingDimensions ?? fallbackEmbeddingDimensions(choices); + + values[ENV_KEYS.embeddingNodesDimension] = embeddingDims.nodesDimension; + values[ENV_KEYS.embeddingTripletsDimension] = embeddingDims.tripletsDimension; + values[ENV_KEYS.embeddingObservationsDimension] = + embeddingDims.observationsDimension; + values[ENV_KEYS.embeddingDataDimension] = embeddingDims.dataDimension; + values[ENV_KEYS.embeddingRelationshipsDimension] = + embeddingDims.relationshipsDimension; + + if (choices.models.embeddingsProvider === "azure" && choices.models.azure) { + values[ENV_KEYS.embeddingsFullEndpoint] = + choices.models.azure.embeddingEndpoint; + values[ENV_KEYS.embeddingsKey] = choices.models.azure.embeddingKey; + } else { + values[ENV_KEYS.embeddingsFullEndpoint] = + values[ENV_KEYS.embeddingsFullEndpoint] ?? AZURE_EMBEDDING_PLACEHOLDER; + values[ENV_KEYS.embeddingsKey] = + values[ENV_KEYS.embeddingsKey] ?? AZURE_KEY_PLACEHOLDER; + } +} diff --git a/tui/src/types.ts b/tui/src/types.ts new file mode 100644 index 0000000000000000000000000000000000000000..9d7dabca0cd46b7fb4acb6bc03e19bc78070e355 --- /dev/null +++ b/tui/src/types.ts @@ -0,0 +1,198 @@ +export type GraphDb = "networkx" | "neo4j"; +export type VectorDb = "postgresql" | "milvus"; +export type DataDb = "postgresql" | "mongo"; +export type ModelsMode = "local" | "remote"; +export type ModelProvider = + | "ollama" + | "azure" + | "openai" + | "anthropic" + | "deepseek" + | "gcp_vertex" + | "amazon_bedrock"; +export type OcrMode = "docparser" | "docling"; +export type PipelineMode = "accurate" | "lightweight"; +export type SearchRetrieval = "hybrid" | "dense" | "bm25"; +export type ServicesRuntime = "docker" | "manual"; +export type PluginSource = "local" | "registry"; + +export function isPipelineMode(value: string): value is PipelineMode { + return value === "accurate" || value === "lightweight"; +} + +export interface DbChoices { + vectorDb: VectorDb; + dataDb: DataDb; + graphDb: GraphDb; +} + +export interface OllamaChoices { + host: string; + port: number; + smallModel: string; + largeModel: string; + embeddingsLocalModel: string; +} + +export interface GcpChoices { + credentialsPath: string; + projectId: string; + smallLlmModel: string; + largeLlmModel: string; + embeddingModel: string; +} + +export interface AzureChoices { + smallLlmModel: string; + largeLlmModel: string; + llmApiVersion: string; + llmEndpoint: string; + llmSubscriptionKey: string; + embeddingEndpoint: string; + embeddingKey: string; + embeddingModel: string; +} + +export interface OpenAIChoices { + apiKey: string; + baseUrl?: string; + smallLlmModel: string; + largeLlmModel: string; + embeddingModel: string; +} + +export interface BedrockChoices { + region: string; + accessKeyId: string; + secretAccessKey: string; + sessionToken?: string; + smallLlmModel: string; + largeLlmModel: string; + embeddingModel: string; +} + +export interface AnthropicChoices { + apiKey: string; + smallLlmModel: string; + largeLlmModel: string; +} + +export interface DeepSeekChoices { + apiKey: string; + smallLlmModel: string; + largeLlmModel: string; +} + +export interface EmbeddingDimensions { + nodesDimension: number; + tripletsDimension: number; + observationsDimension: number; + dataDimension: number; + relationshipsDimension: number; +} + +export interface ModelsChoices { + mode: ModelsMode; + llmSmallProvider: ModelProvider; + llmLargeProvider: ModelProvider; + embeddingsProvider: ModelProvider; + embeddingDimensions?: EmbeddingDimensions; + ollama?: OllamaChoices; + gcp?: GcpChoices; + azure?: AzureChoices; + openai?: OpenAIChoices; + anthropic?: AnthropicChoices; + deepseek?: DeepSeekChoices; + bedrock?: BedrockChoices; +} + +export interface PostgresConnection { + host: string; + port: number; + username: string; + password: string; + systemDatabase: string; + maintenanceDatabase: string; +} + +export interface Neo4jConnection { + host: string; + port: number; + username: string; + password: string; +} + +export interface MilvusConnection { + host: string; + port: number; + uri?: string; + token?: string; +} + +export interface MongoConnection { + host: string; + port: number; + username: string; + password: string; +} + +export interface RedisConnection { + host: string; + port: number; +} + +export interface Connections { + redis: RedisConnection; + postgresql?: PostgresConnection; + neo4j?: Neo4jConnection; + milvus?: MilvusConnection; + mongo?: MongoConnection; +} + +export interface AuthChoices { + brainpatToken: string; +} + +export interface PipelineChoices { + ocrMode: OcrMode; +} + +export interface SearchChoices { + enabled: boolean; + retrieval: SearchRetrieval; +} + +export interface PluginChoice { + name: string; + source: PluginSource; + version?: string; + path?: string; +} + +export interface InitChoices { + dbs: DbChoices; + models: ModelsChoices; + pipeline: PipelineChoices; + search: SearchChoices; + connections: Connections; + auth: AuthChoices; + plugins: PluginChoice[]; + servicesRuntime: ServicesRuntime; + usedDefaults: boolean; +} + +export interface InstallState { + cloned: boolean; + venvCreated: boolean; + depsInstalled: boolean; + envWritten: boolean; + containersStarted: boolean; + selectedServices: string[] | null; + installedPlugins: string[] | null; + servicesRuntime: ServicesRuntime | null; + sourcePath: string; + repoUrl: string; + branch: string; +} + +export type Platform = "darwin" | "linux" | "win32"; diff --git a/tui/tsconfig.json b/tui/tsconfig.json new file mode 100644 index 0000000000000000000000000000000000000000..1288bcb5604bd2563e78a91469d9109b02679b80 --- /dev/null +++ b/tui/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "Bundler", + "lib": ["ES2022"], + "outDir": "dist", + "rootDir": "src", + "strict": true, + "noImplicitAny": true, + "noUncheckedIndexedAccess": true, + "exactOptionalPropertyTypes": false, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "resolveJsonModule": true, + "isolatedModules": true, + "forceConsistentCasingInFileNames": true, + "verbatimModuleSyntax": false, + "declaration": false + }, + "include": ["src/**/*.ts"], + "exclude": ["node_modules", "dist"] +} diff --git a/tui/tsup.config.ts b/tui/tsup.config.ts new file mode 100644 index 0000000000000000000000000000000000000000..e2b87d10448207dbf1cea2db37b56b0a2f6c01db --- /dev/null +++ b/tui/tsup.config.ts @@ -0,0 +1,17 @@ +import { defineConfig } from "tsup"; + +export default defineConfig({ + entry: ["src/index.ts"], + format: ["esm"], + target: "node18", + platform: "node", + outDir: "dist", + clean: true, + splitting: false, + shims: true, + sourcemap: false, + minify: false, + banner: { + js: "#!/usr/bin/env node", + }, +}); diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000000000000000000000000000000000000..37a3dbd34d086c6213e8f0b5393da26fdf65e56f --- /dev/null +++ b/uv.lock @@ -0,0 +1,5657 @@ +version = 1 +revision = 3 +requires-python = ">=3.11, <4.0.0" +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'darwin'", + "python_full_version >= '3.14' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version >= '3.14' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.14' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version == '3.13.*' and sys_platform == 'darwin'", + "python_full_version == '3.13.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.13.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version < '3.12' and sys_platform == 'darwin'", + "python_full_version < '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.12' and sys_platform != 'darwin' and sys_platform != 'linux')", +] + +[[package]] +name = "accelerate" +version = "1.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "psutil" }, + { name = "pyyaml" }, + { name = "safetensors" }, + { name = "torch" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8d/75/94cd5d389649578aca399e5aa822637eec18319a1dadc400ffe2f9a7493f/accelerate-1.14.0.tar.gz", hash = "sha256:41b9c4377a54e0b460a959b0defa1b736e4ca0a2373252d9a539964c2afe3c8d", size = 412167, upload-time = "2026-06-11T13:45:52.326Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/db/253133d7e7cb40d3af384bb2f5c0b4a2b7fdcffbc95c688cc67a20a3c103/accelerate-1.14.0-py3-none-any.whl", hash = "sha256:e94390c2863b873be18f623f9df48a0d8fe5eff13ea7f1a00092b0a7904888c6", size = 389246, upload-time = "2026-06-11T13:45:50.477Z" }, +] + +[[package]] +name = "amqp" +version = "5.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "vine" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/79/fc/ec94a357dfc6683d8c86f8b4cfa5416a4c36b28052ec8260c77aca96a443/amqp-5.3.1.tar.gz", hash = "sha256:cddc00c725449522023bad949f70fff7b48f0b1ade74d170a6f10ab044739432", size = 129013, upload-time = "2024-11-12T19:55:44.051Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/99/fc813cd978842c26c82534010ea849eee9ab3a13ea2b74e95cb9c99e747b/amqp-5.3.1-py3-none-any.whl", hash = "sha256:43b3319e1b4e7d1251833a93d672b4af1e40f3d632d479b98661a95f117880a2", size = 50944, upload-time = "2024-11-12T19:55:41.782Z" }, +] + +[[package]] +name = "annotated-doc" +version = "0.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" }, +] + +[[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, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anthropic" +version = "0.57.1" +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/d7/75/6261a1a8d92aed47e27d2fcfb3a411af73b1435e6ae1186da02b760565d0/anthropic-0.57.1.tar.gz", hash = "sha256:7815dd92245a70d21f65f356f33fc80c5072eada87fb49437767ea2918b2c4b0", size = 423775, upload-time = "2025-07-03T16:57:35.932Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/cf/ca0ba77805aec6171629a8b665c7dc224dab374539c3d27005b5d8c100a0/anthropic-0.57.1-py3-none-any.whl", hash = "sha256:33afc1f395af207d07ff1bffc0a3d1caac53c371793792569c5d2f09283ea306", size = 292779, upload-time = "2025-07-03T16:57:34.636Z" }, +] + +[[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, upload-time = "2021-11-06T17:52:23.524Z" } + +[[package]] +name = "anyio" +version = "4.12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" }, +] + +[[package]] +name = "astroid" +version = "4.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/07/63/0adf26577da5eff6eb7a177876c1cfa213856be9926a000f65c4add9692b/astroid-4.0.4.tar.gz", hash = "sha256:986fed8bcf79fb82c78b18a53352a0b287a73817d6dbcfba3162da36667c49a0", size = 406358, upload-time = "2026-02-07T23:35:07.509Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b0/cf/1c5f42b110e57bc5502eb80dbc3b03d256926062519224835ef08134f1f9/astroid-4.0.4-py3-none-any.whl", hash = "sha256:52f39653876c7dec3e3afd4c2696920e05c83832b9737afc21928f2d2eb7a753", size = 276445, upload-time = "2026-02-07T23:35:05.344Z" }, +] + +[[package]] +name = "async-timeout" +version = "5.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a5/ae/136395dfbfe00dfc94da3f3e136d0b13f394cba8f4841120e34226265780/async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3", size = 9274, upload-time = "2024-11-06T16:41:39.6Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/ba/e2081de779ca30d473f21f5b30e0e737c438205440784c7dfc81efc2b029/async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c", size = 6233, upload-time = "2024-11-06T16:41:37.9Z" }, +] + +[[package]] +name = "attrs" +version = "25.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251, upload-time = "2025-10-06T13:54:44.725Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, +] + +[[package]] +name = "beautifulsoup4" +version = "4.14.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "soupsieve" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c3/b0/1c6a16426d389813b48d95e26898aff79abbde42ad353958ad95cc8c9b21/beautifulsoup4-4.14.3.tar.gz", hash = "sha256:6292b1c5186d356bba669ef9f7f051757099565ad9ada5dd630bd9de5fa7fb86", size = 627737, upload-time = "2025-11-30T15:08:26.084Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1a/39/47f9197bdd44df24d67ac8893641e16f386c984a0619ef2ee4c51fbbc019/beautifulsoup4-4.14.3-py3-none-any.whl", hash = "sha256:0918bfe44902e6ad8d57732ba310582e98da931428d231a5ecb9e7c703a735bb", size = 107721, upload-time = "2025-11-30T15:08:24.087Z" }, +] + +[[package]] +name = "billiard" +version = "4.2.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/23/b12ac0bcdfb7360d664f40a00b1bda139cbbbced012c34e375506dbd0143/billiard-4.2.4.tar.gz", hash = "sha256:55f542c371209e03cd5862299b74e52e4fbcba8250ba611ad94276b369b6a85f", size = 156537, upload-time = "2025-11-30T13:28:48.52Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/87/8bab77b323f16d67be364031220069f79159117dd5e43eeb4be2fef1ac9b/billiard-4.2.4-py3-none-any.whl", hash = "sha256:525b42bdec68d2b983347ac312f892db930858495db601b5836ac24e6477cde5", size = 87070, upload-time = "2025-11-30T13:28:47.016Z" }, +] + +[[package]] +name = "blis" +version = "1.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d0/d0/d8cc8c9a4488a787e7fa430f6055e5bd1ddb22c340a751d9e901b82e2efe/blis-1.3.3.tar.gz", hash = "sha256:034d4560ff3cc43e8aa37e188451b0440e3261d989bb8a42ceee865607715ecd", size = 2644873, upload-time = "2025-11-17T12:28:30.511Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a1/0a/a4c8736bc497d386b0ffc76d321f478c03f1a4725e52092f93b38beb3786/blis-1.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e10c8d3e892b1dbdff365b9d00e08291876fc336915bf1a5e9f188ed087e1a91", size = 6925522, upload-time = "2025-11-17T12:27:29.199Z" }, + { url = "https://files.pythonhosted.org/packages/83/5a/3437009282f23684ecd3963a8b034f9307cdd2bf4484972e5a6b096bf9ac/blis-1.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:66e6249564f1db22e8af1e0513ff64134041fa7e03c8dd73df74db3f4d8415a7", size = 1232787, upload-time = "2025-11-17T12:27:30.996Z" }, + { url = "https://files.pythonhosted.org/packages/d1/0e/82221910d16259ce3017c1442c468a3f206a4143a96fbba9f5b5b81d62e8/blis-1.3.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7260da065958b4e5475f62f44895ef9d673b0f47dcf61b672b22b7dae1a18505", size = 2844596, upload-time = "2025-11-17T12:27:32.601Z" }, + { url = "https://files.pythonhosted.org/packages/6c/93/ab547f1a5c23e20bca16fbcf04021c32aac3f969be737ea4980509a7ca90/blis-1.3.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e9327a6ca67de8ae76fe071e8584cc7f3b2e8bfadece4961d40f2826e1cda2df", size = 11377746, upload-time = "2025-11-17T12:27:35.342Z" }, + { url = "https://files.pythonhosted.org/packages/6e/a6/7733820aa62da32526287a63cd85c103b2b323b186c8ee43b7772ff7017c/blis-1.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c4ae70629cf302035d268858a10ca4eb6242a01b2dc8d64422f8e6dcb8a8ee74", size = 3041954, upload-time = "2025-11-17T12:27:37.479Z" }, + { url = "https://files.pythonhosted.org/packages/87/53/e39d67fd3296b649772780ca6aab081412838ecb54e0b0c6432d01626a50/blis-1.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:45866a9027d43b93e8b59980a23c5d7358b6536fc04606286e39fdcfce1101c2", size = 14251222, upload-time = "2025-11-17T12:27:39.705Z" }, + { url = "https://files.pythonhosted.org/packages/ea/44/b749f8777b020b420bceaaf60f66432fc30cc904ca5b69640ec9cbef11ed/blis-1.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:27f82b8633030f8d095d2b412dffa7eb6dbc8ee43813139909a20012e54422ea", size = 6171233, upload-time = "2025-11-17T12:27:41.921Z" }, + { url = "https://files.pythonhosted.org/packages/16/d1/429cf0cf693d4c7dc2efed969bd474e315aab636e4a95f66c4ed7264912d/blis-1.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2a1c74e100665f8e918ebdbae2794576adf1f691680b5cdb8b29578432f623ef", size = 6929663, upload-time = "2025-11-17T12:27:44.482Z" }, + { url = "https://files.pythonhosted.org/packages/11/69/363c8df8d98b3cc97be19aad6aabb2c9c53f372490d79316bdee92d476e7/blis-1.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3f6c595185176ce021316263e1a1d636a3425b6c48366c1fd712d08d0b71849a", size = 1230939, upload-time = "2025-11-17T12:27:46.19Z" }, + { url = "https://files.pythonhosted.org/packages/96/2a/fbf65d906d823d839076c5150a6f8eb5ecbc5f9135e0b6510609bda1e6b7/blis-1.3.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d734b19fba0be7944f272dfa7b443b37c61f9476d9ab054a9ac53555ceadd2e0", size = 2818835, upload-time = "2025-11-17T12:27:48.167Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ad/58deaa3ad856dd3cc96493e40ffd2ed043d18d4d304f85a65cde1ccbf644/blis-1.3.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1ef6d6e2b599a3a2788eb6d9b443533961265aa4ec49d574ed4bb846e548dcdb", size = 11366550, upload-time = "2025-11-17T12:27:49.958Z" }, + { url = "https://files.pythonhosted.org/packages/78/82/816a7adfe1f7acc8151f01ec86ef64467a3c833932d8f19f8e06613b8a4e/blis-1.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8c888438ae99c500422d50698e3028b65caa8ebb44e24204d87fda2df64058f7", size = 3023686, upload-time = "2025-11-17T12:27:52.062Z" }, + { url = "https://files.pythonhosted.org/packages/1e/e2/0e93b865f648b5519360846669a35f28ee8f4e1d93d054f6850d8afbabde/blis-1.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8177879fd3590b5eecdd377f9deafb5dc8af6d684f065bd01553302fb3fcf9a7", size = 14250939, upload-time = "2025-11-17T12:27:53.847Z" }, + { url = "https://files.pythonhosted.org/packages/20/07/fb43edc2ff0a6a367e4a94fc39eb3b85aa1e55e24cc857af2db145ce9f0d/blis-1.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:f20f7ad69aaffd1ce14fe77de557b6df9b61e0c9e582f75a843715d836b5c8af", size = 6192759, upload-time = "2025-11-17T12:27:56.176Z" }, + { url = "https://files.pythonhosted.org/packages/e6/f7/d26e62d9be3d70473a63e0a5d30bae49c2fe138bebac224adddcdef8a7ce/blis-1.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1e647341f958421a86b028a2efe16ce19c67dba2a05f79e8f7e80b1ff45328aa", size = 6928322, upload-time = "2025-11-17T12:27:57.965Z" }, + { url = "https://files.pythonhosted.org/packages/4a/78/750d12da388f714958eb2f2fd177652323bbe7ec528365c37129edd6eb84/blis-1.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d563160f874abb78a57e346f07312c5323f7ad67b6370052b6b17087ef234a8e", size = 1229635, upload-time = "2025-11-17T12:28:00.118Z" }, + { url = "https://files.pythonhosted.org/packages/e8/36/eac4199c5b200a5f3e93cad197da8d26d909f218eb444c4f552647c95240/blis-1.3.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:30b8a5b90cb6cb81d1ada9ae05aa55fb8e70d9a0ae9db40d2401bb9c1c8f14c4", size = 2815650, upload-time = "2025-11-17T12:28:02.544Z" }, + { url = "https://files.pythonhosted.org/packages/bf/51/472e7b36a6bedb5242a9757e7486f702c3619eff76e256735d0c8b1679c6/blis-1.3.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e9f5c53b277f6ac5b3ca30bc12ebab7ea16c8f8c36b14428abb56924213dc127", size = 11359008, upload-time = "2025-11-17T12:28:04.589Z" }, + { url = "https://files.pythonhosted.org/packages/84/da/d0dfb6d6e6321ae44df0321384c32c322bd07b15740d7422727a1a49fc5d/blis-1.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6297e7616c158b305c9a8a4e47ca5fc9b0785194dd96c903b1a1591a7ca21ddf", size = 3011959, upload-time = "2025-11-17T12:28:06.862Z" }, + { url = "https://files.pythonhosted.org/packages/20/c5/2b0b5e556fa0364ed671051ea078a6d6d7b979b1cfef78d64ad3ca5f0c7f/blis-1.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3f966ca74f89f8a33e568b9a1d71992fc9a0d29a423e047f0a212643e21b5458", size = 14232456, upload-time = "2025-11-17T12:28:08.779Z" }, + { url = "https://files.pythonhosted.org/packages/31/07/4cdc81a47bf862c0b06d91f1bc6782064e8b69ac9b5d4ff51d97e4ff03da/blis-1.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:7a0fc4b237a3a453bdc3c7ab48d91439fcd2d013b665c46948d9eaf9c3e45a97", size = 6192624, upload-time = "2025-11-17T12:28:14.197Z" }, + { url = "https://files.pythonhosted.org/packages/5f/8a/80f7c68fbc24a76fc9c18522c46d6d69329c320abb18e26a707a5d874083/blis-1.3.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c3e33cfbf22a418373766816343fcfcd0556012aa3ffdf562c29cddec448a415", size = 6934081, upload-time = "2025-11-17T12:28:16.436Z" }, + { url = "https://files.pythonhosted.org/packages/e5/52/d1aa3a51a7fc299b0c89dcaa971922714f50b1202769eebbdaadd1b5cff7/blis-1.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6f165930e8d3a85c606d2003211497e28d528c7416fbfeafb6b15600963f7c9b", size = 1231486, upload-time = "2025-11-17T12:28:18.008Z" }, + { url = "https://files.pythonhosted.org/packages/99/4f/badc7bd7f74861b26c10123bba7b9d16f99cd9535ad0128780360713820f/blis-1.3.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:878d4d96d8f2c7a2459024f013f2e4e5f46d708b23437dae970d998e7bff14a0", size = 2814944, upload-time = "2025-11-17T12:28:19.654Z" }, + { url = "https://files.pythonhosted.org/packages/72/a6/f62a3bd814ca19ec7e29ac889fd354adea1217df3183e10217de51e2eb8b/blis-1.3.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f36c0ca84a05ee5d3dbaa38056c4423c1fc29948b17a7923dd2fed8967375d74", size = 11345825, upload-time = "2025-11-17T12:28:21.354Z" }, + { url = "https://files.pythonhosted.org/packages/d4/6c/671af79ee42bc4c968cae35c091ac89e8721c795bfa4639100670dc59139/blis-1.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e5a662c48cd4aad5dae1a950345df23957524f071315837a4c6feb7d3b288990", size = 3008771, upload-time = "2025-11-17T12:28:23.637Z" }, + { url = "https://files.pythonhosted.org/packages/be/92/7cd7f8490da7c98ee01557f2105885cc597217b0e7fd2eeb9e22cdd4ef23/blis-1.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9de26fbd72bac900c273b76d46f0b45b77a28eace2e01f6ac6c2239531a413bb", size = 14219213, upload-time = "2025-11-17T12:28:26.143Z" }, + { url = "https://files.pythonhosted.org/packages/0a/de/acae8e9f9a1f4bb393d41c8265898b0f29772e38eac14e9f69d191e2c006/blis-1.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:9e5fdf4211b1972400f8ff6dafe87cb689c5d84f046b4a76b207c0bd2270faaf", size = 6324695, upload-time = "2025-11-17T12:28:28.401Z" }, +] + +[[package]] +name = "boto3" +version = "1.43.18" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore" }, + { name = "jmespath" }, + { name = "s3transfer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/22/37/2ae45d06423182b4561c03bc33494fafa21a0d1e847f0554f590e3cbbc62/boto3-1.43.18.tar.gz", hash = "sha256:33138883e984eb1937d1553da699182c8ad2099138091e885b65c9accbccea16", size = 113154, upload-time = "2026-05-29T19:33:30.046Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/75/fcb2c10d516496536c50e397248de42a673d8ea8137caf5b578a72b11293/boto3-1.43.18-py3-none-any.whl", hash = "sha256:7b62ce5c0a51428d692aa4f2adc9dc2a4a4c2989bf65a0a12834eeffa99b0b84", size = 140538, upload-time = "2026-05-29T19:33:27.131Z" }, +] + +[[package]] +name = "botocore" +version = "1.43.18" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jmespath" }, + { name = "python-dateutil" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/01/6d/436d69ec484ffc43635b38d7fb7d717d38824671f10e12e77924019ca929/botocore-1.43.18.tar.gz", hash = "sha256:dc8c105351b49688c667065cd5a45fc5b9db982657cefc9e3fbfb9417a55c7df", size = 15424886, upload-time = "2026-05-29T19:33:16.251Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/21/5a/35c92c0af1514581031fe66c398b622176b3c928a6d5cf8133c7207e3bd7/botocore-1.43.18-py3-none-any.whl", hash = "sha256:e2610fce16df9f89deab5f3c163430a814e6804034eb95bef8957c8db60b7dbc", size = 15106258, upload-time = "2026-05-29T19:33:11.18Z" }, +] + +[[package]] +name = "bottleneck" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/14/d8/6d641573e210768816023a64966d66463f2ce9fc9945fa03290c8a18f87c/bottleneck-1.6.0.tar.gz", hash = "sha256:028d46ee4b025ad9ab4d79924113816f825f62b17b87c9e1d0d8ce144a4a0e31", size = 104311, upload-time = "2025-09-08T16:30:38.617Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/96/9d51012d729f97de1e75aad986f3ba50956742a40fc99cbab4c2aa896c1c/bottleneck-1.6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:69ef4514782afe39db2497aaea93b1c167ab7ab3bc5e3930500ef9cf11841db7", size = 100400, upload-time = "2025-09-08T16:29:44.464Z" }, + { url = "https://files.pythonhosted.org/packages/16/f4/4fcbebcbc42376a77e395a6838575950587e5eb82edf47d103f8daa7ba22/bottleneck-1.6.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:727363f99edc6dc83d52ed28224d4cb858c07a01c336c7499c0c2e5dd4fd3e4a", size = 375920, upload-time = "2025-09-08T16:29:45.52Z" }, + { url = "https://files.pythonhosted.org/packages/36/13/7fa8cdc41cbf2dfe0540f98e1e0caf9ffbd681b1a0fc679a91c2698adaf9/bottleneck-1.6.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:847671a9e392220d1dfd2ff2524b4d61ec47b2a36ea78e169d2aa357fd9d933a", size = 367922, upload-time = "2025-09-08T16:29:46.743Z" }, + { url = "https://files.pythonhosted.org/packages/13/7d/dccfa4a2792c1bdc0efdde8267e527727e517df1ff0d4976b84e0268c2f9/bottleneck-1.6.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:daef2603ab7b4ec4f032bb54facf5fa92dacd3a264c2fd9677c9fc22bcb5a245", size = 361379, upload-time = "2025-09-08T16:29:48.042Z" }, + { url = "https://files.pythonhosted.org/packages/93/42/21c0fad823b71c3a8904cbb847ad45136d25573a2d001a9cff48d3985fab/bottleneck-1.6.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:fc7f09bda980d967f2e9f1a746eda57479f824f66de0b92b9835c431a8c922d4", size = 371911, upload-time = "2025-09-08T16:29:49.366Z" }, + { url = "https://files.pythonhosted.org/packages/3b/b0/830ff80f8c74577d53034c494639eac7a0ffc70935c01ceadfbe77f590c2/bottleneck-1.6.0-cp311-cp311-win32.whl", hash = "sha256:1f78bad13ad190180f73cceb92d22f4101bde3d768f4647030089f704ae7cac7", size = 107831, upload-time = "2025-09-08T16:29:51.397Z" }, + { url = "https://files.pythonhosted.org/packages/6f/42/01d4920b0aa51fba503f112c90714547609bbe17b6ecfc1c7ae1da3183df/bottleneck-1.6.0-cp311-cp311-win_amd64.whl", hash = "sha256:8f2adef59fdb9edf2983fe3a4c07e5d1b677c43e5669f4711da2c3daad8321ad", size = 113358, upload-time = "2025-09-08T16:29:52.602Z" }, + { url = "https://files.pythonhosted.org/packages/8d/72/7e3593a2a3dd69ec831a9981a7b1443647acb66a5aec34c1620a5f7f8498/bottleneck-1.6.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3bb16a16a86a655fdbb34df672109a8a227bb5f9c9cf5bb8ae400a639bc52fa3", size = 100515, upload-time = "2025-09-08T16:29:55.141Z" }, + { url = "https://files.pythonhosted.org/packages/b5/d4/e7bbea08f4c0f0bab819d38c1a613da5f194fba7b19aae3e2b3a27e78886/bottleneck-1.6.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0fbf5d0787af9aee6cef4db9cdd14975ce24bd02e0cc30155a51411ebe2ff35f", size = 377451, upload-time = "2025-09-08T16:29:56.718Z" }, + { url = "https://files.pythonhosted.org/packages/fe/80/a6da430e3b1a12fd85f9fe90d3ad8fe9a527ecb046644c37b4b3f4baacfc/bottleneck-1.6.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d08966f4a22384862258940346a72087a6f7cebb19038fbf3a3f6690ee7fd39f", size = 368303, upload-time = "2025-09-08T16:29:57.834Z" }, + { url = "https://files.pythonhosted.org/packages/30/11/abd30a49f3251f4538430e5f876df96f2b39dabf49e05c5836820d2c31fe/bottleneck-1.6.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:604f0b898b43b7bc631c564630e936a8759d2d952641c8b02f71e31dbcd9deaa", size = 361232, upload-time = "2025-09-08T16:29:59.104Z" }, + { url = "https://files.pythonhosted.org/packages/1d/ac/1c0e09d8d92b9951f675bd42463ce76c3c3657b31c5bf53ca1f6dd9eccff/bottleneck-1.6.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d33720bad761e642abc18eda5f188ff2841191c9f63f9d0c052245decc0faeb9", size = 373234, upload-time = "2025-09-08T16:30:00.488Z" }, + { url = "https://files.pythonhosted.org/packages/fb/ea/382c572ae3057ba885d484726bb63629d1f63abedf91c6cd23974eb35a9b/bottleneck-1.6.0-cp312-cp312-win32.whl", hash = "sha256:a1e5907ec2714efbe7075d9207b58c22ab6984a59102e4ecd78dced80dab8374", size = 108020, upload-time = "2025-09-08T16:30:01.773Z" }, + { url = "https://files.pythonhosted.org/packages/48/ad/d71da675eef85ac153eef5111ca0caa924548c9591da00939bcabba8de8e/bottleneck-1.6.0-cp312-cp312-win_amd64.whl", hash = "sha256:81e3822499f057a917b7d3972ebc631ac63c6bbcc79ad3542a66c4c40634e3a6", size = 113493, upload-time = "2025-09-08T16:30:02.872Z" }, + { url = "https://files.pythonhosted.org/packages/97/1a/e117cd5ff7056126d3291deb29ac8066476e60b852555b95beb3fc9d62a0/bottleneck-1.6.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d015de414ca016ebe56440bdf5d3d1204085080527a3c51f5b7b7a3e704fe6fd", size = 100521, upload-time = "2025-09-08T16:30:03.89Z" }, + { url = "https://files.pythonhosted.org/packages/bd/22/05555a9752357e24caa1cd92324d1a7fdde6386aab162fcc451f8f8eedc2/bottleneck-1.6.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:456757c9525b0b12356f472e38020ed4b76b18375fd76e055f8d33fb62956f5e", size = 377719, upload-time = "2025-09-08T16:30:05.135Z" }, + { url = "https://files.pythonhosted.org/packages/11/ee/76593af47097d9633109bed04dbcf2170707dd84313ca29f436f9234bc51/bottleneck-1.6.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c65254d51b6063c55f6272f175e867e2078342ae75f74be29d6612e9627b2c0", size = 368577, upload-time = "2025-09-08T16:30:06.387Z" }, + { url = "https://files.pythonhosted.org/packages/f9/f7/4dcacaf637d2b8d89ea746c74159adda43858d47358978880614c3fa4391/bottleneck-1.6.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a172322895fbb79c6127474f1b0db0866895f0b804a18d5c6b841fea093927fe", size = 361441, upload-time = "2025-09-08T16:30:07.613Z" }, + { url = "https://files.pythonhosted.org/packages/05/34/21eb1eb1c42cb7be2872d0647c292fc75768d14e1f0db66bf907b24b2464/bottleneck-1.6.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d5e81b642eb0d5a5bf00312598d7ed142d389728b694322a118c26813f3d1fa9", size = 373416, upload-time = "2025-09-08T16:30:08.899Z" }, + { url = "https://files.pythonhosted.org/packages/48/cb/7957ff40367a151139b5f1854616bf92e578f10804d226fbcdecfd73aead/bottleneck-1.6.0-cp313-cp313-win32.whl", hash = "sha256:543d3a89d22880cd322e44caff859af6c0489657bf9897977d1f5d3d3f77299c", size = 108029, upload-time = "2025-09-08T16:30:09.909Z" }, + { url = "https://files.pythonhosted.org/packages/90/a8/735df4156fa5595501d5d96a6ee102f49c13d2ce9e2a287ad51806bc3ba0/bottleneck-1.6.0-cp313-cp313-win_amd64.whl", hash = "sha256:48a44307d604ceb81e256903e5d57d3adb96a461b1d3c6a69baa2c67e823bd36", size = 113497, upload-time = "2025-09-08T16:30:10.82Z" }, + { url = "https://files.pythonhosted.org/packages/c7/5c/8c1260df8ade7cebc2a8af513a27082b5e36aa4a5fb762d56ea6d969d893/bottleneck-1.6.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:547e6715115867c4657c9ae8cc5ddac1fec8fdad66690be3a322a7488721b06b", size = 101606, upload-time = "2025-09-08T16:30:11.935Z" }, + { url = "https://files.pythonhosted.org/packages/ce/ea/f03e2944e91ee962922c834ed21e5be6d067c8395681f5dc6c67a0a26853/bottleneck-1.6.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5e4a4a6e05b6f014c307969129e10d1a0afd18f3a2c127b085532a4a76677aef", size = 391804, upload-time = "2025-09-08T16:30:13.13Z" }, + { url = "https://files.pythonhosted.org/packages/0b/58/2b356b8a81eb97637dccee6cf58237198dd828890e38be9afb4e5e58e38e/bottleneck-1.6.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2baae0d1589b4a520b2f9cf03528c0c8b20717b3f05675e212ec2200cf628f12", size = 383443, upload-time = "2025-09-08T16:30:14.318Z" }, + { url = "https://files.pythonhosted.org/packages/55/52/cf7d09ed3736ad0d50c624787f9b580ae3206494d95cc0f4814b93eef728/bottleneck-1.6.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2e407139b322f01d8d5b6b2e8091b810f48a25c7fa5c678cfcdc420dfe8aea0a", size = 375458, upload-time = "2025-09-08T16:30:15.379Z" }, + { url = "https://files.pythonhosted.org/packages/c4/e9/7c87a34a24e339860064f20fac49f6738e94f1717bc8726b9c47705601d8/bottleneck-1.6.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:1adefb89b92aba6de9c6ea871d99bcd29d519f4fb012cc5197917813b4fc2c7f", size = 386384, upload-time = "2025-09-08T16:30:17.012Z" }, + { url = "https://files.pythonhosted.org/packages/59/57/db51855e18a47671801180be748939b4c9422a0544849af1919116346b5f/bottleneck-1.6.0-cp313-cp313t-win32.whl", hash = "sha256:64b8690393494074923780f6abdf5f5577d844b9d9689725d1575a936e74e5f0", size = 109448, upload-time = "2025-09-08T16:30:18.076Z" }, + { url = "https://files.pythonhosted.org/packages/bd/1e/683c090b624f13a5bf88a0be2241dc301e98b2fb72a45812a7ae6e456cc4/bottleneck-1.6.0-cp313-cp313t-win_amd64.whl", hash = "sha256:cb67247f65dcdf62af947c76c6c8b77d9f0ead442cac0edbaa17850d6da4e48d", size = 115190, upload-time = "2025-09-08T16:30:19.106Z" }, + { url = "https://files.pythonhosted.org/packages/77/e2/eb7c08964a3f3c4719f98795ccd21807ee9dd3071a0f9ad652a5f19196ff/bottleneck-1.6.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:98f1d789042511a0f042b3bdcd2903e8567e956d3aa3be189cce3746daeb8550", size = 100544, upload-time = "2025-09-08T16:30:20.22Z" }, + { url = "https://files.pythonhosted.org/packages/99/ec/c6f3be848f37689f481797ce7d9807d5f69a199d7fc0e46044f9b708c468/bottleneck-1.6.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1fad24c99e39ad7623fc2a76d37feb26bd32e4dd170885edf4dbf4bfce2199a3", size = 378315, upload-time = "2025-09-08T16:30:21.409Z" }, + { url = "https://files.pythonhosted.org/packages/bf/8f/2d6600836e2ea8f14fcefac592dc83497e5b88d381470c958cb9cdf88706/bottleneck-1.6.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:643e61e50a6f993debc399b495a1609a55b3bd76b057e433e4089505d9f605c7", size = 368978, upload-time = "2025-09-08T16:30:23.458Z" }, + { url = "https://files.pythonhosted.org/packages/9b/b5/bf72b49f5040212873b985feef5050015645e0a02204b591e1d265fc522a/bottleneck-1.6.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa668efbe4c6b200524ea0ebd537212da9b9801287138016fdf64119d6fcf201", size = 362074, upload-time = "2025-09-08T16:30:24.71Z" }, + { url = "https://files.pythonhosted.org/packages/1d/c8/c4891a0604eb680031390182c6e264247e3a9a8d067d654362245396fadf/bottleneck-1.6.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9f7dd35262e89e28fedd79d45022394b1fa1aceb61d2e747c6d6842e50546daa", size = 374019, upload-time = "2025-09-08T16:30:26.438Z" }, + { url = "https://files.pythonhosted.org/packages/e6/2d/ed096f8d1b9147e84914045dd89bc64e3c32eee49b862d1e20d573a9ab0d/bottleneck-1.6.0-cp314-cp314-win32.whl", hash = "sha256:bd90bec3c470b7fdfafc2fbdcd7a1c55a4e57b5cdad88d40eea5bc9bab759bf1", size = 110173, upload-time = "2025-09-08T16:30:27.521Z" }, + { url = "https://files.pythonhosted.org/packages/33/70/1414acb6ae378a15063cfb19a0a39d69d1b6baae1120a64d2b069902549b/bottleneck-1.6.0-cp314-cp314-win_amd64.whl", hash = "sha256:b43b6d36a62ffdedc6368cf9a708e4d0a30d98656c2b5f33d88894e1bcfd6857", size = 115899, upload-time = "2025-09-08T16:30:28.524Z" }, + { url = "https://files.pythonhosted.org/packages/4e/ed/4570b5d8c1c85ce3c54963ebc37472231ed54f0b0d8dbb5dde14303f775f/bottleneck-1.6.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:53296707a8e195b5dcaa804b714bd222b5e446bd93cd496008122277eb43fa87", size = 101615, upload-time = "2025-09-08T16:30:29.556Z" }, + { url = "https://files.pythonhosted.org/packages/2d/93/c148faa07ae91f266be1f3fad1fde95aa2449e12937f3f3df2dd720b86e0/bottleneck-1.6.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d6df19cc48a83efd70f6d6874332aa31c3f5ca06a98b782449064abbd564cf0e", size = 392411, upload-time = "2025-09-08T16:30:31.186Z" }, + { url = "https://files.pythonhosted.org/packages/6e/1c/e6ad221d345a059e7efb2ad1d46a22d9fdae0486faef70555766e1123966/bottleneck-1.6.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96bb3a52cb3c0aadfedce3106f93ab940a49c9d35cd4ed612e031f6deb27e80f", size = 384022, upload-time = "2025-09-08T16:30:32.364Z" }, + { url = "https://files.pythonhosted.org/packages/4f/40/5b15c01eb8c59d59bc84c94d01d3d30797c961f10ec190f53c27e05d62ab/bottleneck-1.6.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d1db9e831b69d5595b12e79aeb04cb02873db35576467c8dd26cdc1ee6b74581", size = 376004, upload-time = "2025-09-08T16:30:33.731Z" }, + { url = "https://files.pythonhosted.org/packages/74/f6/cb228f5949553a5c01d1d5a3c933f0216d78540d9e0bf8dd4343bb449681/bottleneck-1.6.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:4dd7ac619570865fcb7a0e8925df418005f076286ad2c702dd0f447231d7a055", size = 386909, upload-time = "2025-09-08T16:30:34.973Z" }, + { url = "https://files.pythonhosted.org/packages/09/9a/425065c37a67a9120bf53290371579b83d05bf46f3212cce65d8c01d470a/bottleneck-1.6.0-cp314-cp314t-win32.whl", hash = "sha256:7fb694165df95d428fe00b98b9ea7d126ef786c4a4b7d43ae2530248396cadcb", size = 111636, upload-time = "2025-09-08T16:30:36.044Z" }, + { url = "https://files.pythonhosted.org/packages/ad/23/c41006e42909ec5114a8961818412310aa54646d1eae0495dbff3598a095/bottleneck-1.6.0-cp314-cp314t-win_amd64.whl", hash = "sha256:174b80930ce82bd8456c67f1abb28a5975c68db49d254783ce2cb6983b4fea40", size = 117611, upload-time = "2025-09-08T16:30:37.055Z" }, +] + +[[package]] +name = "brainapi2" +version = "2.19.0.dev0" +source = { editable = "." } +dependencies = [ + { name = "anthropic" }, + { name = "boto3" }, + { name = "celery" }, + { name = "cpp-chunker" }, + { name = "fastapi" }, + { name = "google-genai" }, + { name = "kombu" }, + { name = "langchain" }, + { name = "langchain-aws" }, + { name = "langchain-core" }, + { name = "langchain-google-vertexai" }, + { name = "langchain-openai" }, + { name = "langid" }, + { name = "levenshtein" }, + { name = "mcp" }, + { name = "neo4j" }, + { name = "networkx" }, + { name = "numpy" }, + { name = "openai" }, + { name = "pgvector" }, + { name = "pillow" }, + { name = "psycopg2-binary" }, + { name = "pylint" }, + { name = "pymilvus" }, + { name = "pymongo" }, + { name = "python-dotenv" }, + { name = "python-multipart" }, + { name = "redis" }, + { name = "spacy" }, + { name = "spacy-langdetect" }, + { name = "sympy" }, + { name = "tenacity" }, + { name = "tiktoken" }, + { name = "typer" }, + { name = "uvicorn" }, + { name = "watchfiles" }, +] + +[package.optional-dependencies] +docling-ocr = [ + { name = "accelerate" }, + { name = "docling" }, +] +local-ml = [ + { name = "sentence-transformers" }, +] +postgresql-backend = [ + { name = "networkx" }, + { name = "pgvector" }, + { name = "psycopg2-binary" }, +] + +[package.metadata] +requires-dist = [ + { name = "accelerate", marker = "extra == 'docling-ocr'", specifier = ">=1.14.0,<2.0.0" }, + { name = "anthropic", specifier = ">=0.57.1,<0.58.0" }, + { name = "boto3", specifier = ">=1.40.19,<2.0.0" }, + { name = "celery", specifier = ">=5.5.3,<6.0.0" }, + { name = "cpp-chunker", specifier = ">=0.1.4,<0.2.0" }, + { name = "docling", marker = "extra == 'docling-ocr'", specifier = ">=2.121.0,<3.0.0" }, + { name = "fastapi", specifier = ">=0.141.1,<0.142.0" }, + { name = "google-genai", specifier = ">=1.46.0,<2.0.0" }, + { name = "kombu", extras = ["rabbitmq"], specifier = ">=5.3.0,<6.0.0" }, + { name = "langchain", specifier = ">=1.3.16,<2.0.0" }, + { name = "langchain-aws", specifier = ">=1.0.0,<2.0.0" }, + { name = "langchain-core", specifier = ">=1.6.0,<2.0.0" }, + { name = "langchain-google-vertexai", specifier = ">=3.0.1,<4.0.0" }, + { name = "langchain-openai", specifier = ">=1.6.0,<2.0.0" }, + { name = "langid", specifier = ">=1.1.6,<2.0.0" }, + { name = "levenshtein", specifier = ">=0.27.1,<0.28.0" }, + { name = "mcp", specifier = ">=1.28.1,<2.0.0" }, + { name = "neo4j", specifier = ">=6.0.2,<7.0.0" }, + { name = "networkx", specifier = ">=3.0,<4.0.0" }, + { name = "networkx", marker = "extra == 'postgresql-backend'", specifier = ">=3.0,<4.0.0" }, + { name = "numpy", specifier = ">=1.26.0,<2.0.0" }, + { name = "openai", specifier = ">=2.5.0,<3.0.0" }, + { name = "pgvector", specifier = ">=0.3.0,<1.0.0" }, + { name = "pgvector", marker = "extra == 'postgresql-backend'", specifier = ">=0.3.0,<1.0.0" }, + { name = "pillow", specifier = ">=12.3.0,<13.0.0" }, + { name = "psycopg2-binary", specifier = ">=2.9.6,<3.0.0" }, + { name = "psycopg2-binary", marker = "extra == 'postgresql-backend'", specifier = ">=2.9.6,<3.0.0" }, + { name = "pylint", specifier = ">=4.0.1,<5.0.0" }, + { name = "pymilvus", specifier = ">=2.6.2,<3.0.0" }, + { name = "pymongo", specifier = ">=4.15.3,<5.0.0" }, + { name = "python-dotenv", specifier = ">=1.2.3,<2.0.0" }, + { name = "python-multipart", specifier = ">=0.0.32,<0.1.0" }, + { name = "redis", specifier = ">=6.4.0,<7.0.0" }, + { name = "sentence-transformers", marker = "extra == 'local-ml'", specifier = ">=6.0.0,<7.0.0" }, + { name = "spacy", specifier = ">=3.0.0,<4.0.0" }, + { name = "spacy-langdetect", specifier = ">=0.1.2,<0.2.0" }, + { name = "sympy", specifier = ">=1.12.1,<1.14.0" }, + { name = "tenacity", specifier = ">=8.2.3,<10.0.0" }, + { name = "tiktoken", specifier = ">=0.12.0,<1.0.0" }, + { name = "typer", specifier = ">=0.26.0,<0.27.0" }, + { name = "uvicorn", specifier = ">=0.52.4,<0.53.0" }, + { name = "watchfiles", specifier = ">=1.2.0,<2.0.0" }, +] +provides-extras = ["docling-ocr", "local-ml", "postgresql-backend"] + +[[package]] +name = "cachetools" +version = "7.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/cc/eb3fd22f3b96b8b70ce456d0854ef08434e5ca79c02bf8db3fc07ccfca87/cachetools-7.0.4.tar.gz", hash = "sha256:7042c0e4eea87812f04744ce6ee9ed3de457875eb1f82d8a206c46d6e48b6734", size = 37379, upload-time = "2026-03-08T21:37:17.133Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/bc/72adfb3f2ed19eb0317f89ea9b1eeccc670ae46bc394ec2c4ba1dd8c22b7/cachetools-7.0.4-py3-none-any.whl", hash = "sha256:0c8bb1b9ec8194fa4d764accfde602dfe52f70d0f311e62792d4c3f8c051b1e9", size = 13900, upload-time = "2026-03-08T21:37:15.805Z" }, +] + +[[package]] +name = "catalogue" +version = "2.0.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/38/b4/244d58127e1cdf04cf2dc7d9566f0d24ef01d5ce21811bab088ecc62b5ea/catalogue-2.0.10.tar.gz", hash = "sha256:4f56daa940913d3f09d589c191c74e5a6d51762b3a9e37dd53b7437afd6cda15", size = 19561, upload-time = "2023-09-25T06:29:24.962Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/96/d32b941a501ab566a16358d68b6eb4e4acc373fab3c3c4d7d9e649f7b4bb/catalogue-2.0.10-py3-none-any.whl", hash = "sha256:58c2de0020aa90f4a2da7dfad161bf7b3b054c86a5f09fcedc0b2b740c109a9f", size = 17325, upload-time = "2023-09-25T06:29:23.337Z" }, +] + +[[package]] +name = "celery" +version = "5.6.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "billiard" }, + { name = "click" }, + { name = "click-didyoumean" }, + { name = "click-plugins" }, + { name = "click-repl" }, + { name = "kombu" }, + { name = "python-dateutil" }, + { name = "tzlocal" }, + { name = "vine" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8f/9d/3d13596519cfa7207a6f9834f4b082554845eb3cd2684b5f8535d50c7c44/celery-5.6.2.tar.gz", hash = "sha256:4a8921c3fcf2ad76317d3b29020772103581ed2454c4c042cc55dcc43585009b", size = 1718802, upload-time = "2026-01-04T12:35:58.012Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dd/bd/9ecd619e456ae4ba73b6583cc313f26152afae13e9a82ac4fe7f8856bfd1/celery-5.6.2-py3-none-any.whl", hash = "sha256:3ffafacbe056951b629c7abcf9064c4a2366de0bdfc9fdba421b97ebb68619a5", size = 445502, upload-time = "2026-01-04T12:35:55.894Z" }, +] + +[[package]] +name = "certifi" +version = "2026.2.25" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" }, +] + +[[package]] +name = "cffi" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" }, + { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" }, + { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" }, + { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" }, + { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" }, + { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" }, + { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" }, + { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" }, + { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" }, + { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" }, + { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, + { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, + { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, + { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, + { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, + { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, + { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, + { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, + { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, + { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, + { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, + { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, + { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, + { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, + { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, + { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, + { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, + { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, + { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, + { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, + { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, + { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, + { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, + { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, + { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, + { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, + { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, + { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, + { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, + { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/35/02daf95b9cd686320bb622eb148792655c9412dbb9b67abb5694e5910a24/charset_normalizer-3.4.5.tar.gz", hash = "sha256:95adae7b6c42a6c5b5b559b1a99149f090a57128155daeea91732c8d970d8644", size = 134804, upload-time = "2026-03-06T06:03:19.46Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/9e/bcec3b22c64ecec47d39bf5167c2613efd41898c019dccd4183f6aa5d6a7/charset_normalizer-3.4.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:610f72c0ee565dfb8ae1241b666119582fdbfe7c0975c175be719f940e110694", size = 279531, upload-time = "2026-03-06T06:00:52.252Z" }, + { url = "https://files.pythonhosted.org/packages/58/12/81fd25f7e7078ab5d1eedbb0fac44be4904ae3370a3bf4533c8f2d159acd/charset_normalizer-3.4.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60d68e820af339df4ae8358c7a2e7596badeb61e544438e489035f9fbf3246a5", size = 188006, upload-time = "2026-03-06T06:00:53.8Z" }, + { url = "https://files.pythonhosted.org/packages/ae/6e/f2d30e8c27c1b0736a6520311982cf5286cfc7f6cac77d7bc1325e3a23f2/charset_normalizer-3.4.5-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:10b473fc8dca1c3ad8559985794815f06ca3fc71942c969129070f2c3cdf7281", size = 205085, upload-time = "2026-03-06T06:00:55.311Z" }, + { url = "https://files.pythonhosted.org/packages/d0/90/d12cefcb53b5931e2cf792a33718d7126efb116a320eaa0742c7059a95e4/charset_normalizer-3.4.5-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d4eb8ac7469b2a5d64b5b8c04f84d8bf3ad340f4514b98523805cbf46e3b3923", size = 200545, upload-time = "2026-03-06T06:00:56.532Z" }, + { url = "https://files.pythonhosted.org/packages/03/f4/44d3b830a20e89ff82a3134912d9a1cf6084d64f3b95dcad40f74449a654/charset_normalizer-3.4.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bcb3227c3d9aaf73eaaab1db7ccd80a8995c509ee9941e2aae060ca6e4e5d81", size = 193863, upload-time = "2026-03-06T06:00:57.823Z" }, + { url = "https://files.pythonhosted.org/packages/25/4b/f212119c18a6320a9d4a730d1b4057875cdeabf21b3614f76549042ef8a8/charset_normalizer-3.4.5-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:75ee9c1cce2911581a70a3c0919d8bccf5b1cbc9b0e5171400ec736b4b569497", size = 181827, upload-time = "2026-03-06T06:00:59.323Z" }, + { url = "https://files.pythonhosted.org/packages/74/00/b26158e48b425a202a92965f8069e8a63d9af1481dfa206825d7f74d2a3c/charset_normalizer-3.4.5-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1d1401945cb77787dbd3af2446ff2d75912327c4c3a1526ab7955ecf8600687c", size = 191085, upload-time = "2026-03-06T06:01:00.546Z" }, + { url = "https://files.pythonhosted.org/packages/c4/c2/1c1737bf6fd40335fe53d28fe49afd99ee4143cc57a845e99635ce0b9b6d/charset_normalizer-3.4.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a45e504f5e1be0bd385935a8e1507c442349ca36f511a47057a71c9d1d6ea9e", size = 190688, upload-time = "2026-03-06T06:01:02.479Z" }, + { url = "https://files.pythonhosted.org/packages/5a/3d/abb5c22dc2ef493cd56522f811246a63c5427c08f3e3e50ab663de27fcf4/charset_normalizer-3.4.5-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:e09f671a54ce70b79a1fc1dc6da3072b7ef7251fadb894ed92d9aa8218465a5f", size = 183077, upload-time = "2026-03-06T06:01:04.231Z" }, + { url = "https://files.pythonhosted.org/packages/44/33/5298ad4d419a58e25b3508e87f2758d1442ff00c2471f8e0403dab8edad5/charset_normalizer-3.4.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:d01de5e768328646e6a3fa9e562706f8f6641708c115c62588aef2b941a4f88e", size = 206706, upload-time = "2026-03-06T06:01:05.773Z" }, + { url = "https://files.pythonhosted.org/packages/7b/17/51e7895ac0f87c3b91d276a449ef09f5532a7529818f59646d7a55089432/charset_normalizer-3.4.5-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:131716d6786ad5e3dc542f5cc6f397ba3339dc0fb87f87ac30e550e8987756af", size = 191665, upload-time = "2026-03-06T06:01:07.473Z" }, + { url = "https://files.pythonhosted.org/packages/90/8f/cce9adf1883e98906dbae380d769b4852bb0fa0004bc7d7a2243418d3ea8/charset_normalizer-3.4.5-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:1a374cc0b88aa710e8865dc1bd6edb3743c59f27830f0293ab101e4cf3ce9f85", size = 201950, upload-time = "2026-03-06T06:01:08.973Z" }, + { url = "https://files.pythonhosted.org/packages/08/ca/bce99cd5c397a52919e2769d126723f27a4c037130374c051c00470bcd38/charset_normalizer-3.4.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d31f0d1671e1534e395f9eb84a68e0fb670e1edb1fe819a9d7f564ae3bc4e53f", size = 195830, upload-time = "2026-03-06T06:01:10.155Z" }, + { url = "https://files.pythonhosted.org/packages/87/4f/2e3d023a06911f1281f97b8f036edc9872167036ca6f55cc874a0be6c12c/charset_normalizer-3.4.5-cp311-cp311-win32.whl", hash = "sha256:cace89841c0599d736d3d74a27bc5821288bb47c5441923277afc6059d7fbcb4", size = 132029, upload-time = "2026-03-06T06:01:11.706Z" }, + { url = "https://files.pythonhosted.org/packages/fe/1f/a853b73d386521fd44b7f67ded6b17b7b2367067d9106a5c4b44f9a34274/charset_normalizer-3.4.5-cp311-cp311-win_amd64.whl", hash = "sha256:f8102ae93c0bc863b1d41ea0f4499c20a83229f52ed870850892df555187154a", size = 142404, upload-time = "2026-03-06T06:01:12.865Z" }, + { url = "https://files.pythonhosted.org/packages/b4/10/dba36f76b71c38e9d391abe0fd8a5b818790e053c431adecfc98c35cd2a9/charset_normalizer-3.4.5-cp311-cp311-win_arm64.whl", hash = "sha256:ed98364e1c262cf5f9363c3eca8c2df37024f52a8fa1180a3610014f26eac51c", size = 132796, upload-time = "2026-03-06T06:01:14.106Z" }, + { url = "https://files.pythonhosted.org/packages/9c/b6/9ee9c1a608916ca5feae81a344dffbaa53b26b90be58cc2159e3332d44ec/charset_normalizer-3.4.5-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ed97c282ee4f994ef814042423a529df9497e3c666dca19be1d4cd1129dc7ade", size = 280976, upload-time = "2026-03-06T06:01:15.276Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d8/a54f7c0b96f1df3563e9190f04daf981e365a9b397eedfdfb5dbef7e5c6c/charset_normalizer-3.4.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0294916d6ccf2d069727d65973c3a1ca477d68708db25fd758dd28b0827cff54", size = 189356, upload-time = "2026-03-06T06:01:16.511Z" }, + { url = "https://files.pythonhosted.org/packages/42/69/2bf7f76ce1446759a5787cb87d38f6a61eb47dbbdf035cfebf6347292a65/charset_normalizer-3.4.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:dc57a0baa3eeedd99fafaef7511b5a6ef4581494e8168ee086031744e2679467", size = 206369, upload-time = "2026-03-06T06:01:17.853Z" }, + { url = "https://files.pythonhosted.org/packages/10/9c/949d1a46dab56b959d9a87272482195f1840b515a3380e39986989a893ae/charset_normalizer-3.4.5-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ed1a9a204f317ef879b32f9af507d47e49cd5e7f8e8d5d96358c98373314fc60", size = 203285, upload-time = "2026-03-06T06:01:19.473Z" }, + { url = "https://files.pythonhosted.org/packages/67/5c/ae30362a88b4da237d71ea214a8c7eb915db3eec941adda511729ac25fa2/charset_normalizer-3.4.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7ad83b8f9379176c841f8865884f3514d905bcd2a9a3b210eaa446e7d2223e4d", size = 196274, upload-time = "2026-03-06T06:01:20.728Z" }, + { url = "https://files.pythonhosted.org/packages/b2/07/c9f2cb0e46cb6d64fdcc4f95953747b843bb2181bda678dc4e699b8f0f9a/charset_normalizer-3.4.5-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:a118e2e0b5ae6b0120d5efa5f866e58f2bb826067a646431da4d6a2bdae7950e", size = 184715, upload-time = "2026-03-06T06:01:22.194Z" }, + { url = "https://files.pythonhosted.org/packages/36/64/6b0ca95c44fddf692cd06d642b28f63009d0ce325fad6e9b2b4d0ef86a52/charset_normalizer-3.4.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:754f96058e61a5e22e91483f823e07df16416ce76afa4ebf306f8e1d1296d43f", size = 193426, upload-time = "2026-03-06T06:01:23.795Z" }, + { url = "https://files.pythonhosted.org/packages/50/bc/a730690d726403743795ca3f5bb2baf67838c5fea78236098f324b965e40/charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0c300cefd9b0970381a46394902cd18eaf2aa00163f999590ace991989dcd0fc", size = 191780, upload-time = "2026-03-06T06:01:25.053Z" }, + { url = "https://files.pythonhosted.org/packages/97/4f/6c0bc9af68222b22951552d73df4532b5be6447cee32d58e7e8c74ecbb7b/charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:c108f8619e504140569ee7de3f97d234f0fbae338a7f9f360455071ef9855a95", size = 185805, upload-time = "2026-03-06T06:01:26.294Z" }, + { url = "https://files.pythonhosted.org/packages/dd/b9/a523fb9b0ee90814b503452b2600e4cbc118cd68714d57041564886e7325/charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:d1028de43596a315e2720a9849ee79007ab742c06ad8b45a50db8cdb7ed4a82a", size = 208342, upload-time = "2026-03-06T06:01:27.55Z" }, + { url = "https://files.pythonhosted.org/packages/4d/61/c59e761dee4464050713e50e27b58266cc8e209e518c0b378c1580c959ba/charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:19092dde50335accf365cce21998a1c6dd8eafd42c7b226eb54b2747cdce2fac", size = 193661, upload-time = "2026-03-06T06:01:29.051Z" }, + { url = "https://files.pythonhosted.org/packages/1c/43/729fa30aad69783f755c5ad8649da17ee095311ca42024742701e202dc59/charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4354e401eb6dab9aed3c7b4030514328a6c748d05e1c3e19175008ca7de84fb1", size = 204819, upload-time = "2026-03-06T06:01:30.298Z" }, + { url = "https://files.pythonhosted.org/packages/87/33/d9b442ce5a91b96fc0840455a9e49a611bbadae6122778d0a6a79683dd31/charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a68766a3c58fde7f9aaa22b3786276f62ab2f594efb02d0a1421b6282e852e98", size = 198080, upload-time = "2026-03-06T06:01:31.478Z" }, + { url = "https://files.pythonhosted.org/packages/56/5a/b8b5a23134978ee9885cee2d6995f4c27cc41f9baded0a9685eabc5338f0/charset_normalizer-3.4.5-cp312-cp312-win32.whl", hash = "sha256:1827734a5b308b65ac54e86a618de66f935a4f63a8a462ff1e19a6788d6c2262", size = 132630, upload-time = "2026-03-06T06:01:33.056Z" }, + { url = "https://files.pythonhosted.org/packages/70/53/e44a4c07e8904500aec95865dc3f6464dc3586a039ef0df606eb3ac38e35/charset_normalizer-3.4.5-cp312-cp312-win_amd64.whl", hash = "sha256:728c6a963dfab66ef865f49286e45239384249672cd598576765acc2a640a636", size = 142856, upload-time = "2026-03-06T06:01:34.489Z" }, + { url = "https://files.pythonhosted.org/packages/ea/aa/c5628f7cad591b1cf45790b7a61483c3e36cf41349c98af7813c483fd6e8/charset_normalizer-3.4.5-cp312-cp312-win_arm64.whl", hash = "sha256:75dfd1afe0b1647449e852f4fb428195a7ed0588947218f7ba929f6538487f02", size = 132982, upload-time = "2026-03-06T06:01:35.641Z" }, + { url = "https://files.pythonhosted.org/packages/f5/48/9f34ec4bb24aa3fdba1890c1bddb97c8a4be1bd84ef5c42ac2352563ad05/charset_normalizer-3.4.5-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ac59c15e3f1465f722607800c68713f9fbc2f672b9eb649fe831da4019ae9b23", size = 280788, upload-time = "2026-03-06T06:01:37.126Z" }, + { url = "https://files.pythonhosted.org/packages/0e/09/6003e7ffeb90cc0560da893e3208396a44c210c5ee42efff539639def59b/charset_normalizer-3.4.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:165c7b21d19365464e8f70e5ce5e12524c58b48c78c1f5a57524603c1ab003f8", size = 188890, upload-time = "2026-03-06T06:01:38.73Z" }, + { url = "https://files.pythonhosted.org/packages/42/1e/02706edf19e390680daa694d17e2b8eab4b5f7ac285e2a51168b4b22ee6b/charset_normalizer-3.4.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:28269983f25a4da0425743d0d257a2d6921ea7d9b83599d4039486ec5b9f911d", size = 206136, upload-time = "2026-03-06T06:01:40.016Z" }, + { url = "https://files.pythonhosted.org/packages/c7/87/942c3def1b37baf3cf786bad01249190f3ca3d5e63a84f831e704977de1f/charset_normalizer-3.4.5-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d27ce22ec453564770d29d03a9506d449efbb9fa13c00842262b2f6801c48cce", size = 202551, upload-time = "2026-03-06T06:01:41.522Z" }, + { url = "https://files.pythonhosted.org/packages/94/0a/af49691938dfe175d71b8a929bd7e4ace2809c0c5134e28bc535660d5262/charset_normalizer-3.4.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0625665e4ebdddb553ab185de5db7054393af8879fb0c87bd5690d14379d6819", size = 195572, upload-time = "2026-03-06T06:01:43.208Z" }, + { url = "https://files.pythonhosted.org/packages/20/ea/dfb1792a8050a8e694cfbde1570ff97ff74e48afd874152d38163d1df9ae/charset_normalizer-3.4.5-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:c23eb3263356d94858655b3e63f85ac5d50970c6e8febcdde7830209139cc37d", size = 184438, upload-time = "2026-03-06T06:01:44.755Z" }, + { url = "https://files.pythonhosted.org/packages/72/12/c281e2067466e3ddd0595bfaea58a6946765ace5c72dfa3edc2f5f118026/charset_normalizer-3.4.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e6302ca4ae283deb0af68d2fbf467474b8b6aedcd3dab4db187e07f94c109763", size = 193035, upload-time = "2026-03-06T06:01:46.051Z" }, + { url = "https://files.pythonhosted.org/packages/ba/4f/3792c056e7708e10464bad0438a44708886fb8f92e3c3d29ec5e2d964d42/charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e51ae7d81c825761d941962450f50d041db028b7278e7b08930b4541b3e45cb9", size = 191340, upload-time = "2026-03-06T06:01:47.547Z" }, + { url = "https://files.pythonhosted.org/packages/e7/86/80ddba897127b5c7a9bccc481b0cd36c8fefa485d113262f0fe4332f0bf4/charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:597d10dec876923e5c59e48dbd366e852eacb2b806029491d307daea6b917d7c", size = 185464, upload-time = "2026-03-06T06:01:48.764Z" }, + { url = "https://files.pythonhosted.org/packages/4d/00/b5eff85ba198faacab83e0e4b6f0648155f072278e3b392a82478f8b988b/charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5cffde4032a197bd3b42fd0b9509ec60fb70918d6970e4cc773f20fc9180ca67", size = 208014, upload-time = "2026-03-06T06:01:50.371Z" }, + { url = "https://files.pythonhosted.org/packages/c8/11/d36f70be01597fd30850dde8a1269ebc8efadd23ba5785808454f2389bde/charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2da4eedcb6338e2321e831a0165759c0c620e37f8cd044a263ff67493be8ffb3", size = 193297, upload-time = "2026-03-06T06:01:51.933Z" }, + { url = "https://files.pythonhosted.org/packages/1a/1d/259eb0a53d4910536c7c2abb9cb25f4153548efb42800c6a9456764649c0/charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:65a126fb4b070d05340a84fc709dd9e7c75d9b063b610ece8a60197a291d0adf", size = 204321, upload-time = "2026-03-06T06:01:53.887Z" }, + { url = "https://files.pythonhosted.org/packages/84/31/faa6c5b9d3688715e1ed1bb9d124c384fe2fc1633a409e503ffe1c6398c1/charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c7a80a9242963416bd81f99349d5f3fce1843c303bd404f204918b6d75a75fd6", size = 197509, upload-time = "2026-03-06T06:01:56.439Z" }, + { url = "https://files.pythonhosted.org/packages/fd/a5/c7d9dd1503ffc08950b3260f5d39ec2366dd08254f0900ecbcf3a6197c7c/charset_normalizer-3.4.5-cp313-cp313-win32.whl", hash = "sha256:f1d725b754e967e648046f00c4facc42d414840f5ccc670c5670f59f83693e4f", size = 132284, upload-time = "2026-03-06T06:01:57.812Z" }, + { url = "https://files.pythonhosted.org/packages/b9/0f/57072b253af40c8aa6636e6de7d75985624c1eb392815b2f934199340a89/charset_normalizer-3.4.5-cp313-cp313-win_amd64.whl", hash = "sha256:e37bd100d2c5d3ba35db9c7c5ba5a9228cbcffe5c4778dc824b164e5257813d7", size = 142630, upload-time = "2026-03-06T06:01:59.062Z" }, + { url = "https://files.pythonhosted.org/packages/31/41/1c4b7cc9f13bd9d369ce3bc993e13d374ce25fa38a2663644283ecf422c1/charset_normalizer-3.4.5-cp313-cp313-win_arm64.whl", hash = "sha256:93b3b2cc5cf1b8743660ce77a4f45f3f6d1172068207c1defc779a36eea6bb36", size = 133254, upload-time = "2026-03-06T06:02:00.281Z" }, + { url = "https://files.pythonhosted.org/packages/43/be/0f0fd9bb4a7fa4fb5067fb7d9ac693d4e928d306f80a0d02bde43a7c4aee/charset_normalizer-3.4.5-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8197abe5ca1ffb7d91e78360f915eef5addff270f8a71c1fc5be24a56f3e4873", size = 280232, upload-time = "2026-03-06T06:02:01.508Z" }, + { url = "https://files.pythonhosted.org/packages/28/02/983b5445e4bef49cd8c9da73a8e029f0825f39b74a06d201bfaa2e55142a/charset_normalizer-3.4.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2aecdb364b8a1802afdc7f9327d55dad5366bc97d8502d0f5854e50712dbc5f", size = 189688, upload-time = "2026-03-06T06:02:02.857Z" }, + { url = "https://files.pythonhosted.org/packages/d0/88/152745c5166437687028027dc080e2daed6fe11cfa95a22f4602591c42db/charset_normalizer-3.4.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a66aa5022bf81ab4b1bebfb009db4fd68e0c6d4307a1ce5ef6a26e5878dfc9e4", size = 206833, upload-time = "2026-03-06T06:02:05.127Z" }, + { url = "https://files.pythonhosted.org/packages/cb/0f/ebc15c8b02af2f19be9678d6eed115feeeccc45ce1f4b098d986c13e8769/charset_normalizer-3.4.5-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d77f97e515688bd615c1d1f795d540f32542d514242067adcb8ef532504cb9ee", size = 202879, upload-time = "2026-03-06T06:02:06.446Z" }, + { url = "https://files.pythonhosted.org/packages/38/9c/71336bff6934418dc8d1e8a1644176ac9088068bc571da612767619c97b3/charset_normalizer-3.4.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01a1ed54b953303ca7e310fafe0fe347aab348bd81834a0bcd602eb538f89d66", size = 195764, upload-time = "2026-03-06T06:02:08.763Z" }, + { url = "https://files.pythonhosted.org/packages/b7/95/ce92fde4f98615661871bc282a856cf9b8a15f686ba0af012984660d480b/charset_normalizer-3.4.5-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:b2d37d78297b39a9eb9eb92c0f6df98c706467282055419df141389b23f93362", size = 183728, upload-time = "2026-03-06T06:02:10.137Z" }, + { url = "https://files.pythonhosted.org/packages/1c/e7/f5b4588d94e747ce45ae680f0f242bc2d98dbd4eccfab73e6160b6893893/charset_normalizer-3.4.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e71bbb595973622b817c042bd943c3f3667e9c9983ce3d205f973f486fec98a7", size = 192937, upload-time = "2026-03-06T06:02:11.663Z" }, + { url = "https://files.pythonhosted.org/packages/f9/29/9d94ed6b929bf9f48bf6ede6e7474576499f07c4c5e878fb186083622716/charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4cd966c2559f501c6fd69294d082c2934c8dd4719deb32c22961a5ac6db0df1d", size = 192040, upload-time = "2026-03-06T06:02:13.489Z" }, + { url = "https://files.pythonhosted.org/packages/15/d2/1a093a1cf827957f9445f2fe7298bcc16f8fc5e05c1ed2ad1af0b239035e/charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d5e52d127045d6ae01a1e821acfad2f3a1866c54d0e837828538fabe8d9d1bd6", size = 184107, upload-time = "2026-03-06T06:02:14.83Z" }, + { url = "https://files.pythonhosted.org/packages/0f/7d/82068ce16bd36135df7b97f6333c5d808b94e01d4599a682e2337ed5fd14/charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:30a2b1a48478c3428d047ed9690d57c23038dac838a87ad624c85c0a78ebeb39", size = 208310, upload-time = "2026-03-06T06:02:16.165Z" }, + { url = "https://files.pythonhosted.org/packages/84/4e/4dfb52307bb6af4a5c9e73e482d171b81d36f522b21ccd28a49656baa680/charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:d8ed79b8f6372ca4254955005830fd61c1ccdd8c0fac6603e2c145c61dd95db6", size = 192918, upload-time = "2026-03-06T06:02:18.144Z" }, + { url = "https://files.pythonhosted.org/packages/08/a4/159ff7da662cf7201502ca89980b8f06acf3e887b278956646a8aeb178ab/charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:c5af897b45fa606b12464ccbe0014bbf8c09191e0a66aab6aa9d5cf6e77e0c94", size = 204615, upload-time = "2026-03-06T06:02:19.821Z" }, + { url = "https://files.pythonhosted.org/packages/d6/62/0dd6172203cb6b429ffffc9935001fde42e5250d57f07b0c28c6046deb6b/charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1088345bcc93c58d8d8f3d783eca4a6e7a7752bbff26c3eee7e73c597c191c2e", size = 197784, upload-time = "2026-03-06T06:02:21.86Z" }, + { url = "https://files.pythonhosted.org/packages/c7/5e/1aab5cb737039b9c59e63627dc8bbc0d02562a14f831cc450e5f91d84ce1/charset_normalizer-3.4.5-cp314-cp314-win32.whl", hash = "sha256:ee57b926940ba00bca7ba7041e665cc956e55ef482f851b9b65acb20d867e7a2", size = 133009, upload-time = "2026-03-06T06:02:23.289Z" }, + { url = "https://files.pythonhosted.org/packages/40/65/e7c6c77d7aaa4c0d7974f2e403e17f0ed2cb0fc135f77d686b916bf1eead/charset_normalizer-3.4.5-cp314-cp314-win_amd64.whl", hash = "sha256:4481e6da1830c8a1cc0b746b47f603b653dadb690bcd851d039ffaefe70533aa", size = 143511, upload-time = "2026-03-06T06:02:26.195Z" }, + { url = "https://files.pythonhosted.org/packages/ba/91/52b0841c71f152f563b8e072896c14e3d83b195c188b338d3cc2e582d1d4/charset_normalizer-3.4.5-cp314-cp314-win_arm64.whl", hash = "sha256:97ab7787092eb9b50fb47fa04f24c75b768a606af1bcba1957f07f128a7219e4", size = 133775, upload-time = "2026-03-06T06:02:27.473Z" }, + { url = "https://files.pythonhosted.org/packages/c5/60/3a621758945513adfd4db86827a5bafcc615f913dbd0b4c2ed64a65731be/charset_normalizer-3.4.5-py3-none-any.whl", hash = "sha256:9db5e3fcdcee89a78c04dffb3fe33c79f77bd741a624946db2591c81b2fc85b0", size = 55455, upload-time = "2026-03-06T06:03:17.827Z" }, +] + +[[package]] +name = "click" +version = "8.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/d4/81420972a676e8ffea40450d8c8c92943e7218a78fe9b64359836cc9876b/click-8.4.2.tar.gz", hash = "sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6", size = 338000, upload-time = "2026-06-24T17:45:15.148Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl", hash = "sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76", size = 119243, upload-time = "2026-06-24T17:45:13.73Z" }, +] + +[[package]] +name = "click-didyoumean" +version = "0.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/30/ce/217289b77c590ea1e7c24242d9ddd6e249e52c795ff10fac2c50062c48cb/click_didyoumean-0.3.1.tar.gz", hash = "sha256:4f82fdff0dbe64ef8ab2279bd6aa3f6a99c3b28c05aa09cbfc07c9d7fbb5a463", size = 3089, upload-time = "2024-03-24T08:22:07.499Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1b/5b/974430b5ffdb7a4f1941d13d83c64a0395114503cc357c6b9ae4ce5047ed/click_didyoumean-0.3.1-py3-none-any.whl", hash = "sha256:5c4bb6007cfea5f2fd6583a2fb6701a22a41eb98957e63d0fac41c10e7c3117c", size = 3631, upload-time = "2024-03-24T08:22:06.356Z" }, +] + +[[package]] +name = "click-plugins" +version = "1.1.1.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c3/a4/34847b59150da33690a36da3681d6bbc2ec14ee9a846bc30a6746e5984e4/click_plugins-1.1.1.2.tar.gz", hash = "sha256:d7af3984a99d243c131aa1a828331e7630f4a88a9741fd05c927b204bcf92261", size = 8343, upload-time = "2025-06-25T00:47:37.555Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/9a/2abecb28ae875e39c8cad711eb1186d8d14eab564705325e77e4e6ab9ae5/click_plugins-1.1.1.2-py2.py3-none-any.whl", hash = "sha256:008d65743833ffc1f5417bf0e78e8d2c23aab04d9745ba817bd3e71b0feb6aa6", size = 11051, upload-time = "2025-06-25T00:47:36.731Z" }, +] + +[[package]] +name = "click-repl" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "prompt-toolkit" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cb/a2/57f4ac79838cfae6912f997b4d1a64a858fb0c86d7fcaae6f7b58d267fca/click-repl-0.3.0.tar.gz", hash = "sha256:17849c23dba3d667247dc4defe1757fff98694e90fe37474f3feebb69ced26a9", size = 10449, upload-time = "2023-06-15T12:43:51.141Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/40/9d857001228658f0d59e97ebd4c346fe73e138c6de1bce61dc568a57c7f8/click_repl-0.3.0-py3-none-any.whl", hash = "sha256:fb7e06deb8da8de86180a33a9da97ac316751c094c6899382da7feeeeb51b812", size = 10289, upload-time = "2023-06-15T12:43:48.626Z" }, +] + +[[package]] +name = "cloudpathlib" +version = "0.23.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f4/18/2ac35d6b3015a0c74e923d94fc69baf8307f7c3233de015d69f99e17afa8/cloudpathlib-0.23.0.tar.gz", hash = "sha256:eb38a34c6b8a048ecfd2b2f60917f7cbad4a105b7c979196450c2f541f4d6b4b", size = 53126, upload-time = "2025-10-07T22:47:56.278Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ae/8a/c4bb04426d608be4a3171efa2e233d2c59a5c8937850c10d098e126df18e/cloudpathlib-0.23.0-py3-none-any.whl", hash = "sha256:8520b3b01468fee77de37ab5d50b1b524ea6b4a8731c35d1b7407ac0cd716002", size = 62755, upload-time = "2025-10-07T22:47:54.905Z" }, +] + +[[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, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "colorlog" +version = "6.10.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a2/61/f083b5ac52e505dfc1c624eafbf8c7589a0d7f32daa398d2e7590efa5fda/colorlog-6.10.1.tar.gz", hash = "sha256:eb4ae5cb65fe7fec7773c2306061a8e63e02efc2c72eba9d27b0fa23c94f1321", size = 17162, upload-time = "2025-10-16T16:14:11.978Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/c1/e419ef3723a074172b68aaa89c9f3de486ed4c2399e2dbd8113a4fdcaf9e/colorlog-6.10.1-py3-none-any.whl", hash = "sha256:2d7e8348291948af66122cff006c9f8da6255d224e7cf8e37d8de2df3bad8c9c", size = 11743, upload-time = "2025-10-16T16:14:10.512Z" }, +] + +[[package]] +name = "confection" +version = "0.1.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "srsly" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/51/d3/57c6631159a1b48d273b40865c315cf51f89df7a9d1101094ef12e3a37c2/confection-0.1.5.tar.gz", hash = "sha256:8e72dd3ca6bd4f48913cd220f10b8275978e740411654b6e8ca6d7008c590f0e", size = 38924, upload-time = "2024-05-31T16:17:01.559Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/00/3106b1854b45bd0474ced037dfe6b73b90fe68a68968cef47c23de3d43d2/confection-0.1.5-py3-none-any.whl", hash = "sha256:e29d3c3f8eac06b3f77eb9dfb4bf2fc6bcc9622a98ca00a698e3d019c6430b14", size = 35451, upload-time = "2024-05-31T16:16:59.075Z" }, +] + +[[package]] +name = "cpp-chunker" +version = "0.1.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pybind11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ad/83/5f9dab86c88ca09df1fb66ae001e42aaf5961011c0517297873712511678/cpp_chunker-0.1.4.tar.gz", hash = "sha256:9903ff0ca5f0c3eaede4a07db62ac3426f32bd506ed2b0a041157ddecb7bbc67", size = 11577, upload-time = "2025-09-20T08:29:01.614Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8e/57/85b3a430db230b88c540b8c6263c5d930597319d4179681e51fd72cf13a0/cpp_chunker-0.1.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e7bfd512d7a5d3147ff006cdd0e514aa1c6866623c46b37b7def286266c7e74c", size = 144457, upload-time = "2025-09-20T08:28:58.526Z" }, +] + +[[package]] +name = "cryptography" +version = "50.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/de/41/6cbdcf9142d00fe82836fbb51e503e58088575cf7a0fe1dbff6695bf0840/cryptography-50.0.0.tar.gz", hash = "sha256:eeac2acb5a20ed25e0ad6d1df9891a520b78b404266b6d11778f25d5d691a6c9", size = 880201, upload-time = "2026-07-31T14:25:10.11Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/5c/59086b4aac5e879d38ddbcf74e4be7ade89cebc3eb199a55da998c3bb46a/cryptography-50.0.0-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:031e2d5dd4bb9caa3ca9c82e5a197fd8ae680232cee62603d1a813f3f07e3d03", size = 4001252, upload-time = "2026-07-31T14:23:33.331Z" }, + { url = "https://files.pythonhosted.org/packages/57/ef/8f2df13c7216bcad3e1c74e07f6e193d93e998e114f524a53877c9af27ad/cryptography-50.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fd9192b7b70c573d7f214eb1ae35e00d359f6f5e4b27c7e21e30de1fc6204645", size = 4719554, upload-time = "2026-07-31T14:23:35.611Z" }, + { url = "https://files.pythonhosted.org/packages/d9/41/029086c34d91052fc3b88bcc8056f709a7c915c7a23b235a54eb800b1c97/cryptography-50.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:06a32a980526a6ab9a4b9bf8f7385800791e2bb960903cb6b530e4817509a3b7", size = 4702130, upload-time = "2026-07-31T14:23:37.635Z" }, + { url = "https://files.pythonhosted.org/packages/7d/ff/b6ce0954962e7f7b969f850a883744197bb3910bdfd7b6da162eab7d9f68/cryptography-50.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:a1b30560f2acc95aa8b2e06e716a13dbfc97314747b80d9707e307f77b40d6b3", size = 4725244, upload-time = "2026-07-31T14:23:39.471Z" }, + { url = "https://files.pythonhosted.org/packages/06/1e/63a1027cb7fec360a182208e1b7767d5aa1fe57be3d6aa856e69a321edc0/cryptography-50.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:8d89f3976b10b4ce31118de72329025f70d2c6ead14a8217c5514dd2c6d5a78f", size = 5342265, upload-time = "2026-07-31T14:23:41.286Z" }, + { url = "https://files.pythonhosted.org/packages/6b/72/a1116d683a6d7ece94590013882515de087edf9ef0e6292aae615a44df73/cryptography-50.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:b42a28c1844fd9de8f3f7d540e36b66f3a9c83fceac7170ebc7a6a19edd9dcae", size = 4734609, upload-time = "2026-07-31T14:23:43.139Z" }, + { url = "https://files.pythonhosted.org/packages/15/37/36a9c479bbe49acea2636c7fd3360d20f7b7e079c300352011c44850b181/cryptography-50.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:900131fafd8aead39ac7dd3a7e833be754c17a95cfd91221636949fe4eb0aa8a", size = 4356517, upload-time = "2026-07-31T14:23:44.939Z" }, + { url = "https://files.pythonhosted.org/packages/32/98/8a151d64367204cbc63ec65d37502f1d9c53cf4bfc6ec3c532614dbec60d/cryptography-50.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:07949c449a1abcf60d1ee6e88956d89404c7df3c8258f46589e912988e551987", size = 4724529, upload-time = "2026-07-31T14:23:46.93Z" }, + { url = "https://files.pythonhosted.org/packages/22/f6/ec13b470172126464a86bf54d2294a46d29837fc51ba3e45d4047946fb5e/cryptography-50.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:f89831ef99dd7dd169ab06d63a831adb9e20a87aac6d380266bbda5823349169", size = 5299852, upload-time = "2026-07-31T14:23:48.851Z" }, + { url = "https://files.pythonhosted.org/packages/da/3a/f05e32c99d440c9bb891ea0e36c9091891e36be5a9a87ab2ee6ea20729f6/cryptography-50.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:82148ec5bddac30b51a5b3c1945075f896fa022cb93f8e4a01e9f6ee95292c5f", size = 4734462, upload-time = "2026-07-31T14:23:50.861Z" }, + { url = "https://files.pythonhosted.org/packages/ca/dc/bd72b26be8953f80625f63151efd38eee71c76ca6cf591c08ff34615a79e/cryptography-50.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1489e263a8048bb8b6a8bac662eb2d402ea5d2b7b4699b72f385f1e2772db105", size = 4852708, upload-time = "2026-07-31T14:23:52.715Z" }, + { url = "https://files.pythonhosted.org/packages/27/20/c930314a2ab476d15dec966ec87e2e9637bb02b06106b12c0396c57bb603/cryptography-50.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7cec5b856506da6defb290f30c9ee687d5f5e8cb0bd3f6459dde43b0b4fa40ef", size = 5004179, upload-time = "2026-07-31T14:23:54.887Z" }, + { url = "https://files.pythonhosted.org/packages/32/2e/c9db68a0c4bfa28e310707527c0ee3a2bd254104d2e02e68f368e197aa4c/cryptography-50.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:bd1c592e4d5974f0d08d4888e432157adba757c66da0246918e43677fafa2d30", size = 3840395, upload-time = "2026-07-31T14:23:56.677Z" }, + { url = "https://files.pythonhosted.org/packages/c3/fb/951032a3bf22a5697c83183fb6294a4843772947a70e616c57b3ff5f522e/cryptography-50.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:49e7d93abdbd2990caced757e5fade25302f719c3c8fb6e6fff2dde98999fc41", size = 3989258, upload-time = "2026-07-31T14:23:58.881Z" }, + { url = "https://files.pythonhosted.org/packages/d4/67/91eb047e69c5e845f2f14b8a2e4a1aab0f283cb885531e9e22c8adb176bc/cryptography-50.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:19736989797678c6af1e55cd49055cdbcb55d8f6b5583ac5335f933aba9101dc", size = 4700648, upload-time = "2026-07-31T14:24:00.702Z" }, + { url = "https://files.pythonhosted.org/packages/30/82/85f0f7425c856b9f96459411eb12e74ef72df9caf6f8f15bf23a33ff131f/cryptography-50.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:80b63928fa35083b33966ce1efb70e5b9607181e49dcd1c22c8c005e319f667f", size = 4682442, upload-time = "2026-07-31T14:24:02.538Z" }, + { url = "https://files.pythonhosted.org/packages/1a/28/b555a365adff1cca2fbe7b9e487d68a40de6bc67ff2cb587473eb43de0e7/cryptography-50.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:d58c3db7cd6eed54e6c06744db55456b65ebd7492ddeae9c1e93cfca7aa857d3", size = 4707596, upload-time = "2026-07-31T14:24:04.394Z" }, + { url = "https://files.pythonhosted.org/packages/72/d8/f52538140cc719df62a01cf87d1c7142318d235817109d6f4054d7c352d6/cryptography-50.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:df2a58a472f332225671c35b0a830208b86d004f82baa8530fa3782c85646533", size = 5314552, upload-time = "2026-07-31T14:24:06.31Z" }, + { url = "https://files.pythonhosted.org/packages/38/14/6120e5bd7c5aa022ad15424ba4d5c5269d0d9448ed4d55e492ea91e3c1c4/cryptography-50.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:11b74db56cdbe3cdee6e3f6982ecb70334fa10dce99ed58bf7894aaaa3b2a037", size = 4717113, upload-time = "2026-07-31T14:24:08.349Z" }, + { url = "https://files.pythonhosted.org/packages/fa/71/190bf38c3ee2e0f8efc9860ae100c9df4169742eef274b91e7aa1cb133b9/cryptography-50.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f59e38625469987d7ef6d495323c55e7db6c212eaf6112267e0d3b565a2e9c9f", size = 4338580, upload-time = "2026-07-31T14:24:10.227Z" }, + { url = "https://files.pythonhosted.org/packages/3a/63/504ccfbbe61fd8aa983f7f146399cdf034c72c2fc55f5b2dfdcdcdb20c99/cryptography-50.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:ecfed7367f965a0328cfbdd70da860f15441f002f613185668c6e6ebf5a0ac11", size = 4707038, upload-time = "2026-07-31T14:24:12.169Z" }, + { url = "https://files.pythonhosted.org/packages/01/77/2cf79bbfc4d12ca106437a6e170d6aaa01a373e93093118aaaef0e801bd4/cryptography-50.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:9aa87839c383bdbab6ef865787a1fb877af8dd03464c4400322726feaaadfc6d", size = 5273110, upload-time = "2026-07-31T14:24:14.38Z" }, + { url = "https://files.pythonhosted.org/packages/e5/45/8aae2972c520145377ea3559a605a899bebe227bf070b33cdb445929a9b9/cryptography-50.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:6ba6a53445bd3cfa809ef3ef5f1589aa6ba08784a1d962bf47d0940e871dab1c", size = 4716439, upload-time = "2026-07-31T14:24:16.415Z" }, + { url = "https://files.pythonhosted.org/packages/7b/20/4fe50b619a48c2525cc46e2dbc1ac490708d704be5d467bdaac6dc955682/cryptography-50.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3f5735ffe4996d28b809371756219f5354864902a3b9e7c0b9ee87041209fc9c", size = 4837383, upload-time = "2026-07-31T14:24:18.553Z" }, + { url = "https://files.pythonhosted.org/packages/92/91/3a31366e183343d3703f8995c095f5734676bd6938118047e50fcf279eb4/cryptography-50.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:1b4a266766514614f8aa60416e71f2fc6e575d36e7bdc90f644fadb2f4b75b95", size = 4985772, upload-time = "2026-07-31T14:24:20.385Z" }, + { url = "https://files.pythonhosted.org/packages/74/9a/02ffe35b2853d121689871eb5dce862092562b3a1ed5cc98f1aaed441506/cryptography-50.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:12b9c6996425c76ea6c457ace4f3073e715b8c545add07cd1a8f3a4f90691269", size = 3816291, upload-time = "2026-07-31T14:24:22.125Z" }, + { url = "https://files.pythonhosted.org/packages/03/37/73d005be173aff344af30e9fd2a576575cb2391a7101d9cd3842e1fa8cce/cryptography-50.0.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ccdc4a71a4dabae05de219404f9f4abc38e3b58422177ff93d0da05967dafa07", size = 4036009, upload-time = "2026-07-31T14:24:24.122Z" }, + { url = "https://files.pythonhosted.org/packages/ff/c6/7a6202a534e32103a285b7834a120869557fe198d51d7cfe59754c8bda9c/cryptography-50.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:910e1d2668e7de9648f2bcee30e180db2a6b15c30f887d7c4c93ddf96e3992e3", size = 4745252, upload-time = "2026-07-31T14:24:26.118Z" }, + { url = "https://files.pythonhosted.org/packages/85/4f/0fa8c2f4428198f15d9ff8d63400e27afbf94ce833f6108da1eb3753f945/cryptography-50.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a91296cb61e8df6f86d0c19cc4068228da256bf59bf86049fbd821084565327f", size = 4728939, upload-time = "2026-07-31T14:24:27.994Z" }, + { url = "https://files.pythonhosted.org/packages/d1/63/54dd723490ba2dc09b299682c10b38db38f159728bcaae8c591b8af2f22d/cryptography-50.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e722f16708d854fe924790e051061f6704a472c3bac347b6fd88033ea8dd0dc5", size = 4748483, upload-time = "2026-07-31T14:24:30.254Z" }, + { url = "https://files.pythonhosted.org/packages/1d/dd/7c77d26285cc7f6991efce64a0f5b4f9383bfa5dd8c5033003eaf7db4cdb/cryptography-50.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:d764dcf130c428ef66786f866dd750f53182bc608813489915e9fc106bb0c82f", size = 5367599, upload-time = "2026-07-31T14:24:32.457Z" }, + { url = "https://files.pythonhosted.org/packages/46/c9/f60aed34c013f317f92817b6c171c2d22a78270fa41109bd4b08af26b194/cryptography-50.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:105110f43a471dbd0060b9c9516cb8a6a79233631a04cc2ba16f28323ac6e025", size = 4762647, upload-time = "2026-07-31T14:24:34.599Z" }, + { url = "https://files.pythonhosted.org/packages/be/f3/f9a0173b139372c3a48ed98154b45cc6b9de17c789d5ab552e621c293609/cryptography-50.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:828743d939e9629bc267b8e2d08d8bb67cd4319c771a33d4b18b22dd8fb7440a", size = 4385197, upload-time = "2026-07-31T14:24:36.647Z" }, + { url = "https://files.pythonhosted.org/packages/d8/36/83bb81f6e569bc38e1e4a7bc80f29b46bb9601920bc455fc8e888f5d5742/cryptography-50.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:2a8183b489dc1f7f80f135780fadc1108f14b31b8a40411c7a5b17425f65f28b", size = 4748095, upload-time = "2026-07-31T14:24:39.493Z" }, + { url = "https://files.pythonhosted.org/packages/6b/16/d3008eff98c764979865834c3d386d4fd041b5f52e7f34fc29ac1a5eb515/cryptography-50.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:6e7d61120573a7f2cd94cc095f9e81f6967c61ccdf194285aa143ecec8e0b708", size = 5325948, upload-time = "2026-07-31T14:24:41.556Z" }, + { url = "https://files.pythonhosted.org/packages/9c/f8/d97f9603efda3888187bfdb893f26c41be4735c10631d05d284ee6b047c4/cryptography-50.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:37fdb0d0111f1e2ff07139dfb79f1b49531f8e213c46f1163dd7642979b58c47", size = 4762400, upload-time = "2026-07-31T14:24:43.636Z" }, + { url = "https://files.pythonhosted.org/packages/64/a2/4615c8f7d81a00b1d6e6afe19f694e1543582349fb5f4076f6cb5dc36485/cryptography-50.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c87f62a3d3b9888ed0fdde100ec06aa61ca9cd44bad9057d1dff9a516b5f5bb9", size = 4878208, upload-time = "2026-07-31T14:24:45.522Z" }, + { url = "https://files.pythonhosted.org/packages/d2/1a/efcfb02f91407149a0dacffffab791f7e19bf6385f63b3666dc8b5e5c9c8/cryptography-50.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:65c2c3add92b45fd0709db8594536aea39c2a67af0e27ffcf049c498501140b7", size = 5037050, upload-time = "2026-07-31T14:24:47.697Z" }, + { url = "https://files.pythonhosted.org/packages/57/30/4a22984d4f1bdfb8c054f07a92bc176b97a3134cc1d6c4b3bffb1f3688b4/cryptography-50.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:d24fead1d4d076e1bfb006dcec392074a3cd8d7b4fc8a595aa64073b2b7a96ba", size = 3874135, upload-time = "2026-07-31T14:24:50.085Z" }, + { url = "https://files.pythonhosted.org/packages/9d/3e/e54cde8c01631a5a8226ccd617eab9e57fd5cfdad90f1a9e6bb570794631/cryptography-50.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:5e34edd123674534acd70147f0ca331eaa2c74e6325fb2028c886aa26ba0b68c", size = 3963170, upload-time = "2026-07-31T14:24:51.968Z" }, + { url = "https://files.pythonhosted.org/packages/01/b6/0b9e125e90f3d2dcf599a218a899cda7326a3158cfa258723f0b398b08f6/cryptography-50.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:8eb5e1172eb569ea8a872796576e6a67c276351728b6455d5beb01242b027c6a", size = 4692441, upload-time = "2026-07-31T14:24:53.743Z" }, + { url = "https://files.pythonhosted.org/packages/53/c9/a5151588710785a96d7bc4de27d4cd62f263bbbcb203cfe29df537eb6505/cryptography-50.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:910d11e1a385c654bf738bf3e6b8e6ed5de0f5610fcae2be9e5b398d8081d20e", size = 4699810, upload-time = "2026-07-31T14:24:55.746Z" }, + { url = "https://files.pythonhosted.org/packages/c7/1a/15b92b25eb6ce3089cd49377ae990a0f3ad485a510f968aed1f19dbdcdf2/cryptography-50.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:62598a8a57f815db4c6259a4e97d857dab56697e7de8e8ab02352ab74da1995d", size = 4691924, upload-time = "2026-07-31T14:24:58.082Z" }, + { url = "https://files.pythonhosted.org/packages/62/15/219075012ab13e8905f3cd572204f4acb4b111df787104346b9bc0cea789/cryptography-50.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:07479a1cb08219ab719147e742e76090c9c773321959bb94946fffdd397a6437", size = 4699593, upload-time = "2026-07-31T14:24:59.951Z" }, + { url = "https://files.pythonhosted.org/packages/8e/b5/c2c5fce26f0ee40d21bafe7f191d29a34b35a65ac4fe8a1191d1983612e9/cryptography-50.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c99c003e088647b8a5b7c145d6f78c335f6348332b62e142d411c4b63d1460b9", size = 3813796, upload-time = "2026-07-31T14:25:02.298Z" }, +] + +[[package]] +name = "cuda-bindings" +version = "12.9.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cuda-pathfinder", marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/e7/b47792cc2d01c7e1d37c32402182524774dadd2d26339bd224e0e913832e/cuda_bindings-12.9.4-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c912a3d9e6b6651853eed8eed96d6800d69c08e94052c292fec3f282c5a817c9", size = 12210593, upload-time = "2025-10-21T14:51:36.574Z" }, + { url = "https://files.pythonhosted.org/packages/a9/c1/dabe88f52c3e3760d861401bb994df08f672ec893b8f7592dc91626adcf3/cuda_bindings-12.9.4-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fda147a344e8eaeca0c6ff113d2851ffca8f7dfc0a6c932374ee5c47caa649c8", size = 12151019, upload-time = "2025-10-21T14:51:43.167Z" }, + { url = "https://files.pythonhosted.org/packages/63/56/e465c31dc9111be3441a9ba7df1941fe98f4aa6e71e8788a3fb4534ce24d/cuda_bindings-12.9.4-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:32bdc5a76906be4c61eb98f546a6786c5773a881f3b166486449b5d141e4a39f", size = 11906628, upload-time = "2025-10-21T14:51:49.905Z" }, + { url = "https://files.pythonhosted.org/packages/a3/84/1e6be415e37478070aeeee5884c2022713c1ecc735e6d82d744de0252eee/cuda_bindings-12.9.4-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:56e0043c457a99ac473ddc926fe0dc4046694d99caef633e92601ab52cbe17eb", size = 11925991, upload-time = "2025-10-21T14:51:56.535Z" }, + { url = "https://files.pythonhosted.org/packages/d1/af/6dfd8f2ed90b1d4719bc053ff8940e494640fe4212dc3dd72f383e4992da/cuda_bindings-12.9.4-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8b72ee72a9cc1b531db31eebaaee5c69a8ec3500e32c6933f2d3b15297b53686", size = 11922703, upload-time = "2025-10-21T14:52:03.585Z" }, + { url = "https://files.pythonhosted.org/packages/6c/19/90ac264acc00f6df8a49378eedec9fd2db3061bf9263bf9f39fd3d8377c3/cuda_bindings-12.9.4-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d80bffc357df9988dca279734bc9674c3934a654cab10cadeed27ce17d8635ee", size = 11924658, upload-time = "2025-10-21T14:52:10.411Z" }, +] + +[[package]] +name = "cuda-pathfinder" +version = "1.4.1" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/02/59a5bc738a09def0b49aea0e460bdf97f65206d0d041246147cf6207e69c/cuda_pathfinder-1.4.1-py3-none-any.whl", hash = "sha256:40793006082de88e0950753655e55558a446bed9a7d9d0bcb48b2506d50ed82a", size = 43903, upload-time = "2026-03-06T21:05:24.372Z" }, +] + +[[package]] +name = "cymem" +version = "2.0.13" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/2f0fbb32535c3731b7c2974c569fb9325e0a38ed5565a08e1139a3b71e82/cymem-2.0.13.tar.gz", hash = "sha256:1c91a92ae8c7104275ac26bd4d29b08ccd3e7faff5893d3858cb6fadf1bc1588", size = 12320, upload-time = "2025-11-14T14:58:36.902Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/64/1db41f7576a6b69f70367e3c15e968fd775ba7419e12059c9966ceb826f8/cymem-2.0.13-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:673183466b0ff2e060d97ec5116711d44200b8f7be524323e080d215ee2d44a5", size = 43587, upload-time = "2025-11-14T14:57:22.39Z" }, + { url = "https://files.pythonhosted.org/packages/81/13/57f936fc08551323aab3f92ff6b7f4d4b89d5b4e495c870a67cb8d279757/cymem-2.0.13-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bee2791b3f6fc034ce41268851462bf662ff87e8947e35fb6dd0115b4644a61f", size = 43139, upload-time = "2025-11-14T14:57:23.363Z" }, + { url = "https://files.pythonhosted.org/packages/32/a6/9345754be51e0479aa387b7b6cffc289d0fd3201aaeb8dade4623abd1e02/cymem-2.0.13-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f3aee3adf16272bca81c5826eed55ba3c938add6d8c9e273f01c6b829ecfde22", size = 245063, upload-time = "2025-11-14T14:57:24.839Z" }, + { url = "https://files.pythonhosted.org/packages/d6/01/6bc654101526fa86e82bf6b05d99b2cd47c30a333cfe8622c26c0592beb2/cymem-2.0.13-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:30c4e75a3a1d809e89106b0b21803eb78e839881aa1f5b9bd27b454bc73afde3", size = 244496, upload-time = "2025-11-14T14:57:26.42Z" }, + { url = "https://files.pythonhosted.org/packages/c4/fb/853b7b021e701a1f41687f3704d5f469aeb2a4f898c3fbb8076806885955/cymem-2.0.13-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ec99efa03cf8ec11c8906aa4d4cc0c47df393bc9095c9dd64b89b9b43e220b04", size = 243287, upload-time = "2025-11-14T14:57:27.542Z" }, + { url = "https://files.pythonhosted.org/packages/d4/2b/0e4664cafc581de2896d75000651fd2ce7094d33263f466185c28ffc96e4/cymem-2.0.13-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c90a6ecba994a15b17a3f45d7ec74d34081df2f73bd1b090e2adc0317e4e01b6", size = 248287, upload-time = "2025-11-14T14:57:29.055Z" }, + { url = "https://files.pythonhosted.org/packages/21/0f/f94c6950edbfc2aafb81194fc40b6cacc8e994e9359d3cb4328c5705b9b5/cymem-2.0.13-cp311-cp311-win_amd64.whl", hash = "sha256:ce821e6ba59148ed17c4567113b8683a6a0be9c9ac86f14e969919121efb61a5", size = 40116, upload-time = "2025-11-14T14:57:30.592Z" }, + { url = "https://files.pythonhosted.org/packages/00/df/2455eff6ac0381ff165db6883b311f7016e222e3dd62185517f8e8187ed0/cymem-2.0.13-cp311-cp311-win_arm64.whl", hash = "sha256:0dca715e708e545fd1d97693542378a00394b20a37779c1ae2c8bdbb43acef79", size = 36349, upload-time = "2025-11-14T14:57:31.573Z" }, + { url = "https://files.pythonhosted.org/packages/c9/52/478a2911ab5028cb710b4900d64aceba6f4f882fcb13fd8d40a456a1b6dc/cymem-2.0.13-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e8afbc5162a0fe14b6463e1c4e45248a1b2fe2cbcecc8a5b9e511117080da0eb", size = 43745, upload-time = "2025-11-14T14:57:32.52Z" }, + { url = "https://files.pythonhosted.org/packages/f9/71/f0f8adee945524774b16af326bd314a14a478ed369a728a22834e6785a18/cymem-2.0.13-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c9251d889348fe79a75e9b3e4d1b5fa651fca8a64500820685d73a3acc21b6a8", size = 42927, upload-time = "2025-11-14T14:57:33.827Z" }, + { url = "https://files.pythonhosted.org/packages/62/6d/159780fe162ff715d62b809246e5fc20901cef87ca28b67d255a8d741861/cymem-2.0.13-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:742fc19764467a49ed22e56a4d2134c262d73a6c635409584ae3bf9afa092c33", size = 258346, upload-time = "2025-11-14T14:57:34.917Z" }, + { url = "https://files.pythonhosted.org/packages/eb/12/678d16f7aa1996f947bf17b8cfb917ea9c9674ef5e2bd3690c04123d5680/cymem-2.0.13-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f190a92fe46197ee64d32560eb121c2809bb843341733227f51538ce77b3410d", size = 260843, upload-time = "2025-11-14T14:57:36.503Z" }, + { url = "https://files.pythonhosted.org/packages/31/5d/0dd8c167c08cd85e70d274b7235cfe1e31b3cebc99221178eaf4bbb95c6f/cymem-2.0.13-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d670329ee8dbbbf241b7c08069fe3f1d3a1a3e2d69c7d05ea008a7010d826298", size = 254607, upload-time = "2025-11-14T14:57:38.036Z" }, + { url = "https://files.pythonhosted.org/packages/b7/c9/d6514a412a1160aa65db539836b3d47f9b59f6675f294ec34ae32f867c82/cymem-2.0.13-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a84ba3178d9128b9ffb52ce81ebab456e9fe959125b51109f5b73ebdfc6b60d6", size = 262421, upload-time = "2025-11-14T14:57:39.265Z" }, + { url = "https://files.pythonhosted.org/packages/dd/fe/3ee37d02ca4040f2fb22d34eb415198f955862b5dd47eee01df4c8f5454c/cymem-2.0.13-cp312-cp312-win_amd64.whl", hash = "sha256:2ff1c41fd59b789579fdace78aa587c5fc091991fa59458c382b116fc36e30dc", size = 40176, upload-time = "2025-11-14T14:57:40.706Z" }, + { url = "https://files.pythonhosted.org/packages/94/fb/1b681635bfd5f2274d0caa8f934b58435db6c091b97f5593738065ddb786/cymem-2.0.13-cp312-cp312-win_arm64.whl", hash = "sha256:6bbd701338df7bf408648191dff52472a9b334f71bcd31a21a41d83821050f67", size = 35959, upload-time = "2025-11-14T14:57:41.682Z" }, + { url = "https://files.pythonhosted.org/packages/ce/0f/95a4d1e3bebfdfa7829252369357cf9a764f67569328cd9221f21e2c952e/cymem-2.0.13-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:891fd9030293a8b652dc7fb9fdc79a910a6c76fc679cd775e6741b819ffea476", size = 43478, upload-time = "2025-11-14T14:57:42.682Z" }, + { url = "https://files.pythonhosted.org/packages/bf/a0/8fc929cc29ae466b7b4efc23ece99cbd3ea34992ccff319089c624d667fd/cymem-2.0.13-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:89c4889bd16513ce1644ccfe1e7c473ba7ca150f0621e66feac3a571bde09e7e", size = 42695, upload-time = "2025-11-14T14:57:43.741Z" }, + { url = "https://files.pythonhosted.org/packages/4a/b3/deeb01354ebaf384438083ffe0310209ef903db3e7ba5a8f584b06d28387/cymem-2.0.13-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:45dcaba0f48bef9cc3d8b0b92058640244a95a9f12542210b51318da97c2cf28", size = 250573, upload-time = "2025-11-14T14:57:44.81Z" }, + { url = "https://files.pythonhosted.org/packages/36/36/bc980b9a14409f3356309c45a8d88d58797d02002a9d794dd6c84e809d3a/cymem-2.0.13-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e96848faaafccc0abd631f1c5fb194eac0caee4f5a8777fdbb3e349d3a21741c", size = 254572, upload-time = "2025-11-14T14:57:46.023Z" }, + { url = "https://files.pythonhosted.org/packages/fd/dd/a12522952624685bd0f8968e26d2ed6d059c967413ce6eb52292f538f1b0/cymem-2.0.13-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e02d3e2c3bfeb21185d5a4a70790d9df40629a87d8d7617dc22b4e864f665fa3", size = 248060, upload-time = "2025-11-14T14:57:47.605Z" }, + { url = "https://files.pythonhosted.org/packages/08/11/5dc933ddfeb2dfea747a0b935cb965b9a7580b324d96fc5f5a1b5ff8df29/cymem-2.0.13-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fece5229fd5ecdcd7a0738affb8c59890e13073ae5626544e13825f26c019d3c", size = 254601, upload-time = "2025-11-14T14:57:48.861Z" }, + { url = "https://files.pythonhosted.org/packages/70/66/d23b06166864fa94e13a98e5922986ce774832936473578febce64448d75/cymem-2.0.13-cp313-cp313-win_amd64.whl", hash = "sha256:38aefeb269597c1a0c2ddf1567dd8605489b661fa0369c6406c1acd433b4c7ba", size = 40103, upload-time = "2025-11-14T14:57:50.396Z" }, + { url = "https://files.pythonhosted.org/packages/2f/9e/c7b21271ab88a21760f3afdec84d2bc09ffa9e6c8d774ad9d4f1afab0416/cymem-2.0.13-cp313-cp313-win_arm64.whl", hash = "sha256:717270dcfd8c8096b479c42708b151002ff98e434a7b6f1f916387a6c791e2ad", size = 36016, upload-time = "2025-11-14T14:57:51.611Z" }, + { url = "https://files.pythonhosted.org/packages/7f/28/d3b03427edc04ae04910edf1c24b993881c3ba93a9729a42bcbb816a1808/cymem-2.0.13-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:7e1a863a7f144ffb345397813701509cfc74fc9ed360a4d92799805b4b865dd1", size = 46429, upload-time = "2025-11-14T14:57:52.582Z" }, + { url = "https://files.pythonhosted.org/packages/35/a9/7ed53e481f47ebfb922b0b42e980cec83e98ccb2137dc597ea156642440c/cymem-2.0.13-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c16cb80efc017b054f78998c6b4b013cef509c7b3d802707ce1f85a1d68361bf", size = 46205, upload-time = "2025-11-14T14:57:53.64Z" }, + { url = "https://files.pythonhosted.org/packages/61/39/a3d6ad073cf7f0fbbb8bbf09698c3c8fac11be3f791d710239a4e8dd3438/cymem-2.0.13-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0d78a27c88b26c89bd1ece247d1d5939dba05a1dae6305aad8fd8056b17ddb51", size = 296083, upload-time = "2025-11-14T14:57:55.922Z" }, + { url = "https://files.pythonhosted.org/packages/36/0c/20697c8bc19f624a595833e566f37d7bcb9167b0ce69de896eba7cfc9c2d/cymem-2.0.13-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6d36710760f817194dacb09d9fc45cb6a5062ed75e85f0ef7ad7aeeb13d80cc3", size = 286159, upload-time = "2025-11-14T14:57:57.106Z" }, + { url = "https://files.pythonhosted.org/packages/82/d4/9326e3422d1c2d2b4a8fb859bdcce80138f6ab721ddafa4cba328a505c71/cymem-2.0.13-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c8f30971cadd5dcf73bcfbbc5849b1f1e1f40db8cd846c4aa7d3b5e035c7b583", size = 288186, upload-time = "2025-11-14T14:57:58.334Z" }, + { url = "https://files.pythonhosted.org/packages/ed/bc/68da7dd749b72884dc22e898562f335002d70306069d496376e5ff3b6153/cymem-2.0.13-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:9d441d0e45798ec1fd330373bf7ffa6b795f229275f64016b6a193e6e2a51522", size = 290353, upload-time = "2025-11-14T14:58:00.562Z" }, + { url = "https://files.pythonhosted.org/packages/50/23/dbf2ad6ecd19b99b3aab6203b1a06608bbd04a09c522d836b854f2f30f73/cymem-2.0.13-cp313-cp313t-win_amd64.whl", hash = "sha256:d1c950eebb9f0f15e3ef3591313482a5a611d16fc12d545e2018cd607f40f472", size = 44764, upload-time = "2025-11-14T14:58:01.793Z" }, + { url = "https://files.pythonhosted.org/packages/54/3f/35701c13e1fc7b0895198c8b20068c569a841e0daf8e0b14d1dc0816b28f/cymem-2.0.13-cp313-cp313t-win_arm64.whl", hash = "sha256:042e8611ef862c34a97b13241f5d0da86d58aca3cecc45c533496678e75c5a1f", size = 38964, upload-time = "2025-11-14T14:58:02.87Z" }, + { url = "https://files.pythonhosted.org/packages/a7/2e/f0e1596010a9a57fa9ebd124a678c07c5b2092283781ae51e79edcf5cb98/cymem-2.0.13-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d2a4bf67db76c7b6afc33de44fb1c318207c3224a30da02c70901936b5aafdf1", size = 43812, upload-time = "2025-11-14T14:58:04.227Z" }, + { url = "https://files.pythonhosted.org/packages/bc/45/8ccc21df08fcbfa6aa3efeb7efc11a1c81c90e7476e255768bb9c29ba02a/cymem-2.0.13-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:92a2ce50afa5625fb5ce7c9302cee61e23a57ccac52cd0410b4858e572f8614b", size = 42951, upload-time = "2025-11-14T14:58:05.424Z" }, + { url = "https://files.pythonhosted.org/packages/01/8c/fe16531631f051d3d1226fa42e2d76fd2c8d5cfa893ec93baee90c7a9d90/cymem-2.0.13-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bc116a70cc3a5dc3d1684db5268eff9399a0be8603980005e5b889564f1ea42f", size = 249878, upload-time = "2025-11-14T14:58:06.95Z" }, + { url = "https://files.pythonhosted.org/packages/47/4b/39d67b80ffb260457c05fcc545de37d82e9e2dbafc93dd6b64f17e09b933/cymem-2.0.13-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:68489bf0035c4c280614067ab6a82815b01dc9fcd486742a5306fe9f68deb7ef", size = 252571, upload-time = "2025-11-14T14:58:08.232Z" }, + { url = "https://files.pythonhosted.org/packages/53/0e/76f6531f74dfdfe7107899cce93ab063bb7ee086ccd3910522b31f623c08/cymem-2.0.13-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:03cb7bdb55718d5eb6ef0340b1d2430ba1386db30d33e9134d01ba9d6d34d705", size = 248555, upload-time = "2025-11-14T14:58:09.429Z" }, + { url = "https://files.pythonhosted.org/packages/c7/7c/eee56757db81f0aefc2615267677ae145aff74228f529838425057003c0d/cymem-2.0.13-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1710390e7fb2510a8091a1991024d8ae838fd06b02cdfdcd35f006192e3c6b0e", size = 254177, upload-time = "2025-11-14T14:58:10.594Z" }, + { url = "https://files.pythonhosted.org/packages/77/e0/a4b58ec9e53c836dce07ef39837a64a599f4a21a134fc7ca57a3a8f9a4b5/cymem-2.0.13-cp314-cp314-win_amd64.whl", hash = "sha256:ac699c8ec72a3a9de8109bd78821ab22f60b14cf2abccd970b5ff310e14158ed", size = 40853, upload-time = "2025-11-14T14:58:12.116Z" }, + { url = "https://files.pythonhosted.org/packages/61/81/9931d1f83e5aeba175440af0b28f0c2e6f71274a5a7b688bc3e907669388/cymem-2.0.13-cp314-cp314-win_arm64.whl", hash = "sha256:90c2d0c04bcda12cd5cebe9be93ce3af6742ad8da96e1b1907e3f8e00291def1", size = 36970, upload-time = "2025-11-14T14:58:13.114Z" }, + { url = "https://files.pythonhosted.org/packages/b7/ef/af447c2184dec6dec973be14614df8ccb4d16d1c74e0784ab4f02538433c/cymem-2.0.13-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:ff036bbc1464993552fd1251b0a83fe102af334b301e3896d7aa05a4999ad042", size = 46804, upload-time = "2025-11-14T14:58:14.113Z" }, + { url = "https://files.pythonhosted.org/packages/8c/95/e10f33a8d4fc17f9b933d451038218437f9326c2abb15a3e7f58ce2a06ec/cymem-2.0.13-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:fb8291691ba7ff4e6e000224cc97a744a8d9588418535c9454fd8436911df612", size = 46254, upload-time = "2025-11-14T14:58:15.156Z" }, + { url = "https://files.pythonhosted.org/packages/e7/7a/5efeb2d2ea6ebad2745301ad33a4fa9a8f9a33b66623ee4d9185683007a6/cymem-2.0.13-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d8d06ea59006b1251ad5794bcc00121e148434826090ead0073c7b7fedebe431", size = 296061, upload-time = "2025-11-14T14:58:16.254Z" }, + { url = "https://files.pythonhosted.org/packages/0b/28/2a3f65842cc8443c2c0650cf23d525be06c8761ab212e0a095a88627be1b/cymem-2.0.13-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c0046a619ecc845ccb4528b37b63426a0cbcb4f14d7940add3391f59f13701e6", size = 285784, upload-time = "2025-11-14T14:58:17.412Z" }, + { url = "https://files.pythonhosted.org/packages/98/73/dd5f9729398f0108c2e71d942253d0d484d299d08b02e474d7cfc43ed0b0/cymem-2.0.13-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:18ad5b116a82fa3674bc8838bd3792891b428971e2123ae8c0fd3ca472157c5e", size = 288062, upload-time = "2025-11-14T14:58:20.225Z" }, + { url = "https://files.pythonhosted.org/packages/5a/01/ffe51729a8f961a437920560659073e47f575d4627445216c1177ecd4a41/cymem-2.0.13-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:666ce6146bc61b9318aa70d91ce33f126b6344a25cf0b925621baed0c161e9cc", size = 290465, upload-time = "2025-11-14T14:58:21.815Z" }, + { url = "https://files.pythonhosted.org/packages/fd/ac/c9e7d68607f71ef978c81e334ab2898b426944c71950212b1467186f69f9/cymem-2.0.13-cp314-cp314t-win_amd64.whl", hash = "sha256:84c1168c563d9d1e04546cb65e3e54fde2bf814f7c7faf11fc06436598e386d1", size = 46665, upload-time = "2025-11-14T14:58:23.512Z" }, + { url = "https://files.pythonhosted.org/packages/66/66/150e406a2db5535533aa3c946de58f0371f2e412e23f050c704588023e6e/cymem-2.0.13-cp314-cp314t-win_arm64.whl", hash = "sha256:e9027764dc5f1999fb4b4cabee1d0322c59e330c0a6485b436a68275f614277f", size = 39715, upload-time = "2025-11-14T14:58:24.773Z" }, +] + +[[package]] +name = "defusedxml" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/d5/c66da9b79e5bdb124974bfe172b4daf3c984ebd9c2a06e2b8a4dc7331c72/defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", size = 75520, upload-time = "2021-03-08T10:59:26.269Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604, upload-time = "2021-03-08T10:59:24.45Z" }, +] + +[[package]] +name = "dill" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/81/e1/56027a71e31b02ddc53c7d65b01e68edf64dea2932122fe7746a516f75d5/dill-0.4.1.tar.gz", hash = "sha256:423092df4182177d4d8ba8290c8a5b640c66ab35ec7da59ccfa00f6fa3eea5fa", size = 187315, upload-time = "2026-01-19T02:36:56.85Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/77/dc8c558f7593132cf8fefec57c4f60c83b16941c574ac5f619abb3ae7933/dill-0.4.1-py3-none-any.whl", hash = "sha256:1e1ce33e978ae97fcfcff5638477032b801c46c7c65cf717f95fbc2248f79a9d", size = 120019, upload-time = "2026-01-19T02:36:55.663Z" }, +] + +[[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, upload-time = "2023-12-24T09:54:32.31Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" }, +] + +[[package]] +name = "dnspython" +version = "2.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/8b/57666417c0f90f08bcafa776861060426765fdb422eb10212086fb811d26/dnspython-2.8.0.tar.gz", hash = "sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f", size = 368251, upload-time = "2025-09-07T18:58:00.022Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", size = 331094, upload-time = "2025-09-07T18:57:58.071Z" }, +] + +[[package]] +name = "doclang" +version = "0.7.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "lxml" }, + { name = "typer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/3a/005e4856ad8e9b9879414a4df4dbc56dc3663b96f9d8c920ef210e8931cf/doclang-0.7.3.tar.gz", hash = "sha256:ca50615357e46ebf9597bb9065b9112367103ec24bd539f8ae12649224cf50b0", size = 31569, upload-time = "2026-07-15T08:11:02.917Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/81/334ccc0f0cd7c3d75996b6b596e7f4c62c4c46a0ca042003315c28170159/doclang-0.7.3-py3-none-any.whl", hash = "sha256:9440c4ca9f7e061a7b8d33bdf15b1029be69a4c13cd8952dd6ce541884e4c685", size = 32267, upload-time = "2026-07-15T08:11:01.977Z" }, +] + +[[package]] +name = "docling" +version = "2.121.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docling-slim", extra = ["standard"] }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9a/8d/97e3763f0cc13448521a928298d609b22a77b1eef11c698341bf19ddd41d/docling-2.121.0.tar.gz", hash = "sha256:c8970cbd3907892db218c7f342ff62db5b393ca3cf568db50c8f2384e248a22d", size = 9013, upload-time = "2026-08-20T09:07:44.568Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d3/c8/190eaffb6113dc3f7fdc4282cd67efdd51bba3bacb2e5b31bc5d77a78df6/docling-2.121.0-py3-none-any.whl", hash = "sha256:d1577ac7660ac6cb3ac7e213f1541f8bed1990ff8da4274d94b5bfc91e0fec55", size = 5177, upload-time = "2026-08-20T09:07:43.424Z" }, +] + +[[package]] +name = "docling-core" +version = "2.92.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "defusedxml" }, + { name = "doclang" }, + { name = "jsonref" }, + { name = "jsonschema" }, + { name = "latex2mathml" }, + { name = "pandas" }, + { name = "pillow" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "pyyaml" }, + { name = "tabulate" }, + { name = "typer" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/47/9c/8ab960c09e501b57dda1b823dff3264bed70ab70ef60328767be38ca30f5/docling_core-2.92.0.tar.gz", hash = "sha256:33fd25e38c199336447a21925374400aca13a6b9316a032f111972c2dc0f085c", size = 360897, upload-time = "2026-08-19T10:55:21.431Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bb/d1/7f6d9e737ea5c41c0fd4d2819732fc43416c0a60c5870342381db149d368/docling_core-2.92.0-py3-none-any.whl", hash = "sha256:726d89c23197e53be2f7192bb4c00108ff545830cf9ab7b981fb014fa92b5c02", size = 295507, upload-time = "2026-08-19T10:55:19.992Z" }, +] + +[package.optional-dependencies] +chunking = [ + { name = "semchunk" }, + { name = "transformers" }, + { name = "tree-sitter" }, + { name = "tree-sitter-c" }, + { name = "tree-sitter-javascript" }, + { name = "tree-sitter-python" }, + { name = "tree-sitter-typescript" }, +] + +[[package]] +name = "docling-ibm-models" +version = "3.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "accelerate" }, + { name = "docling-core" }, + { name = "huggingface-hub" }, + { name = "jsonlines" }, + { name = "numpy" }, + { name = "pillow" }, + { name = "pydantic" }, + { name = "rtree" }, + { name = "safetensors", extra = ["torch"] }, + { name = "torch" }, + { name = "torchvision" }, + { name = "tqdm" }, + { name = "transformers" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bd/b5/f95bd7df8acc3b792fd11c6d00c8a9ea3966b10edd734fb194d07be29606/docling_ibm_models-3.14.0.tar.gz", hash = "sha256:def964e3d524f66c7321ef9d48d4021278f14319f01d3f78058cd2324f641e22", size = 100765, upload-time = "2026-08-11T06:58:25.953Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4e/96/c168e6d31397203eeed572c3b2cd1bb1efff7f9b0753cb53855fd505894a/docling_ibm_models-3.14.0-py3-none-any.whl", hash = "sha256:795d39cd0f7b1e14a702e681b0ef0f9bd31deaedddb4e2686ad577296ecb8fc9", size = 94362, upload-time = "2026-08-11T06:58:24.513Z" }, +] + +[[package]] +name = "docling-parse" +version = "7.15.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docling-core" }, + { name = "pillow" }, + { name = "pydantic" }, + { name = "pywin32", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/56/e4/1454330adcc7f850dc639a0fe7d50c69becaf1cb4d3a6cf484671cecd2de/docling_parse-7.15.0.tar.gz", hash = "sha256:fcb1b4e9a0e447f29e8248e46eea502ab4c7a75fe733a2ed911635a9b74b5092", size = 6911331, upload-time = "2026-08-20T08:31:13.52Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/cf/687829d3d52bfa63d9ea8dc928c158a55a8a35eb9a67498be89675e23bd4/docling_parse-7.15.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:b6453b243d998a73a2fbd1245d202dd315fb1f915e1ccfafdce98d391cade8ae", size = 9742716, upload-time = "2026-08-20T08:30:30.609Z" }, + { url = "https://files.pythonhosted.org/packages/a1/38/75dae655a1af5bf90fd1b5a5d3424dfd92b4b2613b89931acd827f069c75/docling_parse-7.15.0-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a00c19112264b4e6b0d7d5baa9ccd74997b6252924e512f1c88cf30de5bea1f5", size = 10263321, upload-time = "2026-08-20T08:30:33.148Z" }, + { url = "https://files.pythonhosted.org/packages/a9/20/e940091a488a9fccac7502389f2662e588364cfab736a1507c63d9149821/docling_parse-7.15.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4cb6494f8ca158c774cd7b68d96b11054ec8a92f64fb3e8cc355a8cc858997fc", size = 10659211, upload-time = "2026-08-20T08:30:34.903Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1f/237eb95370bbac40effeab82435237ba3cbd02e89e7d989f7b7d99a67f40/docling_parse-7.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:bbb301b0e7a283745fa54fa20c4279a95a0500a2228362cff5a4a6f82e740fd5", size = 11718460, upload-time = "2026-08-20T08:30:36.915Z" }, + { url = "https://files.pythonhosted.org/packages/a9/06/42755b3035f13d0dc317cf352d8e98b03df5ee414e75ad469c9eb3b7a1d1/docling_parse-7.15.0-cp311-cp311-win_arm64.whl", hash = "sha256:b59940048cf0216e09a9de9c5c1bd86d061f89e7fe0ca49f6388061626d79043", size = 9031611, upload-time = "2026-08-20T08:30:39.15Z" }, + { url = "https://files.pythonhosted.org/packages/4f/20/e9ecf45568c7d1e8c0e775190bf88bc65b7fa03adb223cd2976e4989b66a/docling_parse-7.15.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:a425599d40aa8b72a443c5fc9e22c120163d398a341af988c953e0e70f1ad0f3", size = 9744461, upload-time = "2026-08-20T08:30:41.97Z" }, + { url = "https://files.pythonhosted.org/packages/ea/fb/09afa8b1813ed0f3b691d59b80b335180e0a69bc8f2906b9aac9737c695d/docling_parse-7.15.0-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4ea8d79e870319a583a68b3e88e95d048dfa496d8b6e5928f5333fe26cc597e3", size = 10266993, upload-time = "2026-08-20T08:30:44.143Z" }, + { url = "https://files.pythonhosted.org/packages/02/34/3d290b2f5f0576645c9d056f087a77f4e45c6662eb6ce77834c85f00affe/docling_parse-7.15.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ca982b7df91f05b0bdcb28f437b2637623baccd205b941297f43b9e93aeb1f14", size = 10664829, upload-time = "2026-08-20T08:30:46.172Z" }, + { url = "https://files.pythonhosted.org/packages/08/84/4733989d2f1687c7dabb08cff13f53005878d1a6638142898f324bc233f4/docling_parse-7.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:6143e824b00dbaf14bd3692792bd0b41348f582fa7ed221ff462079935c127f6", size = 11724625, upload-time = "2026-08-20T08:30:48.371Z" }, + { url = "https://files.pythonhosted.org/packages/47/a3/1a327900335a30b86e2f1330764e340d98c693cc26ec64a1159efcc1ab5f/docling_parse-7.15.0-cp312-cp312-win_arm64.whl", hash = "sha256:f528916ec21c1bec5916db2e9c6ecac4c472fedd6aae4a9cf308f59cf8fcbd7b", size = 9032075, upload-time = "2026-08-20T08:30:50.803Z" }, + { url = "https://files.pythonhosted.org/packages/23/79/a23aa5ea3da51cf2fd320c9fd46e1c031f8b96c5762221315b2b92257a95/docling_parse-7.15.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:01d853d3b7f9c231351f9d15c1cb0746ab56c3d8329a850a1b706ef279af09ed", size = 9744478, upload-time = "2026-08-20T08:30:53.208Z" }, + { url = "https://files.pythonhosted.org/packages/cb/e3/dca5e16ace7c0bcbf6f92fd3318068be2a8d2b0bffef90d6fab5c0e09130/docling_parse-7.15.0-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa573d66c428ceed2fb3ae968823af79eb529abffaf95d60a7ed74cde9f9cc5b", size = 10266974, upload-time = "2026-08-20T08:30:55.226Z" }, + { url = "https://files.pythonhosted.org/packages/11/9f/c65a613bcd5a5fc6c024b1d0a9bb8a00a20111750c60e6b34e708c8dc683/docling_parse-7.15.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3523b631ca10180fe414c2d9cd11579171289936353fa5af3dd29caba541458d", size = 10664851, upload-time = "2026-08-20T08:30:57.243Z" }, + { url = "https://files.pythonhosted.org/packages/3d/07/cdfa96bafea1fed0a078902c38ff677656ed67bb11fe7c6140304931cad5/docling_parse-7.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:6ff600dfeb2c2d9e0e66a00efeb15d59c8d543b38c6f1bcfcb1f86156216e086", size = 11724552, upload-time = "2026-08-20T08:30:59.406Z" }, + { url = "https://files.pythonhosted.org/packages/14/ce/62e5b4a833bf640153dafe3a838e36f12fa045fb77a511c356a927ad8399/docling_parse-7.15.0-cp313-cp313-win_arm64.whl", hash = "sha256:e4bc3bf49bfd56447f35d1fe2e86897893da3b6c154f0c44643ff311e12ff8b5", size = 9032111, upload-time = "2026-08-20T08:31:01.299Z" }, + { url = "https://files.pythonhosted.org/packages/35/b4/fb0f1543245692d261f6d8da7128c60cbdb7b771f24454f834eda797b797/docling_parse-7.15.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:59b0dfa1d1ac97bc2d9d7ec457299e97afcf408bf4126e0cdc790818b8781510", size = 9744706, upload-time = "2026-08-20T08:31:03.491Z" }, + { url = "https://files.pythonhosted.org/packages/8c/4f/bf23632d5111f5c5b6fe27ce17c6d25e5db0413bcdd47ebe4f7e8346dd6f/docling_parse-7.15.0-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:88dd0b133febd1ea21cffd43f3aafeb4161b33b74f0c59ae7c7420438d1abd4a", size = 10267571, upload-time = "2026-08-20T08:31:05.541Z" }, + { url = "https://files.pythonhosted.org/packages/69/13/ef9fe96d023d08603d9d280fa5fd2912ca1ba2190c43e3a80070a858697d/docling_parse-7.15.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5fc8ddd9cb3e864f4894d89271fc239277bdf5efc6c8deeecf1560cb163b3c4c", size = 10665251, upload-time = "2026-08-20T08:31:07.589Z" }, + { url = "https://files.pythonhosted.org/packages/e5/5e/e4bcc86900f1ce665b813f1797b96aea0d7aca00585eb9b1547fcbb5aa56/docling_parse-7.15.0-cp314-cp314-win_amd64.whl", hash = "sha256:9c8d5aa23a90b5d082ed21b22a3384e13ceac4b45ce3d52230f5e05893f5cc6c", size = 12159194, upload-time = "2026-08-20T08:31:09.466Z" }, + { url = "https://files.pythonhosted.org/packages/e3/78/739780b74c5e5ee47e314f9340994f8883cb6173f99ca91e327479af46b5/docling_parse-7.15.0-cp314-cp314-win_arm64.whl", hash = "sha256:b844748fdc27a48737b6d2f0ee0a07cb25cbd340ad8535ac2ae9048f9e214b18", size = 9398542, upload-time = "2026-08-20T08:31:11.511Z" }, +] + +[[package]] +name = "docling-slim" +version = "2.121.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "docling-core" }, + { name = "filetype" }, + { name = "pluggy" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "requests" }, + { name = "tqdm" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/29/a9f372eaf99d81d49b29de33565982e0ac7444e6ec246852601694487d01/docling_slim-2.121.0.tar.gz", hash = "sha256:f3069ded34f62f4e6a4d2ffda78cba0baad2ced4944a4b2d70cb646670ef327b", size = 603636, upload-time = "2026-08-20T09:06:21.054Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/b4/e59422978e7e6a738e9a62399bfa3585b2b0eb4c331592d5e69e100d373c/docling_slim-2.121.0-py3-none-any.whl", hash = "sha256:c97fc9cd889b61a66b9cf5d2d87c6b6935f29fe7aacfd8fd39802be7425e4996", size = 746537, upload-time = "2026-08-20T09:06:19.003Z" }, +] + +[package.optional-dependencies] +standard = [ + { name = "accelerate" }, + { name = "beautifulsoup4" }, + { name = "defusedxml" }, + { name = "docling-core", extra = ["chunking"] }, + { name = "docling-ibm-models" }, + { name = "docling-parse" }, + { name = "httpx" }, + { name = "huggingface-hub" }, + { name = "mail-parser" }, + { name = "marko" }, + { name = "numpy" }, + { name = "openpyxl" }, + { name = "pillow" }, + { name = "polyfactory" }, + { name = "pylatexenc" }, + { name = "pypdfium2" }, + { name = "python-docx" }, + { name = "python-dotenv" }, + { name = "python-oxmsg" }, + { name = "python-pptx" }, + { name = "rapidocr" }, + { name = "rich" }, + { name = "rtree" }, + { name = "scipy" }, + { name = "torch" }, + { name = "torchvision" }, + { name = "typer" }, + { name = "websockets" }, +] + +[[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, upload-time = "2025-07-21T07:35:01.868Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/55/e2/2537ebcff11c1ee1ff17d8d0b6f4db75873e3b0fb32c2d4a2ee31ecb310a/docstring_parser-0.17.0-py3-none-any.whl", hash = "sha256:cf2569abd23dce8099b300f9b4fa8191e9582dda731fd533daf54c4551658708", size = 36896, upload-time = "2025-07-21T07:35:00.684Z" }, +] + +[[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, upload-time = "2024-10-25T17:25:40.039Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/8b/5fe2cc11fee489817272089c4203e679c63b570a5aaeb18d852ae3cbba6a/et_xmlfile-2.0.0-py3-none-any.whl", hash = "sha256:7a91720bc756843502c3b7504c77b8fe44217c85c537d85037f0f536151b2caa", size = 18059, upload-time = "2024-10-25T17:25:39.051Z" }, +] + +[[package]] +name = "faker" +version = "40.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "tzdata", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/70/03/14428edc541467c460d363f6e94bee9acc271f3e62470630fc9a647d0cf2/faker-40.8.0.tar.gz", hash = "sha256:936a3c9be6c004433f20aa4d99095df5dec82b8c7ad07459756041f8c1728875", size = 1956493, upload-time = "2026-03-04T16:18:48.161Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4c/3b/c6348f1e285e75b069085b18110a4e6325b763a5d35d5e204356fc7c20b3/faker-40.8.0-py3-none-any.whl", hash = "sha256:eb21bdba18f7a8375382eb94fb436fce07046893dc94cb20817d28deb0c3d579", size = 1989124, upload-time = "2026-03-04T16:18:46.45Z" }, +] + +[[package]] +name = "fastapi" +version = "0.141.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "pydantic" }, + { name = "starlette" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8a/02/91e3416a8fdd715abb903a952a6bec7cdd8d14eed55d415fc8595524c319/fastapi-0.141.1.tar.gz", hash = "sha256:e8822fc40db1e1858054d7a949a888695bc9bdce70139178e33bd2871a453ca1", size = 425799, upload-time = "2026-07-29T17:18:05.568Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/03/10388a42375ee7e4ac9b94eb2c5c569c8b5795e377e701c9ac3ad63de890/fastapi-0.141.1-py3-none-any.whl", hash = "sha256:bfb91aa2d334c61cb35ba9a116fc123b3d3df31640b801cf57a7a78ec3f603b3", size = 131954, upload-time = "2026-07-29T17:18:04.364Z" }, +] + +[[package]] +name = "filelock" +version = "3.25.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b3/8b/4c32ecde6bea6486a2a5d05340e695174351ff6b06cf651a74c005f9df00/filelock-3.25.1.tar.gz", hash = "sha256:b9a2e977f794ef94d77cdf7d27129ac648a61f585bff3ca24630c1629f701aa9", size = 40319, upload-time = "2026-03-09T19:38:47.309Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/b8/2f664b56a3b4b32d28d3d106c71783073f712ba43ff6d34b9ea0ce36dc7b/filelock-3.25.1-py3-none-any.whl", hash = "sha256:18972df45473c4aa2c7921b609ee9ca4925910cc3a0fb226c96b92fc224ef7bf", size = 26720, upload-time = "2026-03-09T19:38:45.718Z" }, +] + +[[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, upload-time = "2022-11-02T17:34:04.141Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/79/1b8fa1bb3568781e84c9200f951c735f3f157429f44be0495da55894d620/filetype-1.2.0-py2.py3-none-any.whl", hash = "sha256:7ce71b6880181241cf7ac8697a2f1eb6a8bd9b429f7ad6d27b8db9ba5f1c2d25", size = 19970, upload-time = "2022-11-02T17:34:01.425Z" }, +] + +[[package]] +name = "fsspec" +version = "2026.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/51/7c/f60c259dcbf4f0c47cc4ddb8f7720d2dcdc8888c8e5ad84c73ea4531cc5b/fsspec-2026.2.0.tar.gz", hash = "sha256:6544e34b16869f5aacd5b90bdf1a71acb37792ea3ddf6125ee69a22a53fb8bff", size = 313441, upload-time = "2026-02-05T21:50:53.743Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/ab/fb21f4c939bb440104cc2b396d3be1d9b7a9fd3c6c2a53d98c45b3d7c954/fsspec-2026.2.0-py3-none-any.whl", hash = "sha256:98de475b5cb3bd66bedd5c4679e87b4fdfe1a3bf4d707b151b3c07e58c9a2437", size = 202505, upload-time = "2026-02-05T21:50:51.819Z" }, +] + +[[package]] +name = "google-api-core" +version = "2.30.0" +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/22/98/586ec94553b569080caef635f98a3723db36a38eac0e3d7eb3ea9d2e4b9a/google_api_core-2.30.0.tar.gz", hash = "sha256:02edfa9fab31e17fc0befb5f161b3bf93c9096d99aed584625f38065c511ad9b", size = 176959, upload-time = "2026-02-18T20:28:11.926Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/27/09c33d67f7e0dcf06d7ac17d196594e66989299374bfb0d4331d1038e76b/google_api_core-2.30.0-py3-none-any.whl", hash = "sha256:80be49ee937ff9aba0fd79a6eddfde35fe658b9953ab9b79c57dd7061afa8df5", size = 173288, upload-time = "2026-02-18T20:28:10.367Z" }, +] + +[package.optional-dependencies] +grpc = [ + { name = "grpcio" }, + { name = "grpcio-status" }, +] + +[[package]] +name = "google-auth" +version = "2.49.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "pyasn1-modules" }, + { name = "rsa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/59/7371175bfd949abfb1170aa076352131d7281bd9449c0f978604fc4431c3/google_auth-2.49.0.tar.gz", hash = "sha256:9cc2d9259d3700d7a257681f81052db6737495a1a46b610597f4b8bafe5286ae", size = 333444, upload-time = "2026-03-06T21:53:06.07Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/37/45/de64b823b639103de4b63dd193480dce99526bd36be6530c2dba85bf7817/google_auth-2.49.0-py3-none-any.whl", hash = "sha256:f893ef7307f19cf53700b7e2f61b5a6affe3aa0edf9943b13788920ab92d8d87", size = 240676, upload-time = "2026-03-06T21:52:38.304Z" }, +] + +[package.optional-dependencies] +requests = [ + { name = "requests" }, +] + +[[package]] +name = "google-cloud-aiplatform" +version = "1.165.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "docstring-parser" }, + { name = "google-api-core", extra = ["grpc"] }, + { name = "google-auth" }, + { name = "google-cloud-bigquery" }, + { name = "google-cloud-resource-manager" }, + { name = "google-cloud-storage", version = "3.9.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "google-cloud-storage", version = "3.13.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, + { name = "google-genai" }, + { name = "packaging" }, + { name = "proto-plus" }, + { name = "protobuf" }, + { name = "pydantic" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/13/19/45df8264bf80dcc133c777822390c503aa75b878a57e9286d6f520b08c87/google_cloud_aiplatform-1.165.1.tar.gz", hash = "sha256:bd62ba7590255cacd66f9d0439eb731060af460d35bb48dbbeeefce9dfc0a359", size = 11325730, upload-time = "2026-08-19T21:12:30.858Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b4/66/08c325e817a2713f23a028badc72220fc4b80521e999211ad9d935176446/google_cloud_aiplatform-1.165.1-py2.py3-none-any.whl", hash = "sha256:93874bd7993d1d901291595df693a8b9d4d7c89b93d4f41f8664d73f088dec89", size = 9456088, upload-time = "2026-08-19T21:12:26.018Z" }, +] + +[[package]] +name = "google-cloud-bigquery" +version = "3.40.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-api-core", extra = ["grpc"] }, + { name = "google-auth" }, + { name = "google-cloud-core" }, + { name = "google-resumable-media" }, + { name = "packaging" }, + { name = "python-dateutil" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/11/0c/153ee546c288949fcc6794d58811ab5420f3ecad5fa7f9e73f78d9512a6e/google_cloud_bigquery-3.40.1.tar.gz", hash = "sha256:75afcfb6e007238fe1deefb2182105249321145ff921784fe7b1de2b4ba24506", size = 511761, upload-time = "2026-02-12T18:44:18.958Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/f5/081cf5b90adfe524ae0d671781b0d497a75a0f2601d075af518828e22d8f/google_cloud_bigquery-3.40.1-py3-none-any.whl", hash = "sha256:9082a6b8193aba87bed6a2c79cf1152b524c99bb7e7ac33a785e333c09eac868", size = 262018, upload-time = "2026-02-12T18:44:16.913Z" }, +] + +[[package]] +name = "google-cloud-core" +version = "2.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-api-core" }, + { name = "google-auth" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a6/03/ef0bc99d0e0faf4fdbe67ac445e18cdaa74824fd93cd069e7bb6548cb52d/google_cloud_core-2.5.0.tar.gz", hash = "sha256:7c1b7ef5c92311717bd05301aa1a91ffbc565673d3b0b4163a52d8413a186963", size = 36027, upload-time = "2025-10-29T23:17:39.513Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/89/20/bfa472e327c8edee00f04beecc80baeddd2ab33ee0e86fd7654da49d45e9/google_cloud_core-2.5.0-py3-none-any.whl", hash = "sha256:67d977b41ae6c7211ee830c7912e41003ea8194bff15ae7d72fd6f51e57acabc", size = 29469, upload-time = "2025-10-29T23:17:38.548Z" }, +] + +[[package]] +name = "google-cloud-resource-manager" +version = "1.16.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-api-core", extra = ["grpc"] }, + { name = "google-auth" }, + { name = "grpc-google-iam-v1" }, + { name = "grpcio" }, + { name = "proto-plus" }, + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4e/7f/db00b2820475793a52958dc55fe9ec2eb8e863546e05fcece9b921f86ebe/google_cloud_resource_manager-1.16.0.tar.gz", hash = "sha256:cc938f87cc36c2672f062b1e541650629e0d954c405a4dac35ceedee70c267c3", size = 459840, upload-time = "2026-01-15T13:04:07.726Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/ff/4b28bcc791d9d7e4ac8fea00fbd90ccb236afda56746a3b4564d2ae45df3/google_cloud_resource_manager-1.16.0-py3-none-any.whl", hash = "sha256:fb9a2ad2b5053c508e1c407ac31abfd1a22e91c32876c1892830724195819a28", size = 400218, upload-time = "2026-01-15T13:02:47.378Z" }, +] + +[[package]] +name = "google-cloud-storage" +version = "3.9.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version < '3.12' and sys_platform == 'darwin'", + "python_full_version < '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.12' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "google-api-core", marker = "python_full_version < '3.13'" }, + { name = "google-auth", marker = "python_full_version < '3.13'" }, + { name = "google-cloud-core", marker = "python_full_version < '3.13'" }, + { name = "google-crc32c", marker = "python_full_version < '3.13'" }, + { name = "google-resumable-media", marker = "python_full_version < '3.13'" }, + { name = "requests", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f7/b1/4f0798e88285b50dfc60ed3a7de071def538b358db2da468c2e0deecbb40/google_cloud_storage-3.9.0.tar.gz", hash = "sha256:f2d8ca7db2f652be757e92573b2196e10fbc09649b5c016f8b422ad593c641cc", size = 17298544, upload-time = "2026-02-02T13:36:34.119Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/0b/816a6ae3c9fd096937d2e5f9670558908811d57d59ddf69dd4b83b326fd1/google_cloud_storage-3.9.0-py3-none-any.whl", hash = "sha256:2dce75a9e8b3387078cbbdad44757d410ecdb916101f8ba308abf202b6968066", size = 321324, upload-time = "2026-02-02T13:36:32.271Z" }, +] + +[[package]] +name = "google-cloud-storage" +version = "3.13.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'darwin'", + "python_full_version >= '3.14' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version >= '3.14' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.14' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version == '3.13.*' and sys_platform == 'darwin'", + "python_full_version == '3.13.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.13.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "google-api-core", marker = "python_full_version >= '3.13'" }, + { name = "google-auth", marker = "python_full_version >= '3.13'" }, + { name = "google-cloud-core", marker = "python_full_version >= '3.13'" }, + { name = "google-crc32c", marker = "python_full_version >= '3.13'" }, + { name = "google-resumable-media", marker = "python_full_version >= '3.13'" }, + { name = "requests", marker = "python_full_version >= '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ce/7e/73bb7512df1d1aad6ce3f9aed847cd40e0cd400ba4a85d86ab8eb412e9cc/google_cloud_storage-3.13.1.tar.gz", hash = "sha256:a80bf8cac2794808aa61c50c5f769ecbbe2d10331bacd0d69d30e59b14b346b2", size = 17341051, upload-time = "2026-08-06T06:24:42.229Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/6f/d69f0e185e08ddb58c323a0a935af2b492907b5de362bc08933b0a3b5644/google_cloud_storage-3.13.1-py3-none-any.whl", hash = "sha256:98208de6c21e85cecd3eb44551894efff33d98365500e178867d4305854a770a", size = 341486, upload-time = "2026-08-06T06:23:36.548Z" }, +] + +[[package]] +name = "google-cloud-vectorsearch" +version = "0.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-api-core", extra = ["grpc"] }, + { name = "google-auth" }, + { name = "grpcio" }, + { name = "proto-plus" }, + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/78/fe/d5d0548a4e6773ac3c2964d1f4e1cfde4f26971d73e0c7510b77fce17ada/google_cloud_vectorsearch-0.6.0.tar.gz", hash = "sha256:b6a62f9f0aa34713982ff98650c21f052a56e8d36c663b737ad37947f0eab968", size = 405492, upload-time = "2026-03-06T21:52:53.77Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cd/4f/2a0387a5e5ae333f42cc6b2885987a453608f2c62b66d30ec54261f7b34d/google_cloud_vectorsearch-0.6.0-py3-none-any.whl", hash = "sha256:356cad75ab8dc79c03f84991743bec5ac3b90e67b35a7e77838d8454e5d3b4d2", size = 344593, upload-time = "2026-03-06T21:52:40.697Z" }, +] + +[[package]] +name = "google-crc32c" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/03/41/4b9c02f99e4c5fb477122cd5437403b552873f014616ac1d19ac8221a58d/google_crc32c-1.8.0.tar.gz", hash = "sha256:a428e25fb7691024de47fecfbff7ff957214da51eddded0da0ae0e0f03a2cf79", size = 14192, upload-time = "2025-12-16T00:35:25.142Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/ef/21ccfaab3d5078d41efe8612e0ed0bfc9ce22475de074162a91a25f7980d/google_crc32c-1.8.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:014a7e68d623e9a4222d663931febc3033c5c7c9730785727de2a81f87d5bab8", size = 31298, upload-time = "2025-12-16T00:20:32.241Z" }, + { url = "https://files.pythonhosted.org/packages/c5/b8/f8413d3f4b676136e965e764ceedec904fe38ae8de0cdc52a12d8eb1096e/google_crc32c-1.8.0-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:86cfc00fe45a0ac7359e5214a1704e51a99e757d0272554874f419f79838c5f7", size = 30872, upload-time = "2025-12-16T00:33:58.785Z" }, + { url = "https://files.pythonhosted.org/packages/f6/fd/33aa4ec62b290477181c55bb1c9302c9698c58c0ce9a6ab4874abc8b0d60/google_crc32c-1.8.0-cp311-cp311-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:19b40d637a54cb71e0829179f6cb41835f0fbd9e8eb60552152a8b52c36cbe15", size = 33243, upload-time = "2025-12-16T00:40:21.46Z" }, + { url = "https://files.pythonhosted.org/packages/71/03/4820b3bd99c9653d1a5210cb32f9ba4da9681619b4d35b6a052432df4773/google_crc32c-1.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:17446feb05abddc187e5441a45971b8394ea4c1b6efd88ab0af393fd9e0a156a", size = 33608, upload-time = "2025-12-16T00:40:22.204Z" }, + { url = "https://files.pythonhosted.org/packages/7c/43/acf61476a11437bf9733fb2f70599b1ced11ec7ed9ea760fdd9a77d0c619/google_crc32c-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:71734788a88f551fbd6a97be9668a0020698e07b2bf5b3aa26a36c10cdfb27b2", size = 34439, upload-time = "2025-12-16T00:35:20.458Z" }, + { url = "https://files.pythonhosted.org/packages/e9/5f/7307325b1198b59324c0fa9807cafb551afb65e831699f2ce211ad5c8240/google_crc32c-1.8.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:4b8286b659c1335172e39563ab0a768b8015e88e08329fa5321f774275fc3113", size = 31300, upload-time = "2025-12-16T00:21:56.723Z" }, + { url = "https://files.pythonhosted.org/packages/21/8e/58c0d5d86e2220e6a37befe7e6a94dd2f6006044b1a33edf1ff6d9f7e319/google_crc32c-1.8.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:2a3dc3318507de089c5384cc74d54318401410f82aa65b2d9cdde9d297aca7cb", size = 30867, upload-time = "2025-12-16T00:38:31.302Z" }, + { url = "https://files.pythonhosted.org/packages/ce/a9/a780cc66f86335a6019f557a8aaca8fbb970728f0efd2430d15ff1beae0e/google_crc32c-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:14f87e04d613dfa218d6135e81b78272c3b904e2a7053b841481b38a7d901411", size = 33364, upload-time = "2025-12-16T00:40:22.96Z" }, + { url = "https://files.pythonhosted.org/packages/21/3f/3457ea803db0198c9aaca2dd373750972ce28a26f00544b6b85088811939/google_crc32c-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cb5c869c2923d56cb0c8e6bcdd73c009c36ae39b652dbe46a05eb4ef0ad01454", size = 33740, upload-time = "2025-12-16T00:40:23.96Z" }, + { url = "https://files.pythonhosted.org/packages/df/c0/87c2073e0c72515bb8733d4eef7b21548e8d189f094b5dad20b0ecaf64f6/google_crc32c-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:3cc0c8912038065eafa603b238abf252e204accab2a704c63b9e14837a854962", size = 34437, upload-time = "2025-12-16T00:35:21.395Z" }, + { url = "https://files.pythonhosted.org/packages/d1/db/000f15b41724589b0e7bc24bc7a8967898d8d3bc8caf64c513d91ef1f6c0/google_crc32c-1.8.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:3ebb04528e83b2634857f43f9bb8ef5b2bbe7f10f140daeb01b58f972d04736b", size = 31297, upload-time = "2025-12-16T00:23:20.709Z" }, + { url = "https://files.pythonhosted.org/packages/d7/0d/8ebed0c39c53a7e838e2a486da8abb0e52de135f1b376ae2f0b160eb4c1a/google_crc32c-1.8.0-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:450dc98429d3e33ed2926fc99ee81001928d63460f8538f21a5d6060912a8e27", size = 30867, upload-time = "2025-12-16T00:43:14.628Z" }, + { url = "https://files.pythonhosted.org/packages/ce/42/b468aec74a0354b34c8cbf748db20d6e350a68a2b0912e128cabee49806c/google_crc32c-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3b9776774b24ba76831609ffbabce8cdf6fa2bd5e9df37b594221c7e333a81fa", size = 33344, upload-time = "2025-12-16T00:40:24.742Z" }, + { url = "https://files.pythonhosted.org/packages/1c/e8/b33784d6fc77fb5062a8a7854e43e1e618b87d5ddf610a88025e4de6226e/google_crc32c-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:89c17d53d75562edfff86679244830599ee0a48efc216200691de8b02ab6b2b8", size = 33694, upload-time = "2025-12-16T00:40:25.505Z" }, + { url = "https://files.pythonhosted.org/packages/92/b1/d3cbd4d988afb3d8e4db94ca953df429ed6db7282ed0e700d25e6c7bfc8d/google_crc32c-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:57a50a9035b75643996fbf224d6661e386c7162d1dfdab9bc4ca790947d1007f", size = 34435, upload-time = "2025-12-16T00:35:22.107Z" }, + { url = "https://files.pythonhosted.org/packages/21/88/8ecf3c2b864a490b9e7010c84fd203ec8cf3b280651106a3a74dd1b0ca72/google_crc32c-1.8.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:e6584b12cb06796d285d09e33f63309a09368b9d806a551d8036a4207ea43697", size = 31301, upload-time = "2025-12-16T00:24:48.527Z" }, + { url = "https://files.pythonhosted.org/packages/36/c6/f7ff6c11f5ca215d9f43d3629163727a272eabc356e5c9b2853df2bfe965/google_crc32c-1.8.0-cp314-cp314-macosx_12_0_x86_64.whl", hash = "sha256:f4b51844ef67d6cf2e9425983274da75f18b1597bb2c998e1c0a0e8d46f8f651", size = 30868, upload-time = "2025-12-16T00:48:12.163Z" }, + { url = "https://files.pythonhosted.org/packages/56/15/c25671c7aad70f8179d858c55a6ae8404902abe0cdcf32a29d581792b491/google_crc32c-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b0d1a7afc6e8e4635564ba8aa5c0548e3173e41b6384d7711a9123165f582de2", size = 33381, upload-time = "2025-12-16T00:40:26.268Z" }, + { url = "https://files.pythonhosted.org/packages/42/fa/f50f51260d7b0ef5d4898af122d8a7ec5a84e2984f676f746445f783705f/google_crc32c-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8b3f68782f3cbd1bce027e48768293072813469af6a61a86f6bb4977a4380f21", size = 33734, upload-time = "2025-12-16T00:40:27.028Z" }, + { url = "https://files.pythonhosted.org/packages/08/a5/7b059810934a09fb3ccb657e0843813c1fee1183d3bc2c8041800374aa2c/google_crc32c-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:d511b3153e7011a27ab6ee6bb3a5404a55b994dc1a7322c0b87b29606d9790e2", size = 34878, upload-time = "2025-12-16T00:35:23.142Z" }, + { url = "https://files.pythonhosted.org/packages/52/c5/c171e4d8c44fec1422d801a6d2e5d7ddabd733eeda505c79730ee9607f07/google_crc32c-1.8.0-pp311-pypy311_pp73-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:87fa445064e7db928226b2e6f0d5304ab4cd0339e664a4e9a25029f384d9bb93", size = 28615, upload-time = "2025-12-16T00:40:29.298Z" }, + { url = "https://files.pythonhosted.org/packages/9c/97/7d75fe37a7a6ed171a2cf17117177e7aab7e6e0d115858741b41e9dd4254/google_crc32c-1.8.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f639065ea2042d5c034bf258a9f085eaa7af0cd250667c0635a3118e8f92c69c", size = 28800, upload-time = "2025-12-16T00:40:30.322Z" }, +] + +[[package]] +name = "google-genai" +version = "1.66.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "google-auth", extra = ["requests"] }, + { name = "httpx" }, + { name = "pydantic" }, + { name = "requests" }, + { name = "sniffio" }, + { name = "tenacity" }, + { name = "typing-extensions" }, + { name = "websockets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9b/ba/0b343b0770d4710ad2979fd9301d7caa56c940174d5361ed4a7cc4979241/google_genai-1.66.0.tar.gz", hash = "sha256:ffc01647b65046bca6387320057aa51db0ad64bcc72c8e3e914062acfa5f7c49", size = 504386, upload-time = "2026-03-04T22:15:28.156Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/dd/403949d922d4e261b08b64aaa132af4e456c3b15c8e2a2d9e6ef693f66e2/google_genai-1.66.0-py3-none-any.whl", hash = "sha256:7f127a39cf695277104ce4091bb26e417c59bb46e952ff3699c3a982d9c474ee", size = 732174, upload-time = "2026-03-04T22:15:26.63Z" }, +] + +[[package]] +name = "google-resumable-media" +version = "2.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-crc32c" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/64/d7/520b62a35b23038ff005e334dba3ffc75fcf583bee26723f1fd8fd4b6919/google_resumable_media-2.8.0.tar.gz", hash = "sha256:f1157ed8b46994d60a1bc432544db62352043113684d4e030ee02e77ebe9a1ae", size = 2163265, upload-time = "2025-11-17T15:38:06.659Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/0b/93afde9cfe012260e9fe1522f35c9b72d6ee222f316586b1f23ecf44d518/google_resumable_media-2.8.0-py3-none-any.whl", hash = "sha256:dd14a116af303845a8d932ddae161a26e86cc229645bc98b39f026f9b1717582", size = 81340, upload-time = "2025-11-17T15:38:05.594Z" }, +] + +[[package]] +name = "googleapis-common-protos" +version = "1.73.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/99/96/a0205167fa0154f4a542fd6925bdc63d039d88dab3588b875078107e6f06/googleapis_common_protos-1.73.0.tar.gz", hash = "sha256:778d07cd4fbeff84c6f7c72102f0daf98fa2bfd3fa8bea426edc545588da0b5a", size = 147323, upload-time = "2026-03-06T21:53:09.727Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/28/23eea8acd65972bbfe295ce3666b28ac510dfcb115fac089d3edb0feb00a/googleapis_common_protos-1.73.0-py3-none-any.whl", hash = "sha256:dfdaaa2e860f242046be561e6d6cb5c5f1541ae02cfbcb034371aadb2942b4e8", size = 297578, upload-time = "2026-03-06T21:52:33.933Z" }, +] + +[package.optional-dependencies] +grpc = [ + { name = "grpcio" }, +] + +[[package]] +name = "grpc-google-iam-v1" +version = "0.14.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "googleapis-common-protos", extra = ["grpc"] }, + { name = "grpcio" }, + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/1e/1011451679a983f2f5c6771a1682542ecb027776762ad031fd0d7129164b/grpc_google_iam_v1-0.14.3.tar.gz", hash = "sha256:879ac4ef33136c5491a6300e27575a9ec760f6cdf9a2518798c1b8977a5dc389", size = 23745, upload-time = "2025-10-15T21:14:53.318Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/bd/330a1bbdb1afe0b96311249e699b6dc9cfc17916394fd4503ac5aca2514b/grpc_google_iam_v1-0.14.3-py3-none-any.whl", hash = "sha256:7a7f697e017a067206a3dfef44e4c634a34d3dee135fe7d7a4613fe3e59217e6", size = 32690, upload-time = "2025-10-15T21:14:51.72Z" }, +] + +[[package]] +name = "grpcio" +version = "1.78.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/8a/3d098f35c143a89520e568e6539cc098fcd294495910e359889ce8741c84/grpcio-1.78.0.tar.gz", hash = "sha256:7382b95189546f375c174f53a5fa873cef91c4b8005faa05cc5b3beea9c4f1c5", size = 12852416, upload-time = "2026-02-06T09:57:18.093Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/86/c7/d0b780a29b0837bf4ca9580904dfb275c1fc321ded7897d620af7047ec57/grpcio-1.78.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:2777b783f6c13b92bd7b716667452c329eefd646bfb3f2e9dabea2e05dbd34f6", size = 5951525, upload-time = "2026-02-06T09:55:01.989Z" }, + { url = "https://files.pythonhosted.org/packages/c5/b1/96920bf2ee61df85a9503cb6f733fe711c0ff321a5a697d791b075673281/grpcio-1.78.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:9dca934f24c732750389ce49d638069c3892ad065df86cb465b3fa3012b70c9e", size = 11830418, upload-time = "2026-02-06T09:55:04.462Z" }, + { url = "https://files.pythonhosted.org/packages/83/0c/7c1528f098aeb75a97de2bae18c530f56959fb7ad6c882db45d9884d6edc/grpcio-1.78.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:459ab414b35f4496138d0ecd735fed26f1318af5e52cb1efbc82a09f0d5aa911", size = 6524477, upload-time = "2026-02-06T09:55:07.111Z" }, + { url = "https://files.pythonhosted.org/packages/8d/52/e7c1f3688f949058e19a011c4e0dec973da3d0ae5e033909677f967ae1f4/grpcio-1.78.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:082653eecbdf290e6e3e2c276ab2c54b9e7c299e07f4221872380312d8cf395e", size = 7198266, upload-time = "2026-02-06T09:55:10.016Z" }, + { url = "https://files.pythonhosted.org/packages/e5/61/8ac32517c1e856677282c34f2e7812d6c328fa02b8f4067ab80e77fdc9c9/grpcio-1.78.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:85f93781028ec63f383f6bc90db785a016319c561cc11151fbb7b34e0d012303", size = 6730552, upload-time = "2026-02-06T09:55:12.207Z" }, + { url = "https://files.pythonhosted.org/packages/bd/98/b8ee0158199250220734f620b12e4a345955ac7329cfd908d0bf0fda77f0/grpcio-1.78.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f12857d24d98441af6a1d5c87442d624411db486f7ba12550b07788f74b67b04", size = 7304296, upload-time = "2026-02-06T09:55:15.044Z" }, + { url = "https://files.pythonhosted.org/packages/bd/0f/7b72762e0d8840b58032a56fdbd02b78fc645b9fa993d71abf04edbc54f4/grpcio-1.78.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5397fff416b79e4b284959642a4e95ac4b0f1ece82c9993658e0e477d40551ec", size = 8288298, upload-time = "2026-02-06T09:55:17.276Z" }, + { url = "https://files.pythonhosted.org/packages/24/ae/ae4ce56bc5bb5caa3a486d60f5f6083ac3469228faa734362487176c15c5/grpcio-1.78.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:fbe6e89c7ffb48518384068321621b2a69cab509f58e40e4399fdd378fa6d074", size = 7730953, upload-time = "2026-02-06T09:55:19.545Z" }, + { url = "https://files.pythonhosted.org/packages/b5/6e/8052e3a28eb6a820c372b2eb4b5e32d195c661e137d3eca94d534a4cfd8a/grpcio-1.78.0-cp311-cp311-win32.whl", hash = "sha256:6092beabe1966a3229f599d7088b38dfc8ffa1608b5b5cdda31e591e6500f856", size = 4076503, upload-time = "2026-02-06T09:55:21.521Z" }, + { url = "https://files.pythonhosted.org/packages/08/62/f22c98c5265dfad327251fa2f840b591b1df5f5e15d88b19c18c86965b27/grpcio-1.78.0-cp311-cp311-win_amd64.whl", hash = "sha256:1afa62af6e23f88629f2b29ec9e52ec7c65a7176c1e0a83292b93c76ca882558", size = 4799767, upload-time = "2026-02-06T09:55:24.107Z" }, + { url = "https://files.pythonhosted.org/packages/4e/f4/7384ed0178203d6074446b3c4f46c90a22ddf7ae0b3aee521627f54cfc2a/grpcio-1.78.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:f9ab915a267fc47c7e88c387a3a28325b58c898e23d4995f765728f4e3dedb97", size = 5913985, upload-time = "2026-02-06T09:55:26.832Z" }, + { url = "https://files.pythonhosted.org/packages/81/ed/be1caa25f06594463f685b3790b320f18aea49b33166f4141bfdc2bfb236/grpcio-1.78.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:3f8904a8165ab21e07e58bf3e30a73f4dffc7a1e0dbc32d51c61b5360d26f43e", size = 11811853, upload-time = "2026-02-06T09:55:29.224Z" }, + { url = "https://files.pythonhosted.org/packages/24/a7/f06d151afc4e64b7e3cc3e872d331d011c279aaab02831e40a81c691fb65/grpcio-1.78.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:859b13906ce098c0b493af92142ad051bf64c7870fa58a123911c88606714996", size = 6475766, upload-time = "2026-02-06T09:55:31.825Z" }, + { url = "https://files.pythonhosted.org/packages/8a/a8/4482922da832ec0082d0f2cc3a10976d84a7424707f25780b82814aafc0a/grpcio-1.78.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b2342d87af32790f934a79c3112641e7b27d63c261b8b4395350dad43eff1dc7", size = 7170027, upload-time = "2026-02-06T09:55:34.7Z" }, + { url = "https://files.pythonhosted.org/packages/54/bf/f4a3b9693e35d25b24b0b39fa46d7d8a3c439e0a3036c3451764678fec20/grpcio-1.78.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:12a771591ae40bc65ba67048fa52ef4f0e6db8279e595fd349f9dfddeef571f9", size = 6690766, upload-time = "2026-02-06T09:55:36.902Z" }, + { url = "https://files.pythonhosted.org/packages/c7/b9/521875265cc99fe5ad4c5a17010018085cae2810a928bf15ebe7d8bcd9cc/grpcio-1.78.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:185dea0d5260cbb2d224c507bf2a5444d5abbb1fa3594c1ed7e4c709d5eb8383", size = 7266161, upload-time = "2026-02-06T09:55:39.824Z" }, + { url = "https://files.pythonhosted.org/packages/05/86/296a82844fd40a4ad4a95f100b55044b4f817dece732bf686aea1a284147/grpcio-1.78.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:51b13f9aed9d59ee389ad666b8c2214cc87b5de258fa712f9ab05f922e3896c6", size = 8253303, upload-time = "2026-02-06T09:55:42.353Z" }, + { url = "https://files.pythonhosted.org/packages/f3/e4/ea3c0caf5468537f27ad5aab92b681ed7cc0ef5f8c9196d3fd42c8c2286b/grpcio-1.78.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fd5f135b1bd58ab088930b3c613455796dfa0393626a6972663ccdda5b4ac6ce", size = 7698222, upload-time = "2026-02-06T09:55:44.629Z" }, + { url = "https://files.pythonhosted.org/packages/d7/47/7f05f81e4bb6b831e93271fb12fd52ba7b319b5402cbc101d588f435df00/grpcio-1.78.0-cp312-cp312-win32.whl", hash = "sha256:94309f498bcc07e5a7d16089ab984d42ad96af1d94b5a4eb966a266d9fcabf68", size = 4066123, upload-time = "2026-02-06T09:55:47.644Z" }, + { url = "https://files.pythonhosted.org/packages/ad/e7/d6914822c88aa2974dbbd10903d801a28a19ce9cd8bad7e694cbbcf61528/grpcio-1.78.0-cp312-cp312-win_amd64.whl", hash = "sha256:9566fe4ababbb2610c39190791e5b829869351d14369603702e890ef3ad2d06e", size = 4797657, upload-time = "2026-02-06T09:55:49.86Z" }, + { url = "https://files.pythonhosted.org/packages/05/a9/8f75894993895f361ed8636cd9237f4ab39ef87fd30db17467235ed1c045/grpcio-1.78.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:ce3a90455492bf8bfa38e56fbbe1dbd4f872a3d8eeaf7337dc3b1c8aa28c271b", size = 5920143, upload-time = "2026-02-06T09:55:52.035Z" }, + { url = "https://files.pythonhosted.org/packages/55/06/0b78408e938ac424100100fd081189451b472236e8a3a1f6500390dc4954/grpcio-1.78.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:2bf5e2e163b356978b23652c4818ce4759d40f4712ee9ec5a83c4be6f8c23a3a", size = 11803926, upload-time = "2026-02-06T09:55:55.494Z" }, + { url = "https://files.pythonhosted.org/packages/88/93/b59fe7832ff6ae3c78b813ea43dac60e295fa03606d14d89d2e0ec29f4f3/grpcio-1.78.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8f2ac84905d12918e4e55a16da17939eb63e433dc11b677267c35568aa63fc84", size = 6478628, upload-time = "2026-02-06T09:55:58.533Z" }, + { url = "https://files.pythonhosted.org/packages/ed/df/e67e3734527f9926b7d9c0dde6cd998d1d26850c3ed8eeec81297967ac67/grpcio-1.78.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b58f37edab4a3881bc6c9bca52670610e0c9ca14e2ea3cf9debf185b870457fb", size = 7173574, upload-time = "2026-02-06T09:56:01.786Z" }, + { url = "https://files.pythonhosted.org/packages/a6/62/cc03fffb07bfba982a9ec097b164e8835546980aec25ecfa5f9c1a47e022/grpcio-1.78.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:735e38e176a88ce41840c21bb49098ab66177c64c82426e24e0082500cc68af5", size = 6692639, upload-time = "2026-02-06T09:56:04.529Z" }, + { url = "https://files.pythonhosted.org/packages/bf/9a/289c32e301b85bdb67d7ec68b752155e674ee3ba2173a1858f118e399ef3/grpcio-1.78.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2045397e63a7a0ee7957c25f7dbb36ddc110e0cfb418403d110c0a7a68a844e9", size = 7268838, upload-time = "2026-02-06T09:56:08.397Z" }, + { url = "https://files.pythonhosted.org/packages/0e/79/1be93f32add280461fa4773880196572563e9c8510861ac2da0ea0f892b6/grpcio-1.78.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:a9f136fbafe7ccf4ac7e8e0c28b31066e810be52d6e344ef954a3a70234e1702", size = 8251878, upload-time = "2026-02-06T09:56:10.914Z" }, + { url = "https://files.pythonhosted.org/packages/65/65/793f8e95296ab92e4164593674ae6291b204bb5f67f9d4a711489cd30ffa/grpcio-1.78.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:748b6138585379c737adc08aeffd21222abbda1a86a0dca2a39682feb9196c20", size = 7695412, upload-time = "2026-02-06T09:56:13.593Z" }, + { url = "https://files.pythonhosted.org/packages/1c/9f/1e233fe697ecc82845942c2822ed06bb522e70d6771c28d5528e4c50f6a4/grpcio-1.78.0-cp313-cp313-win32.whl", hash = "sha256:271c73e6e5676afe4fc52907686670c7cea22ab2310b76a59b678403ed40d670", size = 4064899, upload-time = "2026-02-06T09:56:15.601Z" }, + { url = "https://files.pythonhosted.org/packages/4d/27/d86b89e36de8a951501fb06a0f38df19853210f341d0b28f83f4aa0ffa08/grpcio-1.78.0-cp313-cp313-win_amd64.whl", hash = "sha256:f2d4e43ee362adfc05994ed479334d5a451ab7bc3f3fee1b796b8ca66895acb4", size = 4797393, upload-time = "2026-02-06T09:56:17.882Z" }, + { url = "https://files.pythonhosted.org/packages/29/f2/b56e43e3c968bfe822fa6ce5bca10d5c723aa40875b48791ce1029bb78c7/grpcio-1.78.0-cp314-cp314-linux_armv7l.whl", hash = "sha256:e87cbc002b6f440482b3519e36e1313eb5443e9e9e73d6a52d43bd2004fcfd8e", size = 5920591, upload-time = "2026-02-06T09:56:20.758Z" }, + { url = "https://files.pythonhosted.org/packages/5d/81/1f3b65bd30c334167bfa8b0d23300a44e2725ce39bba5b76a2460d85f745/grpcio-1.78.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:c41bc64626db62e72afec66b0c8a0da76491510015417c127bfc53b2fe6d7f7f", size = 11813685, upload-time = "2026-02-06T09:56:24.315Z" }, + { url = "https://files.pythonhosted.org/packages/0e/1c/bbe2f8216a5bd3036119c544d63c2e592bdf4a8ec6e4a1867592f4586b26/grpcio-1.78.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8dfffba826efcf366b1e3ccc37e67afe676f290e13a3b48d31a46739f80a8724", size = 6487803, upload-time = "2026-02-06T09:56:27.367Z" }, + { url = "https://files.pythonhosted.org/packages/16/5c/a6b2419723ea7ddce6308259a55e8e7593d88464ce8db9f4aa857aba96fa/grpcio-1.78.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:74be1268d1439eaaf552c698cdb11cd594f0c49295ae6bb72c34ee31abbe611b", size = 7173206, upload-time = "2026-02-06T09:56:29.876Z" }, + { url = "https://files.pythonhosted.org/packages/df/1e/b8801345629a415ea7e26c83d75eb5dbe91b07ffe5210cc517348a8d4218/grpcio-1.78.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:be63c88b32e6c0f1429f1398ca5c09bc64b0d80950c8bb7807d7d7fb36fb84c7", size = 6693826, upload-time = "2026-02-06T09:56:32.305Z" }, + { url = "https://files.pythonhosted.org/packages/34/84/0de28eac0377742679a510784f049738a80424b17287739fc47d63c2439e/grpcio-1.78.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:3c586ac70e855c721bda8f548d38c3ca66ac791dc49b66a8281a1f99db85e452", size = 7277897, upload-time = "2026-02-06T09:56:34.915Z" }, + { url = "https://files.pythonhosted.org/packages/ca/9c/ad8685cfe20559a9edb66f735afdcb2b7d3de69b13666fdfc542e1916ebd/grpcio-1.78.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:35eb275bf1751d2ffbd8f57cdbc46058e857cf3971041521b78b7db94bdaf127", size = 8252404, upload-time = "2026-02-06T09:56:37.553Z" }, + { url = "https://files.pythonhosted.org/packages/3c/05/33a7a4985586f27e1de4803887c417ec7ced145ebd069bc38a9607059e2b/grpcio-1.78.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:207db540302c884b8848036b80db352a832b99dfdf41db1eb554c2c2c7800f65", size = 7696837, upload-time = "2026-02-06T09:56:40.173Z" }, + { url = "https://files.pythonhosted.org/packages/73/77/7382241caf88729b106e49e7d18e3116216c778e6a7e833826eb96de22f7/grpcio-1.78.0-cp314-cp314-win32.whl", hash = "sha256:57bab6deef2f4f1ca76cc04565df38dc5713ae6c17de690721bdf30cb1e0545c", size = 4142439, upload-time = "2026-02-06T09:56:43.258Z" }, + { url = "https://files.pythonhosted.org/packages/48/b2/b096ccce418882fbfda4f7496f9357aaa9a5af1896a9a7f60d9f2b275a06/grpcio-1.78.0-cp314-cp314-win_amd64.whl", hash = "sha256:dce09d6116df20a96acfdbf85e4866258c3758180e8c49845d6ba8248b6d0bbb", size = 4929852, upload-time = "2026-02-06T09:56:45.885Z" }, +] + +[[package]] +name = "grpcio-status" +version = "1.78.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "googleapis-common-protos" }, + { name = "grpcio" }, + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8a/cd/89ce482a931b543b92cdd9b2888805518c4620e0094409acb8c81dd4610a/grpcio_status-1.78.0.tar.gz", hash = "sha256:a34cfd28101bfea84b5aa0f936b4b423019e9213882907166af6b3bddc59e189", size = 13808, upload-time = "2026-02-06T10:01:48.034Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/8a/1241ec22c41028bddd4a052ae9369267b4475265ad0ce7140974548dc3fa/grpcio_status-1.78.0-py3-none-any.whl", hash = "sha256:b492b693d4bf27b47a6c32590701724f1d3b9444b36491878fb71f6208857f34", size = 14523, upload-time = "2026-02-06T10:01:32.584Z" }, +] + +[[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, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "hf-xet" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/ab/522a2ab67f27971a9d48ca666d4fca85ef7d5282d142e31fd087e27b1bbe/hf_xet-1.6.0.tar.gz", hash = "sha256:2e58454a340b3556dfa4972d5451aff4fba8dd42a236600ba1a1d2b1514f0fef", size = 920527, upload-time = "2026-08-03T22:33:13.243Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/62/3c062f593bd92ef4e77a0ef39541e3d82a0a1d3947c8a777a02a13a27828/hf_xet-1.6.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:70cbb9c896901600128cb9b6f06e132954fbede1db30f31f7c6c63f84cb7c31d", size = 4074584, upload-time = "2026-08-03T22:32:47.364Z" }, + { url = "https://files.pythonhosted.org/packages/bb/1e/c0ad437dd267a8e435bef594acf781bbc3874ff0b6435b4962d03ecf7cc4/hf_xet-1.6.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:23379c2f9ec8696d952b16414a2bae72cad86a52df869b050698ba60f538c675", size = 3867381, upload-time = "2026-08-03T22:32:49.049Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ee/7c0d7b6ab336167531b1c30af2af003f054af4c749becbd7209ae33a77c3/hf_xet-1.6.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f2f7278c05c22fd60cb436cda1269649b3e81db65ecdc8496e5e164aa4143e7b", size = 4453982, upload-time = "2026-08-03T22:32:50.568Z" }, + { url = "https://files.pythonhosted.org/packages/63/06/ad8eab1c9525246650cbaa821caa3cdbaca734ab1a5b8c91bea09cbd8d69/hf_xet-1.6.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:948f15d3a9545cfe5932f6bd8b440f6ae630aee108f14b7bd6c561f7c2dcc522", size = 4249445, upload-time = "2026-08-03T22:32:52.391Z" }, + { url = "https://files.pythonhosted.org/packages/d8/26/1eee8aedb0dafc1ab9717dc9ac602cde33361b232dc06803f1f6ed18b58c/hf_xet-1.6.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5153e6bb103ad49d6ea9f1b2e230db5a2ea32551ad09a706d2f61d7c7c80d80e", size = 4451099, upload-time = "2026-08-03T22:32:54.114Z" }, + { url = "https://files.pythonhosted.org/packages/67/57/0b88af1f194ab6c9c650547d9cc06bfeaab836ae4dcdb331676bfb8be95a/hf_xet-1.6.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:35cec30d75c6f9eb9c16a77cef68e85a103b72e24d4b473714ec9ff06428bab9", size = 4664712, upload-time = "2026-08-03T22:32:55.547Z" }, + { url = "https://files.pythonhosted.org/packages/53/a0/26b717a9d1840e8abf48dcec64b5ed8fbe472671d38ad28d30e147132b33/hf_xet-1.6.0-cp314-cp314t-win_amd64.whl", hash = "sha256:5789835d7c6bc9436962853192082374297fb72d7eff7e7762ec25ceb7e25338", size = 4025906, upload-time = "2026-08-03T22:32:57.391Z" }, + { url = "https://files.pythonhosted.org/packages/49/f6/4a9966633c6fef83af997e2cff68ec1963676d412bdfd096df2a93b8e185/hf_xet-1.6.0-cp314-cp314t-win_arm64.whl", hash = "sha256:75765820ce4700db3750c94acc8fe27c5fae4c9ec000a0dbac3ca082acf97765", size = 3849221, upload-time = "2026-08-03T22:32:59.123Z" }, + { url = "https://files.pythonhosted.org/packages/a2/50/7afa2c9c787405864fc47a0d1bbc02c62e9101947ed43c1f43899fc7d91d/hf_xet-1.6.0-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:633dc0cd71d32da58ab8c03ad38e2fac452c15c2b0a2866ebf6ededfe0a5061d", size = 4071729, upload-time = "2026-08-03T22:33:00.721Z" }, + { url = "https://files.pythonhosted.org/packages/4b/69/55b8dcf636142ae660fec1869fcac14c4da2e8412e14d6eee1523be77e9f/hf_xet-1.6.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:f0906082d9932ae0c0057fa194041c22b4e2cdb46b2592ef3b91f020d62a081a", size = 3876287, upload-time = "2026-08-03T22:33:02.251Z" }, + { url = "https://files.pythonhosted.org/packages/67/4e/a28359bf1c1ecf11eba22123168c138698f7cb576ac678f5a2e16cd5da08/hf_xet-1.6.0-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d62671bb130879cef0ee4c9ebe47a14af6c66ec53e6d84dc15936e5ffdfac82f", size = 4464663, upload-time = "2026-08-03T22:33:03.802Z" }, + { url = "https://files.pythonhosted.org/packages/9a/69/1f0cbc2fb22ae6082d094f743d1b8945a3f36f6089cb95f42b7ee348cda7/hf_xet-1.6.0-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:0e6e21fa3cdfcdcd76748564bf593870a5e013f47d97cf10aed63aa222cff5b7", size = 4262538, upload-time = "2026-08-03T22:33:05.287Z" }, + { url = "https://files.pythonhosted.org/packages/d1/3a/4f4f2301ade26e404462d3336fa11f7958d914cabbabdd6e03c3c5d5658c/hf_xet-1.6.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:4fc74352a17015bd0ee90038bc9efe38db894cde45f268b6712b04fce8cd0acb", size = 4460520, upload-time = "2026-08-03T22:33:06.81Z" }, + { url = "https://files.pythonhosted.org/packages/ab/5f/311725e2a905534dfee2dcb5b08414f249147f1f12252bfc2bd24caa075c/hf_xet-1.6.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:8fb4f71cba6129110c3374a33f919001ff130488fc23553698e34cc1c2a1198c", size = 4675937, upload-time = "2026-08-03T22:33:08.616Z" }, + { url = "https://files.pythonhosted.org/packages/98/b7/8c59a66d15205024662f1d66968136f13893f96df1ddc5087e2e281fc95f/hf_xet-1.6.0-cp38-abi3-win_amd64.whl", hash = "sha256:fb4fadde1b2b70bf4c0c14a6dccbe7194b1c28947fefd5bbe3fed9d940676c3b", size = 4033128, upload-time = "2026-08-03T22:33:10.171Z" }, + { url = "https://files.pythonhosted.org/packages/73/63/ca511b6f802f28cf3489b280fe77475bcca8de85e81a6299d7916b5b5555/hf_xet-1.6.0-cp38-abi3-win_arm64.whl", hash = "sha256:3dc3e35441ba395006af5aaacc40ef2e603c51ef46c3530b9156185f00935ea3", size = 3859359, upload-time = "2026-08-03T22:33:11.725Z" }, +] + +[[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, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[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, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "httpx-sse" +version = "0.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/4c/751061ffa58615a32c31b2d82e8482be8dd4a89154f003147acee90f2be9/httpx_sse-0.4.3.tar.gz", hash = "sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d", size = 15943, upload-time = "2025-10-10T21:48:22.271Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/fd/6668e5aec43ab844de6fc74927e155a3b37bf40d7c3790e49fc0406b6578/httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc", size = 8960, upload-time = "2025-10-10T21:48:21.158Z" }, +] + +[[package]] +name = "huggingface-hub" +version = "1.28.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "filelock" }, + { name = "fsspec" }, + { name = "hf-xet", marker = "platform_machine == 'AMD64' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" }, + { name = "httpx" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "tqdm" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c6/ae/222a91937ebee7f62c0ca8f5ee0afd97577caf24c0abb927d1f5c7e9f6d2/huggingface_hub-1.28.0.tar.gz", hash = "sha256:46a2e950c09234de54093d587d1675382f0d08dbd600d9fb599b5932f5b2c6cb", size = 959609, upload-time = "2026-08-18T12:27:15.101Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/0e/eafef18f1a75e125e68395db21131db0cf868a128ecd2fce69b4df6c584b/huggingface_hub-1.28.0-py3-none-any.whl", hash = "sha256:58a8bacb03072edfc38067065e9dc24bbb34805410fcd36a1632de0b329660bb", size = 793202, upload-time = "2026-08-18T12:27:12.719Z" }, +] + +[[package]] +name = "idna" +version = "3.19" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/f7/abb373e5757eaec4b922b92f97ec8d6d7e057cf06778247604fbc4e7c3f3/idna-3.19.tar.gz", hash = "sha256:5e0811a4383b21dc5838069f801c4fb62113b7447663d2530d2bd6e77b49bf15", size = 215237, upload-time = "2026-08-18T05:14:24.27Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/57/b0/0e52c878c53f245edd3a11020f20979b3f490f245af532c7cae3027754b5/idna-3.19-py3-none-any.whl", hash = "sha256:815e7be7a7806d54abb586dc943addc79e8b2ee16915059658cbeff4b1b43bf4", size = 68550, upload-time = "2026-08-18T05:14:22.343Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "isort" +version = "8.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ef/7c/ec4ab396d31b3b395e2e999c8f46dec78c5e29209fac49d1f4dace04041d/isort-8.0.1.tar.gz", hash = "sha256:171ac4ff559cdc060bcfff550bc8404a486fee0caab245679c2abe7cb253c78d", size = 769592, upload-time = "2026-02-28T10:08:20.685Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3e/95/c7c34aa53c16353c56d0b802fba48d5f5caa2cdee7958acbcb795c830416/isort-8.0.1-py3-none-any.whl", hash = "sha256:28b89bc70f751b559aeca209e6120393d43fbe2490de0559662be7a9787e3d75", size = 89733, upload-time = "2026-02-28T10:08:19.466Z" }, +] + +[[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, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "jiter" +version = "0.13.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0d/5e/4ec91646aee381d01cdb9974e30882c9cd3b8c5d1079d6b5ff4af522439a/jiter-0.13.0.tar.gz", hash = "sha256:f2839f9c2c7e2dffc1bc5929a510e14ce0a946be9365fd1219e7ef342dae14f4", size = 164847, upload-time = "2026-02-02T12:37:56.441Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/29/499f8c9eaa8a16751b1c0e45e6f5f1761d180da873d417996cc7bddc8eef/jiter-0.13.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:ea026e70a9a28ebbdddcbcf0f1323128a8db66898a06eaad3a4e62d2f554d096", size = 311157, upload-time = "2026-02-02T12:35:37.758Z" }, + { url = "https://files.pythonhosted.org/packages/50/f6/566364c777d2ab450b92100bea11333c64c38d32caf8dc378b48e5b20c46/jiter-0.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:66aa3e663840152d18cc8ff1e4faad3dd181373491b9cfdc6004b92198d67911", size = 319729, upload-time = "2026-02-02T12:35:39.246Z" }, + { url = "https://files.pythonhosted.org/packages/73/dd/560f13ec5e4f116d8ad2658781646cca91b617ae3b8758d4a5076b278f70/jiter-0.13.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c3524798e70655ff19aec58c7d05adb1f074fecff62da857ea9be2b908b6d701", size = 354766, upload-time = "2026-02-02T12:35:40.662Z" }, + { url = "https://files.pythonhosted.org/packages/7c/0d/061faffcfe94608cbc28a0d42a77a74222bdf5055ccdbe5fd2292b94f510/jiter-0.13.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ec7e287d7fbd02cb6e22f9a00dd9c9cd504c40a61f2c61e7e1f9690a82726b4c", size = 362587, upload-time = "2026-02-02T12:35:42.025Z" }, + { url = "https://files.pythonhosted.org/packages/92/c9/c66a7864982fd38a9773ec6e932e0398d1262677b8c60faecd02ffb67bf3/jiter-0.13.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:47455245307e4debf2ce6c6e65a717550a0244231240dcf3b8f7d64e4c2f22f4", size = 487537, upload-time = "2026-02-02T12:35:43.459Z" }, + { url = "https://files.pythonhosted.org/packages/6c/86/84eb4352cd3668f16d1a88929b5888a3fe0418ea8c1dfc2ad4e7bf6e069a/jiter-0.13.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ee9da221dca6e0429c2704c1b3655fe7b025204a71d4d9b73390c759d776d165", size = 373717, upload-time = "2026-02-02T12:35:44.928Z" }, + { url = "https://files.pythonhosted.org/packages/6e/09/9fe4c159358176f82d4390407a03f506a8659ed13ca3ac93a843402acecf/jiter-0.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24ab43126d5e05f3d53a36a8e11eb2f23304c6c1117844aaaf9a0aa5e40b5018", size = 362683, upload-time = "2026-02-02T12:35:46.636Z" }, + { url = "https://files.pythonhosted.org/packages/c9/5e/85f3ab9caca0c1d0897937d378b4a515cae9e119730563572361ea0c48ae/jiter-0.13.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9da38b4fedde4fb528c740c2564628fbab737166a0e73d6d46cb4bb5463ff411", size = 392345, upload-time = "2026-02-02T12:35:48.088Z" }, + { url = "https://files.pythonhosted.org/packages/12/4c/05b8629ad546191939e6f0c2f17e29f542a398f4a52fb987bc70b6d1eb8b/jiter-0.13.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0b34c519e17658ed88d5047999a93547f8889f3c1824120c26ad6be5f27b6cf5", size = 517775, upload-time = "2026-02-02T12:35:49.482Z" }, + { url = "https://files.pythonhosted.org/packages/4d/88/367ea2eb6bc582c7052e4baf5ddf57ebe5ab924a88e0e09830dfb585c02d/jiter-0.13.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d2a6394e6af690d462310a86b53c47ad75ac8c21dc79f120714ea449979cb1d3", size = 551325, upload-time = "2026-02-02T12:35:51.104Z" }, + { url = "https://files.pythonhosted.org/packages/f3/12/fa377ffb94a2f28c41afaed093e0d70cfe512035d5ecb0cad0ae4792d35e/jiter-0.13.0-cp311-cp311-win32.whl", hash = "sha256:0f0c065695f616a27c920a56ad0d4fc46415ef8b806bf8fc1cacf25002bd24e1", size = 204709, upload-time = "2026-02-02T12:35:52.467Z" }, + { url = "https://files.pythonhosted.org/packages/cb/16/8e8203ce92f844dfcd3d9d6a5a7322c77077248dbb12da52d23193a839cd/jiter-0.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:0733312953b909688ae3c2d58d043aa040f9f1a6a75693defed7bc2cc4bf2654", size = 204560, upload-time = "2026-02-02T12:35:53.925Z" }, + { url = "https://files.pythonhosted.org/packages/44/26/97cc40663deb17b9e13c3a5cf29251788c271b18ee4d262c8f94798b8336/jiter-0.13.0-cp311-cp311-win_arm64.whl", hash = "sha256:5d9b34ad56761b3bf0fbe8f7e55468704107608512350962d3317ffd7a4382d5", size = 189608, upload-time = "2026-02-02T12:35:55.304Z" }, + { url = "https://files.pythonhosted.org/packages/2e/30/7687e4f87086829955013ca12a9233523349767f69653ebc27036313def9/jiter-0.13.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:0a2bd69fc1d902e89925fc34d1da51b2128019423d7b339a45d9e99c894e0663", size = 307958, upload-time = "2026-02-02T12:35:57.165Z" }, + { url = "https://files.pythonhosted.org/packages/c3/27/e57f9a783246ed95481e6749cc5002a8a767a73177a83c63ea71f0528b90/jiter-0.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f917a04240ef31898182f76a332f508f2cc4b57d2b4d7ad2dbfebbfe167eb505", size = 318597, upload-time = "2026-02-02T12:35:58.591Z" }, + { url = "https://files.pythonhosted.org/packages/cf/52/e5719a60ac5d4d7c5995461a94ad5ef962a37c8bf5b088390e6fad59b2ff/jiter-0.13.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1e2b199f446d3e82246b4fd9236d7cb502dc2222b18698ba0d986d2fecc6152", size = 348821, upload-time = "2026-02-02T12:36:00.093Z" }, + { url = "https://files.pythonhosted.org/packages/61/db/c1efc32b8ba4c740ab3fc2d037d8753f67685f475e26b9d6536a4322bcdd/jiter-0.13.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:04670992b576fa65bd056dbac0c39fe8bd67681c380cb2b48efa885711d9d726", size = 364163, upload-time = "2026-02-02T12:36:01.937Z" }, + { url = "https://files.pythonhosted.org/packages/55/8a/fb75556236047c8806995671a18e4a0ad646ed255276f51a20f32dceaeec/jiter-0.13.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5a1aff1fbdb803a376d4d22a8f63f8e7ccbce0b4890c26cc7af9e501ab339ef0", size = 483709, upload-time = "2026-02-02T12:36:03.41Z" }, + { url = "https://files.pythonhosted.org/packages/7e/16/43512e6ee863875693a8e6f6d532e19d650779d6ba9a81593ae40a9088ff/jiter-0.13.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b3fb8c2053acaef8580809ac1d1f7481a0a0bdc012fd7f5d8b18fb696a5a089", size = 370480, upload-time = "2026-02-02T12:36:04.791Z" }, + { url = "https://files.pythonhosted.org/packages/f8/4c/09b93e30e984a187bc8aaa3510e1ec8dcbdcd71ca05d2f56aac0492453aa/jiter-0.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bdaba7d87e66f26a2c45d8cbadcbfc4bf7884182317907baf39cfe9775bb4d93", size = 360735, upload-time = "2026-02-02T12:36:06.994Z" }, + { url = "https://files.pythonhosted.org/packages/1a/1b/46c5e349019874ec5dfa508c14c37e29864ea108d376ae26d90bee238cd7/jiter-0.13.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7b88d649135aca526da172e48083da915ec086b54e8e73a425ba50999468cc08", size = 391814, upload-time = "2026-02-02T12:36:08.368Z" }, + { url = "https://files.pythonhosted.org/packages/15/9e/26184760e85baee7162ad37b7912797d2077718476bf91517641c92b3639/jiter-0.13.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:e404ea551d35438013c64b4f357b0474c7abf9f781c06d44fcaf7a14c69ff9e2", size = 513990, upload-time = "2026-02-02T12:36:09.993Z" }, + { url = "https://files.pythonhosted.org/packages/e9/34/2c9355247d6debad57a0a15e76ab1566ab799388042743656e566b3b7de1/jiter-0.13.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1f4748aad1b4a93c8bdd70f604d0f748cdc0e8744c5547798acfa52f10e79228", size = 548021, upload-time = "2026-02-02T12:36:11.376Z" }, + { url = "https://files.pythonhosted.org/packages/ac/4a/9f2c23255d04a834398b9c2e0e665382116911dc4d06b795710503cdad25/jiter-0.13.0-cp312-cp312-win32.whl", hash = "sha256:0bf670e3b1445fc4d31612199f1744f67f889ee1bbae703c4b54dc097e5dd394", size = 203024, upload-time = "2026-02-02T12:36:12.682Z" }, + { url = "https://files.pythonhosted.org/packages/09/ee/f0ae675a957ae5a8f160be3e87acea6b11dc7b89f6b7ab057e77b2d2b13a/jiter-0.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:15db60e121e11fe186c0b15236bd5d18381b9ddacdcf4e659feb96fc6c969c92", size = 205424, upload-time = "2026-02-02T12:36:13.93Z" }, + { url = "https://files.pythonhosted.org/packages/1b/02/ae611edf913d3cbf02c97cdb90374af2082c48d7190d74c1111dde08bcdd/jiter-0.13.0-cp312-cp312-win_arm64.whl", hash = "sha256:41f92313d17989102f3cb5dd533a02787cdb99454d494344b0361355da52fcb9", size = 186818, upload-time = "2026-02-02T12:36:15.308Z" }, + { url = "https://files.pythonhosted.org/packages/91/9c/7ee5a6ff4b9991e1a45263bfc46731634c4a2bde27dfda6c8251df2d958c/jiter-0.13.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1f8a55b848cbabf97d861495cd65f1e5c590246fabca8b48e1747c4dfc8f85bf", size = 306897, upload-time = "2026-02-02T12:36:16.748Z" }, + { url = "https://files.pythonhosted.org/packages/7c/02/be5b870d1d2be5dd6a91bdfb90f248fbb7dcbd21338f092c6b89817c3dbf/jiter-0.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f556aa591c00f2c45eb1b89f68f52441a016034d18b65da60e2d2875bbbf344a", size = 317507, upload-time = "2026-02-02T12:36:18.351Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/b25d2ec333615f5f284f3a4024f7ce68cfa0604c322c6808b2344c7f5d2b/jiter-0.13.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f7e1d61da332ec412350463891923f960c3073cf1aae93b538f0bb4c8cd46efb", size = 350560, upload-time = "2026-02-02T12:36:19.746Z" }, + { url = "https://files.pythonhosted.org/packages/be/ec/74dcb99fef0aca9fbe56b303bf79f6bd839010cb18ad41000bf6cc71eec0/jiter-0.13.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3097d665a27bc96fd9bbf7f86178037db139f319f785e4757ce7ccbf390db6c2", size = 363232, upload-time = "2026-02-02T12:36:21.243Z" }, + { url = "https://files.pythonhosted.org/packages/1b/37/f17375e0bb2f6a812d4dd92d7616e41917f740f3e71343627da9db2824ce/jiter-0.13.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d01ecc3a8cbdb6f25a37bd500510550b64ddf9f7d64a107d92f3ccb25035d0f", size = 483727, upload-time = "2026-02-02T12:36:22.688Z" }, + { url = "https://files.pythonhosted.org/packages/77/d2/a71160a5ae1a1e66c1395b37ef77da67513b0adba73b993a27fbe47eb048/jiter-0.13.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ed9bbc30f5d60a3bdf63ae76beb3f9db280d7f195dfcfa61af792d6ce912d159", size = 370799, upload-time = "2026-02-02T12:36:24.106Z" }, + { url = "https://files.pythonhosted.org/packages/01/99/ed5e478ff0eb4e8aa5fd998f9d69603c9fd3f32de3bd16c2b1194f68361c/jiter-0.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98fbafb6e88256f4454de33c1f40203d09fc33ed19162a68b3b257b29ca7f663", size = 359120, upload-time = "2026-02-02T12:36:25.519Z" }, + { url = "https://files.pythonhosted.org/packages/16/be/7ffd08203277a813f732ba897352797fa9493faf8dc7995b31f3d9cb9488/jiter-0.13.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5467696f6b827f1116556cb0db620440380434591e93ecee7fd14d1a491b6daa", size = 390664, upload-time = "2026-02-02T12:36:26.866Z" }, + { url = "https://files.pythonhosted.org/packages/d1/84/e0787856196d6d346264d6dcccb01f741e5f0bd014c1d9a2ebe149caf4f3/jiter-0.13.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:2d08c9475d48b92892583df9da592a0e2ac49bcd41fae1fec4f39ba6cf107820", size = 513543, upload-time = "2026-02-02T12:36:28.217Z" }, + { url = "https://files.pythonhosted.org/packages/65/50/ecbd258181c4313cf79bca6c88fb63207d04d5bf5e4f65174114d072aa55/jiter-0.13.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:aed40e099404721d7fcaf5b89bd3b4568a4666358bcac7b6b15c09fb6252ab68", size = 547262, upload-time = "2026-02-02T12:36:29.678Z" }, + { url = "https://files.pythonhosted.org/packages/27/da/68f38d12e7111d2016cd198161b36e1f042bd115c169255bcb7ec823a3bf/jiter-0.13.0-cp313-cp313-win32.whl", hash = "sha256:36ebfbcffafb146d0e6ffb3e74d51e03d9c35ce7c625c8066cdbfc7b953bdc72", size = 200630, upload-time = "2026-02-02T12:36:31.808Z" }, + { url = "https://files.pythonhosted.org/packages/25/65/3bd1a972c9a08ecd22eb3b08a95d1941ebe6938aea620c246cf426ae09c2/jiter-0.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:8d76029f077379374cf0dbc78dbe45b38dec4a2eb78b08b5194ce836b2517afc", size = 202602, upload-time = "2026-02-02T12:36:33.679Z" }, + { url = "https://files.pythonhosted.org/packages/15/fe/13bd3678a311aa67686bb303654792c48206a112068f8b0b21426eb6851e/jiter-0.13.0-cp313-cp313-win_arm64.whl", hash = "sha256:bb7613e1a427cfcb6ea4544f9ac566b93d5bf67e0d48c787eca673ff9c9dff2b", size = 185939, upload-time = "2026-02-02T12:36:35.065Z" }, + { url = "https://files.pythonhosted.org/packages/49/19/a929ec002ad3228bc97ca01dbb14f7632fffdc84a95ec92ceaf4145688ae/jiter-0.13.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fa476ab5dd49f3bf3a168e05f89358c75a17608dbabb080ef65f96b27c19ab10", size = 316616, upload-time = "2026-02-02T12:36:36.579Z" }, + { url = "https://files.pythonhosted.org/packages/52/56/d19a9a194afa37c1728831e5fb81b7722c3de18a3109e8f282bfc23e587a/jiter-0.13.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ade8cb6ff5632a62b7dbd4757d8c5573f7a2e9ae285d6b5b841707d8363205ef", size = 346850, upload-time = "2026-02-02T12:36:38.058Z" }, + { url = "https://files.pythonhosted.org/packages/36/4a/94e831c6bf287754a8a019cb966ed39ff8be6ab78cadecf08df3bb02d505/jiter-0.13.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9950290340acc1adaded363edd94baebcee7dabdfa8bee4790794cd5cfad2af6", size = 358551, upload-time = "2026-02-02T12:36:39.417Z" }, + { url = "https://files.pythonhosted.org/packages/a2/ec/a4c72c822695fa80e55d2b4142b73f0012035d9fcf90eccc56bc060db37c/jiter-0.13.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2b4972c6df33731aac0742b64fd0d18e0a69bc7d6e03108ce7d40c85fd9e3e6d", size = 201950, upload-time = "2026-02-02T12:36:40.791Z" }, + { url = "https://files.pythonhosted.org/packages/b6/00/393553ec27b824fbc29047e9c7cd4a3951d7fbe4a76743f17e44034fa4e4/jiter-0.13.0-cp313-cp313t-win_arm64.whl", hash = "sha256:701a1e77d1e593c1b435315ff625fd071f0998c5f02792038a5ca98899261b7d", size = 185852, upload-time = "2026-02-02T12:36:42.077Z" }, + { url = "https://files.pythonhosted.org/packages/6e/f5/f1997e987211f6f9bd71b8083047b316208b4aca0b529bb5f8c96c89ef3e/jiter-0.13.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:cc5223ab19fe25e2f0bf2643204ad7318896fe3729bf12fde41b77bfc4fafff0", size = 308804, upload-time = "2026-02-02T12:36:43.496Z" }, + { url = "https://files.pythonhosted.org/packages/cd/8f/5482a7677731fd44881f0204981ce2d7175db271f82cba2085dd2212e095/jiter-0.13.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9776ebe51713acf438fd9b4405fcd86893ae5d03487546dae7f34993217f8a91", size = 318787, upload-time = "2026-02-02T12:36:45.071Z" }, + { url = "https://files.pythonhosted.org/packages/f3/b9/7257ac59778f1cd025b26a23c5520a36a424f7f1b068f2442a5b499b7464/jiter-0.13.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:879e768938e7b49b5e90b7e3fecc0dbec01b8cb89595861fb39a8967c5220d09", size = 353880, upload-time = "2026-02-02T12:36:47.365Z" }, + { url = "https://files.pythonhosted.org/packages/c3/87/719eec4a3f0841dad99e3d3604ee4cba36af4419a76f3cb0b8e2e691ad67/jiter-0.13.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:682161a67adea11e3aae9038c06c8b4a9a71023228767477d683f69903ebc607", size = 366702, upload-time = "2026-02-02T12:36:48.871Z" }, + { url = "https://files.pythonhosted.org/packages/d2/65/415f0a75cf6921e43365a1bc227c565cb949caca8b7532776e430cbaa530/jiter-0.13.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a13b68cd1cd8cc9de8f244ebae18ccb3e4067ad205220ef324c39181e23bbf66", size = 486319, upload-time = "2026-02-02T12:36:53.006Z" }, + { url = "https://files.pythonhosted.org/packages/54/a2/9e12b48e82c6bbc6081fd81abf915e1443add1b13d8fc586e1d90bb02bb8/jiter-0.13.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87ce0f14c6c08892b610686ae8be350bf368467b6acd5085a5b65441e2bf36d2", size = 372289, upload-time = "2026-02-02T12:36:54.593Z" }, + { url = "https://files.pythonhosted.org/packages/4e/c1/e4693f107a1789a239c759a432e9afc592366f04e901470c2af89cfd28e1/jiter-0.13.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c365005b05505a90d1c47856420980d0237adf82f70c4aff7aebd3c1cc143ad", size = 360165, upload-time = "2026-02-02T12:36:56.112Z" }, + { url = "https://files.pythonhosted.org/packages/17/08/91b9ea976c1c758240614bd88442681a87672eebc3d9a6dde476874e706b/jiter-0.13.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1317fdffd16f5873e46ce27d0e0f7f4f90f0cdf1d86bf6abeaea9f63ca2c401d", size = 389634, upload-time = "2026-02-02T12:36:57.495Z" }, + { url = "https://files.pythonhosted.org/packages/18/23/58325ef99390d6d40427ed6005bf1ad54f2577866594bcf13ce55675f87d/jiter-0.13.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:c05b450d37ba0c9e21c77fef1f205f56bcee2330bddca68d344baebfc55ae0df", size = 514933, upload-time = "2026-02-02T12:36:58.909Z" }, + { url = "https://files.pythonhosted.org/packages/5b/25/69f1120c7c395fd276c3996bb8adefa9c6b84c12bb7111e5c6ccdcd8526d/jiter-0.13.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:775e10de3849d0631a97c603f996f518159272db00fdda0a780f81752255ee9d", size = 548842, upload-time = "2026-02-02T12:37:00.433Z" }, + { url = "https://files.pythonhosted.org/packages/18/05/981c9669d86850c5fbb0d9e62bba144787f9fba84546ba43d624ee27ef29/jiter-0.13.0-cp314-cp314-win32.whl", hash = "sha256:632bf7c1d28421c00dd8bbb8a3bac5663e1f57d5cd5ed962bce3c73bf62608e6", size = 202108, upload-time = "2026-02-02T12:37:01.718Z" }, + { url = "https://files.pythonhosted.org/packages/8d/96/cdcf54dd0b0341db7d25413229888a346c7130bd20820530905fdb65727b/jiter-0.13.0-cp314-cp314-win_amd64.whl", hash = "sha256:f22ef501c3f87ede88f23f9b11e608581c14f04db59b6a801f354397ae13739f", size = 204027, upload-time = "2026-02-02T12:37:03.075Z" }, + { url = "https://files.pythonhosted.org/packages/fb/f9/724bcaaab7a3cd727031fe4f6995cb86c4bd344909177c186699c8dec51a/jiter-0.13.0-cp314-cp314-win_arm64.whl", hash = "sha256:07b75fe09a4ee8e0c606200622e571e44943f47254f95e2436c8bdcaceb36d7d", size = 187199, upload-time = "2026-02-02T12:37:04.414Z" }, + { url = "https://files.pythonhosted.org/packages/62/92/1661d8b9fd6a3d7a2d89831db26fe3c1509a287d83ad7838831c7b7a5c7e/jiter-0.13.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:964538479359059a35fb400e769295d4b315ae61e4105396d355a12f7fef09f0", size = 318423, upload-time = "2026-02-02T12:37:05.806Z" }, + { url = "https://files.pythonhosted.org/packages/4f/3b/f77d342a54d4ebcd128e520fc58ec2f5b30a423b0fd26acdfc0c6fef8e26/jiter-0.13.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e104da1db1c0991b3eaed391ccd650ae8d947eab1480c733e5a3fb28d4313e40", size = 351438, upload-time = "2026-02-02T12:37:07.189Z" }, + { url = "https://files.pythonhosted.org/packages/76/b3/ba9a69f0e4209bd3331470c723c2f5509e6f0482e416b612431a5061ed71/jiter-0.13.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e3a5f0cde8ff433b8e88e41aa40131455420fb3649a3c7abdda6145f8cb7202", size = 364774, upload-time = "2026-02-02T12:37:08.579Z" }, + { url = "https://files.pythonhosted.org/packages/b3/16/6cdb31fa342932602458dbb631bfbd47f601e03d2e4950740e0b2100b570/jiter-0.13.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:57aab48f40be1db920a582b30b116fe2435d184f77f0e4226f546794cedd9cf0", size = 487238, upload-time = "2026-02-02T12:37:10.066Z" }, + { url = "https://files.pythonhosted.org/packages/ed/b1/956cc7abaca8d95c13aa8d6c9b3f3797241c246cd6e792934cc4c8b250d2/jiter-0.13.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7772115877c53f62beeb8fd853cab692dbc04374ef623b30f997959a4c0e7e95", size = 372892, upload-time = "2026-02-02T12:37:11.656Z" }, + { url = "https://files.pythonhosted.org/packages/26/c4/97ecde8b1e74f67b8598c57c6fccf6df86ea7861ed29da84629cdbba76c4/jiter-0.13.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1211427574b17b633cfceba5040de8081e5abf114f7a7602f73d2e16f9fdaa59", size = 360309, upload-time = "2026-02-02T12:37:13.244Z" }, + { url = "https://files.pythonhosted.org/packages/4b/d7/eabe3cf46715854ccc80be2cd78dd4c36aedeb30751dbf85a1d08c14373c/jiter-0.13.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7beae3a3d3b5212d3a55d2961db3c292e02e302feb43fce6a3f7a31b90ea6dfe", size = 389607, upload-time = "2026-02-02T12:37:14.881Z" }, + { url = "https://files.pythonhosted.org/packages/df/2d/03963fc0804e6109b82decfb9974eb92df3797fe7222428cae12f8ccaa0c/jiter-0.13.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:e5562a0f0e90a6223b704163ea28e831bd3a9faa3512a711f031611e6b06c939", size = 514986, upload-time = "2026-02-02T12:37:16.326Z" }, + { url = "https://files.pythonhosted.org/packages/f6/6c/8c83b45eb3eb1c1e18d841fe30b4b5bc5619d781267ca9bc03e005d8fd0a/jiter-0.13.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:6c26a424569a59140fb51160a56df13f438a2b0967365e987889186d5fc2f6f9", size = 548756, upload-time = "2026-02-02T12:37:17.736Z" }, + { url = "https://files.pythonhosted.org/packages/47/66/eea81dfff765ed66c68fd2ed8c96245109e13c896c2a5015c7839c92367e/jiter-0.13.0-cp314-cp314t-win32.whl", hash = "sha256:24dc96eca9f84da4131cdf87a95e6ce36765c3b156fc9ae33280873b1c32d5f6", size = 201196, upload-time = "2026-02-02T12:37:19.101Z" }, + { url = "https://files.pythonhosted.org/packages/ff/32/4ac9c7a76402f8f00d00842a7f6b83b284d0cf7c1e9d4227bc95aa6d17fa/jiter-0.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0a8d76c7524087272c8ae913f5d9d608bd839154b62c4322ef65723d2e5bb0b8", size = 204215, upload-time = "2026-02-02T12:37:20.495Z" }, + { url = "https://files.pythonhosted.org/packages/f9/8e/7def204fea9f9be8b3c21a6f2dd6c020cf56c7d5ff753e0e23ed7f9ea57e/jiter-0.13.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2c26cf47e2cad140fa23b6d58d435a7c0161f5c514284802f25e87fddfe11024", size = 187152, upload-time = "2026-02-02T12:37:22.124Z" }, + { url = "https://files.pythonhosted.org/packages/79/b3/3c29819a27178d0e461a8571fb63c6ae38be6dc36b78b3ec2876bbd6a910/jiter-0.13.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b1cbfa133241d0e6bdab48dcdc2604e8ba81512f6bbd68ec3e8e1357dd3c316c", size = 307016, upload-time = "2026-02-02T12:37:42.755Z" }, + { url = "https://files.pythonhosted.org/packages/eb/ae/60993e4b07b1ac5ebe46da7aa99fdbb802eb986c38d26e3883ac0125c4e0/jiter-0.13.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:db367d8be9fad6e8ebbac4a7578b7af562e506211036cba2c06c3b998603c3d2", size = 305024, upload-time = "2026-02-02T12:37:44.774Z" }, + { url = "https://files.pythonhosted.org/packages/77/fa/2227e590e9cf98803db2811f172b2d6460a21539ab73006f251c66f44b14/jiter-0.13.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45f6f8efb2f3b0603092401dc2df79fa89ccbc027aaba4174d2d4133ed661434", size = 339337, upload-time = "2026-02-02T12:37:46.668Z" }, + { url = "https://files.pythonhosted.org/packages/2d/92/015173281f7eb96c0ef580c997da8ef50870d4f7f4c9e03c845a1d62ae04/jiter-0.13.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:597245258e6ad085d064780abfb23a284d418d3e61c57362d9449c6c7317ee2d", size = 346395, upload-time = "2026-02-02T12:37:48.09Z" }, + { url = "https://files.pythonhosted.org/packages/80/60/e50fa45dd7e2eae049f0ce964663849e897300433921198aef94b6ffa23a/jiter-0.13.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:3d744a6061afba08dd7ae375dcde870cffb14429b7477e10f67e9e6d68772a0a", size = 305169, upload-time = "2026-02-02T12:37:50.376Z" }, + { url = "https://files.pythonhosted.org/packages/d2/73/a009f41c5eed71c49bec53036c4b33555afcdee70682a18c6f66e396c039/jiter-0.13.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:ff732bd0a0e778f43d5009840f20b935e79087b4dc65bd36f1cd0f9b04b8ff7f", size = 303808, upload-time = "2026-02-02T12:37:52.092Z" }, + { url = "https://files.pythonhosted.org/packages/c4/10/528b439290763bff3d939268085d03382471b442f212dca4ff5f12802d43/jiter-0.13.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab44b178f7981fcaea7e0a5df20e773c663d06ffda0198f1a524e91b2fde7e59", size = 337384, upload-time = "2026-02-02T12:37:53.582Z" }, + { url = "https://files.pythonhosted.org/packages/67/8a/a342b2f0251f3dac4ca17618265d93bf244a2a4d089126e81e4c1056ac50/jiter-0.13.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7bb00b6d26db67a05fe3e12c76edc75f32077fb51deed13822dc648fa373bc19", size = 343768, upload-time = "2026-02-02T12:37:55.055Z" }, +] + +[[package]] +name = "jmespath" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d3/59/322338183ecda247fb5d1763a6cbe46eff7222eaeebafd9fa65d4bf5cb11/jmespath-1.1.0.tar.gz", hash = "sha256:472c87d80f36026ae83c6ddd0f1d05d4e510134ed462851fd5f754c8c3cbb88d", size = 27377, upload-time = "2026-01-22T16:35:26.279Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/2f/967ba146e6d58cf6a652da73885f52fc68001525b4197effc174321d70b4/jmespath-1.1.0-py3-none-any.whl", hash = "sha256:a5663118de4908c91729bea0acadca56526eb2698e83de10cd116ae0f4e97c64", size = 20419, upload-time = "2026-01-22T16:35:24.919Z" }, +] + +[[package]] +name = "joblib" +version = "1.5.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/41/f2/d34e8b3a08a9cc79a50b2208a93dce981fe615b64d5a4d4abee421d898df/joblib-1.5.3.tar.gz", hash = "sha256:8561a3269e6801106863fd0d6d84bb737be9e7631e33aaed3fb9ce5953688da3", size = 331603, upload-time = "2025-12-15T08:41:46.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/91/984aca2ec129e2757d1e4e3c81c3fcda9d0f85b74670a094cc443d9ee949/joblib-1.5.3-py3-none-any.whl", hash = "sha256:5fc3c5039fc5ca8c0276333a188bbd59d6b7ab37fe6632daa76bc7f9ec18e713", size = 309071, upload-time = "2025-12-15T08:41:44.973Z" }, +] + +[[package]] +name = "jsonlines" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/35/87/bcda8e46c88d0e34cad2f09ee2d0c7f5957bccdb9791b0b934ec84d84be4/jsonlines-4.0.0.tar.gz", hash = "sha256:0c6d2c09117550c089995247f605ae4cf77dd1533041d366351f6f298822ea74", size = 11359, upload-time = "2023-09-01T12:34:44.187Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/62/d9ba6323b9202dd2fe166beab8a86d29465c41a0288cbe229fac60c1ab8d/jsonlines-4.0.0-py3-none-any.whl", hash = "sha256:185b334ff2ca5a91362993f42e83588a360cf95ce4b71a73548502bda52a7c55", size = 8701, upload-time = "2023-09-01T12:34:42.563Z" }, +] + +[[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, upload-time = "2023-06-26T12:07:29.144Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/07/02e16ed01e04a374e644b575638ec7987ae846d25ad97bcc9945a3ee4b0e/jsonpatch-1.33-py2.py3-none-any.whl", hash = "sha256:0ae28c0cd062bbd8b8ecc26d7d164fbbea9652a1a3693f3b956c1eae5145dade", size = 12898, upload-time = "2023-06-16T21:01:28.466Z" }, +] + +[[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, upload-time = "2024-06-10T19:24:42.462Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/92/5e77f98553e9e75130c78900d000368476aed74276eb8ae8796f65f00918/jsonpointer-3.0.0-py2.py3-none-any.whl", hash = "sha256:13e088adc14fca8b6aa8177c044e12701e6ad4b28ff10e65f2267a90109c9942", size = 7595, upload-time = "2024-06-10T19:24:40.698Z" }, +] + +[[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, upload-time = "2023-01-16T16:10:04.455Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/ec/e1db9922bceb168197a558a2b8c03a7963f1afe93517ddd3cf99f202f996/jsonref-1.1.0-py3-none-any.whl", hash = "sha256:590dc7773df6c21cbf948b5dac07a72a251db28b0238ceecce0a2abfa8ec30a9", size = 9425, upload-time = "2023-01-16T16:10:02.255Z" }, +] + +[[package]] +name = "jsonschema" +version = "4.26.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/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2025.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, +] + +[[package]] +name = "kombu" +version = "5.6.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "amqp" }, + { name = "packaging" }, + { name = "tzdata" }, + { name = "vine" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b6/a5/607e533ed6c83ae1a696969b8e1c137dfebd5759a2e9682e26ff1b97740b/kombu-5.6.2.tar.gz", hash = "sha256:8060497058066c6f5aed7c26d7cd0d3b574990b09de842a8c5aaed0b92cc5a55", size = 472594, upload-time = "2025-12-29T20:30:07.779Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/0f/834427d8c03ff1d7e867d3db3d176470c64871753252b21b4f4897d1fa45/kombu-5.6.2-py3-none-any.whl", hash = "sha256:efcfc559da324d41d61ca311b0c64965ea35b4c55cc04ee36e55386145dace93", size = 214219, upload-time = "2025-12-29T20:30:05.74Z" }, +] + +[[package]] +name = "langchain" +version = "1.3.16" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "langchain-core" }, + { name = "langgraph" }, + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/73/d5/cda22848e95e17addb8f365a210f78d4474d2e78895a4cc79ddf29ee186c/langchain-1.3.16.tar.gz", hash = "sha256:781fd3bca96813e9d0d74c58a323d0ac05a0c0d5cb2c56ac34185f4f3ec0c4bb", size = 666372, upload-time = "2026-08-20T14:35:14.682Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/92/854f52036abe590c70ae14e398d00a1553cedadcf93e59de6b7b4026b2bb/langchain-1.3.16-py3-none-any.whl", hash = "sha256:67dcb28d69f96018f3062da2075f99b25ea048af7f5baae570afe1d91e033743", size = 147826, upload-time = "2026-08-20T14:35:13.109Z" }, +] + +[[package]] +name = "langchain-aws" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "boto3" }, + { name = "langchain-core" }, + { name = "numpy" }, + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fb/e4/9a3d8914b121d24a30fd1017910eeeebb9a720b6afe5dfb82018684cd889/langchain_aws-1.5.0.tar.gz", hash = "sha256:b5e2b0a34704837f911182774ce1b7c709fbd2dbdea1e9ef7ee7e00b247084f0", size = 515712, upload-time = "2026-05-19T19:42:40.068Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4e/08/f10ae62cbb6bcdd2b62e5a697470351eef1e33bd4a40b7e2e03230582ff0/langchain_aws-1.5.0-py3-none-any.whl", hash = "sha256:c15177686393d0e0f5e86333ae9fb7980119220878590dbd5845efe020fe1c72", size = 202704, upload-time = "2026-05-19T19:42:38.942Z" }, +] + +[[package]] +name = "langchain-core" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, + { name = "jsonpatch" }, + { name = "langchain-protocol" }, + { name = "langsmith" }, + { name = "packaging" }, + { name = "pydantic" }, + { name = "pyyaml" }, + { name = "tenacity" }, + { name = "typing-extensions" }, + { name = "uuid-utils" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/97/88/ebc98df187c525d729725ab39759337c56d5d2803423632376aa35bde899/langchain_core-1.6.0.tar.gz", hash = "sha256:dc72e36678ed26683ec0ad8829b44011fba461d10f9b31dbd9110215e5ec2333", size = 992493, upload-time = "2026-08-19T15:55:40.642Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/4c/508a90b9d2e3bd7738fd93cb2ac2178ce734662c75e69b3b81c445f1b360/langchain_core-1.6.0-py3-none-any.whl", hash = "sha256:d8bb924cd413955d9d3192ccced140407427c3ff51e50ffb941222648da92cb2", size = 570006, upload-time = "2026-08-19T15:55:38.935Z" }, +] + +[[package]] +name = "langchain-google-vertexai" +version = "3.2.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "bottleneck" }, + { name = "google-cloud-aiplatform" }, + { name = "google-cloud-storage", version = "3.9.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "google-cloud-storage", version = "3.13.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, + { name = "google-cloud-vectorsearch" }, + { name = "httpx" }, + { name = "httpx-sse" }, + { name = "langchain-core" }, + { name = "numexpr" }, + { name = "pyarrow" }, + { name = "pydantic" }, + { name = "validators" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/39/1c/4efdcfe094ef6711d9c130428c4c42af61a49a1580fa718dd1e1f6bd5fc3/langchain_google_vertexai-3.2.4.tar.gz", hash = "sha256:f0c16b4b639037f60357dbf6d4aa3b0f301f23be6b33b0688c75902235cb9434", size = 351222, upload-time = "2026-06-09T14:57:58.316Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/a2/af563ff45208d22abc28d0a0e44a7fb8aceaadac201afa6a11c77bfb6338/langchain_google_vertexai-3.2.4-py3-none-any.whl", hash = "sha256:65b5615e596fdabc2e149f0160fded88bebef2bbc1ea70095ff81714f7570183", size = 118884, upload-time = "2026-06-09T14:57:57.062Z" }, +] + +[[package]] +name = "langchain-openai" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "langchain-core" }, + { name = "openai" }, + { name = "tiktoken" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/58/aa/5b4dbe8f90c44729fb7800bfde113ce1bc7c9687115d2e0d20fb377671e9/langchain_openai-1.6.0.tar.gz", hash = "sha256:7c545cc1f2bee048a7e219f375b78cd820b32c876ccf825320e25e1e94132577", size = 3290712, upload-time = "2026-08-19T21:44:45.334Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2b/fd/e22f2ffa2e2cf6043eb56f6a6bc5e18ac24da9b48b92cd458ba54af24b9e/langchain_openai-1.6.0-py3-none-any.whl", hash = "sha256:648112bbd135aa51d60d4aa2fd4ad353628a8d92a2f8e779244a0d9869a16f18", size = 125078, upload-time = "2026-08-19T21:44:44.242Z" }, +] + +[[package]] +name = "langchain-protocol" +version = "0.0.18" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d2/59/b5959aea96faa9146e2e49a7a22882b3528c62efafe9a6a95beab30c2305/langchain_protocol-0.0.18.tar.gz", hash = "sha256:ec3e11782f1ed0c9db38e5a9ed01b0e7a0d3fba406faa8aef6594b73c56a63e6", size = 6150, upload-time = "2026-06-18T17:08:26.959Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/2e/d82db9eec13ad0f72e7aaad5c4bc730ab111934fdc83c85523206eb9b0a0/langchain_protocol-0.0.18-py3-none-any.whl", hash = "sha256:70b53a86fbf9cedc863555effe44da192ab02d556ddbf2cf95b8873adcf41b5a", size = 7221, upload-time = "2026-06-18T17:08:25.996Z" }, +] + +[[package]] +name = "langdetect" +version = "1.0.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/59/59/4bc44158a767a6d66de18c4136c8aa90491d56cc951c10b74dd1e13213c9/langdetect-1.0.7.zip", hash = "sha256:91a170d5f0ade380db809b3ba67f08e95fe6c6c8641f96d67a51ff7e98a9bf30", size = 998060, upload-time = "2016-10-03T07:22:09.284Z" } + +[[package]] +name = "langgraph" +version = "1.2.11" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "langchain-core" }, + { name = "langgraph-checkpoint" }, + { name = "langgraph-prebuilt" }, + { name = "langgraph-sdk" }, + { name = "pydantic" }, + { name = "xxhash" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/56/0d/c8e7ee98896659e1b6555db0ab115a9ca899844744645d5d894032bab1d7/langgraph-1.2.11.tar.gz", hash = "sha256:9ecfe11e50d338b34b15cf4d8a442642de103e8ae6971320efba84e4542eb363", size = 725753, upload-time = "2026-08-11T14:00:36.945Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/7f/c5c30e4be99ff821029c7ac872a480676bb179c9f3df85ea3f38d13f86d4/langgraph-1.2.11-py3-none-any.whl", hash = "sha256:8bab70de7b2d00b5300fb289bcf38d8b241400f3184c1e95e8ce706fb0e8686b", size = 248854, upload-time = "2026-08-11T14:00:35.494Z" }, +] + +[[package]] +name = "langgraph-checkpoint" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "langchain-core" }, + { name = "ormsgpack" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dc/e1/089c4c9e0a2fec7f883f82ae8e6a727138d50074cfeb6644bc2d13b1019b/langgraph_checkpoint-4.2.0.tar.gz", hash = "sha256:51a593b6bee684b0818e5d6e58e28ab340c6db7794575056ce7bd1b746a84ed7", size = 180239, upload-time = "2026-08-07T20:05:03.756Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/71/3b475f09bd57d3a5649792c66353312b4432afd843f301739dfcebd157f0/langgraph_checkpoint-4.2.0-py3-none-any.whl", hash = "sha256:0547fd228935a0b758865de3a3d6d7a2537c308895d0f9ab092ce9151b5da942", size = 56833, upload-time = "2026-08-07T20:05:02.655Z" }, +] + +[[package]] +name = "langgraph-prebuilt" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "langchain-core" }, + { name = "langgraph-checkpoint" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/29/66/ed9b93f56bc17ef22d551892f0ac2b225a97fe0fcf23a511b857f70d590b/langgraph_prebuilt-1.1.0.tar.gz", hash = "sha256:3c579cf6eed2d17f9c157c2d0fcaddcd8688524e7022d3b22b37a3bf4589d528", size = 178833, upload-time = "2026-05-12T03:37:49.332Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/43/3fe1a700b8490ed02679cdbbc8c915eb23a092faf496c9c1118abcd10be3/langgraph_prebuilt-1.1.0-py3-none-any.whl", hash = "sha256:51e311747d755b751d5c6b39b0c1446124d3a7643d2515017e6714b323508fc9", size = 41043, upload-time = "2026-05-12T03:37:48.007Z" }, +] + +[[package]] +name = "langgraph-sdk" +version = "0.4.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, + { name = "langchain-core" }, + { name = "langchain-protocol" }, + { name = "orjson" }, + { name = "websockets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bd/0e/3c0347fd517390e807d9772d2037f9cc65c0227af27f07052ba9c8ebdfa2/langgraph_sdk-0.4.3.tar.gz", hash = "sha256:f101cc043ddd7400ceaef66d934c42fa2f0e7d4ea5029754d286260d39038251", size = 344145, upload-time = "2026-08-19T18:05:21.838Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/a0/f3bc44b46ef730588f66fdf9e9ddc54189db78482a9e914ae6a554a2b2e0/langgraph_sdk-0.4.3-py3-none-any.whl", hash = "sha256:1b7920b39b6dc439843d122a06f04a0cb8b65c02fd086ddf68e18596a230a0e7", size = 161794, upload-time = "2026-08-19T18:05:20.495Z" }, +] + +[[package]] +name = "langid" +version = "1.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ea/4c/0fb7d900d3b0b9c8703be316fbddffecdab23c64e1b46c7a83561d78bd43/langid-1.1.6.tar.gz", hash = "sha256:044bcae1912dab85c33d8e98f2811b8f4ff1213e5e9a9e9510137b84da2cb293", size = 1925978, upload-time = "2016-04-05T22:34:15.786Z" } + +[[package]] +name = "langsmith" +version = "0.11.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "httpx" }, + { name = "orjson", marker = "platform_python_implementation != 'PyPy'" }, + { name = "packaging" }, + { name = "pydantic" }, + { name = "requests" }, + { name = "requests-toolbelt" }, + { name = "sniffio" }, + { name = "typing-extensions" }, + { name = "uuid-utils" }, + { name = "websockets" }, + { name = "xxhash" }, + { name = "zstandard" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cc/57/7b6c11080c9e082ebf1456a2e2372fae8f23a85a5ae2869bdd5ab9a6507d/langsmith-0.11.1.tar.gz", hash = "sha256:47998977366acb3ba3093881fd465cbf11a5f8c2f4e87e40a17dda203f6dedf4", size = 4814724, upload-time = "2026-08-19T15:47:44.116Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/85/0ad6df25588122760b2b40ec182eafc55532c9e66015c83e16675bd34a29/langsmith-0.11.1-py3-none-any.whl", hash = "sha256:cfc3437a9cf27440cd0095c24df945edbceb6df10b579bc8b3980b4ad367835f", size = 744589, upload-time = "2026-08-19T15:47:42.043Z" }, +] + +[[package]] +name = "latex2mathml" +version = "3.78.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1a/26/57b1034c08922d0aefea79430a5e0006ffaee4f0ec59d566613f667ab2f7/latex2mathml-3.78.1.tar.gz", hash = "sha256:f941db80bf41db33f31df87b304e8b588f8166b813b0257c11c98f7a9d0aac71", size = 74030, upload-time = "2025-08-29T23:34:23.178Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3e/76/d661ea2e529c3d464f9efd73f9ac31626b45279eb4306e684054ea20e3d4/latex2mathml-3.78.1-py3-none-any.whl", hash = "sha256:f089b6d75e85b937f99693c93e8c16c0804008672c3dd2a3d25affd36f238100", size = 73892, upload-time = "2025-08-29T23:34:21.98Z" }, +] + +[[package]] +name = "levenshtein" +version = "0.27.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "rapidfuzz" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/82/56/dcf68853b062e3b94bdc3d011cc4198779abc5b9dc134146a062920ce2e2/levenshtein-0.27.3.tar.gz", hash = "sha256:1ac326b2c84215795163d8a5af471188918b8797b4953ec87aaba22c9c1f9fc0", size = 393269, upload-time = "2025-11-01T12:14:31.04Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/fd/42e28a86e2f04a2e064faa1eab7d81a35fb111212b508ce7e450f839943d/levenshtein-0.27.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:245b6ffb6e1b0828cafbce35c500cb3265d0962c121d090669f177968c5a2980", size = 172216, upload-time = "2025-11-01T12:12:55.727Z" }, + { url = "https://files.pythonhosted.org/packages/1d/f4/fe665c8e5d8ebe4266807e43af72db9d4f84d4f513ea86eacca3aaf5f77b/levenshtein-0.27.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8f44c98fa23f489eb7b2ad87d5dd24b6a784434bb5edb73f6b0513309c949690", size = 158616, upload-time = "2025-11-01T12:12:56.99Z" }, + { url = "https://files.pythonhosted.org/packages/22/46/9998bc56729444e350c083635b94c3eae97218b8a618cdc89f6825eec08c/levenshtein-0.27.3-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f5f85a1fc96dfc147bba82b4c67d6346ea26c27ef77a6a9de689118e26dddbe", size = 134222, upload-time = "2025-11-01T12:12:58.437Z" }, + { url = "https://files.pythonhosted.org/packages/19/09/914b3fc22c083728904f8dc7876a2a90a602b4769f27f5320176cbd6f781/levenshtein-0.27.3-cp311-cp311-manylinux_2_24_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:18ceddd38d0e990d2c1c9b72f3e191dace87e2f8f0446207ce9e9cd2bfdfc8a1", size = 114902, upload-time = "2025-11-01T12:12:59.645Z" }, + { url = "https://files.pythonhosted.org/packages/d2/ee/f361bfa5afe24698fb07ae7811e00c2984131023c7688299dea4fd3f2f4c/levenshtein-0.27.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:222b81adca29ee4128183328c6e1b25a48c817d14a008ab49e74be9df963b293", size = 153562, upload-time = "2025-11-01T12:13:00.745Z" }, + { url = "https://files.pythonhosted.org/packages/a3/4f/614d0ab9777ebb91895ce1c9390ec2f244f53f7ddf7e29f36b0ca33f3841/levenshtein-0.27.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ee3769ab6e89c24f901e6b7004100630e86721464d7d0384860a322d7953d3a5", size = 1115732, upload-time = "2025-11-01T12:13:02.219Z" }, + { url = "https://files.pythonhosted.org/packages/24/d9/f33c4e35399349ec2eb7be53ed49459bf6e59c31668868c89cf6f7964029/levenshtein-0.27.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:03eba8fda9f3f2b4b0760263fa20b20a90ab00cbeeab4d0d9d899b4f77912b0a", size = 1009023, upload-time = "2025-11-01T12:13:03.954Z" }, + { url = "https://files.pythonhosted.org/packages/2e/63/e8803a6d71488334c100afc79a98efc8cf0086ad29ee7f1d083f7f2c584d/levenshtein-0.27.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c870b19e2d5c7bc7f16213cc10312b82d873a4d46e1c6d51857a12ef39a76552", size = 1185850, upload-time = "2025-11-01T12:13:05.341Z" }, + { url = "https://files.pythonhosted.org/packages/09/55/a6a815ef76a6d5f7a2ee4e1edc8e8f1f935b9fa278634cc687af19b86de9/levenshtein-0.27.3-cp311-cp311-win32.whl", hash = "sha256:1987622e9b8ba2ae47dc27469291da1f58462660fa34f4358e9d9c1830fb1355", size = 84375, upload-time = "2025-11-01T12:13:06.647Z" }, + { url = "https://files.pythonhosted.org/packages/e5/36/cf4c36ffe91994e772b682ff4c3cb721bd50ac05d4a887baa35f4d3b2268/levenshtein-0.27.3-cp311-cp311-win_amd64.whl", hash = "sha256:a2b2aa81851e01bb09667b07e80c3fbf0f5a7c6ee9cd80caf43cce705e65832a", size = 94598, upload-time = "2025-11-01T12:13:07.68Z" }, + { url = "https://files.pythonhosted.org/packages/92/4b/43e820c3a13033908925eae8614ad7c0be1e5868836770565174012158c0/levenshtein-0.27.3-cp311-cp311-win_arm64.whl", hash = "sha256:a084b335c54def1aef9a594b7163faa44dd00056323808bab783f43d8e4c1395", size = 87133, upload-time = "2025-11-01T12:13:08.701Z" }, + { url = "https://files.pythonhosted.org/packages/7c/8e/3be9d8e0245704e3af5258fb6cb157c3d59902e1351e95edf6ed8a8c0434/levenshtein-0.27.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2de7f095b0ca8e44de9de986ccba661cd0dec3511c751b499e76b60da46805e9", size = 169622, upload-time = "2025-11-01T12:13:10.026Z" }, + { url = "https://files.pythonhosted.org/packages/a6/42/a2b2fda5e8caf6ecd5aac142f946a77574a3961e65da62c12fd7e48e5cb1/levenshtein-0.27.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d9b8b29e5d5145a3c958664c85151b1bb4b26e4ca764380b947e6a96a321217c", size = 159183, upload-time = "2025-11-01T12:13:11.197Z" }, + { url = "https://files.pythonhosted.org/packages/eb/c4/f083fabbd61c449752df1746533538f4a8629e8811931b52f66e6c4290ad/levenshtein-0.27.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fc975465a51b1c5889eadee1a583b81fba46372b4b22df28973e49e8ddb8f54a", size = 133120, upload-time = "2025-11-01T12:13:12.363Z" }, + { url = "https://files.pythonhosted.org/packages/4e/e5/b6421e04cb0629615b8efd6d4d167dd2b1afb5097b87bb83cd992004dcca/levenshtein-0.27.3-cp312-cp312-manylinux_2_24_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:57573ed885118554770979fdee584071b66103f6d50beddeabb54607a1213d81", size = 114988, upload-time = "2025-11-01T12:13:13.486Z" }, + { url = "https://files.pythonhosted.org/packages/e5/77/39ee0e8d3028e90178e1031530ccc98563f8f2f0d905ec784669dcf0fa90/levenshtein-0.27.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23aff800a6dd5d91bb3754a6092085aa7ad46b28e497682c155c74f681cfaa2d", size = 153346, upload-time = "2025-11-01T12:13:14.744Z" }, + { url = "https://files.pythonhosted.org/packages/3c/0d/c0f367bbd260dbd7a4e134fd21f459e0f5eac43deac507952b46a1d8a93a/levenshtein-0.27.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c08a952432b8ad9dccb145f812176db94c52cda732311ddc08d29fd3bf185b0a", size = 1114538, upload-time = "2025-11-01T12:13:15.851Z" }, + { url = "https://files.pythonhosted.org/packages/d8/ef/ae71433f7b4db0bd2af7974785e36cdec899919203fb82e647c5a6109c07/levenshtein-0.27.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:3bfcb2d78ab9cc06a1e75da8fcfb7a430fe513d66cfe54c07e50f32805e5e6db", size = 1009734, upload-time = "2025-11-01T12:13:17.212Z" }, + { url = "https://files.pythonhosted.org/packages/27/dc/62c28b812dcb0953fc32ab7adf3d0e814e43c8560bb28d9269a44d874adf/levenshtein-0.27.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ba7235f6dcb31a217247468295e2dd4c6c1d3ac81629dc5d355d93e1a5f4c185", size = 1185581, upload-time = "2025-11-01T12:13:18.661Z" }, + { url = "https://files.pythonhosted.org/packages/56/e8/2e7ab9c565793220edb8e5432f9a846386a157075bdd032a90e9585bce38/levenshtein-0.27.3-cp312-cp312-win32.whl", hash = "sha256:ea80d70f1d18c161a209be556b9094968627cbaae620e102459ef9c320a98cbb", size = 84660, upload-time = "2025-11-01T12:13:19.87Z" }, + { url = "https://files.pythonhosted.org/packages/2c/a6/907a1fc8587dc91c40156973e09d106ab064c06eb28dc4700ba0fe54d654/levenshtein-0.27.3-cp312-cp312-win_amd64.whl", hash = "sha256:fbaa1219d9b2d955339a37e684256a861e9274a3fe3a6ee1b8ea8724c3231ed9", size = 94909, upload-time = "2025-11-01T12:13:21.323Z" }, + { url = "https://files.pythonhosted.org/packages/d5/d6/e04f0ddf6a71df3cdd1817b71703490ac874601ed460b2af172d3752c321/levenshtein-0.27.3-cp312-cp312-win_arm64.whl", hash = "sha256:2edbaa84f887ea1d9d8e4440af3fdda44769a7855d581c6248d7ee51518402a8", size = 87358, upload-time = "2025-11-01T12:13:22.393Z" }, + { url = "https://files.pythonhosted.org/packages/3e/f2/162e9ea7490b36bbf05776c8e3a8114c75aa78546ddda8e8f36731db3da6/levenshtein-0.27.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e55aa9f9453fd89d4a9ff1f3c4a650b307d5f61a7eed0568a52fbd2ff2eba107", size = 169230, upload-time = "2025-11-01T12:13:23.735Z" }, + { url = "https://files.pythonhosted.org/packages/01/2d/7316ba7f94e3d60e89bd120526bc71e4812866bb7162767a2a10f73f72c5/levenshtein-0.27.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ae4d484453c48939ecd01c5c213530c68dd5cd6e5090f0091ef69799ec7a8a9f", size = 158643, upload-time = "2025-11-01T12:13:25.549Z" }, + { url = "https://files.pythonhosted.org/packages/5e/87/85433cb1e51c45016f061d96fea3106b6969f700e2cbb56c15de82d0deeb/levenshtein-0.27.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d18659832567ee387b266be390da0de356a3aa6cf0e8bc009b6042d8188e131f", size = 132881, upload-time = "2025-11-01T12:13:26.822Z" }, + { url = "https://files.pythonhosted.org/packages/40/1c/3ce66c9a7da169a43dd89146d69df9dec935e6f86c70c6404f48d1291d2c/levenshtein-0.27.3-cp313-cp313-manylinux_2_24_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:027b3d142cc8ea2ab4e60444d7175f65a94dde22a54382b2f7b47cc24936eb53", size = 114650, upload-time = "2025-11-01T12:13:28.382Z" }, + { url = "https://files.pythonhosted.org/packages/73/60/7138e98884ca105c76ef192f5b43165d6eac6f32b432853ebe9f09ee50c9/levenshtein-0.27.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ffdca6989368cc64f347f0423c528520f12775b812e170a0eb0c10e4c9b0f3ff", size = 153127, upload-time = "2025-11-01T12:13:29.781Z" }, + { url = "https://files.pythonhosted.org/packages/df/8f/664ac8b83026d7d1382866b68babae17e92b7b6ff8dc3c6205c0066b8ce1/levenshtein-0.27.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:fa00ab389386032b02a1c9050ec3c6aa824d2bbcc692548fdc44a46b71c058c6", size = 1114602, upload-time = "2025-11-01T12:13:31.651Z" }, + { url = "https://files.pythonhosted.org/packages/2c/c8/8905d96cf2d7ed6af7eb39a8be0925ef335729473c1e9d1f56230ecaffc5/levenshtein-0.27.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:691c9003c6c481b899a5c2f72e8ce05a6d956a9668dc75f2a3ce9f4381a76dc6", size = 1008036, upload-time = "2025-11-01T12:13:33.006Z" }, + { url = "https://files.pythonhosted.org/packages/c7/57/01c37608121380a6357a297625562adad1c1fc8058d4f62279b735108927/levenshtein-0.27.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:12f7fc8bf0c24492fe97905348e020b55b9fc6dbaab7cd452566d1a466cb5e15", size = 1185338, upload-time = "2025-11-01T12:13:34.452Z" }, + { url = "https://files.pythonhosted.org/packages/dd/57/bceab41d40b58dee7927a8d1d18ed3bff7c95c5e530fb60093ce741a8c26/levenshtein-0.27.3-cp313-cp313-win32.whl", hash = "sha256:9f4872e4e19ee48eed39f214eea4eca42e5ef303f8a4a488d8312370674dbf3a", size = 84562, upload-time = "2025-11-01T12:13:35.858Z" }, + { url = "https://files.pythonhosted.org/packages/42/1d/74f1ff589bb687d0cad2bbdceef208dc070f56d1e38a3831da8c00bf13bb/levenshtein-0.27.3-cp313-cp313-win_amd64.whl", hash = "sha256:83aa2422e9a9af2c9d3e56a53e3e8de6bae58d1793628cae48c4282577c5c2c6", size = 94658, upload-time = "2025-11-01T12:13:36.963Z" }, + { url = "https://files.pythonhosted.org/packages/21/3c/22c86d3c8f254141096fd6089d2e9fdf98b1472c7a5d79d36d3557ec2d83/levenshtein-0.27.3-cp313-cp313-win_arm64.whl", hash = "sha256:d4adaf1edbcf38c3f2e290b52f4dcb5c6deff20308c26ef1127a106bc2d23e9f", size = 86929, upload-time = "2025-11-01T12:13:37.997Z" }, + { url = "https://files.pythonhosted.org/packages/0e/bc/9b7cf1b5fa098b86844d42de22549304699deff309c5c9e28b9a3fc4076a/levenshtein-0.27.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:272e24764b8210337b65a1cfd69ce40df5d2de1a3baf1234e7f06d2826ba2e7a", size = 170360, upload-time = "2025-11-01T12:13:39.019Z" }, + { url = "https://files.pythonhosted.org/packages/dc/95/997f2c83bd4712426bf0de8143b5e4403c7ebbafb5d1271983e774de3ae7/levenshtein-0.27.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:329a8e748a4e14d56daaa11f07bce3fde53385d05bad6b3f6dd9ee7802cdc915", size = 159098, upload-time = "2025-11-01T12:13:40.17Z" }, + { url = "https://files.pythonhosted.org/packages/fc/96/123c3316ae2f72c73be4fba9756924af015da4c0e5b12804f5753c0ee511/levenshtein-0.27.3-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a5fea1a9c6b9cc8729e467e2174b4359ff6bac27356bb5f31898e596b4ce133a", size = 136655, upload-time = "2025-11-01T12:13:41.262Z" }, + { url = "https://files.pythonhosted.org/packages/45/72/a3180d437736b1b9eacc3100be655a756deafb91de47c762d40eb45a9d91/levenshtein-0.27.3-cp313-cp313t-manylinux_2_24_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3a61aa825819b6356555091d8a575d1235bd9c3753a68316a261af4856c3b487", size = 117511, upload-time = "2025-11-01T12:13:42.647Z" }, + { url = "https://files.pythonhosted.org/packages/61/f9/ba7c546a4b99347938e6661104064ab6a3651c601d59f241ffdc37510ecc/levenshtein-0.27.3-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a51de7a514e8183f0a82f2947d01b014d2391426543b1c076bf5a26328cec4e4", size = 155656, upload-time = "2025-11-01T12:13:44.208Z" }, + { url = "https://files.pythonhosted.org/packages/42/cd/5edd6e1e02c3e47c8121761756dd0f85f816b636f25509118b687e6b0f96/levenshtein-0.27.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:53cbf726d6e92040c9be7e594d959d496bd62597ea48eba9d96105898acbeafe", size = 1116689, upload-time = "2025-11-01T12:13:45.485Z" }, + { url = "https://files.pythonhosted.org/packages/95/67/25ca0119e0c6ec17226c72638f48ef8887124597ac48ad5da111c0b3a825/levenshtein-0.27.3-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:191b358afead8561c4fcfed22f83c13bb6c8da5f5789e277f0c5aa1c45ca612f", size = 1003166, upload-time = "2025-11-01T12:13:47.126Z" }, + { url = "https://files.pythonhosted.org/packages/45/64/ab216f3fb3cef1ee7e222665537f9340d828ef84c99409ba31f2ef2a3947/levenshtein-0.27.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ba1318d0635b834b8f0397014a7c43f007e65fce396a47614780c881bdff828b", size = 1189362, upload-time = "2025-11-01T12:13:48.627Z" }, + { url = "https://files.pythonhosted.org/packages/31/58/b150034858de0899a5a222974b6710618ebc0779a0695df070f7ab559a0b/levenshtein-0.27.3-cp313-cp313t-win32.whl", hash = "sha256:8dd9e1db6c3b35567043e155a686e4827c4aa28a594bd81e3eea84d3a1bd5875", size = 86149, upload-time = "2025-11-01T12:13:50.588Z" }, + { url = "https://files.pythonhosted.org/packages/0a/c4/bbe46a11073641450200e6a604b3b62d311166e8061c492612a40e560e85/levenshtein-0.27.3-cp313-cp313t-win_amd64.whl", hash = "sha256:7813ecdac7a6223264ebfea0c8d69959c43d21a99694ef28018d22c4265c2af6", size = 96685, upload-time = "2025-11-01T12:13:51.641Z" }, + { url = "https://files.pythonhosted.org/packages/23/65/30b362ad9bfc1085741776a08b6ddee3f434e9daac2920daaee2e26271bf/levenshtein-0.27.3-cp313-cp313t-win_arm64.whl", hash = "sha256:8f05a0d23d13a6f802c7af595d0e43f5b9b98b6ed390cec7a35cb5d6693b882b", size = 88538, upload-time = "2025-11-01T12:13:52.757Z" }, + { url = "https://files.pythonhosted.org/packages/f3/e1/2f705da403f865a5fa3449b155738dc9c53021698fd6926253a9af03180b/levenshtein-0.27.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a6728bfae9a86002f0223576675fc7e2a6e7735da47185a1d13d1eaaa73dd4be", size = 169457, upload-time = "2025-11-01T12:13:53.778Z" }, + { url = "https://files.pythonhosted.org/packages/76/2c/bb6ef359e007fe7b6b3195b68a94f4dd3ecd1885ee337ee8fbd4df55996f/levenshtein-0.27.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8e5037c4a6f97a238e24aad6f98a1e984348b7931b1b04b6bd02bd4f8238150d", size = 158680, upload-time = "2025-11-01T12:13:55.005Z" }, + { url = "https://files.pythonhosted.org/packages/51/7b/de1999f4cf1cfebc3fbbf03a6d58498952d6560d9798af4b0a566e6b6f30/levenshtein-0.27.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c6cf5ecf9026bf24cf66ad019c6583f50058fae3e1b3c20e8812455b55d597f1", size = 133167, upload-time = "2025-11-01T12:13:56.426Z" }, + { url = "https://files.pythonhosted.org/packages/c7/da/aaa7f3a0a8ae8744b284043653652db3d7d93595517f9ed8158c03287692/levenshtein-0.27.3-cp314-cp314-manylinux_2_24_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9285084bd2fc19adb47dab54ed4a71f57f78fe0d754e4a01e3c75409a25aed24", size = 114530, upload-time = "2025-11-01T12:13:57.883Z" }, + { url = "https://files.pythonhosted.org/packages/29/ce/ed422816fb30ffa3bc11597b30d5deca06b4a1388707a04215da73c65b53/levenshtein-0.27.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ce3bbbe92172a08b599d79956182c6b7ab6ec8d4adbe7237417a363b968ad87b", size = 153325, upload-time = "2025-11-01T12:13:59.318Z" }, + { url = "https://files.pythonhosted.org/packages/d9/5a/a225477a0bda154f19f1c07a5e35500d631ae25dfd620b479027d79f0d4c/levenshtein-0.27.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9dac48fab9d166ca90e12fb6cf6c7c8eb9c41aacf7136584411e20f7f136f745", size = 1114956, upload-time = "2025-11-01T12:14:00.543Z" }, + { url = "https://files.pythonhosted.org/packages/ca/c4/a1be1040f3cce516a5e2be68453fd0c32ac63b2e9d31f476723fd8002c09/levenshtein-0.27.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d37a83722dc5326c93d17078e926c4732dc4f3488dc017c6839e34cd16af92b7", size = 1007610, upload-time = "2025-11-01T12:14:02.036Z" }, + { url = "https://files.pythonhosted.org/packages/86/d7/6f50e8a307e0c2befd819b481eb3a4c2eacab3dd8101982423003fac8ea3/levenshtein-0.27.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3466cb8294ce586e49dd467560a153ab8d296015c538223f149f9aefd3d9f955", size = 1185379, upload-time = "2025-11-01T12:14:03.385Z" }, + { url = "https://files.pythonhosted.org/packages/6b/e5/5d8fb1b3ebd5735f53221bf95c923066bcfc132234925820128f7eee5b47/levenshtein-0.27.3-cp314-cp314-win32.whl", hash = "sha256:c848bf2457b268672b7e9e73b44f18f49856420ac50b2564cf115a6e4ef82688", size = 86328, upload-time = "2025-11-01T12:14:04.74Z" }, + { url = "https://files.pythonhosted.org/packages/30/82/8a9ccbdb4e38bd4d516f2804999dccb8cb4bcb4e33f52851735da0c73ea7/levenshtein-0.27.3-cp314-cp314-win_amd64.whl", hash = "sha256:742633f024362a4ed6ef9d7e75d68f74b041ae738985fcf55a0e6d1d4cade438", size = 96640, upload-time = "2025-11-01T12:14:06.24Z" }, + { url = "https://files.pythonhosted.org/packages/14/86/f9d15919f59f5d92c6baa500315e1fa0143a39d811427b83c54f038267ca/levenshtein-0.27.3-cp314-cp314-win_arm64.whl", hash = "sha256:9eed6851224b19e8d588ddb8eb8a4ae3c2dcabf3d1213985f0b94a67e517b1df", size = 89689, upload-time = "2025-11-01T12:14:07.379Z" }, + { url = "https://files.pythonhosted.org/packages/ed/f6/10f44975ae6dc3047b2cd260e3d4c3a5258b8d10690a42904115de24fc51/levenshtein-0.27.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:77de69a345c76227b51a4521cd85442eb3da54c7eb6a06663a20c058fc49e683", size = 170518, upload-time = "2025-11-01T12:14:09.196Z" }, + { url = "https://files.pythonhosted.org/packages/08/07/fa294a145a0c99a814a9a807614962c1ee0f5749ca691645980462027d5d/levenshtein-0.27.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:eba2756dc1f5b962b0ff80e49abb2153d5e809cc5e7fa5e85be9410ce474795d", size = 159097, upload-time = "2025-11-01T12:14:10.404Z" }, + { url = "https://files.pythonhosted.org/packages/ae/50/24bdf37813fc30f293e53b46022b091144f4737a6a66663d2235b311bb98/levenshtein-0.27.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c8fcb498287e971d84260f67808ff1a06b3f6212d80fea75cf5155db80606ff", size = 136650, upload-time = "2025-11-01T12:14:11.579Z" }, + { url = "https://files.pythonhosted.org/packages/d0/a9/0399c7a190b277cdea3acc801129d9d30da57c3fa79519e7b8c3f080d86c/levenshtein-0.27.3-cp314-cp314t-manylinux_2_24_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:f067092c67464faab13e00a5c1a80da93baca8955d4d49579861400762e35591", size = 117515, upload-time = "2025-11-01T12:14:12.877Z" }, + { url = "https://files.pythonhosted.org/packages/bf/a4/1c27533e97578b385a4b8079abe8d1ce2e514717c761efbe4bf7bbd0ac2e/levenshtein-0.27.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:92415f32c68491203f2855d05eef3277d376182d014cf0859c013c89f277fbbf", size = 155711, upload-time = "2025-11-01T12:14:13.985Z" }, + { url = "https://files.pythonhosted.org/packages/50/35/bbc26638394a72b1e31a685ec251c995ee66a630c7e5c86f98770928b632/levenshtein-0.27.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ef61eeaf1e0a42d7d947978d981fe4b9426b98b3dd8c1582c535f10dee044c3f", size = 1116692, upload-time = "2025-11-01T12:14:15.359Z" }, + { url = "https://files.pythonhosted.org/packages/cd/83/32fcf28b388f8dc6c36b54552b9bae289dab07d43df104893158c834cbcc/levenshtein-0.27.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:103bb2e9049d1aa0d1216dd09c1c9106ecfe7541bbdc1a0490b9357d42eec8f2", size = 1003167, upload-time = "2025-11-01T12:14:17.469Z" }, + { url = "https://files.pythonhosted.org/packages/d1/79/1fbf2877ec4b819f373a32ebe3c48a61ee810693593a6015108b0be97b78/levenshtein-0.27.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6a64ddd1986b2a4c468b09544382287315c53585eb067f6e200c337741e057ee", size = 1189417, upload-time = "2025-11-01T12:14:19.081Z" }, + { url = "https://files.pythonhosted.org/packages/d3/ac/dad4e09f1f7459c64172e48e40ed2baf3aa92d38205bcbd1b4ff00853701/levenshtein-0.27.3-cp314-cp314t-win32.whl", hash = "sha256:957244f27dc284ccb030a8b77b8a00deb7eefdcd70052a4b1d96f375780ae9dc", size = 88144, upload-time = "2025-11-01T12:14:20.667Z" }, + { url = "https://files.pythonhosted.org/packages/c0/61/cd51dc8b8a382e17c559a9812734c3a9afc2dab7d36253516335ee16ae50/levenshtein-0.27.3-cp314-cp314t-win_amd64.whl", hash = "sha256:ccd7eaa6d8048c3ec07c93cfbcdefd4a3ae8c6aca3a370f2023ee69341e5f076", size = 98516, upload-time = "2025-11-01T12:14:21.786Z" }, + { url = "https://files.pythonhosted.org/packages/27/5e/3fb67e882c1fee01ebb7abc1c0a6669e5ff8acd060e93bfe7229e9ce6e4f/levenshtein-0.27.3-cp314-cp314t-win_arm64.whl", hash = "sha256:1d8520b89b7a27bb5aadbcc156715619bcbf556a8ac46ad932470945dca6e1bd", size = 91020, upload-time = "2025-11-01T12:14:22.944Z" }, + { url = "https://files.pythonhosted.org/packages/b4/bc/21983893d3f40c6990e2e51c02dd48cfca350a36214be90d7c58f5f85896/levenshtein-0.27.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:d2d7d22b6117a143f0cf101fe18a3ca90bd949fc33716a42d6165b9768d4a78c", size = 166073, upload-time = "2025-11-01T12:14:24.436Z" }, + { url = "https://files.pythonhosted.org/packages/ef/bb/52deb821ebf0cfc61baf7c9ebc5601649cfbfdaaaf156867786d1c5332d5/levenshtein-0.27.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:a55e7a2f317abd28576636e1f840fd268261f447c496a8481a9997a5ce889c59", size = 153629, upload-time = "2025-11-01T12:14:25.623Z" }, + { url = "https://files.pythonhosted.org/packages/60/0c/b72e6e2d16efd57c143785a30370ca50c2e355a9d0d678edb1c024865447/levenshtein-0.27.3-pp311-pypy311_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55fa5f11952c38186bd4719e936eb4595b3d519218634924928787c36840256c", size = 130242, upload-time = "2025-11-01T12:14:26.926Z" }, + { url = "https://files.pythonhosted.org/packages/b5/b0/0aafad0dab03a58fd507773d3ff94ec13efdd3772ba217f85366213ab7ae/levenshtein-0.27.3-pp311-pypy311_pp73-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:559d3588e6766134d95f84f830cf40166360e1769d253f5f83474bff10a24341", size = 150655, upload-time = "2025-11-01T12:14:28.034Z" }, + { url = "https://files.pythonhosted.org/packages/b7/77/42dbcbafe9e0b0eb14cb6b08378c8c3bdc563ee34ee58f62e708e7f8956e/levenshtein-0.27.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:82d40da143c1b9e27adcd34a33dfcc4a0761aa717c5f618b9c6f57dec5d7a958", size = 92370, upload-time = "2025-11-01T12:14:29.143Z" }, +] + +[[package]] +name = "lxml" +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/88/262177de60548e5a2bfc46ad28232c9e9cbde697bd94132aeb80364675cb/lxml-6.0.2.tar.gz", hash = "sha256:cd79f3367bd74b317dda655dc8fcfa304d9eb6e4fb06b7168c5cf27f96e0cd62", size = 4073426, upload-time = "2025-09-22T04:04:59.287Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/d5/becbe1e2569b474a23f0c672ead8a29ac50b2dc1d5b9de184831bda8d14c/lxml-6.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:13e35cbc684aadf05d8711a5d1b5857c92e5e580efa9a0d2be197199c8def607", size = 8634365, upload-time = "2025-09-22T04:00:45.672Z" }, + { url = "https://files.pythonhosted.org/packages/28/66/1ced58f12e804644426b85d0bb8a4478ca77bc1761455da310505f1a3526/lxml-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3b1675e096e17c6fe9c0e8c81434f5736c0739ff9ac6123c87c2d452f48fc938", size = 4650793, upload-time = "2025-09-22T04:00:47.783Z" }, + { url = "https://files.pythonhosted.org/packages/11/84/549098ffea39dfd167e3f174b4ce983d0eed61f9d8d25b7bf2a57c3247fc/lxml-6.0.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8ac6e5811ae2870953390452e3476694196f98d447573234592d30488147404d", size = 4944362, upload-time = "2025-09-22T04:00:49.845Z" }, + { url = "https://files.pythonhosted.org/packages/ac/bd/f207f16abf9749d2037453d56b643a7471d8fde855a231a12d1e095c4f01/lxml-6.0.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5aa0fc67ae19d7a64c3fe725dc9a1bb11f80e01f78289d05c6f62545affec438", size = 5083152, upload-time = "2025-09-22T04:00:51.709Z" }, + { url = "https://files.pythonhosted.org/packages/15/ae/bd813e87d8941d52ad5b65071b1affb48da01c4ed3c9c99e40abb266fbff/lxml-6.0.2-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de496365750cc472b4e7902a485d3f152ecf57bd3ba03ddd5578ed8ceb4c5964", size = 5023539, upload-time = "2025-09-22T04:00:53.593Z" }, + { url = "https://files.pythonhosted.org/packages/02/cd/9bfef16bd1d874fbe0cb51afb00329540f30a3283beb9f0780adbb7eec03/lxml-6.0.2-cp311-cp311-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:200069a593c5e40b8f6fc0d84d86d970ba43138c3e68619ffa234bc9bb806a4d", size = 5344853, upload-time = "2025-09-22T04:00:55.524Z" }, + { url = "https://files.pythonhosted.org/packages/b8/89/ea8f91594bc5dbb879734d35a6f2b0ad50605d7fb419de2b63d4211765cc/lxml-6.0.2-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7d2de809c2ee3b888b59f995625385f74629707c9355e0ff856445cdcae682b7", size = 5225133, upload-time = "2025-09-22T04:00:57.269Z" }, + { url = "https://files.pythonhosted.org/packages/b9/37/9c735274f5dbec726b2db99b98a43950395ba3d4a1043083dba2ad814170/lxml-6.0.2-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:b2c3da8d93cf5db60e8858c17684c47d01fee6405e554fb55018dd85fc23b178", size = 4677944, upload-time = "2025-09-22T04:00:59.052Z" }, + { url = "https://files.pythonhosted.org/packages/20/28/7dfe1ba3475d8bfca3878365075abe002e05d40dfaaeb7ec01b4c587d533/lxml-6.0.2-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:442de7530296ef5e188373a1ea5789a46ce90c4847e597856570439621d9c553", size = 5284535, upload-time = "2025-09-22T04:01:01.335Z" }, + { url = "https://files.pythonhosted.org/packages/e7/cf/5f14bc0de763498fc29510e3532bf2b4b3a1c1d5d0dff2e900c16ba021ef/lxml-6.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2593c77efde7bfea7f6389f1ab249b15ed4aa5bc5cb5131faa3b843c429fbedb", size = 5067343, upload-time = "2025-09-22T04:01:03.13Z" }, + { url = "https://files.pythonhosted.org/packages/1c/b0/bb8275ab5472f32b28cfbbcc6db7c9d092482d3439ca279d8d6fa02f7025/lxml-6.0.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:3e3cb08855967a20f553ff32d147e14329b3ae70ced6edc2f282b94afbc74b2a", size = 4725419, upload-time = "2025-09-22T04:01:05.013Z" }, + { url = "https://files.pythonhosted.org/packages/25/4c/7c222753bc72edca3b99dbadba1b064209bc8ed4ad448af990e60dcce462/lxml-6.0.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:2ed6c667fcbb8c19c6791bbf40b7268ef8ddf5a96940ba9404b9f9a304832f6c", size = 5275008, upload-time = "2025-09-22T04:01:07.327Z" }, + { url = "https://files.pythonhosted.org/packages/6c/8c/478a0dc6b6ed661451379447cdbec77c05741a75736d97e5b2b729687828/lxml-6.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b8f18914faec94132e5b91e69d76a5c1d7b0c73e2489ea8929c4aaa10b76bbf7", size = 5248906, upload-time = "2025-09-22T04:01:09.452Z" }, + { url = "https://files.pythonhosted.org/packages/2d/d9/5be3a6ab2784cdf9accb0703b65e1b64fcdd9311c9f007630c7db0cfcce1/lxml-6.0.2-cp311-cp311-win32.whl", hash = "sha256:6605c604e6daa9e0d7f0a2137bdc47a2e93b59c60a65466353e37f8272f47c46", size = 3610357, upload-time = "2025-09-22T04:01:11.102Z" }, + { url = "https://files.pythonhosted.org/packages/e2/7d/ca6fb13349b473d5732fb0ee3eec8f6c80fc0688e76b7d79c1008481bf1f/lxml-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e5867f2651016a3afd8dd2c8238baa66f1e2802f44bc17e236f547ace6647078", size = 4036583, upload-time = "2025-09-22T04:01:12.766Z" }, + { url = "https://files.pythonhosted.org/packages/ab/a2/51363b5ecd3eab46563645f3a2c3836a2fc67d01a1b87c5017040f39f567/lxml-6.0.2-cp311-cp311-win_arm64.whl", hash = "sha256:4197fb2534ee05fd3e7afaab5d8bfd6c2e186f65ea7f9cd6a82809c887bd1285", size = 3680591, upload-time = "2025-09-22T04:01:14.874Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c8/8ff2bc6b920c84355146cd1ab7d181bc543b89241cfb1ebee824a7c81457/lxml-6.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a59f5448ba2ceccd06995c95ea59a7674a10de0810f2ce90c9006f3cbc044456", size = 8661887, upload-time = "2025-09-22T04:01:17.265Z" }, + { url = "https://files.pythonhosted.org/packages/37/6f/9aae1008083bb501ef63284220ce81638332f9ccbfa53765b2b7502203cf/lxml-6.0.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e8113639f3296706fbac34a30813929e29247718e88173ad849f57ca59754924", size = 4667818, upload-time = "2025-09-22T04:01:19.688Z" }, + { url = "https://files.pythonhosted.org/packages/f1/ca/31fb37f99f37f1536c133476674c10b577e409c0a624384147653e38baf2/lxml-6.0.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a8bef9b9825fa8bc816a6e641bb67219489229ebc648be422af695f6e7a4fa7f", size = 4950807, upload-time = "2025-09-22T04:01:21.487Z" }, + { url = "https://files.pythonhosted.org/packages/da/87/f6cb9442e4bada8aab5ae7e1046264f62fdbeaa6e3f6211b93f4c0dd97f1/lxml-6.0.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:65ea18d710fd14e0186c2f973dc60bb52039a275f82d3c44a0e42b43440ea534", size = 5109179, upload-time = "2025-09-22T04:01:23.32Z" }, + { url = "https://files.pythonhosted.org/packages/c8/20/a7760713e65888db79bbae4f6146a6ae5c04e4a204a3c48896c408cd6ed2/lxml-6.0.2-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c371aa98126a0d4c739ca93ceffa0fd7a5d732e3ac66a46e74339acd4d334564", size = 5023044, upload-time = "2025-09-22T04:01:25.118Z" }, + { url = "https://files.pythonhosted.org/packages/a2/b0/7e64e0460fcb36471899f75831509098f3fd7cd02a3833ac517433cb4f8f/lxml-6.0.2-cp312-cp312-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:700efd30c0fa1a3581d80a748157397559396090a51d306ea59a70020223d16f", size = 5359685, upload-time = "2025-09-22T04:01:27.398Z" }, + { url = "https://files.pythonhosted.org/packages/b9/e1/e5df362e9ca4e2f48ed6411bd4b3a0ae737cc842e96877f5bf9428055ab4/lxml-6.0.2-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c33e66d44fe60e72397b487ee92e01da0d09ba2d66df8eae42d77b6d06e5eba0", size = 5654127, upload-time = "2025-09-22T04:01:29.629Z" }, + { url = "https://files.pythonhosted.org/packages/c6/d1/232b3309a02d60f11e71857778bfcd4acbdb86c07db8260caf7d008b08f8/lxml-6.0.2-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90a345bbeaf9d0587a3aaffb7006aa39ccb6ff0e96a57286c0cb2fd1520ea192", size = 5253958, upload-time = "2025-09-22T04:01:31.535Z" }, + { url = "https://files.pythonhosted.org/packages/35/35/d955a070994725c4f7d80583a96cab9c107c57a125b20bb5f708fe941011/lxml-6.0.2-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:064fdadaf7a21af3ed1dcaa106b854077fbeada827c18f72aec9346847cd65d0", size = 4711541, upload-time = "2025-09-22T04:01:33.801Z" }, + { url = "https://files.pythonhosted.org/packages/1e/be/667d17363b38a78c4bd63cfd4b4632029fd68d2c2dc81f25ce9eb5224dd5/lxml-6.0.2-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fbc74f42c3525ac4ffa4b89cbdd00057b6196bcefe8bce794abd42d33a018092", size = 5267426, upload-time = "2025-09-22T04:01:35.639Z" }, + { url = "https://files.pythonhosted.org/packages/ea/47/62c70aa4a1c26569bc958c9ca86af2bb4e1f614e8c04fb2989833874f7ae/lxml-6.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6ddff43f702905a4e32bc24f3f2e2edfe0f8fde3277d481bffb709a4cced7a1f", size = 5064917, upload-time = "2025-09-22T04:01:37.448Z" }, + { url = "https://files.pythonhosted.org/packages/bd/55/6ceddaca353ebd0f1908ef712c597f8570cc9c58130dbb89903198e441fd/lxml-6.0.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:6da5185951d72e6f5352166e3da7b0dc27aa70bd1090b0eb3f7f7212b53f1bb8", size = 4788795, upload-time = "2025-09-22T04:01:39.165Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e8/fd63e15da5e3fd4c2146f8bbb3c14e94ab850589beab88e547b2dbce22e1/lxml-6.0.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:57a86e1ebb4020a38d295c04fc79603c7899e0df71588043eb218722dabc087f", size = 5676759, upload-time = "2025-09-22T04:01:41.506Z" }, + { url = "https://files.pythonhosted.org/packages/76/47/b3ec58dc5c374697f5ba37412cd2728f427d056315d124dd4b61da381877/lxml-6.0.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:2047d8234fe735ab77802ce5f2297e410ff40f5238aec569ad7c8e163d7b19a6", size = 5255666, upload-time = "2025-09-22T04:01:43.363Z" }, + { url = "https://files.pythonhosted.org/packages/19/93/03ba725df4c3d72afd9596eef4a37a837ce8e4806010569bedfcd2cb68fd/lxml-6.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6f91fd2b2ea15a6800c8e24418c0775a1694eefc011392da73bc6cef2623b322", size = 5277989, upload-time = "2025-09-22T04:01:45.215Z" }, + { url = "https://files.pythonhosted.org/packages/c6/80/c06de80bfce881d0ad738576f243911fccf992687ae09fd80b734712b39c/lxml-6.0.2-cp312-cp312-win32.whl", hash = "sha256:3ae2ce7d6fedfb3414a2b6c5e20b249c4c607f72cb8d2bb7cc9c6ec7c6f4e849", size = 3611456, upload-time = "2025-09-22T04:01:48.243Z" }, + { url = "https://files.pythonhosted.org/packages/f7/d7/0cdfb6c3e30893463fb3d1e52bc5f5f99684a03c29a0b6b605cfae879cd5/lxml-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:72c87e5ee4e58a8354fb9c7c84cbf95a1c8236c127a5d1b7683f04bed8361e1f", size = 4011793, upload-time = "2025-09-22T04:01:50.042Z" }, + { url = "https://files.pythonhosted.org/packages/ea/7b/93c73c67db235931527301ed3785f849c78991e2e34f3fd9a6663ffda4c5/lxml-6.0.2-cp312-cp312-win_arm64.whl", hash = "sha256:61cb10eeb95570153e0c0e554f58df92ecf5109f75eacad4a95baa709e26c3d6", size = 3672836, upload-time = "2025-09-22T04:01:52.145Z" }, + { url = "https://files.pythonhosted.org/packages/53/fd/4e8f0540608977aea078bf6d79f128e0e2c2bba8af1acf775c30baa70460/lxml-6.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9b33d21594afab46f37ae58dfadd06636f154923c4e8a4d754b0127554eb2e77", size = 8648494, upload-time = "2025-09-22T04:01:54.242Z" }, + { url = "https://files.pythonhosted.org/packages/5d/f4/2a94a3d3dfd6c6b433501b8d470a1960a20ecce93245cf2db1706adf6c19/lxml-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6c8963287d7a4c5c9a432ff487c52e9c5618667179c18a204bdedb27310f022f", size = 4661146, upload-time = "2025-09-22T04:01:56.282Z" }, + { url = "https://files.pythonhosted.org/packages/25/2e/4efa677fa6b322013035d38016f6ae859d06cac67437ca7dc708a6af7028/lxml-6.0.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1941354d92699fb5ffe6ed7b32f9649e43c2feb4b97205f75866f7d21aa91452", size = 4946932, upload-time = "2025-09-22T04:01:58.989Z" }, + { url = "https://files.pythonhosted.org/packages/ce/0f/526e78a6d38d109fdbaa5049c62e1d32fdd70c75fb61c4eadf3045d3d124/lxml-6.0.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bb2f6ca0ae2d983ded09357b84af659c954722bbf04dea98030064996d156048", size = 5100060, upload-time = "2025-09-22T04:02:00.812Z" }, + { url = "https://files.pythonhosted.org/packages/81/76/99de58d81fa702cc0ea7edae4f4640416c2062813a00ff24bd70ac1d9c9b/lxml-6.0.2-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb2a12d704f180a902d7fa778c6d71f36ceb7b0d317f34cdc76a5d05aa1dd1df", size = 5019000, upload-time = "2025-09-22T04:02:02.671Z" }, + { url = "https://files.pythonhosted.org/packages/b5/35/9e57d25482bc9a9882cb0037fdb9cc18f4b79d85df94fa9d2a89562f1d25/lxml-6.0.2-cp313-cp313-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:6ec0e3f745021bfed19c456647f0298d60a24c9ff86d9d051f52b509663feeb1", size = 5348496, upload-time = "2025-09-22T04:02:04.904Z" }, + { url = "https://files.pythonhosted.org/packages/a6/8e/cb99bd0b83ccc3e8f0f528e9aa1f7a9965dfec08c617070c5db8d63a87ce/lxml-6.0.2-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:846ae9a12d54e368933b9759052d6206a9e8b250291109c48e350c1f1f49d916", size = 5643779, upload-time = "2025-09-22T04:02:06.689Z" }, + { url = "https://files.pythonhosted.org/packages/d0/34/9e591954939276bb679b73773836c6684c22e56d05980e31d52a9a8deb18/lxml-6.0.2-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ef9266d2aa545d7374938fb5c484531ef5a2ec7f2d573e62f8ce722c735685fd", size = 5244072, upload-time = "2025-09-22T04:02:08.587Z" }, + { url = "https://files.pythonhosted.org/packages/8d/27/b29ff065f9aaca443ee377aff699714fcbffb371b4fce5ac4ca759e436d5/lxml-6.0.2-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:4077b7c79f31755df33b795dc12119cb557a0106bfdab0d2c2d97bd3cf3dffa6", size = 4718675, upload-time = "2025-09-22T04:02:10.783Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9f/f756f9c2cd27caa1a6ef8c32ae47aadea697f5c2c6d07b0dae133c244fbe/lxml-6.0.2-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a7c5d5e5f1081955358533be077166ee97ed2571d6a66bdba6ec2f609a715d1a", size = 5255171, upload-time = "2025-09-22T04:02:12.631Z" }, + { url = "https://files.pythonhosted.org/packages/61/46/bb85ea42d2cb1bd8395484fd72f38e3389611aa496ac7772da9205bbda0e/lxml-6.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8f8d0cbd0674ee89863a523e6994ac25fd5be9c8486acfc3e5ccea679bad2679", size = 5057175, upload-time = "2025-09-22T04:02:14.718Z" }, + { url = "https://files.pythonhosted.org/packages/95/0c/443fc476dcc8e41577f0af70458c50fe299a97bb6b7505bb1ae09aa7f9ac/lxml-6.0.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:2cbcbf6d6e924c28f04a43f3b6f6e272312a090f269eff68a2982e13e5d57659", size = 4785688, upload-time = "2025-09-22T04:02:16.957Z" }, + { url = "https://files.pythonhosted.org/packages/48/78/6ef0b359d45bb9697bc5a626e1992fa5d27aa3f8004b137b2314793b50a0/lxml-6.0.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:dfb874cfa53340009af6bdd7e54ebc0d21012a60a4e65d927c2e477112e63484", size = 5660655, upload-time = "2025-09-22T04:02:18.815Z" }, + { url = "https://files.pythonhosted.org/packages/ff/ea/e1d33808f386bc1339d08c0dcada6e4712d4ed8e93fcad5f057070b7988a/lxml-6.0.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:fb8dae0b6b8b7f9e96c26fdd8121522ce5de9bb5538010870bd538683d30e9a2", size = 5247695, upload-time = "2025-09-22T04:02:20.593Z" }, + { url = "https://files.pythonhosted.org/packages/4f/47/eba75dfd8183673725255247a603b4ad606f4ae657b60c6c145b381697da/lxml-6.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:358d9adae670b63e95bc59747c72f4dc97c9ec58881d4627fe0120da0f90d314", size = 5269841, upload-time = "2025-09-22T04:02:22.489Z" }, + { url = "https://files.pythonhosted.org/packages/76/04/5c5e2b8577bc936e219becb2e98cdb1aca14a4921a12995b9d0c523502ae/lxml-6.0.2-cp313-cp313-win32.whl", hash = "sha256:e8cd2415f372e7e5a789d743d133ae474290a90b9023197fd78f32e2dc6873e2", size = 3610700, upload-time = "2025-09-22T04:02:24.465Z" }, + { url = "https://files.pythonhosted.org/packages/fe/0a/4643ccc6bb8b143e9f9640aa54e38255f9d3b45feb2cbe7ae2ca47e8782e/lxml-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:b30d46379644fbfc3ab81f8f82ae4de55179414651f110a1514f0b1f8f6cb2d7", size = 4010347, upload-time = "2025-09-22T04:02:26.286Z" }, + { url = "https://files.pythonhosted.org/packages/31/ef/dcf1d29c3f530577f61e5fe2f1bd72929acf779953668a8a47a479ae6f26/lxml-6.0.2-cp313-cp313-win_arm64.whl", hash = "sha256:13dcecc9946dca97b11b7c40d29fba63b55ab4170d3c0cf8c0c164343b9bfdcf", size = 3671248, upload-time = "2025-09-22T04:02:27.918Z" }, + { url = "https://files.pythonhosted.org/packages/03/15/d4a377b385ab693ce97b472fe0c77c2b16ec79590e688b3ccc71fba19884/lxml-6.0.2-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:b0c732aa23de8f8aec23f4b580d1e52905ef468afb4abeafd3fec77042abb6fe", size = 8659801, upload-time = "2025-09-22T04:02:30.113Z" }, + { url = "https://files.pythonhosted.org/packages/c8/e8/c128e37589463668794d503afaeb003987373c5f94d667124ffd8078bbd9/lxml-6.0.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4468e3b83e10e0317a89a33d28f7aeba1caa4d1a6fd457d115dd4ffe90c5931d", size = 4659403, upload-time = "2025-09-22T04:02:32.119Z" }, + { url = "https://files.pythonhosted.org/packages/00/ce/74903904339decdf7da7847bb5741fc98a5451b42fc419a86c0c13d26fe2/lxml-6.0.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:abd44571493973bad4598a3be7e1d807ed45aa2adaf7ab92ab7c62609569b17d", size = 4966974, upload-time = "2025-09-22T04:02:34.155Z" }, + { url = "https://files.pythonhosted.org/packages/1f/d3/131dec79ce61c5567fecf82515bd9bc36395df42501b50f7f7f3bd065df0/lxml-6.0.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:370cd78d5855cfbffd57c422851f7d3864e6ae72d0da615fca4dad8c45d375a5", size = 5102953, upload-time = "2025-09-22T04:02:36.054Z" }, + { url = "https://files.pythonhosted.org/packages/3a/ea/a43ba9bb750d4ffdd885f2cd333572f5bb900cd2408b67fdda07e85978a0/lxml-6.0.2-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:901e3b4219fa04ef766885fb40fa516a71662a4c61b80c94d25336b4934b71c0", size = 5055054, upload-time = "2025-09-22T04:02:38.154Z" }, + { url = "https://files.pythonhosted.org/packages/60/23/6885b451636ae286c34628f70a7ed1fcc759f8d9ad382d132e1c8d3d9bfd/lxml-6.0.2-cp314-cp314-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:a4bf42d2e4cf52c28cc1812d62426b9503cdb0c87a6de81442626aa7d69707ba", size = 5352421, upload-time = "2025-09-22T04:02:40.413Z" }, + { url = "https://files.pythonhosted.org/packages/48/5b/fc2ddfc94ddbe3eebb8e9af6e3fd65e2feba4967f6a4e9683875c394c2d8/lxml-6.0.2-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2c7fdaa4d7c3d886a42534adec7cfac73860b89b4e5298752f60aa5984641a0", size = 5673684, upload-time = "2025-09-22T04:02:42.288Z" }, + { url = "https://files.pythonhosted.org/packages/29/9c/47293c58cc91769130fbf85531280e8cc7868f7fbb6d92f4670071b9cb3e/lxml-6.0.2-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:98a5e1660dc7de2200b00d53fa00bcd3c35a3608c305d45a7bbcaf29fa16e83d", size = 5252463, upload-time = "2025-09-22T04:02:44.165Z" }, + { url = "https://files.pythonhosted.org/packages/9b/da/ba6eceb830c762b48e711ded880d7e3e89fc6c7323e587c36540b6b23c6b/lxml-6.0.2-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:dc051506c30b609238d79eda75ee9cab3e520570ec8219844a72a46020901e37", size = 4698437, upload-time = "2025-09-22T04:02:46.524Z" }, + { url = "https://files.pythonhosted.org/packages/a5/24/7be3f82cb7990b89118d944b619e53c656c97dc89c28cfb143fdb7cd6f4d/lxml-6.0.2-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8799481bbdd212470d17513a54d568f44416db01250f49449647b5ab5b5dccb9", size = 5269890, upload-time = "2025-09-22T04:02:48.812Z" }, + { url = "https://files.pythonhosted.org/packages/1b/bd/dcfb9ea1e16c665efd7538fc5d5c34071276ce9220e234217682e7d2c4a5/lxml-6.0.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9261bb77c2dab42f3ecd9103951aeca2c40277701eb7e912c545c1b16e0e4917", size = 5097185, upload-time = "2025-09-22T04:02:50.746Z" }, + { url = "https://files.pythonhosted.org/packages/21/04/a60b0ff9314736316f28316b694bccbbabe100f8483ad83852d77fc7468e/lxml-6.0.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:65ac4a01aba353cfa6d5725b95d7aed6356ddc0a3cd734de00124d285b04b64f", size = 4745895, upload-time = "2025-09-22T04:02:52.968Z" }, + { url = "https://files.pythonhosted.org/packages/d6/bd/7d54bd1846e5a310d9c715921c5faa71cf5c0853372adf78aee70c8d7aa2/lxml-6.0.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:b22a07cbb82fea98f8a2fd814f3d1811ff9ed76d0fc6abc84eb21527596e7cc8", size = 5695246, upload-time = "2025-09-22T04:02:54.798Z" }, + { url = "https://files.pythonhosted.org/packages/fd/32/5643d6ab947bc371da21323acb2a6e603cedbe71cb4c99c8254289ab6f4e/lxml-6.0.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:d759cdd7f3e055d6bc8d9bec3ad905227b2e4c785dc16c372eb5b5e83123f48a", size = 5260797, upload-time = "2025-09-22T04:02:57.058Z" }, + { url = "https://files.pythonhosted.org/packages/33/da/34c1ec4cff1eea7d0b4cd44af8411806ed943141804ac9c5d565302afb78/lxml-6.0.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:945da35a48d193d27c188037a05fec5492937f66fb1958c24fc761fb9d40d43c", size = 5277404, upload-time = "2025-09-22T04:02:58.966Z" }, + { url = "https://files.pythonhosted.org/packages/82/57/4eca3e31e54dc89e2c3507e1cd411074a17565fa5ffc437c4ae0a00d439e/lxml-6.0.2-cp314-cp314-win32.whl", hash = "sha256:be3aaa60da67e6153eb15715cc2e19091af5dc75faef8b8a585aea372507384b", size = 3670072, upload-time = "2025-09-22T04:03:38.05Z" }, + { url = "https://files.pythonhosted.org/packages/e3/e0/c96cf13eccd20c9421ba910304dae0f619724dcf1702864fd59dd386404d/lxml-6.0.2-cp314-cp314-win_amd64.whl", hash = "sha256:fa25afbadead523f7001caf0c2382afd272c315a033a7b06336da2637d92d6ed", size = 4080617, upload-time = "2025-09-22T04:03:39.835Z" }, + { url = "https://files.pythonhosted.org/packages/d5/5d/b3f03e22b3d38d6f188ef044900a9b29b2fe0aebb94625ce9fe244011d34/lxml-6.0.2-cp314-cp314-win_arm64.whl", hash = "sha256:063eccf89df5b24e361b123e257e437f9e9878f425ee9aae3144c77faf6da6d8", size = 3754930, upload-time = "2025-09-22T04:03:41.565Z" }, + { url = "https://files.pythonhosted.org/packages/5e/5c/42c2c4c03554580708fc738d13414801f340c04c3eff90d8d2d227145275/lxml-6.0.2-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:6162a86d86893d63084faaf4ff937b3daea233e3682fb4474db07395794fa80d", size = 8910380, upload-time = "2025-09-22T04:03:01.645Z" }, + { url = "https://files.pythonhosted.org/packages/bf/4f/12df843e3e10d18d468a7557058f8d3733e8b6e12401f30b1ef29360740f/lxml-6.0.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:414aaa94e974e23a3e92e7ca5b97d10c0cf37b6481f50911032c69eeb3991bba", size = 4775632, upload-time = "2025-09-22T04:03:03.814Z" }, + { url = "https://files.pythonhosted.org/packages/e4/0c/9dc31e6c2d0d418483cbcb469d1f5a582a1cd00a1f4081953d44051f3c50/lxml-6.0.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:48461bd21625458dd01e14e2c38dd0aea69addc3c4f960c30d9f59d7f93be601", size = 4975171, upload-time = "2025-09-22T04:03:05.651Z" }, + { url = "https://files.pythonhosted.org/packages/e7/2b/9b870c6ca24c841bdd887504808f0417aa9d8d564114689266f19ddf29c8/lxml-6.0.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:25fcc59afc57d527cfc78a58f40ab4c9b8fd096a9a3f964d2781ffb6eb33f4ed", size = 5110109, upload-time = "2025-09-22T04:03:07.452Z" }, + { url = "https://files.pythonhosted.org/packages/bf/0c/4f5f2a4dd319a178912751564471355d9019e220c20d7db3fb8307ed8582/lxml-6.0.2-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5179c60288204e6ddde3f774a93350177e08876eaf3ab78aa3a3649d43eb7d37", size = 5041061, upload-time = "2025-09-22T04:03:09.297Z" }, + { url = "https://files.pythonhosted.org/packages/12/64/554eed290365267671fe001a20d72d14f468ae4e6acef1e179b039436967/lxml-6.0.2-cp314-cp314t-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:967aab75434de148ec80597b75062d8123cadf2943fb4281f385141e18b21338", size = 5306233, upload-time = "2025-09-22T04:03:11.651Z" }, + { url = "https://files.pythonhosted.org/packages/7a/31/1d748aa275e71802ad9722df32a7a35034246b42c0ecdd8235412c3396ef/lxml-6.0.2-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d100fcc8930d697c6561156c6810ab4a508fb264c8b6779e6e61e2ed5e7558f9", size = 5604739, upload-time = "2025-09-22T04:03:13.592Z" }, + { url = "https://files.pythonhosted.org/packages/8f/41/2c11916bcac09ed561adccacceaedd2bf0e0b25b297ea92aab99fd03d0fa/lxml-6.0.2-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ca59e7e13e5981175b8b3e4ab84d7da57993eeff53c07764dcebda0d0e64ecd", size = 5225119, upload-time = "2025-09-22T04:03:15.408Z" }, + { url = "https://files.pythonhosted.org/packages/99/05/4e5c2873d8f17aa018e6afde417c80cc5d0c33be4854cce3ef5670c49367/lxml-6.0.2-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:957448ac63a42e2e49531b9d6c0fa449a1970dbc32467aaad46f11545be9af1d", size = 4633665, upload-time = "2025-09-22T04:03:17.262Z" }, + { url = "https://files.pythonhosted.org/packages/0f/c9/dcc2da1bebd6275cdc723b515f93edf548b82f36a5458cca3578bc899332/lxml-6.0.2-cp314-cp314t-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b7fc49c37f1786284b12af63152fe1d0990722497e2d5817acfe7a877522f9a9", size = 5234997, upload-time = "2025-09-22T04:03:19.14Z" }, + { url = "https://files.pythonhosted.org/packages/9c/e2/5172e4e7468afca64a37b81dba152fc5d90e30f9c83c7c3213d6a02a5ce4/lxml-6.0.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e19e0643cc936a22e837f79d01a550678da8377d7d801a14487c10c34ee49c7e", size = 5090957, upload-time = "2025-09-22T04:03:21.436Z" }, + { url = "https://files.pythonhosted.org/packages/a5/b3/15461fd3e5cd4ddcb7938b87fc20b14ab113b92312fc97afe65cd7c85de1/lxml-6.0.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:1db01e5cf14345628e0cbe71067204db658e2fb8e51e7f33631f5f4735fefd8d", size = 4764372, upload-time = "2025-09-22T04:03:23.27Z" }, + { url = "https://files.pythonhosted.org/packages/05/33/f310b987c8bf9e61c4dd8e8035c416bd3230098f5e3cfa69fc4232de7059/lxml-6.0.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:875c6b5ab39ad5291588aed6925fac99d0097af0dd62f33c7b43736043d4a2ec", size = 5634653, upload-time = "2025-09-22T04:03:25.767Z" }, + { url = "https://files.pythonhosted.org/packages/70/ff/51c80e75e0bc9382158133bdcf4e339b5886c6ee2418b5199b3f1a61ed6d/lxml-6.0.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:cdcbed9ad19da81c480dfd6dd161886db6096083c9938ead313d94b30aadf272", size = 5233795, upload-time = "2025-09-22T04:03:27.62Z" }, + { url = "https://files.pythonhosted.org/packages/56/4d/4856e897df0d588789dd844dbed9d91782c4ef0b327f96ce53c807e13128/lxml-6.0.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:80dadc234ebc532e09be1975ff538d154a7fa61ea5031c03d25178855544728f", size = 5257023, upload-time = "2025-09-22T04:03:30.056Z" }, + { url = "https://files.pythonhosted.org/packages/0f/85/86766dfebfa87bea0ab78e9ff7a4b4b45225df4b4d3b8cc3c03c5cd68464/lxml-6.0.2-cp314-cp314t-win32.whl", hash = "sha256:da08e7bb297b04e893d91087df19638dc7a6bb858a954b0cc2b9f5053c922312", size = 3911420, upload-time = "2025-09-22T04:03:32.198Z" }, + { url = "https://files.pythonhosted.org/packages/fe/1a/b248b355834c8e32614650b8008c69ffeb0ceb149c793961dd8c0b991bb3/lxml-6.0.2-cp314-cp314t-win_amd64.whl", hash = "sha256:252a22982dca42f6155125ac76d3432e548a7625d56f5a273ee78a5057216eca", size = 4406837, upload-time = "2025-09-22T04:03:34.027Z" }, + { url = "https://files.pythonhosted.org/packages/92/aa/df863bcc39c5e0946263454aba394de8a9084dbaff8ad143846b0d844739/lxml-6.0.2-cp314-cp314t-win_arm64.whl", hash = "sha256:bb4c1847b303835d89d785a18801a883436cdfd5dc3d62947f9c49e24f0f5a2c", size = 3822205, upload-time = "2025-09-22T04:03:36.249Z" }, + { url = "https://files.pythonhosted.org/packages/0b/11/29d08bc103a62c0eba8016e7ed5aeebbf1e4312e83b0b1648dd203b0e87d/lxml-6.0.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1c06035eafa8404b5cf475bb37a9f6088b0aca288d4ccc9d69389750d5543700", size = 3949829, upload-time = "2025-09-22T04:04:45.608Z" }, + { url = "https://files.pythonhosted.org/packages/12/b3/52ab9a3b31e5ab8238da241baa19eec44d2ab426532441ee607165aebb52/lxml-6.0.2-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c7d13103045de1bdd6fe5d61802565f1a3537d70cd3abf596aa0af62761921ee", size = 4226277, upload-time = "2025-09-22T04:04:47.754Z" }, + { url = "https://files.pythonhosted.org/packages/a0/33/1eaf780c1baad88224611df13b1c2a9dfa460b526cacfe769103ff50d845/lxml-6.0.2-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0a3c150a95fbe5ac91de323aa756219ef9cf7fde5a3f00e2281e30f33fa5fa4f", size = 4330433, upload-time = "2025-09-22T04:04:49.907Z" }, + { url = "https://files.pythonhosted.org/packages/7a/c1/27428a2ff348e994ab4f8777d3a0ad510b6b92d37718e5887d2da99952a2/lxml-6.0.2-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60fa43be34f78bebb27812ed90f1925ec99560b0fa1decdb7d12b84d857d31e9", size = 4272119, upload-time = "2025-09-22T04:04:51.801Z" }, + { url = "https://files.pythonhosted.org/packages/f0/d0/3020fa12bcec4ab62f97aab026d57c2f0cfd480a558758d9ca233bb6a79d/lxml-6.0.2-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:21c73b476d3cfe836be731225ec3421fa2f048d84f6df6a8e70433dff1376d5a", size = 4417314, upload-time = "2025-09-22T04:04:55.024Z" }, + { url = "https://files.pythonhosted.org/packages/6c/77/d7f491cbc05303ac6801651aabeb262d43f319288c1ea96c66b1d2692ff3/lxml-6.0.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:27220da5be049e936c3aca06f174e8827ca6445a4353a1995584311487fc4e3e", size = 3518768, upload-time = "2025-09-22T04:04:57.097Z" }, +] + +[[package]] +name = "mail-parser" +version = "4.6.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/0f/b6fc717f7a7d9188e451e9d30e98b3f61866313958f866f9da7ff5ed3c84/mail_parser-4.6.4.tar.gz", hash = "sha256:1929109b7934ee061c1533a897b18c57fd0adb3548b79b5e28d901819bade872", size = 2898988, upload-time = "2026-08-21T21:47:13.58Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/f9/b35a02c12346111e997ebfaa46ebeb759828373a3eb9f0e82c1e826b3530/mail_parser-4.6.4-py3-none-any.whl", hash = "sha256:bc6e437b3afe38091893e7b6ea49c7f2188ad7616abdd4fa3059e85f1b69efce", size = 50073, upload-time = "2026-08-21T21:47:12.07Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, +] + +[[package]] +name = "marko" +version = "2.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/2f/050b6d485f052ddf17d76a41f9334d6fb2a8a85df35347a12d97ed3bc5c1/marko-2.2.2.tar.gz", hash = "sha256:6940308e655f63733ca518c47a68ec9510279dbb916c83616e4c4b5829f052e8", size = 143641, upload-time = "2026-01-05T11:04:41.935Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/f8/36d79bac5701e6786f9880c61bbe57574760a13c1af84ab71e5ed21faecc/marko-2.2.2-py3-none-any.whl", hash = "sha256:f064ae8c10416285ad1d96048dc11e98ef04e662d3342ae416f662b70aa7959e", size = 42701, upload-time = "2026-01-05T11:04:40.75Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, + { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, + { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, + { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, + { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, + { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, + { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, + { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" }, + { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" }, + { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" }, + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + +[[package]] +name = "mccabe" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/ff/0ffefdcac38932a54d2b5eed4e0ba8a408f215002cd178ad1df0f2806ff8/mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", size = 9658, upload-time = "2022-01-24T01:14:51.113Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/1a/1f68f9ba0c207934b35b86a8ca3aad8395a3d6dd7921c0686e23853ff5a9/mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e", size = 7350, upload-time = "2022-01-24T01:14:49.62Z" }, +] + +[[package]] +name = "mcp" +version = "1.29.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "httpx" }, + { name = "httpx-sse" }, + { name = "jsonschema" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "pyjwt", extra = ["crypto"] }, + { name = "python-multipart" }, + { name = "pywin32", marker = "sys_platform == 'win32'" }, + { name = "sse-starlette" }, + { name = "starlette" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, + { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/30/d3/f9acc21dfc886e4f78e2add1a47db46ce16884346afde53f8a064c02c891/mcp-1.29.0.tar.gz", hash = "sha256:52d01f334de1868cc3bb2d6604931126a67631f99a6c5d3b82ba47290315ec36", size = 643148, upload-time = "2026-07-28T13:41:41.939Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/01/c8/248b201f6d753d69fd5d6506011abbb35a946d9142b2ae311a948fd0be3d/mcp-1.29.0-py3-none-any.whl", hash = "sha256:f5a075bb611f23d6f4d080c6a1699fa62772eebc562ba9e66b306ddde1c755f7", size = 223436, upload-time = "2026-07-28T13:41:40.337Z" }, +] + +[[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, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "mpire" +version = "2.10.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pygments" }, + { name = "pywin32", marker = "sys_platform == 'win32'" }, + { name = "tqdm" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3a/93/80ac75c20ce54c785648b4ed363c88f148bf22637e10c9863db4fbe73e74/mpire-2.10.2.tar.gz", hash = "sha256:f66a321e93fadff34585a4bfa05e95bd946cf714b442f51c529038eb45773d97", size = 271270, upload-time = "2024-05-07T14:00:31.815Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/14/1db1729ad6db4999c3a16c47937d601fcb909aaa4224f5eca5a2f145a605/mpire-2.10.2-py3-none-any.whl", hash = "sha256:d627707f7a8d02aa4c7f7d59de399dec5290945ddf7fbd36cbb1d6ebb37a51fb", size = 272756, upload-time = "2024-05-07T14:00:29.633Z" }, +] + +[package.optional-dependencies] +dill = [ + { name = "multiprocess" }, +] + +[[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, upload-time = "2023-03-07T16:47:11.061Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198, upload-time = "2023-03-07T16:47:09.197Z" }, +] + +[[package]] +name = "multiprocess" +version = "0.70.19" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dill" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a2/f2/e783ac7f2aeeed14e9e12801f22529cc7e6b7ab80928d6dcce4e9f00922d/multiprocess-0.70.19.tar.gz", hash = "sha256:952021e0e6c55a4a9fe4cd787895b86e239a40e76802a789d6305398d3975897", size = 2079989, upload-time = "2026-01-19T06:47:39.744Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/aa/714635c727dbfc251139226fa4eaf1b07f00dc12d9cd2eb25f931adaf873/multiprocess-0.70.19-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1bbf1b69af1cf64cd05f65337d9215b88079ec819cd0ea7bac4dab84e162efe7", size = 144743, upload-time = "2026-01-19T06:47:24.562Z" }, + { url = "https://files.pythonhosted.org/packages/0f/e1/155f6abf5e6b5d9cef29b6d0167c180846157a4aca9b9bee1a217f67c959/multiprocess-0.70.19-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:5be9ec7f0c1c49a4f4a6fd20d5dda4aeabc2d39a50f4ad53720f1cd02b3a7c2e", size = 144738, upload-time = "2026-01-19T06:47:26.636Z" }, + { url = "https://files.pythonhosted.org/packages/af/cb/f421c2869d75750a4f32301cc20c4b63fab6376e9a75c8e5e655bdeb3d9b/multiprocess-0.70.19-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:1c3dce098845a0db43b32a0b76a228ca059a668071cfeaa0f40c36c0b1585d45", size = 144741, upload-time = "2026-01-19T06:47:27.985Z" }, + { url = "https://files.pythonhosted.org/packages/e3/45/8004d1e6b9185c1a444d6b55ac5682acf9d98035e54386d967366035a03a/multiprocess-0.70.19-py310-none-any.whl", hash = "sha256:97404393419dcb2a8385910864eedf47a3cadf82c66345b44f036420eb0b5d87", size = 134948, upload-time = "2026-01-19T06:47:32.325Z" }, + { url = "https://files.pythonhosted.org/packages/86/c2/dec9722dc3474c164a0b6bcd9a7ed7da542c98af8cabce05374abab35edd/multiprocess-0.70.19-py311-none-any.whl", hash = "sha256:928851ae7973aea4ce0eaf330bbdafb2e01398a91518d5c8818802845564f45c", size = 144457, upload-time = "2026-01-19T06:47:33.711Z" }, + { url = "https://files.pythonhosted.org/packages/71/70/38998b950a97ea279e6bd657575d22d1a2047256caf707d9a10fbce4f065/multiprocess-0.70.19-py312-none-any.whl", hash = "sha256:3a56c0e85dd5025161bac5ce138dcac1e49174c7d8e74596537e729fd5c53c28", size = 150281, upload-time = "2026-01-19T06:47:35.037Z" }, + { url = "https://files.pythonhosted.org/packages/7f/74/d2c27e03cb84251dfe7249b8e82923643c6d48fa4883b9476b025e7dc7eb/multiprocess-0.70.19-py313-none-any.whl", hash = "sha256:8d5eb4ec5017ba2fab4e34a747c6d2c2b6fecfe9e7236e77988db91580ada952", size = 156414, upload-time = "2026-01-19T06:47:35.915Z" }, + { url = "https://files.pythonhosted.org/packages/a0/61/af9115673a5870fd885247e2f1b68c4f1197737da315b520a91c757a861a/multiprocess-0.70.19-py314-none-any.whl", hash = "sha256:e8cc7fbdff15c0613f0a1f1f8744bef961b0a164c0ca29bdff53e9d2d93c5e5f", size = 160318, upload-time = "2026-01-19T06:47:37.497Z" }, + { url = "https://files.pythonhosted.org/packages/7e/82/69e539c4c2027f1e1697e09aaa2449243085a0edf81ae2c6341e84d769b6/multiprocess-0.70.19-py39-none-any.whl", hash = "sha256:0d4b4397ed669d371c81dcd1ef33fd384a44d6c3de1bd0ca7ac06d837720d3c5", size = 133477, upload-time = "2026-01-19T06:47:38.619Z" }, +] + +[[package]] +name = "murmurhash" +version = "1.0.15" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/23/2e/88c147931ea9725d634840d538622e94122bceaf346233349b7b5c62964b/murmurhash-1.0.15.tar.gz", hash = "sha256:58e2b27b7847f9e2a6edf10b47a8c8dd70a4705f45dccb7bf76aeadacf56ba01", size = 13291, upload-time = "2025-11-14T09:51:15.272Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6b/ca/77d3e69924a8eb4508bb4f0ad34e46adbeedeb93616a71080e61e53dad71/murmurhash-1.0.15-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f32307fb9347680bb4fe1cbef6362fb39bd994f1b59abd8c09ca174e44199081", size = 27397, upload-time = "2025-11-14T09:50:03.077Z" }, + { url = "https://files.pythonhosted.org/packages/e6/53/a936f577d35b245d47b310f29e5e9f09fcac776c8c992f1ab51a9fb0cee2/murmurhash-1.0.15-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:539d8405885d1d19c005f3a2313b47e8e54b0ee89915eb8dfbb430b194328e6c", size = 27692, upload-time = "2025-11-14T09:50:04.144Z" }, + { url = "https://files.pythonhosted.org/packages/4d/64/5f8cfd1fd9cbeb43fcff96672f5bd9e7e1598d1c970f808ecd915490dc20/murmurhash-1.0.15-cp311-cp311-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c4cd739a00f5a4602201b74568ddabae46ec304719d9be752fd8f534a9464b5e", size = 128396, upload-time = "2025-11-14T09:50:05.268Z" }, + { url = "https://files.pythonhosted.org/packages/ac/10/d9ce29d559a75db0d8a3f13ea12c7f541ec9de2afca38dc70418b890eedb/murmurhash-1.0.15-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:44d211bcc3ec203c47dac06f48ee871093fcbdffa6652a6cc5ea7180306680a8", size = 128687, upload-time = "2025-11-14T09:50:06.527Z" }, + { url = "https://files.pythonhosted.org/packages/48/cd/dc97ab7e68cdfa1537a56e36dbc846c5a66701cc39ecee2d4399fe61996c/murmurhash-1.0.15-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f9bf47101354fb1dc4b2e313192566f04ba295c28a37e2f71c692759acc1ba3c", size = 128198, upload-time = "2025-11-14T09:50:08.062Z" }, + { url = "https://files.pythonhosted.org/packages/53/73/32f2aaa22c1e4afae337106baf0c938abf36a6cc879cfee83a00461bbbf7/murmurhash-1.0.15-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3c69b4d3bcd6233782a78907fe10b9b7a796bdc5d28060cf097d067bec280a5d", size = 127214, upload-time = "2025-11-14T09:50:09.265Z" }, + { url = "https://files.pythonhosted.org/packages/82/ed/812103a7f353eba2d83655b08205e13a38c93b4db0692f94756e1eb44516/murmurhash-1.0.15-cp311-cp311-win_amd64.whl", hash = "sha256:e43a69496342ce530bdd670264cb7c8f45490b296e4764c837ce577e3c7ebd53", size = 25241, upload-time = "2025-11-14T09:50:10.373Z" }, + { url = "https://files.pythonhosted.org/packages/eb/5f/2c511bdd28f7c24da37a00116ffd0432b65669d098f0d0260c66ac0ffdc2/murmurhash-1.0.15-cp311-cp311-win_arm64.whl", hash = "sha256:f3e99a6ee36ef5372df5f138e3d9c801420776d3641a34a49e5c2555f44edba7", size = 23216, upload-time = "2025-11-14T09:50:11.651Z" }, + { url = "https://files.pythonhosted.org/packages/b6/46/be8522d3456fdccf1b8b049c6d82e7a3c1114c4fc2cfe14b04cba4b3e701/murmurhash-1.0.15-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d37e3ae44746bca80b1a917c2ea625cf216913564ed43f69d2888e5df97db0cb", size = 27884, upload-time = "2025-11-14T09:50:13.133Z" }, + { url = "https://files.pythonhosted.org/packages/ed/cc/630449bf4f6178d7daf948ce46ad00b25d279065fc30abd8d706be3d87e0/murmurhash-1.0.15-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0861cb11039409eaf46878456b7d985ef17b6b484103a6fc367b2ecec846891d", size = 27855, upload-time = "2025-11-14T09:50:14.859Z" }, + { url = "https://files.pythonhosted.org/packages/ff/30/ea8f601a9bf44db99468696efd59eb9cff1157cd55cb586d67116697583f/murmurhash-1.0.15-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5a301decfaccfec70fe55cb01dde2a012c3014a874542eaa7cc73477bb749616", size = 134088, upload-time = "2025-11-14T09:50:15.958Z" }, + { url = "https://files.pythonhosted.org/packages/c9/de/c40ce8c0877d406691e735b8d6e9c815f36a82b499d358313db5dbe219d7/murmurhash-1.0.15-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:32c6fde7bd7e9407003370a07b5f4addacabe1556ad3dc2cac246b7a2bba3400", size = 133978, upload-time = "2025-11-14T09:50:17.572Z" }, + { url = "https://files.pythonhosted.org/packages/47/84/bd49963ecd84ebab2fe66595e2d1ed41d5e8b5153af5dc930f0bd827007c/murmurhash-1.0.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5d8b43a7011540dc3c7ce66f2134df9732e2bc3bbb4a35f6458bc755e48bde26", size = 132956, upload-time = "2025-11-14T09:50:18.742Z" }, + { url = "https://files.pythonhosted.org/packages/4f/7c/2530769c545074417c862583f05f4245644599f1e9ff619b3dfe2969aafc/murmurhash-1.0.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:43bf4541892ecd95963fcd307bf1c575fc0fee1682f41c93007adee71ca2bb40", size = 134184, upload-time = "2025-11-14T09:50:19.941Z" }, + { url = "https://files.pythonhosted.org/packages/84/a4/b249b042f5afe34d14ada2dc4afc777e883c15863296756179652e081c44/murmurhash-1.0.15-cp312-cp312-win_amd64.whl", hash = "sha256:f4ac15a2089dc42e6eb0966622d42d2521590a12c92480aafecf34c085302cca", size = 25647, upload-time = "2025-11-14T09:50:21.049Z" }, + { url = "https://files.pythonhosted.org/packages/13/bf/028179259aebc18fd4ba5cae2601d1d47517427a537ab44336446431a215/murmurhash-1.0.15-cp312-cp312-win_arm64.whl", hash = "sha256:4a70ca4ae19e600d9be3da64d00710e79dde388a4d162f22078d64844d0ebdda", size = 23338, upload-time = "2025-11-14T09:50:22.359Z" }, + { url = "https://files.pythonhosted.org/packages/29/2f/ba300b5f04dae0409202d6285668b8a9d3ade43a846abee3ef611cb388d5/murmurhash-1.0.15-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fe50dc70e52786759358fd1471e309b94dddfffb9320d9dfea233c7684c894ba", size = 27861, upload-time = "2025-11-14T09:50:23.804Z" }, + { url = "https://files.pythonhosted.org/packages/34/02/29c19d268e6f4ea1ed2a462c901eed1ed35b454e2cbc57da592fad663ac6/murmurhash-1.0.15-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1349a7c23f6092e7998ddc5bd28546cc31a595afc61e9fdb3afc423feec3d7ad", size = 27840, upload-time = "2025-11-14T09:50:25.146Z" }, + { url = "https://files.pythonhosted.org/packages/e2/63/58e2de2b5232cd294c64092688c422196e74f9fa8b3958bdf02d33df24b9/murmurhash-1.0.15-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b3ba6d05de2613535b5a9227d4ad8ef40a540465f64660d4a8800634ae10e04f", size = 133080, upload-time = "2025-11-14T09:50:26.566Z" }, + { url = "https://files.pythonhosted.org/packages/aa/9a/d13e2e9f8ba1ced06840921a50f7cece0a475453284158a3018b72679761/murmurhash-1.0.15-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fa1b70b3cc2801ab44179c65827bbd12009c68b34e9d9ce7125b6a0bd35af63c", size = 132648, upload-time = "2025-11-14T09:50:27.788Z" }, + { url = "https://files.pythonhosted.org/packages/b2/e1/47994f1813fa205c84977b0ff51ae6709f8539af052c7491a5f863d82bdc/murmurhash-1.0.15-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:213d710fb6f4ef3bc11abbfad0fa94a75ffb675b7dc158c123471e5de869f9af", size = 131502, upload-time = "2025-11-14T09:50:29.339Z" }, + { url = "https://files.pythonhosted.org/packages/b9/ea/90c1fd00b4aeb704fb5e84cd666b33ffd7f245155048071ffbb51d2bb57d/murmurhash-1.0.15-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b65a5c4e7f5d71f7ccac2d2b60bdf7092d7976270878cfec59d5a66a533db823", size = 132736, upload-time = "2025-11-14T09:50:30.545Z" }, + { url = "https://files.pythonhosted.org/packages/00/db/da73462dbfa77f6433b128d2120ba7ba300f8c06dc4f4e022c38d240a5f5/murmurhash-1.0.15-cp313-cp313-win_amd64.whl", hash = "sha256:9aba94c5d841e1904cd110e94ceb7f49cfb60a874bbfb27e0373622998fb7c7c", size = 25682, upload-time = "2025-11-14T09:50:31.624Z" }, + { url = "https://files.pythonhosted.org/packages/bb/83/032729ef14971b938fbef41ee125fc8800020ee229bd35178b6ede8ee934/murmurhash-1.0.15-cp313-cp313-win_arm64.whl", hash = "sha256:263807eca40d08c7b702413e45cca75ecb5883aa337237dc5addb660f1483378", size = 23370, upload-time = "2025-11-14T09:50:33.264Z" }, + { url = "https://files.pythonhosted.org/packages/10/83/7547d9205e9bd2f8e5dfd0b682cc9277594f98909f228eb359489baec1df/murmurhash-1.0.15-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:694fd42a74b7ce257169d14c24aa616aa6cd4ccf8abe50eca0557e08da99d055", size = 29955, upload-time = "2025-11-14T09:50:34.488Z" }, + { url = "https://files.pythonhosted.org/packages/b7/c7/3afd5de7a5b3ae07fe2d3a3271b327ee1489c58ba2b2f2159bd31a25edb9/murmurhash-1.0.15-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a2ea4546ba426390beff3cd10db8f0152fdc9072c4f2583ec7d8aa9f3e4ac070", size = 30108, upload-time = "2025-11-14T09:50:35.53Z" }, + { url = "https://files.pythonhosted.org/packages/02/69/d6637ee67d78ebb2538c00411f28ea5c154886bbe1db16c49435a8a4ab16/murmurhash-1.0.15-cp313-cp313t-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:34e5a91139c40b10f98d0b297907f5d5267b4b1b2e5dd2eb74a021824f751b98", size = 164054, upload-time = "2025-11-14T09:50:36.591Z" }, + { url = "https://files.pythonhosted.org/packages/ab/4c/89e590165b4c7da6bf941441212a721a270195332d3aacfdfdf527d466ca/murmurhash-1.0.15-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:dc35606868a5961cf42e79314ca0bddf5a400ce377b14d83192057928d6252ec", size = 168153, upload-time = "2025-11-14T09:50:37.856Z" }, + { url = "https://files.pythonhosted.org/packages/07/7a/95c42df0c21d2e413b9fcd17317a7587351daeb264dc29c6aec1fdbd26f8/murmurhash-1.0.15-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:43cc6ac3b91ca0f7a5ae9c063ba4d6c26972c97fd7c25280ecc666413e4c5535", size = 164345, upload-time = "2025-11-14T09:50:39.346Z" }, + { url = "https://files.pythonhosted.org/packages/d0/22/9d02c880a88b83bb3ce7d6a38fb727373ab78d82e5f3d8d9fc5612219f90/murmurhash-1.0.15-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:847d712136cb462f0e4bd6229ee2d9eb996d8854eb8312dff3d20c8f5181fda5", size = 161990, upload-time = "2025-11-14T09:50:40.689Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e3/750232524e0dc262e8dcede6536dafc766faadd9a52f1d23746b02948ad8/murmurhash-1.0.15-cp313-cp313t-win_amd64.whl", hash = "sha256:2680851af6901dbe66cc4aa7ef8e263de47e6e1b425ae324caa571bdf18f8d58", size = 28812, upload-time = "2025-11-14T09:50:41.971Z" }, + { url = "https://files.pythonhosted.org/packages/ff/89/4ad9d215ef6ade89f27a72dc4e86b98ef1a43534cc3e6a6900a362a0bf0a/murmurhash-1.0.15-cp313-cp313t-win_arm64.whl", hash = "sha256:189a8de4d657b5da9efd66601b0636330b08262b3a55431f2379097c986995d0", size = 25398, upload-time = "2025-11-14T09:50:43.023Z" }, + { url = "https://files.pythonhosted.org/packages/1c/69/726df275edf07688146966e15eaaa23168100b933a2e1a29b37eb56c6db8/murmurhash-1.0.15-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:7c4280136b738e85ff76b4bdc4341d0b867ee753e73fd8b6994288080c040d0b", size = 28029, upload-time = "2025-11-14T09:50:44.124Z" }, + { url = "https://files.pythonhosted.org/packages/59/8f/24ecf9061bc2b20933df8aba47c73e904274ea8811c8300cab92f6f82372/murmurhash-1.0.15-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d4d681f474830489e2ec1d912095cfff027fbaf2baa5414c7e9d25b89f0fab68", size = 27912, upload-time = "2025-11-14T09:50:45.266Z" }, + { url = "https://files.pythonhosted.org/packages/ba/26/fff3caba25aa3c0622114e03c69fb66c839b22335b04d7cce91a3a126d44/murmurhash-1.0.15-cp314-cp314-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d7e47c5746785db6a43b65fac47b9e63dd71dfbd89a8c92693425b9715e68c6e", size = 131847, upload-time = "2025-11-14T09:50:46.819Z" }, + { url = "https://files.pythonhosted.org/packages/df/e4/0f2b9fc533467a27afb4e906c33f32d5f637477de87dd94690e0c44335a6/murmurhash-1.0.15-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e8e674f02a99828c8a671ba99cd03299381b2f0744e6f25c29cadfc6151dc724", size = 132267, upload-time = "2025-11-14T09:50:48.298Z" }, + { url = "https://files.pythonhosted.org/packages/da/bf/9d1c107989728ec46e25773d503aa54070b32822a18cfa7f9d5f41bc17a5/murmurhash-1.0.15-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:26fd7c7855ac4850ad8737991d7b0e3e501df93ebaf0cf45aa5954303085fdba", size = 131894, upload-time = "2025-11-14T09:50:49.485Z" }, + { url = "https://files.pythonhosted.org/packages/0d/81/dcf27c71445c0e993b10e33169a098ca60ee702c5c58fcbde205fa6332a6/murmurhash-1.0.15-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:cb8ebafae60d5f892acff533cc599a359954d8c016a829514cb3f6e9ee10f322", size = 132054, upload-time = "2025-11-14T09:50:50.747Z" }, + { url = "https://files.pythonhosted.org/packages/bc/32/e874a14b2d2246bd2d16f80f49fad393a3865d4ee7d66d2cae939a67a29a/murmurhash-1.0.15-cp314-cp314-win_amd64.whl", hash = "sha256:898a629bf111f1aeba4437e533b5b836c0a9d2dd12d6880a9c75f6ca13e30e22", size = 26579, upload-time = "2025-11-14T09:50:52.278Z" }, + { url = "https://files.pythonhosted.org/packages/af/8e/4fca051ed8ae4d23a15aaf0a82b18cb368e8cf84f1e3b474d5749ec46069/murmurhash-1.0.15-cp314-cp314-win_arm64.whl", hash = "sha256:88dc1dd53b7b37c0df1b8b6bce190c12763014492f0269ff7620dc6027f470f4", size = 24341, upload-time = "2025-11-14T09:50:53.295Z" }, + { url = "https://files.pythonhosted.org/packages/38/9c/c72c2a4edd86aac829337ab9f83cf04cdb15e5d503e4c9a3a243f30a261c/murmurhash-1.0.15-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:6cb4e962ec4f928b30c271b2d84e6707eff6d942552765b663743cfa618b294b", size = 30146, upload-time = "2025-11-14T09:50:54.705Z" }, + { url = "https://files.pythonhosted.org/packages/ac/d7/72b47ebc86436cd0aa1fd4c6e8779521ec389397ac11389990278d0f7a47/murmurhash-1.0.15-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5678a3ea4fbf0cbaaca2bed9b445f556f294d5f799c67185d05ffcb221a77faf", size = 30141, upload-time = "2025-11-14T09:50:55.829Z" }, + { url = "https://files.pythonhosted.org/packages/64/bb/6d2f09135079c34dc2d26e961c52742d558b320c61503f273eab6ba743d9/murmurhash-1.0.15-cp314-cp314t-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ef19f38c6b858eef83caf710773db98c8f7eb2193b4c324650c74f3d8ba299e0", size = 163898, upload-time = "2025-11-14T09:50:56.946Z" }, + { url = "https://files.pythonhosted.org/packages/b9/e2/9c1b462e33f9cb2d632056f07c90b502fc20bd7da50a15d0557343bd2fed/murmurhash-1.0.15-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22aa3ceaedd2e57078b491ed08852d512b84ff4ff9bb2ff3f9bf0eec7f214c9e", size = 168040, upload-time = "2025-11-14T09:50:58.234Z" }, + { url = "https://files.pythonhosted.org/packages/e8/73/8694db1408fcdfa73589f7df6c445437ea146986fa1e393ec60d26d6e30c/murmurhash-1.0.15-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bba0e0262c0d08682b028cb963ac477bd9839029486fa1333fc5c01fb6072749", size = 164239, upload-time = "2025-11-14T09:50:59.95Z" }, + { url = "https://files.pythonhosted.org/packages/2d/f9/8e360bdfc3c44e267e7e046f0e0b9922766da92da26959a6963f597e6bb5/murmurhash-1.0.15-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:4fd8189ee293a09f30f4931408f40c28ccd42d9de4f66595f8814879339378bc", size = 161811, upload-time = "2025-11-14T09:51:01.289Z" }, + { url = "https://files.pythonhosted.org/packages/f9/31/97649680595b1096803d877ababb9a67c07f4378f177ec885eea28b9db6d/murmurhash-1.0.15-cp314-cp314t-win_amd64.whl", hash = "sha256:66395b1388f7daa5103db92debe06842ae3be4c0749ef6db68b444518666cdcc", size = 29817, upload-time = "2025-11-14T09:51:02.493Z" }, + { url = "https://files.pythonhosted.org/packages/76/66/4fce8755f25d77324401886c00017c556be7ca3039575b94037aff905385/murmurhash-1.0.15-cp314-cp314t-win_arm64.whl", hash = "sha256:c22e56c6a0b70598a66e456de5272f76088bc623688da84ef403148a6d41851d", size = 26219, upload-time = "2025-11-14T09:51:03.563Z" }, +] + +[[package]] +name = "neo4j" +version = "6.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytz" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1b/01/d6ce65e4647f6cb2b9cca3b813978f7329b54b4e36660aaec1ddf0ccce7a/neo4j-6.1.0.tar.gz", hash = "sha256:b5dde8c0d8481e7b6ae3733569d990dd3e5befdc5d452f531ad1884ed3500b84", size = 239629, upload-time = "2026-01-12T11:27:34.777Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/70/5c/ee71e2dd955045425ef44283f40ba1da67673cf06404916ca2950ac0cd39/neo4j-6.1.0-py3-none-any.whl", hash = "sha256:3bd93941f3a3559af197031157220af9fd71f4f93a311db687bd69ffa417b67d", size = 325326, upload-time = "2026-01-12T11:27:33.196Z" }, +] + +[[package]] +name = "networkx" +version = "3.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6a/51/63fe664f3908c97be9d2e4f1158eb633317598cfa6e1fc14af5383f17512/networkx-3.6.1.tar.gz", hash = "sha256:26b7c357accc0c8cde558ad486283728b65b6a95d85ee1cd66bafab4c8168509", size = 2517025, upload-time = "2025-12-08T17:02:39.908Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl", hash = "sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762", size = 2068504, upload-time = "2025-12-08T17:02:38.159Z" }, +] + +[[package]] +name = "numexpr" +version = "2.14.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cb/2f/fdba158c9dbe5caca9c3eca3eaffffb251f2fb8674bf8e2d0aed5f38d319/numexpr-2.14.1.tar.gz", hash = "sha256:4be00b1086c7b7a5c32e31558122b7b80243fe098579b170967da83f3152b48b", size = 119400, upload-time = "2025-10-13T16:17:27.351Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/a3/67999bdd1ed1f938d38f3fedd4969632f2f197b090e50505f7cc1fa82510/numexpr-2.14.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2d03fcb4644a12f70a14d74006f72662824da5b6128bf1bcd10cc3ed80e64c34", size = 163195, upload-time = "2025-10-13T16:16:31.212Z" }, + { url = "https://files.pythonhosted.org/packages/25/95/d64f680ea1fc56d165457287e0851d6708800f9fcea346fc1b9957942ee6/numexpr-2.14.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2773ee1133f77009a1fc2f34fe236f3d9823779f5f75450e183137d49f00499f", size = 152088, upload-time = "2025-10-13T16:16:33.186Z" }, + { url = "https://files.pythonhosted.org/packages/0e/7f/3bae417cb13ae08afd86d08bb0301c32440fe0cae4e6262b530e0819aeda/numexpr-2.14.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ebe4980f9494b9f94d10d2e526edc29e72516698d3bf95670ba79415492212a4", size = 451126, upload-time = "2025-10-13T16:13:22.248Z" }, + { url = "https://files.pythonhosted.org/packages/4c/1a/edbe839109518364ac0bd9e918cf874c755bb2c128040e920f198c494263/numexpr-2.14.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2a381e5e919a745c9503bcefffc1c7f98c972c04ec58fc8e999ed1a929e01ba6", size = 442012, upload-time = "2025-10-13T16:14:51.416Z" }, + { url = "https://files.pythonhosted.org/packages/66/b1/be4ce99bff769a5003baddac103f34681997b31d4640d5a75c0e8ed59c78/numexpr-2.14.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d08856cfc1b440eb1caaa60515235369654321995dd68eb9377577392020f6cb", size = 1415975, upload-time = "2025-10-13T16:13:26.088Z" }, + { url = "https://files.pythonhosted.org/packages/e7/33/b33b8fdc032a05d9ebb44a51bfcd4b92c178a2572cd3e6c1b03d8a4b45b2/numexpr-2.14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:03130afa04edf83a7b590d207444f05a00363c9b9ea5d81c0f53b1ea13fad55a", size = 1464683, upload-time = "2025-10-13T16:14:58.87Z" }, + { url = "https://files.pythonhosted.org/packages/d0/b2/ddcf0ac6cf0a1d605e5aecd4281507fd79a9628a67896795ab2e975de5df/numexpr-2.14.1-cp311-cp311-win32.whl", hash = "sha256:db78fa0c9fcbaded3ae7453faf060bd7a18b0dc10299d7fcd02d9362be1213ed", size = 166838, upload-time = "2025-10-13T16:17:06.765Z" }, + { url = "https://files.pythonhosted.org/packages/64/72/4ca9bd97b2eb6dce9f5e70a3b6acec1a93e1fb9b079cb4cba2cdfbbf295d/numexpr-2.14.1-cp311-cp311-win_amd64.whl", hash = "sha256:e9b2f957798c67a2428be96b04bce85439bed05efe78eb78e4c2ca43737578e7", size = 160069, upload-time = "2025-10-13T16:17:08.752Z" }, + { url = "https://files.pythonhosted.org/packages/9d/20/c473fc04a371f5e2f8c5749e04505c13e7a8ede27c09e9f099b2ad6f43d6/numexpr-2.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:91ebae0ab18c799b0e6b8c5a8d11e1fa3848eb4011271d99848b297468a39430", size = 162790, upload-time = "2025-10-13T16:16:34.903Z" }, + { url = "https://files.pythonhosted.org/packages/45/93/b6760dd1904c2a498e5f43d1bb436f59383c3ddea3815f1461dfaa259373/numexpr-2.14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:47041f2f7b9e69498fb311af672ba914a60e6e6d804011caacb17d66f639e659", size = 152196, upload-time = "2025-10-13T16:16:36.593Z" }, + { url = "https://files.pythonhosted.org/packages/72/94/cc921e35593b820521e464cbbeaf8212bbdb07f16dc79fe283168df38195/numexpr-2.14.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d686dfb2c1382d9e6e0ee0b7647f943c1886dba3adbf606c625479f35f1956c1", size = 452468, upload-time = "2025-10-13T16:13:29.531Z" }, + { url = "https://files.pythonhosted.org/packages/d9/43/560e9ba23c02c904b5934496486d061bcb14cd3ebba2e3cf0e2dccb6c22b/numexpr-2.14.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eee6d4fbbbc368e6cdd0772734d6249128d957b3b8ad47a100789009f4de7083", size = 443631, upload-time = "2025-10-13T16:15:02.473Z" }, + { url = "https://files.pythonhosted.org/packages/7b/6c/78f83b6219f61c2c22d71ab6e6c2d4e5d7381334c6c29b77204e59edb039/numexpr-2.14.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3a2839efa25f3c8d4133252ea7342d8f81226c7c4dda81f97a57e090b9d87a48", size = 1417670, upload-time = "2025-10-13T16:13:33.464Z" }, + { url = "https://files.pythonhosted.org/packages/0e/bb/1ccc9dcaf46281568ce769888bf16294c40e98a5158e4b16c241de31d0d3/numexpr-2.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9f9137f1351b310436662b5dc6f4082a245efa8950c3b0d9008028df92fefb9b", size = 1466212, upload-time = "2025-10-13T16:15:12.828Z" }, + { url = "https://files.pythonhosted.org/packages/31/9f/203d82b9e39dadd91d64bca55b3c8ca432e981b822468dcef41a4418626b/numexpr-2.14.1-cp312-cp312-win32.whl", hash = "sha256:36f8d5c1bd1355df93b43d766790f9046cccfc1e32b7c6163f75bcde682cda07", size = 166996, upload-time = "2025-10-13T16:17:10.369Z" }, + { url = "https://files.pythonhosted.org/packages/1f/67/ffe750b5452eb66de788c34e7d21ec6d886abb4d7c43ad1dc88ceb3d998f/numexpr-2.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:fdd886f4b7dbaf167633ee396478f0d0aa58ea2f9e7ccc3c6431019623e8d68f", size = 160187, upload-time = "2025-10-13T16:17:11.974Z" }, + { url = "https://files.pythonhosted.org/packages/73/b4/9f6d637fd79df42be1be29ee7ba1f050fab63b7182cb922a0e08adc12320/numexpr-2.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:09078ba73cffe94745abfbcc2d81ab8b4b4e9d7bfbbde6cac2ee5dbf38eee222", size = 162794, upload-time = "2025-10-13T16:16:38.291Z" }, + { url = "https://files.pythonhosted.org/packages/35/ae/d58558d8043de0c49f385ea2fa789e3cfe4d436c96be80200c5292f45f15/numexpr-2.14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dce0b5a0447baa7b44bc218ec2d7dcd175b8eee6083605293349c0c1d9b82fb6", size = 152203, upload-time = "2025-10-13T16:16:39.907Z" }, + { url = "https://files.pythonhosted.org/packages/13/65/72b065f9c75baf8f474fd5d2b768350935989d4917db1c6c75b866d4067c/numexpr-2.14.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:06855053de7a3a8425429bd996e8ae3c50b57637ad3e757e0fa0602a7874be30", size = 455860, upload-time = "2025-10-13T16:13:35.811Z" }, + { url = "https://files.pythonhosted.org/packages/fc/f9/c9457652dfe28e2eb898372da2fe786c6db81af9540c0f853ee04a0699cc/numexpr-2.14.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05f9366d23a2e991fd5a8b5e61a17558f028ba86158a4552f8f239b005cdf83c", size = 446574, upload-time = "2025-10-13T16:15:17.367Z" }, + { url = "https://files.pythonhosted.org/packages/b6/99/8d3879c4d67d3db5560cf2de65ce1778b80b75f6fa415eb5c3e7bd37ba27/numexpr-2.14.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c5f1b1605695778896534dfc6e130d54a65cd52be7ed2cd0cfee3981fd676bf5", size = 1417306, upload-time = "2025-10-13T16:13:42.813Z" }, + { url = "https://files.pythonhosted.org/packages/ea/05/6bddac9f18598ba94281e27a6943093f7d0976544b0cb5d92272c64719bd/numexpr-2.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a4ba71db47ea99c659d88ee6233fa77b6dc83392f1d324e0c90ddf617ae3f421", size = 1466145, upload-time = "2025-10-13T16:15:27.464Z" }, + { url = "https://files.pythonhosted.org/packages/24/5d/cbeb67aca0c5a76ead13df7e8bd8dd5e0d49145f90da697ba1d9f07005b0/numexpr-2.14.1-cp313-cp313-win32.whl", hash = "sha256:638dce8320f4a1483d5ca4fda69f60a70ed7e66be6e68bc23fb9f1a6b78a9e3b", size = 166996, upload-time = "2025-10-13T16:17:13.803Z" }, + { url = "https://files.pythonhosted.org/packages/cc/23/9281bceaeb282cead95f0aa5f7f222ffc895670ea689cc1398355f6e3001/numexpr-2.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:9fdcd4735121658a313f878fd31136d1bfc6a5b913219e7274e9fca9f8dac3bb", size = 160189, upload-time = "2025-10-13T16:17:15.417Z" }, + { url = "https://files.pythonhosted.org/packages/f3/76/7aac965fd93a56803cbe502aee2adcad667253ae34b0badf6c5af7908b6c/numexpr-2.14.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:557887ad7f5d3c2a40fd7310e50597045a68e66b20a77b3f44d7bc7608523b4b", size = 163524, upload-time = "2025-10-13T16:16:42.213Z" }, + { url = "https://files.pythonhosted.org/packages/58/65/79d592d5e63fbfab3b59a60c386853d9186a44a3fa3c87ba26bdc25b6195/numexpr-2.14.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:af111c8fe6fc55d15e4c7cab11920fc50740d913636d486545b080192cd0ad73", size = 152919, upload-time = "2025-10-13T16:16:44.229Z" }, + { url = "https://files.pythonhosted.org/packages/84/78/3c8335f713d4aeb99fa758d7c62f0be1482d4947ce5b508e2052bb7aeee9/numexpr-2.14.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:33265294376e7e2ae4d264d75b798a915d2acf37b9dd2b9405e8b04f84d05cfc", size = 465972, upload-time = "2025-10-13T16:13:45.061Z" }, + { url = "https://files.pythonhosted.org/packages/35/81/9ee5f69b811e8f18746c12d6f71848617684edd3161927f95eee7a305631/numexpr-2.14.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:83647d846d3eeeb9a9255311236135286728b398d0d41d35dedb532dca807fe9", size = 456953, upload-time = "2025-10-13T16:15:31.186Z" }, + { url = "https://files.pythonhosted.org/packages/6d/39/9b8bc6e294d85cbb54a634e47b833e9f3276a8bdf7ce92aa808718a0212d/numexpr-2.14.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6e575fd3ad41ddf3355d0c7ef6bd0168619dc1779a98fe46693cad5e95d25e6e", size = 1426199, upload-time = "2025-10-13T16:13:48.231Z" }, + { url = "https://files.pythonhosted.org/packages/1e/ce/0d4fcd31ab49319740d934fba1734d7dad13aa485532ca754e555ca16c8b/numexpr-2.14.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:67ea4771029ce818573b1998f5ca416bd255156feea017841b86176a938f7d19", size = 1474214, upload-time = "2025-10-13T16:15:38.893Z" }, + { url = "https://files.pythonhosted.org/packages/b7/47/b2a93cbdb3ba4e009728ad1b9ef1550e2655ea2c86958ebaf03b9615f275/numexpr-2.14.1-cp313-cp313t-win32.whl", hash = "sha256:15015d47d3d1487072d58c0e7682ef2eb608321e14099c39d52e2dd689483611", size = 167676, upload-time = "2025-10-13T16:17:17.351Z" }, + { url = "https://files.pythonhosted.org/packages/86/99/ee3accc589ed032eea68e12172515ed96a5568534c213ad109e1f4411df1/numexpr-2.14.1-cp313-cp313t-win_amd64.whl", hash = "sha256:94c711f6d8f17dfb4606842b403699603aa591ab9f6bf23038b488ea9cfb0f09", size = 161096, upload-time = "2025-10-13T16:17:19.174Z" }, + { url = "https://files.pythonhosted.org/packages/ac/36/9db78dfbfdfa1f8bf0872993f1a334cdd8fca5a5b6567e47dcb128bcb7c2/numexpr-2.14.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:ede79f7ff06629f599081de644546ce7324f1581c09b0ac174da88a470d39c21", size = 162848, upload-time = "2025-10-13T16:16:46.216Z" }, + { url = "https://files.pythonhosted.org/packages/13/c1/a5c78ae637402c5550e2e0ba175275d2515d432ec28af0cdc23c9b476e65/numexpr-2.14.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2eac7a5a2f70b3768c67056445d1ceb4ecd9b853c8eda9563823b551aeaa5082", size = 152270, upload-time = "2025-10-13T16:16:47.92Z" }, + { url = "https://files.pythonhosted.org/packages/9a/ed/aabd8678077848dd9a751c5558c2057839f5a09e2a176d8dfcd0850ee00e/numexpr-2.14.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5aedf38d4c0c19d3cecfe0334c3f4099fb496f54c146223d30fa930084bc8574", size = 455918, upload-time = "2025-10-13T16:13:50.338Z" }, + { url = "https://files.pythonhosted.org/packages/88/e1/3db65117f02cdefb0e5e4c440daf1c30beb45051b7f47aded25b7f4f2f34/numexpr-2.14.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:439ec4d57b853792ebe5456e3160312281c3a7071ecac5532ded3278ede614de", size = 446512, upload-time = "2025-10-13T16:15:42.313Z" }, + { url = "https://files.pythonhosted.org/packages/9a/fb/7ceb9ee55b5f67e4a3e4d73d5af4c7e37e3c9f37f54bee90361b64b17e3f/numexpr-2.14.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e23b87f744e04e302d82ac5e2189ae20a533566aec76a46885376e20b0645bf8", size = 1417845, upload-time = "2025-10-13T16:13:53.836Z" }, + { url = "https://files.pythonhosted.org/packages/45/2d/9b5764d0eafbbb2889288f80de773791358acf6fad1a55767538d8b79599/numexpr-2.14.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:44f84e0e5af219dbb62a081606156420815890e041b87252fbcea5df55214c4c", size = 1466211, upload-time = "2025-10-13T16:15:48.985Z" }, + { url = "https://files.pythonhosted.org/packages/5d/21/204db708eccd71aa8bc55bcad55bc0fc6c5a4e01ad78e14ee5714a749386/numexpr-2.14.1-cp314-cp314-win32.whl", hash = "sha256:1f1a5e817c534539351aa75d26088e9e1e0ef1b3a6ab484047618a652ccc4fc3", size = 168835, upload-time = "2025-10-13T16:17:20.82Z" }, + { url = "https://files.pythonhosted.org/packages/4f/3e/d83e9401a1c3449a124f7d4b3fb44084798e0d30f7c11e60712d9b94cf11/numexpr-2.14.1-cp314-cp314-win_amd64.whl", hash = "sha256:587c41509bc373dfb1fe6086ba55a73147297247bedb6d588cda69169fc412f2", size = 162608, upload-time = "2025-10-13T16:17:22.228Z" }, + { url = "https://files.pythonhosted.org/packages/7f/d6/ec947806bb57836d6379a8c8a253c2aeaa602b12fef2336bfd2462bb4ed5/numexpr-2.14.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:ec368819502b64f190c3f71be14a304780b5935c42aae5bf22c27cc2cbba70b5", size = 163525, upload-time = "2025-10-13T16:16:50.133Z" }, + { url = "https://files.pythonhosted.org/packages/0d/77/048f30dcf661a3d52963a88c29b52b6d5ce996d38e9313a56a922451c1e0/numexpr-2.14.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7e87f6d203ac57239de32261c941e9748f9309cbc0da6295eabd0c438b920d3a", size = 152917, upload-time = "2025-10-13T16:16:52.055Z" }, + { url = "https://files.pythonhosted.org/packages/9e/d3/956a13e628d722d649fbf2fded615134a308c082e122a48bad0e90a99ce9/numexpr-2.14.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dd72d8c2a165fe45ea7650b16eb8cc1792a94a722022006bb97c86fe51fd2091", size = 466242, upload-time = "2025-10-13T16:13:55.795Z" }, + { url = "https://files.pythonhosted.org/packages/d6/dd/abe848678d82486940892f2cacf39e82eec790e8930d4d713d3f9191063b/numexpr-2.14.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:70d80fcb418a54ca208e9a38e58ddc425c07f66485176b261d9a67c7f2864f73", size = 457149, upload-time = "2025-10-13T16:15:52.036Z" }, + { url = "https://files.pythonhosted.org/packages/fd/bb/797b583b5fb9da5700a5708ca6eb4f889c94d81abb28de4d642c0f4b3258/numexpr-2.14.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:edea2f20c2040df8b54ee8ca8ebda63de9545b2112872466118e9df4d0ae99f3", size = 1426493, upload-time = "2025-10-13T16:13:59.244Z" }, + { url = "https://files.pythonhosted.org/packages/77/c4/0519ab028fdc35e3e7ee700def7f2b4631b175cd9e1202bd7966c1695c33/numexpr-2.14.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:790447be6879a6c51b9545f79612d24c9ea0a41d537a84e15e6a8ddef0b6268e", size = 1474413, upload-time = "2025-10-13T16:15:59.211Z" }, + { url = "https://files.pythonhosted.org/packages/d4/4a/33044878c8f4a75213cfe9c11d4c02058bb710a7a063fe14f362e8de1077/numexpr-2.14.1-cp314-cp314t-win32.whl", hash = "sha256:538961096c2300ea44240209181e31fae82759d26b51713b589332b9f2a4117e", size = 169502, upload-time = "2025-10-13T16:17:23.829Z" }, + { url = "https://files.pythonhosted.org/packages/41/a2/5a1a2c72528b429337f49911b18c302ecd36eeab00f409147e1aa4ae4519/numexpr-2.14.1-cp314-cp314t-win_amd64.whl", hash = "sha256:a40b350cd45b4446076fa11843fa32bbe07024747aeddf6d467290bf9011b392", size = 163589, upload-time = "2025-10-13T16:17:25.696Z" }, +] + +[[package]] +name = "numpy" +version = "1.26.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/65/6e/09db70a523a96d25e115e71cc56a6f9031e7b8cd166c1ac8438307c14058/numpy-1.26.4.tar.gz", hash = "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010", size = 15786129, upload-time = "2024-02-06T00:26:44.495Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/57/baae43d14fe163fa0e4c47f307b6b2511ab8d7d30177c491960504252053/numpy-1.26.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c66707fabe114439db9068ee468c26bbdf909cac0fb58686a42a24de1760c71", size = 20630554, upload-time = "2024-02-05T23:51:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/1a/2e/151484f49fd03944c4a3ad9c418ed193cfd02724e138ac8a9505d056c582/numpy-1.26.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:edd8b5fe47dab091176d21bb6de568acdd906d1887a4584a15a9a96a1dca06ef", size = 13997127, upload-time = "2024-02-05T23:52:15.314Z" }, + { url = "https://files.pythonhosted.org/packages/79/ae/7e5b85136806f9dadf4878bf73cf223fe5c2636818ba3ab1c585d0403164/numpy-1.26.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ab55401287bfec946ced39700c053796e7cc0e3acbef09993a9ad2adba6ca6e", size = 14222994, upload-time = "2024-02-05T23:52:47.569Z" }, + { url = "https://files.pythonhosted.org/packages/3a/d0/edc009c27b406c4f9cbc79274d6e46d634d139075492ad055e3d68445925/numpy-1.26.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:666dbfb6ec68962c033a450943ded891bed2d54e6755e35e5835d63f4f6931d5", size = 18252005, upload-time = "2024-02-05T23:53:15.637Z" }, + { url = "https://files.pythonhosted.org/packages/09/bf/2b1aaf8f525f2923ff6cfcf134ae5e750e279ac65ebf386c75a0cf6da06a/numpy-1.26.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:96ff0b2ad353d8f990b63294c8986f1ec3cb19d749234014f4e7eb0112ceba5a", size = 13885297, upload-time = "2024-02-05T23:53:42.16Z" }, + { url = "https://files.pythonhosted.org/packages/df/a0/4e0f14d847cfc2a633a1c8621d00724f3206cfeddeb66d35698c4e2cf3d2/numpy-1.26.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:60dedbb91afcbfdc9bc0b1f3f402804070deed7392c23eb7a7f07fa857868e8a", size = 18093567, upload-time = "2024-02-05T23:54:11.696Z" }, + { url = "https://files.pythonhosted.org/packages/d2/b7/a734c733286e10a7f1a8ad1ae8c90f2d33bf604a96548e0a4a3a6739b468/numpy-1.26.4-cp311-cp311-win32.whl", hash = "sha256:1af303d6b2210eb850fcf03064d364652b7120803a0b872f5211f5234b399f20", size = 5968812, upload-time = "2024-02-05T23:54:26.453Z" }, + { url = "https://files.pythonhosted.org/packages/3f/6b/5610004206cf7f8e7ad91c5a85a8c71b2f2f8051a0c0c4d5916b76d6cbb2/numpy-1.26.4-cp311-cp311-win_amd64.whl", hash = "sha256:cd25bcecc4974d09257ffcd1f098ee778f7834c3ad767fe5db785be9a4aa9cb2", size = 15811913, upload-time = "2024-02-05T23:54:53.933Z" }, + { url = "https://files.pythonhosted.org/packages/95/12/8f2020a8e8b8383ac0177dc9570aad031a3beb12e38847f7129bacd96228/numpy-1.26.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b3ce300f3644fb06443ee2222c2201dd3a89ea6040541412b8fa189341847218", size = 20335901, upload-time = "2024-02-05T23:55:32.801Z" }, + { url = "https://files.pythonhosted.org/packages/75/5b/ca6c8bd14007e5ca171c7c03102d17b4f4e0ceb53957e8c44343a9546dcc/numpy-1.26.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:03a8c78d01d9781b28a6989f6fa1bb2c4f2d51201cf99d3dd875df6fbd96b23b", size = 13685868, upload-time = "2024-02-05T23:55:56.28Z" }, + { url = "https://files.pythonhosted.org/packages/79/f8/97f10e6755e2a7d027ca783f63044d5b1bc1ae7acb12afe6a9b4286eac17/numpy-1.26.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9fad7dcb1aac3c7f0584a5a8133e3a43eeb2fe127f47e3632d43d677c66c102b", size = 13925109, upload-time = "2024-02-05T23:56:20.368Z" }, + { url = "https://files.pythonhosted.org/packages/0f/50/de23fde84e45f5c4fda2488c759b69990fd4512387a8632860f3ac9cd225/numpy-1.26.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675d61ffbfa78604709862923189bad94014bef562cc35cf61d3a07bba02a7ed", size = 17950613, upload-time = "2024-02-05T23:56:56.054Z" }, + { url = "https://files.pythonhosted.org/packages/4c/0c/9c603826b6465e82591e05ca230dfc13376da512b25ccd0894709b054ed0/numpy-1.26.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab47dbe5cc8210f55aa58e4805fe224dac469cde56b9f731a4c098b91917159a", size = 13572172, upload-time = "2024-02-05T23:57:21.56Z" }, + { url = "https://files.pythonhosted.org/packages/76/8c/2ba3902e1a0fc1c74962ea9bb33a534bb05984ad7ff9515bf8d07527cadd/numpy-1.26.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1dda2e7b4ec9dd512f84935c5f126c8bd8b9f2fc001e9f54af255e8c5f16b0e0", size = 17786643, upload-time = "2024-02-05T23:57:56.585Z" }, + { url = "https://files.pythonhosted.org/packages/28/4a/46d9e65106879492374999e76eb85f87b15328e06bd1550668f79f7b18c6/numpy-1.26.4-cp312-cp312-win32.whl", hash = "sha256:50193e430acfc1346175fcbdaa28ffec49947a06918b7b92130744e81e640110", size = 5677803, upload-time = "2024-02-05T23:58:08.963Z" }, + { url = "https://files.pythonhosted.org/packages/16/2e/86f24451c2d530c88daf997cb8d6ac622c1d40d19f5a031ed68a4b73a374/numpy-1.26.4-cp312-cp312-win_amd64.whl", hash = "sha256:08beddf13648eb95f8d867350f6a018a4be2e5ad54c8d8caed89ebca558b2818", size = 15517754, upload-time = "2024-02-05T23:58:36.364Z" }, +] + +[[package]] +name = "nvidia-cublas-cu12" +version = "12.8.4.1" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/61/e24b560ab2e2eaeb3c839129175fb330dfcfc29e5203196e5541a4c44682/nvidia_cublas_cu12-12.8.4.1-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:8ac4e771d5a348c551b2a426eda6193c19aa630236b418086020df5ba9667142", size = 594346921, upload-time = "2025-03-07T01:44:31.254Z" }, +] + +[[package]] +name = "nvidia-cuda-cupti-cu12" +version = "12.8.90" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/02/2adcaa145158bf1a8295d83591d22e4103dbfd821bcaf6f3f53151ca4ffa/nvidia_cuda_cupti_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ea0cb07ebda26bb9b29ba82cda34849e73c166c18162d3913575b0c9db9a6182", size = 10248621, upload-time = "2025-03-07T01:40:21.213Z" }, +] + +[[package]] +name = "nvidia-cuda-nvrtc-cu12" +version = "12.8.93" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/6b/32f747947df2da6994e999492ab306a903659555dddc0fbdeb9d71f75e52/nvidia_cuda_nvrtc_cu12-12.8.93-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:a7756528852ef889772a84c6cd89d41dfa74667e24cca16bb31f8f061e3e9994", size = 88040029, upload-time = "2025-03-07T01:42:13.562Z" }, +] + +[[package]] +name = "nvidia-cuda-runtime-cu12" +version = "12.8.90" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/9b/a997b638fcd068ad6e4d53b8551a7d30fe8b404d6f1804abf1df69838932/nvidia_cuda_runtime_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:adade8dcbd0edf427b7204d480d6066d33902cab2a4707dcfc48a2d0fd44ab90", size = 954765, upload-time = "2025-03-07T01:40:01.615Z" }, +] + +[[package]] +name = "nvidia-cudnn-cu12" +version = "9.10.2.21" +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/ba/51/e123d997aa098c61d029f76663dedbfb9bc8dcf8c60cbd6adbe42f76d049/nvidia_cudnn_cu12-9.10.2.21-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:949452be657fa16687d0930933f032835951ef0892b37d2d53824d1a84dc97a8", size = 706758467, upload-time = "2025-06-06T21:54:08.597Z" }, +] + +[[package]] +name = "nvidia-cufft-cu12" +version = "11.3.3.83" +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/1f/13/ee4e00f30e676b66ae65b4f08cb5bcbb8392c03f54f2d5413ea99a5d1c80/nvidia_cufft_cu12-11.3.3.83-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d2dd21ec0b88cf61b62e6b43564355e5222e4a3fb394cac0db101f2dd0d4f74", size = 193118695, upload-time = "2025-03-07T01:45:27.821Z" }, +] + +[[package]] +name = "nvidia-cufile-cu12" +version = "1.13.1.3" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bb/fe/1bcba1dfbfb8d01be8d93f07bfc502c93fa23afa6fd5ab3fc7c1df71038a/nvidia_cufile_cu12-1.13.1.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1d069003be650e131b21c932ec3d8969c1715379251f8d23a1860554b1cb24fc", size = 1197834, upload-time = "2025-03-07T01:45:50.723Z" }, +] + +[[package]] +name = "nvidia-curand-cu12" +version = "10.3.9.90" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/aa/6584b56dc84ebe9cf93226a5cde4d99080c8e90ab40f0c27bda7a0f29aa1/nvidia_curand_cu12-10.3.9.90-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:b32331d4f4df5d6eefa0554c565b626c7216f87a06a4f56fab27c3b68a830ec9", size = 63619976, upload-time = "2025-03-07T01:46:23.323Z" }, +] + +[[package]] +name = "nvidia-cusolver-cu12" +version = "11.7.3.90" +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/85/48/9a13d2975803e8cf2777d5ed57b87a0b6ca2cc795f9a4f59796a910bfb80/nvidia_cusolver_cu12-11.7.3.90-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:4376c11ad263152bd50ea295c05370360776f8c3427b30991df774f9fb26c450", size = 267506905, upload-time = "2025-03-07T01:47:16.273Z" }, +] + +[[package]] +name = "nvidia-cusparse-cu12" +version = "12.5.8.93" +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/c2/f5/e1854cb2f2bcd4280c44736c93550cc300ff4b8c95ebe370d0aa7d2b473d/nvidia_cusparse_cu12-12.5.8.93-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1ec05d76bbbd8b61b06a80e1eaf8cf4959c3d4ce8e711b65ebd0443bb0ebb13b", size = 288216466, upload-time = "2025-03-07T01:48:13.779Z" }, +] + +[[package]] +name = "nvidia-cusparselt-cu12" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/79/12978b96bd44274fe38b5dde5cfb660b1d114f70a65ef962bcbbed99b549/nvidia_cusparselt_cu12-0.7.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:f1bb701d6b930d5a7cea44c19ceb973311500847f81b634d802b7b539dc55623", size = 287193691, upload-time = "2025-02-26T00:15:44.104Z" }, +] + +[[package]] +name = "nvidia-nccl-cu12" +version = "2.27.5" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6e/89/f7a07dc961b60645dbbf42e80f2bc85ade7feb9a491b11a1e973aa00071f/nvidia_nccl_cu12-2.27.5-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ad730cf15cb5d25fe849c6e6ca9eb5b76db16a80f13f425ac68d8e2e55624457", size = 322348229, upload-time = "2025-06-26T04:11:28.385Z" }, +] + +[[package]] +name = "nvidia-nvjitlink-cu12" +version = "12.8.93" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f6/74/86a07f1d0f42998ca31312f998bd3b9a7eff7f52378f4f270c8679c77fb9/nvidia_nvjitlink_cu12-12.8.93-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:81ff63371a7ebd6e6451970684f916be2eab07321b73c9d244dc2b4da7f73b88", size = 39254836, upload-time = "2025-03-07T01:49:55.661Z" }, +] + +[[package]] +name = "nvidia-nvshmem-cu12" +version = "3.4.5" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/09/6ea3ea725f82e1e76684f0708bbedd871fc96da89945adeba65c3835a64c/nvidia_nvshmem_cu12-3.4.5-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:042f2500f24c021db8a06c5eec2539027d57460e1c1a762055a6554f72c369bd", size = 139103095, upload-time = "2025-09-06T00:32:31.266Z" }, +] + +[[package]] +name = "nvidia-nvtx-cu12" +version = "12.8.90" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/eb/86626c1bbc2edb86323022371c39aa48df6fd8b0a1647bc274577f72e90b/nvidia_nvtx_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5b17e2001cc0d751a5bc2c6ec6d26ad95913324a4adb86788c944f8ce9ba441f", size = 89954, upload-time = "2025-03-07T01:42:44.131Z" }, +] + +[[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, upload-time = "2023-12-01T16:22:53.025Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/d3/b64c356a907242d719fc668b71befd73324e47ab46c8ebbbede252c154b2/olefile-0.47-py2.py3-none-any.whl", hash = "sha256:543c7da2a7adadf21214938bb79c83ea12b473a4b6ee4ad4bf854e7715e13d1f", size = 114565, upload-time = "2023-12-01T16:22:51.518Z" }, +] + +[[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, upload-time = "2022-12-08T20:59:22.753Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e3/94/1843518e420fa3ed6919835845df698c7e27e183cb997394e4a670973a65/omegaconf-2.3.0-py3-none-any.whl", hash = "sha256:7b4df175cdb08ba400f45cae3bdcae7ba8365db4d165fc65fd04b050ab63b46b", size = 79500, upload-time = "2022-12-08T20:59:19.686Z" }, +] + +[[package]] +name = "openai" +version = "2.54.0" +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/50/9a/8c75e8c8a5b407a0586faeb2afac91674ff955c191ecc1d6d3b6669f6788/openai-2.54.0.tar.gz", hash = "sha256:e3e6f8bc1ba30ddf381ace1a14340eed381cb984a1a59bd0f34b5be3b5d49cfa", size = 1100285, upload-time = "2026-08-11T18:46:59.035Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/a8/bb76c7356de8ad57f59d5ff993d434df0607f07f08bcc9c9a5c275e399c0/openai-2.54.0-py3-none-any.whl", hash = "sha256:89089789197ccdb87f173a03145ed1598d00795220c93e96cf712b1cbf5e5f2b", size = 1660351, upload-time = "2026-08-11T18:46:56.684Z" }, +] + +[[package]] +name = "opencv-python" +version = "4.11.0.86" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/17/06/68c27a523103dad5837dc5b87e71285280c4f098c60e4fe8a8db6486ab09/opencv-python-4.11.0.86.tar.gz", hash = "sha256:03d60ccae62304860d232272e4a4fda93c39d595780cb40b161b310244b736a4", size = 95171956, upload-time = "2025-01-16T13:52:24.737Z" } +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, upload-time = "2025-01-16T13:52:25.887Z" }, + { 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, upload-time = "2025-01-16T13:55:21.222Z" }, + { 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, upload-time = "2025-01-16T13:51:35.822Z" }, + { 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, upload-time = "2025-01-16T13:52:08.836Z" }, + { url = "https://files.pythonhosted.org/packages/fb/d7/1d5941a9dde095468b288d989ff6539dd69cd429dbf1b9e839013d21b6f0/opencv_python-4.11.0.86-cp37-abi3-win32.whl", hash = "sha256:810549cb2a4aedaa84ad9a1c92fbfdfc14090e2749cedf2c1589ad8359aa169b", size = 29384337, upload-time = "2025-01-16T13:52:13.549Z" }, + { 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, upload-time = "2025-01-16T13:52:21.928Z" }, +] + +[[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, upload-time = "2024-06-28T14:03:44.161Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/da/977ded879c29cbd04de313843e76868e6e13408a94ed6b987245dc7c8506/openpyxl-3.1.5-py2.py3-none-any.whl", hash = "sha256:5282c12b107bffeef825f4617dc029afaf41d0ea60823bbb665ef3079dc79de2", size = 250910, upload-time = "2024-06-28T14:03:41.161Z" }, +] + +[[package]] +name = "orjson" +version = "3.11.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/45/b268004f745ede84e5798b48ee12b05129d19235d0e15267aa57dcdb400b/orjson-3.11.7.tar.gz", hash = "sha256:9b1a67243945819ce55d24a30b59d6a168e86220452d2c96f4d1f093e71c0c49", size = 6144992, upload-time = "2026-02-02T15:38:49.29Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/37/02/da6cb01fc6087048d7f61522c327edf4250f1683a58a839fdcc435746dd5/orjson-3.11.7-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9487abc2c2086e7c8eb9a211d2ce8855bae0e92586279d0d27b341d5ad76c85c", size = 228664, upload-time = "2026-02-02T15:37:25.542Z" }, + { url = "https://files.pythonhosted.org/packages/c1/c2/5885e7a5881dba9a9af51bc564e8967225a642b3e03d089289a35054e749/orjson-3.11.7-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:79cacb0b52f6004caf92405a7e1f11e6e2de8bdf9019e4f76b44ba045125cd6b", size = 125344, upload-time = "2026-02-02T15:37:26.92Z" }, + { url = "https://files.pythonhosted.org/packages/a4/1d/4e7688de0a92d1caf600dfd5fb70b4c5bfff51dfa61ac555072ef2d0d32a/orjson-3.11.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c2e85fe4698b6a56d5e2ebf7ae87544d668eb6bde1ad1226c13f44663f20ec9e", size = 128404, upload-time = "2026-02-02T15:37:28.108Z" }, + { url = "https://files.pythonhosted.org/packages/2f/b2/ec04b74ae03a125db7bd69cffd014b227b7f341e3261bf75b5eb88a1aa92/orjson-3.11.7-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b8d14b71c0b12963fe8a62aac87119f1afdf4cb88a400f61ca5ae581449efcb5", size = 123677, upload-time = "2026-02-02T15:37:30.287Z" }, + { url = "https://files.pythonhosted.org/packages/4c/69/f95bdf960605f08f827f6e3291fe243d8aa9c5c9ff017a8d7232209184c3/orjson-3.11.7-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:91c81ef070c8f3220054115e1ef468b1c9ce8497b4e526cb9f68ab4dc0a7ac62", size = 128950, upload-time = "2026-02-02T15:37:31.595Z" }, + { url = "https://files.pythonhosted.org/packages/a4/1b/de59c57bae1d148ef298852abd31909ac3089cff370dfd4cd84cc99cbc42/orjson-3.11.7-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:411ebaf34d735e25e358a6d9e7978954a9c9d58cfb47bc6683cdc3964cd2f910", size = 141756, upload-time = "2026-02-02T15:37:32.985Z" }, + { url = "https://files.pythonhosted.org/packages/ee/9e/9decc59f4499f695f65c650f6cfa6cd4c37a3fbe8fa235a0a3614cb54386/orjson-3.11.7-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a16bcd08ab0bcdfc7e8801d9c4a9cc17e58418e4d48ddc6ded4e9e4b1a94062b", size = 130812, upload-time = "2026-02-02T15:37:34.204Z" }, + { url = "https://files.pythonhosted.org/packages/28/e6/59f932bcabd1eac44e334fe8e3281a92eacfcb450586e1f4bde0423728d8/orjson-3.11.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c0b51672e466fd7e56230ffbae7f1639e18d0ce023351fb75da21b71bc2c960", size = 133444, upload-time = "2026-02-02T15:37:35.446Z" }, + { url = "https://files.pythonhosted.org/packages/f1/36/b0f05c0eaa7ca30bc965e37e6a2956b0d67adb87a9872942d3568da846ae/orjson-3.11.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:136dcd6a2e796dfd9ffca9fc027d778567b0b7c9968d092842d3c323cef88aa8", size = 138609, upload-time = "2026-02-02T15:37:36.657Z" }, + { url = "https://files.pythonhosted.org/packages/b8/03/58ec7d302b8d86944c60c7b4b82975d5161fcce4c9bc8c6cb1d6741b6115/orjson-3.11.7-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:7ba61079379b0ae29e117db13bda5f28d939766e410d321ec1624afc6a0b0504", size = 408918, upload-time = "2026-02-02T15:37:38.076Z" }, + { url = "https://files.pythonhosted.org/packages/06/3a/868d65ef9a8b99be723bd510de491349618abd9f62c826cf206d962db295/orjson-3.11.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0527a4510c300e3b406591b0ba69b5dc50031895b0a93743526a3fc45f59d26e", size = 143998, upload-time = "2026-02-02T15:37:39.706Z" }, + { url = "https://files.pythonhosted.org/packages/5b/c7/1e18e1c83afe3349f4f6dc9e14910f0ae5f82eac756d1412ea4018938535/orjson-3.11.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a709e881723c9b18acddcfb8ba357322491ad553e277cf467e1e7e20e2d90561", size = 134802, upload-time = "2026-02-02T15:37:41.002Z" }, + { url = "https://files.pythonhosted.org/packages/d4/0b/ccb7ee1a65b37e8eeb8b267dc953561d72370e85185e459616d4345bab34/orjson-3.11.7-cp311-cp311-win32.whl", hash = "sha256:c43b8b5bab288b6b90dac410cca7e986a4fa747a2e8f94615aea407da706980d", size = 127828, upload-time = "2026-02-02T15:37:42.241Z" }, + { url = "https://files.pythonhosted.org/packages/af/9e/55c776dffda3f381e0f07d010a4f5f3902bf48eaba1bb7684d301acd4924/orjson-3.11.7-cp311-cp311-win_amd64.whl", hash = "sha256:6543001328aa857187f905308a028935864aefe9968af3848401b6fe80dbb471", size = 124941, upload-time = "2026-02-02T15:37:43.444Z" }, + { url = "https://files.pythonhosted.org/packages/aa/8e/424a620fa7d263b880162505fb107ef5e0afaa765b5b06a88312ac291560/orjson-3.11.7-cp311-cp311-win_arm64.whl", hash = "sha256:1ee5cc7160a821dfe14f130bc8e63e7611051f964b463d9e2a3a573204446a4d", size = 126245, upload-time = "2026-02-02T15:37:45.18Z" }, + { url = "https://files.pythonhosted.org/packages/80/bf/76f4f1665f6983385938f0e2a5d7efa12a58171b8456c252f3bae8a4cf75/orjson-3.11.7-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:bd03ea7606833655048dab1a00734a2875e3e86c276e1d772b2a02556f0d895f", size = 228545, upload-time = "2026-02-02T15:37:46.376Z" }, + { url = "https://files.pythonhosted.org/packages/79/53/6c72c002cb13b5a978a068add59b25a8bdf2800ac1c9c8ecdb26d6d97064/orjson-3.11.7-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:89e440ebc74ce8ab5c7bc4ce6757b4a6b1041becb127df818f6997b5c71aa60b", size = 125224, upload-time = "2026-02-02T15:37:47.697Z" }, + { url = "https://files.pythonhosted.org/packages/2c/83/10e48852865e5dd151bdfe652c06f7da484578ed02c5fca938e3632cb0b8/orjson-3.11.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ede977b5fe5ac91b1dffc0a517ca4542d2ec8a6a4ff7b2652d94f640796342a", size = 128154, upload-time = "2026-02-02T15:37:48.954Z" }, + { url = "https://files.pythonhosted.org/packages/6e/52/a66e22a2b9abaa374b4a081d410edab6d1e30024707b87eab7c734afe28d/orjson-3.11.7-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b7b1dae39230a393df353827c855a5f176271c23434cfd2db74e0e424e693e10", size = 123548, upload-time = "2026-02-02T15:37:50.187Z" }, + { url = "https://files.pythonhosted.org/packages/de/38/605d371417021359f4910c496f764c48ceb8997605f8c25bf1dfe58c0ebe/orjson-3.11.7-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed46f17096e28fb28d2975834836a639af7278aa87c84f68ab08fbe5b8bd75fa", size = 129000, upload-time = "2026-02-02T15:37:51.426Z" }, + { url = "https://files.pythonhosted.org/packages/44/98/af32e842b0ffd2335c89714d48ca4e3917b42f5d6ee5537832e069a4b3ac/orjson-3.11.7-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3726be79e36e526e3d9c1aceaadbfb4a04ee80a72ab47b3f3c17fefb9812e7b8", size = 141686, upload-time = "2026-02-02T15:37:52.607Z" }, + { url = "https://files.pythonhosted.org/packages/96/0b/fc793858dfa54be6feee940c1463370ece34b3c39c1ca0aa3845f5ba9892/orjson-3.11.7-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0724e265bc548af1dedebd9cb3d24b4e1c1e685a343be43e87ba922a5c5fff2f", size = 130812, upload-time = "2026-02-02T15:37:53.944Z" }, + { url = "https://files.pythonhosted.org/packages/dc/91/98a52415059db3f374757d0b7f0f16e3b5cd5976c90d1c2b56acaea039e6/orjson-3.11.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e7745312efa9e11c17fbd3cb3097262d079da26930ae9ae7ba28fb738367cbad", size = 133440, upload-time = "2026-02-02T15:37:55.615Z" }, + { url = "https://files.pythonhosted.org/packages/dc/b6/cb540117bda61791f46381f8c26c8f93e802892830a6055748d3bb1925ab/orjson-3.11.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f904c24bdeabd4298f7a977ef14ca2a022ca921ed670b92ecd16ab6f3d01f867", size = 138386, upload-time = "2026-02-02T15:37:56.814Z" }, + { url = "https://files.pythonhosted.org/packages/63/1a/50a3201c334a7f17c231eee5f841342190723794e3b06293f26e7cf87d31/orjson-3.11.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b9fc4d0f81f394689e0814617aadc4f2ea0e8025f38c226cbf22d3b5ddbf025d", size = 408853, upload-time = "2026-02-02T15:37:58.291Z" }, + { url = "https://files.pythonhosted.org/packages/87/cd/8de1c67d0be44fdc22701e5989c0d015a2adf391498ad42c4dc589cd3013/orjson-3.11.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:849e38203e5be40b776ed2718e587faf204d184fc9a008ae441f9442320c0cab", size = 144130, upload-time = "2026-02-02T15:38:00.163Z" }, + { url = "https://files.pythonhosted.org/packages/0f/fe/d605d700c35dd55f51710d159fc54516a280923cd1b7e47508982fbb387d/orjson-3.11.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4682d1db3bcebd2b64757e0ddf9e87ae5f00d29d16c5cdf3a62f561d08cc3dd2", size = 134818, upload-time = "2026-02-02T15:38:01.507Z" }, + { url = "https://files.pythonhosted.org/packages/e4/e4/15ecc67edb3ddb3e2f46ae04475f2d294e8b60c1825fbe28a428b93b3fbd/orjson-3.11.7-cp312-cp312-win32.whl", hash = "sha256:f4f7c956b5215d949a1f65334cf9d7612dde38f20a95f2315deef167def91a6f", size = 127923, upload-time = "2026-02-02T15:38:02.75Z" }, + { url = "https://files.pythonhosted.org/packages/34/70/2e0855361f76198a3965273048c8e50a9695d88cd75811a5b46444895845/orjson-3.11.7-cp312-cp312-win_amd64.whl", hash = "sha256:bf742e149121dc5648ba0a08ea0871e87b660467ef168a3a5e53bc1fbd64bb74", size = 125007, upload-time = "2026-02-02T15:38:04.032Z" }, + { url = "https://files.pythonhosted.org/packages/68/40/c2051bd19fc467610fed469dc29e43ac65891571138f476834ca192bc290/orjson-3.11.7-cp312-cp312-win_arm64.whl", hash = "sha256:26c3b9132f783b7d7903bf1efb095fed8d4a3a85ec0d334ee8beff3d7a4749d5", size = 126089, upload-time = "2026-02-02T15:38:05.297Z" }, + { url = "https://files.pythonhosted.org/packages/89/25/6e0e52cac5aab51d7b6dcd257e855e1dec1c2060f6b28566c509b4665f62/orjson-3.11.7-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:1d98b30cc1313d52d4af17d9c3d307b08389752ec5f2e5febdfada70b0f8c733", size = 228390, upload-time = "2026-02-02T15:38:06.8Z" }, + { url = "https://files.pythonhosted.org/packages/a5/29/a77f48d2fc8a05bbc529e5ff481fb43d914f9e383ea2469d4f3d51df3d00/orjson-3.11.7-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:d897e81f8d0cbd2abb82226d1860ad2e1ab3ff16d7b08c96ca00df9d45409ef4", size = 125189, upload-time = "2026-02-02T15:38:08.181Z" }, + { url = "https://files.pythonhosted.org/packages/89/25/0a16e0729a0e6a1504f9d1a13cdd365f030068aab64cec6958396b9969d7/orjson-3.11.7-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:814be4b49b228cfc0b3c565acf642dd7d13538f966e3ccde61f4f55be3e20785", size = 128106, upload-time = "2026-02-02T15:38:09.41Z" }, + { url = "https://files.pythonhosted.org/packages/66/da/a2e505469d60666a05ab373f1a6322eb671cb2ba3a0ccfc7d4bc97196787/orjson-3.11.7-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d06e5c5fed5caedd2e540d62e5b1c25e8c82431b9e577c33537e5fa4aa909539", size = 123363, upload-time = "2026-02-02T15:38:10.73Z" }, + { url = "https://files.pythonhosted.org/packages/23/bf/ed73f88396ea35c71b38961734ea4a4746f7ca0768bf28fd551d37e48dd0/orjson-3.11.7-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:31c80ce534ac4ea3739c5ee751270646cbc46e45aea7576a38ffec040b4029a1", size = 129007, upload-time = "2026-02-02T15:38:12.138Z" }, + { url = "https://files.pythonhosted.org/packages/73/3c/b05d80716f0225fc9008fbf8ab22841dcc268a626aa550561743714ce3bf/orjson-3.11.7-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f50979824bde13d32b4320eedd513431c921102796d86be3eee0b58e58a3ecd1", size = 141667, upload-time = "2026-02-02T15:38:13.398Z" }, + { url = "https://files.pythonhosted.org/packages/61/e8/0be9b0addd9bf86abfc938e97441dcd0375d494594b1c8ad10fe57479617/orjson-3.11.7-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9e54f3808e2b6b945078c41aa8d9b5834b28c50843846e97807e5adb75fa9705", size = 130832, upload-time = "2026-02-02T15:38:14.698Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ec/c68e3b9021a31d9ec15a94931db1410136af862955854ed5dd7e7e4f5bff/orjson-3.11.7-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a12b80df61aab7b98b490fe9e4879925ba666fccdfcd175252ce4d9035865ace", size = 133373, upload-time = "2026-02-02T15:38:16.109Z" }, + { url = "https://files.pythonhosted.org/packages/d2/45/f3466739aaafa570cc8e77c6dbb853c48bf56e3b43738020e2661e08b0ac/orjson-3.11.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:996b65230271f1a97026fd0e6a753f51fbc0c335d2ad0c6201f711b0da32693b", size = 138307, upload-time = "2026-02-02T15:38:17.453Z" }, + { url = "https://files.pythonhosted.org/packages/e1/84/9f7f02288da1ffb31405c1be07657afd1eecbcb4b64ee2817b6fe0f785fa/orjson-3.11.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ab49d4b2a6a1d415ddb9f37a21e02e0d5dbfe10b7870b21bf779fc21e9156157", size = 408695, upload-time = "2026-02-02T15:38:18.831Z" }, + { url = "https://files.pythonhosted.org/packages/18/07/9dd2f0c0104f1a0295ffbe912bc8d63307a539b900dd9e2c48ef7810d971/orjson-3.11.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:390a1dce0c055ddf8adb6aa94a73b45a4a7d7177b5c584b8d1c1947f2ba60fb3", size = 144099, upload-time = "2026-02-02T15:38:20.28Z" }, + { url = "https://files.pythonhosted.org/packages/a5/66/857a8e4a3292e1f7b1b202883bcdeb43a91566cf59a93f97c53b44bd6801/orjson-3.11.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1eb80451a9c351a71dfaf5b7ccc13ad065405217726b59fdbeadbcc544f9d223", size = 134806, upload-time = "2026-02-02T15:38:22.186Z" }, + { url = "https://files.pythonhosted.org/packages/0a/5b/6ebcf3defc1aab3a338ca777214966851e92efb1f30dc7fc8285216e6d1b/orjson-3.11.7-cp313-cp313-win32.whl", hash = "sha256:7477aa6a6ec6139c5cb1cc7b214643592169a5494d200397c7fc95d740d5fcf3", size = 127914, upload-time = "2026-02-02T15:38:23.511Z" }, + { url = "https://files.pythonhosted.org/packages/00/04/c6f72daca5092e3117840a1b1e88dfc809cc1470cf0734890d0366b684a1/orjson-3.11.7-cp313-cp313-win_amd64.whl", hash = "sha256:b9f95dcdea9d4f805daa9ddf02617a89e484c6985fa03055459f90e87d7a0757", size = 124986, upload-time = "2026-02-02T15:38:24.836Z" }, + { url = "https://files.pythonhosted.org/packages/03/ba/077a0f6f1085d6b806937246860fafbd5b17f3919c70ee3f3d8d9c713f38/orjson-3.11.7-cp313-cp313-win_arm64.whl", hash = "sha256:800988273a014a0541483dc81021247d7eacb0c845a9d1a34a422bc718f41539", size = 126045, upload-time = "2026-02-02T15:38:26.216Z" }, + { url = "https://files.pythonhosted.org/packages/e9/1e/745565dca749813db9a093c5ebc4bac1a9475c64d54b95654336ac3ed961/orjson-3.11.7-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:de0a37f21d0d364954ad5de1970491d7fbd0fb1ef7417d4d56a36dc01ba0c0a0", size = 228391, upload-time = "2026-02-02T15:38:27.757Z" }, + { url = "https://files.pythonhosted.org/packages/46/19/e40f6225da4d3aa0c8dc6e5219c5e87c2063a560fe0d72a88deb59776794/orjson-3.11.7-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:c2428d358d85e8da9d37cba18b8c4047c55222007a84f97156a5b22028dfbfc0", size = 125188, upload-time = "2026-02-02T15:38:29.241Z" }, + { url = "https://files.pythonhosted.org/packages/9d/7e/c4de2babef2c0817fd1f048fd176aa48c37bec8aef53d2fa932983032cce/orjson-3.11.7-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c4bc6c6ac52cdaa267552544c73e486fecbd710b7ac09bc024d5a78555a22f6", size = 128097, upload-time = "2026-02-02T15:38:30.618Z" }, + { url = "https://files.pythonhosted.org/packages/eb/74/233d360632bafd2197f217eee7fb9c9d0229eac0c18128aee5b35b0014fe/orjson-3.11.7-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bd0d68edd7dfca1b2eca9361a44ac9f24b078de3481003159929a0573f21a6bf", size = 123364, upload-time = "2026-02-02T15:38:32.363Z" }, + { url = "https://files.pythonhosted.org/packages/79/51/af79504981dd31efe20a9e360eb49c15f06df2b40e7f25a0a52d9ae888e8/orjson-3.11.7-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:623ad1b9548ef63886319c16fa317848e465a21513b31a6ad7b57443c3e0dcf5", size = 129076, upload-time = "2026-02-02T15:38:33.68Z" }, + { url = "https://files.pythonhosted.org/packages/67/e2/da898eb68b72304f8de05ca6715870d09d603ee98d30a27e8a9629abc64b/orjson-3.11.7-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6e776b998ac37c0396093d10290e60283f59cfe0fc3fccbd0ccc4bd04dd19892", size = 141705, upload-time = "2026-02-02T15:38:34.989Z" }, + { url = "https://files.pythonhosted.org/packages/c5/89/15364d92acb3d903b029e28d834edb8780c2b97404cbf7929aa6b9abdb24/orjson-3.11.7-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:652c6c3af76716f4a9c290371ba2e390ede06f6603edb277b481daf37f6f464e", size = 130855, upload-time = "2026-02-02T15:38:36.379Z" }, + { url = "https://files.pythonhosted.org/packages/c2/8b/ecdad52d0b38d4b8f514be603e69ccd5eacf4e7241f972e37e79792212ec/orjson-3.11.7-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a56df3239294ea5964adf074c54bcc4f0ccd21636049a2cf3ca9cf03b5d03cf1", size = 133386, upload-time = "2026-02-02T15:38:37.704Z" }, + { url = "https://files.pythonhosted.org/packages/b9/0e/45e1dcf10e17d0924b7c9162f87ec7b4ca79e28a0548acf6a71788d3e108/orjson-3.11.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:bda117c4148e81f746655d5a3239ae9bd00cb7bc3ca178b5fc5a5997e9744183", size = 138295, upload-time = "2026-02-02T15:38:39.096Z" }, + { url = "https://files.pythonhosted.org/packages/63/d7/4d2e8b03561257af0450f2845b91fbd111d7e526ccdf737267108075e0ba/orjson-3.11.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:23d6c20517a97a9daf1d48b580fcdc6f0516c6f4b5038823426033690b4d2650", size = 408720, upload-time = "2026-02-02T15:38:40.634Z" }, + { url = "https://files.pythonhosted.org/packages/78/cf/d45343518282108b29c12a65892445fc51f9319dc3c552ceb51bb5905ed2/orjson-3.11.7-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:8ff206156006da5b847c9304b6308a01e8cdbc8cce824e2779a5ba71c3def141", size = 144152, upload-time = "2026-02-02T15:38:42.262Z" }, + { url = "https://files.pythonhosted.org/packages/a9/3a/d6001f51a7275aacd342e77b735c71fa04125a3f93c36fee4526bc8c654e/orjson-3.11.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:962d046ee1765f74a1da723f4b33e3b228fe3a48bd307acce5021dfefe0e29b2", size = 134814, upload-time = "2026-02-02T15:38:43.627Z" }, + { url = "https://files.pythonhosted.org/packages/1d/d3/f19b47ce16820cc2c480f7f1723e17f6d411b3a295c60c8ad3aa9ff1c96a/orjson-3.11.7-cp314-cp314-win32.whl", hash = "sha256:89e13dd3f89f1c38a9c9eba5fbf7cdc2d1feca82f5f290864b4b7a6aac704576", size = 127997, upload-time = "2026-02-02T15:38:45.06Z" }, + { url = "https://files.pythonhosted.org/packages/12/df/172771902943af54bf661a8d102bdf2e7f932127968080632bda6054b62c/orjson-3.11.7-cp314-cp314-win_amd64.whl", hash = "sha256:845c3e0d8ded9c9271cd79596b9b552448b885b97110f628fb687aee2eed11c1", size = 124985, upload-time = "2026-02-02T15:38:46.388Z" }, + { url = "https://files.pythonhosted.org/packages/6f/1c/f2a8d8a1b17514660a614ce5f7aac74b934e69f5abc2700cc7ced882a009/orjson-3.11.7-cp314-cp314-win_arm64.whl", hash = "sha256:4a2e9c5be347b937a2e0203866f12bba36082e89b402ddb9e927d5822e43088d", size = 126038, upload-time = "2026-02-02T15:38:47.703Z" }, +] + +[[package]] +name = "ormsgpack" +version = "1.12.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/12/0c/f1761e21486942ab9bb6feaebc610fa074f7c5e496e6962dea5873348077/ormsgpack-1.12.2.tar.gz", hash = "sha256:944a2233640273bee67521795a73cf1e959538e0dfb7ac635505010455e53b33", size = 39031, upload-time = "2026-01-18T20:55:28.023Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/08/8b68f24b18e69d92238aa8f258218e6dfeacf4381d9d07ab8df303f524a9/ormsgpack-1.12.2-cp311-cp311-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:bd5f4bf04c37888e864f08e740c5a573c4017f6fd6e99fa944c5c935fabf2dd9", size = 378266, upload-time = "2026-01-18T20:55:59.876Z" }, + { url = "https://files.pythonhosted.org/packages/0d/24/29fc13044ecb7c153523ae0a1972269fcd613650d1fa1a9cec1044c6b666/ormsgpack-1.12.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34d5b28b3570e9fed9a5a76528fc7230c3c76333bc214798958e58e9b79cc18a", size = 203035, upload-time = "2026-01-18T20:55:30.59Z" }, + { url = "https://files.pythonhosted.org/packages/ad/c2/00169fb25dd8f9213f5e8a549dfb73e4d592009ebc85fbbcd3e1dcac575b/ormsgpack-1.12.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3708693412c28f3538fb5a65da93787b6bbab3484f6bc6e935bfb77a62400ae5", size = 210539, upload-time = "2026-01-18T20:55:48.569Z" }, + { url = "https://files.pythonhosted.org/packages/1b/33/543627f323ff3c73091f51d6a20db28a1a33531af30873ea90c5ac95a9b5/ormsgpack-1.12.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:43013a3f3e2e902e1d05e72c0f1aeb5bedbb8e09240b51e26792a3c89267e181", size = 212401, upload-time = "2026-01-18T20:56:10.101Z" }, + { url = "https://files.pythonhosted.org/packages/e8/5d/f70e2c3da414f46186659d24745483757bcc9adccb481a6eb93e2b729301/ormsgpack-1.12.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7c8b1667a72cbba74f0ae7ecf3105a5e01304620ed14528b2cb4320679d2869b", size = 387082, upload-time = "2026-01-18T20:56:12.047Z" }, + { url = "https://files.pythonhosted.org/packages/c0/d6/06e8dc920c7903e051f30934d874d4afccc9bb1c09dcaf0bc03a7de4b343/ormsgpack-1.12.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:df6961442140193e517303d0b5d7bc2e20e69a879c2d774316125350c4a76b92", size = 482346, upload-time = "2026-01-18T20:56:05.152Z" }, + { url = "https://files.pythonhosted.org/packages/66/c4/f337ac0905eed9c393ef990c54565cd33644918e0a8031fe48c098c71dbf/ormsgpack-1.12.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c6a4c34ddef109647c769d69be65fa1de7a6022b02ad45546a69b3216573eb4a", size = 425181, upload-time = "2026-01-18T20:55:37.83Z" }, + { url = "https://files.pythonhosted.org/packages/78/29/6d5758fabef3babdf4bbbc453738cc7de9cd3334e4c38dd5737e27b85653/ormsgpack-1.12.2-cp311-cp311-win_amd64.whl", hash = "sha256:73670ed0375ecc303858e3613f407628dd1fca18fe6ac57b7b7ce66cc7bb006c", size = 117182, upload-time = "2026-01-18T20:55:31.472Z" }, + { url = "https://files.pythonhosted.org/packages/c4/57/17a15549233c37e7fd054c48fe9207492e06b026dbd872b826a0b5f833b6/ormsgpack-1.12.2-cp311-cp311-win_arm64.whl", hash = "sha256:c2be829954434e33601ae5da328cccce3266b098927ca7a30246a0baec2ce7bd", size = 111464, upload-time = "2026-01-18T20:55:38.811Z" }, + { url = "https://files.pythonhosted.org/packages/4c/36/16c4b1921c308a92cef3bf6663226ae283395aa0ff6e154f925c32e91ff5/ormsgpack-1.12.2-cp312-cp312-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:7a29d09b64b9694b588ff2f80e9826bdceb3a2b91523c5beae1fab27d5c940e7", size = 378618, upload-time = "2026-01-18T20:55:50.835Z" }, + { url = "https://files.pythonhosted.org/packages/c0/68/468de634079615abf66ed13bb5c34ff71da237213f29294363beeeca5306/ormsgpack-1.12.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b39e629fd2e1c5b2f46f99778450b59454d1f901bc507963168985e79f09c5d", size = 203186, upload-time = "2026-01-18T20:56:11.163Z" }, + { url = "https://files.pythonhosted.org/packages/73/a9/d756e01961442688b7939bacd87ce13bfad7d26ce24f910f6028178b2cc8/ormsgpack-1.12.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:958dcb270d30a7cb633a45ee62b9444433fa571a752d2ca484efdac07480876e", size = 210738, upload-time = "2026-01-18T20:56:09.181Z" }, + { url = "https://files.pythonhosted.org/packages/7b/ba/795b1036888542c9113269a3f5690ab53dd2258c6fb17676ac4bd44fcf94/ormsgpack-1.12.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58d379d72b6c5e964851c77cfedfb386e474adee4fd39791c2c5d9efb53505cc", size = 212569, upload-time = "2026-01-18T20:56:06.135Z" }, + { url = "https://files.pythonhosted.org/packages/6c/aa/bff73c57497b9e0cba8837c7e4bcab584b1a6dbc91a5dd5526784a5030c8/ormsgpack-1.12.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8463a3fc5f09832e67bdb0e2fda6d518dc4281b133166146a67f54c08496442e", size = 387166, upload-time = "2026-01-18T20:55:36.738Z" }, + { url = "https://files.pythonhosted.org/packages/d3/cf/f8283cba44bcb7b14f97b6274d449db276b3a86589bdb363169b51bc12de/ormsgpack-1.12.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:eddffb77eff0bad4e67547d67a130604e7e2dfbb7b0cde0796045be4090f35c6", size = 482498, upload-time = "2026-01-18T20:55:29.626Z" }, + { url = "https://files.pythonhosted.org/packages/05/be/71e37b852d723dfcbe952ad04178c030df60d6b78eba26bfd14c9a40575e/ormsgpack-1.12.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fcd55e5f6ba0dbce624942adf9f152062135f991a0126064889f68eb850de0dd", size = 425518, upload-time = "2026-01-18T20:55:49.556Z" }, + { url = "https://files.pythonhosted.org/packages/7a/0c/9803aa883d18c7ef197213cd2cbf73ba76472a11fe100fb7dab2884edf48/ormsgpack-1.12.2-cp312-cp312-win_amd64.whl", hash = "sha256:d024b40828f1dde5654faebd0d824f9cc29ad46891f626272dd5bfd7af2333a4", size = 117462, upload-time = "2026-01-18T20:55:47.726Z" }, + { url = "https://files.pythonhosted.org/packages/c8/9e/029e898298b2cc662f10d7a15652a53e3b525b1e7f07e21fef8536a09bb8/ormsgpack-1.12.2-cp312-cp312-win_arm64.whl", hash = "sha256:da538c542bac7d1c8f3f2a937863dba36f013108ce63e55745941dda4b75dbb6", size = 111559, upload-time = "2026-01-18T20:55:54.273Z" }, + { url = "https://files.pythonhosted.org/packages/eb/29/bb0eba3288c0449efbb013e9c6f58aea79cf5cb9ee1921f8865f04c1a9d7/ormsgpack-1.12.2-cp313-cp313-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:5ea60cb5f210b1cfbad8c002948d73447508e629ec375acb82910e3efa8ff355", size = 378661, upload-time = "2026-01-18T20:55:57.765Z" }, + { url = "https://files.pythonhosted.org/packages/6e/31/5efa31346affdac489acade2926989e019e8ca98129658a183e3add7af5e/ormsgpack-1.12.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3601f19afdbea273ed70b06495e5794606a8b690a568d6c996a90d7255e51c1", size = 203194, upload-time = "2026-01-18T20:56:08.252Z" }, + { url = "https://files.pythonhosted.org/packages/eb/56/d0087278beef833187e0167f8527235ebe6f6ffc2a143e9de12a98b1ce87/ormsgpack-1.12.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:29a9f17a3dac6054c0dce7925e0f4995c727f7c41859adf9b5572180f640d172", size = 210778, upload-time = "2026-01-18T20:55:17.694Z" }, + { url = "https://files.pythonhosted.org/packages/1c/a2/072343e1413d9443e5a252a8eb591c2d5b1bffbe5e7bfc78c069361b92eb/ormsgpack-1.12.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39c1bd2092880e413902910388be8715f70b9f15f20779d44e673033a6146f2d", size = 212592, upload-time = "2026-01-18T20:55:32.747Z" }, + { url = "https://files.pythonhosted.org/packages/a2/8b/a0da3b98a91d41187a63b02dda14267eefc2a74fcb43cc2701066cf1510e/ormsgpack-1.12.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:50b7249244382209877deedeee838aef1542f3d0fc28b8fe71ca9d7e1896a0d7", size = 387164, upload-time = "2026-01-18T20:55:40.853Z" }, + { url = "https://files.pythonhosted.org/packages/19/bb/6d226bc4cf9fc20d8eb1d976d027a3f7c3491e8f08289a2e76abe96a65f3/ormsgpack-1.12.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:5af04800d844451cf102a59c74a841324868d3f1625c296a06cc655c542a6685", size = 482516, upload-time = "2026-01-18T20:55:42.033Z" }, + { url = "https://files.pythonhosted.org/packages/fb/f1/bb2c7223398543dedb3dbf8bb93aaa737b387de61c5feaad6f908841b782/ormsgpack-1.12.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cec70477d4371cd524534cd16472d8b9cc187e0e3043a8790545a9a9b296c258", size = 425539, upload-time = "2026-01-18T20:55:24.727Z" }, + { url = "https://files.pythonhosted.org/packages/7b/e8/0fb45f57a2ada1fed374f7494c8cd55e2f88ccd0ab0a669aa3468716bf5f/ormsgpack-1.12.2-cp313-cp313-win_amd64.whl", hash = "sha256:21f4276caca5c03a818041d637e4019bc84f9d6ca8baa5ea03e5cc8bf56140e9", size = 117459, upload-time = "2026-01-18T20:55:56.876Z" }, + { url = "https://files.pythonhosted.org/packages/7a/d4/0cfeea1e960d550a131001a7f38a5132c7ae3ebde4c82af1f364ccc5d904/ormsgpack-1.12.2-cp313-cp313-win_arm64.whl", hash = "sha256:baca4b6773d20a82e36d6fd25f341064244f9f86a13dead95dd7d7f996f51709", size = 111577, upload-time = "2026-01-18T20:55:43.605Z" }, + { url = "https://files.pythonhosted.org/packages/94/16/24d18851334be09c25e87f74307c84950f18c324a4d3c0b41dabdbf19c29/ormsgpack-1.12.2-cp314-cp314-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:bc68dd5915f4acf66ff2010ee47c8906dc1cf07399b16f4089f8c71733f6e36c", size = 378717, upload-time = "2026-01-18T20:55:26.164Z" }, + { url = "https://files.pythonhosted.org/packages/b5/a2/88b9b56f83adae8032ac6a6fa7f080c65b3baf9b6b64fd3d37bd202991d4/ormsgpack-1.12.2-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:46d084427b4132553940070ad95107266656cb646ea9da4975f85cb1a6676553", size = 203183, upload-time = "2026-01-18T20:55:18.815Z" }, + { url = "https://files.pythonhosted.org/packages/a9/80/43e4555963bf602e5bdc79cbc8debd8b6d5456c00d2504df9775e74b450b/ormsgpack-1.12.2-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c010da16235806cf1d7bc4c96bf286bfa91c686853395a299b3ddb49499a3e13", size = 210814, upload-time = "2026-01-18T20:55:33.973Z" }, + { url = "https://files.pythonhosted.org/packages/78/e1/7cfbf28de8bca6efe7e525b329c31277d1b64ce08dcba723971c241a9d60/ormsgpack-1.12.2-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18867233df592c997154ff942a6503df274b5ac1765215bceba7a231bea2745d", size = 212634, upload-time = "2026-01-18T20:55:28.634Z" }, + { url = "https://files.pythonhosted.org/packages/95/f8/30ae5716e88d792a4e879debee195653c26ddd3964c968594ddef0a3cc7e/ormsgpack-1.12.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b009049086ddc6b8f80c76b3955df1aa22a5fbd7673c525cd63bf91f23122ede", size = 387139, upload-time = "2026-01-18T20:56:02.013Z" }, + { url = "https://files.pythonhosted.org/packages/dc/81/aee5b18a3e3a0e52f718b37ab4b8af6fae0d9d6a65103036a90c2a8ffb5d/ormsgpack-1.12.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:1dcc17d92b6390d4f18f937cf0b99054824a7815818012ddca925d6e01c2e49e", size = 482578, upload-time = "2026-01-18T20:55:35.117Z" }, + { url = "https://files.pythonhosted.org/packages/bd/17/71c9ba472d5d45f7546317f467a5fc941929cd68fb32796ca3d13dcbaec2/ormsgpack-1.12.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f04b5e896d510b07c0ad733d7fce2d44b260c5e6c402d272128f8941984e4285", size = 425539, upload-time = "2026-01-18T20:56:04.009Z" }, + { url = "https://files.pythonhosted.org/packages/2e/a6/ac99cd7fe77e822fed5250ff4b86fa66dd4238937dd178d2299f10b69816/ormsgpack-1.12.2-cp314-cp314-win_amd64.whl", hash = "sha256:ae3aba7eed4ca7cb79fd3436eddd29140f17ea254b91604aa1eb19bfcedb990f", size = 117493, upload-time = "2026-01-18T20:56:07.343Z" }, + { url = "https://files.pythonhosted.org/packages/3a/67/339872846a1ae4592535385a1c1f93614138566d7af094200c9c3b45d1e5/ormsgpack-1.12.2-cp314-cp314-win_arm64.whl", hash = "sha256:118576ea6006893aea811b17429bfc561b4778fad393f5f538c84af70b01260c", size = 111579, upload-time = "2026-01-18T20:55:21.161Z" }, + { url = "https://files.pythonhosted.org/packages/49/c2/6feb972dc87285ad381749d3882d8aecbde9f6ecf908dd717d33d66df095/ormsgpack-1.12.2-cp314-cp314t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:7121b3d355d3858781dc40dafe25a32ff8a8242b9d80c692fd548a4b1f7fd3c8", size = 378721, upload-time = "2026-01-18T20:55:52.12Z" }, + { url = "https://files.pythonhosted.org/packages/a3/9a/900a6b9b413e0f8a471cf07830f9cf65939af039a362204b36bd5b581d8b/ormsgpack-1.12.2-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ee766d2e78251b7a63daf1cddfac36a73562d3ddef68cacfb41b2af64698033", size = 203170, upload-time = "2026-01-18T20:55:44.469Z" }, + { url = "https://files.pythonhosted.org/packages/87/4c/27a95466354606b256f24fad464d7c97ab62bce6cc529dd4673e1179b8fb/ormsgpack-1.12.2-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:292410a7d23de9b40444636b9b8f1e4e4b814af7f1ef476e44887e52a123f09d", size = 212816, upload-time = "2026-01-18T20:55:23.501Z" }, + { url = "https://files.pythonhosted.org/packages/73/cd/29cee6007bddf7a834e6cd6f536754c0535fcb939d384f0f37a38b1cddb8/ormsgpack-1.12.2-cp314-cp314t-win_amd64.whl", hash = "sha256:837dd316584485b72ef451d08dd3e96c4a11d12e4963aedb40e08f89685d8ec2", size = 117232, upload-time = "2026-01-18T20:55:45.448Z" }, +] + +[[package]] +name = "packaging" +version = "26.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, +] + +[[package]] +name = "pandas" +version = "2.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "python-dateutil" }, + { name = "pytz" }, + { name = "tzdata" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/33/01/d40b85317f86cf08d853a4f495195c73815fdf205eef3993821720274518/pandas-2.3.3.tar.gz", hash = "sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b", size = 4495223, upload-time = "2025-09-29T23:34:51.853Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/fa/7ac648108144a095b4fb6aa3de1954689f7af60a14cf25583f4960ecb878/pandas-2.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:602b8615ebcc4a0c1751e71840428ddebeb142ec02c786e8ad6b1ce3c8dec523", size = 11578790, upload-time = "2025-09-29T23:18:30.065Z" }, + { url = "https://files.pythonhosted.org/packages/9b/35/74442388c6cf008882d4d4bdfc4109be87e9b8b7ccd097ad1e7f006e2e95/pandas-2.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8fe25fc7b623b0ef6b5009149627e34d2a4657e880948ec3c840e9402e5c1b45", size = 10833831, upload-time = "2025-09-29T23:38:56.071Z" }, + { url = "https://files.pythonhosted.org/packages/fe/e4/de154cbfeee13383ad58d23017da99390b91d73f8c11856f2095e813201b/pandas-2.3.3-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b468d3dad6ff947df92dcb32ede5b7bd41a9b3cceef0a30ed925f6d01fb8fa66", size = 12199267, upload-time = "2025-09-29T23:18:41.627Z" }, + { url = "https://files.pythonhosted.org/packages/bf/c9/63f8d545568d9ab91476b1818b4741f521646cbdd151c6efebf40d6de6f7/pandas-2.3.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b98560e98cb334799c0b07ca7967ac361a47326e9b4e5a7dfb5ab2b1c9d35a1b", size = 12789281, upload-time = "2025-09-29T23:18:56.834Z" }, + { url = "https://files.pythonhosted.org/packages/f2/00/a5ac8c7a0e67fd1a6059e40aa08fa1c52cc00709077d2300e210c3ce0322/pandas-2.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37b5848ba49824e5c30bedb9c830ab9b7751fd049bc7914533e01c65f79791", size = 13240453, upload-time = "2025-09-29T23:19:09.247Z" }, + { url = "https://files.pythonhosted.org/packages/27/4d/5c23a5bc7bd209231618dd9e606ce076272c9bc4f12023a70e03a86b4067/pandas-2.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:db4301b2d1f926ae677a751eb2bd0e8c5f5319c9cb3f88b0becbbb0b07b34151", size = 13890361, upload-time = "2025-09-29T23:19:25.342Z" }, + { url = "https://files.pythonhosted.org/packages/8e/59/712db1d7040520de7a4965df15b774348980e6df45c129b8c64d0dbe74ef/pandas-2.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:f086f6fe114e19d92014a1966f43a3e62285109afe874f067f5abbdcbb10e59c", size = 11348702, upload-time = "2025-09-29T23:19:38.296Z" }, + { url = "https://files.pythonhosted.org/packages/9c/fb/231d89e8637c808b997d172b18e9d4a4bc7bf31296196c260526055d1ea0/pandas-2.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d21f6d74eb1725c2efaa71a2bfc661a0689579b58e9c0ca58a739ff0b002b53", size = 11597846, upload-time = "2025-09-29T23:19:48.856Z" }, + { url = "https://files.pythonhosted.org/packages/5c/bd/bf8064d9cfa214294356c2d6702b716d3cf3bb24be59287a6a21e24cae6b/pandas-2.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3fd2f887589c7aa868e02632612ba39acb0b8948faf5cc58f0850e165bd46f35", size = 10729618, upload-time = "2025-09-29T23:39:08.659Z" }, + { url = "https://files.pythonhosted.org/packages/57/56/cf2dbe1a3f5271370669475ead12ce77c61726ffd19a35546e31aa8edf4e/pandas-2.3.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecaf1e12bdc03c86ad4a7ea848d66c685cb6851d807a26aa245ca3d2017a1908", size = 11737212, upload-time = "2025-09-29T23:19:59.765Z" }, + { url = "https://files.pythonhosted.org/packages/e5/63/cd7d615331b328e287d8233ba9fdf191a9c2d11b6af0c7a59cfcec23de68/pandas-2.3.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b3d11d2fda7eb164ef27ffc14b4fcab16a80e1ce67e9f57e19ec0afaf715ba89", size = 12362693, upload-time = "2025-09-29T23:20:14.098Z" }, + { url = "https://files.pythonhosted.org/packages/a6/de/8b1895b107277d52f2b42d3a6806e69cfef0d5cf1d0ba343470b9d8e0a04/pandas-2.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a68e15f780eddf2b07d242e17a04aa187a7ee12b40b930bfdd78070556550e98", size = 12771002, upload-time = "2025-09-29T23:20:26.76Z" }, + { url = "https://files.pythonhosted.org/packages/87/21/84072af3187a677c5893b170ba2c8fbe450a6ff911234916da889b698220/pandas-2.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:371a4ab48e950033bcf52b6527eccb564f52dc826c02afd9a1bc0ab731bba084", size = 13450971, upload-time = "2025-09-29T23:20:41.344Z" }, + { url = "https://files.pythonhosted.org/packages/86/41/585a168330ff063014880a80d744219dbf1dd7a1c706e75ab3425a987384/pandas-2.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:a16dcec078a01eeef8ee61bf64074b4e524a2a3f4b3be9326420cabe59c4778b", size = 10992722, upload-time = "2025-09-29T23:20:54.139Z" }, + { url = "https://files.pythonhosted.org/packages/cd/4b/18b035ee18f97c1040d94debd8f2e737000ad70ccc8f5513f4eefad75f4b/pandas-2.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:56851a737e3470de7fa88e6131f41281ed440d29a9268dcbf0002da5ac366713", size = 11544671, upload-time = "2025-09-29T23:21:05.024Z" }, + { url = "https://files.pythonhosted.org/packages/31/94/72fac03573102779920099bcac1c3b05975c2cb5f01eac609faf34bed1ca/pandas-2.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdcd9d1167f4885211e401b3036c0c8d9e274eee67ea8d0758a256d60704cfe8", size = 10680807, upload-time = "2025-09-29T23:21:15.979Z" }, + { url = "https://files.pythonhosted.org/packages/16/87/9472cf4a487d848476865321de18cc8c920b8cab98453ab79dbbc98db63a/pandas-2.3.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e32e7cc9af0f1cc15548288a51a3b681cc2a219faa838e995f7dc53dbab1062d", size = 11709872, upload-time = "2025-09-29T23:21:27.165Z" }, + { url = "https://files.pythonhosted.org/packages/15/07/284f757f63f8a8d69ed4472bfd85122bd086e637bf4ed09de572d575a693/pandas-2.3.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:318d77e0e42a628c04dc56bcef4b40de67918f7041c2b061af1da41dcff670ac", size = 12306371, upload-time = "2025-09-29T23:21:40.532Z" }, + { url = "https://files.pythonhosted.org/packages/33/81/a3afc88fca4aa925804a27d2676d22dcd2031c2ebe08aabd0ae55b9ff282/pandas-2.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4e0a175408804d566144e170d0476b15d78458795bb18f1304fb94160cabf40c", size = 12765333, upload-time = "2025-09-29T23:21:55.77Z" }, + { url = "https://files.pythonhosted.org/packages/8d/0f/b4d4ae743a83742f1153464cf1a8ecfafc3ac59722a0b5c8602310cb7158/pandas-2.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2d9ab0fc11822b5eece72ec9587e172f63cff87c00b062f6e37448ced4493", size = 13418120, upload-time = "2025-09-29T23:22:10.109Z" }, + { url = "https://files.pythonhosted.org/packages/4f/c7/e54682c96a895d0c808453269e0b5928a07a127a15704fedb643e9b0a4c8/pandas-2.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:f8bfc0e12dc78f777f323f55c58649591b2cd0c43534e8355c51d3fede5f4dee", size = 10993991, upload-time = "2025-09-29T23:25:04.889Z" }, + { url = "https://files.pythonhosted.org/packages/f9/ca/3f8d4f49740799189e1395812f3bf23b5e8fc7c190827d55a610da72ce55/pandas-2.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:75ea25f9529fdec2d2e93a42c523962261e567d250b0013b16210e1d40d7c2e5", size = 12048227, upload-time = "2025-09-29T23:22:24.343Z" }, + { url = "https://files.pythonhosted.org/packages/0e/5a/f43efec3e8c0cc92c4663ccad372dbdff72b60bdb56b2749f04aa1d07d7e/pandas-2.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:74ecdf1d301e812db96a465a525952f4dde225fdb6d8e5a521d47e1f42041e21", size = 11411056, upload-time = "2025-09-29T23:22:37.762Z" }, + { url = "https://files.pythonhosted.org/packages/46/b1/85331edfc591208c9d1a63a06baa67b21d332e63b7a591a5ba42a10bb507/pandas-2.3.3-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6435cb949cb34ec11cc9860246ccb2fdc9ecd742c12d3304989017d53f039a78", size = 11645189, upload-time = "2025-09-29T23:22:51.688Z" }, + { url = "https://files.pythonhosted.org/packages/44/23/78d645adc35d94d1ac4f2a3c4112ab6f5b8999f4898b8cdf01252f8df4a9/pandas-2.3.3-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:900f47d8f20860de523a1ac881c4c36d65efcb2eb850e6948140fa781736e110", size = 12121912, upload-time = "2025-09-29T23:23:05.042Z" }, + { url = "https://files.pythonhosted.org/packages/53/da/d10013df5e6aaef6b425aa0c32e1fc1f3e431e4bcabd420517dceadce354/pandas-2.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a45c765238e2ed7d7c608fc5bc4a6f88b642f2f01e70c0c23d2224dd21829d86", size = 12712160, upload-time = "2025-09-29T23:23:28.57Z" }, + { url = "https://files.pythonhosted.org/packages/bd/17/e756653095a083d8a37cbd816cb87148debcfcd920129b25f99dd8d04271/pandas-2.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c4fc4c21971a1a9f4bdb4c73978c7f7256caa3e62b323f70d6cb80db583350bc", size = 13199233, upload-time = "2025-09-29T23:24:24.876Z" }, + { url = "https://files.pythonhosted.org/packages/04/fd/74903979833db8390b73b3a8a7d30d146d710bd32703724dd9083950386f/pandas-2.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:ee15f284898e7b246df8087fc82b87b01686f98ee67d85a17b7ab44143a3a9a0", size = 11540635, upload-time = "2025-09-29T23:25:52.486Z" }, + { url = "https://files.pythonhosted.org/packages/21/00/266d6b357ad5e6d3ad55093a7e8efc7dd245f5a842b584db9f30b0f0a287/pandas-2.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1611aedd912e1ff81ff41c745822980c49ce4a7907537be8692c8dbc31924593", size = 10759079, upload-time = "2025-09-29T23:26:33.204Z" }, + { url = "https://files.pythonhosted.org/packages/ca/05/d01ef80a7a3a12b2f8bbf16daba1e17c98a2f039cbc8e2f77a2c5a63d382/pandas-2.3.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d2cefc361461662ac48810cb14365a365ce864afe85ef1f447ff5a1e99ea81c", size = 11814049, upload-time = "2025-09-29T23:27:15.384Z" }, + { url = "https://files.pythonhosted.org/packages/15/b2/0e62f78c0c5ba7e3d2c5945a82456f4fac76c480940f805e0b97fcbc2f65/pandas-2.3.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ee67acbbf05014ea6c763beb097e03cd629961c8a632075eeb34247120abcb4b", size = 12332638, upload-time = "2025-09-29T23:27:51.625Z" }, + { url = "https://files.pythonhosted.org/packages/c5/33/dd70400631b62b9b29c3c93d2feee1d0964dc2bae2e5ad7a6c73a7f25325/pandas-2.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c46467899aaa4da076d5abc11084634e2d197e9460643dd455ac3db5856b24d6", size = 12886834, upload-time = "2025-09-29T23:28:21.289Z" }, + { url = "https://files.pythonhosted.org/packages/d3/18/b5d48f55821228d0d2692b34fd5034bb185e854bdb592e9c640f6290e012/pandas-2.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6253c72c6a1d990a410bc7de641d34053364ef8bcd3126f7e7450125887dffe3", size = 13409925, upload-time = "2025-09-29T23:28:58.261Z" }, + { url = "https://files.pythonhosted.org/packages/a6/3d/124ac75fcd0ecc09b8fdccb0246ef65e35b012030defb0e0eba2cbbbe948/pandas-2.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:1b07204a219b3b7350abaae088f451860223a52cfb8a6c53358e7948735158e5", size = 11109071, upload-time = "2025-09-29T23:32:27.484Z" }, + { url = "https://files.pythonhosted.org/packages/89/9c/0e21c895c38a157e0faa1fb64587a9226d6dd46452cac4532d80c3c4a244/pandas-2.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2462b1a365b6109d275250baaae7b760fd25c726aaca0054649286bcfbb3e8ec", size = 12048504, upload-time = "2025-09-29T23:29:31.47Z" }, + { url = "https://files.pythonhosted.org/packages/d7/82/b69a1c95df796858777b68fbe6a81d37443a33319761d7c652ce77797475/pandas-2.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0242fe9a49aa8b4d78a4fa03acb397a58833ef6199e9aa40a95f027bb3a1b6e7", size = 11410702, upload-time = "2025-09-29T23:29:54.591Z" }, + { url = "https://files.pythonhosted.org/packages/f9/88/702bde3ba0a94b8c73a0181e05144b10f13f29ebfc2150c3a79062a8195d/pandas-2.3.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a21d830e78df0a515db2b3d2f5570610f5e6bd2e27749770e8bb7b524b89b450", size = 11634535, upload-time = "2025-09-29T23:30:21.003Z" }, + { url = "https://files.pythonhosted.org/packages/a4/1e/1bac1a839d12e6a82ec6cb40cda2edde64a2013a66963293696bbf31fbbb/pandas-2.3.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e3ebdb170b5ef78f19bfb71b0dc5dc58775032361fa188e814959b74d726dd5", size = 12121582, upload-time = "2025-09-29T23:30:43.391Z" }, + { url = "https://files.pythonhosted.org/packages/44/91/483de934193e12a3b1d6ae7c8645d083ff88dec75f46e827562f1e4b4da6/pandas-2.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d051c0e065b94b7a3cea50eb1ec32e912cd96dba41647eb24104b6c6c14c5788", size = 12699963, upload-time = "2025-09-29T23:31:10.009Z" }, + { url = "https://files.pythonhosted.org/packages/70/44/5191d2e4026f86a2a109053e194d3ba7a31a2d10a9c2348368c63ed4e85a/pandas-2.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3869faf4bd07b3b66a9f462417d0ca3a9df29a9f6abd5d0d0dbab15dac7abe87", size = 13202175, upload-time = "2025-09-29T23:31:59.173Z" }, +] + +[[package]] +name = "pgvector" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/25/6c/6d8b4b03b958c02fa8687ec6063c49d952a189f8c91ebbe51e877dfab8f7/pgvector-0.4.2.tar.gz", hash = "sha256:322cac0c1dc5d41c9ecf782bd9991b7966685dee3a00bc873631391ed949513a", size = 31354, upload-time = "2025-12-05T01:07:17.87Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/26/6cee8a1ce8c43625ec561aff19df07f9776b7525d9002c86bceb3e0ac970/pgvector-0.4.2-py3-none-any.whl", hash = "sha256:549d45f7a18593783d5eec609ea1684a724ba8405c4cb182a0b2b08aeff04e08", size = 27441, upload-time = "2025-12-05T01:07:16.536Z" }, +] + +[[package]] +name = "pillow" +version = "12.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1c/3d/bb7fca845737cf9d7dbde16ed1843984665ff2e0a518f5db43e77ec540b9/pillow-12.3.0.tar.gz", hash = "sha256:3b8182a766685eaa002637e28b4ec8d6b18819a0c71f579bf0dbaa5830297cce", size = 47025035, upload-time = "2026-07-01T11:56:38.965Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/c8/0a78b0e02d7ac54bc03e5321c9220da52f0c2ea83b21f7c40e7f3169c502/pillow-12.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:00808c5e14ef63ac5161091d242999076604ff74b883423a11e5d7bbb38bf756", size = 5392415, upload-time = "2026-07-01T11:53:47.162Z" }, + { url = "https://files.pythonhosted.org/packages/b2/5b/a02d30018abd97ced9f5a6c63d28597694a00d066516b9c1c6de45859fc9/pillow-12.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:37d6d0a00072fd2948eb22bce7e1475f34569d90c87c59f7a2ec59541b77f7a6", size = 4785266, upload-time = "2026-07-01T11:53:49.079Z" }, + { url = "https://files.pythonhosted.org/packages/c8/98/766667a4be768150a202836acd9fad19c06824ca86c4286d3cf6b274964e/pillow-12.3.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bcb46e2f9feff8d06323983bd83ed00c201fdcab3d74973e7072a889b3979fcd", size = 6263814, upload-time = "2026-07-01T11:53:51.32Z" }, + { url = "https://files.pythonhosted.org/packages/3b/2d/ede717bc1144f63886c21fd349bb95860b0d1a21149ff16f2bb362b612b6/pillow-12.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23d27a3e0307ec2244cc51e7287b919aa68d097504ebe19df4e76a98a3eea5bd", size = 6934408, upload-time = "2026-07-01T11:53:53.487Z" }, + { url = "https://files.pythonhosted.org/packages/a3/48/9c58b685e69d49c31af6c8eb9012055fab7e665785165c84796e2c73ce72/pillow-12.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4f883547d4b7f0495ebe7056b0cc2aea76094e7a4abc8e933540f3271df27d9c", size = 6337160, upload-time = "2026-07-01T11:53:55.457Z" }, + { url = "https://files.pythonhosted.org/packages/ff/fa/dc2a5c0ba6df93f67c31d34b808b7ce440b40cdbf96f0b81cde1d1e6fa93/pillow-12.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:236ff70b9312fb68943c703aa842ca6a758abfa45ac187a5e7c1452e96ef72b5", size = 7045172, upload-time = "2026-07-01T11:53:57.736Z" }, + { url = "https://files.pythonhosted.org/packages/86/a5/444817a4d4c4c2417df00513086ca196f388d8f9ef40c2e4ccd1ad1af54b/pillow-12.3.0-cp311-cp311-win32.whl", hash = "sha256:10e41f0fbf1eec8cfd234b8fe17a4caac7c9d0db4c204d3c173a8f9f6ef3232b", size = 6472232, upload-time = "2026-07-01T11:53:59.767Z" }, + { url = "https://files.pythonhosted.org/packages/63/c6/4bad1b18d132a50b27e1365e1ab163616f7a5bb56d330f66f9d1d9d4f9d4/pillow-12.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:8e95e1385e4998ae9694eeaa4730ba5457ff61185b3a55e2e7bea0880aef452a", size = 7233653, upload-time = "2026-07-01T11:54:02.066Z" }, + { url = "https://files.pythonhosted.org/packages/fd/16/00f91ab7760dc842f5aad55217e80fc4a7067a0604535249bc8a2d6d9870/pillow-12.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:ebaea975e03d3141d9d3a507df75c9b3ec90fa9d2ffd07567b3a978d9d790b26", size = 2568195, upload-time = "2026-07-01T11:54:04.622Z" }, + { url = "https://files.pythonhosted.org/packages/37/bf/fb3ebff8ddcb76aac5a01389251bbbb9519922a9b520d8247c1ca864a25d/pillow-12.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ba09209fbe443b4acccebe845d8a138b89a8f4fbaeedd44953490b5315d5e965", size = 5345969, upload-time = "2026-07-01T11:54:06.397Z" }, + { url = "https://files.pythonhosted.org/packages/d8/66/9a386a92561f402389a4fc70c18838bf6d35eb5eb5c6850b4b2dc64f5048/pillow-12.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffd0c5368496f41b0944be820fcb7a838aa6e623d250b01acf2643939c3f99d7", size = 4780323, upload-time = "2026-07-01T11:54:09.351Z" }, + { url = "https://files.pythonhosted.org/packages/25/27/ac8f99618ffd3dde21db0f4d4b1d2ab00c0880595bfd17df103f7f39fd0c/pillow-12.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d9c7f76c0673154f044e9d78c8655fb4213f6ca31a836df48b40fe5d187717b9", size = 6266838, upload-time = "2026-07-01T11:54:11.71Z" }, + { url = "https://files.pythonhosted.org/packages/84/21/a35af28dcc61f37ed850a2d64c65c701321dfbf25085e469d5559360cbbf/pillow-12.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:78cb2c6865a35ab8ff8b75fd122f6033b92a62c82801110e48ddd6c936a45d91", size = 6940830, upload-time = "2026-07-01T11:54:13.732Z" }, + { url = "https://files.pythonhosted.org/packages/eb/51/8b08617af3ad95e33ce6d7dd2c99ed6c8298f7fb131636303956be022e25/pillow-12.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e491916b378fba47242221bb9ead245211b70d504f495d105d17b14a24b4907c", size = 6344383, upload-time = "2026-07-01T11:54:15.756Z" }, + { url = "https://files.pythonhosted.org/packages/1d/72/cf78ac9780bb93c28328f408973845a309d4d145041665f734572ced1b52/pillow-12.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0dd2064cbc55aaec028ef5fbb60fa47bb6c3e7918e07ff17935284b227a9d2df", size = 7052934, upload-time = "2026-07-01T11:54:17.721Z" }, + { url = "https://files.pythonhosted.org/packages/20/20/25e0f4dc178a6bc0696793720055519a0de89e7661dae886992decbd2f81/pillow-12.3.0-cp312-cp312-win32.whl", hash = "sha256:dbce0b29841537a2fa4a214c2bbf14de3587c9680caa9b4e217568472490b28f", size = 6472684, upload-time = "2026-07-01T11:54:19.839Z" }, + { url = "https://files.pythonhosted.org/packages/45/89/da2f7971a317f83d807fdd4065c0af40208e59e692cc43d315a71a0e96d1/pillow-12.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a2b55dd6b2a4c4b7d87ffa56bdb33fdc5fdb9a462173861a7bc097f17d91cb09", size = 7227137, upload-time = "2026-07-01T11:54:22.025Z" }, + { url = "https://files.pythonhosted.org/packages/de/47/4845a0a6c0dbf1db8456bd9fc791f13c5ced7ced20606d08a0aacfd25b49/pillow-12.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:331b624368d4f1d069149002f25f44bc61c8919ce8ddb3c45bdad8f6e2d89510", size = 2568267, upload-time = "2026-07-01T11:54:24.051Z" }, + { url = "https://files.pythonhosted.org/packages/9d/ac/31fb64e1e7efb5a4b50cd3d92049ba89ac6e4d8d3bb6a74e15048ca3353e/pillow-12.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:21900ce7ba264168cd50defae43cd75d25c833ad4ad6e73ffc5596d12e25ac89", size = 4161684, upload-time = "2026-07-01T11:54:25.934Z" }, + { url = "https://files.pythonhosted.org/packages/87/b4/9805e23d2b4d77842b468513841fda254ee42f0289d25088340e4ff46e2d/pillow-12.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:4e8c2a84d977f50b9daed6eeaf3baef67d00d5d74d932288f02cb94518ee3ace", size = 4255487, upload-time = "2026-07-01T11:54:27.935Z" }, + { url = "https://files.pythonhosted.org/packages/df/39/ecf519435a200c693fe053a6ee4d835b41cf963a4dfc2551c4e637cb2a71/pillow-12.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:ae26d61dfa7a47befdc7572b521024e8745f3d809bd95ca9505a7bba9ef849ec", size = 3696433, upload-time = "2026-07-01T11:54:29.813Z" }, + { url = "https://files.pythonhosted.org/packages/42/92/2fc3ffad878ae8dd5469ec1bc8eb83b71f48e13efdf68f02709003982a32/pillow-12.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7a743ff716f746fc19a9557f60dab1600d4613255f8a7aeb3cdde4db7eb15a66", size = 5345889, upload-time = "2026-07-01T11:54:31.97Z" }, + { url = "https://files.pythonhosted.org/packages/10/76/8803c13605b763d33d156c4678fc77f8443389c0c51c8aef707bb02015f4/pillow-12.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d69141514cc30b774ceea5e3ed3a6635c8d8a96edf664689b890f4089111fb35", size = 4780109, upload-time = "2026-07-01T11:54:34.026Z" }, + { url = "https://files.pythonhosted.org/packages/1f/01/e18aff37cb0b4aac47ac90f016d347a49aca667ef97f190b06ac2aabc928/pillow-12.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f7401aebd7f581d7f83a439d87d474999317ee099218e5ad25d125290990ba65", size = 6263736, upload-time = "2026-07-01T11:54:36.131Z" }, + { url = "https://files.pythonhosted.org/packages/f7/62/de5bdd77d935331f4f802edc11e4d82950f642caad6cb2f949837b8560e2/pillow-12.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0847a763afefb695bc912d7c131e7e0632d4edc1d8698f58ddabec8e46b8b6d3", size = 6937129, upload-time = "2026-07-01T11:54:38.216Z" }, + { url = "https://files.pythonhosted.org/packages/70/4d/105627a13300c5e0df1d174230b32fd1273062c96f7745fd552b945d1e1d/pillow-12.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:571b9fcb07b97ef3a492028fb3d2dc0993ca23a06138b0315286566d29ef718a", size = 6339562, upload-time = "2026-07-01T11:54:40.354Z" }, + { url = "https://files.pythonhosted.org/packages/6b/1d/f13de01a553988ab895ba1c722e06cf3144d4f57656fd5b81b6d881f1179/pillow-12.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:756c768d0c9c2955feb7a56c37ea24aea2e369f8d36a88da270b6a9f19e62b5e", size = 7049439, upload-time = "2026-07-01T11:54:42.489Z" }, + { url = "https://files.pythonhosted.org/packages/c9/f9/066794cca041b969964f779ee5fa66a9498bbf34248ac39c5d7954e4198f/pillow-12.3.0-cp313-cp313-win32.whl", hash = "sha256:a876864214e136f0eb367788dbd7df045f4806801518e2cfe9e13229cfe06d8f", size = 6473287, upload-time = "2026-07-01T11:54:44.9Z" }, + { url = "https://files.pythonhosted.org/packages/a6/9b/7a58e61d62be561da3a356fe2384d4059a6345fc130e23ef1c36a5b81d24/pillow-12.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:1cca606cd25738df4ed873d5ad46bbdb3d83b5cbca291f6b4ff13a4df6b0bbe8", size = 7239691, upload-time = "2026-07-01T11:54:47.141Z" }, + { url = "https://files.pythonhosted.org/packages/aa/b0/c4ed4f0ef8f8fa5ee8351537db6650bb8189f7e118842978dd6589065692/pillow-12.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:b629de27fda84b42cde7edef0d85f13b958b47f6e9bbcbba9b673c562a89bd8b", size = 2568185, upload-time = "2026-07-01T11:54:49.137Z" }, + { url = "https://files.pythonhosted.org/packages/dc/01/001f65b68192f0228cc1dbbc8d2530ab5d58b61037ba0587f946fea607cd/pillow-12.3.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:9cf95fe4d0f84c82d282745d9bb08ad9f926efa00be4697e767b814ce40d4330", size = 4161736, upload-time = "2026-07-01T11:54:51.156Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d2/0219746d0fd16fc8a84498e79452375be3797d3ce4044596ce565164b84f/pillow-12.3.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:8728f216dcdb6e6d555cf971cb34076139ad74b31fc2c14da4fafc741c5f6217", size = 4255435, upload-time = "2026-07-01T11:54:53.414Z" }, + { url = "https://files.pythonhosted.org/packages/c8/02/8d0bc62ef0302318c46ff2a512822d2610e81c7aa46c9b3abe6cbaca5ad0/pillow-12.3.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:a45650e8ce7fafffd731db8550230db6b0d306d181a90b67d3e6bca2f1990930", size = 3696262, upload-time = "2026-07-01T11:54:55.739Z" }, + { url = "https://files.pythonhosted.org/packages/85/e2/73c77d218410b14f5f2d565e8a998d5317b7b9c75368d29985139f7a46f0/pillow-12.3.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ba54cfebe86920a559a7c4d6b9050791c20513650a1952ebe3368c7dc70306f8", size = 5350344, upload-time = "2026-07-01T11:54:57.657Z" }, + { url = "https://files.pythonhosted.org/packages/c7/da/32c752228ae345f489e3a42499d817b6c3996da7e8a3bc7a04fc806b243b/pillow-12.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e158cb00350dc278f3b91551101aa7d12415a66ebf2c91d8d5ac14e56ddd3ad0", size = 4780131, upload-time = "2026-07-01T11:54:59.713Z" }, + { url = "https://files.pythonhosted.org/packages/b1/9d/8b2c807dbef61a5197c047afe99823787eb66f63daf9fb2432f91d6f0462/pillow-12.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9aeb04d6aef139de265b29683e119b638208f88cf73cdd1658aa07221165321", size = 6263757, upload-time = "2026-07-01T11:55:01.778Z" }, + { url = "https://files.pythonhosted.org/packages/5c/44/c85361f65dbe00eea8576ee467c768d25129989efb76e94f205e9ca9bb46/pillow-12.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:251bf95b67017e27b13d82f5b326234ca62d70f9cf4c2b9032de2358a3b12c7b", size = 6936962, upload-time = "2026-07-01T11:55:03.93Z" }, + { url = "https://files.pythonhosted.org/packages/18/7e/e483414b35800b86b6f08dbbc7803fb5cd52c4d6f897f47d53ea2c7e6f65/pillow-12.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fe3cca2e4e8a592be0f269a1ca4835c25199d9f3ce815c8491048f785b0a0198", size = 6339171, upload-time = "2026-07-01T11:55:05.989Z" }, + { url = "https://files.pythonhosted.org/packages/f0/f4/68c491844841ede6bed70189546b3ee9731cf9f2cbad396faff5e1ccba45/pillow-12.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:23aceaa007d6172b02c277f0cd359c79492bbb14f7072b4ede9fbcaf20648130", size = 7048116, upload-time = "2026-07-01T11:55:08.131Z" }, + { url = "https://files.pythonhosted.org/packages/a3/34/77f3f793fed8efc7d243f21b33c5a3f0d1c97ee70346d3db855587e155ff/pillow-12.3.0-cp314-cp314-win32.whl", hash = "sha256:af8d94b0db561cf68b88a267c5c44b49e134f525d0dc2cb7ed413a66bc23559a", size = 6467209, upload-time = "2026-07-01T11:55:10.408Z" }, + { url = "https://files.pythonhosted.org/packages/f1/e0/492879f69d94f91f60fc8cd05ba03650e9520afebb2fb7aa12777d7c7f38/pillow-12.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:fdafc9cce40277e0f7a0feabce0ee50dd2fa1800f3b38015e51296b5e814048d", size = 7237707, upload-time = "2026-07-01T11:55:12.745Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ac/6b11f2875f1c2ac040d84e1bbf9cf22a88038f901ca1037898b280b38365/pillow-12.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:e91206ee562682b51b98ef4b26a6ef48fd84e15fd4c4bc5ec768eb641d206838", size = 2565995, upload-time = "2026-07-01T11:55:14.736Z" }, + { url = "https://files.pythonhosted.org/packages/52/69/c2208e56af9bfc1913afb24020297a691eb1d4ef688474c8a04913f65e04/pillow-12.3.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:164b31cd1a0490ab6efae01aa5df49da7061be0af1b30e035b6e9a1bfe34ee6e", size = 5352503, upload-time = "2026-07-01T11:55:17.076Z" }, + { url = "https://files.pythonhosted.org/packages/07/70/e5686d753e898a45d778ff1718dba8516ead6ab6b95d85fc8c4b70650cf2/pillow-12.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5afb51d599ea772b8365ae807ae557f18bccfe46ab261fd1c2a9ed700fc6eb17", size = 4782956, upload-time = "2026-07-01T11:55:19.448Z" }, + { url = "https://files.pythonhosted.org/packages/d5/37/25c6692f06927ee973ff18c8d9ee98ad0b4d84ee67a09610c2dd1447958e/pillow-12.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3edce1d53195db527e0191f84b71d02022de0540bf43a16ed734ed7537b07385", size = 6322855, upload-time = "2026-07-01T11:55:21.613Z" }, + { url = "https://files.pythonhosted.org/packages/cc/91/420637fcb8f1bc11029e403b4538e6694744428d8246118e45719f944556/pillow-12.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bf16ba1b4d0b6b7c8e534936632270cf70eb00dbe09005bc345b2677b726855c", size = 6989642, upload-time = "2026-07-01T11:55:24.006Z" }, + { url = "https://files.pythonhosted.org/packages/10/08/b94d7811281ccf0d143a1cf768d1c49e1e54af63e7b708ab2ee3eb87face/pillow-12.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:24870b09b224f7ae3c39ed07d10e819d06f8720bc551847b1d623832b5b0e28d", size = 6391281, upload-time = "2026-07-01T11:55:26.252Z" }, + { url = "https://files.pythonhosted.org/packages/d2/87/24233f785f55474dc02ce3e739c5528a77e3a862e9333d1dd7a25cc31f70/pillow-12.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:30f2aa603c41533cc25c05acd0da21636e84a315768feb631c937177db558931", size = 7096716, upload-time = "2026-07-01T11:55:28.318Z" }, + { url = "https://files.pythonhosted.org/packages/23/26/fcb2f6e37175b04f53570b59937867e2b80ee1685e744023153028fc14f9/pillow-12.3.0-cp314-cp314t-win32.whl", hash = "sha256:4b0a7fe987b14c31ebda6083f74f22b561fd3739bc0ac51e019622e3d72668c7", size = 6474125, upload-time = "2026-07-01T11:55:30.956Z" }, + { url = "https://files.pythonhosted.org/packages/90/de/3634abee5f1c9e13c56787b7d5517b0ba8d6de51700b95578cf338349c9f/pillow-12.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:962864dc93511324d51ddbb5b9f8731bf71675b93ca612a07441896f4688fb8c", size = 7242939, upload-time = "2026-07-01T11:55:34.044Z" }, + { url = "https://files.pythonhosted.org/packages/ce/2a/fd13f8eb24de5714a6eb444a3d67e2842c6c576e159a43793adf23051351/pillow-12.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0740a512dc522224c77d9aa5a8d70d8b7d73fb91f2c21125d8d025d3b8990e45", size = 2567506, upload-time = "2026-07-01T11:55:35.988Z" }, + { url = "https://files.pythonhosted.org/packages/5d/dc/8fdce34ec725a33c81c6ba122b904d6b9024e50ea9ac7bede62fab54506c/pillow-12.3.0-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:0feb2e9d6ad6c9e3c06effe9d00f3f1e618a6643273576b016f591e9315a7139", size = 4162063, upload-time = "2026-07-01T11:55:37.941Z" }, + { url = "https://files.pythonhosted.org/packages/76/66/2044b9a63d3b84ff048228dfcb7cd9bf0df983e8470971bf7d4c57b693de/pillow-12.3.0-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:9e881fca225083806662a5c43d627d215f258ff43c890f831966c7d7ba9c7402", size = 4255549, upload-time = "2026-07-01T11:55:40.022Z" }, + { url = "https://files.pythonhosted.org/packages/52/7e/1f67e6f4ece6b582ee4b539decbcc9f848dc245a93ed8cd7338bafef72f1/pillow-12.3.0-cp315-cp315-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:4998562bf62a445225f22e07c896bb04b35b1b1f2eb6d760584c9c51d7a5f78c", size = 3696331, upload-time = "2026-07-01T11:55:41.98Z" }, + { url = "https://files.pythonhosted.org/packages/12/40/d306fc2c8e4d45d7f175c77edca7063be7b86fe7fe6e68f4353bf71d808c/pillow-12.3.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:dc624f6bc473dacdf7ef7eb8678d0d08edf15cd94fad6ae5c7d6cc67a4e4902f", size = 5350370, upload-time = "2026-07-01T11:55:44.028Z" }, + { url = "https://files.pythonhosted.org/packages/dd/44/668fb1437e8ce420f62d6106eb66e44a5971602a4d794615bdf79315d82d/pillow-12.3.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:71d6097b330eea8fd15097780c8e89cb1a8ce7838669f48c5bacd6f663dd4701", size = 4780147, upload-time = "2026-07-01T11:55:46.073Z" }, + { url = "https://files.pythonhosted.org/packages/0c/08/93fa2e70e30a2d81547e481b6ee2bb9522117221fb1e0ce4b5df70967677/pillow-12.3.0-cp315-cp315-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:28ce87c5ab450a9dd970b52e5aca5fe63ed432d18a2eaddd1979a00a1ba24ace", size = 6273659, upload-time = "2026-07-01T11:55:48.264Z" }, + { url = "https://files.pythonhosted.org/packages/f8/6d/043e96ff814fc31a33077e4cba86082167db520c93632afdf2042febbb0c/pillow-12.3.0-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6b02afb9b97f65fbca5f31db6a2a3ba21aa93030225f150fa3f249717e938fb4", size = 6947439, upload-time = "2026-07-01T11:55:50.503Z" }, + { url = "https://files.pythonhosted.org/packages/af/92/ba71d2ee2ac0edf3fa33bd9d5ee9ee080da70b1766f3ca3934f9938ddac9/pillow-12.3.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:1182d52bc2d5e5d7d0949503aa7e36d12f42205dc287e4883f407b1988820d39", size = 6353577, upload-time = "2026-07-01T11:55:52.697Z" }, + { url = "https://files.pythonhosted.org/packages/0f/ce/e63064e2122923ff687c8ad792d0d736a7b3920a56a46982e81a7fdd25d6/pillow-12.3.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:e795b7eb908249c4e43c7c99fac7c2c75dab0c43566e37db472a355f63693d71", size = 7060394, upload-time = "2026-07-01T11:55:55.149Z" }, + { url = "https://files.pythonhosted.org/packages/54/76/a09cc3ccc8d773a7283d34c38bec1708f9e3cc932093cbc4c5e71ac4060b/pillow-12.3.0-cp315-cp315-win32.whl", hash = "sha256:57b3d78c95ba9059768b10e28b813002261d3f3dfc55cc48b0c988f625175827", size = 6467375, upload-time = "2026-07-01T11:55:57.769Z" }, + { url = "https://files.pythonhosted.org/packages/3e/03/1846c49ba3b1d5550392a4bbd06d6fb4578e1cd91a803198b5c90f5f7d53/pillow-12.3.0-cp315-cp315-win_amd64.whl", hash = "sha256:fa4ecea169a355be7a3ade2c783e2ed12f0e40d2c5621cda8b3297faf7fbb9f5", size = 7237048, upload-time = "2026-07-01T11:55:59.975Z" }, + { url = "https://files.pythonhosted.org/packages/fb/bb/89f35dcc79610423f9f195504d7def7f0d1416a711541b42867e25fe3412/pillow-12.3.0-cp315-cp315-win_arm64.whl", hash = "sha256:877c3f311ff35410f690861c4409e7ccbf0cd2f878e50628a28e5a0bb689e658", size = 2566006, upload-time = "2026-07-01T11:56:02.143Z" }, + { url = "https://files.pythonhosted.org/packages/30/88/707027ba09942dfa2c28759b5c222d769290a41c6d20ea60ec250801941f/pillow-12.3.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:e9871b1ffbfa9656b60aeee92ed5136a5742696006fa322b29ea3d8da0ecc9cf", size = 5352509, upload-time = "2026-07-01T11:56:04.2Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6d/00352fa25332c2569cd387851f568cc5a4b75a9adbfb37ac4fbce4c02eec/pillow-12.3.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:53aa02d20d10c3d814d536aa4e5ac9b84ca0ff5a88377963b085ad6822f93e64", size = 4783167, upload-time = "2026-07-01T11:56:06.631Z" }, + { url = "https://files.pythonhosted.org/packages/13/4f/9e049dfa21af7c22427275720e2490267ba8138120add5c4c574deb69782/pillow-12.3.0-cp315-cp315t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:446c34dcc4324b084a53b705127dc15717b22c5e140ae0a3c38349d4efec071e", size = 6329237, upload-time = "2026-07-01T11:56:08.868Z" }, + { url = "https://files.pythonhosted.org/packages/36/16/cf6eeaae8d0fce8dd390a33437cf68c5d5bd73834a2bc6e2f14efda0ab45/pillow-12.3.0-cp315-cp315t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf1845d02ad822a369a49f2bb9345b1614744267682e7a03527dc3bf6eea1777", size = 6997047, upload-time = "2026-07-01T11:56:11.379Z" }, + { url = "https://files.pythonhosted.org/packages/1e/69/dbf769bdd55f48bf5733cac28edc6364ffaa072ec9ba336266e4fe66be55/pillow-12.3.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:186941b6aef820ad110fb01fb06eb925374dc3a21b17e37ec9a53b250c6fe2d1", size = 6400440, upload-time = "2026-07-01T11:56:13.908Z" }, + { url = "https://files.pythonhosted.org/packages/a0/e1/ffc9cfc2eea0d178da8018e18e959301ad9d6bc9f3edb7181e748a474b97/pillow-12.3.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:f13c32a3abd6079a66d9526e18dad9b6d280384d49d7c54040cd57b6424041d9", size = 7105895, upload-time = "2026-07-01T11:56:16.575Z" }, + { url = "https://files.pythonhosted.org/packages/18/f0/a5595c1e8c3ae44b9828cb2f0fa8155e5095ef04d6327b8f61cf44a3df85/pillow-12.3.0-cp315-cp315t-win32.whl", hash = "sha256:1657923d2d45afb66526e5b933e5b3052e6bdea196c90d3abb2424e18c77dae8", size = 6474384, upload-time = "2026-07-01T11:56:18.855Z" }, + { url = "https://files.pythonhosted.org/packages/e4/04/62bcd9f844984c5938d3b05264a61d797a29d3e0812341a8204af70bbdee/pillow-12.3.0-cp315-cp315t-win_amd64.whl", hash = "sha256:8cd2f7bdda092d99c9fc2fb7391354f306d01443d22785d0cbfafa2e2c8bb418", size = 7243537, upload-time = "2026-07-01T11:56:21.214Z" }, + { url = "https://files.pythonhosted.org/packages/3d/68/1f3066acedf37673694a7141381d8f811ae97f30d34413d236abe7d489f1/pillow-12.3.0-cp315-cp315t-win_arm64.whl", hash = "sha256:06ff022112bc9cbf83b60f8e028d94ad87b60621706487e65f673de61610ab59", size = 2567491, upload-time = "2026-07-01T11:56:23.506Z" }, + { url = "https://files.pythonhosted.org/packages/75/18/2e8b40223153ccbc60df07f9e8928dc0c76202aa4e55ae9f53962b6510d6/pillow-12.3.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b3c777e849237620b022f7f297dd67705f9f5cf1685f09f02e46f93e92725468", size = 5302510, upload-time = "2026-07-01T11:56:25.736Z" }, + { url = "https://files.pythonhosted.org/packages/46/3e/51fabf59d5ab801ceab709453d3ab6b180083496579549de4c45ced6528a/pillow-12.3.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:b343699e8308bdc51978310e1c959c584e7869cc8c40780058c87da7781a1e94", size = 4736058, upload-time = "2026-07-01T11:56:28.041Z" }, + { url = "https://files.pythonhosted.org/packages/bf/20/22fe9384b7949e25fb1293bcfc84fb82590ff4ea6b37c95b24d26d793d86/pillow-12.3.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbd139c8447d25dd750ab79ee274cc5e1fe80fc56340ab10b18a195e1b6eca3e", size = 5237776, upload-time = "2026-07-01T11:56:30.263Z" }, + { url = "https://files.pythonhosted.org/packages/08/14/f6ba68107680ffa74b39985f3f30884e41318fbc4250caa423c79b4788bb/pillow-12.3.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e7e480451b9fa137494bccd3a7d69adbe8ac65a87d97be61e11f1b1050a5bac3", size = 5860358, upload-time = "2026-07-01T11:56:32.68Z" }, + { url = "https://files.pythonhosted.org/packages/36/54/0169bc772ec491108b62f644f8ecf1fe5d8ae5ebafde2ee2142210166903/pillow-12.3.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:04f01d28a6aaff387bf842a13be313df23ba0597a44f1a976c9feb3c6ff4711a", size = 7231786, upload-time = "2026-07-01T11:56:35.046Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.9.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/19/56/8d4c30c8a1d07013911a8fdbd8f89440ef9f08d07a1b50ab8ca8be5a20f9/platformdirs-4.9.4.tar.gz", hash = "sha256:1ec356301b7dc906d83f371c8f487070e99d3ccf9e501686456394622a01a934", size = 28737, upload-time = "2026-03-05T18:34:13.271Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/d7/97f7e3a6abb67d8080dd406fd4df842c2be0efaf712d1c899c32a075027c/platformdirs-4.9.4-py3-none-any.whl", hash = "sha256:68a9a4619a666ea6439f2ff250c12a853cd1cbd5158d258bd824a7df6be2f868", size = 21216, upload-time = "2026-03-05T18:34:12.172Z" }, +] + +[[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, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "polyfactory" +version = "3.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "faker" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/85/68/7717bd9e63ed254617a7d3dc9260904fb736d6ea203e58ffddcb186c64e4/polyfactory-3.3.0.tar.gz", hash = "sha256:237258b6ff43edf362ffd1f68086bb796466f786adfa002b0ac256dbf2246e9a", size = 348668, upload-time = "2026-02-22T09:46:28.01Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dd/34/b6f19941adcdaf415b5e8a8d577499f5b6a76b59cbae37f9b125a9ffe9f2/polyfactory-3.3.0-py3-none-any.whl", hash = "sha256:686abcaa761930d3df87b91e95b26b8d8cb9fdbbbe0b03d5f918acff5c72606e", size = 62707, upload-time = "2026-02-22T09:46:25.985Z" }, +] + +[[package]] +name = "preshed" +version = "3.0.12" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cymem" }, + { name = "murmurhash" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bf/34/eb4f5f0f678e152a96e826da867d2f41c4b18a2d589e40e1dd3347219e91/preshed-3.0.12.tar.gz", hash = "sha256:b73f9a8b54ee1d44529cc6018356896cff93d48f755f29c134734d9371c0d685", size = 15027, upload-time = "2025-11-17T13:00:33.621Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/54/d1e02d0a0ea348fb6a769506166e366abfe87ee917c2f11f7139c7acbf10/preshed-3.0.12-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:bc45fda3fd4ae1ae15c37f18f0777cf389ce9184ef8884b39b18894416fd1341", size = 128439, upload-time = "2025-11-17T12:59:21.317Z" }, + { url = "https://files.pythonhosted.org/packages/8c/cb/685ca57ca6e438345b3f6c20226705a0e056a3de399a5bf8a9ee89b3dd2b/preshed-3.0.12-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:75d6e628bc78c022dbb9267242715718f862c3105927732d166076ff009d65de", size = 124544, upload-time = "2025-11-17T12:59:22.944Z" }, + { url = "https://files.pythonhosted.org/packages/f8/07/018fcd3bf298304e1570065cf80601ac16acd29f799578fd47b715dd3ca2/preshed-3.0.12-cp311-cp311-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b901cff5c814facf7a864b0a4c14a16d45fa1379899a585b3fb48ee36a2dccdb", size = 824728, upload-time = "2025-11-17T12:59:24.614Z" }, + { url = "https://files.pythonhosted.org/packages/79/dc/d888b328fcedae530df53396d9fc0006026aa8793fec54d7d34f57f31ff5/preshed-3.0.12-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d1099253bf73dd3c39313280bd5331841f769637b27ddb576ff362c4e7bad298", size = 825969, upload-time = "2025-11-17T12:59:26.493Z" }, + { url = "https://files.pythonhosted.org/packages/21/51/f19933301f42ece1ffef1f7f4c370d09f0351c43c528e66fac24560e44d2/preshed-3.0.12-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1af4a049ffe9d0246e5dc10d6f54820ed064c40e5c3f7b6526127c664008297c", size = 842346, upload-time = "2025-11-17T12:59:28.092Z" }, + { url = "https://files.pythonhosted.org/packages/51/46/025f60fd3d51bf60606a0f8f0cd39c40068b9b5e4d249bca1682e4ff09c3/preshed-3.0.12-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:57159bcedca0cb4c99390f8a6e730f8659fdb663a5a3efcd9c4531e0f54b150e", size = 865504, upload-time = "2025-11-17T12:59:29.648Z" }, + { url = "https://files.pythonhosted.org/packages/88/b5/2e6ee5ab19b03e7983fc5e1850c812fb71dc178dd140d6aca3b45306bdf7/preshed-3.0.12-cp311-cp311-win_amd64.whl", hash = "sha256:8fe9cf1745e203e5aa58b8700436f78da1dcf0f0e2efb0054b467effd9d7d19d", size = 117736, upload-time = "2025-11-17T12:59:30.974Z" }, + { url = "https://files.pythonhosted.org/packages/1e/17/8a0a8f4b01e71b5fb7c5cd4c9fec04d7b852d42f1f9e096b01e7d2b16b17/preshed-3.0.12-cp311-cp311-win_arm64.whl", hash = "sha256:12d880f8786cb6deac34e99b8b07146fb92d22fbca0023208e03325f5944606b", size = 105127, upload-time = "2025-11-17T12:59:32.171Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f7/ff3aca937eeaee19c52c45ddf92979546e52ed0686e58be4bc09c47e7d88/preshed-3.0.12-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2779861f5d69480493519ed123a622a13012d1182126779036b99d9d989bf7e9", size = 129958, upload-time = "2025-11-17T12:59:33.391Z" }, + { url = "https://files.pythonhosted.org/packages/80/24/fd654a9c0f5f3ed1a9b1d8a392f063ae9ca29ad0b462f0732ae0147f7cee/preshed-3.0.12-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffe1fd7d92f51ed34383e20d8b734780c814ca869cfdb7e07f2d31651f90cdf4", size = 124550, upload-time = "2025-11-17T12:59:34.688Z" }, + { url = "https://files.pythonhosted.org/packages/71/49/8271c7f680696f4b0880f44357d2a903d649cb9f6e60a1efc97a203104df/preshed-3.0.12-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:91893404858502cc4e856d338fef3d2a4a552135f79a1041c24eb919817c19db", size = 874987, upload-time = "2025-11-17T12:59:36.062Z" }, + { url = "https://files.pythonhosted.org/packages/a3/a5/ca200187ca1632f1e2c458b72f1bd100fa8b55deecd5d72e1e4ebf09e98c/preshed-3.0.12-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9e06e8f2ba52f183eb9817a616cdebe84a211bb859a2ffbc23f3295d0b189638", size = 866499, upload-time = "2025-11-17T12:59:37.586Z" }, + { url = "https://files.pythonhosted.org/packages/87/a1/943b61f850c44899910c21996cb542d0ef5931744c6d492fdfdd8457e693/preshed-3.0.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bbe8b8a2d4f9af14e8a39ecca524b9de6defc91d8abcc95eb28f42da1c23272c", size = 878064, upload-time = "2025-11-17T12:59:39.651Z" }, + { url = "https://files.pythonhosted.org/packages/3e/75/d7fff7f1fa3763619aa85d6ba70493a5d9c6e6ea7958a6e8c9d3e6e88bbe/preshed-3.0.12-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5d0aaac9c5862f5471fddd0c931dc64d3af2efc5fe3eb48b50765adb571243b9", size = 900540, upload-time = "2025-11-17T12:59:41.384Z" }, + { url = "https://files.pythonhosted.org/packages/e4/12/a2285b78bd097a1e53fb90a1743bc8ce0d35e5b65b6853f3b3c47da398ca/preshed-3.0.12-cp312-cp312-win_amd64.whl", hash = "sha256:0eb8d411afcb1e3b12a0602fb6a0e33140342a732a795251a0ce452aba401dc0", size = 118298, upload-time = "2025-11-17T12:59:42.65Z" }, + { url = "https://files.pythonhosted.org/packages/0b/34/4e8443fe99206a2fcfc63659969a8f8c8ab184836533594a519f3899b1ad/preshed-3.0.12-cp312-cp312-win_arm64.whl", hash = "sha256:dcd3d12903c9f720a39a5c5f1339f7f46e3ab71279fb7a39776768fb840b6077", size = 104746, upload-time = "2025-11-17T12:59:43.934Z" }, + { url = "https://files.pythonhosted.org/packages/1e/36/1d3df6f9f37efc34be4ee3013b3bb698b06f1e372f80959851b54d8efdb2/preshed-3.0.12-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3deb3ab93d50c785eaa7694a8e169eb12d00263a99c91d56511fe943bcbacfb6", size = 128023, upload-time = "2025-11-17T12:59:45.157Z" }, + { url = "https://files.pythonhosted.org/packages/fb/d4/3ca81f42978da1b81aa57b3e9b5193d8093e187787a3b2511d16b30b7c62/preshed-3.0.12-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a604350001238dab63dc14774ee30c257b5d71c7be976dbecd1f1ed37529f60f", size = 122851, upload-time = "2025-11-17T12:59:46.439Z" }, + { url = "https://files.pythonhosted.org/packages/17/73/f388398f8d789f69b510272d144a9186d658423f6d3ecc484c0fe392acec/preshed-3.0.12-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:04fb860a8aab18d2201f06159337eda5568dc5eed218570d960fad79e783c7d0", size = 835926, upload-time = "2025-11-17T12:59:47.882Z" }, + { url = "https://files.pythonhosted.org/packages/35/c6/b7170933451cbc27eaefd57b36f61a5e7e7c8da50ae24f819172e0ca8a4d/preshed-3.0.12-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d0c8fcd44996031c46a0aa6773c7b7aa5ee58c3ee87bc05236dacd5599d35063", size = 827294, upload-time = "2025-11-17T12:59:49.365Z" }, + { url = "https://files.pythonhosted.org/packages/7d/ec/6504730d811c0a375721db2107d31684ec17ee5b7bb3796ecfa41e704d41/preshed-3.0.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b07efc3abd3714ce01cf67db0a2dada6e829ab7def74039d446e49ddb32538c5", size = 838809, upload-time = "2025-11-17T12:59:51.234Z" }, + { url = "https://files.pythonhosted.org/packages/7e/1a/09d13240c1fbadcc0603e2fe029623045a36c88b4b50b02e7fdc89e3b88e/preshed-3.0.12-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f184ef184b76e0e4707bce2395008779e4dfa638456b13b18469c2c1a42903a6", size = 861448, upload-time = "2025-11-17T12:59:52.702Z" }, + { url = "https://files.pythonhosted.org/packages/0d/35/9523160153037ee8337672249449be416ee92236f32602e7dd643767814f/preshed-3.0.12-cp313-cp313-win_amd64.whl", hash = "sha256:ebb3da2dc62ab09e5dc5a00ec38e7f5cdf8741c175714ab4a80773d8ee31b495", size = 117413, upload-time = "2025-11-17T12:59:54.4Z" }, + { url = "https://files.pythonhosted.org/packages/79/eb/4263e6e896753b8e2ffa93035458165850a5ea81d27e8888afdbfd8fa9c4/preshed-3.0.12-cp313-cp313-win_arm64.whl", hash = "sha256:b36a2cf57a5ca6e78e69b569c92ef3bdbfb00e3a14859e201eec6ab3bdc27085", size = 104041, upload-time = "2025-11-17T12:59:55.596Z" }, + { url = "https://files.pythonhosted.org/packages/77/39/7b33910b7ba3db9ce1515c39eb4657232913fb171fe701f792ef50726e60/preshed-3.0.12-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:0d8b458dfbd6cc5007d045fa5638231328e3d6f214fd24ab999cc10f8b9097e5", size = 129211, upload-time = "2025-11-17T12:59:57.182Z" }, + { url = "https://files.pythonhosted.org/packages/32/67/97dceebe0b2b4dd94333e4ec283d38614f92996de615859a952da082890d/preshed-3.0.12-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8e9196e2ea704243a69df203e0c9185eb7c5c58c3632ba1c1e2e2e0aa3aae3b4", size = 123311, upload-time = "2025-11-17T12:59:58.449Z" }, + { url = "https://files.pythonhosted.org/packages/4b/6f/f3772f6eaad1eae787f82ffb65a81a4a1993277eacf5a78a29da34608323/preshed-3.0.12-cp314-cp314-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ffa644e1730012ed435fb9d0c3031ea19a06b11136eff5e9b96b2aa25ec7a5f5", size = 831683, upload-time = "2025-11-17T13:00:00.229Z" }, + { url = "https://files.pythonhosted.org/packages/1a/93/997d39ca61202486dd06c669b4707a5b8e5d0c2c922db9f7744fd6a12096/preshed-3.0.12-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:39e83a16ce53e4a3c41c091fe4fe1c3d28604e63928040da09ba0c5d5a7ca41e", size = 830035, upload-time = "2025-11-17T13:00:02.191Z" }, + { url = "https://files.pythonhosted.org/packages/0a/f2/51bf44e3fdbef08d40a832181842cd9b21b11c3f930989f4ff17e9201e12/preshed-3.0.12-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2ec9bc0baee426303a644c7bf531333d4e7fd06fedf07f62ee09969c208d578d", size = 841728, upload-time = "2025-11-17T13:00:03.643Z" }, + { url = "https://files.pythonhosted.org/packages/d3/b1/2d0e3d23d9f885f7647654d770227eb13e4d892deb9b0ed50b993d63fb18/preshed-3.0.12-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7db058f1b4a3d4d51c4c05b379c6cc9c36fcad00160923cb20ca1c7030581ea4", size = 858860, upload-time = "2025-11-17T13:00:05.185Z" }, + { url = "https://files.pythonhosted.org/packages/e7/57/7c28c7f6f9bfce02796b54f1f6acd2cebb3fa3f14a2dce6fb3c686e3c3a8/preshed-3.0.12-cp314-cp314-win_amd64.whl", hash = "sha256:c87a54a55a2ba98d0c3fd7886295f2825397aff5a7157dcfb89124f6aa2dca41", size = 120325, upload-time = "2025-11-17T13:00:06.428Z" }, + { url = "https://files.pythonhosted.org/packages/33/c3/df235ca679a08e09103983ec17c668f96abe897eadbe18d635972b43d8a9/preshed-3.0.12-cp314-cp314-win_arm64.whl", hash = "sha256:d9c5f10b4b971d71d163c2416b91b7136eae54ef3183b1742bb5993269af1b18", size = 107393, upload-time = "2025-11-17T13:00:07.718Z" }, + { url = "https://files.pythonhosted.org/packages/7e/f1/51a2a72381c8aa3aeb8305d88e720c745048527107e649c01b8d49d6b5bf/preshed-3.0.12-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:2739a9c57efcfa16466fa6e0257d67f0075a9979dc729585fbadaed7383ab449", size = 137703, upload-time = "2025-11-17T13:00:09.001Z" }, + { url = "https://files.pythonhosted.org/packages/3f/ab/f3c3d50647f3af6ce6441c596a4f6fb0216d549432ef51f61c0c1744c9b9/preshed-3.0.12-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:364249656bfbf98b4008fac707f35835580ec56207f7cbecdafef6ebb6a595a6", size = 134889, upload-time = "2025-11-17T13:00:10.29Z" }, + { url = "https://files.pythonhosted.org/packages/54/9a/012dbae28a0b88cd98eae99f87701ffbe3a7d2ea3de345cb8a6a6e1b16cd/preshed-3.0.12-cp314-cp314t-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7f933d509ee762a90f62573aaf189eba94dfee478fca13ea2183b2f8a1bb8f7e", size = 911078, upload-time = "2025-11-17T13:00:11.911Z" }, + { url = "https://files.pythonhosted.org/packages/88/c1/0cd0f8cdb91f63c298320cf946c4b97adfb8e8d3a5d454267410c90fcfaa/preshed-3.0.12-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f73f4e29bf90e58034e6f5fa55e6029f3f2d7c042a7151ed487b49898b0ce887", size = 930506, upload-time = "2025-11-17T13:00:13.375Z" }, + { url = "https://files.pythonhosted.org/packages/20/1a/cab79b3181b2150eeeb0e2541c2bd4e0830e1e068b8836b24ea23610cec3/preshed-3.0.12-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:a61ede0c3d18f1ae128113f785a396351a46f4634beccfdf617b0a86008b154d", size = 900009, upload-time = "2025-11-17T13:00:14.781Z" }, + { url = "https://files.pythonhosted.org/packages/31/9a/5ea9d6d95d5c07ba70166330a43bff7f0a074d0134eb7984eca6551e8c70/preshed-3.0.12-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:eafc08a86f77be78e722d96aa8a3a0aef0e3c7ac2f2ada22186a138e63d4033c", size = 910826, upload-time = "2025-11-17T13:00:16.861Z" }, + { url = "https://files.pythonhosted.org/packages/92/71/39024f9873ff317eac724b2759e94d013703800d970d51de77ccc6afff7e/preshed-3.0.12-cp314-cp314t-win_amd64.whl", hash = "sha256:fadaad54973b8697d5ef008735e150bd729a127b6497fd2cb068842074a6f3a7", size = 141358, upload-time = "2025-11-17T13:00:18.167Z" }, + { url = "https://files.pythonhosted.org/packages/9d/0d/431bb85252119f5d2260417fa7d164619b31eed8f1725b364dc0ade43a8e/preshed-3.0.12-cp314-cp314t-win_arm64.whl", hash = "sha256:c0c0d3b66b4c1e40aa6042721492f7b07fc9679ab6c361bc121aa54a1c3ef63f", size = 114839, upload-time = "2025-11-17T13:00:19.513Z" }, +] + +[[package]] +name = "prompt-toolkit" +version = "3.0.52" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wcwidth" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a1/96/06e01a7b38dce6fe1db213e061a4602dd6032a8a97ef6c1a862537732421/prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855", size = 434198, upload-time = "2025-08-27T15:24:02.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" }, +] + +[[package]] +name = "proto-plus" +version = "1.27.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3a/02/8832cde80e7380c600fbf55090b6ab7b62bd6825dbedde6d6657c15a1f8e/proto_plus-1.27.1.tar.gz", hash = "sha256:912a7460446625b792f6448bade9e55cd4e41e6ac10e27009ef71a7f317fa147", size = 56929, upload-time = "2026-02-02T17:34:49.035Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/79/ac273cbbf744691821a9cca88957257f41afe271637794975ca090b9588b/proto_plus-1.27.1-py3-none-any.whl", hash = "sha256:e4643061f3a4d0de092d62aa4ad09fa4756b2cbb89d4627f3985018216f9fefc", size = 50480, upload-time = "2026-02-02T17:34:47.339Z" }, +] + +[[package]] +name = "protobuf" +version = "6.33.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/66/70/e908e9c5e52ef7c3a6c7902c9dfbb34c7e29c25d2f81ade3856445fd5c94/protobuf-6.33.6.tar.gz", hash = "sha256:a6768d25248312c297558af96a9f9c929e8c4cee0659cb07e780731095f38135", size = 444531, upload-time = "2026-03-18T19:05:00.988Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/9f/2f509339e89cfa6f6a4c4ff50438db9ca488dec341f7e454adad60150b00/protobuf-6.33.6-cp310-abi3-win32.whl", hash = "sha256:7d29d9b65f8afef196f8334e80d6bc1d5d4adedb449971fefd3723824e6e77d3", size = 425739, upload-time = "2026-03-18T19:04:48.373Z" }, + { url = "https://files.pythonhosted.org/packages/76/5d/683efcd4798e0030c1bab27374fd13a89f7c2515fb1f3123efdfaa5eab57/protobuf-6.33.6-cp310-abi3-win_amd64.whl", hash = "sha256:0cd27b587afca21b7cfa59a74dcbd48a50f0a6400cfb59391340ad729d91d326", size = 437089, upload-time = "2026-03-18T19:04:50.381Z" }, + { url = "https://files.pythonhosted.org/packages/5c/01/a3c3ed5cd186f39e7880f8303cc51385a198a81469d53d0fdecf1f64d929/protobuf-6.33.6-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:9720e6961b251bde64edfdab7d500725a2af5280f3f4c87e57c0208376aa8c3a", size = 427737, upload-time = "2026-03-18T19:04:51.866Z" }, + { url = "https://files.pythonhosted.org/packages/ee/90/b3c01fdec7d2f627b3a6884243ba328c1217ed2d978def5c12dc50d328a3/protobuf-6.33.6-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:e2afbae9b8e1825e3529f88d514754e094278bb95eadc0e199751cdd9a2e82a2", size = 324610, upload-time = "2026-03-18T19:04:53.096Z" }, + { url = "https://files.pythonhosted.org/packages/9b/ca/25afc144934014700c52e05103c2421997482d561f3101ff352e1292fb81/protobuf-6.33.6-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:c96c37eec15086b79762ed265d59ab204dabc53056e3443e702d2681f4b39ce3", size = 339381, upload-time = "2026-03-18T19:04:54.616Z" }, + { url = "https://files.pythonhosted.org/packages/16/92/d1e32e3e0d894fe00b15ce28ad4944ab692713f2e7f0a99787405e43533a/protobuf-6.33.6-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:e9db7e292e0ab79dd108d7f1a94fe31601ce1ee3f7b79e0692043423020b0593", size = 323436, upload-time = "2026-03-18T19:04:55.768Z" }, + { url = "https://files.pythonhosted.org/packages/c4/72/02445137af02769918a93807b2b7890047c32bfb9f90371cbc12688819eb/protobuf-6.33.6-py3-none-any.whl", hash = "sha256:77179e006c476e69bf8e8ce866640091ec42e1beb80b213c3900006ecfba6901", size = 170656, upload-time = "2026-03-18T19:04:59.826Z" }, +] + +[[package]] +name = "psutil" +version = "7.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/c6/d1ddf4abb55e93cebc4f2ed8b5d6dbad109ecb8d63748dd2b20ab5e57ebe/psutil-7.2.2.tar.gz", hash = "sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372", size = 493740, upload-time = "2026-01-28T18:14:54.428Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/08/510cbdb69c25a96f4ae523f733cdc963ae654904e8db864c07585ef99875/psutil-7.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2edccc433cbfa046b980b0df0171cd25bcaeb3a68fe9022db0979e7aa74a826b", size = 130595, upload-time = "2026-01-28T18:14:57.293Z" }, + { url = "https://files.pythonhosted.org/packages/d6/f5/97baea3fe7a5a9af7436301f85490905379b1c6f2dd51fe3ecf24b4c5fbf/psutil-7.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e78c8603dcd9a04c7364f1a3e670cea95d51ee865e4efb3556a3a63adef958ea", size = 131082, upload-time = "2026-01-28T18:14:59.732Z" }, + { url = "https://files.pythonhosted.org/packages/37/d6/246513fbf9fa174af531f28412297dd05241d97a75911ac8febefa1a53c6/psutil-7.2.2-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1a571f2330c966c62aeda00dd24620425d4b0cc86881c89861fbc04549e5dc63", size = 181476, upload-time = "2026-01-28T18:15:01.884Z" }, + { url = "https://files.pythonhosted.org/packages/b8/b5/9182c9af3836cca61696dabe4fd1304e17bc56cb62f17439e1154f225dd3/psutil-7.2.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:917e891983ca3c1887b4ef36447b1e0873e70c933afc831c6b6da078ba474312", size = 184062, upload-time = "2026-01-28T18:15:04.436Z" }, + { url = "https://files.pythonhosted.org/packages/16/ba/0756dca669f5a9300d0cbcbfae9a4c30e446dfc7440ffe43ded5724bfd93/psutil-7.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:ab486563df44c17f5173621c7b198955bd6b613fb87c71c161f827d3fb149a9b", size = 139893, upload-time = "2026-01-28T18:15:06.378Z" }, + { url = "https://files.pythonhosted.org/packages/1c/61/8fa0e26f33623b49949346de05ec1ddaad02ed8ba64af45f40a147dbfa97/psutil-7.2.2-cp313-cp313t-win_arm64.whl", hash = "sha256:ae0aefdd8796a7737eccea863f80f81e468a1e4cf14d926bd9b6f5f2d5f90ca9", size = 135589, upload-time = "2026-01-28T18:15:08.03Z" }, + { url = "https://files.pythonhosted.org/packages/81/69/ef179ab5ca24f32acc1dac0c247fd6a13b501fd5534dbae0e05a1c48b66d/psutil-7.2.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:eed63d3b4d62449571547b60578c5b2c4bcccc5387148db46e0c2313dad0ee00", size = 130664, upload-time = "2026-01-28T18:15:09.469Z" }, + { url = "https://files.pythonhosted.org/packages/7b/64/665248b557a236d3fa9efc378d60d95ef56dd0a490c2cd37dafc7660d4a9/psutil-7.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7b6d09433a10592ce39b13d7be5a54fbac1d1228ed29abc880fb23df7cb694c9", size = 131087, upload-time = "2026-01-28T18:15:11.724Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2e/e6782744700d6759ebce3043dcfa661fb61e2fb752b91cdeae9af12c2178/psutil-7.2.2-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fa4ecf83bcdf6e6c8f4449aff98eefb5d0604bf88cb883d7da3d8d2d909546a", size = 182383, upload-time = "2026-01-28T18:15:13.445Z" }, + { url = "https://files.pythonhosted.org/packages/57/49/0a41cefd10cb7505cdc04dab3eacf24c0c2cb158a998b8c7b1d27ee2c1f5/psutil-7.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e452c464a02e7dc7822a05d25db4cde564444a67e58539a00f929c51eddda0cf", size = 185210, upload-time = "2026-01-28T18:15:16.002Z" }, + { url = "https://files.pythonhosted.org/packages/dd/2c/ff9bfb544f283ba5f83ba725a3c5fec6d6b10b8f27ac1dc641c473dc390d/psutil-7.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c7663d4e37f13e884d13994247449e9f8f574bc4655d509c3b95e9ec9e2b9dc1", size = 141228, upload-time = "2026-01-28T18:15:18.385Z" }, + { url = "https://files.pythonhosted.org/packages/f2/fc/f8d9c31db14fcec13748d373e668bc3bed94d9077dbc17fb0eebc073233c/psutil-7.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:11fe5a4f613759764e79c65cf11ebdf26e33d6dd34336f8a337aa2996d71c841", size = 136284, upload-time = "2026-01-28T18:15:19.912Z" }, + { url = "https://files.pythonhosted.org/packages/e7/36/5ee6e05c9bd427237b11b3937ad82bb8ad2752d72c6969314590dd0c2f6e/psutil-7.2.2-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486", size = 129090, upload-time = "2026-01-28T18:15:22.168Z" }, + { url = "https://files.pythonhosted.org/packages/80/c4/f5af4c1ca8c1eeb2e92ccca14ce8effdeec651d5ab6053c589b074eda6e1/psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979", size = 129859, upload-time = "2026-01-28T18:15:23.795Z" }, + { url = "https://files.pythonhosted.org/packages/b5/70/5d8df3b09e25bce090399cf48e452d25c935ab72dad19406c77f4e828045/psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9", size = 155560, upload-time = "2026-01-28T18:15:25.976Z" }, + { url = "https://files.pythonhosted.org/packages/63/65/37648c0c158dc222aba51c089eb3bdfa238e621674dc42d48706e639204f/psutil-7.2.2-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e", size = 156997, upload-time = "2026-01-28T18:15:27.794Z" }, + { url = "https://files.pythonhosted.org/packages/8e/13/125093eadae863ce03c6ffdbae9929430d116a246ef69866dad94da3bfbc/psutil-7.2.2-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8", size = 148972, upload-time = "2026-01-28T18:15:29.342Z" }, + { url = "https://files.pythonhosted.org/packages/04/78/0acd37ca84ce3ddffaa92ef0f571e073faa6d8ff1f0559ab1272188ea2be/psutil-7.2.2-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc", size = 148266, upload-time = "2026-01-28T18:15:31.597Z" }, + { url = "https://files.pythonhosted.org/packages/b4/90/e2159492b5426be0c1fef7acba807a03511f97c5f86b3caeda6ad92351a7/psutil-7.2.2-cp37-abi3-win_amd64.whl", hash = "sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988", size = 137737, upload-time = "2026-01-28T18:15:33.849Z" }, + { url = "https://files.pythonhosted.org/packages/8c/c7/7bb2e321574b10df20cbde462a94e2b71d05f9bbda251ef27d104668306a/psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee", size = 134617, upload-time = "2026-01-28T18:15:36.514Z" }, +] + +[[package]] +name = "psycopg2-binary" +version = "2.9.12" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2a/60/a3624f79acea344c16fbef3a94d28b89a8042ddfb8f3e4ca83f538671409/psycopg2_binary-2.9.12.tar.gz", hash = "sha256:5ac9444edc768c02a6b6a591f070b8aae28ff3a99be57560ac996001580f294c", size = 379686, upload-time = "2026-04-21T09:40:34.304Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d5/19/d4ce60954f3bb9d8e3bc5e5c4d1f2487de2d3851bf2391d54954c9df12a6/psycopg2_binary-2.9.12-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5c8ce6c61bd1b1f6b9c24ee32211599f6166af2c55abb19456090a21fd16554b", size = 3712338, upload-time = "2026-04-20T23:34:03.961Z" }, + { url = "https://files.pythonhosted.org/packages/53/71/c85409ee0d78890f0660eff262e815e7dd2bb741a17611d82e9e8cd9dc5e/psycopg2_binary-2.9.12-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b4a9eaa6e7f4ff91bec10aa3fb296878e75187bced5cc4bafe17dc40915e1326", size = 3822407, upload-time = "2026-04-20T23:34:05.977Z" }, + { url = "https://files.pythonhosted.org/packages/3c/ed/60486c2c7f0d4d1ede2bfb1ed27e2498477ce646bc7f6b2759906303117e/psycopg2_binary-2.9.12-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:c6528cefc8e50fcc6f4a107e27a672058b36cc5736d665476aeb413ba88dbb06", size = 4578425, upload-time = "2026-04-20T23:34:08.246Z" }, + { url = "https://files.pythonhosted.org/packages/0b/b9/656cb03fad9f4f49f2145c334b1126ee75189929ca4e6187d485a2d59951/psycopg2_binary-2.9.12-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e4e184b1fb6072bf05388aa41c697e1b2d01b3473f107e7ec44f186a32cfd0b8", size = 4273709, upload-time = "2026-04-20T23:34:10.974Z" }, + { url = "https://files.pythonhosted.org/packages/99/66/08cf0da0e25cc6fb142c89be45fc8418792858f0c4cbff5e24530ff02cd6/psycopg2_binary-2.9.12-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4766ab678563054d3f1d064a4db19cc4b5f9e3a8d9018592a8285cf200c248f3", size = 5893779, upload-time = "2026-04-20T23:34:13.905Z" }, + { url = "https://files.pythonhosted.org/packages/17/d7/eecd9ce8e146d3721115d82d3836efdbb712187e4590325df549989d18f4/psycopg2_binary-2.9.12-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5a0253224780c978746cb9be55a946bcdaf40fe3519c0f622924cdabdafe2c39", size = 4109308, upload-time = "2026-04-20T23:34:16.761Z" }, + { url = "https://files.pythonhosted.org/packages/b6/2e/b1dc289b362cc8d45697b57eefbd673186f49a4ea0906928988e3affcc98/psycopg2_binary-2.9.12-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0dc9228d47c46bda253d2ecd6bb93b56a9f2d7ad33b684a1fa3622bf74ffe30c", size = 3654405, upload-time = "2026-04-20T23:34:19.303Z" }, + { url = "https://files.pythonhosted.org/packages/eb/e4/4c4aea6473214dbdbd0fbba11aa4691e76dc01722c55724c5951719865ff/psycopg2_binary-2.9.12-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f921f3cd87035ef7df233383011d7a53ea1d346224752c1385f1edfd790ceb6a", size = 3299187, upload-time = "2026-04-20T23:34:21.206Z" }, + { url = "https://files.pythonhosted.org/packages/ba/5d/b03b99986446a4f57b170ed9a2579fb7ff9783ca0fa5226b19db99737fee/psycopg2_binary-2.9.12-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:3d999bd982a723113c1a45b55a7a6a90d64d0ed2278020ed625c490ff7bef96c", size = 3047716, upload-time = "2026-04-20T23:34:23.077Z" }, + { url = "https://files.pythonhosted.org/packages/14/86/382ee4afbd1d97500c9d2862b20c2fdeddf4b7335e984df3fb4309f64108/psycopg2_binary-2.9.12-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:29d4d134bd0ab46ffb04e94aa3c5fa3ef582e9026609165e2f758ff76fc3a3be", size = 3349237, upload-time = "2026-04-20T23:34:25.211Z" }, + { url = "https://files.pythonhosted.org/packages/a8/16/9a57c75ba1eda7165c017342f526810d5f5a12647dde749c99ae9a7141d7/psycopg2_binary-2.9.12-cp311-cp311-win_amd64.whl", hash = "sha256:cb4a1dacdd48077150dc762a9e5ddbf32c256d66cb46f80839391aa458774936", size = 2757036, upload-time = "2026-04-20T23:34:27.77Z" }, + { url = "https://files.pythonhosted.org/packages/e2/9f/ef4ef3c8e15083df90ca35265cfd1a081a2f0cc07bb229c6314c6af817f4/psycopg2_binary-2.9.12-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5cdc05117180c5fa9c40eea8ea559ce64d73824c39d928b7da9fb5f6a9392433", size = 3712459, upload-time = "2026-04-20T23:34:30.549Z" }, + { url = "https://files.pythonhosted.org/packages/b5/01/3dd14e46ba48c1e1a6ec58ee599fa1b5efa00c246d5046cd903d0eeb1af1/psycopg2_binary-2.9.12-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d3227a3bc228c10d21011a99245edca923e4e8bf461857e869a507d9a41fe9f6", size = 3822936, upload-time = "2026-04-20T23:34:32.77Z" }, + { url = "https://files.pythonhosted.org/packages/a6/f7/0640e4901119d8a9f7a1784b927f494e2198e213ceb593753d1f2c8b1b30/psycopg2_binary-2.9.12-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:995ce929eede89db6254b50827e2b7fd61e50d11f0b116b29fffe4a2e53c4580", size = 4578676, upload-time = "2026-04-20T23:34:35.18Z" }, + { url = "https://files.pythonhosted.org/packages/b0/55/44df3965b5f297c50cc0b1b594a31c67d6127a9d133045b8a66611b14dfb/psycopg2_binary-2.9.12-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9fe06d93e72f1c048e731a2e3e7854a5bfaa58fc736068df90b352cefe66f03f", size = 4274917, upload-time = "2026-04-20T23:34:37.982Z" }, + { url = "https://files.pythonhosted.org/packages/b0/4b/74535248b1eac0c9336862e8617c765ac94dac76f9e25d7c4a79588c8907/psycopg2_binary-2.9.12-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40e7b28b63aaf737cb3a1edc3a9bbc9a9f4ad3dcb7152e8c1130e4050eddcb7d", size = 5894843, upload-time = "2026-04-20T23:34:40.856Z" }, + { url = "https://files.pythonhosted.org/packages/f2/ba/f1bf8d2ae71868ad800b661099086ee52bc0f8d9f05be1acd8ebb06757cc/psycopg2_binary-2.9.12-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:89d19a9f7899e8eb0656a2b3a08e0da04c720a06db6e0033eab5928aabe60fa9", size = 4110556, upload-time = "2026-04-20T23:34:44.016Z" }, + { url = "https://files.pythonhosted.org/packages/45/46/c15706c338403b7c420bcc0c2905aad116cc064545686d8bf85f1999ea00/psycopg2_binary-2.9.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:612b965daee295ae2da8f8218ce1d274645dc76ef3f1abf6a0a94fd57eff876d", size = 3655714, upload-time = "2026-04-20T23:34:46.233Z" }, + { url = "https://files.pythonhosted.org/packages/b3/7c/a2d5dc09b64a4564db242a0fe418fde7d33f6f8259dd2c5b9d7def00fb5a/psycopg2_binary-2.9.12-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:b9a339b79d37c1b45f3235265f07cdeb0cb5ad7acd2ac7720a5920989c17c24e", size = 3301154, upload-time = "2026-04-20T23:34:49.528Z" }, + { url = "https://files.pythonhosted.org/packages/c0/e8/cc8c9a4ce71461f9ec548d38cadc41dc184b34c73e6455450775a9334ccd/psycopg2_binary-2.9.12-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:3471336e1acfd9c7fe507b8bad5af9317b6a89294f9eb37bd9a030bb7bebcdc6", size = 3048882, upload-time = "2026-04-20T23:34:51.86Z" }, + { url = "https://files.pythonhosted.org/packages/19/6a/31e2296bc0787c5ab75d3d118e40b239db8151b5192b90b77c72bc9256e9/psycopg2_binary-2.9.12-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7af18183109e23502c8b2ae7f6926c0882766f35b5175a4cd737ad825e4d7a1b", size = 3351298, upload-time = "2026-04-20T23:34:54.124Z" }, + { url = "https://files.pythonhosted.org/packages/5f/a8/75f4e3e11203b590150abed2cf7794b9c9c9f7eceddae955191138b44dde/psycopg2_binary-2.9.12-cp312-cp312-win_amd64.whl", hash = "sha256:398fcd4db988c7d7d3713e2b8e18939776fd3fb447052daae4f24fa39daede4c", size = 2757230, upload-time = "2026-04-20T23:34:56.242Z" }, + { url = "https://files.pythonhosted.org/packages/91/bb/4608c96f970f6e0c56572e87027ef4404f709382a3503e9934526d7ba051/psycopg2_binary-2.9.12-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7c729a73c7b1b84de3582f73cdd27d905121dc2c531f3d9a3c32a3011033b965", size = 3712419, upload-time = "2026-04-20T23:34:58.754Z" }, + { url = "https://files.pythonhosted.org/packages/5e/af/48f76af9d50d61cf390f8cd657b503168b089e2e9298e48465d029fcc713/psycopg2_binary-2.9.12-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4413d0caef93c5cf50b96863df4c2efe8c269bf2267df353225595e7e15e8df7", size = 3822990, upload-time = "2026-04-20T23:35:00.821Z" }, + { url = "https://files.pythonhosted.org/packages/7a/df/aba0f99397cd811d32e06fc0cc781f1f3ce98bc0e729cb423925085d781a/psycopg2_binary-2.9.12-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:4dfcf8e45ebb0c663be34a3442f65e17311f3367089cd4e5e3a3e8e62c978777", size = 4578696, upload-time = "2026-04-20T23:35:03.409Z" }, + { url = "https://files.pythonhosted.org/packages/95/9c/eaa74021ac4e4d5c2f83d82fc6615a63f4fe6c94dc4e94c3990427053f67/psycopg2_binary-2.9.12-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c41321a14dd74aceb6a9a643b9253a334521babfa763fa873e33d89cfa122fb5", size = 4274982, upload-time = "2026-04-20T23:35:05.583Z" }, + { url = "https://files.pythonhosted.org/packages/35/ed/c25deff98bd26187ba48b3b250a3ffc3037c46c5b89362534a15d200e0db/psycopg2_binary-2.9.12-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:83946ba43979ebfdc99a3cd0ee775c89f221df026984ba19d46133d8d75d3cd9", size = 5894867, upload-time = "2026-04-20T23:35:07.902Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/8d0e21ca77373c6c9589e5c4528f6e8f0c08c62cafc76fb0bddb7a2cee22/psycopg2_binary-2.9.12-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:411e85815652d13560fbe731878daa5d92378c4995a22302071890ec3397d019", size = 4110578, upload-time = "2026-04-20T23:35:10.149Z" }, + { url = "https://files.pythonhosted.org/packages/00/fc/f481e2435bd8f742d0123309174aae4165160ad3ef17c1b99c3622c241d2/psycopg2_binary-2.9.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c8ad4c08e00f7679559eaed7aff1edfffc60c086b976f93972f686384a95e2c", size = 3655816, upload-time = "2026-04-20T23:35:12.56Z" }, + { url = "https://files.pythonhosted.org/packages/53/79/b9f46466bdbe9f239c96cde8be33c1aace4842f06013b47b730dc9759187/psycopg2_binary-2.9.12-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:00814e40fa23c2b37ef0a1e3c749d89982c73a9cb5046137f0752a22d432e82f", size = 3301307, upload-time = "2026-04-20T23:35:15.029Z" }, + { url = "https://files.pythonhosted.org/packages/3f/19/7dc003b32fe35024df89b658104f7c8538a8b2dcbde7a4e746ce929742e7/psycopg2_binary-2.9.12-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:98062447aebc20ed20add1f547a364fd0ef8933640d5372ff1873f8deb9b61be", size = 3048968, upload-time = "2026-04-20T23:35:16.757Z" }, + { url = "https://files.pythonhosted.org/packages/91/58/2dbd7db5c604d45f4950d988506aae672a14126ec22998ced5021cbb76bb/psycopg2_binary-2.9.12-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:66a7685d7e548f10fb4ce32fb01a7b7f4aa702134de92a292c7bd9e0d3dbd290", size = 3351369, upload-time = "2026-04-20T23:35:18.933Z" }, + { url = "https://files.pythonhosted.org/packages/42/ee/dee8dcaad07f735824de3d6563bc67119fa6c28257b17977a8d624f02fab/psycopg2_binary-2.9.12-cp313-cp313-win_amd64.whl", hash = "sha256:b6937f5fe4e180aeee87de907a2fa982ded6f7f15d7218f78a083e4e1d68f2a0", size = 2757347, upload-time = "2026-04-20T23:35:21.283Z" }, + { url = "https://files.pythonhosted.org/packages/13/1b/708c0dca874acfad6d65314271859899a79007686f3a1f74e82a2ed4b645/psycopg2_binary-2.9.12-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:6f3b3de8a74ef8db215f22edffb19e32dc6fa41340456de7ec99efdc8a7b3ec2", size = 3712428, upload-time = "2026-04-20T23:35:23.453Z" }, + { url = "https://files.pythonhosted.org/packages/d6/39/ddbea9d4b4de6aca9431b6ed253f530f8a02d3b8f9bcfd0dbfe2b3de6fe4/psycopg2_binary-2.9.12-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1006fb62f0f0bc5ce256a832356c6262e91be43f5e4eb15b5eaf38079464caf2", size = 3823184, upload-time = "2026-04-20T23:35:25.92Z" }, + { url = "https://files.pythonhosted.org/packages/bf/a0/bc2fef74b106fa345567122a0659e6d94512ed7dc0131ec44c9e5aba3725/psycopg2_binary-2.9.12-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:840066105706cd2eb29b9a1c2329620056582a4bf3e8169dec5c447042d0869f", size = 4579157, upload-time = "2026-04-20T23:35:28.542Z" }, + { url = "https://files.pythonhosted.org/packages/57/d7/d4e3b2005d3de607ca4fbb0e8742e248056e52184a6b94ebda3c1c2c329b/psycopg2_binary-2.9.12-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:863f5d12241ebe1c76a72a04c2113b6dc905f90b9cef0e9be0efd994affd9354", size = 4274970, upload-time = "2026-04-20T23:35:30.418Z" }, + { url = "https://files.pythonhosted.org/packages/2e/42/c9853f8db3967fe08bcde11f53d53b85d351750cae726ce001cb68afa9c1/psycopg2_binary-2.9.12-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a99eaab34a9010f1a086b126de467466620a750634d114d20455f3a824aae033", size = 5895175, upload-time = "2026-04-20T23:35:33.584Z" }, + { url = "https://files.pythonhosted.org/packages/eb/fd/b82b5601a97630308bef079f545ffec481bbbc795c2ba5ec416a01d03f60/psycopg2_binary-2.9.12-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ffdd7dc5463ccd61845ac37b7012d0f35a1548df9febe14f8dd549be4a0bc81e", size = 4110658, upload-time = "2026-04-20T23:35:35.638Z" }, + { url = "https://files.pythonhosted.org/packages/62/8c/32ca69b0389ef25dd22937bf9e8fbe2ce27aea20b05ded48c4ce4cb42475/psycopg2_binary-2.9.12-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:54a0dfecab1b48731f934e06139dfe11e24219fb6d0ceb32177cf0375f14c7b5", size = 3656251, upload-time = "2026-04-20T23:35:37.854Z" }, + { url = "https://files.pythonhosted.org/packages/c4/29/96992a2b59e3b9d730fcf9612d0a387305025dc867a9fc490a9e496e074e/psycopg2_binary-2.9.12-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:96937c9c5d891f772430f418a7a8b4691a90c3e6b93cf72b5bd7cad8cbca32a5", size = 3301810, upload-time = "2026-04-20T23:35:39.927Z" }, + { url = "https://files.pythonhosted.org/packages/56/ad/44b06659949b243ae10112cd3b20a197f9bf3e81d5651379b9eb889bfaad/psycopg2_binary-2.9.12-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:77b348775efd4cdab410ec6609d81ccecd1139c90265fa583a7255c8064bc03d", size = 3048977, upload-time = "2026-04-20T23:35:41.806Z" }, + { url = "https://files.pythonhosted.org/packages/1d/f2/10a1bcebadb6aa55e280e1f58975c36a7b560ea525184c7aa4064c466633/psycopg2_binary-2.9.12-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:527e6342b3e44c2f0544f6b8e927d60de7f163f5723b8f1dfa7d2a84298738cd", size = 3351466, upload-time = "2026-04-20T23:35:43.993Z" }, + { url = "https://files.pythonhosted.org/packages/20/be/b732c8418ffa5bcfda002890f5dc4c869fc17db66ff11f53b17cfe44afc0/psycopg2_binary-2.9.12-cp314-cp314-win_amd64.whl", hash = "sha256:f12ae41fcafadb39b2785e64a40f9db05d6de2ac114077457e0e7c597f3af980", size = 2848762, upload-time = "2026-04-20T23:35:46.421Z" }, +] + +[[package]] +name = "pyarrow" +version = "23.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/88/22/134986a4cc224d593c1afde5494d18ff629393d74cc2eddb176669f234a4/pyarrow-23.0.1.tar.gz", hash = "sha256:b8c5873e33440b2bc2f4a79d2b47017a89c5a24116c055625e6f2ee50523f019", size = 1167336, upload-time = "2026-02-16T10:14:12.39Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b0/41/8e6b6ef7e225d4ceead8459427a52afdc23379768f54dd3566014d7618c1/pyarrow-23.0.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:6f0147ee9e0386f519c952cc670eb4a8b05caa594eeffe01af0e25f699e4e9bb", size = 34302230, upload-time = "2026-02-16T10:09:03.859Z" }, + { url = "https://files.pythonhosted.org/packages/bf/4a/1472c00392f521fea03ae93408bf445cc7bfa1ab81683faf9bc188e36629/pyarrow-23.0.1-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:0ae6e17c828455b6265d590100c295193f93cc5675eb0af59e49dbd00d2de350", size = 35850050, upload-time = "2026-02-16T10:09:11.877Z" }, + { url = "https://files.pythonhosted.org/packages/0c/b2/bd1f2f05ded56af7f54d702c8364c9c43cd6abb91b0e9933f3d77b4f4132/pyarrow-23.0.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:fed7020203e9ef273360b9e45be52a2a47d3103caf156a30ace5247ffb51bdbd", size = 44491918, upload-time = "2026-02-16T10:09:18.144Z" }, + { url = "https://files.pythonhosted.org/packages/0b/62/96459ef5b67957eac38a90f541d1c28833d1b367f014a482cb63f3b7cd2d/pyarrow-23.0.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:26d50dee49d741ac0e82185033488d28d35be4d763ae6f321f97d1140eb7a0e9", size = 47562811, upload-time = "2026-02-16T10:09:25.792Z" }, + { url = "https://files.pythonhosted.org/packages/7d/94/1170e235add1f5f45a954e26cd0e906e7e74e23392dcb560de471f7366ec/pyarrow-23.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3c30143b17161310f151f4a2bcfe41b5ff744238c1039338779424e38579d701", size = 48183766, upload-time = "2026-02-16T10:09:34.645Z" }, + { url = "https://files.pythonhosted.org/packages/0e/2d/39a42af4570377b99774cdb47f63ee6c7da7616bd55b3d5001aa18edfe4f/pyarrow-23.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:db2190fa79c80a23fdd29fef4b8992893f024ae7c17d2f5f4db7171fa30c2c78", size = 50607669, upload-time = "2026-02-16T10:09:44.153Z" }, + { url = "https://files.pythonhosted.org/packages/00/ca/db94101c187f3df742133ac837e93b1f269ebdac49427f8310ee40b6a58f/pyarrow-23.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:f00f993a8179e0e1c9713bcc0baf6d6c01326a406a9c23495ec1ba9c9ebf2919", size = 27527698, upload-time = "2026-02-16T10:09:50.263Z" }, + { url = "https://files.pythonhosted.org/packages/9a/4b/4166bb5abbfe6f750fc60ad337c43ecf61340fa52ab386da6e8dbf9e63c4/pyarrow-23.0.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:f4b0dbfa124c0bb161f8b5ebb40f1a680b70279aa0c9901d44a2b5a20806039f", size = 34214575, upload-time = "2026-02-16T10:09:56.225Z" }, + { url = "https://files.pythonhosted.org/packages/e1/da/3f941e3734ac8088ea588b53e860baeddac8323ea40ce22e3d0baa865cc9/pyarrow-23.0.1-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:7707d2b6673f7de054e2e83d59f9e805939038eebe1763fe811ee8fa5c0cd1a7", size = 35832540, upload-time = "2026-02-16T10:10:03.428Z" }, + { url = "https://files.pythonhosted.org/packages/88/7c/3d841c366620e906d54430817531b877ba646310296df42ef697308c2705/pyarrow-23.0.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:86ff03fb9f1a320266e0de855dee4b17da6794c595d207f89bba40d16b5c78b9", size = 44470940, upload-time = "2026-02-16T10:10:10.704Z" }, + { url = "https://files.pythonhosted.org/packages/2c/a5/da83046273d990f256cb79796a190bbf7ec999269705ddc609403f8c6b06/pyarrow-23.0.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:813d99f31275919c383aab17f0f455a04f5a429c261cc411b1e9a8f5e4aaaa05", size = 47586063, upload-time = "2026-02-16T10:10:17.95Z" }, + { url = "https://files.pythonhosted.org/packages/5b/3c/b7d2ebcff47a514f47f9da1e74b7949138c58cfeb108cdd4ee62f43f0cf3/pyarrow-23.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bf5842f960cddd2ef757d486041d57c96483efc295a8c4a0e20e704cbbf39c67", size = 48173045, upload-time = "2026-02-16T10:10:25.363Z" }, + { url = "https://files.pythonhosted.org/packages/43/b2/b40961262213beaba6acfc88698eb773dfce32ecdf34d19291db94c2bd73/pyarrow-23.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:564baf97c858ecc03ec01a41062e8f4698abc3e6e2acd79c01c2e97880a19730", size = 50621741, upload-time = "2026-02-16T10:10:33.477Z" }, + { url = "https://files.pythonhosted.org/packages/f6/70/1fdda42d65b28b078e93d75d371b2185a61da89dda4def8ba6ba41ebdeb4/pyarrow-23.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:07deae7783782ac7250989a7b2ecde9b3c343a643f82e8a4df03d93b633006f0", size = 27620678, upload-time = "2026-02-16T10:10:39.31Z" }, + { url = "https://files.pythonhosted.org/packages/47/10/2cbe4c6f0fb83d2de37249567373d64327a5e4d8db72f486db42875b08f6/pyarrow-23.0.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:6b8fda694640b00e8af3c824f99f789e836720aa8c9379fb435d4c4953a756b8", size = 34210066, upload-time = "2026-02-16T10:10:45.487Z" }, + { url = "https://files.pythonhosted.org/packages/cb/4f/679fa7e84dadbaca7a65f7cdba8d6c83febbd93ca12fa4adf40ba3b6362b/pyarrow-23.0.1-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:8ff51b1addc469b9444b7c6f3548e19dc931b172ab234e995a60aea9f6e6025f", size = 35825526, upload-time = "2026-02-16T10:10:52.266Z" }, + { url = "https://files.pythonhosted.org/packages/f9/63/d2747d930882c9d661e9398eefc54f15696547b8983aaaf11d4a2e8b5426/pyarrow-23.0.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:71c5be5cbf1e1cb6169d2a0980850bccb558ddc9b747b6206435313c47c37677", size = 44473279, upload-time = "2026-02-16T10:11:01.557Z" }, + { url = "https://files.pythonhosted.org/packages/b3/93/10a48b5e238de6d562a411af6467e71e7aedbc9b87f8d3a35f1560ae30fb/pyarrow-23.0.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:9b6f4f17b43bc39d56fec96e53fe89d94bac3eb134137964371b45352d40d0c2", size = 47585798, upload-time = "2026-02-16T10:11:09.401Z" }, + { url = "https://files.pythonhosted.org/packages/5c/20/476943001c54ef078dbf9542280e22741219a184a0632862bca4feccd666/pyarrow-23.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9fc13fc6c403d1337acab46a2c4346ca6c9dec5780c3c697cf8abfd5e19b6b37", size = 48179446, upload-time = "2026-02-16T10:11:17.781Z" }, + { url = "https://files.pythonhosted.org/packages/4b/b6/5dd0c47b335fcd8edba9bfab78ad961bd0fd55ebe53468cc393f45e0be60/pyarrow-23.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5c16ed4f53247fa3ffb12a14d236de4213a4415d127fe9cebed33d51671113e2", size = 50623972, upload-time = "2026-02-16T10:11:26.185Z" }, + { url = "https://files.pythonhosted.org/packages/d5/09/a532297c9591a727d67760e2e756b83905dd89adb365a7f6e9c72578bcc1/pyarrow-23.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:cecfb12ef629cf6be0b1887f9f86463b0dd3dc3195ae6224e74006be4736035a", size = 27540749, upload-time = "2026-02-16T10:12:23.297Z" }, + { url = "https://files.pythonhosted.org/packages/a5/8e/38749c4b1303e6ae76b3c80618f84861ae0c55dd3c2273842ea6f8258233/pyarrow-23.0.1-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:29f7f7419a0e30264ea261fdc0e5fe63ce5a6095003db2945d7cd78df391a7e1", size = 34471544, upload-time = "2026-02-16T10:11:32.535Z" }, + { url = "https://files.pythonhosted.org/packages/a3/73/f237b2bc8c669212f842bcfd842b04fc8d936bfc9d471630569132dc920d/pyarrow-23.0.1-cp313-cp313t-macosx_12_0_x86_64.whl", hash = "sha256:33d648dc25b51fd8055c19e4261e813dfc4d2427f068bcecc8b53d01b81b0500", size = 35949911, upload-time = "2026-02-16T10:11:39.813Z" }, + { url = "https://files.pythonhosted.org/packages/0c/86/b912195eee0903b5611bf596833def7d146ab2d301afeb4b722c57ffc966/pyarrow-23.0.1-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:cd395abf8f91c673dd3589cadc8cc1ee4e8674fa61b2e923c8dd215d9c7d1f41", size = 44520337, upload-time = "2026-02-16T10:11:47.764Z" }, + { url = "https://files.pythonhosted.org/packages/69/c2/f2a717fb824f62d0be952ea724b4f6f9372a17eed6f704b5c9526f12f2f1/pyarrow-23.0.1-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:00be9576d970c31defb5c32eb72ef585bf600ef6d0a82d5eccaae96639cf9d07", size = 47548944, upload-time = "2026-02-16T10:11:56.607Z" }, + { url = "https://files.pythonhosted.org/packages/84/a7/90007d476b9f0dc308e3bc57b832d004f848fd6c0da601375d20d92d1519/pyarrow-23.0.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c2139549494445609f35a5cda4eb94e2c9e4d704ce60a095b342f82460c73a83", size = 48236269, upload-time = "2026-02-16T10:12:04.47Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3f/b16fab3e77709856eb6ac328ce35f57a6d4a18462c7ca5186ef31b45e0e0/pyarrow-23.0.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:7044b442f184d84e2351e5084600f0d7343d6117aabcbc1ac78eb1ae11eb4125", size = 50604794, upload-time = "2026-02-16T10:12:11.797Z" }, + { url = "https://files.pythonhosted.org/packages/e9/a1/22df0620a9fac31d68397a75465c344e83c3dfe521f7612aea33e27ab6c0/pyarrow-23.0.1-cp313-cp313t-win_amd64.whl", hash = "sha256:a35581e856a2fafa12f3f54fce4331862b1cfb0bef5758347a858a4aa9d6bae8", size = 27660642, upload-time = "2026-02-16T10:12:17.746Z" }, + { url = "https://files.pythonhosted.org/packages/8d/1b/6da9a89583ce7b23ac611f183ae4843cd3a6cf54f079549b0e8c14031e73/pyarrow-23.0.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:5df1161da23636a70838099d4aaa65142777185cc0cdba4037a18cee7d8db9ca", size = 34238755, upload-time = "2026-02-16T10:12:32.819Z" }, + { url = "https://files.pythonhosted.org/packages/ae/b5/d58a241fbe324dbaeb8df07be6af8752c846192d78d2272e551098f74e88/pyarrow-23.0.1-cp314-cp314-macosx_12_0_x86_64.whl", hash = "sha256:fa8e51cb04b9f8c9c5ace6bab63af9a1f88d35c0d6cbf53e8c17c098552285e1", size = 35847826, upload-time = "2026-02-16T10:12:38.949Z" }, + { url = "https://files.pythonhosted.org/packages/54/a5/8cbc83f04aba433ca7b331b38f39e000efd9f0c7ce47128670e737542996/pyarrow-23.0.1-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:0b95a3994f015be13c63148fef8832e8a23938128c185ee951c98908a696e0eb", size = 44536859, upload-time = "2026-02-16T10:12:45.467Z" }, + { url = "https://files.pythonhosted.org/packages/36/2e/c0f017c405fcdc252dbccafbe05e36b0d0eb1ea9a958f081e01c6972927f/pyarrow-23.0.1-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:4982d71350b1a6e5cfe1af742c53dfb759b11ce14141870d05d9e540d13bc5d1", size = 47614443, upload-time = "2026-02-16T10:12:55.525Z" }, + { url = "https://files.pythonhosted.org/packages/af/6b/2314a78057912f5627afa13ba43809d9d653e6630859618b0fd81a4e0759/pyarrow-23.0.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c250248f1fe266db627921c89b47b7c06fee0489ad95b04d50353537d74d6886", size = 48232991, upload-time = "2026-02-16T10:13:04.729Z" }, + { url = "https://files.pythonhosted.org/packages/40/f2/1bcb1d3be3460832ef3370d621142216e15a2c7c62602a4ea19ec240dd64/pyarrow-23.0.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5f4763b83c11c16e5f4c15601ba6dfa849e20723b46aa2617cb4bffe8768479f", size = 50645077, upload-time = "2026-02-16T10:13:14.147Z" }, + { url = "https://files.pythonhosted.org/packages/eb/3f/b1da7b61cd66566a4d4c8383d376c606d1c34a906c3f1cb35c479f59d1aa/pyarrow-23.0.1-cp314-cp314-win_amd64.whl", hash = "sha256:3a4c85ef66c134161987c17b147d6bffdca4566f9a4c1d81a0a01cdf08414ea5", size = 28234271, upload-time = "2026-02-16T10:14:09.397Z" }, + { url = "https://files.pythonhosted.org/packages/b5/78/07f67434e910a0f7323269be7bfbf58699bd0c1d080b18a1ab49ba943fe8/pyarrow-23.0.1-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:17cd28e906c18af486a499422740298c52d7c6795344ea5002a7720b4eadf16d", size = 34488692, upload-time = "2026-02-16T10:13:21.541Z" }, + { url = "https://files.pythonhosted.org/packages/50/76/34cf7ae93ece1f740a04910d9f7e80ba166b9b4ab9596a953e9e62b90fe1/pyarrow-23.0.1-cp314-cp314t-macosx_12_0_x86_64.whl", hash = "sha256:76e823d0e86b4fb5e1cf4a58d293036e678b5a4b03539be933d3b31f9406859f", size = 35964383, upload-time = "2026-02-16T10:13:28.63Z" }, + { url = "https://files.pythonhosted.org/packages/46/90/459b827238936d4244214be7c684e1b366a63f8c78c380807ae25ed92199/pyarrow-23.0.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:a62e1899e3078bf65943078b3ad2a6ddcacf2373bc06379aac61b1e548a75814", size = 44538119, upload-time = "2026-02-16T10:13:35.506Z" }, + { url = "https://files.pythonhosted.org/packages/28/a1/93a71ae5881e99d1f9de1d4554a87be37da11cd6b152239fb5bd924fdc64/pyarrow-23.0.1-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:df088e8f640c9fae3b1f495b3c64755c4e719091caf250f3a74d095ddf3c836d", size = 47571199, upload-time = "2026-02-16T10:13:42.504Z" }, + { url = "https://files.pythonhosted.org/packages/88/a3/d2c462d4ef313521eaf2eff04d204ac60775263f1fb08c374b543f79f610/pyarrow-23.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:46718a220d64677c93bc243af1d44b55998255427588e400677d7192671845c7", size = 48259435, upload-time = "2026-02-16T10:13:49.226Z" }, + { url = "https://files.pythonhosted.org/packages/cc/f1/11a544b8c3d38a759eb3fbb022039117fd633e9a7b19e4841cc3da091915/pyarrow-23.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a09f3876e87f48bc2f13583ab551f0379e5dfb83210391e68ace404181a20690", size = 50629149, upload-time = "2026-02-16T10:13:57.238Z" }, + { url = "https://files.pythonhosted.org/packages/50/f2/c0e76a0b451ffdf0cf788932e182758eb7558953f4f27f1aff8e2518b653/pyarrow-23.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:527e8d899f14bd15b740cd5a54ad56b7f98044955373a17179d5956ddb93d9ce", size = 28365807, upload-time = "2026-02-16T10:14:03.892Z" }, +] + +[[package]] +name = "pyasn1" +version = "0.6.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a4/9a/23310166d960def5897e91fe20e5b724601b02a22e84ba1f94232c0b7f67/pyasn1-0.6.4.tar.gz", hash = "sha256:9c447d8431c947fe4c8febc4ed9e760bc29011a5b01e5c74b67025bd9fb8ce81", size = 151262, upload-time = "2026-07-09T01:12:33.988Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/3b/6163796d69c3977d1e4287bea4a6979161cbbdd170ebb430511e8e1999ce/pyasn1-0.6.4-py3-none-any.whl", hash = "sha256:deda9277cfd454080ec40b207fb6df82206a3a2688735233cdcd8d3d565f088b", size = 84410, upload-time = "2026-07-09T01:12:32.92Z" }, +] + +[[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, upload-time = "2025-03-28T02:41:22.17Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259, upload-time = "2025-03-28T02:41:19.028Z" }, +] + +[[package]] +name = "pybind11" +version = "3.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a5/98/9118a0659646f1628c592ef9bb48e0056efa6bf27c951fd12a178e0136fb/pybind11-3.0.2.tar.gz", hash = "sha256:432f01aeb68e361a3a7fc7575c2c7f497595bf640f747acd909ff238dd766e06", size = 577131, upload-time = "2026-02-17T04:46:52.556Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/c5/e98d9c51f3d5300d5e40ad9037dd6b3b60736fd02ab68dcc98c96be7592d/pybind11-3.0.2-py3-none-any.whl", hash = "sha256:f8a6500548919cc33bcd220d5f984688326f574fa97f1107f2f4fdb4c6fb019f", size = 310158, upload-time = "2026-02-17T04:46:49.91Z" }, +] + +[[package]] +name = "pyclipper" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/21/3c06205bb407e1f79b73b7b4dfb3950bd9537c4f625a68ab5cc41177f5bc/pyclipper-1.4.0.tar.gz", hash = "sha256:9882bd889f27da78add4dd6f881d25697efc740bf840274e749988d25496c8e1", size = 54489, upload-time = "2025-12-01T13:15:35.015Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/e3/64cf7794319b088c288706087141e53ac259c7959728303276d18adc665d/pyclipper-1.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:adcb7ca33c5bdc33cd775e8b3eadad54873c802a6d909067a57348bcb96e7a2d", size = 264281, upload-time = "2025-12-01T13:14:55.47Z" }, + { url = "https://files.pythonhosted.org/packages/34/cd/44ec0da0306fa4231e76f1c2cb1fa394d7bde8db490a2b24d55b39865f69/pyclipper-1.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fd24849d2b94ec749ceac7c34c9f01010d23b6e9d9216cf2238b8481160e703d", size = 139426, upload-time = "2025-12-01T13:14:56.683Z" }, + { url = "https://files.pythonhosted.org/packages/ad/88/d8f6c6763ea622fe35e19c75d8b39ed6c55191ddc82d65e06bc46b26cb8e/pyclipper-1.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1b6c8d75ba20c6433c9ea8f1a0feb7e4d3ac06a09ad1fd6d571afc1ddf89b869", size = 989649, upload-time = "2025-12-01T13:14:58.28Z" }, + { url = "https://files.pythonhosted.org/packages/ff/e9/ea7d68c8c4af3842d6515bedcf06418610ad75f111e64c92c1d4785a1513/pyclipper-1.4.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:58e29d7443d7cc0e83ee9daf43927730386629786d00c63b04fe3b53ac01462c", size = 962842, upload-time = "2025-12-01T13:15:00.044Z" }, + { url = "https://files.pythonhosted.org/packages/4e/b7/0b4a272d8726e51ab05e2b933d8cc47f29757fb8212e38b619e170e6015c/pyclipper-1.4.0-cp311-cp311-win32.whl", hash = "sha256:a8d2b5fb75ebe57e21ce61e79a9131edec2622ff23cc665e4d1d1f201bc1a801", size = 95098, upload-time = "2025-12-01T13:15:01.359Z" }, + { url = "https://files.pythonhosted.org/packages/3a/76/4901de2919198bb2bd3d989f86d4a1dff363962425bb2d63e24e6c990042/pyclipper-1.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:e9b973467d9c5fa9bc30bb6ac95f9f4d7c3d9fc25f6cf2d1cc972088e5955c01", size = 104362, upload-time = "2025-12-01T13:15:02.439Z" }, + { url = "https://files.pythonhosted.org/packages/90/1b/7a07b68e0842324d46c03e512d8eefa9cb92ba2a792b3b4ebf939dafcac3/pyclipper-1.4.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:222ac96c8b8281b53d695b9c4fedc674f56d6d4320ad23f1bdbd168f4e316140", size = 265676, upload-time = "2025-12-01T13:15:04.15Z" }, + { url = "https://files.pythonhosted.org/packages/6b/dd/8bd622521c05d04963420ae6664093f154343ed044c53ea260a310c8bb4d/pyclipper-1.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f3672dbafbb458f1b96e1ee3e610d174acb5ace5bd2ed5d1252603bb797f2fc6", size = 140458, upload-time = "2025-12-01T13:15:05.76Z" }, + { url = "https://files.pythonhosted.org/packages/7a/06/6e3e241882bf7d6ab23d9c69ba4e85f1ec47397cbbeee948a16cf75e21ed/pyclipper-1.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d1f807e2b4760a8e5c6d6b4e8c1d71ef52b7fe1946ff088f4fa41e16a881a5ca", size = 978235, upload-time = "2025-12-01T13:15:06.993Z" }, + { url = "https://files.pythonhosted.org/packages/cf/f4/3418c1cd5eea640a9fa2501d4bc0b3655fa8d40145d1a4f484b987990a75/pyclipper-1.4.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce1f83c9a4e10ea3de1959f0ae79e9a5bd41346dff648fee6228ba9eaf8b3872", size = 961388, upload-time = "2025-12-01T13:15:08.467Z" }, + { url = "https://files.pythonhosted.org/packages/ac/94/c85401d24be634af529c962dd5d781f3cb62a67cd769534df2cb3feee97a/pyclipper-1.4.0-cp312-cp312-win32.whl", hash = "sha256:3ef44b64666ebf1cb521a08a60c3e639d21b8c50bfbe846ba7c52a0415e936f4", size = 95169, upload-time = "2025-12-01T13:15:10.098Z" }, + { url = "https://files.pythonhosted.org/packages/97/77/dfea08e3b230b82ee22543c30c35d33d42f846a77f96caf7c504dd54fab1/pyclipper-1.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:d1e5498d883b706a4ce636247f0d830c6eb34a25b843a1b78e2c969754ca9037", size = 104619, upload-time = "2025-12-01T13:15:11.592Z" }, + { url = "https://files.pythonhosted.org/packages/67/d0/cbce7d47de1e6458f66a4d999b091640134deb8f2c7351eab993b70d2e10/pyclipper-1.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d49df13cbb2627ccb13a1046f3ea6ebf7177b5504ec61bdef87d6a704046fd6e", size = 264342, upload-time = "2025-12-01T13:15:12.697Z" }, + { url = "https://files.pythonhosted.org/packages/ce/cc/742b9d69d96c58ac156947e1b56d0f81cbacbccf869e2ac7229f2f86dc4e/pyclipper-1.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:37bfec361e174110cdddffd5ecd070a8064015c99383d95eb692c253951eee8a", size = 139839, upload-time = "2025-12-01T13:15:13.911Z" }, + { url = "https://files.pythonhosted.org/packages/db/48/dd301d62c1529efdd721b47b9e5fb52120fcdac5f4d3405cfc0d2f391414/pyclipper-1.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:14c8bdb5a72004b721c4e6f448d2c2262d74a7f0c9e3076aeff41e564a92389f", size = 972142, upload-time = "2025-12-01T13:15:15.477Z" }, + { url = "https://files.pythonhosted.org/packages/07/bf/d493fd1b33bb090fa64e28c1009374d5d72fa705f9331cd56517c35e381e/pyclipper-1.4.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f2a50c22c3a78cb4e48347ecf06930f61ce98cf9252f2e292aa025471e9d75b1", size = 952789, upload-time = "2025-12-01T13:15:17.042Z" }, + { url = "https://files.pythonhosted.org/packages/cf/88/b95ea8ea21ddca34aa14b123226a81526dd2faaa993f9aabd3ed21231604/pyclipper-1.4.0-cp313-cp313-win32.whl", hash = "sha256:c9a3faa416ff536cee93417a72bfb690d9dea136dc39a39dbbe1e5dadf108c9c", size = 94817, upload-time = "2025-12-01T13:15:18.724Z" }, + { url = "https://files.pythonhosted.org/packages/ba/42/0a1920d276a0e1ca21dc0d13ee9e3ba10a9a8aa3abac76cd5e5a9f503306/pyclipper-1.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:d4b2d7c41086f1927d14947c563dfc7beed2f6c0d9af13c42fe3dcdc20d35832", size = 104007, upload-time = "2025-12-01T13:15:19.763Z" }, + { url = "https://files.pythonhosted.org/packages/1a/20/04d58c70f3ccd404f179f8dd81d16722a05a3bf1ab61445ee64e8218c1f8/pyclipper-1.4.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:7c87480fc91a5af4c1ba310bdb7de2f089a3eeef5fe351a3cedc37da1fcced1c", size = 265167, upload-time = "2025-12-01T13:15:20.844Z" }, + { url = "https://files.pythonhosted.org/packages/bd/2e/a570c1abe69b7260ca0caab4236ce6ea3661193ebf8d1bd7f78ccce537a5/pyclipper-1.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:81d8bb2d1fb9d66dc7ea4373b176bb4b02443a7e328b3b603a73faec088b952e", size = 139966, upload-time = "2025-12-01T13:15:22.036Z" }, + { url = "https://files.pythonhosted.org/packages/e8/3b/e0859e54adabdde8a24a29d3f525ebb31c71ddf2e8d93edce83a3c212ffc/pyclipper-1.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:773c0e06b683214dcfc6711be230c83b03cddebe8a57eae053d4603dd63582f9", size = 968216, upload-time = "2025-12-01T13:15:23.18Z" }, + { url = "https://files.pythonhosted.org/packages/f6/6b/e3c4febf0a35ae643ee579b09988dd931602b5bf311020535fd9e5b7e715/pyclipper-1.4.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9bc45f2463d997848450dbed91c950ca37c6cf27f84a49a5cad4affc0b469e39", size = 954198, upload-time = "2025-12-01T13:15:24.522Z" }, + { url = "https://files.pythonhosted.org/packages/fc/74/728efcee02e12acb486ce9d56fa037120c9bf5b77c54bbdbaa441c14a9d9/pyclipper-1.4.0-cp314-cp314-win32.whl", hash = "sha256:0b8c2105b3b3c44dbe1a266f64309407fe30bf372cf39a94dc8aaa97df00da5b", size = 96951, upload-time = "2025-12-01T13:15:25.79Z" }, + { url = "https://files.pythonhosted.org/packages/e3/d7/7f4354e69f10a917e5c7d5d72a499ef2e10945312f5e72c414a0a08d2ae4/pyclipper-1.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:6c317e182590c88ec0194149995e3d71a979cfef3b246383f4e035f9d4a11826", size = 106782, upload-time = "2025-12-01T13:15:26.945Z" }, + { url = "https://files.pythonhosted.org/packages/63/60/fc32c7a3d7f61a970511ec2857ecd09693d8ac80d560ee7b8e67a6d268c9/pyclipper-1.4.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:f160a2c6ba036f7eaf09f1f10f4fbfa734234af9112fb5187877efed78df9303", size = 269880, upload-time = "2025-12-01T13:15:28.117Z" }, + { url = "https://files.pythonhosted.org/packages/49/df/c4a72d3f62f0ba03ec440c4fff56cd2d674a4334d23c5064cbf41c9583f6/pyclipper-1.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:a9f11ad133257c52c40d50de7a0ca3370a0cdd8e3d11eec0604ad3c34ba549e9", size = 141706, upload-time = "2025-12-01T13:15:30.134Z" }, + { url = "https://files.pythonhosted.org/packages/c5/0b/cf55df03e2175e1e2da9db585241401e0bc98f76bee3791bed39d0313449/pyclipper-1.4.0-cp314-cp314t-win32.whl", hash = "sha256:bbc827b77442c99deaeee26e0e7f172355ddb097a5e126aea206d447d3b26286", size = 105308, upload-time = "2025-12-01T13:15:31.225Z" }, + { url = "https://files.pythonhosted.org/packages/8f/dc/53df8b6931d47080b4fe4ee8450d42e660ee1c5c1556c7ab73359182b769/pyclipper-1.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:29dae3e0296dff8502eeb7639fcfee794b0eec8590ba3563aee28db269da6b04", size = 117608, upload-time = "2025-12-01T13:15:32.69Z" }, + { url = "https://files.pythonhosted.org/packages/18/59/81050abdc9e5b90ffc2c765738c5e40e9abd8e44864aaa737b600f16c562/pyclipper-1.4.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:98b2a40f98e1fc1b29e8a6094072e7e0c7dfe901e573bf6cfc6eb7ce84a7ae87", size = 126495, upload-time = "2025-12-01T13:15:33.743Z" }, +] + +[[package]] +name = "pycparser" +version = "3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, +] + +[[package]] +name = "pydantic" +version = "2.12.5" +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/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.41.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873, upload-time = "2025-11-04T13:39:31.373Z" }, + { url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826, upload-time = "2025-11-04T13:39:32.897Z" }, + { url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869, upload-time = "2025-11-04T13:39:34.469Z" }, + { url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890, upload-time = "2025-11-04T13:39:36.053Z" }, + { url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740, upload-time = "2025-11-04T13:39:37.753Z" }, + { url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021, upload-time = "2025-11-04T13:39:40.94Z" }, + { url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378, upload-time = "2025-11-04T13:39:42.523Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761, upload-time = "2025-11-04T13:39:44.553Z" }, + { url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303, upload-time = "2025-11-04T13:39:46.238Z" }, + { url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355, upload-time = "2025-11-04T13:39:48.002Z" }, + { url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875, upload-time = "2025-11-04T13:39:49.705Z" }, + { url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549, upload-time = "2025-11-04T13:39:51.842Z" }, + { url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305, upload-time = "2025-11-04T13:39:53.485Z" }, + { url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902, upload-time = "2025-11-04T13:39:56.488Z" }, + { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" }, + { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" }, + { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" }, + { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" }, + { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" }, + { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" }, + { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" }, + { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" }, + { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" }, + { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" }, + { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" }, + { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" }, + { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" }, + { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" }, + { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" }, + { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" }, + { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" }, + { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" }, + { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" }, + { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" }, + { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" }, + { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" }, + { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" }, + { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" }, + { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" }, + { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" }, + { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" }, + { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" }, + { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" }, + { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" }, + { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" }, + { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" }, + { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" }, + { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" }, + { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" }, + { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" }, + { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" }, + { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" }, + { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" }, + { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" }, + { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" }, + { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" }, + { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" }, + { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, + { url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441, upload-time = "2025-11-04T13:42:39.557Z" }, + { url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291, upload-time = "2025-11-04T13:42:42.169Z" }, + { url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632, upload-time = "2025-11-04T13:42:44.564Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905, upload-time = "2025-11-04T13:42:47.156Z" }, + { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" }, + { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" }, + { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" }, + { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, + { url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980, upload-time = "2025-11-04T13:43:25.97Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865, upload-time = "2025-11-04T13:43:28.763Z" }, + { url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256, upload-time = "2025-11-04T13:43:31.71Z" }, + { url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762, upload-time = "2025-11-04T13:43:34.744Z" }, + { url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141, upload-time = "2025-11-04T13:43:37.701Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317, upload-time = "2025-11-04T13:43:40.406Z" }, + { url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992, upload-time = "2025-11-04T13:43:43.602Z" }, + { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" }, +] + +[[package]] +name = "pydantic-settings" +version = "2.15.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/68/ca/31c57507b13119d7d3cfa1576dad2911a4861e3be07b579395f4e9d393f9/pydantic_settings-2.15.0.tar.gz", hash = "sha256:694b793e84f766ba76a90ebdefc01d0a9a045dab0382bee70393da93712ad117", size = 261253, upload-time = "2026-08-07T09:24:57.419Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/30/a4/2bffa9f8e804325a09867f0e9d30795c80ea9f8d62560bd1b6ad6220eb2f/pydantic_settings-2.15.0-py3-none-any.whl", hash = "sha256:0ba092c291c94baceb5eff768aa0d56400a457585bc0175925a5a5510303da42", size = 69413, upload-time = "2026-08-07T09:24:55.839Z" }, +] + +[[package]] +name = "pygments" +version = "2.21.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/49/2e/ced460408999b33da6b31b0021b0f37d329e202d4169aeb164493778f25b/pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c", size = 5005329, upload-time = "2026-08-17T08:02:48.824Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", size = 1250147, upload-time = "2026-08-17T08:02:44.912Z" }, +] + +[[package]] +name = "pyjwt" +version = "2.13.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3b/81/58d0ac84e1ef3a3843791d6954d94c0b33d526c75eeb1efbce9d0a4c4077/pyjwt-2.13.0.tar.gz", hash = "sha256:41571c89ca91598c79e8ef18a2d07367d4810fbbd6f637794879baf1b7703423", size = 107515, upload-time = "2026-05-21T19:54:36.618Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/5e/ecf12fdb62546d64385c158514e9b2b671f7832108ef2ecd2020ce0af2d1/pyjwt-2.13.0-py3-none-any.whl", hash = "sha256:66adcc2aff09b3f1bbd95fc1e1577df8ac8723c978552fd43304c8a290ac5728", size = 31274, upload-time = "2026-05-21T19:54:35.362Z" }, +] + +[package.optional-dependencies] +crypto = [ + { name = "cryptography" }, +] + +[[package]] +name = "pylatexenc" +version = "2.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5d/ab/34ec41718af73c00119d0351b7a2531d2ebddb51833a36448fc7b862be60/pylatexenc-2.10.tar.gz", hash = "sha256:3dd8fd84eb46dc30bee1e23eaab8d8fb5a7f507347b23e5f38ad9675c84f40d3", size = 162597, upload-time = "2021-04-06T07:56:07.854Z" } + +[[package]] +name = "pylint" +version = "4.0.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "astroid" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "dill" }, + { name = "isort" }, + { name = "mccabe" }, + { name = "platformdirs" }, + { name = "tomlkit" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/b6/74d9a8a68b8067efce8d07707fe6a236324ee1e7808d2eb3646ec8517c7d/pylint-4.0.5.tar.gz", hash = "sha256:8cd6a618df75deb013bd7eb98327a95f02a6fb839205a6bbf5456ef96afb317c", size = 1572474, upload-time = "2026-02-20T09:07:33.621Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d5/6f/9ac2548e290764781f9e7e2aaf0685b086379dabfb29ca38536985471eaf/pylint-4.0.5-py3-none-any.whl", hash = "sha256:00f51c9b14a3b3ae08cff6b2cdd43f28165c78b165b628692e428fb1f8dc2cf2", size = 536694, upload-time = "2026-02-20T09:07:31.028Z" }, +] + +[[package]] +name = "pymilvus" +version = "2.6.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cachetools" }, + { name = "grpcio" }, + { name = "orjson" }, + { name = "pandas" }, + { name = "protobuf" }, + { name = "python-dotenv" }, + { name = "setuptools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/94/0c/92adff800a04cd3e9b3f17c06fa972c8d590846b1e0bac0ccf39e054b596/pymilvus-2.6.9.tar.gz", hash = "sha256:c53a3d84ff15814e251be13edda70a98a1c8a6090d7597a908387cbb94a9504a", size = 1493560, upload-time = "2026-02-10T11:01:27.415Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/56/ab7f0a5aba6fc06dc210a059d6f6d2ee1f3371d40e2b4366a409576554b8/pymilvus-2.6.9-py3-none-any.whl", hash = "sha256:3e14e8072f6429dcd79d52a24dc021c594cb80841ddd76cb974bc539d1f4cdda", size = 301225, upload-time = "2026-02-10T11:01:25.796Z" }, +] + +[[package]] +name = "pymongo" +version = "4.16.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dnspython" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/65/9c/a4895c4b785fc9865a84a56e14b5bd21ca75aadc3dab79c14187cdca189b/pymongo-4.16.0.tar.gz", hash = "sha256:8ba8405065f6e258a6f872fe62d797a28f383a12178c7153c01ed04e845c600c", size = 2495323, upload-time = "2026-01-07T18:05:48.107Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/3a/907414a763c4270b581ad6d960d0c6221b74a70eda216a1fdd8fa82ba89f/pymongo-4.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6f2077ec24e2f1248f9cac7b9a2dfb894e50cc7939fcebfb1759f99304caabef", size = 862561, upload-time = "2026-01-07T18:04:00.628Z" }, + { url = "https://files.pythonhosted.org/packages/8c/58/787d8225dd65cb2383c447346ea5e200ecfde89962d531111521e3b53018/pymongo-4.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4d4f7ba040f72a9f43a44059872af5a8c8c660aa5d7f90d5344f2ed1c3c02721", size = 862923, upload-time = "2026-01-07T18:04:02.213Z" }, + { url = "https://files.pythonhosted.org/packages/5d/a7/cc2865aae32bc77ade7b35f957a58df52680d7f8506f93c6edbf458e5738/pymongo-4.16.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8a0f73af1ea56c422b2dcfc0437459148a799ef4231c6aee189d2d4c59d6728f", size = 1426779, upload-time = "2026-01-07T18:04:03.942Z" }, + { url = "https://files.pythonhosted.org/packages/81/25/3e96eb7998eec05382174da2fefc58d28613f46bbdf821045539d0ed60ab/pymongo-4.16.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa30cd16ddd2f216d07ba01d9635c873e97ddb041c61cf0847254edc37d1c60e", size = 1454207, upload-time = "2026-01-07T18:04:05.387Z" }, + { url = "https://files.pythonhosted.org/packages/86/7b/8e817a7df8c5d565d39dd4ca417a5e0ef46cc5cc19aea9405f403fec6449/pymongo-4.16.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1d638b0b1b294d95d0fdc73688a3b61e05cc4188872818cd240d51460ccabcb5", size = 1511654, upload-time = "2026-01-07T18:04:08.458Z" }, + { url = "https://files.pythonhosted.org/packages/39/7a/50c4d075ccefcd281cdcfccc5494caa5665b096b85e65a5d6afabb80e09e/pymongo-4.16.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:21d02cc10a158daa20cb040985e280e7e439832fc6b7857bff3d53ef6914ad50", size = 1496794, upload-time = "2026-01-07T18:04:10.355Z" }, + { url = "https://files.pythonhosted.org/packages/0f/cd/ebdc1aaca5deeaf47310c369ef4083e8550e04e7bf7e3752cfb7d95fcdb8/pymongo-4.16.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4fbb8d3552c2ad99d9e236003c0b5f96d5f05e29386ba7abae73949bfebc13dd", size = 1448371, upload-time = "2026-01-07T18:04:11.76Z" }, + { url = "https://files.pythonhosted.org/packages/3d/c9/50fdd78c37f68ea49d590c027c96919fbccfd98f3a4cb39f84f79970bd37/pymongo-4.16.0-cp311-cp311-win32.whl", hash = "sha256:be1099a8295b1a722d03fb7b48be895d30f4301419a583dcf50e9045968a041c", size = 841024, upload-time = "2026-01-07T18:04:13.522Z" }, + { url = "https://files.pythonhosted.org/packages/4a/dd/a3aa1ade0cf9980744db703570afac70a62c85b432c391dea0577f6da7bb/pymongo-4.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:61567f712bda04c7545a037e3284b4367cad8d29b3dec84b4bf3b2147020a75b", size = 855838, upload-time = "2026-01-07T18:04:14.923Z" }, + { url = "https://files.pythonhosted.org/packages/bf/10/9ad82593ccb895e8722e4884bad4c5ce5e8ff6683b740d7823a6c2bcfacf/pymongo-4.16.0-cp311-cp311-win_arm64.whl", hash = "sha256:c53338613043038005bf2e41a2fafa08d29cdbc0ce80891b5366c819456c1ae9", size = 845007, upload-time = "2026-01-07T18:04:17.099Z" }, + { url = "https://files.pythonhosted.org/packages/6a/03/6dd7c53cbde98de469a3e6fb893af896dca644c476beb0f0c6342bcc368b/pymongo-4.16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bd4911c40a43a821dfd93038ac824b756b6e703e26e951718522d29f6eb166a8", size = 917619, upload-time = "2026-01-07T18:04:19.173Z" }, + { url = "https://files.pythonhosted.org/packages/73/e1/328915f2734ea1f355dc9b0e98505ff670f5fab8be5e951d6ed70971c6aa/pymongo-4.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:25a6b03a68f9907ea6ec8bc7cf4c58a1b51a18e23394f962a6402f8e46d41211", size = 917364, upload-time = "2026-01-07T18:04:20.861Z" }, + { url = "https://files.pythonhosted.org/packages/41/fe/4769874dd9812a1bc2880a9785e61eba5340da966af888dd430392790ae0/pymongo-4.16.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:91ac0cb0fe2bf17616c2039dac88d7c9a5088f5cb5829b27c9d250e053664d31", size = 1686901, upload-time = "2026-01-07T18:04:22.219Z" }, + { url = "https://files.pythonhosted.org/packages/fa/8d/15707b9669fdc517bbc552ac60da7124dafe7ac1552819b51e97ed4038b4/pymongo-4.16.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cf0ec79e8ca7077f455d14d915d629385153b6a11abc0b93283ed73a8013e376", size = 1723034, upload-time = "2026-01-07T18:04:24.055Z" }, + { url = "https://files.pythonhosted.org/packages/5b/af/3d5d16ff11d447d40c1472da1b366a31c7380d7ea2922a449c7f7f495567/pymongo-4.16.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2d0082631a7510318befc2b4fdab140481eb4b9dd62d9245e042157085da2a70", size = 1797161, upload-time = "2026-01-07T18:04:25.964Z" }, + { url = "https://files.pythonhosted.org/packages/fb/04/725ab8664eeec73ec125b5a873448d80f5d8cf2750aaaf804cbc538a50a5/pymongo-4.16.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:85dc2f3444c346ea019a371e321ac868a4fab513b7a55fe368f0cc78de8177cc", size = 1780938, upload-time = "2026-01-07T18:04:28.745Z" }, + { url = "https://files.pythonhosted.org/packages/22/50/dd7e9095e1ca35f93c3c844c92eb6eb0bc491caeb2c9bff3b32fe3c9b18f/pymongo-4.16.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dabbf3c14de75a20cc3c30bf0c6527157224a93dfb605838eabb1a2ee3be008d", size = 1714342, upload-time = "2026-01-07T18:04:30.331Z" }, + { url = "https://files.pythonhosted.org/packages/03/c9/542776987d5c31ae8e93e92680ea2b6e5a2295f398b25756234cabf38a39/pymongo-4.16.0-cp312-cp312-win32.whl", hash = "sha256:60307bb91e0ab44e560fe3a211087748b2b5f3e31f403baf41f5b7b0a70bd104", size = 887868, upload-time = "2026-01-07T18:04:32.124Z" }, + { url = "https://files.pythonhosted.org/packages/2e/d4/b4045a7ccc5680fb496d01edf749c7a9367cc8762fbdf7516cf807ef679b/pymongo-4.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:f513b2c6c0d5c491f478422f6b5b5c27ac1af06a54c93ef8631806f7231bd92e", size = 907554, upload-time = "2026-01-07T18:04:33.685Z" }, + { url = "https://files.pythonhosted.org/packages/60/4c/33f75713d50d5247f2258405142c0318ff32c6f8976171c4fcae87a9dbdf/pymongo-4.16.0-cp312-cp312-win_arm64.whl", hash = "sha256:dfc320f08ea9a7ec5b2403dc4e8150636f0d6150f4b9792faaae539c88e7db3b", size = 892971, upload-time = "2026-01-07T18:04:35.594Z" }, + { url = "https://files.pythonhosted.org/packages/47/84/148d8b5da8260f4679d6665196ae04ab14ffdf06f5fe670b0ab11942951f/pymongo-4.16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d15f060bc6d0964a8bb70aba8f0cb6d11ae99715438f640cff11bbcf172eb0e8", size = 972009, upload-time = "2026-01-07T18:04:38.303Z" }, + { url = "https://files.pythonhosted.org/packages/1e/5e/9f3a8daf583d0adaaa033a3e3e58194d2282737dc164014ff33c7a081103/pymongo-4.16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4a19ea46a0fe71248965305a020bc076a163311aefbaa1d83e47d06fa30ac747", size = 971784, upload-time = "2026-01-07T18:04:39.669Z" }, + { url = "https://files.pythonhosted.org/packages/ad/f2/b6c24361fcde24946198573c0176406bfd5f7b8538335f3d939487055322/pymongo-4.16.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:311d4549d6bf1f8c61d025965aebb5ba29d1481dc6471693ab91610aaffbc0eb", size = 1947174, upload-time = "2026-01-07T18:04:41.368Z" }, + { url = "https://files.pythonhosted.org/packages/47/1a/8634192f98cf740b3d174e1018dd0350018607d5bd8ac35a666dc49c732b/pymongo-4.16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:46ffb728d92dd5b09fc034ed91acf5595657c7ca17d4cf3751322cd554153c17", size = 1991727, upload-time = "2026-01-07T18:04:42.965Z" }, + { url = "https://files.pythonhosted.org/packages/5a/2f/0c47ac84572b28e23028a23a3798a1f725e1c23b0cf1c1424678d16aff42/pymongo-4.16.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:acda193f440dd88c2023cb00aa8bd7b93a9df59978306d14d87a8b12fe426b05", size = 2082497, upload-time = "2026-01-07T18:04:44.652Z" }, + { url = "https://files.pythonhosted.org/packages/ba/57/9f46ef9c862b2f0cf5ce798f3541c201c574128d31ded407ba4b3918d7b6/pymongo-4.16.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5d9fdb386cf958e6ef6ff537d6149be7edb76c3268cd6833e6c36aa447e4443f", size = 2064947, upload-time = "2026-01-07T18:04:46.228Z" }, + { url = "https://files.pythonhosted.org/packages/b8/56/5421c0998f38e32288100a07f6cb2f5f9f352522157c901910cb2927e211/pymongo-4.16.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:91899dd7fb9a8c50f09c3c1cf0cb73bfbe2737f511f641f19b9650deb61c00ca", size = 1980478, upload-time = "2026-01-07T18:04:48.017Z" }, + { url = "https://files.pythonhosted.org/packages/92/93/bfc448d025e12313a937d6e1e0101b50cc9751636b4b170e600fe3203063/pymongo-4.16.0-cp313-cp313-win32.whl", hash = "sha256:2cd60cd1e05de7f01927f8e25ca26b3ea2c09de8723241e5d3bcfdc70eaff76b", size = 934672, upload-time = "2026-01-07T18:04:49.538Z" }, + { url = "https://files.pythonhosted.org/packages/96/10/12710a5e01218d50c3dd165fd72c5ed2699285f77348a3b1a119a191d826/pymongo-4.16.0-cp313-cp313-win_amd64.whl", hash = "sha256:3ead8a0050c53eaa55935895d6919d393d0328ec24b2b9115bdbe881aa222673", size = 959237, upload-time = "2026-01-07T18:04:51.382Z" }, + { url = "https://files.pythonhosted.org/packages/0c/56/d288bcd1d05bc17ec69df1d0b1d67bc710c7c5dbef86033a5a4d2e2b08e6/pymongo-4.16.0-cp313-cp313-win_arm64.whl", hash = "sha256:dbbc5b254c36c37d10abb50e899bc3939bbb7ab1e7c659614409af99bd3e7675", size = 940909, upload-time = "2026-01-07T18:04:52.904Z" }, + { url = "https://files.pythonhosted.org/packages/30/9e/4d343f8d0512002fce17915a89477b9f916bda1205729e042d8f23acf194/pymongo-4.16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:8a254d49a9ffe9d7f888e3c677eed3729b14ce85abb08cd74732cead6ccc3c66", size = 1026634, upload-time = "2026-01-07T18:04:54.359Z" }, + { url = "https://files.pythonhosted.org/packages/c3/e3/341f88c5535df40c0450fda915f582757bb7d988cdfc92990a5e27c4c324/pymongo-4.16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a1bf44e13cf2d44d2ea2e928a8140d5d667304abe1a61c4d55b4906f389fbe64", size = 1026252, upload-time = "2026-01-07T18:04:56.642Z" }, + { url = "https://files.pythonhosted.org/packages/af/64/9471b22eb98f0a2ca0b8e09393de048502111b2b5b14ab1bd9e39708aab5/pymongo-4.16.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f1c5f1f818b669875d191323a48912d3fcd2e4906410e8297bb09ac50c4d5ccc", size = 2207399, upload-time = "2026-01-07T18:04:58.255Z" }, + { url = "https://files.pythonhosted.org/packages/87/ac/47c4d50b25a02f21764f140295a2efaa583ee7f17992a5e5fa542b3a690f/pymongo-4.16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77cfd37a43a53b02b7bd930457c7994c924ad8bbe8dff91817904bcbf291b371", size = 2260595, upload-time = "2026-01-07T18:04:59.788Z" }, + { url = "https://files.pythonhosted.org/packages/ee/1b/0ce1ce9dd036417646b2fe6f63b58127acff3cf96eeb630c34ec9cd675ff/pymongo-4.16.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:36ef2fee50eee669587d742fb456e349634b4fcf8926208766078b089054b24b", size = 2366958, upload-time = "2026-01-07T18:05:01.942Z" }, + { url = "https://files.pythonhosted.org/packages/3e/3c/a5a17c0d413aa9d6c17bc35c2b472e9e79cda8068ba8e93433b5f43028e9/pymongo-4.16.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:55f8d5a6fe2fa0b823674db2293f92d74cd5f970bc0360f409a1fc21003862d3", size = 2346081, upload-time = "2026-01-07T18:05:03.576Z" }, + { url = "https://files.pythonhosted.org/packages/65/19/f815533d1a88fb8a3b6c6e895bb085ffdae68ccb1e6ed7102202a307f8e2/pymongo-4.16.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9caacac0dd105e2555521002e2d17afc08665187017b466b5753e84c016628e6", size = 2246053, upload-time = "2026-01-07T18:05:05.459Z" }, + { url = "https://files.pythonhosted.org/packages/c6/88/4be3ec78828dc64b212c123114bd6ae8db5b7676085a7b43cc75d0131bd2/pymongo-4.16.0-cp314-cp314-win32.whl", hash = "sha256:c789236366525c3ee3cd6e4e450a9ff629a7d1f4d88b8e18a0aea0615fd7ecf8", size = 989461, upload-time = "2026-01-07T18:05:07.018Z" }, + { url = "https://files.pythonhosted.org/packages/af/5a/ab8d5af76421b34db483c9c8ebc3a2199fb80ae63dc7e18f4cf1df46306a/pymongo-4.16.0-cp314-cp314-win_amd64.whl", hash = "sha256:2b0714d7764efb29bf9d3c51c964aed7c4c7237b341f9346f15ceaf8321fdb35", size = 1017803, upload-time = "2026-01-07T18:05:08.499Z" }, + { url = "https://files.pythonhosted.org/packages/f6/f4/98d68020728ac6423cf02d17cfd8226bf6cce5690b163d30d3f705e8297e/pymongo-4.16.0-cp314-cp314-win_arm64.whl", hash = "sha256:12762e7cc0f8374a8cae3b9f9ed8dabb5d438c7b33329232dd9b7de783454033", size = 997184, upload-time = "2026-01-07T18:05:09.944Z" }, + { url = "https://files.pythonhosted.org/packages/50/00/dc3a271daf06401825b9c1f4f76f018182c7738281ea54b9762aea0560c1/pymongo-4.16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1c01e8a7cd0ea66baf64a118005535ab5bf9f9eb63a1b50ac3935dccf9a54abe", size = 1083303, upload-time = "2026-01-07T18:05:11.702Z" }, + { url = "https://files.pythonhosted.org/packages/b8/4b/b5375ee21d12eababe46215011ebc63801c0d2c5ffdf203849d0d79f9852/pymongo-4.16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:4c4872299ebe315a79f7f922051061634a64fda95b6b17677ba57ef00b2ba2a4", size = 1083233, upload-time = "2026-01-07T18:05:13.182Z" }, + { url = "https://files.pythonhosted.org/packages/ee/e3/52efa3ca900622c7dcb56c5e70f15c906816d98905c22d2ee1f84d9a7b60/pymongo-4.16.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:78037d02389745e247fe5ab0bcad5d1ab30726eaac3ad79219c7d6bbb07eec53", size = 2527438, upload-time = "2026-01-07T18:05:14.981Z" }, + { url = "https://files.pythonhosted.org/packages/cb/96/43b1be151c734e7766c725444bcbfa1de6b60cc66bfb406203746839dd25/pymongo-4.16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c126fb72be2518395cc0465d4bae03125119136462e1945aea19840e45d89cfc", size = 2600399, upload-time = "2026-01-07T18:05:16.794Z" }, + { url = "https://files.pythonhosted.org/packages/e7/62/fa64a5045dfe3a1cd9217232c848256e7bc0136cffb7da4735c5e0d30e40/pymongo-4.16.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f3867dc225d9423c245a51eaac2cfcd53dde8e0a8d8090bb6aed6e31bd6c2d4f", size = 2720960, upload-time = "2026-01-07T18:05:18.498Z" }, + { url = "https://files.pythonhosted.org/packages/54/7b/01577eb97e605502821273a5bc16ce0fb0be5c978fe03acdbff471471202/pymongo-4.16.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f25001a955073b80510c0c3db0e043dbbc36904fd69e511c74e3d8640b8a5111", size = 2699344, upload-time = "2026-01-07T18:05:20.073Z" }, + { url = "https://files.pythonhosted.org/packages/55/68/6ef6372d516f703479c3b6cbbc45a5afd307173b1cbaccd724e23919bb1a/pymongo-4.16.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d9885aad05f82fd7ea0c9ca505d60939746b39263fa273d0125170da8f59098", size = 2577133, upload-time = "2026-01-07T18:05:22.052Z" }, + { url = "https://files.pythonhosted.org/packages/15/c7/b5337093bb01da852f945802328665f85f8109dbe91d81ea2afe5ff059b9/pymongo-4.16.0-cp314-cp314t-win32.whl", hash = "sha256:948152b30eddeae8355495f9943a3bf66b708295c0b9b6f467de1c620f215487", size = 1040560, upload-time = "2026-01-07T18:05:23.888Z" }, + { url = "https://files.pythonhosted.org/packages/96/8c/5b448cd1b103f3889d5713dda37304c81020ff88e38a826e8a75ddff4610/pymongo-4.16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f6e42c1bc985d9beee884780ae6048790eb4cd565c46251932906bdb1630034a", size = 1075081, upload-time = "2026-01-07T18:05:26.874Z" }, + { url = "https://files.pythonhosted.org/packages/32/cd/ddc794cdc8500f6f28c119c624252fb6dfb19481c6d7ed150f13cf468a6d/pymongo-4.16.0-cp314-cp314t-win_arm64.whl", hash = "sha256:6b2a20edb5452ac8daa395890eeb076c570790dfce6b7a44d788af74c2f8cf96", size = 1047725, upload-time = "2026-01-07T18:05:28.47Z" }, +] + +[[package]] +name = "pypdfium2" +version = "5.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3b/01/be763b9081c7eb823196e7d13d9c145bf75ac43f3c1466de81c21c24b381/pypdfium2-5.6.0.tar.gz", hash = "sha256:bcb9368acfe3547054698abbdae68ba0cbd2d3bda8e8ee437e061deef061976d", size = 270714, upload-time = "2026-03-08T01:05:06.5Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/b1/129ed0177521a93a892f8a6a215dd3260093e30e77ef7035004bb8af7b6c/pypdfium2-5.6.0-py3-none-android_23_arm64_v8a.whl", hash = "sha256:fb7858c9707708555b4a719b5548a6e7f5d26bc82aef55ae4eb085d7a2190b11", size = 3346059, upload-time = "2026-03-08T01:04:21.37Z" }, + { url = "https://files.pythonhosted.org/packages/86/34/cbdece6886012180a7f2c7b2c360c415cf5e1f83f1973d2c9201dae3506a/pypdfium2-5.6.0-py3-none-android_23_armeabi_v7a.whl", hash = "sha256:6a7e1f4597317786f994bfb947eef480e53933f804a990193ab89eef8243f805", size = 2804418, upload-time = "2026-03-08T01:04:23.384Z" }, + { url = "https://files.pythonhosted.org/packages/6e/f6/9f9e190fe0e5a6b86b82f83bd8b5d3490348766062381140ca5cad8e00b1/pypdfium2-5.6.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e468c38997573f0e86f03273c2c1fbdea999de52ba43fee96acaa2f6b2ad35f7", size = 3412541, upload-time = "2026-03-08T01:04:25.45Z" }, + { url = "https://files.pythonhosted.org/packages/ee/8d/e57492cb2228ba56ed57de1ff044c8ac114b46905f8b1445c33299ba0488/pypdfium2-5.6.0-py3-none-macosx_11_0_x86_64.whl", hash = "sha256:ad3abddc5805424f962e383253ccad6a0d1d2ebd86afa9a9e1b9ca659773cd0d", size = 3592320, upload-time = "2026-03-08T01:04:27.509Z" }, + { url = "https://files.pythonhosted.org/packages/f9/8a/8ab82e33e9c551494cbe1526ea250ca8cc4e9e98d6a4fc6b6f8d959aa1d1/pypdfium2-5.6.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f6b5eb9eae5c45076395454522ca26add72ba8bd1fe473e1e4721aa58521470c", size = 3596450, upload-time = "2026-03-08T01:04:29.183Z" }, + { url = "https://files.pythonhosted.org/packages/f5/b5/602a792282312ccb158cc63849528079d94b0a11efdc61f2a359edfb41e9/pypdfium2-5.6.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:258624da8ef45cdc426e11b33e9d83f9fb723c1c201c6e0f4ab5a85966c6b876", size = 3325442, upload-time = "2026-03-08T01:04:30.886Z" }, + { url = "https://files.pythonhosted.org/packages/81/1f/9e48ec05ed8d19d736c2d1f23c1bd0f20673f02ef846a2576c69e237f15d/pypdfium2-5.6.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e9367451c8a00931d6612db0822525a18c06f649d562cd323a719e46ac19c9bb", size = 3727434, upload-time = "2026-03-08T01:04:33.619Z" }, + { url = "https://files.pythonhosted.org/packages/33/90/0efd020928b4edbd65f4f3c2af0c84e20b43a3ada8fa6d04f999a97afe7a/pypdfium2-5.6.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a757869f891eac1cc1372e38a4aa01adac8abc8fe2a8a4e2ebf50595e3bf5937", size = 4139029, upload-time = "2026-03-08T01:04:36.08Z" }, + { url = "https://files.pythonhosted.org/packages/ff/49/a640b288a48dab1752281dd9b72c0679fccea107874e80a65a606b00efa9/pypdfium2-5.6.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:515be355222cc57ae9e62cd5c7c350b8e0c863efc539f80c7d75e2811ba45cb6", size = 3646387, upload-time = "2026-03-08T01:04:38.151Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3b/a344c19c01021eeb5d830c102e4fc9b1602f19c04aa7d11abbe2d188fd8e/pypdfium2-5.6.0-py3-none-manylinux_2_27_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1c4753c7caf7d004211d7f57a21f10d127f5e0e5510a14d24bc073e7220a3ea", size = 3097212, upload-time = "2026-03-08T01:04:40.776Z" }, + { url = "https://files.pythonhosted.org/packages/50/96/e48e13789ace22aeb9b7510904a1b1493ec588196e11bbacc122da330b3d/pypdfium2-5.6.0-py3-none-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c49729090281fdd85775fb8912c10bd19e99178efaa98f145ab06e7ce68554d2", size = 2965026, upload-time = "2026-03-08T01:04:42.857Z" }, + { url = "https://files.pythonhosted.org/packages/cb/06/3100e44d4935f73af8f5d633d3bd40f0d36d606027085a0ef1f0566a6320/pypdfium2-5.6.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a4a1749a8d4afd62924a8d95cfa4f2e26fc32957ce34ac3b674be6f127ed252e", size = 4131431, upload-time = "2026-03-08T01:04:44.982Z" }, + { url = "https://files.pythonhosted.org/packages/64/ef/d8df63569ce9a66c8496057782eb8af78e0d28667922d62ec958434e3d4b/pypdfium2-5.6.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:36469ebd0fdffb7130ce45ed9c44f8232d91571c89eb851bd1633c64b6f6114f", size = 3747469, upload-time = "2026-03-08T01:04:46.702Z" }, + { url = "https://files.pythonhosted.org/packages/a6/47/fd2c6a67a49fade1acd719fbd11f7c375e7219912923ef2de0ea0ac1544e/pypdfium2-5.6.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:9da900df09be3cf546b637a127a7b6428fb22d705951d731269e25fd3adef457", size = 4337578, upload-time = "2026-03-08T01:04:49.007Z" }, + { url = "https://files.pythonhosted.org/packages/6b/f5/836c83e54b01e09478c4d6bf4912651d6053c932250fcee953f5c72d8e4a/pypdfium2-5.6.0-py3-none-musllinux_1_2_ppc64le.whl", hash = "sha256:45fccd5622233c5ec91a885770ae7dd4004d4320ac05a4ad8fa03a66dea40244", size = 4376104, upload-time = "2026-03-08T01:04:51.04Z" }, + { url = "https://files.pythonhosted.org/packages/6e/7f/b940b6a1664daf8f9bad87c6c99b84effa3611615b8708d10392dc33036c/pypdfium2-5.6.0-py3-none-musllinux_1_2_riscv64.whl", hash = "sha256:282dc030e767cd61bd0299f9d581052b91188e2b87561489057a8e7963e7e0cb", size = 3929824, upload-time = "2026-03-08T01:04:53.544Z" }, + { url = "https://files.pythonhosted.org/packages/88/79/00267d92a6a58c229e364d474f5698efe446e0c7f4f152f58d0138715e99/pypdfium2-5.6.0-py3-none-musllinux_1_2_s390x.whl", hash = "sha256:a1c1dfe950382c76a7bba1ba160ec5e40df8dd26b04a1124ae268fda55bc4cbe", size = 4270201, upload-time = "2026-03-08T01:04:55.81Z" }, + { url = "https://files.pythonhosted.org/packages/e1/ab/b127f38aba41746bdf9ace15ba08411d7ef6ecba1326d529ba414eb1ed50/pypdfium2-5.6.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:43b0341ca6feb6c92e4b7a9eb4813e5466f5f5e8b6baeb14df0a94d5f312c00b", size = 4180793, upload-time = "2026-03-08T01:04:57.961Z" }, + { url = "https://files.pythonhosted.org/packages/0e/8c/a01c8e4302448b614d25a85c08298b0d3e9dfbdac5bd1b2f32c9b02e83d9/pypdfium2-5.6.0-py3-none-win32.whl", hash = "sha256:9dfcd4ff49a2b9260d00e38539ab28190d59e785e83030b30ffaf7a29c42155d", size = 3596753, upload-time = "2026-03-08T01:05:00.566Z" }, + { url = "https://files.pythonhosted.org/packages/9b/5f/2d871adf46761bb002a62686545da6348afe838d19af03df65d1ece786a2/pypdfium2-5.6.0-py3-none-win_amd64.whl", hash = "sha256:c6bc8dd63d0568f4b592f3e03de756afafc0e44aa1fe8878cc4aba1b11ae7374", size = 3716526, upload-time = "2026-03-08T01:05:02.433Z" }, + { url = "https://files.pythonhosted.org/packages/3a/80/0d9b162098597fbe3ac2b269b1682c0c3e8db9ba87679603fdd9b19afaa6/pypdfium2-5.6.0-py3-none-win_arm64.whl", hash = "sha256:5538417b199bdcb3207370c88df61f2ba3dac7a3253f82e1aa2708e6376b6f90", size = 3515049, upload-time = "2026-03-08T01:05:04.587Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, +] + +[[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, upload-time = "2024-03-01T18:36:20.211Z" } +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, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[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, upload-time = "2025-06-16T20:46:27.921Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/00/1e03a4989fa5795da308cd774f05b704ace555a70f9bf9d3be057b680bcf/python_docx-1.2.0-py3-none-any.whl", hash = "sha256:3fd478f3250fbbbfd3b94fe1e985955737c145627498896a8a6bf81f4baf66c7", size = 252987, upload-time = "2025-06-16T20:46:22.506Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6a/53/ed9d74092561d4b01a2ef1349d52cdbc135e526c245f366b089cfca6de49/python_dotenv-1.2.3.tar.gz", hash = "sha256:a20a594dabeaa385725aa239d5244871c143ecb356add8a20fcf23773a6c3a35", size = 58945, upload-time = "2026-08-16T16:54:54.067Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/17/c5c6b53ddc18f297992099b3d9ec16c855c0ccc83263a21fe4d1c625ec6c/python_dotenv-1.2.3-py3-none-any.whl", hash = "sha256:904552145e8bfed22162c09dab1c2b9b54fefa7b23ba780f4f26ca0316b0f0d9", size = 22780, upload-time = "2026-08-16T16:54:52.473Z" }, +] + +[[package]] +name = "python-multipart" +version = "0.0.32" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5b/42/55c32bb9b12693c092ad250a0e82edb5b31ddeda6eb772de5f308b3804ad/python_multipart-0.0.32.tar.gz", hash = "sha256:be54b7f3fa167bb83e4fcd936b887b708f4e57fe75911c02aebf53efaf8d938e", size = 46881, upload-time = "2026-06-04T16:18:58.647Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/04/e8135ebd1ad02c56ec633277529b2602ff99ff634be76cdba5744cf554fd/python_multipart-0.0.32-py3-none-any.whl", hash = "sha256:ff6d3f776f16878c894e52e107296ffc890e913c611b1a4ec6c44e2821fe2e23", size = 30042, upload-time = "2026-06-04T16:18:57.319Z" }, +] + +[[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, upload-time = "2025-02-03T17:13:47.415Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/53/67/f56c69a98c7eb244025845506387d0f961681657c9fcd8b2d2edd148f9d2/python_oxmsg-0.0.2-py3-none-any.whl", hash = "sha256:22be29b14c46016bcd05e34abddfd8e05ee82082f53b82753d115da3fc7d0355", size = 31455, upload-time = "2025-02-03T17:13:46.061Z" }, +] + +[[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, upload-time = "2024-08-07T17:33:37.772Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/4f/00be2196329ebbff56ce564aa94efb0fbc828d00de250b1980de1a34ab49/python_pptx-1.0.2-py3-none-any.whl", hash = "sha256:160838e0b8565a8b1f67947675886e9fea18aa5e795db7ae531606d68e785cba", size = 472788, upload-time = "2024-08-07T17:33:28.192Z" }, +] + +[[package]] +name = "pytz" +version = "2026.1.post1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/56/db/b8721d71d945e6a8ac63c0fc900b2067181dbb50805958d4d4661cf7d277/pytz-2026.1.post1.tar.gz", hash = "sha256:3378dde6a0c3d26719182142c56e60c7f9af7e968076f31aae569d72a0358ee1", size = 321088, upload-time = "2026-03-03T07:47:50.683Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/99/781fe0c827be2742bcc775efefccb3b048a3a9c6ce9aec0cbf4a101677e5/pytz-2026.1.post1-py2.py3-none-any.whl", hash = "sha256:f2fd16142fda348286a75e1a524be810bb05d444e5a081f37f7affc635035f7a", size = 510489, upload-time = "2026-03-03T07:47:49.167Z" }, +] + +[[package]] +name = "pywin32" +version = "311" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/af/449a6a91e5d6db51420875c54f6aff7c97a86a3b13a0b4f1a5c13b988de3/pywin32-311-cp311-cp311-win32.whl", hash = "sha256:184eb5e436dea364dcd3d2316d577d625c0351bf237c4e9a5fabbcfa5a58b151", size = 8697031, upload-time = "2025-07-14T20:13:13.266Z" }, + { url = "https://files.pythonhosted.org/packages/51/8f/9bb81dd5bb77d22243d33c8397f09377056d5c687aa6d4042bea7fbf8364/pywin32-311-cp311-cp311-win_amd64.whl", hash = "sha256:3ce80b34b22b17ccbd937a6e78e7225d80c52f5ab9940fe0506a1a16f3dab503", size = 9508308, upload-time = "2025-07-14T20:13:15.147Z" }, + { url = "https://files.pythonhosted.org/packages/44/7b/9c2ab54f74a138c491aba1b1cd0795ba61f144c711daea84a88b63dc0f6c/pywin32-311-cp311-cp311-win_arm64.whl", hash = "sha256:a733f1388e1a842abb67ffa8e7aad0e70ac519e09b0f6a784e65a136ec7cefd2", size = 8703930, upload-time = "2025-07-14T20:13:16.945Z" }, + { url = "https://files.pythonhosted.org/packages/e7/ab/01ea1943d4eba0f850c3c61e78e8dd59757ff815ff3ccd0a84de5f541f42/pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31", size = 8706543, upload-time = "2025-07-14T20:13:20.765Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a8/a0e8d07d4d051ec7502cd58b291ec98dcc0c3fff027caad0470b72cfcc2f/pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067", size = 9495040, upload-time = "2025-07-14T20:13:22.543Z" }, + { url = "https://files.pythonhosted.org/packages/ba/3a/2ae996277b4b50f17d61f0603efd8253cb2d79cc7ae159468007b586396d/pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852", size = 8710102, upload-time = "2025-07-14T20:13:24.682Z" }, + { url = "https://files.pythonhosted.org/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700, upload-time = "2025-07-14T20:13:26.471Z" }, + { url = "https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700, upload-time = "2025-07-14T20:13:28.243Z" }, + { url = "https://files.pythonhosted.org/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318, upload-time = "2025-07-14T20:13:30.348Z" }, + { url = "https://files.pythonhosted.org/packages/c9/31/097f2e132c4f16d99a22bfb777e0fd88bd8e1c634304e102f313af69ace5/pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee", size = 8840714, upload-time = "2025-07-14T20:13:32.449Z" }, + { url = "https://files.pythonhosted.org/packages/90/4b/07c77d8ba0e01349358082713400435347df8426208171ce297da32c313d/pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87", size = 9656800, upload-time = "2025-07-14T20:13:34.312Z" }, + { url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540, upload-time = "2025-07-14T20:13:36.379Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "rapidfuzz" +version = "3.14.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d3/28/9d808fe62375b9aab5ba92fa9b29371297b067c2790b2d7cda648b1e2f8d/rapidfuzz-3.14.3.tar.gz", hash = "sha256:2491937177868bc4b1e469087601d53f925e8d270ccc21e07404b4b5814b7b5f", size = 57863900, upload-time = "2025-11-01T11:54:52.321Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/25/5b0a33ad3332ee1213068c66f7c14e9e221be90bab434f0cb4defa9d6660/rapidfuzz-3.14.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:dea2d113e260a5da0c4003e0a5e9fdf24a9dc2bb9eaa43abd030a1e46ce7837d", size = 1953885, upload-time = "2025-11-01T11:52:47.75Z" }, + { url = "https://files.pythonhosted.org/packages/2d/ab/f1181f500c32c8fcf7c966f5920c7e56b9b1d03193386d19c956505c312d/rapidfuzz-3.14.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e6c31a4aa68cfa75d7eede8b0ed24b9e458447db604c2db53f358be9843d81d3", size = 1390200, upload-time = "2025-11-01T11:52:49.491Z" }, + { url = "https://files.pythonhosted.org/packages/14/2a/0f2de974ececad873865c6bb3ea3ad07c976ac293d5025b2d73325aac1d4/rapidfuzz-3.14.3-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:02821366d928e68ddcb567fed8723dad7ea3a979fada6283e6914d5858674850", size = 1389319, upload-time = "2025-11-01T11:52:51.224Z" }, + { url = "https://files.pythonhosted.org/packages/ed/69/309d8f3a0bb3031fd9b667174cc4af56000645298af7c2931be5c3d14bb4/rapidfuzz-3.14.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cfe8df315ab4e6db4e1be72c5170f8e66021acde22cd2f9d04d2058a9fd8162e", size = 3178495, upload-time = "2025-11-01T11:52:53.005Z" }, + { url = "https://files.pythonhosted.org/packages/10/b7/f9c44a99269ea5bf6fd6a40b84e858414b6e241288b9f2b74af470d222b1/rapidfuzz-3.14.3-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:769f31c60cd79420188fcdb3c823227fc4a6deb35cafec9d14045c7f6743acae", size = 1228443, upload-time = "2025-11-01T11:52:54.991Z" }, + { url = "https://files.pythonhosted.org/packages/f2/0a/3b3137abac7f19c9220e14cd7ce993e35071a7655e7ef697785a3edfea1a/rapidfuzz-3.14.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:54fa03062124e73086dae66a3451c553c1e20a39c077fd704dc7154092c34c63", size = 2411998, upload-time = "2025-11-01T11:52:56.629Z" }, + { url = "https://files.pythonhosted.org/packages/f3/b6/983805a844d44670eaae63831024cdc97ada4e9c62abc6b20703e81e7f9b/rapidfuzz-3.14.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:834d1e818005ed0d4ae38f6b87b86fad9b0a74085467ece0727d20e15077c094", size = 2530120, upload-time = "2025-11-01T11:52:58.298Z" }, + { url = "https://files.pythonhosted.org/packages/b4/cc/2c97beb2b1be2d7595d805682472f1b1b844111027d5ad89b65e16bdbaaa/rapidfuzz-3.14.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:948b00e8476a91f510dd1ec07272efc7d78c275d83b630455559671d4e33b678", size = 4283129, upload-time = "2025-11-01T11:53:00.188Z" }, + { url = "https://files.pythonhosted.org/packages/4d/03/2f0e5e94941045aefe7eafab72320e61285c07b752df9884ce88d6b8b835/rapidfuzz-3.14.3-cp311-cp311-win32.whl", hash = "sha256:43d0305c36f504232f18ea04e55f2059bb89f169d3119c4ea96a0e15b59e2a91", size = 1724224, upload-time = "2025-11-01T11:53:02.149Z" }, + { url = "https://files.pythonhosted.org/packages/cf/99/5fa23e204435803875daefda73fd61baeabc3c36b8fc0e34c1705aab8c7b/rapidfuzz-3.14.3-cp311-cp311-win_amd64.whl", hash = "sha256:ef6bf930b947bd0735c550683939a032090f1d688dfd8861d6b45307b96fd5c5", size = 1544259, upload-time = "2025-11-01T11:53:03.66Z" }, + { url = "https://files.pythonhosted.org/packages/48/35/d657b85fcc615a42661b98ac90ce8e95bd32af474603a105643963749886/rapidfuzz-3.14.3-cp311-cp311-win_arm64.whl", hash = "sha256:f3eb0ff3b75d6fdccd40b55e7414bb859a1cda77c52762c9c82b85569f5088e7", size = 814734, upload-time = "2025-11-01T11:53:05.008Z" }, + { url = "https://files.pythonhosted.org/packages/fa/8e/3c215e860b458cfbedb3ed73bc72e98eb7e0ed72f6b48099604a7a3260c2/rapidfuzz-3.14.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:685c93ea961d135893b5984a5a9851637d23767feabe414ec974f43babbd8226", size = 1945306, upload-time = "2025-11-01T11:53:06.452Z" }, + { url = "https://files.pythonhosted.org/packages/36/d9/31b33512015c899f4a6e6af64df8dfe8acddf4c8b40a4b3e0e6e1bcd00e5/rapidfuzz-3.14.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fa7c8f26f009f8c673fbfb443792f0cf8cf50c4e18121ff1e285b5e08a94fbdb", size = 1390788, upload-time = "2025-11-01T11:53:08.721Z" }, + { url = "https://files.pythonhosted.org/packages/a9/67/2ee6f8de6e2081ccd560a571d9c9063184fe467f484a17fa90311a7f4a2e/rapidfuzz-3.14.3-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:57f878330c8d361b2ce76cebb8e3e1dc827293b6abf404e67d53260d27b5d941", size = 1374580, upload-time = "2025-11-01T11:53:10.164Z" }, + { url = "https://files.pythonhosted.org/packages/30/83/80d22997acd928eda7deadc19ccd15883904622396d6571e935993e0453a/rapidfuzz-3.14.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c5f545f454871e6af05753a0172849c82feaf0f521c5ca62ba09e1b382d6382", size = 3154947, upload-time = "2025-11-01T11:53:12.093Z" }, + { url = "https://files.pythonhosted.org/packages/5b/cf/9f49831085a16384695f9fb096b99662f589e30b89b4a589a1ebc1a19d34/rapidfuzz-3.14.3-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:07aa0b5d8863e3151e05026a28e0d924accf0a7a3b605da978f0359bb804df43", size = 1223872, upload-time = "2025-11-01T11:53:13.664Z" }, + { url = "https://files.pythonhosted.org/packages/c8/0f/41ee8034e744b871c2e071ef0d360686f5ccfe5659f4fd96c3ec406b3c8b/rapidfuzz-3.14.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73b07566bc7e010e7b5bd490fb04bb312e820970180df6b5655e9e6224c137db", size = 2392512, upload-time = "2025-11-01T11:53:15.109Z" }, + { url = "https://files.pythonhosted.org/packages/da/86/280038b6b0c2ccec54fb957c732ad6b41cc1fd03b288d76545b9cf98343f/rapidfuzz-3.14.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:6de00eb84c71476af7d3110cf25d8fe7c792d7f5fa86764ef0b4ca97e78ca3ed", size = 2521398, upload-time = "2025-11-01T11:53:17.146Z" }, + { url = "https://files.pythonhosted.org/packages/fa/7b/05c26f939607dca0006505e3216248ae2de631e39ef94dd63dbbf0860021/rapidfuzz-3.14.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d7843a1abf0091773a530636fdd2a49a41bcae22f9910b86b4f903e76ddc82dc", size = 4259416, upload-time = "2025-11-01T11:53:19.34Z" }, + { url = "https://files.pythonhosted.org/packages/40/eb/9e3af4103d91788f81111af1b54a28de347cdbed8eaa6c91d5e98a889aab/rapidfuzz-3.14.3-cp312-cp312-win32.whl", hash = "sha256:dea97ac3ca18cd3ba8f3d04b5c1fe4aa60e58e8d9b7793d3bd595fdb04128d7a", size = 1709527, upload-time = "2025-11-01T11:53:20.949Z" }, + { url = "https://files.pythonhosted.org/packages/b8/63/d06ecce90e2cf1747e29aeab9f823d21e5877a4c51b79720b2d3be7848f8/rapidfuzz-3.14.3-cp312-cp312-win_amd64.whl", hash = "sha256:b5100fd6bcee4d27f28f4e0a1c6b5127bc8ba7c2a9959cad9eab0bf4a7ab3329", size = 1538989, upload-time = "2025-11-01T11:53:22.428Z" }, + { url = "https://files.pythonhosted.org/packages/fc/6d/beee32dcda64af8128aab3ace2ccb33d797ed58c434c6419eea015fec779/rapidfuzz-3.14.3-cp312-cp312-win_arm64.whl", hash = "sha256:4e49c9e992bc5fc873bd0fff7ef16a4405130ec42f2ce3d2b735ba5d3d4eb70f", size = 811161, upload-time = "2025-11-01T11:53:23.811Z" }, + { url = "https://files.pythonhosted.org/packages/e4/4f/0d94d09646853bd26978cb3a7541b6233c5760687777fa97da8de0d9a6ac/rapidfuzz-3.14.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:dbcb726064b12f356bf10fffdb6db4b6dce5390b23627c08652b3f6e49aa56ae", size = 1939646, upload-time = "2025-11-01T11:53:25.292Z" }, + { url = "https://files.pythonhosted.org/packages/b6/eb/f96aefc00f3bbdbab9c0657363ea8437a207d7545ac1c3789673e05d80bd/rapidfuzz-3.14.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1704fc70d214294e554a2421b473779bcdeef715881c5e927dc0f11e1692a0ff", size = 1385512, upload-time = "2025-11-01T11:53:27.594Z" }, + { url = "https://files.pythonhosted.org/packages/26/34/71c4f7749c12ee223dba90017a5947e8f03731a7cc9f489b662a8e9e643d/rapidfuzz-3.14.3-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc65e72790ddfd310c2c8912b45106e3800fefe160b0c2ef4d6b6fec4e826457", size = 1373571, upload-time = "2025-11-01T11:53:29.096Z" }, + { url = "https://files.pythonhosted.org/packages/32/00/ec8597a64f2be301ce1ee3290d067f49f6a7afb226b67d5f15b56d772ba5/rapidfuzz-3.14.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43e38c1305cffae8472572a0584d4ffc2f130865586a81038ca3965301f7c97c", size = 3156759, upload-time = "2025-11-01T11:53:30.777Z" }, + { url = "https://files.pythonhosted.org/packages/61/d5/b41eeb4930501cc899d5a9a7b5c9a33d85a670200d7e81658626dcc0ecc0/rapidfuzz-3.14.3-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:e195a77d06c03c98b3fc06b8a28576ba824392ce40de8c708f96ce04849a052e", size = 1222067, upload-time = "2025-11-01T11:53:32.334Z" }, + { url = "https://files.pythonhosted.org/packages/2a/7d/6d9abb4ffd1027c6ed837b425834f3bed8344472eb3a503ab55b3407c721/rapidfuzz-3.14.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1b7ef2f4b8583a744338a18f12c69693c194fb6777c0e9ada98cd4d9e8f09d10", size = 2394775, upload-time = "2025-11-01T11:53:34.24Z" }, + { url = "https://files.pythonhosted.org/packages/15/ce/4f3ab4c401c5a55364da1ffff8cc879fc97b4e5f4fa96033827da491a973/rapidfuzz-3.14.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:a2135b138bcdcb4c3742d417f215ac2d8c2b87bde15b0feede231ae95f09ec41", size = 2526123, upload-time = "2025-11-01T11:53:35.779Z" }, + { url = "https://files.pythonhosted.org/packages/c1/4b/54f804975376a328f57293bd817c12c9036171d15cf7292032e3f5820b2d/rapidfuzz-3.14.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:33a325ed0e8e1aa20c3e75f8ab057a7b248fdea7843c2a19ade0008906c14af0", size = 4262874, upload-time = "2025-11-01T11:53:37.866Z" }, + { url = "https://files.pythonhosted.org/packages/e9/b6/958db27d8a29a50ee6edd45d33debd3ce732e7209183a72f57544cd5fe22/rapidfuzz-3.14.3-cp313-cp313-win32.whl", hash = "sha256:8383b6d0d92f6cd008f3c9216535be215a064b2cc890398a678b56e6d280cb63", size = 1707972, upload-time = "2025-11-01T11:53:39.442Z" }, + { url = "https://files.pythonhosted.org/packages/07/75/fde1f334b0cec15b5946d9f84d73250fbfcc73c236b4bc1b25129d90876b/rapidfuzz-3.14.3-cp313-cp313-win_amd64.whl", hash = "sha256:e6b5e3036976f0fde888687d91be86d81f9ac5f7b02e218913c38285b756be6c", size = 1537011, upload-time = "2025-11-01T11:53:40.92Z" }, + { url = "https://files.pythonhosted.org/packages/2e/d7/d83fe001ce599dc7ead57ba1debf923dc961b6bdce522b741e6b8c82f55c/rapidfuzz-3.14.3-cp313-cp313-win_arm64.whl", hash = "sha256:7ba009977601d8b0828bfac9a110b195b3e4e79b350dcfa48c11269a9f1918a0", size = 810744, upload-time = "2025-11-01T11:53:42.723Z" }, + { url = "https://files.pythonhosted.org/packages/92/13/a486369e63ff3c1a58444d16b15c5feb943edd0e6c28a1d7d67cb8946b8f/rapidfuzz-3.14.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a0a28add871425c2fe94358c6300bbeb0bc2ed828ca003420ac6825408f5a424", size = 1967702, upload-time = "2025-11-01T11:53:44.554Z" }, + { url = "https://files.pythonhosted.org/packages/f1/82/efad25e260b7810f01d6b69122685e355bed78c94a12784bac4e0beb2afb/rapidfuzz-3.14.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:010e12e2411a4854b0434f920e72b717c43f8ec48d57e7affe5c42ecfa05dd0e", size = 1410702, upload-time = "2025-11-01T11:53:46.066Z" }, + { url = "https://files.pythonhosted.org/packages/ba/1a/34c977b860cde91082eae4a97ae503f43e0d84d4af301d857679b66f9869/rapidfuzz-3.14.3-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cfc3d57abd83c734d1714ec39c88a34dd69c85474918ebc21296f1e61eb5ca8", size = 1382337, upload-time = "2025-11-01T11:53:47.62Z" }, + { url = "https://files.pythonhosted.org/packages/88/74/f50ea0e24a5880a9159e8fd256b84d8f4634c2f6b4f98028bdd31891d907/rapidfuzz-3.14.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:89acb8cbb52904f763e5ac238083b9fc193bed8d1f03c80568b20e4cef43a519", size = 3165563, upload-time = "2025-11-01T11:53:49.216Z" }, + { url = "https://files.pythonhosted.org/packages/e8/7a/e744359404d7737049c26099423fc54bcbf303de5d870d07d2fb1410f567/rapidfuzz-3.14.3-cp313-cp313t-manylinux_2_31_armv7l.whl", hash = "sha256:7d9af908c2f371bfb9c985bd134e295038e3031e666e4b2ade1e7cb7f5af2f1a", size = 1214727, upload-time = "2025-11-01T11:53:50.883Z" }, + { url = "https://files.pythonhosted.org/packages/d3/2e/87adfe14ce75768ec6c2b8acd0e05e85e84be4be5e3d283cdae360afc4fe/rapidfuzz-3.14.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:1f1925619627f8798f8c3a391d81071336942e5fe8467bc3c567f982e7ce2897", size = 2403349, upload-time = "2025-11-01T11:53:52.322Z" }, + { url = "https://files.pythonhosted.org/packages/70/17/6c0b2b2bff9c8b12e12624c07aa22e922b0c72a490f180fa9183d1ef2c75/rapidfuzz-3.14.3-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:152555187360978119e98ce3e8263d70dd0c40c7541193fc302e9b7125cf8f58", size = 2507596, upload-time = "2025-11-01T11:53:53.835Z" }, + { url = "https://files.pythonhosted.org/packages/c3/d1/87852a7cbe4da7b962174c749a47433881a63a817d04f3e385ea9babcd9e/rapidfuzz-3.14.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:52619d25a09546b8db078981ca88939d72caa6b8701edd8b22e16482a38e799f", size = 4273595, upload-time = "2025-11-01T11:53:55.961Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ab/1d0354b7d1771a28fa7fe089bc23acec2bdd3756efa2419f463e3ed80e16/rapidfuzz-3.14.3-cp313-cp313t-win32.whl", hash = "sha256:489ce98a895c98cad284f0a47960c3e264c724cb4cfd47a1430fa091c0c25204", size = 1757773, upload-time = "2025-11-01T11:53:57.628Z" }, + { url = "https://files.pythonhosted.org/packages/0b/0c/71ef356adc29e2bdf74cd284317b34a16b80258fa0e7e242dd92cc1e6d10/rapidfuzz-3.14.3-cp313-cp313t-win_amd64.whl", hash = "sha256:656e52b054d5b5c2524169240e50cfa080b04b1c613c5f90a2465e84888d6f15", size = 1576797, upload-time = "2025-11-01T11:53:59.455Z" }, + { url = "https://files.pythonhosted.org/packages/fe/d2/0e64fc27bb08d4304aa3d11154eb5480bcf5d62d60140a7ee984dc07468a/rapidfuzz-3.14.3-cp313-cp313t-win_arm64.whl", hash = "sha256:c7e40c0a0af02ad6e57e89f62bef8604f55a04ecae90b0ceeda591bbf5923317", size = 829940, upload-time = "2025-11-01T11:54:01.1Z" }, + { url = "https://files.pythonhosted.org/packages/32/6f/1b88aaeade83abc5418788f9e6b01efefcd1a69d65ded37d89cd1662be41/rapidfuzz-3.14.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:442125473b247227d3f2de807a11da6c08ccf536572d1be943f8e262bae7e4ea", size = 1942086, upload-time = "2025-11-01T11:54:02.592Z" }, + { url = "https://files.pythonhosted.org/packages/a0/2c/b23861347436cb10f46c2bd425489ec462790faaa360a54a7ede5f78de88/rapidfuzz-3.14.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1ec0c8c0c3d4f97ced46b2e191e883f8c82dbbf6d5ebc1842366d7eff13cd5a6", size = 1386993, upload-time = "2025-11-01T11:54:04.12Z" }, + { url = "https://files.pythonhosted.org/packages/83/86/5d72e2c060aa1fbdc1f7362d938f6b237dff91f5b9fc5dd7cc297e112250/rapidfuzz-3.14.3-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2dc37bc20272f388b8c3a4eba4febc6e77e50a8f450c472def4751e7678f55e4", size = 1379126, upload-time = "2025-11-01T11:54:05.777Z" }, + { url = "https://files.pythonhosted.org/packages/c9/bc/ef2cee3e4d8b3fc22705ff519f0d487eecc756abdc7c25d53686689d6cf2/rapidfuzz-3.14.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dee362e7e79bae940a5e2b3f6d09c6554db6a4e301cc68343886c08be99844f1", size = 3159304, upload-time = "2025-11-01T11:54:07.351Z" }, + { url = "https://files.pythonhosted.org/packages/a0/36/dc5f2f62bbc7bc90be1f75eeaf49ed9502094bb19290dfb4747317b17f12/rapidfuzz-3.14.3-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:4b39921df948388a863f0e267edf2c36302983459b021ab928d4b801cbe6a421", size = 1218207, upload-time = "2025-11-01T11:54:09.641Z" }, + { url = "https://files.pythonhosted.org/packages/df/7e/8f4be75c1bc62f47edf2bbbe2370ee482fae655ebcc4718ac3827ead3904/rapidfuzz-3.14.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:beda6aa9bc44d1d81242e7b291b446be352d3451f8217fcb068fc2933927d53b", size = 2401245, upload-time = "2025-11-01T11:54:11.543Z" }, + { url = "https://files.pythonhosted.org/packages/05/38/f7c92759e1bb188dd05b80d11c630ba59b8d7856657baf454ff56059c2ab/rapidfuzz-3.14.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:6a014ba09657abfcfeed64b7d09407acb29af436d7fc075b23a298a7e4a6b41c", size = 2518308, upload-time = "2025-11-01T11:54:13.134Z" }, + { url = "https://files.pythonhosted.org/packages/c7/ac/85820f70fed5ecb5f1d9a55f1e1e2090ef62985ef41db289b5ac5ec56e28/rapidfuzz-3.14.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:32eeafa3abce138bb725550c0e228fc7eaeec7059aa8093d9cbbec2b58c2371a", size = 4265011, upload-time = "2025-11-01T11:54:15.087Z" }, + { url = "https://files.pythonhosted.org/packages/46/a9/616930721ea9835c918af7cde22bff17f9db3639b0c1a7f96684be7f5630/rapidfuzz-3.14.3-cp314-cp314-win32.whl", hash = "sha256:adb44d996fc610c7da8c5048775b21db60dd63b1548f078e95858c05c86876a3", size = 1742245, upload-time = "2025-11-01T11:54:17.19Z" }, + { url = "https://files.pythonhosted.org/packages/06/8a/f2fa5e9635b1ccafda4accf0e38246003f69982d7c81f2faa150014525a4/rapidfuzz-3.14.3-cp314-cp314-win_amd64.whl", hash = "sha256:f3d15d8527e2b293e38ce6e437631af0708df29eafd7c9fc48210854c94472f9", size = 1584856, upload-time = "2025-11-01T11:54:18.764Z" }, + { url = "https://files.pythonhosted.org/packages/ef/97/09e20663917678a6d60d8e0e29796db175b1165e2079830430342d5298be/rapidfuzz-3.14.3-cp314-cp314-win_arm64.whl", hash = "sha256:576e4b9012a67e0bf54fccb69a7b6c94d4e86a9540a62f1a5144977359133583", size = 833490, upload-time = "2025-11-01T11:54:20.753Z" }, + { url = "https://files.pythonhosted.org/packages/03/1b/6b6084576ba87bf21877c77218a0c97ba98cb285b0c02eaaee3acd7c4513/rapidfuzz-3.14.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:cec3c0da88562727dd5a5a364bd9efeb535400ff0bfb1443156dd139a1dd7b50", size = 1968658, upload-time = "2025-11-01T11:54:22.25Z" }, + { url = "https://files.pythonhosted.org/packages/38/c0/fb02a0db80d95704b0a6469cc394e8c38501abf7e1c0b2afe3261d1510c2/rapidfuzz-3.14.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d1fa009f8b1100e4880868137e7bf0501422898f7674f2adcd85d5a67f041296", size = 1410742, upload-time = "2025-11-01T11:54:23.863Z" }, + { url = "https://files.pythonhosted.org/packages/a4/72/3fbf12819fc6afc8ec75a45204013b40979d068971e535a7f3512b05e765/rapidfuzz-3.14.3-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b86daa7419b5e8b180690efd1fdbac43ff19230803282521c5b5a9c83977655", size = 1382810, upload-time = "2025-11-01T11:54:25.571Z" }, + { url = "https://files.pythonhosted.org/packages/0f/18/0f1991d59bb7eee28922a00f79d83eafa8c7bfb4e8edebf4af2a160e7196/rapidfuzz-3.14.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c7bd1816db05d6c5ffb3a4df0a2b7b56fb8c81ef584d08e37058afa217da91b1", size = 3166349, upload-time = "2025-11-01T11:54:27.195Z" }, + { url = "https://files.pythonhosted.org/packages/0d/f0/baa958b1989c8f88c78bbb329e969440cf330b5a01a982669986495bb980/rapidfuzz-3.14.3-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:33da4bbaf44e9755b0ce192597f3bde7372fe2e381ab305f41b707a95ac57aa7", size = 1214994, upload-time = "2025-11-01T11:54:28.821Z" }, + { url = "https://files.pythonhosted.org/packages/e4/a0/cd12ec71f9b2519a3954febc5740291cceabc64c87bc6433afcb36259f3b/rapidfuzz-3.14.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3fecce764cf5a991ee2195a844196da840aba72029b2612f95ac68a8b74946bf", size = 2403919, upload-time = "2025-11-01T11:54:30.393Z" }, + { url = "https://files.pythonhosted.org/packages/0b/ce/019bd2176c1644098eced4f0595cb4b3ef52e4941ac9a5854f209d0a6e16/rapidfuzz-3.14.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:ecd7453e02cf072258c3a6b8e930230d789d5d46cc849503729f9ce475d0e785", size = 2508346, upload-time = "2025-11-01T11:54:32.048Z" }, + { url = "https://files.pythonhosted.org/packages/23/f8/be16c68e2c9e6c4f23e8f4adbb7bccc9483200087ed28ff76c5312da9b14/rapidfuzz-3.14.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ea188aa00e9bcae8c8411f006a5f2f06c4607a02f24eab0d8dc58566aa911f35", size = 4274105, upload-time = "2025-11-01T11:54:33.701Z" }, + { url = "https://files.pythonhosted.org/packages/a1/d1/5ab148e03f7e6ec8cd220ccf7af74d3aaa4de26dd96df58936beb7cba820/rapidfuzz-3.14.3-cp314-cp314t-win32.whl", hash = "sha256:7ccbf68100c170e9a0581accbe9291850936711548c6688ce3bfb897b8c589ad", size = 1793465, upload-time = "2025-11-01T11:54:35.331Z" }, + { url = "https://files.pythonhosted.org/packages/cd/97/433b2d98e97abd9fff1c470a109b311669f44cdec8d0d5aa250aceaed1fb/rapidfuzz-3.14.3-cp314-cp314t-win_amd64.whl", hash = "sha256:9ec02e62ae765a318d6de38df609c57fc6dacc65c0ed1fd489036834fd8a620c", size = 1623491, upload-time = "2025-11-01T11:54:38.085Z" }, + { url = "https://files.pythonhosted.org/packages/e2/f6/e2176eb94f94892441bce3ddc514c179facb65db245e7ce3356965595b19/rapidfuzz-3.14.3-cp314-cp314t-win_arm64.whl", hash = "sha256:e805e52322ae29aa945baf7168b6c898120fbc16d2b8f940b658a5e9e3999253", size = 851487, upload-time = "2025-11-01T11:54:40.176Z" }, + { url = "https://files.pythonhosted.org/packages/c9/33/b5bd6475c7c27164b5becc9b0e3eb978f1e3640fea590dd3dced6006ee83/rapidfuzz-3.14.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7cf174b52cb3ef5d49e45d0a1133b7e7d0ecf770ed01f97ae9962c5c91d97d23", size = 1888499, upload-time = "2025-11-01T11:54:42.094Z" }, + { url = "https://files.pythonhosted.org/packages/30/d2/89d65d4db4bb931beade9121bc71ad916b5fa9396e807d11b33731494e8e/rapidfuzz-3.14.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:442cba39957a008dfc5bdef21a9c3f4379e30ffb4e41b8555dbaf4887eca9300", size = 1336747, upload-time = "2025-11-01T11:54:43.957Z" }, + { url = "https://files.pythonhosted.org/packages/85/33/cd87d92b23f0b06e8914a61cea6850c6d495ca027f669fab7a379041827a/rapidfuzz-3.14.3-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1faa0f8f76ba75fd7b142c984947c280ef6558b5067af2ae9b8729b0a0f99ede", size = 1352187, upload-time = "2025-11-01T11:54:45.518Z" }, + { url = "https://files.pythonhosted.org/packages/22/20/9d30b4a1ab26aac22fff17d21dec7e9089ccddfe25151d0a8bb57001dc3d/rapidfuzz-3.14.3-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1e6eefec45625c634926a9fd46c9e4f31118ac8f3156fff9494422cee45207e6", size = 3101472, upload-time = "2025-11-01T11:54:47.255Z" }, + { url = "https://files.pythonhosted.org/packages/b1/ad/fa2d3e5c29a04ead7eaa731c7cd1f30f9ec3c77b3a578fdf90280797cbcb/rapidfuzz-3.14.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56fefb4382bb12250f164250240b9dd7772e41c5c8ae976fd598a32292449cc5", size = 1511361, upload-time = "2025-11-01T11:54:49.057Z" }, +] + +[[package]] +name = "rapidocr" +version = "3.9.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorlog" }, + { name = "numpy" }, + { name = "omegaconf" }, + { name = "opencv-python" }, + { name = "pillow" }, + { name = "pyclipper" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "shapely" }, + { name = "six" }, + { name = "tqdm" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/55/ed/0ee9b9281986974be9d2406ae0134c8d7c91d2fc613f16ffda9701eeda6f/rapidocr-3.9.2-py3-none-any.whl", hash = "sha256:04d6b8d151f823d930bd91910555f57bea897c0c44fa6794267b94cf9c1ef9a0", size = 27275208, upload-time = "2026-07-21T10:59:01.599Z" }, +] + +[[package]] +name = "redis" +version = "6.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "async-timeout", marker = "python_full_version < '3.11.3'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0d/d6/e8b92798a5bd67d659d51a18170e91c16ac3b59738d91894651ee255ed49/redis-6.4.0.tar.gz", hash = "sha256:b01bc7282b8444e28ec36b261df5375183bb47a07eb9c603f284e89cbc5ef010", size = 4647399, upload-time = "2025-08-07T08:10:11.441Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/02/89e2ed7e85db6c93dfa9e8f691c5087df4e3551ab39081a4d7c6d1f90e05/redis-6.4.0-py3-none-any.whl", hash = "sha256:f0544fa9604264e9464cdf4814e7d4830f74b165d52f2a330a760a88dd248b7f", size = 279847, upload-time = "2025-08-07T08:10:09.84Z" }, +] + +[[package]] +name = "referencing" +version = "0.37.0" +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/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, +] + +[[package]] +name = "regex" +version = "2026.2.28" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8b/71/41455aa99a5a5ac1eaf311f5d8efd9ce6433c03ac1e0962de163350d0d97/regex-2026.2.28.tar.gz", hash = "sha256:a729e47d418ea11d03469f321aaf67cdee8954cde3ff2cf8403ab87951ad10f2", size = 415184, upload-time = "2026-02-28T02:19:42.792Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/db/8cbfd0ba3f302f2d09dd0019a9fcab74b63fee77a76c937d0e33161fb8c1/regex-2026.2.28-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e621fb7c8dc147419b28e1702f58a0177ff8308a76fa295c71f3e7827849f5d9", size = 488462, upload-time = "2026-02-28T02:16:22.616Z" }, + { url = "https://files.pythonhosted.org/packages/5d/10/ccc22c52802223f2368731964ddd117799e1390ffc39dbb31634a83022ee/regex-2026.2.28-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0d5bef2031cbf38757a0b0bc4298bb4824b6332d28edc16b39247228fbdbad97", size = 290774, upload-time = "2026-02-28T02:16:23.993Z" }, + { url = "https://files.pythonhosted.org/packages/62/b9/6796b3bf3101e64117201aaa3a5a030ec677ecf34b3cd6141b5d5c6c67d5/regex-2026.2.28-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bcb399ed84eabf4282587ba151f2732ad8168e66f1d3f85b1d038868fe547703", size = 288724, upload-time = "2026-02-28T02:16:25.403Z" }, + { url = "https://files.pythonhosted.org/packages/9c/02/291c0ae3f3a10cea941d0f5366da1843d8d1fa8a25b0671e20a0e454bb38/regex-2026.2.28-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7c1b34dfa72f826f535b20712afa9bb3ba580020e834f3c69866c5bddbf10098", size = 791924, upload-time = "2026-02-28T02:16:26.863Z" }, + { url = "https://files.pythonhosted.org/packages/0f/57/f0235cc520d9672742196c5c15098f8f703f2758d48d5a7465a56333e496/regex-2026.2.28-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:851fa70df44325e1e4cdb79c5e676e91a78147b1b543db2aec8734d2add30ec2", size = 860095, upload-time = "2026-02-28T02:16:28.772Z" }, + { url = "https://files.pythonhosted.org/packages/b3/7c/393c94cbedda79a0f5f2435ebd01644aba0b338d327eb24b4aa5b8d6c07f/regex-2026.2.28-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:516604edd17b1c2c3e579cf4e9b25a53bf8fa6e7cedddf1127804d3e0140ca64", size = 906583, upload-time = "2026-02-28T02:16:30.977Z" }, + { url = "https://files.pythonhosted.org/packages/2c/73/a72820f47ca5abf2b5d911d0407ba5178fc52cf9780191ed3a54f5f419a2/regex-2026.2.28-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e7ce83654d1ab701cb619285a18a8e5a889c1216d746ddc710c914ca5fd71022", size = 800234, upload-time = "2026-02-28T02:16:32.55Z" }, + { url = "https://files.pythonhosted.org/packages/34/b3/6e6a4b7b31fa998c4cf159a12cbeaf356386fbd1a8be743b1e80a3da51e4/regex-2026.2.28-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f2791948f7c70bb9335a9102df45e93d428f4b8128020d85920223925d73b9e1", size = 772803, upload-time = "2026-02-28T02:16:34.029Z" }, + { url = "https://files.pythonhosted.org/packages/10/e7/5da0280c765d5a92af5e1cd324b3fe8464303189cbaa449de9a71910e273/regex-2026.2.28-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:03a83cc26aa2acda6b8b9dfe748cf9e84cbd390c424a1de34fdcef58961a297a", size = 781117, upload-time = "2026-02-28T02:16:36.253Z" }, + { url = "https://files.pythonhosted.org/packages/76/39/0b8d7efb256ae34e1b8157acc1afd8758048a1cf0196e1aec2e71fd99f4b/regex-2026.2.28-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:ec6f5674c5dc836994f50f1186dd1fafde4be0666aae201ae2fcc3d29d8adf27", size = 854224, upload-time = "2026-02-28T02:16:38.119Z" }, + { url = "https://files.pythonhosted.org/packages/21/ff/a96d483ebe8fe6d1c67907729202313895d8de8495569ec319c6f29d0438/regex-2026.2.28-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:50c2fc924749543e0eacc93ada6aeeb3ea5f6715825624baa0dccaec771668ae", size = 761898, upload-time = "2026-02-28T02:16:40.333Z" }, + { url = "https://files.pythonhosted.org/packages/89/bd/d4f2e75cb4a54b484e796017e37c0d09d8a0a837de43d17e238adf163f4e/regex-2026.2.28-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:ba55c50f408fb5c346a3a02d2ce0ebc839784e24f7c9684fde328ff063c3cdea", size = 844832, upload-time = "2026-02-28T02:16:41.875Z" }, + { url = "https://files.pythonhosted.org/packages/8a/a7/428a135cf5e15e4e11d1e696eb2bf968362f8ea8a5f237122e96bc2ae950/regex-2026.2.28-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:edb1b1b3a5576c56f08ac46f108c40333f222ebfd5cf63afdfa3aab0791ebe5b", size = 788347, upload-time = "2026-02-28T02:16:43.472Z" }, + { url = "https://files.pythonhosted.org/packages/a9/59/68691428851cf9c9c3707217ab1d9b47cfeec9d153a49919e6c368b9e926/regex-2026.2.28-cp311-cp311-win32.whl", hash = "sha256:948c12ef30ecedb128903c2c2678b339746eb7c689c5c21957c4a23950c96d15", size = 266033, upload-time = "2026-02-28T02:16:45.094Z" }, + { url = "https://files.pythonhosted.org/packages/42/8b/1483de1c57024e89296cbcceb9cccb3f625d416ddb46e570be185c9b05a9/regex-2026.2.28-cp311-cp311-win_amd64.whl", hash = "sha256:fd63453f10d29097cc3dc62d070746523973fb5aa1c66d25f8558bebd47fed61", size = 277978, upload-time = "2026-02-28T02:16:46.75Z" }, + { url = "https://files.pythonhosted.org/packages/a4/36/abec45dc6e7252e3dbc797120496e43bb5730a7abf0d9cb69340696a2f2d/regex-2026.2.28-cp311-cp311-win_arm64.whl", hash = "sha256:00f2b8d9615aa165fdff0a13f1a92049bfad555ee91e20d246a51aa0b556c60a", size = 270340, upload-time = "2026-02-28T02:16:48.626Z" }, + { url = "https://files.pythonhosted.org/packages/07/42/9061b03cf0fc4b5fa2c3984cbbaed54324377e440a5c5a29d29a72518d62/regex-2026.2.28-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fcf26c3c6d0da98fada8ae4ef0aa1c3405a431c0a77eb17306d38a89b02adcd7", size = 489574, upload-time = "2026-02-28T02:16:50.455Z" }, + { url = "https://files.pythonhosted.org/packages/77/83/0c8a5623a233015595e3da499c5a1c13720ac63c107897a6037bb97af248/regex-2026.2.28-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:02473c954af35dd2defeb07e44182f5705b30ea3f351a7cbffa9177beb14da5d", size = 291426, upload-time = "2026-02-28T02:16:52.52Z" }, + { url = "https://files.pythonhosted.org/packages/9e/06/3ef1ac6910dc3295ebd71b1f9bfa737e82cfead211a18b319d45f85ddd09/regex-2026.2.28-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9b65d33a17101569f86d9c5966a8b1d7fbf8afdda5a8aa219301b0a80f58cf7d", size = 289200, upload-time = "2026-02-28T02:16:54.08Z" }, + { url = "https://files.pythonhosted.org/packages/dd/c9/8cc8d850b35ab5650ff6756a1cb85286e2000b66c97520b29c1587455344/regex-2026.2.28-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e71dcecaa113eebcc96622c17692672c2d104b1d71ddf7adeda90da7ddeb26fc", size = 796765, upload-time = "2026-02-28T02:16:55.905Z" }, + { url = "https://files.pythonhosted.org/packages/e9/5d/57702597627fc23278ebf36fbb497ac91c0ce7fec89ac6c81e420ca3e38c/regex-2026.2.28-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:481df4623fa4969c8b11f3433ed7d5e3dc9cec0f008356c3212b3933fb77e3d8", size = 863093, upload-time = "2026-02-28T02:16:58.094Z" }, + { url = "https://files.pythonhosted.org/packages/02/6d/f3ecad537ca2811b4d26b54ca848cf70e04fcfc138667c146a9f3157779c/regex-2026.2.28-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:64e7c6ad614573e0640f271e811a408d79a9e1fe62a46adb602f598df42a818d", size = 909455, upload-time = "2026-02-28T02:17:00.918Z" }, + { url = "https://files.pythonhosted.org/packages/9e/40/bb226f203caa22c1043c1ca79b36340156eca0f6a6742b46c3bb222a3a57/regex-2026.2.28-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6b08a06976ff4fb0d83077022fde3eca06c55432bb997d8c0495b9a4e9872f4", size = 802037, upload-time = "2026-02-28T02:17:02.842Z" }, + { url = "https://files.pythonhosted.org/packages/44/7c/c6d91d8911ac6803b45ca968e8e500c46934e58c0903cbc6d760ee817a0a/regex-2026.2.28-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:864cdd1a2ef5716b0ab468af40139e62ede1b3a53386b375ec0786bb6783fc05", size = 775113, upload-time = "2026-02-28T02:17:04.506Z" }, + { url = "https://files.pythonhosted.org/packages/dc/8d/4a9368d168d47abd4158580b8c848709667b1cd293ff0c0c277279543bd0/regex-2026.2.28-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:511f7419f7afab475fd4d639d4aedfc54205bcb0800066753ef68a59f0f330b5", size = 784194, upload-time = "2026-02-28T02:17:06.888Z" }, + { url = "https://files.pythonhosted.org/packages/cc/bf/2c72ab5d8b7be462cb1651b5cc333da1d0068740342f350fcca3bca31947/regex-2026.2.28-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:b42f7466e32bf15a961cf09f35fa6323cc72e64d3d2c990b10de1274a5da0a59", size = 856846, upload-time = "2026-02-28T02:17:09.11Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f4/6b65c979bb6d09f51bb2d2a7bc85de73c01ec73335d7ddd202dcb8cd1c8f/regex-2026.2.28-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:8710d61737b0c0ce6836b1da7109f20d495e49b3809f30e27e9560be67a257bf", size = 763516, upload-time = "2026-02-28T02:17:11.004Z" }, + { url = "https://files.pythonhosted.org/packages/8e/32/29ea5e27400ee86d2cc2b4e80aa059df04eaf78b4f0c18576ae077aeff68/regex-2026.2.28-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4390c365fd2d45278f45afd4673cb90f7285f5701607e3ad4274df08e36140ae", size = 849278, upload-time = "2026-02-28T02:17:12.693Z" }, + { url = "https://files.pythonhosted.org/packages/1d/91/3233d03b5f865111cd517e1c95ee8b43e8b428d61fa73764a80c9bb6f537/regex-2026.2.28-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cb3b1db8ff6c7b8bf838ab05583ea15230cb2f678e569ab0e3a24d1e8320940b", size = 790068, upload-time = "2026-02-28T02:17:14.9Z" }, + { url = "https://files.pythonhosted.org/packages/76/92/abc706c1fb03b4580a09645b206a3fc032f5a9f457bc1a8038ac555658ab/regex-2026.2.28-cp312-cp312-win32.whl", hash = "sha256:f8ed9a5d4612df9d4de15878f0bc6aa7a268afbe5af21a3fdd97fa19516e978c", size = 266416, upload-time = "2026-02-28T02:17:17.15Z" }, + { url = "https://files.pythonhosted.org/packages/fa/06/2a6f7dff190e5fa9df9fb4acf2fdf17a1aa0f7f54596cba8de608db56b3a/regex-2026.2.28-cp312-cp312-win_amd64.whl", hash = "sha256:01d65fd24206c8e1e97e2e31b286c59009636c022eb5d003f52760b0f42155d4", size = 277297, upload-time = "2026-02-28T02:17:18.723Z" }, + { url = "https://files.pythonhosted.org/packages/b7/f0/58a2484851fadf284458fdbd728f580d55c1abac059ae9f048c63b92f427/regex-2026.2.28-cp312-cp312-win_arm64.whl", hash = "sha256:c0b5ccbb8ffb433939d248707d4a8b31993cb76ab1a0187ca886bf50e96df952", size = 270408, upload-time = "2026-02-28T02:17:20.328Z" }, + { url = "https://files.pythonhosted.org/packages/87/f6/dc9ef48c61b79c8201585bf37fa70cd781977da86e466cd94e8e95d2443b/regex-2026.2.28-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6d63a07e5ec8ce7184452cb00c41c37b49e67dc4f73b2955b5b8e782ea970784", size = 489311, upload-time = "2026-02-28T02:17:22.591Z" }, + { url = "https://files.pythonhosted.org/packages/95/c8/c20390f2232d3f7956f420f4ef1852608ad57aa26c3dd78516cb9f3dc913/regex-2026.2.28-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e59bc8f30414d283ae8ee1617b13d8112e7135cb92830f0ec3688cb29152585a", size = 291285, upload-time = "2026-02-28T02:17:24.355Z" }, + { url = "https://files.pythonhosted.org/packages/d2/a6/ba1068a631ebd71a230e7d8013fcd284b7c89c35f46f34a7da02082141b1/regex-2026.2.28-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:de0cf053139f96219ccfabb4a8dd2d217c8c82cb206c91d9f109f3f552d6b43d", size = 289051, upload-time = "2026-02-28T02:17:26.722Z" }, + { url = "https://files.pythonhosted.org/packages/1d/1b/7cc3b7af4c244c204b7a80924bd3d85aecd9ba5bc82b485c5806ee8cda9e/regex-2026.2.28-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fb4db2f17e6484904f986c5a657cec85574c76b5c5e61c7aae9ffa1bc6224f95", size = 796842, upload-time = "2026-02-28T02:17:29.064Z" }, + { url = "https://files.pythonhosted.org/packages/24/87/26bd03efc60e0d772ac1e7b60a2e6325af98d974e2358f659c507d3c76db/regex-2026.2.28-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:52b017b35ac2214d0db5f4f90e303634dc44e4aba4bd6235a27f97ecbe5b0472", size = 863083, upload-time = "2026-02-28T02:17:31.363Z" }, + { url = "https://files.pythonhosted.org/packages/ae/54/aeaf4afb1aa0a65e40de52a61dc2ac5b00a83c6cb081c8a1d0dda74f3010/regex-2026.2.28-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:69fc560ccbf08a09dc9b52ab69cacfae51e0ed80dc5693078bdc97db2f91ae96", size = 909412, upload-time = "2026-02-28T02:17:33.248Z" }, + { url = "https://files.pythonhosted.org/packages/12/2f/049901def913954e640d199bbc6a7ca2902b6aeda0e5da9d17f114100ec2/regex-2026.2.28-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e61eea47230eba62a31f3e8a0e3164d0f37ef9f40529fb2c79361bc6b53d2a92", size = 802101, upload-time = "2026-02-28T02:17:35.053Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a5/512fb9ff7f5b15ea204bb1967ebb649059446decacccb201381f9fa6aad4/regex-2026.2.28-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4f5c0b182ad4269e7381b7c27fdb0408399881f7a92a4624fd5487f2971dfc11", size = 775260, upload-time = "2026-02-28T02:17:37.692Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a8/9a92935878aba19bd72706b9db5646a6f993d99b3f6ed42c02ec8beb1d61/regex-2026.2.28-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:96f6269a2882fbb0ee76967116b83679dc628e68eaea44e90884b8d53d833881", size = 784311, upload-time = "2026-02-28T02:17:39.855Z" }, + { url = "https://files.pythonhosted.org/packages/09/d3/fc51a8a738a49a6b6499626580554c9466d3ea561f2b72cfdc72e4149773/regex-2026.2.28-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b5acd4b6a95f37c3c3828e5d053a7d4edaedb85de551db0153754924cb7c83e3", size = 856876, upload-time = "2026-02-28T02:17:42.317Z" }, + { url = "https://files.pythonhosted.org/packages/08/b7/2e641f3d084b120ca4c52e8c762a78da0b32bf03ef546330db3e2635dc5f/regex-2026.2.28-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2234059cfe33d9813a3677ef7667999caea9eeaa83fef98eb6ce15c6cf9e0215", size = 763632, upload-time = "2026-02-28T02:17:45.073Z" }, + { url = "https://files.pythonhosted.org/packages/fe/6d/0009021d97e79ee99f3d8641f0a8d001eed23479ade4c3125a5480bf3e2d/regex-2026.2.28-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:c15af43c72a7fb0c97cbc66fa36a43546eddc5c06a662b64a0cbf30d6ac40944", size = 849320, upload-time = "2026-02-28T02:17:47.192Z" }, + { url = "https://files.pythonhosted.org/packages/05/7a/51cfbad5758f8edae430cb21961a9c8d04bce1dae4d2d18d4186eec7cfa1/regex-2026.2.28-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9185cc63359862a6e80fe97f696e04b0ad9a11c4ac0a4a927f979f611bfe3768", size = 790152, upload-time = "2026-02-28T02:17:49.067Z" }, + { url = "https://files.pythonhosted.org/packages/90/3d/a83e2b6b3daa142acb8c41d51de3876186307d5cb7490087031747662500/regex-2026.2.28-cp313-cp313-win32.whl", hash = "sha256:fb66e5245db9652abd7196ace599b04d9c0e4aa7c8f0e2803938377835780081", size = 266398, upload-time = "2026-02-28T02:17:50.744Z" }, + { url = "https://files.pythonhosted.org/packages/85/4f/16e9ebb1fe5425e11b9596c8d57bf8877dcb32391da0bfd33742e3290637/regex-2026.2.28-cp313-cp313-win_amd64.whl", hash = "sha256:71a911098be38c859ceb3f9a9ce43f4ed9f4c6720ad8684a066ea246b76ad9ff", size = 277282, upload-time = "2026-02-28T02:17:53.074Z" }, + { url = "https://files.pythonhosted.org/packages/07/b4/92851335332810c5a89723bf7a7e35c7209f90b7d4160024501717b28cc9/regex-2026.2.28-cp313-cp313-win_arm64.whl", hash = "sha256:39bb5727650b9a0275c6a6690f9bb3fe693a7e6cc5c3155b1240aedf8926423e", size = 270382, upload-time = "2026-02-28T02:17:54.888Z" }, + { url = "https://files.pythonhosted.org/packages/24/07/6c7e4cec1e585959e96cbc24299d97e4437a81173217af54f1804994e911/regex-2026.2.28-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:97054c55db06ab020342cc0d35d6f62a465fa7662871190175f1ad6c655c028f", size = 492541, upload-time = "2026-02-28T02:17:56.813Z" }, + { url = "https://files.pythonhosted.org/packages/7c/13/55eb22ada7f43d4f4bb3815b6132183ebc331c81bd496e2d1f3b8d862e0d/regex-2026.2.28-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0d25a10811de831c2baa6aef3c0be91622f44dd8d31dd12e69f6398efb15e48b", size = 292984, upload-time = "2026-02-28T02:17:58.538Z" }, + { url = "https://files.pythonhosted.org/packages/5b/11/c301f8cb29ce9644a5ef85104c59244e6e7e90994a0f458da4d39baa8e17/regex-2026.2.28-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d6cfe798d8da41bb1862ed6e0cba14003d387c3c0c4a5d45591076ae9f0ce2f8", size = 291509, upload-time = "2026-02-28T02:18:00.208Z" }, + { url = "https://files.pythonhosted.org/packages/b5/43/aabe384ec1994b91796e903582427bc2ffaed9c4103819ed3c16d8e749f3/regex-2026.2.28-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fd0ce43e71d825b7c0661f9c54d4d74bd97c56c3fd102a8985bcfea48236bacb", size = 809429, upload-time = "2026-02-28T02:18:02.328Z" }, + { url = "https://files.pythonhosted.org/packages/04/b8/8d2d987a816720c4f3109cee7c06a4b24ad0e02d4fc74919ab619e543737/regex-2026.2.28-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:00945d007fd74a9084d2ab79b695b595c6b7ba3698972fadd43e23230c6979c1", size = 869422, upload-time = "2026-02-28T02:18:04.23Z" }, + { url = "https://files.pythonhosted.org/packages/fc/ad/2c004509e763c0c3719f97c03eca26473bffb3868d54c5f280b8cd4f9e3d/regex-2026.2.28-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bec23c11cbbf09a4df32fe50d57cbdd777bc442269b6e39a1775654f1c95dee2", size = 915175, upload-time = "2026-02-28T02:18:06.791Z" }, + { url = "https://files.pythonhosted.org/packages/55/c2/fd429066da487ef555a9da73bf214894aec77fc8c66a261ee355a69871a8/regex-2026.2.28-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5cdcc17d935c8f9d3f4db5c2ebe2640c332e3822ad5d23c2f8e0228e6947943a", size = 812044, upload-time = "2026-02-28T02:18:08.736Z" }, + { url = "https://files.pythonhosted.org/packages/5b/ca/feedb7055c62a3f7f659971bf45f0e0a87544b6b0cf462884761453f97c5/regex-2026.2.28-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a448af01e3d8031c89c5d902040b124a5e921a25c4e5e07a861ca591ce429341", size = 782056, upload-time = "2026-02-28T02:18:10.777Z" }, + { url = "https://files.pythonhosted.org/packages/95/30/1aa959ed0d25c1dd7dd5047ea8ba482ceaef38ce363c401fd32a6b923e60/regex-2026.2.28-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:10d28e19bd4888e4abf43bd3925f3c134c52fdf7259219003588a42e24c2aa25", size = 798743, upload-time = "2026-02-28T02:18:13.025Z" }, + { url = "https://files.pythonhosted.org/packages/3b/1f/dadb9cf359004784051c897dcf4d5d79895f73a1bbb7b827abaa4814ae80/regex-2026.2.28-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:99985a2c277dcb9ccb63f937451af5d65177af1efdeb8173ac55b61095a0a05c", size = 864633, upload-time = "2026-02-28T02:18:16.84Z" }, + { url = "https://files.pythonhosted.org/packages/a7/f1/b9a25eb24e1cf79890f09e6ec971ee5b511519f1851de3453bc04f6c902b/regex-2026.2.28-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:e1e7b24cb3ae9953a560c563045d1ba56ee4749fbd05cf21ba571069bd7be81b", size = 770862, upload-time = "2026-02-28T02:18:18.892Z" }, + { url = "https://files.pythonhosted.org/packages/02/9a/c5cb10b7aa6f182f9247a30cc9527e326601f46f4df864ac6db588d11fcd/regex-2026.2.28-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:d8511a01d0e4ee1992eb3ba19e09bc1866fe03f05129c3aec3fdc4cbc77aad3f", size = 854788, upload-time = "2026-02-28T02:18:21.475Z" }, + { url = "https://files.pythonhosted.org/packages/0a/50/414ba0731c4bd40b011fa4703b2cc86879ec060c64f2a906e65a56452589/regex-2026.2.28-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:aaffaecffcd2479ce87aa1e74076c221700b7c804e48e98e62500ee748f0f550", size = 800184, upload-time = "2026-02-28T02:18:23.492Z" }, + { url = "https://files.pythonhosted.org/packages/69/50/0c7290987f97e7e6830b0d853f69dc4dc5852c934aae63e7fdcd76b4c383/regex-2026.2.28-cp313-cp313t-win32.whl", hash = "sha256:ef77bdde9c9eba3f7fa5b58084b29bbcc74bcf55fdbeaa67c102a35b5bd7e7cc", size = 269137, upload-time = "2026-02-28T02:18:25.375Z" }, + { url = "https://files.pythonhosted.org/packages/68/80/ef26ff90e74ceb4051ad6efcbbb8a4be965184a57e879ebcbdef327d18fa/regex-2026.2.28-cp313-cp313t-win_amd64.whl", hash = "sha256:98adf340100cbe6fbaf8e6dc75e28f2c191b1be50ffefe292fb0e6f6eefdb0d8", size = 280682, upload-time = "2026-02-28T02:18:27.205Z" }, + { url = "https://files.pythonhosted.org/packages/69/8b/fbad9c52e83ffe8f97e3ed1aa0516e6dff6bb633a41da9e64645bc7efdc5/regex-2026.2.28-cp313-cp313t-win_arm64.whl", hash = "sha256:2fb950ac1d88e6b6a9414381f403797b236f9fa17e1eee07683af72b1634207b", size = 271735, upload-time = "2026-02-28T02:18:29.015Z" }, + { url = "https://files.pythonhosted.org/packages/cf/03/691015f7a7cb1ed6dacb2ea5de5682e4858e05a4c5506b2839cd533bbcd6/regex-2026.2.28-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:78454178c7df31372ea737996fb7f36b3c2c92cccc641d251e072478afb4babc", size = 489497, upload-time = "2026-02-28T02:18:30.889Z" }, + { url = "https://files.pythonhosted.org/packages/c6/ba/8db8fd19afcbfa0e1036eaa70c05f20ca8405817d4ad7a38a6b4c2f031ac/regex-2026.2.28-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:5d10303dd18cedfd4d095543998404df656088240bcfd3cd20a8f95b861f74bd", size = 291295, upload-time = "2026-02-28T02:18:33.426Z" }, + { url = "https://files.pythonhosted.org/packages/5a/79/9aa0caf089e8defef9b857b52fc53801f62ff868e19e5c83d4a96612eba1/regex-2026.2.28-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:19a9c9e0a8f24f39d575a6a854d516b48ffe4cbdcb9de55cb0570a032556ecff", size = 289275, upload-time = "2026-02-28T02:18:35.247Z" }, + { url = "https://files.pythonhosted.org/packages/eb/26/ee53117066a30ef9c883bf1127eece08308ccf8ccd45c45a966e7a665385/regex-2026.2.28-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:09500be324f49b470d907b3ef8af9afe857f5cca486f853853f7945ddbf75911", size = 797176, upload-time = "2026-02-28T02:18:37.15Z" }, + { url = "https://files.pythonhosted.org/packages/05/1b/67fb0495a97259925f343ae78b5d24d4a6624356ae138b57f18bd43006e4/regex-2026.2.28-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fb1c4ff62277d87a7335f2c1ea4e0387b8f2b3ad88a64efd9943906aafad4f33", size = 863813, upload-time = "2026-02-28T02:18:39.478Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/93ac9bbafc53618091c685c7ed40239a90bf9f2a82c983f0baa97cb7ae07/regex-2026.2.28-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b8b3f1be1738feadc69f62daa250c933e85c6f34fa378f54a7ff43807c1b9117", size = 908678, upload-time = "2026-02-28T02:18:41.619Z" }, + { url = "https://files.pythonhosted.org/packages/c7/7a/a8f5e0561702b25239846a16349feece59712ae20598ebb205580332a471/regex-2026.2.28-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc8ed8c3f41c27acb83f7b6a9eb727a73fc6663441890c5cb3426a5f6a91ce7d", size = 801528, upload-time = "2026-02-28T02:18:43.624Z" }, + { url = "https://files.pythonhosted.org/packages/96/5d/ed6d4cbde80309854b1b9f42d9062fee38ade15f7eb4909f6ef2440403b5/regex-2026.2.28-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa539be029844c0ce1114762d2952ab6cfdd7c7c9bd72e0db26b94c3c36dcc5a", size = 775373, upload-time = "2026-02-28T02:18:46.102Z" }, + { url = "https://files.pythonhosted.org/packages/6a/e9/6e53c34e8068b9deec3e87210086ecb5b9efebdefca6b0d3fa43d66dcecb/regex-2026.2.28-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7900157786428a79615a8264dac1f12c9b02957c473c8110c6b1f972dcecaddf", size = 784859, upload-time = "2026-02-28T02:18:48.269Z" }, + { url = "https://files.pythonhosted.org/packages/48/3c/736e1c7ca7f0dcd2ae33819888fdc69058a349b7e5e84bc3e2f296bbf794/regex-2026.2.28-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:0b1d2b07614d95fa2bf8a63fd1e98bd8fa2b4848dc91b1efbc8ba219fdd73952", size = 857813, upload-time = "2026-02-28T02:18:50.576Z" }, + { url = "https://files.pythonhosted.org/packages/6e/7c/48c4659ad9da61f58e79dbe8c05223e0006696b603c16eb6b5cbfbb52c27/regex-2026.2.28-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:b389c61aa28a79c2e0527ac36da579869c2e235a5b208a12c5b5318cda2501d8", size = 763705, upload-time = "2026-02-28T02:18:52.59Z" }, + { url = "https://files.pythonhosted.org/packages/cf/a1/bc1c261789283128165f71b71b4b221dd1b79c77023752a6074c102f18d8/regex-2026.2.28-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f467cb602f03fbd1ab1908f68b53c649ce393fde056628dc8c7e634dab6bfc07", size = 848734, upload-time = "2026-02-28T02:18:54.595Z" }, + { url = "https://files.pythonhosted.org/packages/10/d8/979407faf1397036e25a5ae778157366a911c0f382c62501009f4957cf86/regex-2026.2.28-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e8c8cb2deba42f5ec1ede46374e990f8adc5e6456a57ac1a261b19be6f28e4e6", size = 789871, upload-time = "2026-02-28T02:18:57.34Z" }, + { url = "https://files.pythonhosted.org/packages/03/23/da716821277115fcb1f4e3de1e5dc5023a1e6533598c486abf5448612579/regex-2026.2.28-cp314-cp314-win32.whl", hash = "sha256:9036b400b20e4858d56d117108d7813ed07bb7803e3eed766675862131135ca6", size = 271825, upload-time = "2026-02-28T02:18:59.202Z" }, + { url = "https://files.pythonhosted.org/packages/91/ff/90696f535d978d5f16a52a419be2770a8d8a0e7e0cfecdbfc31313df7fab/regex-2026.2.28-cp314-cp314-win_amd64.whl", hash = "sha256:1d367257cd86c1cbb97ea94e77b373a0bbc2224976e247f173d19e8f18b4afa7", size = 280548, upload-time = "2026-02-28T02:19:01.049Z" }, + { url = "https://files.pythonhosted.org/packages/69/f9/5e1b5652fc0af3fcdf7677e7df3ad2a0d47d669b34ac29a63bb177bb731b/regex-2026.2.28-cp314-cp314-win_arm64.whl", hash = "sha256:5e68192bb3a1d6fb2836da24aa494e413ea65853a21505e142e5b1064a595f3d", size = 273444, upload-time = "2026-02-28T02:19:03.255Z" }, + { url = "https://files.pythonhosted.org/packages/d3/eb/8389f9e940ac89bcf58d185e230a677b4fd07c5f9b917603ad5c0f8fa8fe/regex-2026.2.28-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:a5dac14d0872eeb35260a8e30bac07ddf22adc1e3a0635b52b02e180d17c9c7e", size = 492546, upload-time = "2026-02-28T02:19:05.378Z" }, + { url = "https://files.pythonhosted.org/packages/7b/c7/09441d27ce2a6fa6a61ea3150ea4639c1dcda9b31b2ea07b80d6937b24dd/regex-2026.2.28-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:ec0c608b7a7465ffadb344ed7c987ff2f11ee03f6a130b569aa74d8a70e8333c", size = 292986, upload-time = "2026-02-28T02:19:07.24Z" }, + { url = "https://files.pythonhosted.org/packages/fb/69/4144b60ed7760a6bd235e4087041f487aa4aa62b45618ce018b0c14833ea/regex-2026.2.28-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c7815afb0ca45456613fdaf60ea9c993715511c8d53a83bc468305cbc0ee23c7", size = 291518, upload-time = "2026-02-28T02:19:09.698Z" }, + { url = "https://files.pythonhosted.org/packages/2d/be/77e5426cf5948c82f98c53582009ca9e94938c71f73a8918474f2e2990bb/regex-2026.2.28-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b059e71ec363968671693a78c5053bd9cb2fe410f9b8e4657e88377ebd603a2e", size = 809464, upload-time = "2026-02-28T02:19:12.494Z" }, + { url = "https://files.pythonhosted.org/packages/45/99/2c8c5ac90dc7d05c6e7d8e72c6a3599dc08cd577ac476898e91ca787d7f1/regex-2026.2.28-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b8cf76f1a29f0e99dcfd7aef1551a9827588aae5a737fe31442021165f1920dc", size = 869553, upload-time = "2026-02-28T02:19:15.151Z" }, + { url = "https://files.pythonhosted.org/packages/53/34/daa66a342f0271e7737003abf6c3097aa0498d58c668dbd88362ef94eb5d/regex-2026.2.28-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:180e08a435a0319e6a4821c3468da18dc7001987e1c17ae1335488dfe7518dd8", size = 915289, upload-time = "2026-02-28T02:19:17.331Z" }, + { url = "https://files.pythonhosted.org/packages/c5/c7/e22c2aaf0a12e7e22ab19b004bb78d32ca1ecc7ef245949935463c5567de/regex-2026.2.28-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1e496956106fd59ba6322a8ea17141a27c5040e5ee8f9433ae92d4e5204462a0", size = 812156, upload-time = "2026-02-28T02:19:20.011Z" }, + { url = "https://files.pythonhosted.org/packages/7f/bb/2dc18c1efd9051cf389cd0d7a3a4d90f6804b9fff3a51b5dc3c85b935f71/regex-2026.2.28-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bba2b18d70eeb7b79950f12f633beeecd923f7c9ad6f6bae28e59b4cb3ab046b", size = 782215, upload-time = "2026-02-28T02:19:22.047Z" }, + { url = "https://files.pythonhosted.org/packages/17/1e/9e4ec9b9013931faa32226ec4aa3c71fe664a6d8a2b91ac56442128b332f/regex-2026.2.28-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6db7bfae0f8a2793ff1f7021468ea55e2699d0790eb58ee6ab36ae43aa00bc5b", size = 798925, upload-time = "2026-02-28T02:19:24.173Z" }, + { url = "https://files.pythonhosted.org/packages/71/57/a505927e449a9ccb41e2cc8d735e2abe3444b0213d1cf9cb364a8c1f2524/regex-2026.2.28-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:d0b02e8b7e5874b48ae0f077ecca61c1a6a9f9895e9c6dfb191b55b242862033", size = 864701, upload-time = "2026-02-28T02:19:26.376Z" }, + { url = "https://files.pythonhosted.org/packages/a6/ad/c62cb60cdd93e13eac5b3d9d6bd5d284225ed0e3329426f94d2552dd7cca/regex-2026.2.28-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:25b6eb660c5cf4b8c3407a1ed462abba26a926cc9965e164268a3267bcc06a43", size = 770899, upload-time = "2026-02-28T02:19:29.38Z" }, + { url = "https://files.pythonhosted.org/packages/3c/5a/874f861f5c3d5ab99633e8030dee1bc113db8e0be299d1f4b07f5b5ec349/regex-2026.2.28-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:5a932ea8ad5d0430351ff9c76c8db34db0d9f53c1d78f06022a21f4e290c5c18", size = 854727, upload-time = "2026-02-28T02:19:31.494Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ca/d2c03b0efde47e13db895b975b2be6a73ed90b8ba963677927283d43bf74/regex-2026.2.28-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:1c2c95e1a2b0f89d01e821ff4de1be4b5d73d1f4b0bf679fa27c1ad8d2327f1a", size = 800366, upload-time = "2026-02-28T02:19:34.248Z" }, + { url = "https://files.pythonhosted.org/packages/14/bd/ee13b20b763b8989f7c75d592bfd5de37dc1181814a2a2747fedcf97e3ba/regex-2026.2.28-cp314-cp314t-win32.whl", hash = "sha256:bbb882061f742eb5d46f2f1bd5304055be0a66b783576de3d7eef1bed4778a6e", size = 274936, upload-time = "2026-02-28T02:19:36.313Z" }, + { url = "https://files.pythonhosted.org/packages/cb/e7/d8020e39414c93af7f0d8688eabcecece44abfd5ce314b21dfda0eebd3d8/regex-2026.2.28-cp314-cp314t-win_amd64.whl", hash = "sha256:6591f281cb44dc13de9585b552cec6fc6cf47fb2fe7a48892295ee9bc4a612f9", size = 284779, upload-time = "2026-02-28T02:19:38.625Z" }, + { url = "https://files.pythonhosted.org/packages/13/c0/ad225f4a405827486f1955283407cf758b6d2fb966712644c5f5aef33d1b/regex-2026.2.28-cp314-cp314t-win_arm64.whl", hash = "sha256:dee50f1be42222f89767b64b283283ef963189da0dda4a515aa54a5563c62dec", size = 275010, upload-time = "2026-02-28T02:19:40.65Z" }, +] + +[[package]] +name = "requests" +version = "2.34.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, +] + +[[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, upload-time = "2023-05-01T04:11:33.229Z" } +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, upload-time = "2023-05-01T04:11:28.427Z" }, +] + +[[package]] +name = "rich" +version = "14.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/c6/f3b320c27991c46f43ee9d856302c70dc2d0fb2dba4842ff739d5f46b393/rich-14.3.3.tar.gz", hash = "sha256:b8daa0b9e4eef54dd8cf7c86c03713f53241884e814f4e2f5fb342fe520f639b", size = 230582, upload-time = "2026-02-19T17:23:12.474Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/25/b208c5683343959b670dc001595f2f3737e051da617f66c31f7c4fa93abc/rich-14.3.3-py3-none-any.whl", hash = "sha256:793431c1f8619afa7d3b52b2cdec859562b950ea0d4b6b505397612db8d5362d", size = 310458, upload-time = "2026-02-19T17:23:13.732Z" }, +] + +[[package]] +name = "rpds-py" +version = "0.30.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/6e/f964e88b3d2abee2a82c1ac8366da848fce1c6d834dc2132c3fda3970290/rpds_py-0.30.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a2bffea6a4ca9f01b3f8e548302470306689684e61602aa3d141e34da06cf425", size = 370157, upload-time = "2025-11-30T20:21:53.789Z" }, + { url = "https://files.pythonhosted.org/packages/94/ba/24e5ebb7c1c82e74c4e4f33b2112a5573ddc703915b13a073737b59b86e0/rpds_py-0.30.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc4f992dfe1e2bc3ebc7444f6c7051b4bc13cd8e33e43511e8ffd13bf407010d", size = 359676, upload-time = "2025-11-30T20:21:55.475Z" }, + { url = "https://files.pythonhosted.org/packages/84/86/04dbba1b087227747d64d80c3b74df946b986c57af0a9f0c98726d4d7a3b/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:422c3cb9856d80b09d30d2eb255d0754b23e090034e1deb4083f8004bd0761e4", size = 389938, upload-time = "2025-11-30T20:21:57.079Z" }, + { url = "https://files.pythonhosted.org/packages/42/bb/1463f0b1722b7f45431bdd468301991d1328b16cffe0b1c2918eba2c4eee/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07ae8a593e1c3c6b82ca3292efbe73c30b61332fd612e05abee07c79359f292f", size = 402932, upload-time = "2025-11-30T20:21:58.47Z" }, + { url = "https://files.pythonhosted.org/packages/99/ee/2520700a5c1f2d76631f948b0736cdf9b0acb25abd0ca8e889b5c62ac2e3/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12f90dd7557b6bd57f40abe7747e81e0c0b119bef015ea7726e69fe550e394a4", size = 525830, upload-time = "2025-11-30T20:21:59.699Z" }, + { url = "https://files.pythonhosted.org/packages/e0/ad/bd0331f740f5705cc555a5e17fdf334671262160270962e69a2bdef3bf76/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99b47d6ad9a6da00bec6aabe5a6279ecd3c06a329d4aa4771034a21e335c3a97", size = 412033, upload-time = "2025-11-30T20:22:00.991Z" }, + { url = "https://files.pythonhosted.org/packages/f8/1e/372195d326549bb51f0ba0f2ecb9874579906b97e08880e7a65c3bef1a99/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33f559f3104504506a44bb666b93a33f5d33133765b0c216a5bf2f1e1503af89", size = 390828, upload-time = "2025-11-30T20:22:02.723Z" }, + { url = "https://files.pythonhosted.org/packages/ab/2b/d88bb33294e3e0c76bc8f351a3721212713629ffca1700fa94979cb3eae8/rpds_py-0.30.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:946fe926af6e44f3697abbc305ea168c2c31d3e3ef1058cf68f379bf0335a78d", size = 404683, upload-time = "2025-11-30T20:22:04.367Z" }, + { url = "https://files.pythonhosted.org/packages/50/32/c759a8d42bcb5289c1fac697cd92f6fe01a018dd937e62ae77e0e7f15702/rpds_py-0.30.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:495aeca4b93d465efde585977365187149e75383ad2684f81519f504f5c13038", size = 421583, upload-time = "2025-11-30T20:22:05.814Z" }, + { url = "https://files.pythonhosted.org/packages/2b/81/e729761dbd55ddf5d84ec4ff1f47857f4374b0f19bdabfcf929164da3e24/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9a0ca5da0386dee0655b4ccdf46119df60e0f10da268d04fe7cc87886872ba7", size = 572496, upload-time = "2025-11-30T20:22:07.713Z" }, + { url = "https://files.pythonhosted.org/packages/14/f6/69066a924c3557c9c30baa6ec3a0aa07526305684c6f86c696b08860726c/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8d6d1cc13664ec13c1b84241204ff3b12f9bb82464b8ad6e7a5d3486975c2eed", size = 598669, upload-time = "2025-11-30T20:22:09.312Z" }, + { url = "https://files.pythonhosted.org/packages/5f/48/905896b1eb8a05630d20333d1d8ffd162394127b74ce0b0784ae04498d32/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3896fa1be39912cf0757753826bc8bdc8ca331a28a7c4ae46b7a21280b06bb85", size = 561011, upload-time = "2025-11-30T20:22:11.309Z" }, + { url = "https://files.pythonhosted.org/packages/22/16/cd3027c7e279d22e5eb431dd3c0fbc677bed58797fe7581e148f3f68818b/rpds_py-0.30.0-cp311-cp311-win32.whl", hash = "sha256:55f66022632205940f1827effeff17c4fa7ae1953d2b74a8581baaefb7d16f8c", size = 221406, upload-time = "2025-11-30T20:22:13.101Z" }, + { url = "https://files.pythonhosted.org/packages/fa/5b/e7b7aa136f28462b344e652ee010d4de26ee9fd16f1bfd5811f5153ccf89/rpds_py-0.30.0-cp311-cp311-win_amd64.whl", hash = "sha256:a51033ff701fca756439d641c0ad09a41d9242fa69121c7d8769604a0a629825", size = 236024, upload-time = "2025-11-30T20:22:14.853Z" }, + { url = "https://files.pythonhosted.org/packages/14/a6/364bba985e4c13658edb156640608f2c9e1d3ea3c81b27aa9d889fff0e31/rpds_py-0.30.0-cp311-cp311-win_arm64.whl", hash = "sha256:47b0ef6231c58f506ef0b74d44e330405caa8428e770fec25329ed2cb971a229", size = 229069, upload-time = "2025-11-30T20:22:16.577Z" }, + { url = "https://files.pythonhosted.org/packages/03/e7/98a2f4ac921d82f33e03f3835f5bf3a4a40aa1bfdc57975e74a97b2b4bdd/rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad", size = 375086, upload-time = "2025-11-30T20:22:17.93Z" }, + { url = "https://files.pythonhosted.org/packages/4d/a1/bca7fd3d452b272e13335db8d6b0b3ecde0f90ad6f16f3328c6fb150c889/rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05", size = 359053, upload-time = "2025-11-30T20:22:19.297Z" }, + { url = "https://files.pythonhosted.org/packages/65/1c/ae157e83a6357eceff62ba7e52113e3ec4834a84cfe07fa4b0757a7d105f/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28", size = 390763, upload-time = "2025-11-30T20:22:21.661Z" }, + { url = "https://files.pythonhosted.org/packages/d4/36/eb2eb8515e2ad24c0bd43c3ee9cd74c33f7ca6430755ccdb240fd3144c44/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd", size = 408951, upload-time = "2025-11-30T20:22:23.408Z" }, + { url = "https://files.pythonhosted.org/packages/d6/65/ad8dc1784a331fabbd740ef6f71ce2198c7ed0890dab595adb9ea2d775a1/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f", size = 514622, upload-time = "2025-11-30T20:22:25.16Z" }, + { url = "https://files.pythonhosted.org/packages/63/8e/0cfa7ae158e15e143fe03993b5bcd743a59f541f5952e1546b1ac1b5fd45/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1", size = 414492, upload-time = "2025-11-30T20:22:26.505Z" }, + { url = "https://files.pythonhosted.org/packages/60/1b/6f8f29f3f995c7ffdde46a626ddccd7c63aefc0efae881dc13b6e5d5bb16/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23", size = 394080, upload-time = "2025-11-30T20:22:27.934Z" }, + { url = "https://files.pythonhosted.org/packages/6d/d5/a266341051a7a3ca2f4b750a3aa4abc986378431fc2da508c5034d081b70/rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6", size = 408680, upload-time = "2025-11-30T20:22:29.341Z" }, + { url = "https://files.pythonhosted.org/packages/10/3b/71b725851df9ab7a7a4e33cf36d241933da66040d195a84781f49c50490c/rpds_py-0.30.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51", size = 423589, upload-time = "2025-11-30T20:22:31.469Z" }, + { url = "https://files.pythonhosted.org/packages/00/2b/e59e58c544dc9bd8bd8384ecdb8ea91f6727f0e37a7131baeff8d6f51661/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5", size = 573289, upload-time = "2025-11-30T20:22:32.997Z" }, + { url = "https://files.pythonhosted.org/packages/da/3e/a18e6f5b460893172a7d6a680e86d3b6bc87a54c1f0b03446a3c8c7b588f/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e", size = 599737, upload-time = "2025-11-30T20:22:34.419Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e2/714694e4b87b85a18e2c243614974413c60aa107fd815b8cbc42b873d1d7/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394", size = 563120, upload-time = "2025-11-30T20:22:35.903Z" }, + { url = "https://files.pythonhosted.org/packages/6f/ab/d5d5e3bcedb0a77f4f613706b750e50a5a3ba1c15ccd3665ecc636c968fd/rpds_py-0.30.0-cp312-cp312-win32.whl", hash = "sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf", size = 223782, upload-time = "2025-11-30T20:22:37.271Z" }, + { url = "https://files.pythonhosted.org/packages/39/3b/f786af9957306fdc38a74cef405b7b93180f481fb48453a114bb6465744a/rpds_py-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b", size = 240463, upload-time = "2025-11-30T20:22:39.021Z" }, + { url = "https://files.pythonhosted.org/packages/f3/d2/b91dc748126c1559042cfe41990deb92c4ee3e2b415f6b5234969ffaf0cc/rpds_py-0.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e", size = 230868, upload-time = "2025-11-30T20:22:40.493Z" }, + { url = "https://files.pythonhosted.org/packages/ed/dc/d61221eb88ff410de3c49143407f6f3147acf2538c86f2ab7ce65ae7d5f9/rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2", size = 374887, upload-time = "2025-11-30T20:22:41.812Z" }, + { url = "https://files.pythonhosted.org/packages/fd/32/55fb50ae104061dbc564ef15cc43c013dc4a9f4527a1f4d99baddf56fe5f/rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8", size = 358904, upload-time = "2025-11-30T20:22:43.479Z" }, + { url = "https://files.pythonhosted.org/packages/58/70/faed8186300e3b9bdd138d0273109784eea2396c68458ed580f885dfe7ad/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4", size = 389945, upload-time = "2025-11-30T20:22:44.819Z" }, + { url = "https://files.pythonhosted.org/packages/bd/a8/073cac3ed2c6387df38f71296d002ab43496a96b92c823e76f46b8af0543/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136", size = 407783, upload-time = "2025-11-30T20:22:46.103Z" }, + { url = "https://files.pythonhosted.org/packages/77/57/5999eb8c58671f1c11eba084115e77a8899d6e694d2a18f69f0ba471ec8b/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7", size = 515021, upload-time = "2025-11-30T20:22:47.458Z" }, + { url = "https://files.pythonhosted.org/packages/e0/af/5ab4833eadc36c0a8ed2bc5c0de0493c04f6c06de223170bd0798ff98ced/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2", size = 414589, upload-time = "2025-11-30T20:22:48.872Z" }, + { url = "https://files.pythonhosted.org/packages/b7/de/f7192e12b21b9e9a68a6d0f249b4af3fdcdff8418be0767a627564afa1f1/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6", size = 394025, upload-time = "2025-11-30T20:22:50.196Z" }, + { url = "https://files.pythonhosted.org/packages/91/c4/fc70cd0249496493500e7cc2de87504f5aa6509de1e88623431fec76d4b6/rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e", size = 408895, upload-time = "2025-11-30T20:22:51.87Z" }, + { url = "https://files.pythonhosted.org/packages/58/95/d9275b05ab96556fefff73a385813eb66032e4c99f411d0795372d9abcea/rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d", size = 422799, upload-time = "2025-11-30T20:22:53.341Z" }, + { url = "https://files.pythonhosted.org/packages/06/c1/3088fc04b6624eb12a57eb814f0d4997a44b0d208d6cace713033ff1a6ba/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7", size = 572731, upload-time = "2025-11-30T20:22:54.778Z" }, + { url = "https://files.pythonhosted.org/packages/d8/42/c612a833183b39774e8ac8fecae81263a68b9583ee343db33ab571a7ce55/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31", size = 599027, upload-time = "2025-11-30T20:22:56.212Z" }, + { url = "https://files.pythonhosted.org/packages/5f/60/525a50f45b01d70005403ae0e25f43c0384369ad24ffe46e8d9068b50086/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95", size = 563020, upload-time = "2025-11-30T20:22:58.2Z" }, + { url = "https://files.pythonhosted.org/packages/0b/5d/47c4655e9bcd5ca907148535c10e7d489044243cc9941c16ed7cd53be91d/rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d", size = 223139, upload-time = "2025-11-30T20:23:00.209Z" }, + { url = "https://files.pythonhosted.org/packages/f2/e1/485132437d20aa4d3e1d8b3fb5a5e65aa8139f1e097080c2a8443201742c/rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15", size = 240224, upload-time = "2025-11-30T20:23:02.008Z" }, + { url = "https://files.pythonhosted.org/packages/24/95/ffd128ed1146a153d928617b0ef673960130be0009c77d8fbf0abe306713/rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1", size = 230645, upload-time = "2025-11-30T20:23:03.43Z" }, + { url = "https://files.pythonhosted.org/packages/ff/1b/b10de890a0def2a319a2626334a7f0ae388215eb60914dbac8a3bae54435/rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a", size = 364443, upload-time = "2025-11-30T20:23:04.878Z" }, + { url = "https://files.pythonhosted.org/packages/0d/bf/27e39f5971dc4f305a4fb9c672ca06f290f7c4e261c568f3dea16a410d47/rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e", size = 353375, upload-time = "2025-11-30T20:23:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/40/58/442ada3bba6e8e6615fc00483135c14a7538d2ffac30e2d933ccf6852232/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000", size = 383850, upload-time = "2025-11-30T20:23:07.825Z" }, + { url = "https://files.pythonhosted.org/packages/14/14/f59b0127409a33c6ef6f5c1ebd5ad8e32d7861c9c7adfa9a624fc3889f6c/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db", size = 392812, upload-time = "2025-11-30T20:23:09.228Z" }, + { url = "https://files.pythonhosted.org/packages/b3/66/e0be3e162ac299b3a22527e8913767d869e6cc75c46bd844aa43fb81ab62/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2", size = 517841, upload-time = "2025-11-30T20:23:11.186Z" }, + { url = "https://files.pythonhosted.org/packages/3d/55/fa3b9cf31d0c963ecf1ba777f7cf4b2a2c976795ac430d24a1f43d25a6ba/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa", size = 408149, upload-time = "2025-11-30T20:23:12.864Z" }, + { url = "https://files.pythonhosted.org/packages/60/ca/780cf3b1a32b18c0f05c441958d3758f02544f1d613abf9488cd78876378/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083", size = 383843, upload-time = "2025-11-30T20:23:14.638Z" }, + { url = "https://files.pythonhosted.org/packages/82/86/d5f2e04f2aa6247c613da0c1dd87fcd08fa17107e858193566048a1e2f0a/rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9", size = 396507, upload-time = "2025-11-30T20:23:16.105Z" }, + { url = "https://files.pythonhosted.org/packages/4b/9a/453255d2f769fe44e07ea9785c8347edaf867f7026872e76c1ad9f7bed92/rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0", size = 414949, upload-time = "2025-11-30T20:23:17.539Z" }, + { url = "https://files.pythonhosted.org/packages/a3/31/622a86cdc0c45d6df0e9ccb6becdba5074735e7033c20e401a6d9d0e2ca0/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94", size = 565790, upload-time = "2025-11-30T20:23:19.029Z" }, + { url = "https://files.pythonhosted.org/packages/1c/5d/15bbf0fb4a3f58a3b1c67855ec1efcc4ceaef4e86644665fff03e1b66d8d/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08", size = 590217, upload-time = "2025-11-30T20:23:20.885Z" }, + { url = "https://files.pythonhosted.org/packages/6d/61/21b8c41f68e60c8cc3b2e25644f0e3681926020f11d06ab0b78e3c6bbff1/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27", size = 555806, upload-time = "2025-11-30T20:23:22.488Z" }, + { url = "https://files.pythonhosted.org/packages/f9/39/7e067bb06c31de48de3eb200f9fc7c58982a4d3db44b07e73963e10d3be9/rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6", size = 211341, upload-time = "2025-11-30T20:23:24.449Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4d/222ef0b46443cf4cf46764d9c630f3fe4abaa7245be9417e56e9f52b8f65/rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d", size = 225768, upload-time = "2025-11-30T20:23:25.908Z" }, + { url = "https://files.pythonhosted.org/packages/86/81/dad16382ebbd3d0e0328776d8fd7ca94220e4fa0798d1dc5e7da48cb3201/rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0", size = 362099, upload-time = "2025-11-30T20:23:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/2b/60/19f7884db5d5603edf3c6bce35408f45ad3e97e10007df0e17dd57af18f8/rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be", size = 353192, upload-time = "2025-11-30T20:23:29.151Z" }, + { url = "https://files.pythonhosted.org/packages/bf/c4/76eb0e1e72d1a9c4703c69607cec123c29028bff28ce41588792417098ac/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f", size = 384080, upload-time = "2025-11-30T20:23:30.785Z" }, + { url = "https://files.pythonhosted.org/packages/72/87/87ea665e92f3298d1b26d78814721dc39ed8d2c74b86e83348d6b48a6f31/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f", size = 394841, upload-time = "2025-11-30T20:23:32.209Z" }, + { url = "https://files.pythonhosted.org/packages/77/ad/7783a89ca0587c15dcbf139b4a8364a872a25f861bdb88ed99f9b0dec985/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87", size = 516670, upload-time = "2025-11-30T20:23:33.742Z" }, + { url = "https://files.pythonhosted.org/packages/5b/3c/2882bdac942bd2172f3da574eab16f309ae10a3925644e969536553cb4ee/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18", size = 408005, upload-time = "2025-11-30T20:23:35.253Z" }, + { url = "https://files.pythonhosted.org/packages/ce/81/9a91c0111ce1758c92516a3e44776920b579d9a7c09b2b06b642d4de3f0f/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad", size = 382112, upload-time = "2025-11-30T20:23:36.842Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8e/1da49d4a107027e5fbc64daeab96a0706361a2918da10cb41769244b805d/rpds_py-0.30.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07", size = 399049, upload-time = "2025-11-30T20:23:38.343Z" }, + { url = "https://files.pythonhosted.org/packages/df/5a/7ee239b1aa48a127570ec03becbb29c9d5a9eb092febbd1699d567cae859/rpds_py-0.30.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f", size = 415661, upload-time = "2025-11-30T20:23:40.263Z" }, + { url = "https://files.pythonhosted.org/packages/70/ea/caa143cf6b772f823bc7929a45da1fa83569ee49b11d18d0ada7f5ee6fd6/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65", size = 565606, upload-time = "2025-11-30T20:23:42.186Z" }, + { url = "https://files.pythonhosted.org/packages/64/91/ac20ba2d69303f961ad8cf55bf7dbdb4763f627291ba3d0d7d67333cced9/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f", size = 591126, upload-time = "2025-11-30T20:23:44.086Z" }, + { url = "https://files.pythonhosted.org/packages/21/20/7ff5f3c8b00c8a95f75985128c26ba44503fb35b8e0259d812766ea966c7/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53", size = 553371, upload-time = "2025-11-30T20:23:46.004Z" }, + { url = "https://files.pythonhosted.org/packages/72/c7/81dadd7b27c8ee391c132a6b192111ca58d866577ce2d9b0ca157552cce0/rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed", size = 215298, upload-time = "2025-11-30T20:23:47.696Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d2/1aaac33287e8cfb07aab2e6b8ac1deca62f6f65411344f1433c55e6f3eb8/rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950", size = 228604, upload-time = "2025-11-30T20:23:49.501Z" }, + { url = "https://files.pythonhosted.org/packages/e8/95/ab005315818cc519ad074cb7784dae60d939163108bd2b394e60dc7b5461/rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6", size = 222391, upload-time = "2025-11-30T20:23:50.96Z" }, + { url = "https://files.pythonhosted.org/packages/9e/68/154fe0194d83b973cdedcdcc88947a2752411165930182ae41d983dcefa6/rpds_py-0.30.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb", size = 364868, upload-time = "2025-11-30T20:23:52.494Z" }, + { url = "https://files.pythonhosted.org/packages/83/69/8bbc8b07ec854d92a8b75668c24d2abcb1719ebf890f5604c61c9369a16f/rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8", size = 353747, upload-time = "2025-11-30T20:23:54.036Z" }, + { url = "https://files.pythonhosted.org/packages/ab/00/ba2e50183dbd9abcce9497fa5149c62b4ff3e22d338a30d690f9af970561/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7", size = 383795, upload-time = "2025-11-30T20:23:55.556Z" }, + { url = "https://files.pythonhosted.org/packages/05/6f/86f0272b84926bcb0e4c972262f54223e8ecc556b3224d281e6598fc9268/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898", size = 393330, upload-time = "2025-11-30T20:23:57.033Z" }, + { url = "https://files.pythonhosted.org/packages/cb/e9/0e02bb2e6dc63d212641da45df2b0bf29699d01715913e0d0f017ee29438/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e", size = 518194, upload-time = "2025-11-30T20:23:58.637Z" }, + { url = "https://files.pythonhosted.org/packages/ee/ca/be7bca14cf21513bdf9c0606aba17d1f389ea2b6987035eb4f62bd923f25/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419", size = 408340, upload-time = "2025-11-30T20:24:00.2Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c7/736e00ebf39ed81d75544c0da6ef7b0998f8201b369acf842f9a90dc8fce/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551", size = 383765, upload-time = "2025-11-30T20:24:01.759Z" }, + { url = "https://files.pythonhosted.org/packages/4a/3f/da50dfde9956aaf365c4adc9533b100008ed31aea635f2b8d7b627e25b49/rpds_py-0.30.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8", size = 396834, upload-time = "2025-11-30T20:24:03.687Z" }, + { url = "https://files.pythonhosted.org/packages/4e/00/34bcc2565b6020eab2623349efbdec810676ad571995911f1abdae62a3a0/rpds_py-0.30.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5", size = 415470, upload-time = "2025-11-30T20:24:05.232Z" }, + { url = "https://files.pythonhosted.org/packages/8c/28/882e72b5b3e6f718d5453bd4d0d9cf8df36fddeb4ddbbab17869d5868616/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404", size = 565630, upload-time = "2025-11-30T20:24:06.878Z" }, + { url = "https://files.pythonhosted.org/packages/3b/97/04a65539c17692de5b85c6e293520fd01317fd878ea1995f0367d4532fb1/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856", size = 591148, upload-time = "2025-11-30T20:24:08.445Z" }, + { url = "https://files.pythonhosted.org/packages/85/70/92482ccffb96f5441aab93e26c4d66489eb599efdcf96fad90c14bbfb976/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40", size = 556030, upload-time = "2025-11-30T20:24:10.956Z" }, + { url = "https://files.pythonhosted.org/packages/20/53/7c7e784abfa500a2b6b583b147ee4bb5a2b3747a9166bab52fec4b5b5e7d/rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0", size = 211570, upload-time = "2025-11-30T20:24:12.735Z" }, + { url = "https://files.pythonhosted.org/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3", size = 226532, upload-time = "2025-11-30T20:24:14.634Z" }, + { url = "https://files.pythonhosted.org/packages/69/71/3f34339ee70521864411f8b6992e7ab13ac30d8e4e3309e07c7361767d91/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c2262bdba0ad4fc6fb5545660673925c2d2a5d9e2e0fb603aad545427be0fc58", size = 372292, upload-time = "2025-11-30T20:24:16.537Z" }, + { url = "https://files.pythonhosted.org/packages/57/09/f183df9b8f2d66720d2ef71075c59f7e1b336bec7ee4c48f0a2b06857653/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ee6af14263f25eedc3bb918a3c04245106a42dfd4f5c2285ea6f997b1fc3f89a", size = 362128, upload-time = "2025-11-30T20:24:18.086Z" }, + { url = "https://files.pythonhosted.org/packages/7a/68/5c2594e937253457342e078f0cc1ded3dd7b2ad59afdbf2d354869110a02/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3adbb8179ce342d235c31ab8ec511e66c73faa27a47e076ccc92421add53e2bb", size = 391542, upload-time = "2025-11-30T20:24:20.092Z" }, + { url = "https://files.pythonhosted.org/packages/49/5c/31ef1afd70b4b4fbdb2800249f34c57c64beb687495b10aec0365f53dfc4/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:250fa00e9543ac9b97ac258bd37367ff5256666122c2d0f2bc97577c60a1818c", size = 404004, upload-time = "2025-11-30T20:24:22.231Z" }, + { url = "https://files.pythonhosted.org/packages/e3/63/0cfbea38d05756f3440ce6534d51a491d26176ac045e2707adc99bb6e60a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9854cf4f488b3d57b9aaeb105f06d78e5529d3145b1e4a41750167e8c213c6d3", size = 527063, upload-time = "2025-11-30T20:24:24.302Z" }, + { url = "https://files.pythonhosted.org/packages/42/e6/01e1f72a2456678b0f618fc9a1a13f882061690893c192fcad9f2926553a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:993914b8e560023bc0a8bf742c5f303551992dcb85e247b1e5c7f4a7d145bda5", size = 413099, upload-time = "2025-11-30T20:24:25.916Z" }, + { url = "https://files.pythonhosted.org/packages/b8/25/8df56677f209003dcbb180765520c544525e3ef21ea72279c98b9aa7c7fb/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58edca431fb9b29950807e301826586e5bbf24163677732429770a697ffe6738", size = 392177, upload-time = "2025-11-30T20:24:27.834Z" }, + { url = "https://files.pythonhosted.org/packages/4a/b4/0a771378c5f16f8115f796d1f437950158679bcd2a7c68cf251cfb00ed5b/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:dea5b552272a944763b34394d04577cf0f9bd013207bc32323b5a89a53cf9c2f", size = 406015, upload-time = "2025-11-30T20:24:29.457Z" }, + { url = "https://files.pythonhosted.org/packages/36/d8/456dbba0af75049dc6f63ff295a2f92766b9d521fa00de67a2bd6427d57a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ba3af48635eb83d03f6c9735dfb21785303e73d22ad03d489e88adae6eab8877", size = 423736, upload-time = "2025-11-30T20:24:31.22Z" }, + { url = "https://files.pythonhosted.org/packages/13/64/b4d76f227d5c45a7e0b796c674fd81b0a6c4fbd48dc29271857d8219571c/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:dff13836529b921e22f15cb099751209a60009731a68519630a24d61f0b1b30a", size = 573981, upload-time = "2025-11-30T20:24:32.934Z" }, + { url = "https://files.pythonhosted.org/packages/20/91/092bacadeda3edf92bf743cc96a7be133e13a39cdbfd7b5082e7ab638406/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:1b151685b23929ab7beec71080a8889d4d6d9fa9a983d213f07121205d48e2c4", size = 599782, upload-time = "2025-11-30T20:24:35.169Z" }, + { url = "https://files.pythonhosted.org/packages/d1/b7/b95708304cd49b7b6f82fdd039f1748b66ec2b21d6a45180910802f1abf1/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e", size = 562191, upload-time = "2025-11-30T20:24:36.853Z" }, +] + +[[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, upload-time = "2025-04-16T09:51:18.218Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762", size = 34696, upload-time = "2025-04-16T09:51:17.142Z" }, +] + +[[package]] +name = "rtree" +version = "1.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/95/09/7302695875a019514de9a5dd17b8320e7a19d6e7bc8f85dcfb79a4ce2da3/rtree-1.4.1.tar.gz", hash = "sha256:c6b1b3550881e57ebe530cc6cffefc87cd9bf49c30b37b894065a9f810875e46", size = 52425, upload-time = "2025-08-13T19:32:01.413Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/d9/108cd989a4c0954e60b3cdc86fd2826407702b5375f6dfdab2802e5fed98/rtree-1.4.1-py3-none-macosx_10_9_x86_64.whl", hash = "sha256:d672184298527522d4914d8ae53bf76982b86ca420b0acde9298a7a87d81d4a4", size = 468484, upload-time = "2025-08-13T19:31:50.593Z" }, + { url = "https://files.pythonhosted.org/packages/f3/cf/2710b6fd6b07ea0aef317b29f335790ba6adf06a28ac236078ed9bd8a91d/rtree-1.4.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:a7e48d805e12011c2cf739a29d6a60ae852fb1de9fc84220bbcef67e6e595d7d", size = 436325, upload-time = "2025-08-13T19:31:52.367Z" }, + { url = "https://files.pythonhosted.org/packages/55/e1/4d075268a46e68db3cac51846eb6a3ab96ed481c585c5a1ad411b3c23aad/rtree-1.4.1-py3-none-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:efa8c4496e31e9ad58ff6c7df89abceac7022d906cb64a3e18e4fceae6b77f65", size = 459789, upload-time = "2025-08-13T19:31:53.926Z" }, + { url = "https://files.pythonhosted.org/packages/d1/75/e5d44be90525cd28503e7f836d077ae6663ec0687a13ba7810b4114b3668/rtree-1.4.1-py3-none-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:12de4578f1b3381a93a655846900be4e3d5f4cd5e306b8b00aa77c1121dc7e8c", size = 507644, upload-time = "2025-08-13T19:31:55.164Z" }, + { url = "https://files.pythonhosted.org/packages/fd/85/b8684f769a142163b52859a38a486493b05bafb4f2fb71d4f945de28ebf9/rtree-1.4.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:b558edda52eca3e6d1ee629042192c65e6b7f2c150d6d6cd207ce82f85be3967", size = 1454478, upload-time = "2025-08-13T19:31:56.808Z" }, + { url = "https://files.pythonhosted.org/packages/e9/a4/c2292b95246b9165cc43a0c3757e80995d58bc9b43da5cb47ad6e3535213/rtree-1.4.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:f155bc8d6bac9dcd383481dee8c130947a4866db1d16cb6dff442329a038a0dc", size = 1555140, upload-time = "2025-08-13T19:31:58.031Z" }, + { url = "https://files.pythonhosted.org/packages/74/25/5282c8270bfcd620d3e73beb35b40ac4ab00f0a898d98ebeb41ef0989ec8/rtree-1.4.1-py3-none-win_amd64.whl", hash = "sha256:efe125f416fd27150197ab8521158662943a40f87acab8028a1aac4ad667a489", size = 389358, upload-time = "2025-08-13T19:31:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/3f/50/0a9e7e7afe7339bd5e36911f0ceb15fed51945836ed803ae5afd661057fd/rtree-1.4.1-py3-none-win_arm64.whl", hash = "sha256:3d46f55729b28138e897ffef32f7ce93ac335cb67f9120125ad3742a220800f0", size = 355253, upload-time = "2025-08-13T19:32:00.296Z" }, +] + +[[package]] +name = "s3transfer" +version = "0.18.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e0/1f/12417f7f493fc45e1f9fd5d4a9b6c125cf8d2cf3f8ddbdfab3e76406e9d6/s3transfer-0.18.0.tar.gz", hash = "sha256:3760b8b7ec1315da54048b2d626276732bee4300d054d492d4e1d43e20d4ecbd", size = 160560, upload-time = "2026-05-28T19:39:09.124Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2b/58/a58fc997655386daa2e25784e30c288aa3e3819e401f77029ee4899fb55a/s3transfer-0.18.0-py3-none-any.whl", hash = "sha256:239c13b09e65ad0346e1be7348b8a202dcad44ac7ea7c6eb858fc881dce739b6", size = 88572, upload-time = "2026-05-28T19:39:07.999Z" }, +] + +[[package]] +name = "safetensors" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/29/9c/6e74567782559a63bd040a236edca26fd71bc7ba88de2ef35d75df3bca5e/safetensors-0.7.0.tar.gz", hash = "sha256:07663963b67e8bd9f0b8ad15bb9163606cd27cc5a1b96235a50d8369803b96b0", size = 200878, upload-time = "2025-11-19T15:18:43.199Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/47/aef6c06649039accf914afef490268e1067ed82be62bcfa5b7e886ad15e8/safetensors-0.7.0-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:c82f4d474cf725255d9e6acf17252991c3c8aac038d6ef363a4bf8be2f6db517", size = 467781, upload-time = "2025-11-19T15:18:35.84Z" }, + { url = "https://files.pythonhosted.org/packages/e8/00/374c0c068e30cd31f1e1b46b4b5738168ec79e7689ca82ee93ddfea05109/safetensors-0.7.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:94fd4858284736bb67a897a41608b5b0c2496c9bdb3bf2af1fa3409127f20d57", size = 447058, upload-time = "2025-11-19T15:18:34.416Z" }, + { url = "https://files.pythonhosted.org/packages/f1/06/578ffed52c2296f93d7fd2d844cabfa92be51a587c38c8afbb8ae449ca89/safetensors-0.7.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e07d91d0c92a31200f25351f4acb2bc6aff7f48094e13ebb1d0fb995b54b6542", size = 491748, upload-time = "2025-11-19T15:18:09.79Z" }, + { url = "https://files.pythonhosted.org/packages/ae/33/1debbbb70e4791dde185edb9413d1fe01619255abb64b300157d7f15dddd/safetensors-0.7.0-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8469155f4cb518bafb4acf4865e8bb9d6804110d2d9bdcaa78564b9fd841e104", size = 503881, upload-time = "2025-11-19T15:18:16.145Z" }, + { url = "https://files.pythonhosted.org/packages/8e/1c/40c2ca924d60792c3be509833df711b553c60effbd91da6f5284a83f7122/safetensors-0.7.0-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:54bef08bf00a2bff599982f6b08e8770e09cc012d7bba00783fc7ea38f1fb37d", size = 623463, upload-time = "2025-11-19T15:18:21.11Z" }, + { url = "https://files.pythonhosted.org/packages/9b/3a/13784a9364bd43b0d61eef4bea2845039bc2030458b16594a1bd787ae26e/safetensors-0.7.0-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:42cb091236206bb2016d245c377ed383aa7f78691748f3bb6ee1bfa51ae2ce6a", size = 532855, upload-time = "2025-11-19T15:18:25.719Z" }, + { url = "https://files.pythonhosted.org/packages/a0/60/429e9b1cb3fc651937727befe258ea24122d9663e4d5709a48c9cbfceecb/safetensors-0.7.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac7252938f0696ddea46f5e855dd3138444e82236e3be475f54929f0c510d48", size = 507152, upload-time = "2025-11-19T15:18:33.023Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a8/4b45e4e059270d17af60359713ffd83f97900d45a6afa73aaa0d737d48b6/safetensors-0.7.0-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1d060c70284127fa805085d8f10fbd0962792aed71879d00864acda69dbab981", size = 541856, upload-time = "2025-11-19T15:18:31.075Z" }, + { url = "https://files.pythonhosted.org/packages/06/87/d26d8407c44175d8ae164a95b5a62707fcc445f3c0c56108e37d98070a3d/safetensors-0.7.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:cdab83a366799fa730f90a4ebb563e494f28e9e92c4819e556152ad55e43591b", size = 674060, upload-time = "2025-11-19T15:18:37.211Z" }, + { url = "https://files.pythonhosted.org/packages/11/f5/57644a2ff08dc6325816ba7217e5095f17269dada2554b658442c66aed51/safetensors-0.7.0-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:672132907fcad9f2aedcb705b2d7b3b93354a2aec1b2f706c4db852abe338f85", size = 771715, upload-time = "2025-11-19T15:18:38.689Z" }, + { url = "https://files.pythonhosted.org/packages/86/31/17883e13a814bd278ae6e266b13282a01049b0c81341da7fd0e3e71a80a3/safetensors-0.7.0-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:5d72abdb8a4d56d4020713724ba81dac065fedb7f3667151c4a637f1d3fb26c0", size = 714377, upload-time = "2025-11-19T15:18:40.162Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d8/0c8a7dc9b41dcac53c4cbf9df2b9c83e0e0097203de8b37a712b345c0be5/safetensors-0.7.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b0f6d66c1c538d5a94a73aa9ddca8ccc4227e6c9ff555322ea40bdd142391dd4", size = 677368, upload-time = "2025-11-19T15:18:41.627Z" }, + { url = "https://files.pythonhosted.org/packages/05/e5/cb4b713c8a93469e3c5be7c3f8d77d307e65fe89673e731f5c2bfd0a9237/safetensors-0.7.0-cp38-abi3-win32.whl", hash = "sha256:c74af94bf3ac15ac4d0f2a7c7b4663a15f8c2ab15ed0fc7531ca61d0835eccba", size = 326423, upload-time = "2025-11-19T15:18:45.74Z" }, + { url = "https://files.pythonhosted.org/packages/5d/e6/ec8471c8072382cb91233ba7267fd931219753bb43814cbc71757bfd4dab/safetensors-0.7.0-cp38-abi3-win_amd64.whl", hash = "sha256:d1239932053f56f3456f32eb9625590cc7582e905021f94636202a864d470755", size = 341380, upload-time = "2025-11-19T15:18:44.427Z" }, +] + +[package.optional-dependencies] +torch = [ + { name = "numpy" }, + { name = "packaging" }, + { name = "torch" }, +] + +[[package]] +name = "scikit-learn" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "joblib" }, + { name = "numpy" }, + { name = "scipy" }, + { name = "threadpoolctl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0e/d4/40988bf3b8e34feec1d0e6a051446b1f66225f8529b9309becaeef62b6c4/scikit_learn-1.8.0.tar.gz", hash = "sha256:9bccbb3b40e3de10351f8f5068e105d0f4083b1a65fa07b6634fbc401a6287fd", size = 7335585, upload-time = "2025-12-10T07:08:53.618Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c9/92/53ea2181da8ac6bf27170191028aee7251f8f841f8d3edbfdcaf2008fde9/scikit_learn-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:146b4d36f800c013d267b29168813f7a03a43ecd2895d04861f1240b564421da", size = 8595835, upload-time = "2025-12-10T07:07:39.385Z" }, + { url = "https://files.pythonhosted.org/packages/01/18/d154dc1638803adf987910cdd07097d9c526663a55666a97c124d09fb96a/scikit_learn-1.8.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:f984ca4b14914e6b4094c5d52a32ea16b49832c03bd17a110f004db3c223e8e1", size = 8080381, upload-time = "2025-12-10T07:07:41.93Z" }, + { url = "https://files.pythonhosted.org/packages/8a/44/226142fcb7b7101e64fdee5f49dbe6288d4c7af8abf593237b70fca080a4/scikit_learn-1.8.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5e30adb87f0cc81c7690a84f7932dd66be5bac57cfe16b91cb9151683a4a2d3b", size = 8799632, upload-time = "2025-12-10T07:07:43.899Z" }, + { url = "https://files.pythonhosted.org/packages/36/4d/4a67f30778a45d542bbea5db2dbfa1e9e100bf9ba64aefe34215ba9f11f6/scikit_learn-1.8.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ada8121bcb4dac28d930febc791a69f7cb1673c8495e5eee274190b73a4559c1", size = 9103788, upload-time = "2025-12-10T07:07:45.982Z" }, + { url = "https://files.pythonhosted.org/packages/89/3c/45c352094cfa60050bcbb967b1faf246b22e93cb459f2f907b600f2ceda5/scikit_learn-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:c57b1b610bd1f40ba43970e11ce62821c2e6569e4d74023db19c6b26f246cb3b", size = 8081706, upload-time = "2025-12-10T07:07:48.111Z" }, + { url = "https://files.pythonhosted.org/packages/3d/46/5416595bb395757f754feb20c3d776553a386b661658fb21b7c814e89efe/scikit_learn-1.8.0-cp311-cp311-win_arm64.whl", hash = "sha256:2838551e011a64e3053ad7618dda9310175f7515f1742fa2d756f7c874c05961", size = 7688451, upload-time = "2025-12-10T07:07:49.873Z" }, + { url = "https://files.pythonhosted.org/packages/90/74/e6a7cc4b820e95cc38cf36cd74d5aa2b42e8ffc2d21fe5a9a9c45c1c7630/scikit_learn-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5fb63362b5a7ddab88e52b6dbb47dac3fd7dafeee740dc6c8d8a446ddedade8e", size = 8548242, upload-time = "2025-12-10T07:07:51.568Z" }, + { url = "https://files.pythonhosted.org/packages/49/d8/9be608c6024d021041c7f0b3928d4749a706f4e2c3832bbede4fb4f58c95/scikit_learn-1.8.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:5025ce924beccb28298246e589c691fe1b8c1c96507e6d27d12c5fadd85bfd76", size = 8079075, upload-time = "2025-12-10T07:07:53.697Z" }, + { url = "https://files.pythonhosted.org/packages/dd/47/f187b4636ff80cc63f21cd40b7b2d177134acaa10f6bb73746130ee8c2e5/scikit_learn-1.8.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4496bb2cf7a43ce1a2d7524a79e40bc5da45cf598dbf9545b7e8316ccba47bb4", size = 8660492, upload-time = "2025-12-10T07:07:55.574Z" }, + { url = "https://files.pythonhosted.org/packages/97/74/b7a304feb2b49df9fafa9382d4d09061a96ee9a9449a7cbea7988dda0828/scikit_learn-1.8.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0bcfe4d0d14aec44921545fd2af2338c7471de9cb701f1da4c9d85906ab847a", size = 8931904, upload-time = "2025-12-10T07:07:57.666Z" }, + { url = "https://files.pythonhosted.org/packages/9f/c4/0ab22726a04ede56f689476b760f98f8f46607caecff993017ac1b64aa5d/scikit_learn-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:35c007dedb2ffe38fe3ee7d201ebac4a2deccd2408e8621d53067733e3c74809", size = 8019359, upload-time = "2025-12-10T07:07:59.838Z" }, + { url = "https://files.pythonhosted.org/packages/24/90/344a67811cfd561d7335c1b96ca21455e7e472d281c3c279c4d3f2300236/scikit_learn-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:8c497fff237d7b4e07e9ef1a640887fa4fb765647f86fbe00f969ff6280ce2bb", size = 7641898, upload-time = "2025-12-10T07:08:01.36Z" }, + { url = "https://files.pythonhosted.org/packages/03/aa/e22e0768512ce9255eba34775be2e85c2048da73da1193e841707f8f039c/scikit_learn-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0d6ae97234d5d7079dc0040990a6f7aeb97cb7fa7e8945f1999a429b23569e0a", size = 8513770, upload-time = "2025-12-10T07:08:03.251Z" }, + { url = "https://files.pythonhosted.org/packages/58/37/31b83b2594105f61a381fc74ca19e8780ee923be2d496fcd8d2e1147bd99/scikit_learn-1.8.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:edec98c5e7c128328124a029bceb09eda2d526997780fef8d65e9a69eead963e", size = 8044458, upload-time = "2025-12-10T07:08:05.336Z" }, + { url = "https://files.pythonhosted.org/packages/2d/5a/3f1caed8765f33eabb723596666da4ebbf43d11e96550fb18bdec42b467b/scikit_learn-1.8.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:74b66d8689d52ed04c271e1329f0c61635bcaf5b926db9b12d58914cdc01fe57", size = 8610341, upload-time = "2025-12-10T07:08:07.732Z" }, + { url = "https://files.pythonhosted.org/packages/38/cf/06896db3f71c75902a8e9943b444a56e727418f6b4b4a90c98c934f51ed4/scikit_learn-1.8.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8fdf95767f989b0cfedb85f7ed8ca215d4be728031f56ff5a519ee1e3276dc2e", size = 8900022, upload-time = "2025-12-10T07:08:09.862Z" }, + { url = "https://files.pythonhosted.org/packages/1c/f9/9b7563caf3ec8873e17a31401858efab6b39a882daf6c1bfa88879c0aa11/scikit_learn-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:2de443b9373b3b615aec1bb57f9baa6bb3a9bd093f1269ba95c17d870422b271", size = 7989409, upload-time = "2025-12-10T07:08:12.028Z" }, + { url = "https://files.pythonhosted.org/packages/49/bd/1f4001503650e72c4f6009ac0c4413cb17d2d601cef6f71c0453da2732fc/scikit_learn-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:eddde82a035681427cbedded4e6eff5e57fa59216c2e3e90b10b19ab1d0a65c3", size = 7619760, upload-time = "2025-12-10T07:08:13.688Z" }, + { url = "https://files.pythonhosted.org/packages/d2/7d/a630359fc9dcc95496588c8d8e3245cc8fd81980251079bc09c70d41d951/scikit_learn-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:7cc267b6108f0a1499a734167282c00c4ebf61328566b55ef262d48e9849c735", size = 8826045, upload-time = "2025-12-10T07:08:15.215Z" }, + { url = "https://files.pythonhosted.org/packages/cc/56/a0c86f6930cfcd1c7054a2bc417e26960bb88d32444fe7f71d5c2cfae891/scikit_learn-1.8.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:fe1c011a640a9f0791146011dfd3c7d9669785f9fed2b2a5f9e207536cf5c2fd", size = 8420324, upload-time = "2025-12-10T07:08:17.561Z" }, + { url = "https://files.pythonhosted.org/packages/46/1e/05962ea1cebc1cf3876667ecb14c283ef755bf409993c5946ade3b77e303/scikit_learn-1.8.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72358cce49465d140cc4e7792015bb1f0296a9742d5622c67e31399b75468b9e", size = 8680651, upload-time = "2025-12-10T07:08:19.952Z" }, + { url = "https://files.pythonhosted.org/packages/fe/56/a85473cd75f200c9759e3a5f0bcab2d116c92a8a02ee08ccd73b870f8bb4/scikit_learn-1.8.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:80832434a6cc114f5219211eec13dcbc16c2bac0e31ef64c6d346cde3cf054cb", size = 8925045, upload-time = "2025-12-10T07:08:22.11Z" }, + { url = "https://files.pythonhosted.org/packages/cc/b7/64d8cfa896c64435ae57f4917a548d7ac7a44762ff9802f75a79b77cb633/scikit_learn-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ee787491dbfe082d9c3013f01f5991658b0f38aa8177e4cd4bf434c58f551702", size = 8507994, upload-time = "2025-12-10T07:08:23.943Z" }, + { url = "https://files.pythonhosted.org/packages/5e/37/e192ea709551799379958b4c4771ec507347027bb7c942662c7fbeba31cb/scikit_learn-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf97c10a3f5a7543f9b88cbf488d33d175e9146115a451ae34568597ba33dcde", size = 7869518, upload-time = "2025-12-10T07:08:25.71Z" }, + { url = "https://files.pythonhosted.org/packages/24/05/1af2c186174cc92dcab2233f327336058c077d38f6fe2aceb08e6ab4d509/scikit_learn-1.8.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c22a2da7a198c28dd1a6e1136f19c830beab7fdca5b3e5c8bba8394f8a5c45b3", size = 8528667, upload-time = "2025-12-10T07:08:27.541Z" }, + { url = "https://files.pythonhosted.org/packages/a8/25/01c0af38fe969473fb292bba9dc2b8f9b451f3112ff242c647fee3d0dfe7/scikit_learn-1.8.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:6b595b07a03069a2b1740dc08c2299993850ea81cce4fe19b2421e0c970de6b7", size = 8066524, upload-time = "2025-12-10T07:08:29.822Z" }, + { url = "https://files.pythonhosted.org/packages/be/ce/a0623350aa0b68647333940ee46fe45086c6060ec604874e38e9ab7d8e6c/scikit_learn-1.8.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:29ffc74089f3d5e87dfca4c2c8450f88bdc61b0fc6ed5d267f3988f19a1309f6", size = 8657133, upload-time = "2025-12-10T07:08:31.865Z" }, + { url = "https://files.pythonhosted.org/packages/b8/cb/861b41341d6f1245e6ca80b1c1a8c4dfce43255b03df034429089ca2a2c5/scikit_learn-1.8.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fb65db5d7531bccf3a4f6bec3462223bea71384e2cda41da0f10b7c292b9e7c4", size = 8923223, upload-time = "2025-12-10T07:08:34.166Z" }, + { url = "https://files.pythonhosted.org/packages/76/18/a8def8f91b18cd1ba6e05dbe02540168cb24d47e8dcf69e8d00b7da42a08/scikit_learn-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:56079a99c20d230e873ea40753102102734c5953366972a71d5cb39a32bc40c6", size = 8096518, upload-time = "2025-12-10T07:08:36.339Z" }, + { url = "https://files.pythonhosted.org/packages/d1/77/482076a678458307f0deb44e29891d6022617b2a64c840c725495bee343f/scikit_learn-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:3bad7565bc9cf37ce19a7c0d107742b320c1285df7aab1a6e2d28780df167242", size = 7754546, upload-time = "2025-12-10T07:08:38.128Z" }, + { url = "https://files.pythonhosted.org/packages/2d/d1/ef294ca754826daa043b2a104e59960abfab4cf653891037d19dd5b6f3cf/scikit_learn-1.8.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:4511be56637e46c25721e83d1a9cea9614e7badc7040c4d573d75fbe257d6fd7", size = 8848305, upload-time = "2025-12-10T07:08:41.013Z" }, + { url = "https://files.pythonhosted.org/packages/5b/e2/b1f8b05138ee813b8e1a4149f2f0d289547e60851fd1bb268886915adbda/scikit_learn-1.8.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:a69525355a641bf8ef136a7fa447672fb54fe8d60cab5538d9eb7c6438543fb9", size = 8432257, upload-time = "2025-12-10T07:08:42.873Z" }, + { url = "https://files.pythonhosted.org/packages/26/11/c32b2138a85dcb0c99f6afd13a70a951bfdff8a6ab42d8160522542fb647/scikit_learn-1.8.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c2656924ec73e5939c76ac4c8b026fc203b83d8900362eb2599d8aee80e4880f", size = 8678673, upload-time = "2025-12-10T07:08:45.362Z" }, + { url = "https://files.pythonhosted.org/packages/c7/57/51f2384575bdec454f4fe4e7a919d696c9ebce914590abf3e52d47607ab8/scikit_learn-1.8.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15fc3b5d19cc2be65404786857f2e13c70c83dd4782676dd6814e3b89dc8f5b9", size = 8922467, upload-time = "2025-12-10T07:08:47.408Z" }, + { url = "https://files.pythonhosted.org/packages/35/4d/748c9e2872637a57981a04adc038dacaa16ba8ca887b23e34953f0b3f742/scikit_learn-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:00d6f1d66fbcf4eba6e356e1420d33cc06c70a45bb1363cd6f6a8e4ebbbdece2", size = 8774395, upload-time = "2025-12-10T07:08:49.337Z" }, + { url = "https://files.pythonhosted.org/packages/60/22/d7b2ebe4704a5e50790ba089d5c2ae308ab6bb852719e6c3bd4f04c3a363/scikit_learn-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:f28dd15c6bb0b66ba09728cf09fd8736c304be29409bd8445a080c1280619e8c", size = 8002647, upload-time = "2025-12-10T07:08:51.601Z" }, +] + +[[package]] +name = "scipy" +version = "1.17.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7a/97/5a3609c4f8d58b039179648e62dd220f89864f56f7357f5d4f45c29eb2cc/scipy-1.17.1.tar.gz", hash = "sha256:95d8e012d8cb8816c226aef832200b1d45109ed4464303e997c5b13122b297c0", size = 30573822, upload-time = "2026-02-23T00:26:24.851Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/75/b4ce781849931fef6fd529afa6b63711d5a733065722d0c3e2724af9e40a/scipy-1.17.1-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:1f95b894f13729334fb990162e911c9e5dc1ab390c58aa6cbecb389c5b5e28ec", size = 31613675, upload-time = "2026-02-23T00:16:00.13Z" }, + { url = "https://files.pythonhosted.org/packages/f7/58/bccc2861b305abdd1b8663d6130c0b3d7cc22e8d86663edbc8401bfd40d4/scipy-1.17.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:e18f12c6b0bc5a592ed23d3f7b891f68fd7f8241d69b7883769eb5d5dfb52696", size = 28162057, upload-time = "2026-02-23T00:16:09.456Z" }, + { url = "https://files.pythonhosted.org/packages/6d/ee/18146b7757ed4976276b9c9819108adbc73c5aad636e5353e20746b73069/scipy-1.17.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:a3472cfbca0a54177d0faa68f697d8ba4c80bbdc19908c3465556d9f7efce9ee", size = 20334032, upload-time = "2026-02-23T00:16:17.358Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e6/cef1cf3557f0c54954198554a10016b6a03b2ec9e22a4e1df734936bd99c/scipy-1.17.1-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:766e0dc5a616d026a3a1cffa379af959671729083882f50307e18175797b3dfd", size = 22709533, upload-time = "2026-02-23T00:16:25.791Z" }, + { url = "https://files.pythonhosted.org/packages/4d/60/8804678875fc59362b0fb759ab3ecce1f09c10a735680318ac30da8cd76b/scipy-1.17.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:744b2bf3640d907b79f3fd7874efe432d1cf171ee721243e350f55234b4cec4c", size = 33062057, upload-time = "2026-02-23T00:16:36.931Z" }, + { url = "https://files.pythonhosted.org/packages/09/7d/af933f0f6e0767995b4e2d705a0665e454d1c19402aa7e895de3951ebb04/scipy-1.17.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43af8d1f3bea642559019edfe64e9b11192a8978efbd1539d7bc2aaa23d92de4", size = 35349300, upload-time = "2026-02-23T00:16:49.108Z" }, + { url = "https://files.pythonhosted.org/packages/b4/3d/7ccbbdcbb54c8fdc20d3b6930137c782a163fa626f0aef920349873421ba/scipy-1.17.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cd96a1898c0a47be4520327e01f874acfd61fb48a9420f8aa9f6483412ffa444", size = 35127333, upload-time = "2026-02-23T00:17:01.293Z" }, + { url = "https://files.pythonhosted.org/packages/e8/19/f926cb11c42b15ba08e3a71e376d816ac08614f769b4f47e06c3580c836a/scipy-1.17.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4eb6c25dd62ee8d5edf68a8e1c171dd71c292fdae95d8aeb3dd7d7de4c364082", size = 37741314, upload-time = "2026-02-23T00:17:12.576Z" }, + { url = "https://files.pythonhosted.org/packages/95/da/0d1df507cf574b3f224ccc3d45244c9a1d732c81dcb26b1e8a766ae271a8/scipy-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:d30e57c72013c2a4fe441c2fcb8e77b14e152ad48b5464858e07e2ad9fbfceff", size = 36607512, upload-time = "2026-02-23T00:17:23.424Z" }, + { url = "https://files.pythonhosted.org/packages/68/7f/bdd79ceaad24b671543ffe0ef61ed8e659440eb683b66f033454dcee90eb/scipy-1.17.1-cp311-cp311-win_arm64.whl", hash = "sha256:9ecb4efb1cd6e8c4afea0daa91a87fbddbce1b99d2895d151596716c0b2e859d", size = 24599248, upload-time = "2026-02-23T00:17:34.561Z" }, + { url = "https://files.pythonhosted.org/packages/35/48/b992b488d6f299dbe3f11a20b24d3dda3d46f1a635ede1c46b5b17a7b163/scipy-1.17.1-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:35c3a56d2ef83efc372eaec584314bd0ef2e2f0d2adb21c55e6ad5b344c0dcb8", size = 31610954, upload-time = "2026-02-23T00:17:49.855Z" }, + { url = "https://files.pythonhosted.org/packages/b2/02/cf107b01494c19dc100f1d0b7ac3cc08666e96ba2d64db7626066cee895e/scipy-1.17.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:fcb310ddb270a06114bb64bbe53c94926b943f5b7f0842194d585c65eb4edd76", size = 28172662, upload-time = "2026-02-23T00:18:01.64Z" }, + { url = "https://files.pythonhosted.org/packages/cf/a9/599c28631bad314d219cf9ffd40e985b24d603fc8a2f4ccc5ae8419a535b/scipy-1.17.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:cc90d2e9c7e5c7f1a482c9875007c095c3194b1cfedca3c2f3291cdc2bc7c086", size = 20344366, upload-time = "2026-02-23T00:18:12.015Z" }, + { url = "https://files.pythonhosted.org/packages/35/f5/906eda513271c8deb5af284e5ef0206d17a96239af79f9fa0aebfe0e36b4/scipy-1.17.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:c80be5ede8f3f8eded4eff73cc99a25c388ce98e555b17d31da05287015ffa5b", size = 22704017, upload-time = "2026-02-23T00:18:21.502Z" }, + { url = "https://files.pythonhosted.org/packages/da/34/16f10e3042d2f1d6b66e0428308ab52224b6a23049cb2f5c1756f713815f/scipy-1.17.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e19ebea31758fac5893a2ac360fedd00116cbb7628e650842a6691ba7ca28a21", size = 32927842, upload-time = "2026-02-23T00:18:35.367Z" }, + { url = "https://files.pythonhosted.org/packages/01/8e/1e35281b8ab6d5d72ebe9911edcdffa3f36b04ed9d51dec6dd140396e220/scipy-1.17.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:02ae3b274fde71c5e92ac4d54bc06c42d80e399fec704383dcd99b301df37458", size = 35235890, upload-time = "2026-02-23T00:18:49.188Z" }, + { url = "https://files.pythonhosted.org/packages/c5/5c/9d7f4c88bea6e0d5a4f1bc0506a53a00e9fcb198de372bfe4d3652cef482/scipy-1.17.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8a604bae87c6195d8b1045eddece0514d041604b14f2727bbc2b3020172045eb", size = 35003557, upload-time = "2026-02-23T00:18:54.74Z" }, + { url = "https://files.pythonhosted.org/packages/65/94/7698add8f276dbab7a9de9fb6b0e02fc13ee61d51c7c3f85ac28b65e1239/scipy-1.17.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f590cd684941912d10becc07325a3eeb77886fe981415660d9265c4c418d0bea", size = 37625856, upload-time = "2026-02-23T00:19:00.307Z" }, + { url = "https://files.pythonhosted.org/packages/a2/84/dc08d77fbf3d87d3ee27f6a0c6dcce1de5829a64f2eae85a0ecc1f0daa73/scipy-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:41b71f4a3a4cab9d366cd9065b288efc4d4f3c0b37a91a8e0947fb5bd7f31d87", size = 36549682, upload-time = "2026-02-23T00:19:07.67Z" }, + { url = "https://files.pythonhosted.org/packages/bc/98/fe9ae9ffb3b54b62559f52dedaebe204b408db8109a8c66fdd04869e6424/scipy-1.17.1-cp312-cp312-win_arm64.whl", hash = "sha256:f4115102802df98b2b0db3cce5cb9b92572633a1197c77b7553e5203f284a5b3", size = 24547340, upload-time = "2026-02-23T00:19:12.024Z" }, + { url = "https://files.pythonhosted.org/packages/76/27/07ee1b57b65e92645f219b37148a7e7928b82e2b5dbeccecb4dff7c64f0b/scipy-1.17.1-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:5e3c5c011904115f88a39308379c17f91546f77c1667cea98739fe0fccea804c", size = 31590199, upload-time = "2026-02-23T00:19:17.192Z" }, + { url = "https://files.pythonhosted.org/packages/ec/ae/db19f8ab842e9b724bf5dbb7db29302a91f1e55bc4d04b1025d6d605a2c5/scipy-1.17.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:6fac755ca3d2c3edcb22f479fceaa241704111414831ddd3bc6056e18516892f", size = 28154001, upload-time = "2026-02-23T00:19:22.241Z" }, + { url = "https://files.pythonhosted.org/packages/5b/58/3ce96251560107b381cbd6e8413c483bbb1228a6b919fa8652b0d4090e7f/scipy-1.17.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:7ff200bf9d24f2e4d5dc6ee8c3ac64d739d3a89e2326ba68aaf6c4a2b838fd7d", size = 20325719, upload-time = "2026-02-23T00:19:26.329Z" }, + { url = "https://files.pythonhosted.org/packages/b2/83/15087d945e0e4d48ce2377498abf5ad171ae013232ae31d06f336e64c999/scipy-1.17.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:4b400bdc6f79fa02a4d86640310dde87a21fba0c979efff5248908c6f15fad1b", size = 22683595, upload-time = "2026-02-23T00:19:30.304Z" }, + { url = "https://files.pythonhosted.org/packages/b4/e0/e58fbde4a1a594c8be8114eb4aac1a55bcd6587047efc18a61eb1f5c0d30/scipy-1.17.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b64ca7d4aee0102a97f3ba22124052b4bd2152522355073580bf4845e2550b6", size = 32896429, upload-time = "2026-02-23T00:19:35.536Z" }, + { url = "https://files.pythonhosted.org/packages/f5/5f/f17563f28ff03c7b6799c50d01d5d856a1d55f2676f537ca8d28c7f627cd/scipy-1.17.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:581b2264fc0aa555f3f435a5944da7504ea3a065d7029ad60e7c3d1ae09c5464", size = 35203952, upload-time = "2026-02-23T00:19:42.259Z" }, + { url = "https://files.pythonhosted.org/packages/8d/a5/9afd17de24f657fdfe4df9a3f1ea049b39aef7c06000c13db1530d81ccca/scipy-1.17.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:beeda3d4ae615106d7094f7e7cef6218392e4465cc95d25f900bebabfded0950", size = 34979063, upload-time = "2026-02-23T00:19:47.547Z" }, + { url = "https://files.pythonhosted.org/packages/8b/13/88b1d2384b424bf7c924f2038c1c409f8d88bb2a8d49d097861dd64a57b2/scipy-1.17.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6609bc224e9568f65064cfa72edc0f24ee6655b47575954ec6339534b2798369", size = 37598449, upload-time = "2026-02-23T00:19:53.238Z" }, + { url = "https://files.pythonhosted.org/packages/35/e5/d6d0e51fc888f692a35134336866341c08655d92614f492c6860dc45bb2c/scipy-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:37425bc9175607b0268f493d79a292c39f9d001a357bebb6b88fdfaff13f6448", size = 36510943, upload-time = "2026-02-23T00:20:50.89Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fd/3be73c564e2a01e690e19cc618811540ba5354c67c8680dce3281123fb79/scipy-1.17.1-cp313-cp313-win_arm64.whl", hash = "sha256:5cf36e801231b6a2059bf354720274b7558746f3b1a4efb43fcf557ccd484a87", size = 24545621, upload-time = "2026-02-23T00:20:55.871Z" }, + { url = "https://files.pythonhosted.org/packages/6f/6b/17787db8b8114933a66f9dcc479a8272e4b4da75fe03b0c282f7b0ade8cd/scipy-1.17.1-cp313-cp313t-macosx_10_14_x86_64.whl", hash = "sha256:d59c30000a16d8edc7e64152e30220bfbd724c9bbb08368c054e24c651314f0a", size = 31936708, upload-time = "2026-02-23T00:19:58.694Z" }, + { url = "https://files.pythonhosted.org/packages/38/2e/524405c2b6392765ab1e2b722a41d5da33dc5c7b7278184a8ad29b6cb206/scipy-1.17.1-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:010f4333c96c9bb1a4516269e33cb5917b08ef2166d5556ca2fd9f082a9e6ea0", size = 28570135, upload-time = "2026-02-23T00:20:03.934Z" }, + { url = "https://files.pythonhosted.org/packages/fd/c3/5bd7199f4ea8556c0c8e39f04ccb014ac37d1468e6cfa6a95c6b3562b76e/scipy-1.17.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:2ceb2d3e01c5f1d83c4189737a42d9cb2fc38a6eeed225e7515eef71ad301dce", size = 20741977, upload-time = "2026-02-23T00:20:07.935Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b8/8ccd9b766ad14c78386599708eb745f6b44f08400a5fd0ade7cf89b6fc93/scipy-1.17.1-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:844e165636711ef41f80b4103ed234181646b98a53c8f05da12ca5ca289134f6", size = 23029601, upload-time = "2026-02-23T00:20:12.161Z" }, + { url = "https://files.pythonhosted.org/packages/6d/a0/3cb6f4d2fb3e17428ad2880333cac878909ad1a89f678527b5328b93c1d4/scipy-1.17.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:158dd96d2207e21c966063e1635b1063cd7787b627b6f07305315dd73d9c679e", size = 33019667, upload-time = "2026-02-23T00:20:17.208Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c3/2d834a5ac7bf3a0c806ad1508efc02dda3c8c61472a56132d7894c312dea/scipy-1.17.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:74cbb80d93260fe2ffa334efa24cb8f2f0f622a9b9febf8b483c0b865bfb3475", size = 35264159, upload-time = "2026-02-23T00:20:23.087Z" }, + { url = "https://files.pythonhosted.org/packages/4d/77/d3ed4becfdbd217c52062fafe35a72388d1bd82c2d0ba5ca19d6fcc93e11/scipy-1.17.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:dbc12c9f3d185f5c737d801da555fb74b3dcfa1a50b66a1a93e09190f41fab50", size = 35102771, upload-time = "2026-02-23T00:20:28.636Z" }, + { url = "https://files.pythonhosted.org/packages/bd/12/d19da97efde68ca1ee5538bb261d5d2c062f0c055575128f11a2730e3ac1/scipy-1.17.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:94055a11dfebe37c656e70317e1996dc197e1a15bbcc351bcdd4610e128fe1ca", size = 37665910, upload-time = "2026-02-23T00:20:34.743Z" }, + { url = "https://files.pythonhosted.org/packages/06/1c/1172a88d507a4baaf72c5a09bb6c018fe2ae0ab622e5830b703a46cc9e44/scipy-1.17.1-cp313-cp313t-win_amd64.whl", hash = "sha256:e30bdeaa5deed6bc27b4cc490823cd0347d7dae09119b8803ae576ea0ce52e4c", size = 36562980, upload-time = "2026-02-23T00:20:40.575Z" }, + { url = "https://files.pythonhosted.org/packages/70/b0/eb757336e5a76dfa7911f63252e3b7d1de00935d7705cf772db5b45ec238/scipy-1.17.1-cp313-cp313t-win_arm64.whl", hash = "sha256:a720477885a9d2411f94a93d16f9d89bad0f28ca23c3f8daa521e2dcc3f44d49", size = 24856543, upload-time = "2026-02-23T00:20:45.313Z" }, + { url = "https://files.pythonhosted.org/packages/cf/83/333afb452af6f0fd70414dc04f898647ee1423979ce02efa75c3b0f2c28e/scipy-1.17.1-cp314-cp314-macosx_10_14_x86_64.whl", hash = "sha256:a48a72c77a310327f6a3a920092fa2b8fd03d7deaa60f093038f22d98e096717", size = 31584510, upload-time = "2026-02-23T00:21:01.015Z" }, + { url = "https://files.pythonhosted.org/packages/ed/a6/d05a85fd51daeb2e4ea71d102f15b34fedca8e931af02594193ae4fd25f7/scipy-1.17.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:45abad819184f07240d8a696117a7aacd39787af9e0b719d00285549ed19a1e9", size = 28170131, upload-time = "2026-02-23T00:21:05.888Z" }, + { url = "https://files.pythonhosted.org/packages/db/7b/8624a203326675d7746a254083a187398090a179335b2e4a20e2ddc46e83/scipy-1.17.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:3fd1fcdab3ea951b610dc4cef356d416d5802991e7e32b5254828d342f7b7e0b", size = 20342032, upload-time = "2026-02-23T00:21:09.904Z" }, + { url = "https://files.pythonhosted.org/packages/c9/35/2c342897c00775d688d8ff3987aced3426858fd89d5a0e26e020b660b301/scipy-1.17.1-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:7bdf2da170b67fdf10bca777614b1c7d96ae3ca5794fd9587dce41eb2966e866", size = 22678766, upload-time = "2026-02-23T00:21:14.313Z" }, + { url = "https://files.pythonhosted.org/packages/ef/f2/7cdb8eb308a1a6ae1e19f945913c82c23c0c442a462a46480ce487fdc0ac/scipy-1.17.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:adb2642e060a6549c343603a3851ba76ef0b74cc8c079a9a58121c7ec9fe2350", size = 32957007, upload-time = "2026-02-23T00:21:19.663Z" }, + { url = "https://files.pythonhosted.org/packages/0b/2e/7eea398450457ecb54e18e9d10110993fa65561c4f3add5e8eccd2b9cd41/scipy-1.17.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eee2cfda04c00a857206a4330f0c5e3e56535494e30ca445eb19ec624ae75118", size = 35221333, upload-time = "2026-02-23T00:21:25.278Z" }, + { url = "https://files.pythonhosted.org/packages/d9/77/5b8509d03b77f093a0d52e606d3c4f79e8b06d1d38c441dacb1e26cacf46/scipy-1.17.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d2650c1fb97e184d12d8ba010493ee7b322864f7d3d00d3f9bb97d9c21de4068", size = 35042066, upload-time = "2026-02-23T00:21:31.358Z" }, + { url = "https://files.pythonhosted.org/packages/f9/df/18f80fb99df40b4070328d5ae5c596f2f00fffb50167e31439e932f29e7d/scipy-1.17.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:08b900519463543aa604a06bec02461558a6e1cef8fdbb8098f77a48a83c8118", size = 37612763, upload-time = "2026-02-23T00:21:37.247Z" }, + { url = "https://files.pythonhosted.org/packages/4b/39/f0e8ea762a764a9dc52aa7dabcfad51a354819de1f0d4652b6a1122424d6/scipy-1.17.1-cp314-cp314-win_amd64.whl", hash = "sha256:3877ac408e14da24a6196de0ddcace62092bfc12a83823e92e49e40747e52c19", size = 37290984, upload-time = "2026-02-23T00:22:35.023Z" }, + { url = "https://files.pythonhosted.org/packages/7c/56/fe201e3b0f93d1a8bcf75d3379affd228a63d7e2d80ab45467a74b494947/scipy-1.17.1-cp314-cp314-win_arm64.whl", hash = "sha256:f8885db0bc2bffa59d5c1b72fad7a6a92d3e80e7257f967dd81abb553a90d293", size = 25192877, upload-time = "2026-02-23T00:22:39.798Z" }, + { url = "https://files.pythonhosted.org/packages/96/ad/f8c414e121f82e02d76f310f16db9899c4fcde36710329502a6b2a3c0392/scipy-1.17.1-cp314-cp314t-macosx_10_14_x86_64.whl", hash = "sha256:1cc682cea2ae55524432f3cdff9e9a3be743d52a7443d0cba9017c23c87ae2f6", size = 31949750, upload-time = "2026-02-23T00:21:42.289Z" }, + { url = "https://files.pythonhosted.org/packages/7c/b0/c741e8865d61b67c81e255f4f0a832846c064e426636cd7de84e74d209be/scipy-1.17.1-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:2040ad4d1795a0ae89bfc7e8429677f365d45aa9fd5e4587cf1ea737f927b4a1", size = 28585858, upload-time = "2026-02-23T00:21:47.706Z" }, + { url = "https://files.pythonhosted.org/packages/ed/1b/3985219c6177866628fa7c2595bfd23f193ceebbe472c98a08824b9466ff/scipy-1.17.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:131f5aaea57602008f9822e2115029b55d4b5f7c070287699fe45c661d051e39", size = 20757723, upload-time = "2026-02-23T00:21:52.039Z" }, + { url = "https://files.pythonhosted.org/packages/c0/19/2a04aa25050d656d6f7b9e7b685cc83d6957fb101665bfd9369ca6534563/scipy-1.17.1-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:9cdc1a2fcfd5c52cfb3045feb399f7b3ce822abdde3a193a6b9a60b3cb5854ca", size = 23043098, upload-time = "2026-02-23T00:21:56.185Z" }, + { url = "https://files.pythonhosted.org/packages/86/f1/3383beb9b5d0dbddd030335bf8a8b32d4317185efe495374f134d8be6cce/scipy-1.17.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e3dcd57ab780c741fde8dc68619de988b966db759a3c3152e8e9142c26295ad", size = 33030397, upload-time = "2026-02-23T00:22:01.404Z" }, + { url = "https://files.pythonhosted.org/packages/41/68/8f21e8a65a5a03f25a79165ec9d2b28c00e66dc80546cf5eb803aeeff35b/scipy-1.17.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a9956e4d4f4a301ebf6cde39850333a6b6110799d470dbbb1e25326ac447f52a", size = 35281163, upload-time = "2026-02-23T00:22:07.024Z" }, + { url = "https://files.pythonhosted.org/packages/84/8d/c8a5e19479554007a5632ed7529e665c315ae7492b4f946b0deb39870e39/scipy-1.17.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:a4328d245944d09fd639771de275701ccadf5f781ba0ff092ad141e017eccda4", size = 35116291, upload-time = "2026-02-23T00:22:12.585Z" }, + { url = "https://files.pythonhosted.org/packages/52/52/e57eceff0e342a1f50e274264ed47497b59e6a4e3118808ee58ddda7b74a/scipy-1.17.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a77cbd07b940d326d39a1d1b37817e2ee4d79cb30e7338f3d0cddffae70fcaa2", size = 37682317, upload-time = "2026-02-23T00:22:18.513Z" }, + { url = "https://files.pythonhosted.org/packages/11/2f/b29eafe4a3fbc3d6de9662b36e028d5f039e72d345e05c250e121a230dd4/scipy-1.17.1-cp314-cp314t-win_amd64.whl", hash = "sha256:eb092099205ef62cd1782b006658db09e2fed75bffcae7cc0d44052d8aa0f484", size = 37345327, upload-time = "2026-02-23T00:22:24.442Z" }, + { url = "https://files.pythonhosted.org/packages/07/39/338d9219c4e87f3e708f18857ecd24d22a0c3094752393319553096b98af/scipy-1.17.1-cp314-cp314t-win_arm64.whl", hash = "sha256:200e1050faffacc162be6a486a984a0497866ec54149a01270adc8a59b7c7d21", size = 25489165, upload-time = "2026-02-23T00:22:29.563Z" }, +] + +[[package]] +name = "semchunk" +version = "3.2.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mpire", extra = ["dill"] }, + { name = "tqdm" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a9/a0/ce7e3d6cc76498fd594e667d10a03f17d7cced129e46869daec23523bf5a/semchunk-3.2.5.tar.gz", hash = "sha256:ee15e9a06a69a411937dd8fcf0a25d7ef389c5195863140436872a02c95b0218", size = 17667, upload-time = "2025-10-28T02:12:38.025Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/95/12d226ee4d207cb1f77a216baa7e1a8bae2639733c140abe8d0316d23a18/semchunk-3.2.5-py3-none-any.whl", hash = "sha256:fd09cc5f380bd010b8ca773bd81893f7eaf11d37dd8362a83d46cedaf5dae076", size = 13048, upload-time = "2025-10-28T02:12:36.724Z" }, +] + +[[package]] +name = "sentence-transformers" +version = "6.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, + { name = "numpy" }, + { name = "scikit-learn" }, + { name = "scipy" }, + { name = "tokenizers" }, + { name = "torch" }, + { name = "tqdm" }, + { name = "transformers" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e7/43/6b53e6a2098440ce21478742facbc058f1a66ba2cb80b24bdc64942e1e2c/sentence_transformers-6.0.0.tar.gz", hash = "sha256:9e8c2c24f3b1c7473cd5f519a3d3cff60daaeb95533b82d045ffb43ee5f2dac4", size = 575048, upload-time = "2026-08-18T13:33:49.919Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/fe/9d19b01fe87945f9455c617bf5d33dfbf29fe06ab6580bc0bea06080c788/sentence_transformers-6.0.0-py3-none-any.whl", hash = "sha256:b974ac67523ea2a955afa87b1024129305472bd884367dcc969261cb086790e9", size = 739640, upload-time = "2026-08-18T13:33:48.428Z" }, +] + +[[package]] +name = "setuptools" +version = "84.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6d/44/f5da03a8ef95d369145c5bb53050e7877c9f3d312e128605fd9504829143/setuptools-84.0.0.tar.gz", hash = "sha256:f4695c21257f0d9b537ec2692c941d02ee143b7cc1276941349a546573b2ef73", size = 1168449, upload-time = "2026-08-08T18:27:58.365Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/9c/c510029fc6ef33a6275cd2c5d3cecd6613dfd6aa401d57c54f1c18852ccf/setuptools-84.0.0-py3-none-any.whl", hash = "sha256:51a52592b3b99e102b609654876bd65f19f999935166d1352678931132b0c670", size = 818216, upload-time = "2026-08-08T18:27:56.719Z" }, +] + +[[package]] +name = "shapely" +version = "2.1.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4d/bc/0989043118a27cccb4e906a46b7565ce36ca7b57f5a18b78f4f1b0f72d9d/shapely-2.1.2.tar.gz", hash = "sha256:2ed4ecb28320a433db18a5bf029986aa8afcfd740745e78847e330d5d94922a9", size = 315489, upload-time = "2025-09-24T13:51:41.432Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/8d/1ff672dea9ec6a7b5d422eb6d095ed886e2e523733329f75fdcb14ee1149/shapely-2.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:91121757b0a36c9aac3427a651a7e6567110a4a67c97edf04f8d55d4765f6618", size = 1820038, upload-time = "2025-09-24T13:50:15.628Z" }, + { url = "https://files.pythonhosted.org/packages/4f/ce/28fab8c772ce5db23a0d86bf0adaee0c4c79d5ad1db766055fa3dab442e2/shapely-2.1.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:16a9c722ba774cf50b5d4541242b4cce05aafd44a015290c82ba8a16931ff63d", size = 1626039, upload-time = "2025-09-24T13:50:16.881Z" }, + { url = "https://files.pythonhosted.org/packages/70/8b/868b7e3f4982f5006e9395c1e12343c66a8155c0374fdc07c0e6a1ab547d/shapely-2.1.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cc4f7397459b12c0b196c9efe1f9d7e92463cbba142632b4cc6d8bbbbd3e2b09", size = 3001519, upload-time = "2025-09-24T13:50:18.606Z" }, + { url = "https://files.pythonhosted.org/packages/13/02/58b0b8d9c17c93ab6340edd8b7308c0c5a5b81f94ce65705819b7416dba5/shapely-2.1.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:136ab87b17e733e22f0961504d05e77e7be8c9b5a8184f685b4a91a84efe3c26", size = 3110842, upload-time = "2025-09-24T13:50:21.77Z" }, + { url = "https://files.pythonhosted.org/packages/af/61/8e389c97994d5f331dcffb25e2fa761aeedfb52b3ad9bcdd7b8671f4810a/shapely-2.1.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:16c5d0fc45d3aa0a69074979f4f1928ca2734fb2e0dde8af9611e134e46774e7", size = 4021316, upload-time = "2025-09-24T13:50:23.626Z" }, + { url = "https://files.pythonhosted.org/packages/d3/d4/9b2a9fe6039f9e42ccf2cb3e84f219fd8364b0c3b8e7bbc857b5fbe9c14c/shapely-2.1.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6ddc759f72b5b2b0f54a7e7cde44acef680a55019eb52ac63a7af2cf17cb9cd2", size = 4178586, upload-time = "2025-09-24T13:50:25.443Z" }, + { url = "https://files.pythonhosted.org/packages/16/f6/9840f6963ed4decf76b08fd6d7fed14f8779fb7a62cb45c5617fa8ac6eab/shapely-2.1.2-cp311-cp311-win32.whl", hash = "sha256:2fa78b49485391224755a856ed3b3bd91c8455f6121fee0db0e71cefb07d0ef6", size = 1543961, upload-time = "2025-09-24T13:50:26.968Z" }, + { url = "https://files.pythonhosted.org/packages/38/1e/3f8ea46353c2a33c1669eb7327f9665103aa3a8dfe7f2e4ef714c210b2c2/shapely-2.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:c64d5c97b2f47e3cd9b712eaced3b061f2b71234b3fc263e0fcf7d889c6559dc", size = 1722856, upload-time = "2025-09-24T13:50:28.497Z" }, + { url = "https://files.pythonhosted.org/packages/24/c0/f3b6453cf2dfa99adc0ba6675f9aaff9e526d2224cbd7ff9c1a879238693/shapely-2.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fe2533caae6a91a543dec62e8360fe86ffcdc42a7c55f9dfd0128a977a896b94", size = 1833550, upload-time = "2025-09-24T13:50:30.019Z" }, + { url = "https://files.pythonhosted.org/packages/86/07/59dee0bc4b913b7ab59ab1086225baca5b8f19865e6101db9ebb7243e132/shapely-2.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ba4d1333cc0bc94381d6d4308d2e4e008e0bd128bdcff5573199742ee3634359", size = 1643556, upload-time = "2025-09-24T13:50:32.291Z" }, + { url = "https://files.pythonhosted.org/packages/26/29/a5397e75b435b9895cd53e165083faed5d12fd9626eadec15a83a2411f0f/shapely-2.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0bd308103340030feef6c111d3eb98d50dc13feea33affc8a6f9fa549e9458a3", size = 2988308, upload-time = "2025-09-24T13:50:33.862Z" }, + { url = "https://files.pythonhosted.org/packages/b9/37/e781683abac55dde9771e086b790e554811a71ed0b2b8a1e789b7430dd44/shapely-2.1.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1e7d4d7ad262a48bb44277ca12c7c78cb1b0f56b32c10734ec9a1d30c0b0c54b", size = 3099844, upload-time = "2025-09-24T13:50:35.459Z" }, + { url = "https://files.pythonhosted.org/packages/d8/f3/9876b64d4a5a321b9dc482c92bb6f061f2fa42131cba643c699f39317cb9/shapely-2.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e9eddfe513096a71896441a7c37db72da0687b34752c4e193577a145c71736fc", size = 3988842, upload-time = "2025-09-24T13:50:37.478Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a0/704c7292f7014c7e74ec84eddb7b109e1fbae74a16deae9c1504b1d15565/shapely-2.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:980c777c612514c0cf99bc8a9de6d286f5e186dcaf9091252fcd444e5638193d", size = 4152714, upload-time = "2025-09-24T13:50:39.9Z" }, + { url = "https://files.pythonhosted.org/packages/53/46/319c9dc788884ad0785242543cdffac0e6530e4d0deb6c4862bc4143dcf3/shapely-2.1.2-cp312-cp312-win32.whl", hash = "sha256:9111274b88e4d7b54a95218e243282709b330ef52b7b86bc6aaf4f805306f454", size = 1542745, upload-time = "2025-09-24T13:50:41.414Z" }, + { url = "https://files.pythonhosted.org/packages/ec/bf/cb6c1c505cb31e818e900b9312d514f381fbfa5c4363edfce0fcc4f8c1a4/shapely-2.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:743044b4cfb34f9a67205cee9279feaf60ba7d02e69febc2afc609047cb49179", size = 1722861, upload-time = "2025-09-24T13:50:43.35Z" }, + { url = "https://files.pythonhosted.org/packages/c3/90/98ef257c23c46425dc4d1d31005ad7c8d649fe423a38b917db02c30f1f5a/shapely-2.1.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b510dda1a3672d6879beb319bc7c5fd302c6c354584690973c838f46ec3e0fa8", size = 1832644, upload-time = "2025-09-24T13:50:44.886Z" }, + { url = "https://files.pythonhosted.org/packages/6d/ab/0bee5a830d209adcd3a01f2d4b70e587cdd9fd7380d5198c064091005af8/shapely-2.1.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8cff473e81017594d20ec55d86b54bc635544897e13a7cfc12e36909c5309a2a", size = 1642887, upload-time = "2025-09-24T13:50:46.735Z" }, + { url = "https://files.pythonhosted.org/packages/2d/5e/7d7f54ba960c13302584c73704d8c4d15404a51024631adb60b126a4ae88/shapely-2.1.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fe7b77dc63d707c09726b7908f575fc04ff1d1ad0f3fb92aec212396bc6cfe5e", size = 2970931, upload-time = "2025-09-24T13:50:48.374Z" }, + { url = "https://files.pythonhosted.org/packages/f2/a2/83fc37e2a58090e3d2ff79175a95493c664bcd0b653dd75cb9134645a4e5/shapely-2.1.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7ed1a5bbfb386ee8332713bf7508bc24e32d24b74fc9a7b9f8529a55db9f4ee6", size = 3082855, upload-time = "2025-09-24T13:50:50.037Z" }, + { url = "https://files.pythonhosted.org/packages/44/2b/578faf235a5b09f16b5f02833c53822294d7f21b242f8e2d0cf03fb64321/shapely-2.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a84e0582858d841d54355246ddfcbd1fce3179f185da7470f41ce39d001ee1af", size = 3979960, upload-time = "2025-09-24T13:50:51.74Z" }, + { url = "https://files.pythonhosted.org/packages/4d/04/167f096386120f692cc4ca02f75a17b961858997a95e67a3cb6a7bbd6b53/shapely-2.1.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dc3487447a43d42adcdf52d7ac73804f2312cbfa5d433a7d2c506dcab0033dfd", size = 4142851, upload-time = "2025-09-24T13:50:53.49Z" }, + { url = "https://files.pythonhosted.org/packages/48/74/fb402c5a6235d1c65a97348b48cdedb75fb19eca2b1d66d04969fc1c6091/shapely-2.1.2-cp313-cp313-win32.whl", hash = "sha256:9c3a3c648aedc9f99c09263b39f2d8252f199cb3ac154fadc173283d7d111350", size = 1541890, upload-time = "2025-09-24T13:50:55.337Z" }, + { url = "https://files.pythonhosted.org/packages/41/47/3647fe7ad990af60ad98b889657a976042c9988c2807cf322a9d6685f462/shapely-2.1.2-cp313-cp313-win_amd64.whl", hash = "sha256:ca2591bff6645c216695bdf1614fca9c82ea1144d4a7591a466fef64f28f0715", size = 1722151, upload-time = "2025-09-24T13:50:57.153Z" }, + { url = "https://files.pythonhosted.org/packages/3c/49/63953754faa51ffe7d8189bfbe9ca34def29f8c0e34c67cbe2a2795f269d/shapely-2.1.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2d93d23bdd2ed9dc157b46bc2f19b7da143ca8714464249bef6771c679d5ff40", size = 1834130, upload-time = "2025-09-24T13:50:58.49Z" }, + { url = "https://files.pythonhosted.org/packages/7f/ee/dce001c1984052970ff60eb4727164892fb2d08052c575042a47f5a9e88f/shapely-2.1.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:01d0d304b25634d60bd7cf291828119ab55a3bab87dc4af1e44b07fb225f188b", size = 1642802, upload-time = "2025-09-24T13:50:59.871Z" }, + { url = "https://files.pythonhosted.org/packages/da/e7/fc4e9a19929522877fa602f705706b96e78376afb7fad09cad5b9af1553c/shapely-2.1.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8d8382dd120d64b03698b7298b89611a6ea6f55ada9d39942838b79c9bc89801", size = 3018460, upload-time = "2025-09-24T13:51:02.08Z" }, + { url = "https://files.pythonhosted.org/packages/a1/18/7519a25db21847b525696883ddc8e6a0ecaa36159ea88e0fef11466384d0/shapely-2.1.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:19efa3611eef966e776183e338b2d7ea43569ae99ab34f8d17c2c054d3205cc0", size = 3095223, upload-time = "2025-09-24T13:51:04.472Z" }, + { url = "https://files.pythonhosted.org/packages/48/de/b59a620b1f3a129c3fecc2737104a0a7e04e79335bd3b0a1f1609744cf17/shapely-2.1.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:346ec0c1a0fcd32f57f00e4134d1200e14bf3f5ae12af87ba83ca275c502498c", size = 4030760, upload-time = "2025-09-24T13:51:06.455Z" }, + { url = "https://files.pythonhosted.org/packages/96/b3/c6655ee7232b417562bae192ae0d3ceaadb1cc0ffc2088a2ddf415456cc2/shapely-2.1.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6305993a35989391bd3476ee538a5c9a845861462327efe00dd11a5c8c709a99", size = 4170078, upload-time = "2025-09-24T13:51:08.584Z" }, + { url = "https://files.pythonhosted.org/packages/a0/8e/605c76808d73503c9333af8f6cbe7e1354d2d238bda5f88eea36bfe0f42a/shapely-2.1.2-cp313-cp313t-win32.whl", hash = "sha256:c8876673449f3401f278c86eb33224c5764582f72b653a415d0e6672fde887bf", size = 1559178, upload-time = "2025-09-24T13:51:10.73Z" }, + { url = "https://files.pythonhosted.org/packages/36/f7/d317eb232352a1f1444d11002d477e54514a4a6045536d49d0c59783c0da/shapely-2.1.2-cp313-cp313t-win_amd64.whl", hash = "sha256:4a44bc62a10d84c11a7a3d7c1c4fe857f7477c3506e24c9062da0db0ae0c449c", size = 1739756, upload-time = "2025-09-24T13:51:12.105Z" }, + { url = "https://files.pythonhosted.org/packages/fc/c4/3ce4c2d9b6aabd27d26ec988f08cb877ba9e6e96086eff81bfea93e688c7/shapely-2.1.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:9a522f460d28e2bf4e12396240a5fc1518788b2fcd73535166d748399ef0c223", size = 1831290, upload-time = "2025-09-24T13:51:13.56Z" }, + { url = "https://files.pythonhosted.org/packages/17/b9/f6ab8918fc15429f79cb04afa9f9913546212d7fb5e5196132a2af46676b/shapely-2.1.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1ff629e00818033b8d71139565527ced7d776c269a49bd78c9df84e8f852190c", size = 1641463, upload-time = "2025-09-24T13:51:14.972Z" }, + { url = "https://files.pythonhosted.org/packages/a5/57/91d59ae525ca641e7ac5551c04c9503aee6f29b92b392f31790fcb1a4358/shapely-2.1.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f67b34271dedc3c653eba4e3d7111aa421d5be9b4c4c7d38d30907f796cb30df", size = 2970145, upload-time = "2025-09-24T13:51:16.961Z" }, + { url = "https://files.pythonhosted.org/packages/8a/cb/4948be52ee1da6927831ab59e10d4c29baa2a714f599f1f0d1bc747f5777/shapely-2.1.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:21952dc00df38a2c28375659b07a3979d22641aeb104751e769c3ee825aadecf", size = 3073806, upload-time = "2025-09-24T13:51:18.712Z" }, + { url = "https://files.pythonhosted.org/packages/03/83/f768a54af775eb41ef2e7bec8a0a0dbe7d2431c3e78c0a8bdba7ab17e446/shapely-2.1.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1f2f33f486777456586948e333a56ae21f35ae273be99255a191f5c1fa302eb4", size = 3980803, upload-time = "2025-09-24T13:51:20.37Z" }, + { url = "https://files.pythonhosted.org/packages/9f/cb/559c7c195807c91c79d38a1f6901384a2878a76fbdf3f1048893a9b7534d/shapely-2.1.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:cf831a13e0d5a7eb519e96f58ec26e049b1fad411fc6fc23b162a7ce04d9cffc", size = 4133301, upload-time = "2025-09-24T13:51:21.887Z" }, + { url = "https://files.pythonhosted.org/packages/80/cd/60d5ae203241c53ef3abd2ef27c6800e21afd6c94e39db5315ea0cbafb4a/shapely-2.1.2-cp314-cp314-win32.whl", hash = "sha256:61edcd8d0d17dd99075d320a1dd39c0cb9616f7572f10ef91b4b5b00c4aeb566", size = 1583247, upload-time = "2025-09-24T13:51:23.401Z" }, + { url = "https://files.pythonhosted.org/packages/74/d4/135684f342e909330e50d31d441ace06bf83c7dc0777e11043f99167b123/shapely-2.1.2-cp314-cp314-win_amd64.whl", hash = "sha256:a444e7afccdb0999e203b976adb37ea633725333e5b119ad40b1ca291ecf311c", size = 1773019, upload-time = "2025-09-24T13:51:24.873Z" }, + { url = "https://files.pythonhosted.org/packages/a3/05/a44f3f9f695fa3ada22786dc9da33c933da1cbc4bfe876fe3a100bafe263/shapely-2.1.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:5ebe3f84c6112ad3d4632b1fd2290665aa75d4cef5f6c5d77c4c95b324527c6a", size = 1834137, upload-time = "2025-09-24T13:51:26.665Z" }, + { url = "https://files.pythonhosted.org/packages/52/7e/4d57db45bf314573427b0a70dfca15d912d108e6023f623947fa69f39b72/shapely-2.1.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5860eb9f00a1d49ebb14e881f5caf6c2cf472c7fd38bd7f253bbd34f934eb076", size = 1642884, upload-time = "2025-09-24T13:51:28.029Z" }, + { url = "https://files.pythonhosted.org/packages/5a/27/4e29c0a55d6d14ad7422bf86995d7ff3f54af0eba59617eb95caf84b9680/shapely-2.1.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b705c99c76695702656327b819c9660768ec33f5ce01fa32b2af62b56ba400a1", size = 3018320, upload-time = "2025-09-24T13:51:29.903Z" }, + { url = "https://files.pythonhosted.org/packages/9f/bb/992e6a3c463f4d29d4cd6ab8963b75b1b1040199edbd72beada4af46bde5/shapely-2.1.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a1fd0ea855b2cf7c9cddaf25543e914dd75af9de08785f20ca3085f2c9ca60b0", size = 3094931, upload-time = "2025-09-24T13:51:32.699Z" }, + { url = "https://files.pythonhosted.org/packages/9c/16/82e65e21070e473f0ed6451224ed9fa0be85033d17e0c6e7213a12f59d12/shapely-2.1.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:df90e2db118c3671a0754f38e36802db75fe0920d211a27481daf50a711fdf26", size = 4030406, upload-time = "2025-09-24T13:51:34.189Z" }, + { url = "https://files.pythonhosted.org/packages/7c/75/c24ed871c576d7e2b64b04b1fe3d075157f6eb54e59670d3f5ffb36e25c7/shapely-2.1.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:361b6d45030b4ac64ddd0a26046906c8202eb60d0f9f53085f5179f1d23021a0", size = 4169511, upload-time = "2025-09-24T13:51:36.297Z" }, + { url = "https://files.pythonhosted.org/packages/b1/f7/b3d1d6d18ebf55236eec1c681ce5e665742aab3c0b7b232720a7d43df7b6/shapely-2.1.2-cp314-cp314t-win32.whl", hash = "sha256:b54df60f1fbdecc8ebc2c5b11870461a6417b3d617f555e5033f1505d36e5735", size = 1602607, upload-time = "2025-09-24T13:51:37.757Z" }, + { url = "https://files.pythonhosted.org/packages/9a/f6/f09272a71976dfc138129b8faf435d064a811ae2f708cb147dccdf7aacdb/shapely-2.1.2-cp314-cp314t-win_amd64.whl", hash = "sha256:0036ac886e0923417932c2e6369b6c52e38e0ff5d9120b90eef5cd9a5fc5cae9", size = 1796682, upload-time = "2025-09-24T13:51:39.233Z" }, +] + +[[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, upload-time = "2023-10-24T04:13:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, +] + +[[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, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "smart-open" +version = "7.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e8/be/a66598b305763861a9ab15ff0f2fbc44e47b1ce7a776797337a4eef37c66/smart_open-7.5.1.tar.gz", hash = "sha256:3f08e16827c4733699e6b2cc40328a3568f900cb12ad9a3ad233ba6c872d9fe7", size = 54034, upload-time = "2026-02-23T11:01:28.979Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5e/ea/dcdecd68acebb49d3fd560473a43499b1635076f7f1ae8641c060fe7ce74/smart_open-7.5.1-py3-none-any.whl", hash = "sha256:3e07cbbd9c8a908bcb8e25d48becf1a5cbb4886fa975e9f34c672ed171df2318", size = 64108, upload-time = "2026-02-23T11:01:27.429Z" }, +] + +[[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, upload-time = "2024-02-25T23:20:04.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, +] + +[[package]] +name = "soupsieve" +version = "2.8.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/ae/2d9c981590ed9999a0d91755b47fc74f74de286b0f5cee14c9269041e6c4/soupsieve-2.8.3.tar.gz", hash = "sha256:3267f1eeea4251fb42728b6dfb746edc9acaffc4a45b27e19450b676586e8349", size = 118627, upload-time = "2026-01-20T04:27:02.457Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/2c/1462b1d0a634697ae9e55b3cecdcb64788e8b7d63f54d923fcd0bb140aed/soupsieve-2.8.3-py3-none-any.whl", hash = "sha256:ed64f2ba4eebeab06cc4962affce381647455978ffc1e36bb79a545b91f45a95", size = 37016, upload-time = "2026-01-20T04:27:01.012Z" }, +] + +[[package]] +name = "spacy" +version = "3.8.11" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "catalogue" }, + { name = "cymem" }, + { name = "jinja2" }, + { name = "murmurhash" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "preshed" }, + { name = "pydantic" }, + { name = "requests" }, + { name = "setuptools" }, + { name = "spacy-legacy" }, + { name = "spacy-loggers" }, + { name = "srsly" }, + { name = "thinc" }, + { name = "tqdm" }, + { name = "typer-slim" }, + { name = "wasabi" }, + { name = "weasel" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/59/9f/424244b0e2656afc9ff82fb7a96931a47397bfce5ba382213827b198312a/spacy-3.8.11.tar.gz", hash = "sha256:54e1e87b74a2f9ea807ffd606166bf29ac45e2bd81ff7f608eadc7b05787d90d", size = 1326804, upload-time = "2025-11-17T20:40:03.079Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/74/d3/0c795e6f31ee3535b6e70d08e89fc22247b95b61f94fc8334a01d39bf871/spacy-3.8.11-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a12d83e8bfba07563300ae5e0086548e41aa4bfe3734c97dda87e0eec813df0d", size = 6487958, upload-time = "2025-11-17T20:38:40.378Z" }, + { url = "https://files.pythonhosted.org/packages/4e/2a/83ca9b4d0a2b31adcf0ced49fa667212d12958f75d4e238618a60eb50b10/spacy-3.8.11-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e07a50b69500ef376326545353a470f00d1ed7203c76341b97242af976e3681a", size = 6148078, upload-time = "2025-11-17T20:38:42.524Z" }, + { url = "https://files.pythonhosted.org/packages/2c/f0/ff520df18a6152ba2dbf808c964014308e71a48feb4c7563f2a6cd6e668d/spacy-3.8.11-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:718b7bb5e83c76cb841ed6e407f7b40255d0b46af7101a426c20e04af3afd64e", size = 32056451, upload-time = "2025-11-17T20:38:44.92Z" }, + { url = "https://files.pythonhosted.org/packages/9d/3a/6c44c0b9b6a70595888b8d021514ded065548a5b10718ac253bd39f9fd73/spacy-3.8.11-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f860f9d51c1aeb2d61852442b232576e4ca4d239cb3d1b40ac452118b8eb2c68", size = 32302908, upload-time = "2025-11-17T20:38:47.672Z" }, + { url = "https://files.pythonhosted.org/packages/db/77/00e99e00efd4c2456772befc48400c2e19255140660d663e16b6924a0f2e/spacy-3.8.11-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ff8d928ce70d751b7bb27f60ee5e3a308216efd4ab4517291e6ff05d9b194840", size = 32280936, upload-time = "2025-11-17T20:38:50.893Z" }, + { url = "https://files.pythonhosted.org/packages/d8/da/692b51e9e5be2766d2d1fb9a7c8122cfd99c337570e621f09c40ce94ad17/spacy-3.8.11-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3f3cb91d7d42fafd92b8d5bf9f696571170d2f0747f85724a2c5b997753e33c9", size = 33117270, upload-time = "2025-11-17T20:38:53.596Z" }, + { url = "https://files.pythonhosted.org/packages/9b/13/a542ac9b61d071f3328fda1fd8087b523fb7a4f2c340010bc70b1f762485/spacy-3.8.11-cp311-cp311-win_amd64.whl", hash = "sha256:745c190923584935272188c604e0cc170f4179aace1025814a25d92ee90cf3de", size = 15348350, upload-time = "2025-11-17T20:38:56.833Z" }, + { url = "https://files.pythonhosted.org/packages/23/53/975c16514322f6385d6caa5929771613d69f5458fb24f03e189ba533f279/spacy-3.8.11-cp311-cp311-win_arm64.whl", hash = "sha256:27535d81d9dee0483b66660cadd93d14c1668f55e4faf4386aca4a11a41a8b97", size = 14701913, upload-time = "2025-11-17T20:38:59.507Z" }, + { url = "https://files.pythonhosted.org/packages/51/fb/01eadf4ba70606b3054702dc41fc2ccf7d70fb14514b3cd57f0ff78ebea8/spacy-3.8.11-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:aa1ee8362074c30098feaaf2dd888c829a1a79c4311eec1b117a0a61f16fa6dd", size = 6073726, upload-time = "2025-11-17T20:39:01.679Z" }, + { url = "https://files.pythonhosted.org/packages/3a/f8/07b03a2997fc2621aaeafae00af50f55522304a7da6926b07027bb6d0709/spacy-3.8.11-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:75a036d04c2cf11d6cb566c0a689860cc5a7a75b439e8fea1b3a6b673dabf25d", size = 5724702, upload-time = "2025-11-17T20:39:03.486Z" }, + { url = "https://files.pythonhosted.org/packages/13/0c/c4fa0f379dbe3258c305d2e2df3760604a9fcd71b34f8f65c23e43f4cf55/spacy-3.8.11-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cb599d2747d4a59a5f90e8a453c149b13db382a8297925cf126333141dbc4f7", size = 32727774, upload-time = "2025-11-17T20:39:05.894Z" }, + { url = "https://files.pythonhosted.org/packages/ce/8e/6a4ba82bed480211ebdf5341b0f89e7271b454307525ac91b5e447825914/spacy-3.8.11-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:94632e302ad2fb79dc285bf1e9e4d4a178904d5c67049e0e02b7fb4a77af85c4", size = 33215053, upload-time = "2025-11-17T20:39:08.588Z" }, + { url = "https://files.pythonhosted.org/packages/a6/bc/44d863d248e9d7358c76a0aa8b3f196b8698df520650ed8de162e18fbffb/spacy-3.8.11-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:aeca6cf34009d48cda9fb1bbfb532469e3d643817241a73e367b34ab99a5806f", size = 32074195, upload-time = "2025-11-17T20:39:11.601Z" }, + { url = "https://files.pythonhosted.org/packages/6f/7d/0b115f3f16e1dd2d3f99b0f89497867fc11c41aed94f4b7a4367b4b54136/spacy-3.8.11-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:368a79b8df925b15d89dccb5e502039446fb2ce93cf3020e092d5b962c3349b9", size = 32996143, upload-time = "2025-11-17T20:39:14.705Z" }, + { url = "https://files.pythonhosted.org/packages/7d/48/7e9581b476df76aaf9ee182888d15322e77c38b0bbbd5e80160ba0bddd4c/spacy-3.8.11-cp312-cp312-win_amd64.whl", hash = "sha256:88d65941a87f58d75afca1785bd64d01183a92f7269dcbcf28bd9d6f6a77d1a7", size = 14217511, upload-time = "2025-11-17T20:39:17.316Z" }, + { url = "https://files.pythonhosted.org/packages/7b/1f/307a16f32f90aa5ee7ad8d29ff8620a57132b80a4c8c536963d46d192e1a/spacy-3.8.11-cp312-cp312-win_arm64.whl", hash = "sha256:97b865d6d3658e2ab103a67d6c8a2d678e193e84a07f40d9938565b669ceee39", size = 13614446, upload-time = "2025-11-17T20:39:19.748Z" }, + { url = "https://files.pythonhosted.org/packages/ed/5c/3f07cff8bc478fcf48a915ca9fe8637486a1ec676587ed3e6fd775423301/spacy-3.8.11-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ea4adeb399636059925be085c5bb852c1f3a2ebe1c2060332cbad6257d223bbc", size = 6051355, upload-time = "2025-11-17T20:39:22.243Z" }, + { url = "https://files.pythonhosted.org/packages/6d/44/4671e8098b62befec69c7848538a0824086559f74065284bbd57a5747781/spacy-3.8.11-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dd785e6bd85a58fa037da0c18fcd7250e2daecdfc30464d3882912529d1ad588", size = 5700468, upload-time = "2025-11-17T20:39:23.87Z" }, + { url = "https://files.pythonhosted.org/packages/0c/98/5708bdfb39f94af0655568e14d953886117e18bd04c3aa3ab5ff1a60ea89/spacy-3.8.11-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:598c177054eb6196deed03cac6fb7a3229f4789719ad0c9f7483f9491e375749", size = 32521877, upload-time = "2025-11-17T20:39:26.291Z" }, + { url = "https://files.pythonhosted.org/packages/c6/1f/731beb48f2c7415a71e2f655876fea8a0b3a6798be3d4d51b794f939623d/spacy-3.8.11-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a5a449ed3f2d03399481870b776f3ec61f2b831812d63dc1acedf6da70e5ab03", size = 32848355, upload-time = "2025-11-17T20:39:28.971Z" }, + { url = "https://files.pythonhosted.org/packages/47/6b/f3d131d3f9bb1c7de4f355a12adcd0a5fa77f9f624711ddd0f19c517e88b/spacy-3.8.11-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a6c35c2cb93bade9b7360d1f9db608a066246a41301bb579309efb50764ba55b", size = 31764944, upload-time = "2025-11-17T20:39:31.788Z" }, + { url = "https://files.pythonhosted.org/packages/72/bf/37ea8134667a4f2787b5f0e0146f2e8df1fb36ab67d598ad06eb5ed2e7db/spacy-3.8.11-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0156ae575b20290021573faa1fed8a82b11314e9a1c28f034713359a5240a325", size = 32718517, upload-time = "2025-11-17T20:39:35.286Z" }, + { url = "https://files.pythonhosted.org/packages/79/fe/436435dfa93cc355ed511f21cf3cda5302b7aa29716457317eb07f1cf2da/spacy-3.8.11-cp313-cp313-win_amd64.whl", hash = "sha256:6f39cf36f86bd6a8882076f86ca80f246c73aa41d7ebc8679fbbe41b6f8ec045", size = 14211913, upload-time = "2025-11-17T20:39:37.906Z" }, + { url = "https://files.pythonhosted.org/packages/c8/23/f89cfa51f54aa5e9c6c7a37f8bf4952d678f0902a5e1d81dfda33a94bfb2/spacy-3.8.11-cp313-cp313-win_arm64.whl", hash = "sha256:9a7151eee0814a5ced36642b42b1ecc8f98ac7225f3e378fb9f862ffbe84b8bf", size = 13605169, upload-time = "2025-11-17T20:39:40.455Z" }, + { url = "https://files.pythonhosted.org/packages/d7/78/ddeb09116b593f3cccc7eb489a713433076b11cf8cdfb98aec641b73a2c2/spacy-3.8.11-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:43c24d19a3f85bde0872935294a31fd9b3a6db3f92bb2b75074177cd3acec03f", size = 6067734, upload-time = "2025-11-17T20:39:42.629Z" }, + { url = "https://files.pythonhosted.org/packages/65/bb/1bb630250dc70e00fa3821879c6e2cb65c19425aba38840d3484061285c1/spacy-3.8.11-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b6158c21da57b8373d2d1afb2b73977c4bc4235d2563e7788d44367fc384939a", size = 5732963, upload-time = "2025-11-17T20:39:44.872Z" }, + { url = "https://files.pythonhosted.org/packages/7a/56/c58071b3db23932ab2b934af3462a958e7edf472da9668e4869fe2a2199e/spacy-3.8.11-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1c0bd1bde1d91f1d7a44774ca4ca3fcf064946b72599a8eb34c25e014362ace1", size = 32447290, upload-time = "2025-11-17T20:39:47.392Z" }, + { url = "https://files.pythonhosted.org/packages/34/eb/d3947efa2b46848372e89ced8371671d77219612a3eebef15db5690aa4d2/spacy-3.8.11-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:99b767c41a772e544cf2d48e0808764f42f17eb2fd6188db4a729922ff7f0c1e", size = 32488011, upload-time = "2025-11-17T20:39:50.408Z" }, + { url = "https://files.pythonhosted.org/packages/04/9e/8c6c01558b62388557247e553e48874f52637a5648b957ed01fbd628391d/spacy-3.8.11-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a3c500f04c164e4366a1163a61bf39fd50f0c63abdb1fc17991281ec52a54ab4", size = 31731340, upload-time = "2025-11-17T20:39:53.221Z" }, + { url = "https://files.pythonhosted.org/packages/23/1f/21812ec34b187ef6ba223389760dfea09bbe27d2b84b553c5205576b4ac2/spacy-3.8.11-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a2bfe45c0c1530eaabc68f5434c52b1be8df10d5c195c54d4dc2e70cea97dc65", size = 32478557, upload-time = "2025-11-17T20:39:55.826Z" }, + { url = "https://files.pythonhosted.org/packages/f3/16/a0c9174a232dfe7b48281c05364957e2c6d0f80ef26b67ce8d28a49c2d91/spacy-3.8.11-cp314-cp314-win_amd64.whl", hash = "sha256:45d0bbc8442d18dcea9257be0d1ab26e884067e038b1fa133405bf2f20c74edf", size = 14396041, upload-time = "2025-11-17T20:39:58.557Z" }, + { url = "https://files.pythonhosted.org/packages/aa/d0/a6aad5b73d523e4686474b0cfcf46f37f3d7a18765be5c1f56c1dcee4c18/spacy-3.8.11-cp314-cp314-win_arm64.whl", hash = "sha256:90a12961ecc44e0195fd42db9f0ce4aade17e6fe03f8ab98d4549911d9e6f992", size = 13823760, upload-time = "2025-11-17T20:40:00.831Z" }, +] + +[[package]] +name = "spacy-langdetect" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "langdetect" }, + { name = "pytest" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/29/70/72dad19abe81ca8e85ff951da170915211d42d705a001d7e353af349a704/spacy_langdetect-0.1.2-py3-none-any.whl", hash = "sha256:fb77878fb2445933cb6db836fc20a0d712fa685e1bd7e3b6a447d0556e54ebde", size = 5012, upload-time = "2019-05-02T06:47:23.473Z" }, +] + +[[package]] +name = "spacy-legacy" +version = "3.0.12" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d9/79/91f9d7cc8db5642acad830dcc4b49ba65a7790152832c4eceb305e46d681/spacy-legacy-3.0.12.tar.gz", hash = "sha256:b37d6e0c9b6e1d7ca1cf5bc7152ab64a4c4671f59c85adaf7a3fcb870357a774", size = 23806, upload-time = "2023-01-23T09:04:15.104Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/55/12e842c70ff8828e34e543a2c7176dac4da006ca6901c9e8b43efab8bc6b/spacy_legacy-3.0.12-py2.py3-none-any.whl", hash = "sha256:476e3bd0d05f8c339ed60f40986c07387c0a71479245d6d0f4298dbd52cda55f", size = 29971, upload-time = "2023-01-23T09:04:13.45Z" }, +] + +[[package]] +name = "spacy-loggers" +version = "1.0.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/67/3d/926db774c9c98acf66cb4ed7faf6c377746f3e00b84b700d0868b95d0712/spacy-loggers-1.0.5.tar.gz", hash = "sha256:d60b0bdbf915a60e516cc2e653baeff946f0cfc461b452d11a4d5458c6fe5f24", size = 20811, upload-time = "2023-09-11T12:26:52.323Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/78/d1a1a026ef3af911159398c939b1509d5c36fe524c7b644f34a5146c4e16/spacy_loggers-1.0.5-py3-none-any.whl", hash = "sha256:196284c9c446cc0cdb944005384270d775fdeaf4f494d8e269466cfa497ef645", size = 22343, upload-time = "2023-09-11T12:26:50.586Z" }, +] + +[[package]] +name = "srsly" +version = "2.5.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "catalogue" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cf/77/5633c4ba65e3421b72b5b4bd93aa328360b351b3a1e5bf3c90eb224668e5/srsly-2.5.2.tar.gz", hash = "sha256:4092bc843c71b7595c6c90a0302a197858c5b9fe43067f62ae6a45bc3baa1c19", size = 492055, upload-time = "2025-11-17T14:11:02.543Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/59/6e/2e3d07b38c1c2e98487f0af92f93b392c6741062d85c65cdc18c7b77448a/srsly-2.5.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e7e07babdcece2405b32c9eea25ef415749f214c889545e38965622bb66837ce", size = 655286, upload-time = "2025-11-17T14:09:52.468Z" }, + { url = "https://files.pythonhosted.org/packages/a1/e7/587bcade6b72f919133e587edf60e06039d88049aef9015cd0bdea8df189/srsly-2.5.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1718fe40b73e5cc73b14625233f57e15fb23643d146f53193e8fe653a49e9a0f", size = 653094, upload-time = "2025-11-17T14:09:53.837Z" }, + { url = "https://files.pythonhosted.org/packages/8d/24/5c3aabe292cb4eb906c828f2866624e3a65603ef0a73e964e486ff146b84/srsly-2.5.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d7b07e6103db7dd3199c0321935b0c8b9297fd6e018a66de97dc836068440111", size = 1141286, upload-time = "2025-11-17T14:09:55.535Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fe/2cbdcef2495e0c40dafb96da205d9ab3b9e59f64938277800bf65f923281/srsly-2.5.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f2dedf03b2ae143dd70039f097d128fb901deba2482c3a749ac0a985ac735aad", size = 1144667, upload-time = "2025-11-17T14:09:57.24Z" }, + { url = "https://files.pythonhosted.org/packages/91/7c/9a2c9d8141daf7b7a6f092c2be403421a0ab280e7c03cc62c223f37fdf47/srsly-2.5.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9d5be1d8b79a4c4180073461425cb49c8924a184ab49d976c9c81a7bf87731d9", size = 1103935, upload-time = "2025-11-17T14:09:58.576Z" }, + { url = "https://files.pythonhosted.org/packages/f1/ad/8ae727430368fedbb1a7fa41b62d7a86237558bc962c5c5a9aa8bfa82548/srsly-2.5.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c8e42d6bcddda2e6fc1a8438cc050c4a36d0e457a63bcc7117d23c5175dfedec", size = 1117985, upload-time = "2025-11-17T14:10:00.348Z" }, + { url = "https://files.pythonhosted.org/packages/60/69/d6afaef1a8d5192fd802752115c7c3cc104493a7d604b406112b8bc2b610/srsly-2.5.2-cp311-cp311-win_amd64.whl", hash = "sha256:e7362981e687eead00248525c3ef3b8ddd95904c93362c481988d91b26b6aeef", size = 654148, upload-time = "2025-11-17T14:10:01.772Z" }, + { url = "https://files.pythonhosted.org/packages/8f/1c/21f658d98d602a559491b7886c7ca30245c2cd8987ff1b7709437c0f74b1/srsly-2.5.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6f92b4f883e6be4ca77f15980b45d394d310f24903e25e1b2c46df783c7edcce", size = 656161, upload-time = "2025-11-17T14:10:03.181Z" }, + { url = "https://files.pythonhosted.org/packages/2f/a2/bc6fd484ed703857043ae9abd6c9aea9152f9480a6961186ee6c1e0c49e8/srsly-2.5.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ac4790a54b00203f1af5495b6b8ac214131139427f30fcf05cf971dde81930eb", size = 653237, upload-time = "2025-11-17T14:10:04.636Z" }, + { url = "https://files.pythonhosted.org/packages/ab/ea/e3895da29a15c8d325e050ad68a0d1238eece1d2648305796adf98dcba66/srsly-2.5.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ce5c6b016050857a7dd365c9dcdd00d96e7ac26317cfcb175db387e403de05bf", size = 1174418, upload-time = "2025-11-17T14:10:05.945Z" }, + { url = "https://files.pythonhosted.org/packages/a6/a5/21996231f53ee97191d0746c3a672ba33a4d86a19ffad85a1c0096c91c5f/srsly-2.5.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:539c6d0016e91277b5e9be31ebed03f03c32580d49c960e4a92c9003baecf69e", size = 1183089, upload-time = "2025-11-17T14:10:07.335Z" }, + { url = "https://files.pythonhosted.org/packages/7b/df/eb17aa8e4a828e8df7aa7dc471295529d9126e6b710f1833ebe0d8568a8e/srsly-2.5.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9f24b2c4f4c29da04083f09158543eb3f8893ba0ac39818693b3b259ee8044f0", size = 1122594, upload-time = "2025-11-17T14:10:08.899Z" }, + { url = "https://files.pythonhosted.org/packages/80/74/1654a80e6c8ec3ee32370ea08a78d3651e0ba1c4d6e6be31c9efdb9a2d10/srsly-2.5.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d34675047460a3f6999e43478f40d9b43917ea1e93a75c41d05bf7648f3e872d", size = 1139594, upload-time = "2025-11-17T14:10:10.286Z" }, + { url = "https://files.pythonhosted.org/packages/73/aa/8393344ca7f0e81965febba07afc5cad68335ed0426408d480b861ab915b/srsly-2.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:81fd133ba3c66c07f0e3a889d2b4c852984d71ea833a665238a9d47d8e051ba5", size = 654750, upload-time = "2025-11-17T14:10:11.637Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c5/dc29e65419692444253ea549106be156c5911041f16791f3b62fb90c14f2/srsly-2.5.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d976d6ae8e66006797b919e3d58533dce64cd48a5447a8ff7277f9b0505b0185", size = 654723, upload-time = "2025-11-17T14:10:13.305Z" }, + { url = "https://files.pythonhosted.org/packages/80/8c/8111e7e8c766b47b5a5f9864f27f532cf6bb92837a3e277eb297170bd6af/srsly-2.5.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:24f52ecd27409ea24ba116ee9f07a2bb1c4b9ba11284b32a0bf2ca364499d1c1", size = 651651, upload-time = "2025-11-17T14:10:14.907Z" }, + { url = "https://files.pythonhosted.org/packages/45/de/3f99d4e44af427ee09004df6586d0746640536b382c948f456be027c599b/srsly-2.5.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b0667ce1effb32a57522db10705db7c78d144547fcacc8a06df62c4bb7f96e", size = 1158012, upload-time = "2025-11-17T14:10:16.176Z" }, + { url = "https://files.pythonhosted.org/packages/c3/2f/66044ef5a10a487652913c1a7f32396cb0e9e32ecfc3fdc0a0bc0382e703/srsly-2.5.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:60782f6f79c340cdaf1ba7cbaa1d354a0f7c8f86b285f1e14e75edb51452895a", size = 1163258, upload-time = "2025-11-17T14:10:17.471Z" }, + { url = "https://files.pythonhosted.org/packages/74/6b/698834048672b52937e8cf09b554adb81b106c0492f9bc62e41e3b46a69b/srsly-2.5.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:eec51abb1b58e1e6c689714104aeeba6290c40c0bfad0243b9b594df89f05881", size = 1112214, upload-time = "2025-11-17T14:10:18.679Z" }, + { url = "https://files.pythonhosted.org/packages/85/17/1efc70426be93d32a3c6c5c12d795eb266a9255d8b537fcb924a3de57fcb/srsly-2.5.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:76464e45f73afd20c2c34d2ef145bf788afc32e7d45f36f6393ed92a85189ed3", size = 1130687, upload-time = "2025-11-17T14:10:20.346Z" }, + { url = "https://files.pythonhosted.org/packages/e2/25/07f8c8a778bc0447ee15e37089b08af81b24fcc1d4a2c09eff4c3a79b241/srsly-2.5.2-cp313-cp313-win_amd64.whl", hash = "sha256:009424a96d763951e4872b36ba38823f973bef094a1adbc11102e23e8d1ef429", size = 653128, upload-time = "2025-11-17T14:10:21.552Z" }, + { url = "https://files.pythonhosted.org/packages/39/03/3d248f538abc141d9c7ed1aa10e61506c0f95515a61066ee90e888f0cd8f/srsly-2.5.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a0911dcf1026f982bd8c5f73e1c43f1bc868416408fcbc1f3d99eb59475420c5", size = 659866, upload-time = "2025-11-17T14:10:22.811Z" }, + { url = "https://files.pythonhosted.org/packages/43/22/0fcff4c977ddfb32a6b10f33d904868b16ce655323756281f973c5a3449e/srsly-2.5.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f0ff3ac2942aee44235ca3c7712fcbd6e0d1a092e10ee16e07cef459ed6d7f65", size = 655868, upload-time = "2025-11-17T14:10:24.036Z" }, + { url = "https://files.pythonhosted.org/packages/1b/c1/e158f26a5597ac31b0f306d2584411ec1f984058e8171d76c678bf439e96/srsly-2.5.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:78385fb75e1bf7b81ffde97555aee094d270a5e0ea66f8280f6e95f5bb508b3e", size = 1156753, upload-time = "2025-11-17T14:10:25.366Z" }, + { url = "https://files.pythonhosted.org/packages/d9/bc/2001cd27fd6ecdae79050cf6b655ca646dedc0b69a756e6a87993cc47314/srsly-2.5.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2e9943b70bd7655b9eefca77aab838c3b7acea00c9dd244fd218a43dc61c518b", size = 1157916, upload-time = "2025-11-17T14:10:26.705Z" }, + { url = "https://files.pythonhosted.org/packages/5c/dd/56f563c2d0cd76c8fd22fb9f1589f18af50b54d31dd3323ceb05fe7999b8/srsly-2.5.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7d235a2bb08f5240e47c6aba4d9688b228d830fbf4c858388d9c151a10039e6d", size = 1114582, upload-time = "2025-11-17T14:10:27.997Z" }, + { url = "https://files.pythonhosted.org/packages/2e/e6/e155facc965a119e6f5d32b7e95082cadfb62cc5d97087d53db93f3a5a98/srsly-2.5.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ad94ee18b3042a6cdfdc022556e2ed9a7b52b876de86fe334c4d8ec58d59ecbc", size = 1129875, upload-time = "2025-11-17T14:10:29.295Z" }, + { url = "https://files.pythonhosted.org/packages/b6/3a/c12a4d556349c9f491b0a9d27968483f22934d2a02dfb14fb1d3a7d9b837/srsly-2.5.2-cp314-cp314-win_amd64.whl", hash = "sha256:6658467165d8fa4aec0f5f6e2da8fe977e087eaff13322b0ff20450f0d762cee", size = 658858, upload-time = "2025-11-17T14:10:30.612Z" }, + { url = "https://files.pythonhosted.org/packages/70/db/52510cbf478ab3ae8cb6c95aff3a499f2ded69df6d84df8a293630e9f10a/srsly-2.5.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:517e907792acf574979752ce33e7b15985c95d4ed7d8e38ee47f36063dc985ac", size = 666843, upload-time = "2025-11-17T14:10:32.082Z" }, + { url = "https://files.pythonhosted.org/packages/3d/da/4257b1d4c3eb005ecd135414398c033c13c4d3dffb715f63c3acd63d8d1a/srsly-2.5.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e5602797e6f87bf030b11ad356828142367c5c81e923303b5ff2a88dfb12d1e4", size = 663981, upload-time = "2025-11-17T14:10:33.542Z" }, + { url = "https://files.pythonhosted.org/packages/c6/f8/1ec5edd7299d8599def20fc3440372964f7c750022db8063e321747d1cf8/srsly-2.5.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3452306118f8604daaaac6d770ee8f910fca449e8f066dcc96a869b43ece5340", size = 1267808, upload-time = "2025-11-17T14:10:35.285Z" }, + { url = "https://files.pythonhosted.org/packages/3e/5c/4ef9782c9a3f331ef80e1ea8fc6fab50fc3d32ae61a494625d2c5f30cc4c/srsly-2.5.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e2d59f1ce00d73397a7f5b9fc33e76d17816ce051abe4eb920cec879d2a9d4f4", size = 1252838, upload-time = "2025-11-17T14:10:37.024Z" }, + { url = "https://files.pythonhosted.org/packages/39/da/d13cfc662d71eec3ccd4072433bf435bd2e11e1c5340150b4cc43fad46f4/srsly-2.5.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ebda3736651d33d92b17e26c525ba8d0b94d0ee379c9f92e8d937ba89dca8978", size = 1244558, upload-time = "2025-11-17T14:10:38.73Z" }, + { url = "https://files.pythonhosted.org/packages/26/50/92bf62dfb19532b823ef52251bb7003149e1d4a89f50a63332c8ff5f894b/srsly-2.5.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:74a9338fcc044f4bdc7113b2d9db2db8e0a263c69f1cba965acf12c845d8b365", size = 1244935, upload-time = "2025-11-17T14:10:42.324Z" }, + { url = "https://files.pythonhosted.org/packages/95/81/6ea10ef6228ce4438a240c803639f7ccf5eae3469fbc015f33bd84aa8df1/srsly-2.5.2-cp314-cp314t-win_amd64.whl", hash = "sha256:8e2b9058623c44b07441eb0d711dfdf6302f917f0634d0a294cae37578dcf899", size = 676105, upload-time = "2025-11-17T14:10:43.633Z" }, +] + +[[package]] +name = "sse-starlette" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/db/3c/fa6517610dc641262b77cc7bf994ecd17465812c1b0585fe33e11be758ab/sse_starlette-3.0.3.tar.gz", hash = "sha256:88cfb08747e16200ea990c8ca876b03910a23b547ab3bd764c0d8eb81019b971", size = 21943, upload-time = "2025-10-30T18:44:20.117Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/23/a0/984525d19ca5c8a6c33911a0c164b11490dd0f90ff7fd689f704f84e9a11/sse_starlette-3.0.3-py3-none-any.whl", hash = "sha256:af5bf5a6f3933df1d9c7f8539633dc8444ca6a97ab2e2a7cd3b6e431ac03a431", size = 11765, upload-time = "2025-10-30T18:44:18.834Z" }, +] + +[[package]] +name = "starlette" +version = "1.6.0" +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/b5/b4/205b0d5241d934e8add0c38aa924c4f9fb7330834ff11e5444db964ec3f9/starlette-1.6.0.tar.gz", hash = "sha256:d4e3ac5e546444960c710297a3c9fc3f7ebae1b7e963f3d36173b49da535be9b", size = 2716969, upload-time = "2026-08-08T18:27:57.512Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/cb/6a6a47d5b464bd08695d254f3da6e7986cc70c9fa5d778eda57538edfe56/starlette-1.6.0-py3-none-any.whl", hash = "sha256:a86dd39d14bb45f85a3d18525215a9ef0cfd1f192ac793220e72598c90335f0c", size = 75969, upload-time = "2026-08-08T18:27:56.196Z" }, +] + +[[package]] +name = "sympy" +version = "1.13.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mpmath" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/11/8a/5a7fd6284fa8caac23a26c9ddf9c30485a48169344b4bd3b0f02fef1890f/sympy-1.13.3.tar.gz", hash = "sha256:b27fd2c6530e0ab39e275fc9b683895367e51d5da91baa8d3d64db2565fec4d9", size = 7533196, upload-time = "2024-09-18T21:54:25.591Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/ff/c87e0622b1dadea79d2fb0b25ade9ed98954c9033722eb707053d310d4f3/sympy-1.13.3-py3-none-any.whl", hash = "sha256:54612cf55a62755ee71824ce692986f23c88ffa77207b30c1368eda4a7060f73", size = 6189483, upload-time = "2024-09-18T21:54:23.097Z" }, +] + +[[package]] +name = "tabulate" +version = "0.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/46/58/8c37dea7bbf769b20d58e7ace7e5edfe65b849442b00ffcdd56be88697c6/tabulate-0.10.0.tar.gz", hash = "sha256:e2cfde8f79420f6deeffdeda9aaec3b6bc5abce947655d17ac662b126e48a60d", size = 91754, upload-time = "2026-03-04T18:55:34.402Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/55/db07de81b5c630da5cbf5c7df646580ca26dfaefa593667fc6f2fe016d2e/tabulate-0.10.0-py3-none-any.whl", hash = "sha256:f0b0622e567335c8fabaaa659f1b33bcb6ddfe2e496071b743aa113f8774f2d3", size = 39814, upload-time = "2026-03-04T18:55:31.284Z" }, +] + +[[package]] +name = "tenacity" +version = "9.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/47/c6/ee486fd809e357697ee8a44d3d69222b344920433d3b6666ccd9b374630c/tenacity-9.1.4.tar.gz", hash = "sha256:adb31d4c263f2bd041081ab33b498309a57c77f9acf2db65aadf0898179cf93a", size = 49413, upload-time = "2026-02-07T10:45:33.841Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/c1/eb8f9debc45d3b7918a32ab756658a0904732f75e555402972246b0b8e71/tenacity-9.1.4-py3-none-any.whl", hash = "sha256:6095a360c919085f28c6527de529e76a06ad89b23659fa881ae0649b867a9d55", size = 28926, upload-time = "2026-02-07T10:45:32.24Z" }, +] + +[[package]] +name = "thinc" +version = "8.3.10" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "blis" }, + { name = "catalogue" }, + { name = "confection" }, + { name = "cymem" }, + { name = "murmurhash" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "preshed" }, + { name = "pydantic" }, + { name = "setuptools" }, + { name = "srsly" }, + { name = "wasabi" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2f/3a/2d0f0be132b9faaa6d56f04565ae122684273e4bf4eab8dee5f48dc00f68/thinc-8.3.10.tar.gz", hash = "sha256:5a75109f4ee1c968fc055ce651a17cb44b23b000d9e95f04a4d047ab3cb3e34e", size = 194196, upload-time = "2025-11-17T17:21:46.435Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/43/01b662540888140b5e9f76c957c7118c203cb91f17867ce78fc4f2d3800f/thinc-8.3.10-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:72793e0bd3f0f391ca36ab0996b3c21db7045409bd3740840e7d6fcd9a044d81", size = 818632, upload-time = "2025-11-17T17:20:49.123Z" }, + { url = "https://files.pythonhosted.org/packages/f0/ba/e0edcc84014bdde1bc9a082408279616a061566a82b5e3b90b9e64f33c1b/thinc-8.3.10-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4b13311acb061e04e3a0c4bd677b85ec2971e3a3674558252443b5446e378256", size = 770622, upload-time = "2025-11-17T17:20:50.467Z" }, + { url = "https://files.pythonhosted.org/packages/f3/51/0558f8cb69c13e1114428726a3fb36fe1adc5821a62ccd3fa7b7c1a5bd9a/thinc-8.3.10-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9ffddcf311fb7c998eb8988d22c618dc0f33b26303853c0445edb8a69819ac60", size = 4094652, upload-time = "2025-11-17T17:20:52.104Z" }, + { url = "https://files.pythonhosted.org/packages/a0/c9/bb78601f74f9bcadb2d3d4d5b057c4dc3f2e52d9771bad3d93a4e38a9dc1/thinc-8.3.10-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9b1e0511e8421f20abe4f22d8c8073a0d7ce4a31597cc7a404fdbad72bf38058", size = 4124379, upload-time = "2025-11-17T17:20:53.781Z" }, + { url = "https://files.pythonhosted.org/packages/f6/3e/961e1b9794111c89f2ceadfef5692aba5097bec4aaaf89f1b8a04c5bc961/thinc-8.3.10-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e31e49441dfad8fd64b8ca5f5c9b8c33ee87a553bf79c830a15b4cd02efcc444", size = 5094221, upload-time = "2025-11-17T17:20:55.466Z" }, + { url = "https://files.pythonhosted.org/packages/e5/de/da163a1533faaef5b17dd11dfb9ffd9fd5627dbef56e1160da6edbe1b224/thinc-8.3.10-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9de5dd73ce7135dcf41d68625d35cd9f5cf8e5f55a3932001a188b45057c3379", size = 5262834, upload-time = "2025-11-17T17:20:57.459Z" }, + { url = "https://files.pythonhosted.org/packages/4c/4e/449d29e33f7ddda6ba1b9e06de3ea5155c2dc33c21f438f8faafebde4e13/thinc-8.3.10-cp311-cp311-win_amd64.whl", hash = "sha256:b6d64e390a1996d489872b9d99a584142542aba59ebdc60f941f473732582f6f", size = 1791864, upload-time = "2025-11-17T17:20:59.817Z" }, + { url = "https://files.pythonhosted.org/packages/4a/b3/68038d88d45d83a501c3f19bd654d275b7ac730c807f52bbb46f35f591bc/thinc-8.3.10-cp311-cp311-win_arm64.whl", hash = "sha256:3991b6ad72e611dfbfb58235de5b67bcc9f61426127cc023607f97e8c5f43e0e", size = 1717563, upload-time = "2025-11-17T17:21:01.634Z" }, + { url = "https://files.pythonhosted.org/packages/d3/34/ba3b386d92edf50784b60ee34318d47c7f49c198268746ef7851c5bbe8cf/thinc-8.3.10-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:51bc6ef735bdbcab75ab2916731b8f61f94c66add6f9db213d900d3c6a244f95", size = 794509, upload-time = "2025-11-17T17:21:03.21Z" }, + { url = "https://files.pythonhosted.org/packages/07/f3/9f52d18115cd9d8d7b2590d226cb2752d2a5ffec61576b19462b48410184/thinc-8.3.10-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4f48b4d346915f98e9722c0c50ef911cc16c6790a2b7afebc6e1a2c96a6ce6c6", size = 741084, upload-time = "2025-11-17T17:21:04.568Z" }, + { url = "https://files.pythonhosted.org/packages/ad/9c/129c2b740c4e3d3624b6fb3dec1577ef27cb804bc1647f9bc3e1801ea20c/thinc-8.3.10-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5003f4db2db22cc8d686db8db83509acc3c50f4c55ebdcb2bbfcc1095096f7d2", size = 3846337, upload-time = "2025-11-17T17:21:06.079Z" }, + { url = "https://files.pythonhosted.org/packages/22/d2/738cf188dea8240c2be081c83ea47270fea585eba446171757d2cdb9b675/thinc-8.3.10-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b12484c3ed0632331fada2c334680dd6bc35972d0717343432dfc701f04a9b4c", size = 3901216, upload-time = "2025-11-17T17:21:07.842Z" }, + { url = "https://files.pythonhosted.org/packages/22/92/32f66eb9b1a29b797bf378a0874615d810d79eefca1d6c736c5ca3f8b918/thinc-8.3.10-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8677c446d3f9b97a465472c58683b785b25dfcf26c683e3f4e8f8c7c188e4362", size = 4827286, upload-time = "2025-11-17T17:21:09.62Z" }, + { url = "https://files.pythonhosted.org/packages/c4/5f/7ceae1e1f2029efd67ed88e23cd6dc13a5ee647cdc2b35113101b2a62c10/thinc-8.3.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:759c385ac08dcf950238b60b96a28f9c04618861141766928dff4a51b1679b25", size = 5024421, upload-time = "2025-11-17T17:21:11.199Z" }, + { url = "https://files.pythonhosted.org/packages/0b/66/30f9d8d41049b78bc614213d492792fbcfeb1b28642adf661c42110a7ebd/thinc-8.3.10-cp312-cp312-win_amd64.whl", hash = "sha256:bf3f188c3fa1fdcefd547d1f90a1245c29025d6d0e3f71d7fdf21dad210b990c", size = 1718631, upload-time = "2025-11-17T17:21:12.965Z" }, + { url = "https://files.pythonhosted.org/packages/f8/44/32e2a5018a1165a304d25eb9b1c74e5310da19a533a35331e8d824dc6a88/thinc-8.3.10-cp312-cp312-win_arm64.whl", hash = "sha256:234b7e57a6ef4e0260d99f4e8fdc328ed12d0ba9bbd98fdaa567294a17700d1c", size = 1642224, upload-time = "2025-11-17T17:21:14.371Z" }, + { url = "https://files.pythonhosted.org/packages/53/fc/17a2818d1f460b8c4f33b8bd3f21b19d263a647bfd23b572768d175e6b64/thinc-8.3.10-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c7c3a50ddd423d1c49419899acef4ac80d800af3b423593acb9e40578384b543", size = 789771, upload-time = "2025-11-17T17:21:15.784Z" }, + { url = "https://files.pythonhosted.org/packages/8d/24/649f54774b1fbe791a1c2efd7d7f0a95cfd9244902553ca7dcf19daab1dd/thinc-8.3.10-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1a1cb110398f51fc2b9a07a2a4daec6f91e166533a9c9f1c565225330f46569a", size = 737051, upload-time = "2025-11-17T17:21:17.933Z" }, + { url = "https://files.pythonhosted.org/packages/b2/8c/5840c6c504c1fa9718e1c74d6e04d77a474f594888867dbba53f9317285f/thinc-8.3.10-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:42318746a67403d04be57d862fe0c0015b58b6fb9bbbf7b6db01f3f103b73a99", size = 3839221, upload-time = "2025-11-17T17:21:20.003Z" }, + { url = "https://files.pythonhosted.org/packages/45/ef/e7fca88074cb0aa1c1a23195470b4549492c2797fe7dc9ff79a85500153a/thinc-8.3.10-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6b0e41e79973f8828adead770f885db8d0f199bfbaa9591d1d896c385842e993", size = 3885024, upload-time = "2025-11-17T17:21:21.735Z" }, + { url = "https://files.pythonhosted.org/packages/9a/eb/805e277aa019896009028d727460f071c6cf83843d70f6a69e58994d2203/thinc-8.3.10-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9ed982daa1eddbad813bfd079546483b849a68b98c01ad4a7e4efd125ddc5d7b", size = 4815939, upload-time = "2025-11-17T17:21:23.942Z" }, + { url = "https://files.pythonhosted.org/packages/4f/f5/6425f12a60e3782091c9ec16394b9239f0c18c52c70218f3c8c047ff985c/thinc-8.3.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d22bd381410749dec5f629b3162b7d1f1e2d9b7364fd49a7ea555b61c93772b9", size = 5020260, upload-time = "2025-11-17T17:21:25.507Z" }, + { url = "https://files.pythonhosted.org/packages/85/a2/ae98feffe0b161400e87b7bfc8859e6fa1e6023fa7bcfa0a8cacd83b39a1/thinc-8.3.10-cp313-cp313-win_amd64.whl", hash = "sha256:9c32830446a57da13b6856cacb0225bc2f2104f279d9928d40500081c13aa9ec", size = 1717562, upload-time = "2025-11-17T17:21:27.468Z" }, + { url = "https://files.pythonhosted.org/packages/b8/e0/faa1d04a6890ea33b9541727d2a3ca88bad794a89f73b9111af6f9aefe10/thinc-8.3.10-cp313-cp313-win_arm64.whl", hash = "sha256:aa43f9af76781d32f5f9fe29299204c8841d71e64cbb56e0e4f3d1e0387c2783", size = 1641536, upload-time = "2025-11-17T17:21:30.129Z" }, + { url = "https://files.pythonhosted.org/packages/b8/32/7a96e1f2cac159d778c6b0ab4ddd8a139bb57c602cef793b7606cd32428d/thinc-8.3.10-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:44d7038a5d28572105332b44ec9c4c3b6f7953b41d224588ad0473c9b79ccf9e", size = 793037, upload-time = "2025-11-17T17:21:32.538Z" }, + { url = "https://files.pythonhosted.org/packages/12/d8/81e8495e8ef412767c09d1f9d0d86dc60cd22e6ed75e61b49fbf1dcfcd65/thinc-8.3.10-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:639f20952af722cb0ab4c3d8a00e661686b60c04f82ef48d12064ceda3b8cd0c", size = 740768, upload-time = "2025-11-17T17:21:34.852Z" }, + { url = "https://files.pythonhosted.org/packages/c2/6d/716488a301d65c5463e92cb0eddae3672ca84f1d70937808cea9760f759c/thinc-8.3.10-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9306e62c7e7066c63b0c0ba1d164ae0c23bf38edf5a7df2e09cce69a2c290500", size = 3834983, upload-time = "2025-11-17T17:21:36.81Z" }, + { url = "https://files.pythonhosted.org/packages/9c/a1/d28b21cab9b79e9c803671bebd14489e14c5226136fad6a1c44f96f8e4ef/thinc-8.3.10-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2982604c21096de1a87b04a781a645863eece71ec6ee9f139ac01b998fb5622d", size = 3845215, upload-time = "2025-11-17T17:21:38.362Z" }, + { url = "https://files.pythonhosted.org/packages/93/9d/ff64ead5f1c2298d9e6a9ccc1c676b2347ac06162ad3c5e5d895c32a719e/thinc-8.3.10-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c6b82698e27846004d4eafc38317ace482eced888d4445f7fb9c548fd36777af", size = 4826596, upload-time = "2025-11-17T17:21:40.027Z" }, + { url = "https://files.pythonhosted.org/packages/4a/44/b80c863608d0fd31641a2d50658560c22d4841f1e445529201e22b3e1d0f/thinc-8.3.10-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2950acab8ae77427a86d11655ed0a161bc83a1edf9d31ba5c43deca6cd27ed4f", size = 4988146, upload-time = "2025-11-17T17:21:41.73Z" }, + { url = "https://files.pythonhosted.org/packages/93/6d/1bdd9344b2e7299faa55129dda624d50c334eed16a3761eb8b1dacd8bfcd/thinc-8.3.10-cp314-cp314-win_amd64.whl", hash = "sha256:c253139a5c873edf75a3b17ec9d8b6caebee072fdb489594bc64e35115df7625", size = 1738054, upload-time = "2025-11-17T17:21:43.328Z" }, + { url = "https://files.pythonhosted.org/packages/45/c4/44e3163d48e398efb3748481656963ac6265c14288012871c921dc81d004/thinc-8.3.10-cp314-cp314-win_arm64.whl", hash = "sha256:ad6da67f534995d6ec257f16665377d7ad95bef5c1b1c89618fd4528657a6f24", size = 1665001, upload-time = "2025-11-17T17:21:45.019Z" }, +] + +[[package]] +name = "threadpoolctl" +version = "3.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b7/4d/08c89e34946fce2aec4fbb45c9016efd5f4d7f24af8e5d93296e935631d8/threadpoolctl-3.6.0.tar.gz", hash = "sha256:8ab8b4aa3491d812b623328249fab5302a68d2d71745c8a4c719a2fcaba9f44e", size = 21274, upload-time = "2025-03-13T13:49:23.031Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl", hash = "sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb", size = 18638, upload-time = "2025-03-13T13:49:21.846Z" }, +] + +[[package]] +name = "tiktoken" +version = "0.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "regex" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/ab/4d017d0f76ec3171d469d80fc03dfbb4e48a4bcaddaa831b31d526f05edc/tiktoken-0.12.0.tar.gz", hash = "sha256:b18ba7ee2b093863978fcb14f74b3707cdc8d4d4d3836853ce7ec60772139931", size = 37806, upload-time = "2025-10-06T20:22:45.419Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/46/21ea696b21f1d6d1efec8639c204bdf20fde8bafb351e1355c72c5d7de52/tiktoken-0.12.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6e227c7f96925003487c33b1b32265fad2fbcec2b7cf4817afb76d416f40f6bb", size = 1051565, upload-time = "2025-10-06T20:21:44.566Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d9/35c5d2d9e22bb2a5f74ba48266fb56c63d76ae6f66e02feb628671c0283e/tiktoken-0.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c06cf0fcc24c2cb2adb5e185c7082a82cba29c17575e828518c2f11a01f445aa", size = 995284, upload-time = "2025-10-06T20:21:45.622Z" }, + { url = "https://files.pythonhosted.org/packages/01/84/961106c37b8e49b9fdcf33fe007bb3a8fdcc380c528b20cc7fbba80578b8/tiktoken-0.12.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:f18f249b041851954217e9fd8e5c00b024ab2315ffda5ed77665a05fa91f42dc", size = 1129201, upload-time = "2025-10-06T20:21:47.074Z" }, + { url = "https://files.pythonhosted.org/packages/6a/d0/3d9275198e067f8b65076a68894bb52fd253875f3644f0a321a720277b8a/tiktoken-0.12.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:47a5bc270b8c3db00bb46ece01ef34ad050e364b51d406b6f9730b64ac28eded", size = 1152444, upload-time = "2025-10-06T20:21:48.139Z" }, + { url = "https://files.pythonhosted.org/packages/78/db/a58e09687c1698a7c592e1038e01c206569b86a0377828d51635561f8ebf/tiktoken-0.12.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:508fa71810c0efdcd1b898fda574889ee62852989f7c1667414736bcb2b9a4bd", size = 1195080, upload-time = "2025-10-06T20:21:49.246Z" }, + { url = "https://files.pythonhosted.org/packages/9e/1b/a9e4d2bf91d515c0f74afc526fd773a812232dd6cda33ebea7f531202325/tiktoken-0.12.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a1af81a6c44f008cba48494089dd98cccb8b313f55e961a52f5b222d1e507967", size = 1255240, upload-time = "2025-10-06T20:21:50.274Z" }, + { url = "https://files.pythonhosted.org/packages/9d/15/963819345f1b1fb0809070a79e9dd96938d4ca41297367d471733e79c76c/tiktoken-0.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:3e68e3e593637b53e56f7237be560f7a394451cb8c11079755e80ae64b9e6def", size = 879422, upload-time = "2025-10-06T20:21:51.734Z" }, + { url = "https://files.pythonhosted.org/packages/a4/85/be65d39d6b647c79800fd9d29241d081d4eeb06271f383bb87200d74cf76/tiktoken-0.12.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b97f74aca0d78a1ff21b8cd9e9925714c15a9236d6ceacf5c7327c117e6e21e8", size = 1050728, upload-time = "2025-10-06T20:21:52.756Z" }, + { url = "https://files.pythonhosted.org/packages/4a/42/6573e9129bc55c9bf7300b3a35bef2c6b9117018acca0dc760ac2d93dffe/tiktoken-0.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2b90f5ad190a4bb7c3eb30c5fa32e1e182ca1ca79f05e49b448438c3e225a49b", size = 994049, upload-time = "2025-10-06T20:21:53.782Z" }, + { url = "https://files.pythonhosted.org/packages/66/c5/ed88504d2f4a5fd6856990b230b56d85a777feab84e6129af0822f5d0f70/tiktoken-0.12.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:65b26c7a780e2139e73acc193e5c63ac754021f160df919add909c1492c0fb37", size = 1129008, upload-time = "2025-10-06T20:21:54.832Z" }, + { url = "https://files.pythonhosted.org/packages/f4/90/3dae6cc5436137ebd38944d396b5849e167896fc2073da643a49f372dc4f/tiktoken-0.12.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:edde1ec917dfd21c1f2f8046b86348b0f54a2c0547f68149d8600859598769ad", size = 1152665, upload-time = "2025-10-06T20:21:56.129Z" }, + { url = "https://files.pythonhosted.org/packages/a3/fe/26df24ce53ffde419a42f5f53d755b995c9318908288c17ec3f3448313a3/tiktoken-0.12.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:35a2f8ddd3824608b3d650a000c1ef71f730d0c56486845705a8248da00f9fe5", size = 1194230, upload-time = "2025-10-06T20:21:57.546Z" }, + { url = "https://files.pythonhosted.org/packages/20/cc/b064cae1a0e9fac84b0d2c46b89f4e57051a5f41324e385d10225a984c24/tiktoken-0.12.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:83d16643edb7fa2c99eff2ab7733508aae1eebb03d5dfc46f5565862810f24e3", size = 1254688, upload-time = "2025-10-06T20:21:58.619Z" }, + { url = "https://files.pythonhosted.org/packages/81/10/b8523105c590c5b8349f2587e2fdfe51a69544bd5a76295fc20f2374f470/tiktoken-0.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:ffc5288f34a8bc02e1ea7047b8d041104791d2ddbf42d1e5fa07822cbffe16bd", size = 878694, upload-time = "2025-10-06T20:21:59.876Z" }, + { url = "https://files.pythonhosted.org/packages/00/61/441588ee21e6b5cdf59d6870f86beb9789e532ee9718c251b391b70c68d6/tiktoken-0.12.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:775c2c55de2310cc1bc9a3ad8826761cbdc87770e586fd7b6da7d4589e13dab3", size = 1050802, upload-time = "2025-10-06T20:22:00.96Z" }, + { url = "https://files.pythonhosted.org/packages/1f/05/dcf94486d5c5c8d34496abe271ac76c5b785507c8eae71b3708f1ad9b45a/tiktoken-0.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a01b12f69052fbe4b080a2cfb867c4de12c704b56178edf1d1d7b273561db160", size = 993995, upload-time = "2025-10-06T20:22:02.788Z" }, + { url = "https://files.pythonhosted.org/packages/a0/70/5163fe5359b943f8db9946b62f19be2305de8c3d78a16f629d4165e2f40e/tiktoken-0.12.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:01d99484dc93b129cd0964f9d34eee953f2737301f18b3c7257bf368d7615baa", size = 1128948, upload-time = "2025-10-06T20:22:03.814Z" }, + { url = "https://files.pythonhosted.org/packages/0c/da/c028aa0babf77315e1cef357d4d768800c5f8a6de04d0eac0f377cb619fa/tiktoken-0.12.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:4a1a4fcd021f022bfc81904a911d3df0f6543b9e7627b51411da75ff2fe7a1be", size = 1151986, upload-time = "2025-10-06T20:22:05.173Z" }, + { url = "https://files.pythonhosted.org/packages/a0/5a/886b108b766aa53e295f7216b509be95eb7d60b166049ce2c58416b25f2a/tiktoken-0.12.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:981a81e39812d57031efdc9ec59fa32b2a5a5524d20d4776574c4b4bd2e9014a", size = 1194222, upload-time = "2025-10-06T20:22:06.265Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f8/4db272048397636ac7a078d22773dd2795b1becee7bc4922fe6207288d57/tiktoken-0.12.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9baf52f84a3f42eef3ff4e754a0db79a13a27921b457ca9832cf944c6be4f8f3", size = 1255097, upload-time = "2025-10-06T20:22:07.403Z" }, + { url = "https://files.pythonhosted.org/packages/8e/32/45d02e2e0ea2be3a9ed22afc47d93741247e75018aac967b713b2941f8ea/tiktoken-0.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:b8a0cd0c789a61f31bf44851defbd609e8dd1e2c8589c614cc1060940ef1f697", size = 879117, upload-time = "2025-10-06T20:22:08.418Z" }, + { url = "https://files.pythonhosted.org/packages/ce/76/994fc868f88e016e6d05b0da5ac24582a14c47893f4474c3e9744283f1d5/tiktoken-0.12.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d5f89ea5680066b68bcb797ae85219c72916c922ef0fcdd3480c7d2315ffff16", size = 1050309, upload-time = "2025-10-06T20:22:10.939Z" }, + { url = "https://files.pythonhosted.org/packages/f6/b8/57ef1456504c43a849821920d582a738a461b76a047f352f18c0b26c6516/tiktoken-0.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b4e7ed1c6a7a8a60a3230965bdedba8cc58f68926b835e519341413370e0399a", size = 993712, upload-time = "2025-10-06T20:22:12.115Z" }, + { url = "https://files.pythonhosted.org/packages/72/90/13da56f664286ffbae9dbcfadcc625439142675845baa62715e49b87b68b/tiktoken-0.12.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:fc530a28591a2d74bce821d10b418b26a094bf33839e69042a6e86ddb7a7fb27", size = 1128725, upload-time = "2025-10-06T20:22:13.541Z" }, + { url = "https://files.pythonhosted.org/packages/05/df/4f80030d44682235bdaecd7346c90f67ae87ec8f3df4a3442cb53834f7e4/tiktoken-0.12.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:06a9f4f49884139013b138920a4c393aa6556b2f8f536345f11819389c703ebb", size = 1151875, upload-time = "2025-10-06T20:22:14.559Z" }, + { url = "https://files.pythonhosted.org/packages/22/1f/ae535223a8c4ef4c0c1192e3f9b82da660be9eb66b9279e95c99288e9dab/tiktoken-0.12.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:04f0e6a985d95913cabc96a741c5ffec525a2c72e9df086ff17ebe35985c800e", size = 1194451, upload-time = "2025-10-06T20:22:15.545Z" }, + { url = "https://files.pythonhosted.org/packages/78/a7/f8ead382fce0243cb625c4f266e66c27f65ae65ee9e77f59ea1653b6d730/tiktoken-0.12.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0ee8f9ae00c41770b5f9b0bb1235474768884ae157de3beb5439ca0fd70f3e25", size = 1253794, upload-time = "2025-10-06T20:22:16.624Z" }, + { url = "https://files.pythonhosted.org/packages/93/e0/6cc82a562bc6365785a3ff0af27a2a092d57c47d7a81d9e2295d8c36f011/tiktoken-0.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:dc2dd125a62cb2b3d858484d6c614d136b5b848976794edfb63688d539b8b93f", size = 878777, upload-time = "2025-10-06T20:22:18.036Z" }, + { url = "https://files.pythonhosted.org/packages/72/05/3abc1db5d2c9aadc4d2c76fa5640134e475e58d9fbb82b5c535dc0de9b01/tiktoken-0.12.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:a90388128df3b3abeb2bfd1895b0681412a8d7dc644142519e6f0a97c2111646", size = 1050188, upload-time = "2025-10-06T20:22:19.563Z" }, + { url = "https://files.pythonhosted.org/packages/e3/7b/50c2f060412202d6c95f32b20755c7a6273543b125c0985d6fa9465105af/tiktoken-0.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:da900aa0ad52247d8794e307d6446bd3cdea8e192769b56276695d34d2c9aa88", size = 993978, upload-time = "2025-10-06T20:22:20.702Z" }, + { url = "https://files.pythonhosted.org/packages/14/27/bf795595a2b897e271771cd31cb847d479073497344c637966bdf2853da1/tiktoken-0.12.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:285ba9d73ea0d6171e7f9407039a290ca77efcdb026be7769dccc01d2c8d7fff", size = 1129271, upload-time = "2025-10-06T20:22:22.06Z" }, + { url = "https://files.pythonhosted.org/packages/f5/de/9341a6d7a8f1b448573bbf3425fa57669ac58258a667eb48a25dfe916d70/tiktoken-0.12.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:d186a5c60c6a0213f04a7a802264083dea1bbde92a2d4c7069e1a56630aef830", size = 1151216, upload-time = "2025-10-06T20:22:23.085Z" }, + { url = "https://files.pythonhosted.org/packages/75/0d/881866647b8d1be4d67cb24e50d0c26f9f807f994aa1510cb9ba2fe5f612/tiktoken-0.12.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:604831189bd05480f2b885ecd2d1986dc7686f609de48208ebbbddeea071fc0b", size = 1194860, upload-time = "2025-10-06T20:22:24.602Z" }, + { url = "https://files.pythonhosted.org/packages/b3/1e/b651ec3059474dab649b8d5b69f5c65cd8fcd8918568c1935bd4136c9392/tiktoken-0.12.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8f317e8530bb3a222547b85a58583238c8f74fd7a7408305f9f63246d1a0958b", size = 1254567, upload-time = "2025-10-06T20:22:25.671Z" }, + { url = "https://files.pythonhosted.org/packages/80/57/ce64fd16ac390fafde001268c364d559447ba09b509181b2808622420eec/tiktoken-0.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:399c3dd672a6406719d84442299a490420b458c44d3ae65516302a99675888f3", size = 921067, upload-time = "2025-10-06T20:22:26.753Z" }, + { url = "https://files.pythonhosted.org/packages/ac/a4/72eed53e8976a099539cdd5eb36f241987212c29629d0a52c305173e0a68/tiktoken-0.12.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2c714c72bc00a38ca969dae79e8266ddec999c7ceccd603cc4f0d04ccd76365", size = 1050473, upload-time = "2025-10-06T20:22:27.775Z" }, + { url = "https://files.pythonhosted.org/packages/e6/d7/0110b8f54c008466b19672c615f2168896b83706a6611ba6e47313dbc6e9/tiktoken-0.12.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:cbb9a3ba275165a2cb0f9a83f5d7025afe6b9d0ab01a22b50f0e74fee2ad253e", size = 993855, upload-time = "2025-10-06T20:22:28.799Z" }, + { url = "https://files.pythonhosted.org/packages/5f/77/4f268c41a3957c418b084dd576ea2fad2e95da0d8e1ab705372892c2ca22/tiktoken-0.12.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:dfdfaa5ffff8993a3af94d1125870b1d27aed7cb97aa7eb8c1cefdbc87dbee63", size = 1129022, upload-time = "2025-10-06T20:22:29.981Z" }, + { url = "https://files.pythonhosted.org/packages/4e/2b/fc46c90fe5028bd094cd6ee25a7db321cb91d45dc87531e2bdbb26b4867a/tiktoken-0.12.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:584c3ad3d0c74f5269906eb8a659c8bfc6144a52895d9261cdaf90a0ae5f4de0", size = 1150736, upload-time = "2025-10-06T20:22:30.996Z" }, + { url = "https://files.pythonhosted.org/packages/28/c0/3c7a39ff68022ddfd7d93f3337ad90389a342f761c4d71de99a3ccc57857/tiktoken-0.12.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:54c891b416a0e36b8e2045b12b33dd66fb34a4fe7965565f1b482da50da3e86a", size = 1194908, upload-time = "2025-10-06T20:22:32.073Z" }, + { url = "https://files.pythonhosted.org/packages/ab/0d/c1ad6f4016a3968c048545f5d9b8ffebf577774b2ede3e2e352553b685fe/tiktoken-0.12.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5edb8743b88d5be814b1a8a8854494719080c28faaa1ccbef02e87354fe71ef0", size = 1253706, upload-time = "2025-10-06T20:22:33.385Z" }, + { url = "https://files.pythonhosted.org/packages/af/df/c7891ef9d2712ad774777271d39fdef63941ffba0a9d59b7ad1fd2765e57/tiktoken-0.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f61c0aea5565ac82e2ec50a05e02a6c44734e91b51c10510b084ea1b8e633a71", size = 920667, upload-time = "2025-10-06T20:22:34.444Z" }, +] + +[[package]] +name = "tokenizers" +version = "0.22.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/73/6f/f80cfef4a312e1fb34baf7d85c72d4411afde10978d4657f8cdd811d3ccc/tokenizers-0.22.2.tar.gz", hash = "sha256:473b83b915e547aa366d1eee11806deaf419e17be16310ac0a14077f1e28f917", size = 372115, upload-time = "2026-01-05T10:45:15.988Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/97/5dbfabf04c7e348e655e907ed27913e03db0923abb5dfdd120d7b25630e1/tokenizers-0.22.2-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:544dd704ae7238755d790de45ba8da072e9af3eea688f698b137915ae959281c", size = 3100275, upload-time = "2026-01-05T10:41:02.158Z" }, + { url = "https://files.pythonhosted.org/packages/2e/47/174dca0502ef88b28f1c9e06b73ce33500eedfac7a7692108aec220464e7/tokenizers-0.22.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:1e418a55456beedca4621dbab65a318981467a2b188e982a23e117f115ce5001", size = 2981472, upload-time = "2026-01-05T10:41:00.276Z" }, + { url = "https://files.pythonhosted.org/packages/d6/84/7990e799f1309a8b87af6b948f31edaa12a3ed22d11b352eaf4f4b2e5753/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2249487018adec45d6e3554c71d46eb39fa8ea67156c640f7513eb26f318cec7", size = 3290736, upload-time = "2026-01-05T10:40:32.165Z" }, + { url = "https://files.pythonhosted.org/packages/78/59/09d0d9ba94dcd5f4f1368d4858d24546b4bdc0231c2354aa31d6199f0399/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25b85325d0815e86e0bac263506dd114578953b7b53d7de09a6485e4a160a7dd", size = 3168835, upload-time = "2026-01-05T10:40:38.847Z" }, + { url = "https://files.pythonhosted.org/packages/47/50/b3ebb4243e7160bda8d34b731e54dd8ab8b133e50775872e7a434e524c28/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bfb88f22a209ff7b40a576d5324bf8286b519d7358663db21d6246fb17eea2d5", size = 3521673, upload-time = "2026-01-05T10:40:56.614Z" }, + { url = "https://files.pythonhosted.org/packages/e0/fa/89f4cb9e08df770b57adb96f8cbb7e22695a4cb6c2bd5f0c4f0ebcf33b66/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c774b1276f71e1ef716e5486f21e76333464f47bece56bbd554485982a9e03e", size = 3724818, upload-time = "2026-01-05T10:40:44.507Z" }, + { url = "https://files.pythonhosted.org/packages/64/04/ca2363f0bfbe3b3d36e95bf67e56a4c88c8e3362b658e616d1ac185d47f2/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df6c4265b289083bf710dff49bc51ef252f9d5be33a45ee2bed151114a56207b", size = 3379195, upload-time = "2026-01-05T10:40:51.139Z" }, + { url = "https://files.pythonhosted.org/packages/2e/76/932be4b50ef6ccedf9d3c6639b056a967a86258c6d9200643f01269211ca/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:369cc9fc8cc10cb24143873a0d95438bb8ee257bb80c71989e3ee290e8d72c67", size = 3274982, upload-time = "2026-01-05T10:40:58.331Z" }, + { url = "https://files.pythonhosted.org/packages/1d/28/5f9f5a4cc211b69e89420980e483831bcc29dade307955cc9dc858a40f01/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:29c30b83d8dcd061078b05ae0cb94d3c710555fbb44861139f9f83dcca3dc3e4", size = 9478245, upload-time = "2026-01-05T10:41:04.053Z" }, + { url = "https://files.pythonhosted.org/packages/6c/fb/66e2da4704d6aadebf8cb39f1d6d1957df667ab24cff2326b77cda0dcb85/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:37ae80a28c1d3265bb1f22464c856bd23c02a05bb211e56d0c5301a435be6c1a", size = 9560069, upload-time = "2026-01-05T10:45:10.673Z" }, + { url = "https://files.pythonhosted.org/packages/16/04/fed398b05caa87ce9b1a1bb5166645e38196081b225059a6edaff6440fac/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:791135ee325f2336f498590eb2f11dc5c295232f288e75c99a36c5dbce63088a", size = 9899263, upload-time = "2026-01-05T10:45:12.559Z" }, + { url = "https://files.pythonhosted.org/packages/05/a1/d62dfe7376beaaf1394917e0f8e93ee5f67fea8fcf4107501db35996586b/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:38337540fbbddff8e999d59970f3c6f35a82de10053206a7562f1ea02d046fa5", size = 10033429, upload-time = "2026-01-05T10:45:14.333Z" }, + { url = "https://files.pythonhosted.org/packages/fd/18/a545c4ea42af3df6effd7d13d250ba77a0a86fb20393143bbb9a92e434d4/tokenizers-0.22.2-cp39-abi3-win32.whl", hash = "sha256:a6bf3f88c554a2b653af81f3204491c818ae2ac6fbc09e76ef4773351292bc92", size = 2502363, upload-time = "2026-01-05T10:45:20.593Z" }, + { url = "https://files.pythonhosted.org/packages/65/71/0670843133a43d43070abeb1949abfdef12a86d490bea9cd9e18e37c5ff7/tokenizers-0.22.2-cp39-abi3-win_amd64.whl", hash = "sha256:c9ea31edff2968b44a88f97d784c2f16dc0729b8b143ed004699ebca91f05c48", size = 2747786, upload-time = "2026-01-05T10:45:18.411Z" }, + { url = "https://files.pythonhosted.org/packages/72/f4/0de46cfa12cdcbcd464cc59fde36912af405696f687e53a091fb432f694c/tokenizers-0.22.2-cp39-abi3-win_arm64.whl", hash = "sha256:9ce725d22864a1e965217204946f830c37876eee3b2ba6fc6255e8e903d5fcbc", size = 2612133, upload-time = "2026-01-05T10:45:17.232Z" }, +] + +[[package]] +name = "tomlkit" +version = "0.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/af/14b24e41977adb296d6bd1fb59402cf7d60ce364f90c890bd2ec65c43b5a/tomlkit-0.14.0.tar.gz", hash = "sha256:cf00efca415dbd57575befb1f6634c4f42d2d87dbba376128adb42c121b87064", size = 187167, upload-time = "2026-01-13T01:14:53.304Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/11/87d6d29fb5d237229d67973a6c9e06e048f01cf4994dee194ab0ea841814/tomlkit-0.14.0-py3-none-any.whl", hash = "sha256:592064ed85b40fa213469f81ac584f67a4f2992509a7c3ea2d632208623a3680", size = 39310, upload-time = "2026-01-13T01:14:51.965Z" }, +] + +[[package]] +name = "torch" +version = "2.10.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cuda-bindings", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "filelock" }, + { name = "fsspec" }, + { name = "jinja2" }, + { name = "networkx" }, + { 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-nvshmem-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/0f/8b/4b61d6e13f7108f36910df9ab4b58fd389cc2520d54d81b88660804aad99/torch-2.10.0-2-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:418997cb02d0a0f1497cf6a09f63166f9f5df9f3e16c8a716ab76a72127c714f", size = 79423467, upload-time = "2026-02-10T21:44:48.711Z" }, + { url = "https://files.pythonhosted.org/packages/d3/54/a2ba279afcca44bbd320d4e73675b282fcee3d81400ea1b53934efca6462/torch-2.10.0-2-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:13ec4add8c3faaed8d13e0574f5cd4a323c11655546f91fbe6afa77b57423574", size = 79498202, upload-time = "2026-02-10T21:44:52.603Z" }, + { url = "https://files.pythonhosted.org/packages/ec/23/2c9fe0c9c27f7f6cb865abcea8a4568f29f00acaeadfc6a37f6801f84cb4/torch-2.10.0-2-cp313-none-macosx_11_0_arm64.whl", hash = "sha256:e521c9f030a3774ed770a9c011751fb47c4d12029a3d6522116e48431f2ff89e", size = 79498254, upload-time = "2026-02-10T21:44:44.095Z" }, + { url = "https://files.pythonhosted.org/packages/36/ab/7b562f1808d3f65414cd80a4f7d4bb00979d9355616c034c171249e1a303/torch-2.10.0-3-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:ac5bdcbb074384c66fa160c15b1ead77839e3fe7ed117d667249afce0acabfac", size = 915518691, upload-time = "2026-03-11T14:15:43.147Z" }, + { url = "https://files.pythonhosted.org/packages/b3/7a/abada41517ce0011775f0f4eacc79659bc9bc6c361e6bfe6f7052a6b9363/torch-2.10.0-3-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:98c01b8bb5e3240426dcde1446eed6f40c778091c8544767ef1168fc663a05a6", size = 915622781, upload-time = "2026-03-11T14:17:11.354Z" }, + { url = "https://files.pythonhosted.org/packages/ab/c6/4dfe238342ffdcec5aef1c96c457548762d33c40b45a1ab7033bb26d2ff2/torch-2.10.0-3-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:80b1b5bfe38eb0e9f5ff09f206dcac0a87aadd084230d4a36eea5ec5232c115b", size = 915627275, upload-time = "2026-03-11T14:16:11.325Z" }, + { url = "https://files.pythonhosted.org/packages/d8/f0/72bf18847f58f877a6a8acf60614b14935e2f156d942483af1ffc081aea0/torch-2.10.0-3-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:46b3574d93a2a8134b3f5475cfb98e2eb46771794c57015f6ad1fb795ec25e49", size = 915523474, upload-time = "2026-03-11T14:17:44.422Z" }, + { url = "https://files.pythonhosted.org/packages/f4/39/590742415c3030551944edc2ddc273ea1fdfe8ffb2780992e824f1ebee98/torch-2.10.0-3-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:b1d5e2aba4eb7f8e87fbe04f86442887f9167a35f092afe4c237dfcaaef6e328", size = 915632474, upload-time = "2026-03-11T14:15:13.666Z" }, + { url = "https://files.pythonhosted.org/packages/b6/8e/34949484f764dde5b222b7fe3fede43e4a6f0da9d7f8c370bb617d629ee2/torch-2.10.0-3-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:0228d20b06701c05a8f978357f657817a4a63984b0c90745def81c18aedfa591", size = 915523882, upload-time = "2026-03-11T14:14:46.311Z" }, + { url = "https://files.pythonhosted.org/packages/78/89/f5554b13ebd71e05c0b002f95148033e730d3f7067f67423026cc9c69410/torch-2.10.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:3282d9febd1e4e476630a099692b44fdc214ee9bf8ee5377732d9d9dfe5712e4", size = 145992610, upload-time = "2026-01-21T16:25:26.327Z" }, + { url = "https://files.pythonhosted.org/packages/ae/30/a3a2120621bf9c17779b169fc17e3dc29b230c29d0f8222f499f5e159aa8/torch-2.10.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:a2f9edd8dbc99f62bc4dfb78af7bf89499bca3d753423ac1b4e06592e467b763", size = 915607863, upload-time = "2026-01-21T16:25:06.696Z" }, + { url = "https://files.pythonhosted.org/packages/6f/3d/c87b33c5f260a2a8ad68da7147e105f05868c281c63d65ed85aa4da98c66/torch-2.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:29b7009dba4b7a1c960260fc8ac85022c784250af43af9fb0ebafc9883782ebd", size = 113723116, upload-time = "2026-01-21T16:25:21.916Z" }, + { url = "https://files.pythonhosted.org/packages/61/d8/15b9d9d3a6b0c01b883787bd056acbe5cc321090d4b216d3ea89a8fcfdf3/torch-2.10.0-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:b7bd80f3477b830dd166c707c5b0b82a898e7b16f59a7d9d42778dd058272e8b", size = 79423461, upload-time = "2026-01-21T16:24:50.266Z" }, + { url = "https://files.pythonhosted.org/packages/cc/af/758e242e9102e9988969b5e621d41f36b8f258bb4a099109b7a4b4b50ea4/torch-2.10.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:5fd4117d89ffd47e3dcc71e71a22efac24828ad781c7e46aaaf56bf7f2796acf", size = 145996088, upload-time = "2026-01-21T16:24:44.171Z" }, + { url = "https://files.pythonhosted.org/packages/23/8e/3c74db5e53bff7ed9e34c8123e6a8bfef718b2450c35eefab85bb4a7e270/torch-2.10.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:787124e7db3b379d4f1ed54dd12ae7c741c16a4d29b49c0226a89bea50923ffb", size = 915711952, upload-time = "2026-01-21T16:23:53.503Z" }, + { url = "https://files.pythonhosted.org/packages/6e/01/624c4324ca01f66ae4c7cd1b74eb16fb52596dce66dbe51eff95ef9e7a4c/torch-2.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:2c66c61f44c5f903046cc696d088e21062644cbe541c7f1c4eaae88b2ad23547", size = 113757972, upload-time = "2026-01-21T16:24:39.516Z" }, + { url = "https://files.pythonhosted.org/packages/c9/5c/dee910b87c4d5c0fcb41b50839ae04df87c1cfc663cf1b5fca7ea565eeaa/torch-2.10.0-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:6d3707a61863d1c4d6ebba7be4ca320f42b869ee657e9b2c21c736bf17000294", size = 79498198, upload-time = "2026-01-21T16:24:34.704Z" }, + { url = "https://files.pythonhosted.org/packages/c9/6f/f2e91e34e3fcba2e3fc8d8f74e7d6c22e74e480bbd1db7bc8900fdf3e95c/torch-2.10.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:5c4d217b14741e40776dd7074d9006fd28b8a97ef5654db959d8635b2fe5f29b", size = 146004247, upload-time = "2026-01-21T16:24:29.335Z" }, + { url = "https://files.pythonhosted.org/packages/98/fb/5160261aeb5e1ee12ee95fe599d0541f7c976c3701d607d8fc29e623229f/torch-2.10.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:6b71486353fce0f9714ca0c9ef1c850a2ae766b409808acd58e9678a3edb7738", size = 915716445, upload-time = "2026-01-21T16:22:45.353Z" }, + { url = "https://files.pythonhosted.org/packages/6a/16/502fb1b41e6d868e8deb5b0e3ae926bbb36dab8ceb0d1b769b266ad7b0c3/torch-2.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:c2ee399c644dc92ef7bc0d4f7e74b5360c37cdbe7c5ba11318dda49ffac2bc57", size = 113757050, upload-time = "2026-01-21T16:24:19.204Z" }, + { url = "https://files.pythonhosted.org/packages/1a/0b/39929b148f4824bc3ad6f9f72a29d4ad865bcf7ebfc2fa67584773e083d2/torch-2.10.0-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:3202429f58309b9fa96a614885eace4b7995729f44beb54d3e4a47773649d382", size = 79851305, upload-time = "2026-01-21T16:24:09.209Z" }, + { url = "https://files.pythonhosted.org/packages/d8/14/21fbce63bc452381ba5f74a2c0a959fdf5ad5803ccc0c654e752e0dbe91a/torch-2.10.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:aae1b29cd68e50a9397f5ee897b9c24742e9e306f88a807a27d617f07adb3bd8", size = 146005472, upload-time = "2026-01-21T16:22:29.022Z" }, + { url = "https://files.pythonhosted.org/packages/54/fd/b207d1c525cb570ef47f3e9f836b154685011fce11a2f444ba8a4084d042/torch-2.10.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:6021db85958db2f07ec94e1bc77212721ba4920c12a18dc552d2ae36a3eb163f", size = 915612644, upload-time = "2026-01-21T16:21:47.019Z" }, + { url = "https://files.pythonhosted.org/packages/36/53/0197f868c75f1050b199fe58f9bf3bf3aecac9b4e85cc9c964383d745403/torch-2.10.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ff43db38af76fda183156153983c9a096fc4c78d0cd1e07b14a2314c7f01c2c8", size = 113997015, upload-time = "2026-01-21T16:23:00.767Z" }, + { url = "https://files.pythonhosted.org/packages/0e/13/e76b4d9c160e89fff48bf16b449ea324bda84745d2ab30294c37c2434c0d/torch-2.10.0-cp313-none-macosx_11_0_arm64.whl", hash = "sha256:cdf2a523d699b70d613243211ecaac14fe9c5df8a0b0a9c02add60fb2a413e0f", size = 79498248, upload-time = "2026-01-21T16:23:09.315Z" }, + { url = "https://files.pythonhosted.org/packages/4f/93/716b5ac0155f1be70ed81bacc21269c3ece8dba0c249b9994094110bfc51/torch-2.10.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:bf0d9ff448b0218e0433aeb198805192346c4fd659c852370d5cc245f602a06a", size = 79464992, upload-time = "2026-01-21T16:23:05.162Z" }, + { url = "https://files.pythonhosted.org/packages/69/2b/51e663ff190c9d16d4a8271203b71bc73a16aa7619b9f271a69b9d4a936b/torch-2.10.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:233aed0659a2503b831d8a67e9da66a62c996204c0bba4f4c442ccc0c68a3f60", size = 146018567, upload-time = "2026-01-21T16:22:23.393Z" }, + { url = "https://files.pythonhosted.org/packages/5e/cd/4b95ef7f293b927c283db0b136c42be91c8ec6845c44de0238c8c23bdc80/torch-2.10.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:682497e16bdfa6efeec8cde66531bc8d1fbbbb4d8788ec6173c089ed3cc2bfe5", size = 915721646, upload-time = "2026-01-21T16:21:16.983Z" }, + { url = "https://files.pythonhosted.org/packages/56/97/078a007208f8056d88ae43198833469e61a0a355abc0b070edd2c085eb9a/torch-2.10.0-cp314-cp314-win_amd64.whl", hash = "sha256:6528f13d2a8593a1a412ea07a99812495bec07e9224c28b2a25c0a30c7da025c", size = 113752373, upload-time = "2026-01-21T16:22:13.471Z" }, + { url = "https://files.pythonhosted.org/packages/d8/94/71994e7d0d5238393df9732fdab607e37e2b56d26a746cb59fdb415f8966/torch-2.10.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:f5ab4ba32383061be0fb74bda772d470140a12c1c3b58a0cfbf3dae94d164c28", size = 79850324, upload-time = "2026-01-21T16:22:09.494Z" }, + { url = "https://files.pythonhosted.org/packages/e2/65/1a05346b418ea8ccd10360eef4b3e0ce688fba544e76edec26913a8d0ee0/torch-2.10.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:716b01a176c2a5659c98f6b01bf868244abdd896526f1c692712ab36dbaf9b63", size = 146006482, upload-time = "2026-01-21T16:22:18.42Z" }, + { url = "https://files.pythonhosted.org/packages/1d/b9/5f6f9d9e859fc3235f60578fa64f52c9c6e9b4327f0fe0defb6de5c0de31/torch-2.10.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:d8f5912ba938233f86361e891789595ff35ca4b4e2ac8fe3670895e5976731d6", size = 915613050, upload-time = "2026-01-21T16:20:49.035Z" }, + { url = "https://files.pythonhosted.org/packages/66/4d/35352043ee0eaffdeff154fad67cd4a31dbed7ff8e3be1cc4549717d6d51/torch-2.10.0-cp314-cp314t-win_amd64.whl", hash = "sha256:71283a373f0ee2c89e0f0d5f446039bdabe8dbc3c9ccf35f0f784908b0acd185", size = 113995816, upload-time = "2026-01-21T16:22:05.312Z" }, +] + +[[package]] +name = "torchvision" +version = "0.25.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "pillow" }, + { name = "torch" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/3e/be/c704bceaf11c4f6b19d64337a34a877fcdfe3bd68160a8c9ae9bea4a35a3/torchvision-0.25.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:db74a551946b75d19f9996c419a799ffdf6a223ecf17c656f90da011f1d75b20", size = 1874923, upload-time = "2026-01-21T16:27:46.574Z" }, + { url = "https://files.pythonhosted.org/packages/ae/e9/f143cd71232430de1f547ceab840f68c55e127d72558b1061a71d0b193cd/torchvision-0.25.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:f49964f96644dbac2506dffe1a0a7ec0f2bf8cf7a588c3319fed26e6329ffdf3", size = 2344808, upload-time = "2026-01-21T16:27:43.191Z" }, + { url = "https://files.pythonhosted.org/packages/43/ae/ad5d6165797de234c9658752acb4fce65b78a6a18d82efdf8367c940d8da/torchvision-0.25.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:153c0d2cbc34b7cf2da19d73450f24ba36d2b75ec9211b9962b5022fb9e4ecee", size = 8070752, upload-time = "2026-01-21T16:27:33.748Z" }, + { url = "https://files.pythonhosted.org/packages/23/19/55b28aecdc7f38df57b8eb55eb0b14a62b470ed8efeb22cdc74224df1d6a/torchvision-0.25.0-cp311-cp311-win_amd64.whl", hash = "sha256:ea580ffd6094cc01914ad32f8c8118174f18974629af905cea08cb6d5d48c7b7", size = 4038722, upload-time = "2026-01-21T16:27:41.355Z" }, + { url = "https://files.pythonhosted.org/packages/56/3a/6ea0d73f49a9bef38a1b3a92e8dd455cea58470985d25635beab93841748/torchvision-0.25.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c2abe430c90b1d5e552680037d68da4eb80a5852ebb1c811b2b89d299b10573b", size = 1874920, upload-time = "2026-01-21T16:27:45.348Z" }, + { url = "https://files.pythonhosted.org/packages/51/f8/c0e1ef27c66e15406fece94930e7d6feee4cb6374bbc02d945a630d6426e/torchvision-0.25.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:b75deafa2dfea3e2c2a525559b04783515e3463f6e830cb71de0fb7ea36fe233", size = 2344556, upload-time = "2026-01-21T16:27:40.125Z" }, + { url = "https://files.pythonhosted.org/packages/68/2f/f24b039169db474e8688f649377de082a965fbf85daf4e46c44412f1d15a/torchvision-0.25.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:f25aa9e380865b11ea6e9d99d84df86b9cc959f1a007cd966fc6f1ab2ed0e248", size = 8072351, upload-time = "2026-01-21T16:27:21.074Z" }, + { url = "https://files.pythonhosted.org/packages/ad/16/8f650c2e288977cf0f8f85184b90ee56ed170a4919347fc74ee99286ed6f/torchvision-0.25.0-cp312-cp312-win_amd64.whl", hash = "sha256:f9c55ae8d673ab493325d1267cbd285bb94d56f99626c00ac4644de32a59ede3", size = 4303059, upload-time = "2026-01-21T16:27:11.08Z" }, + { url = "https://files.pythonhosted.org/packages/f5/5b/1562a04a6a5a4cf8cf40016a0cdeda91ede75d6962cff7f809a85ae966a5/torchvision-0.25.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:24e11199e4d84ba9c5ee7825ebdf1cd37ce8deec225117f10243cae984ced3ec", size = 1874918, upload-time = "2026-01-21T16:27:39.02Z" }, + { url = "https://files.pythonhosted.org/packages/36/b1/3d6c42f62c272ce34fcce609bb8939bdf873dab5f1b798fd4e880255f129/torchvision-0.25.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:5f271136d2d2c0b7a24c5671795c6e4fd8da4e0ea98aeb1041f62bc04c4370ef", size = 2309106, upload-time = "2026-01-21T16:27:30.624Z" }, + { url = "https://files.pythonhosted.org/packages/c7/60/59bb9c8b67cce356daeed4cb96a717caa4f69c9822f72e223a0eae7a9bd9/torchvision-0.25.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:855c0dc6d37f462482da7531c6788518baedca1e0847f3df42a911713acdfe52", size = 8071522, upload-time = "2026-01-21T16:27:29.392Z" }, + { url = "https://files.pythonhosted.org/packages/32/a5/9a9b1de0720f884ea50dbf9acb22cbe5312e51d7b8c4ac6ba9b51efd9bba/torchvision-0.25.0-cp313-cp313-win_amd64.whl", hash = "sha256:cef0196be31be421f6f462d1e9da1101be7332d91984caa6f8022e6c78a5877f", size = 4321911, upload-time = "2026-01-21T16:27:35.195Z" }, + { url = "https://files.pythonhosted.org/packages/52/99/dca81ed21ebaeff2b67cc9f815a20fdaa418b69f5f9ea4c6ed71721470db/torchvision-0.25.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a8f8061284395ce31bcd460f2169013382ccf411148ceb2ee38e718e9860f5a7", size = 1896209, upload-time = "2026-01-21T16:27:32.159Z" }, + { url = "https://files.pythonhosted.org/packages/28/cc/2103149761fdb4eaed58a53e8437b2d716d48f05174fab1d9fcf1e2a2244/torchvision-0.25.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:146d02c9876858420adf41f3189fe90e3d6a409cbfa65454c09f25fb33bf7266", size = 2310735, upload-time = "2026-01-21T16:27:22.327Z" }, + { url = "https://files.pythonhosted.org/packages/76/ad/f4c985ad52ddd3b22711c588501be1b330adaeaf6850317f66751711b78c/torchvision-0.25.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:c4d395cb2c4a2712f6eb93a34476cdf7aae74bb6ea2ea1917f858e96344b00aa", size = 8089557, upload-time = "2026-01-21T16:27:27.666Z" }, + { url = "https://files.pythonhosted.org/packages/63/cc/0ea68b5802e5e3c31f44b307e74947bad5a38cc655231d845534ed50ddb8/torchvision-0.25.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5e6b449e9fa7d642142c0e27c41e5a43b508d57ed8e79b7c0a0c28652da8678c", size = 4344260, upload-time = "2026-01-21T16:27:17.018Z" }, + { url = "https://files.pythonhosted.org/packages/9e/1f/fa839532660e2602b7e704d65010787c5bb296258b44fa8b9c1cd6175e7d/torchvision-0.25.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:620a236288d594dcec7634c754484542dc0a5c1b0e0b83a34bda5e91e9b7c3a1", size = 1896193, upload-time = "2026-01-21T16:27:24.785Z" }, + { url = "https://files.pythonhosted.org/packages/80/ed/d51889da7ceaf5ff7a0574fb28f9b6b223df19667265395891f81b364ab3/torchvision-0.25.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:0b5e7f50002a8145a98c5694a018e738c50e2972608310c7e88e1bd4c058f6ce", size = 2309331, upload-time = "2026-01-21T16:27:19.97Z" }, + { url = "https://files.pythonhosted.org/packages/90/a5/f93fcffaddd8f12f9e812256830ec9c9ca65abbf1bc369379f9c364d1ff4/torchvision-0.25.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:632db02300e83793812eee4f61ae6a2686dab10b4cfd628b620dc47747aa9d03", size = 8088713, upload-time = "2026-01-21T16:27:15.281Z" }, + { url = "https://files.pythonhosted.org/packages/1f/eb/d0096eed5690d962853213f2ee00d91478dfcb586b62dbbb449fb8abc3a6/torchvision-0.25.0-cp314-cp314-win_amd64.whl", hash = "sha256:d1abd5ed030c708f5dbf4812ad5f6fbe9384b63c40d6bd79f8df41a4a759a917", size = 4325058, upload-time = "2026-01-21T16:27:26.165Z" }, + { url = "https://files.pythonhosted.org/packages/97/36/96374a4c7ab50dea9787ce987815614ccfe988a42e10ac1a2e3e5b60319a/torchvision-0.25.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ad9a8a5877782944d99186e4502a614770fe906626d76e9cd32446a0ac3075f2", size = 1896207, upload-time = "2026-01-21T16:27:23.383Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e2/7abb10a867db79b226b41da419b63b69c0bd5b82438c4a4ed50e084c552f/torchvision-0.25.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:40a122c3cf4d14b651f095e0f672b688dde78632783fc5cd3d4d5e4f6a828563", size = 2310741, upload-time = "2026-01-21T16:27:18.712Z" }, + { url = "https://files.pythonhosted.org/packages/08/e6/0927784e6ffc340b6676befde1c60260bd51641c9c574b9298d791a9cda4/torchvision-0.25.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:846890161b825b38aa85fc37fb3ba5eea74e7091ff28bab378287111483b6443", size = 8089772, upload-time = "2026-01-21T16:27:14.048Z" }, + { url = "https://files.pythonhosted.org/packages/b6/37/e7ca4ec820d434c0f23f824eb29f0676a0c3e7a118f1514f5b949c3356da/torchvision-0.25.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f07f01d27375ad89d72aa2b3f2180f07da95dd9d2e4c758e015c0acb2da72977", size = 4425879, upload-time = "2026-01-21T16:27:12.579Z" }, +] + +[[package]] +name = "tqdm" +version = "4.67.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/09/a9/6ba95a270c6f1fbcd8dac228323f2777d886cb206987444e4bce66338dd4/tqdm-4.67.3.tar.gz", hash = "sha256:7d825f03f89244ef73f1d4ce193cb1774a8179fd96f31d7e1dcde62092b960bb", size = 169598, upload-time = "2026-02-03T17:35:53.048Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" }, +] + +[[package]] +name = "transformers" +version = "5.8.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "regex" }, + { name = "safetensors" }, + { name = "tokenizers" }, + { name = "tqdm" }, + { name = "typer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e7/e6/4134ea2fbea322cddc7ffc94a0d8ee47fe32ce8e876b320cd37d88edfc4d/transformers-5.8.1.tar.gz", hash = "sha256:4dd5b6de4105725104d84fd6abd74b305f4debfc251b38c648ee5dd087cf543b", size = 8532019, upload-time = "2026-05-13T03:21:57.234Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/b1/8be7e7ef0b5200491312201918b6125ef9c9df9dd0f0240ccef9ac824e6b/transformers-5.8.1-py3-none-any.whl", hash = "sha256:5340fb95962162cdfdae5cc91d7f8fedd92ed75216c1154c5e1f590fcf56dd0e", size = 10632882, upload-time = "2026-05-13T03:21:52.876Z" }, +] + +[[package]] +name = "tree-sitter" +version = "0.25.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/66/7c/0350cfc47faadc0d3cf7d8237a4e34032b3014ddf4a12ded9933e1648b55/tree-sitter-0.25.2.tar.gz", hash = "sha256:fe43c158555da46723b28b52e058ad444195afd1db3ca7720c59a254544e9c20", size = 177961, upload-time = "2025-09-25T17:37:59.751Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/22/88a1e00b906d26fa8a075dd19c6c3116997cb884bf1b3c023deb065a344d/tree_sitter-0.25.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b8ca72d841215b6573ed0655b3a5cd1133f9b69a6fa561aecad40dca9029d75b", size = 146752, upload-time = "2025-09-25T17:37:24.775Z" }, + { url = "https://files.pythonhosted.org/packages/57/1c/22cc14f3910017b7a76d7358df5cd315a84fe0c7f6f7b443b49db2e2790d/tree_sitter-0.25.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cc0351cfe5022cec5a77645f647f92a936b38850346ed3f6d6babfbeeeca4d26", size = 137765, upload-time = "2025-09-25T17:37:26.103Z" }, + { url = "https://files.pythonhosted.org/packages/1c/0c/d0de46ded7d5b34631e0f630d9866dab22d3183195bf0f3b81de406d6622/tree_sitter-0.25.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1799609636c0193e16c38f366bda5af15b1ce476df79ddaae7dd274df9e44266", size = 604643, upload-time = "2025-09-25T17:37:27.398Z" }, + { url = "https://files.pythonhosted.org/packages/34/38/b735a58c1c2f60a168a678ca27b4c1a9df725d0bf2d1a8a1c571c033111e/tree_sitter-0.25.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3e65ae456ad0d210ee71a89ee112ac7e72e6c2e5aac1b95846ecc7afa68a194c", size = 632229, upload-time = "2025-09-25T17:37:28.463Z" }, + { url = "https://files.pythonhosted.org/packages/32/f6/cda1e1e6cbff5e28d8433578e2556d7ba0b0209d95a796128155b97e7693/tree_sitter-0.25.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:49ee3c348caa459244ec437ccc7ff3831f35977d143f65311572b8ba0a5f265f", size = 629861, upload-time = "2025-09-25T17:37:29.593Z" }, + { url = "https://files.pythonhosted.org/packages/f9/19/427e5943b276a0dd74c2a1f1d7a7393443f13d1ee47dedb3f8127903c080/tree_sitter-0.25.2-cp311-cp311-win_amd64.whl", hash = "sha256:56ac6602c7d09c2c507c55e58dc7026b8988e0475bd0002f8a386cce5e8e8adc", size = 127304, upload-time = "2025-09-25T17:37:30.549Z" }, + { url = "https://files.pythonhosted.org/packages/eb/d9/eef856dc15f784d85d1397a17f3ee0f82df7778efce9e1961203abfe376a/tree_sitter-0.25.2-cp311-cp311-win_arm64.whl", hash = "sha256:b3d11a3a3ac89bb8a2543d75597f905a9926f9c806f40fcca8242922d1cc6ad5", size = 113990, upload-time = "2025-09-25T17:37:31.852Z" }, + { url = "https://files.pythonhosted.org/packages/3c/9e/20c2a00a862f1c2897a436b17edb774e831b22218083b459d0d081c9db33/tree_sitter-0.25.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ddabfff809ffc983fc9963455ba1cecc90295803e06e140a4c83e94c1fa3d960", size = 146941, upload-time = "2025-09-25T17:37:34.813Z" }, + { url = "https://files.pythonhosted.org/packages/ef/04/8512e2062e652a1016e840ce36ba1cc33258b0dcc4e500d8089b4054afec/tree_sitter-0.25.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c0c0ab5f94938a23fe81928a21cc0fac44143133ccc4eb7eeb1b92f84748331c", size = 137699, upload-time = "2025-09-25T17:37:36.349Z" }, + { url = "https://files.pythonhosted.org/packages/47/8a/d48c0414db19307b0fb3bb10d76a3a0cbe275bb293f145ee7fba2abd668e/tree_sitter-0.25.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dd12d80d91d4114ca097626eb82714618dcdfacd6a5e0955216c6485c350ef99", size = 607125, upload-time = "2025-09-25T17:37:37.725Z" }, + { url = "https://files.pythonhosted.org/packages/39/d1/b95f545e9fc5001b8a78636ef942a4e4e536580caa6a99e73dd0a02e87aa/tree_sitter-0.25.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b43a9e4c89d4d0839de27cd4d6902d33396de700e9ff4c5ab7631f277a85ead9", size = 635418, upload-time = "2025-09-25T17:37:38.922Z" }, + { url = "https://files.pythonhosted.org/packages/de/4d/b734bde3fb6f3513a010fa91f1f2875442cdc0382d6a949005cd84563d8f/tree_sitter-0.25.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fbb1706407c0e451c4f8cc016fec27d72d4b211fdd3173320b1ada7a6c74c3ac", size = 631250, upload-time = "2025-09-25T17:37:40.039Z" }, + { url = "https://files.pythonhosted.org/packages/46/f2/5f654994f36d10c64d50a192239599fcae46677491c8dd53e7579c35a3e3/tree_sitter-0.25.2-cp312-cp312-win_amd64.whl", hash = "sha256:6d0302550bbe4620a5dc7649517c4409d74ef18558276ce758419cf09e578897", size = 127156, upload-time = "2025-09-25T17:37:41.132Z" }, + { url = "https://files.pythonhosted.org/packages/67/23/148c468d410efcf0a9535272d81c258d840c27b34781d625f1f627e2e27d/tree_sitter-0.25.2-cp312-cp312-win_arm64.whl", hash = "sha256:0c8b6682cac77e37cfe5cf7ec388844957f48b7bd8d6321d0ca2d852994e10d5", size = 113984, upload-time = "2025-09-25T17:37:42.074Z" }, + { url = "https://files.pythonhosted.org/packages/8c/67/67492014ce32729b63d7ef318a19f9cfedd855d677de5773476caf771e96/tree_sitter-0.25.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0628671f0de69bb279558ef6b640bcfc97864fe0026d840f872728a86cd6b6cd", size = 146926, upload-time = "2025-09-25T17:37:43.041Z" }, + { url = "https://files.pythonhosted.org/packages/4e/9c/a278b15e6b263e86c5e301c82a60923fa7c59d44f78d7a110a89a413e640/tree_sitter-0.25.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f5ddcd3e291a749b62521f71fc953f66f5fd9743973fd6dd962b092773569601", size = 137712, upload-time = "2025-09-25T17:37:44.039Z" }, + { url = "https://files.pythonhosted.org/packages/54/9a/423bba15d2bf6473ba67846ba5244b988cd97a4b1ea2b146822162256794/tree_sitter-0.25.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd88fbb0f6c3a0f28f0a68d72df88e9755cf5215bae146f5a1bdc8362b772053", size = 607873, upload-time = "2025-09-25T17:37:45.477Z" }, + { url = "https://files.pythonhosted.org/packages/ed/4c/b430d2cb43f8badfb3a3fa9d6cd7c8247698187b5674008c9d67b2a90c8e/tree_sitter-0.25.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b878e296e63661c8e124177cc3084b041ba3f5936b43076d57c487822426f614", size = 636313, upload-time = "2025-09-25T17:37:46.68Z" }, + { url = "https://files.pythonhosted.org/packages/9d/27/5f97098dbba807331d666a0997662e82d066e84b17d92efab575d283822f/tree_sitter-0.25.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d77605e0d353ba3fe5627e5490f0fbfe44141bafa4478d88ef7954a61a848dae", size = 631370, upload-time = "2025-09-25T17:37:47.993Z" }, + { url = "https://files.pythonhosted.org/packages/d4/3c/87caaed663fabc35e18dc704cd0e9800a0ee2f22bd18b9cbe7c10799895d/tree_sitter-0.25.2-cp313-cp313-win_amd64.whl", hash = "sha256:463c032bd02052d934daa5f45d183e0521ceb783c2548501cf034b0beba92c9b", size = 127157, upload-time = "2025-09-25T17:37:48.967Z" }, + { url = "https://files.pythonhosted.org/packages/d5/23/f8467b408b7988aff4ea40946a4bd1a2c1a73d17156a9d039bbaff1e2ceb/tree_sitter-0.25.2-cp313-cp313-win_arm64.whl", hash = "sha256:b3f63a1796886249bd22c559a5944d64d05d43f2be72961624278eff0dcc5cb8", size = 113975, upload-time = "2025-09-25T17:37:49.922Z" }, + { url = "https://files.pythonhosted.org/packages/07/e3/d9526ba71dfbbe4eba5e51d89432b4b333a49a1e70712aa5590cd22fc74f/tree_sitter-0.25.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:65d3c931013ea798b502782acab986bbf47ba2c452610ab0776cf4a8ef150fc0", size = 146776, upload-time = "2025-09-25T17:37:50.898Z" }, + { url = "https://files.pythonhosted.org/packages/42/97/4bd4ad97f85a23011dd8a535534bb1035c4e0bac1234d58f438e15cff51f/tree_sitter-0.25.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:bda059af9d621918efb813b22fb06b3fe00c3e94079c6143fcb2c565eb44cb87", size = 137732, upload-time = "2025-09-25T17:37:51.877Z" }, + { url = "https://files.pythonhosted.org/packages/b6/19/1e968aa0b1b567988ed522f836498a6a9529a74aab15f09dd9ac1e41f505/tree_sitter-0.25.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eac4e8e4c7060c75f395feec46421eb61212cb73998dbe004b7384724f3682ab", size = 609456, upload-time = "2025-09-25T17:37:52.925Z" }, + { url = "https://files.pythonhosted.org/packages/48/b6/cf08f4f20f4c9094006ef8828555484e842fc468827ad6e56011ab668dbd/tree_sitter-0.25.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:260586381b23be33b6191a07cea3d44ecbd6c01aa4c6b027a0439145fcbc3358", size = 636772, upload-time = "2025-09-25T17:37:54.647Z" }, + { url = "https://files.pythonhosted.org/packages/57/e2/d42d55bf56360987c32bc7b16adb06744e425670b823fb8a5786a1cea991/tree_sitter-0.25.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7d2ee1acbacebe50ba0f85fff1bc05e65d877958f00880f49f9b2af38dce1af0", size = 631522, upload-time = "2025-09-25T17:37:55.833Z" }, + { url = "https://files.pythonhosted.org/packages/03/87/af9604ebe275a9345d88c3ace0cf2a1341aa3f8ef49dd9fc11662132df8a/tree_sitter-0.25.2-cp314-cp314-win_amd64.whl", hash = "sha256:4973b718fcadfb04e59e746abfbb0288694159c6aeecd2add59320c03368c721", size = 130864, upload-time = "2025-09-25T17:37:57.453Z" }, + { url = "https://files.pythonhosted.org/packages/a6/6e/e64621037357acb83d912276ffd30a859ef117f9c680f2e3cb955f47c680/tree_sitter-0.25.2-cp314-cp314-win_arm64.whl", hash = "sha256:b8d4429954a3beb3e844e2872610d2a4800ba4eb42bb1990c6a4b1949b18459f", size = 117470, upload-time = "2025-09-25T17:37:58.431Z" }, +] + +[[package]] +name = "tree-sitter-c" +version = "0.24.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/f5/ba8cd08d717277551ade8537d3aa2a94b907c6c6e0fbcf4e4d8b1c747fa3/tree_sitter_c-0.24.1.tar.gz", hash = "sha256:7d2d0cda0b8dda428c81440c1e94367f9f13548eedca3f49768bde66b1422ad6", size = 228014, upload-time = "2025-05-24T17:32:58.384Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/15/c7/c817be36306e457c2d36cc324789046390d9d8c555c38772429ffdb7d361/tree_sitter_c-0.24.1-cp310-abi3-macosx_10_9_x86_64.whl", hash = "sha256:9c06ac26a1efdcc8b26a8a6970fbc6997c4071857359e5837d4c42892d45fe1e", size = 80940, upload-time = "2025-05-24T17:32:49.967Z" }, + { url = "https://files.pythonhosted.org/packages/7a/42/283909467290b24fdbc29bb32ee20e409a19a55002b43175d66d091ca1a4/tree_sitter_c-0.24.1-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:942bcd7cbecd810dcf7ca6f8f834391ebf0771a89479646d891ba4ca2fdfdc88", size = 86304, upload-time = "2025-05-24T17:32:51.271Z" }, + { url = "https://files.pythonhosted.org/packages/94/53/fb4f61d4e5f15ec3da85774a4df8e58d3b5b73036cf167f0203b4dd9d158/tree_sitter_c-0.24.1-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9a74cfd7a11ca5a961fafd4d751892ee65acae667d2818968a6f079397d8d28c", size = 109996, upload-time = "2025-05-24T17:32:52.119Z" }, + { url = "https://files.pythonhosted.org/packages/5e/e8/fc541d34ee81c386c5453c2596c1763e8e9cd7cb0725f39d7dfa2276afa4/tree_sitter_c-0.24.1-cp310-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6a807705a3978911dc7ee26a7ad36dcfacb6adfc13c190d496660ec9bd66707", size = 98137, upload-time = "2025-05-24T17:32:53.361Z" }, + { url = "https://files.pythonhosted.org/packages/32/c6/d0563319cae0d5b5780a92e2806074b24afea2a07aa4c10599b899bda3ec/tree_sitter_c-0.24.1-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:789781afcb710df34144f7e2a20cd80e325114b9119e3956c6bd1dd2d365df98", size = 94148, upload-time = "2025-05-24T17:32:54.855Z" }, + { url = "https://files.pythonhosted.org/packages/50/5a/6361df7f3fa2310c53a0d26b4702a261c332da16fa9d801e381e3a86e25f/tree_sitter_c-0.24.1-cp310-abi3-win_amd64.whl", hash = "sha256:290bff0f9c79c966496ebae45042f77543e6e4aea725f40587a8611d566231a8", size = 84703, upload-time = "2025-05-24T17:32:56.084Z" }, + { url = "https://files.pythonhosted.org/packages/22/6a/210a302e8025ac492cbaea58d3720d66b7d8034c5d747ac5e4d2d235aa25/tree_sitter_c-0.24.1-cp310-abi3-win_arm64.whl", hash = "sha256:d46bbda06f838c2dcb91daf767813671fd366b49ad84ff37db702129267b46e1", size = 82715, upload-time = "2025-05-24T17:32:57.248Z" }, +] + +[[package]] +name = "tree-sitter-javascript" +version = "0.25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/59/e0/e63103c72a9d3dfd89a31e02e660263ad84b7438e5f44ee82e443e65bbde/tree_sitter_javascript-0.25.0.tar.gz", hash = "sha256:329b5414874f0588a98f1c291f1b28138286617aa907746ffe55adfdcf963f38", size = 132338, upload-time = "2025-09-01T07:13:44.792Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/df/5106ac250cd03661ebc3cc75da6b3d9f6800a3606393a0122eca58038104/tree_sitter_javascript-0.25.0-cp310-abi3-macosx_10_9_x86_64.whl", hash = "sha256:b70f887fb269d6e58c349d683f59fa647140c410cfe2bee44a883b20ec92e3dc", size = 64052, upload-time = "2025-09-01T07:13:36.865Z" }, + { url = "https://files.pythonhosted.org/packages/b1/8f/6b4b2bc90d8ab3955856ce852cc9d1e82c81d7ab9646385f0e75ffd5b5d3/tree_sitter_javascript-0.25.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:8264a996b8845cfce06965152a013b5d9cbb7d199bc3503e12b5682e62bb1de1", size = 66440, upload-time = "2025-09-01T07:13:37.962Z" }, + { url = "https://files.pythonhosted.org/packages/5f/c4/7da74ecdcd8a398f88bd003a87c65403b5fe0e958cdd43fbd5fd4a398fcf/tree_sitter_javascript-0.25.0-cp310-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9dc04ba91fc8583344e57c1f1ed5b2c97ecaaf47480011b92fbeab8dda96db75", size = 99728, upload-time = "2025-09-01T07:13:38.755Z" }, + { url = "https://files.pythonhosted.org/packages/96/c8/97da3af4796495e46421e9344738addb3602fa6426ea695be3fcbadbee37/tree_sitter_javascript-0.25.0-cp310-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:199d09985190852e0912da2b8d26c932159be314bc04952cf917ed0e4c633e6b", size = 106072, upload-time = "2025-09-01T07:13:39.798Z" }, + { url = "https://files.pythonhosted.org/packages/13/be/c964e8130be08cc9bd6627d845f0e4460945b158429d39510953bbcb8fcc/tree_sitter_javascript-0.25.0-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:dfcf789064c58dc13c0a4edb550acacfc6f0f280577f1e7a00de3e89fc7f8ddc", size = 104388, upload-time = "2025-09-01T07:13:40.866Z" }, + { url = "https://files.pythonhosted.org/packages/ee/89/9b773dee0f8961d1bb8d7baf0a204ab587618df19897c1ef260916f318ec/tree_sitter_javascript-0.25.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1b852d3aee8a36186dbcc32c798b11b4869f9b5041743b63b65c2ef793db7a54", size = 98377, upload-time = "2025-09-01T07:13:41.838Z" }, + { url = "https://files.pythonhosted.org/packages/3b/dc/d90cb1790f8cec9b4878d278ad9faf7c8f893189ce0f855304fd704fc274/tree_sitter_javascript-0.25.0-cp310-abi3-win_amd64.whl", hash = "sha256:e5ed840f5bd4a3f0272e441d19429b26eedc257abe5574c8546da6b556865e3c", size = 62975, upload-time = "2025-09-01T07:13:42.828Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1f/f9eba1038b7d4394410f3c0a6ec2122b590cd7acb03f196e52fa57ebbe72/tree_sitter_javascript-0.25.0-cp310-abi3-win_arm64.whl", hash = "sha256:622a69d677aa7f6ee2931d8c77c981a33f0ebb6d275aa9d43d3397c879a9bb0b", size = 61668, upload-time = "2025-09-01T07:13:43.803Z" }, +] + +[[package]] +name = "tree-sitter-python" +version = "0.25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b8/8b/c992ff0e768cb6768d5c96234579bf8842b3a633db641455d86dd30d5dac/tree_sitter_python-0.25.0.tar.gz", hash = "sha256:b13e090f725f5b9c86aa455a268553c65cadf325471ad5b65cd29cac8a1a68ac", size = 159845, upload-time = "2025-09-11T06:47:58.159Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cf/64/a4e503c78a4eb3ac46d8e72a29c1b1237fa85238d8e972b063e0751f5a94/tree_sitter_python-0.25.0-cp310-abi3-macosx_10_9_x86_64.whl", hash = "sha256:14a79a47ddef72f987d5a2c122d148a812169d7484ff5c75a3db9609d419f361", size = 73790, upload-time = "2025-09-11T06:47:47.652Z" }, + { url = "https://files.pythonhosted.org/packages/e6/1d/60d8c2a0cc63d6ec4ba4e99ce61b802d2e39ef9db799bdf2a8f932a6cd4b/tree_sitter_python-0.25.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:480c21dbd995b7fe44813e741d71fed10ba695e7caab627fb034e3828469d762", size = 76691, upload-time = "2025-09-11T06:47:49.038Z" }, + { url = "https://files.pythonhosted.org/packages/aa/cb/d9b0b67d037922d60cbe0359e0c86457c2da721bc714381a63e2c8e35eba/tree_sitter_python-0.25.0-cp310-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:86f118e5eecad616ecdb81d171a36dde9bef5a0b21ed71ea9c3e390813c3baf5", size = 108133, upload-time = "2025-09-11T06:47:50.499Z" }, + { url = "https://files.pythonhosted.org/packages/40/bd/bf4787f57e6b2860f3f1c8c62f045b39fb32d6bac4b53d7a9e66de968440/tree_sitter_python-0.25.0-cp310-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be71650ca2b93b6e9649e5d65c6811aad87a7614c8c1003246b303f6b150f61b", size = 110603, upload-time = "2025-09-11T06:47:51.985Z" }, + { url = "https://files.pythonhosted.org/packages/5d/25/feff09f5c2f32484fbce15db8b49455c7572346ce61a699a41972dea7318/tree_sitter_python-0.25.0-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e6d5b5799628cc0f24691ab2a172a8e676f668fe90dc60468bee14084a35c16d", size = 108998, upload-time = "2025-09-11T06:47:53.046Z" }, + { url = "https://files.pythonhosted.org/packages/75/69/4946da3d6c0df316ccb938316ce007fb565d08f89d02d854f2d308f0309f/tree_sitter_python-0.25.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:71959832fc5d9642e52c11f2f7d79ae520b461e63334927e93ca46cd61cd9683", size = 107268, upload-time = "2025-09-11T06:47:54.388Z" }, + { url = "https://files.pythonhosted.org/packages/ed/a2/996fc2dfa1076dc460d3e2f3c75974ea4b8f02f6bc925383aaae519920e8/tree_sitter_python-0.25.0-cp310-abi3-win_amd64.whl", hash = "sha256:9bcde33f18792de54ee579b00e1b4fe186b7926825444766f849bf7181793a76", size = 76073, upload-time = "2025-09-11T06:47:55.773Z" }, + { url = "https://files.pythonhosted.org/packages/07/19/4b5569d9b1ebebb5907d11554a96ef3fa09364a30fcfabeff587495b512f/tree_sitter_python-0.25.0-cp310-abi3-win_arm64.whl", hash = "sha256:0fbf6a3774ad7e89ee891851204c2e2c47e12b63a5edbe2e9156997731c128bb", size = 74169, upload-time = "2025-09-11T06:47:56.747Z" }, +] + +[[package]] +name = "tree-sitter-typescript" +version = "0.23.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1e/fc/bb52958f7e399250aee093751e9373a6311cadbe76b6e0d109b853757f35/tree_sitter_typescript-0.23.2.tar.gz", hash = "sha256:7b167b5827c882261cb7a50dfa0fb567975f9b315e87ed87ad0a0a3aedb3834d", size = 773053, upload-time = "2024-11-11T02:36:11.396Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/28/95/4c00680866280e008e81dd621fd4d3f54aa3dad1b76b857a19da1b2cc426/tree_sitter_typescript-0.23.2-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:3cd752d70d8e5371fdac6a9a4df9d8924b63b6998d268586f7d374c9fba2a478", size = 286677, upload-time = "2024-11-11T02:35:58.839Z" }, + { url = "https://files.pythonhosted.org/packages/8f/2f/1f36fda564518d84593f2740d5905ac127d590baf5c5753cef2a88a89c15/tree_sitter_typescript-0.23.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:c7cc1b0ff5d91bac863b0e38b1578d5505e718156c9db577c8baea2557f66de8", size = 302008, upload-time = "2024-11-11T02:36:00.733Z" }, + { url = "https://files.pythonhosted.org/packages/96/2d/975c2dad292aa9994f982eb0b69cc6fda0223e4b6c4ea714550477d8ec3a/tree_sitter_typescript-0.23.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b1eed5b0b3a8134e86126b00b743d667ec27c63fc9de1b7bb23168803879e31", size = 351987, upload-time = "2024-11-11T02:36:02.669Z" }, + { url = "https://files.pythonhosted.org/packages/49/d1/a71c36da6e2b8a4ed5e2970819b86ef13ba77ac40d9e333cb17df6a2c5db/tree_sitter_typescript-0.23.2-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e96d36b85bcacdeb8ff5c2618d75593ef12ebaf1b4eace3477e2bdb2abb1752c", size = 344960, upload-time = "2024-11-11T02:36:04.443Z" }, + { url = "https://files.pythonhosted.org/packages/7f/cb/f57b149d7beed1a85b8266d0c60ebe4c46e79c9ba56bc17b898e17daf88e/tree_sitter_typescript-0.23.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:8d4f0f9bcb61ad7b7509d49a1565ff2cc363863644a234e1e0fe10960e55aea0", size = 340245, upload-time = "2024-11-11T02:36:06.473Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ab/dd84f0e2337296a5f09749f7b5483215d75c8fa9e33738522e5ed81f7254/tree_sitter_typescript-0.23.2-cp39-abi3-win_amd64.whl", hash = "sha256:3f730b66396bc3e11811e4465c41ee45d9e9edd6de355a58bbbc49fa770da8f9", size = 278015, upload-time = "2024-11-11T02:36:07.631Z" }, + { url = "https://files.pythonhosted.org/packages/9f/e4/81f9a935789233cf412a0ed5fe04c883841d2c8fb0b7e075958a35c65032/tree_sitter_typescript-0.23.2-cp39-abi3-win_arm64.whl", hash = "sha256:05db58f70b95ef0ea126db5560f3775692f609589ed6f8dd0af84b7f19f1cbb7", size = 274052, upload-time = "2024-11-11T02:36:09.514Z" }, +] + +[[package]] +name = "triton" +version = "3.6.0" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/12/b05ba554d2c623bffa59922b94b0775673de251f468a9609bc9e45de95e9/triton-3.6.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8e323d608e3a9bfcc2d9efcc90ceefb764a82b99dea12a86d643c72539ad5d3", size = 188214640, upload-time = "2026-01-20T16:00:35.869Z" }, + { url = "https://files.pythonhosted.org/packages/ab/a8/cdf8b3e4c98132f965f88c2313a4b493266832ad47fb52f23d14d4f86bb5/triton-3.6.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:74caf5e34b66d9f3a429af689c1c7128daba1d8208df60e81106b115c00d6fca", size = 188266850, upload-time = "2026-01-20T16:00:43.041Z" }, + { url = "https://files.pythonhosted.org/packages/f9/0b/37d991d8c130ce81a8728ae3c25b6e60935838e9be1b58791f5997b24a54/triton-3.6.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:10c7f76c6e72d2ef08df639e3d0d30729112f47a56b0c81672edc05ee5116ac9", size = 188289450, upload-time = "2026-01-20T16:00:49.136Z" }, + { url = "https://files.pythonhosted.org/packages/35/f8/9c66bfc55361ec6d0e4040a0337fb5924ceb23de4648b8a81ae9d33b2b38/triton-3.6.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d002e07d7180fd65e622134fbd980c9a3d4211fb85224b56a0a0efbd422ab72f", size = 188400296, upload-time = "2026-01-20T16:00:56.042Z" }, + { url = "https://files.pythonhosted.org/packages/df/3d/9e7eee57b37c80cec63322c0231bb6da3cfe535a91d7a4d64896fcb89357/triton-3.6.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a17a5d5985f0ac494ed8a8e54568f092f7057ef60e1b0fa09d3fd1512064e803", size = 188273063, upload-time = "2026-01-20T16:01:07.278Z" }, + { url = "https://files.pythonhosted.org/packages/f6/56/6113c23ff46c00aae423333eb58b3e60bdfe9179d542781955a5e1514cb3/triton-3.6.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:46bd1c1af4b6704e554cad2eeb3b0a6513a980d470ccfa63189737340c7746a7", size = 188397994, upload-time = "2026-01-20T16:01:14.236Z" }, +] + +[[package]] +name = "typer" +version = "0.26.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "rich" }, + { name = "shellingham" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7c/f7/68adc395201b20b872d68e975386832e8005ffeacedd43a1d837a32815be/typer-0.26.8.tar.gz", hash = "sha256:c244a6bd558886fe3f8780efb6bdd28bb9aff005a94eedebaa5cb32926fe2f7e", size = 202097, upload-time = "2026-06-26T09:22:45.705Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/80/87/b9fd69c92c6102a066e1b86a35243f53e70bd4c709f2a26d9f4fee4f4dc0/typer-0.26.8-py3-none-any.whl", hash = "sha256:3512ca79ac5c11113414b36e80281b872884477722440691c89d1112e321a49c", size = 122564, upload-time = "2026-06-26T09:22:44.72Z" }, +] + +[[package]] +name = "typer-slim" +version = "0.21.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "click" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a5/ca/0d9d822fd8a4c7e830cba36a2557b070d4b4a9558a0460377a61f8fb315d/typer_slim-0.21.2.tar.gz", hash = "sha256:78f20d793036a62aaf9c3798306142b08261d4b2a941c6e463081239f062a2f9", size = 120497, upload-time = "2026-02-10T19:33:45.836Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/03/e09325cfc40a33a82b31ba1a3f1d97e85246736856a45a43b19fcb48b1c2/typer_slim-0.21.2-py3-none-any.whl", hash = "sha256:4705082bb6c66c090f60e47c8be09a93158c139ce0aa98df7c6c47e723395e5f", size = 56790, upload-time = "2026-02-10T19:33:47.221Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "tzdata" +version = "2025.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/a7/c202b344c5ca7daf398f3b8a477eeb205cf3b6f32e7ec3a6bac0629ca975/tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7", size = 196772, upload-time = "2025-12-13T17:45:35.667Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1", size = 348521, upload-time = "2025-12-13T17:45:33.889Z" }, +] + +[[package]] +name = "tzlocal" +version = "5.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "tzdata", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8b/2e/c14812d3d4d9cd1773c6be938f89e5735a1f11a9f184ac3639b93cef35d5/tzlocal-5.3.1.tar.gz", hash = "sha256:cceffc7edecefea1f595541dbd6e990cb1ea3d19bf01b2809f362a03dd7921fd", size = 30761, upload-time = "2025-03-05T21:17:41.549Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/14/e2a54fabd4f08cd7af1c07030603c3356b74da07f7cc056e600436edfa17/tzlocal-5.3.1-py3-none-any.whl", hash = "sha256:eb1a66c3ef5847adf7a834f1be0800581b683b5608e74f86ecbcef8ab91bb85d", size = 18026, upload-time = "2025-03-05T21:17:39.857Z" }, +] + +[[package]] +name = "urllib3" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, +] + +[[package]] +name = "uuid-utils" +version = "0.14.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/d1/38a573f0c631c062cf42fa1f5d021d4dd3c31fb23e4376e4b56b0c9fbbed/uuid_utils-0.14.1.tar.gz", hash = "sha256:9bfc95f64af80ccf129c604fb6b8ca66c6f256451e32bc4570f760e4309c9b69", size = 22195, upload-time = "2026-02-20T22:50:38.833Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/b7/add4363039a34506a58457d96d4aa2126061df3a143eb4d042aedd6a2e76/uuid_utils-0.14.1-cp39-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:93a3b5dc798a54a1feb693f2d1cb4cf08258c32ff05ae4929b5f0a2ca624a4f0", size = 604679, upload-time = "2026-02-20T22:50:27.469Z" }, + { url = "https://files.pythonhosted.org/packages/dd/84/d1d0bef50d9e66d31b2019997c741b42274d53dde2e001b7a83e9511c339/uuid_utils-0.14.1-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:ccd65a4b8e83af23eae5e56d88034b2fe7264f465d3e830845f10d1591b81741", size = 309346, upload-time = "2026-02-20T22:50:31.857Z" }, + { url = "https://files.pythonhosted.org/packages/ef/ed/b6d6fd52a6636d7c3eddf97d68da50910bf17cd5ac221992506fb56cf12e/uuid_utils-0.14.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b56b0cacd81583834820588378e432b0696186683b813058b707aedc1e16c4b1", size = 344714, upload-time = "2026-02-20T22:50:42.642Z" }, + { url = "https://files.pythonhosted.org/packages/a8/a7/a19a1719fb626fe0b31882db36056d44fe904dc0cf15b06fdf56b2679cf7/uuid_utils-0.14.1-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bb3cf14de789097320a3c56bfdfdd51b1225d11d67298afbedee7e84e3837c96", size = 350914, upload-time = "2026-02-20T22:50:36.487Z" }, + { url = "https://files.pythonhosted.org/packages/1d/fc/f6690e667fdc3bb1a73f57951f97497771c56fe23e3d302d7404be394d4f/uuid_utils-0.14.1-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:60e0854a90d67f4b0cc6e54773deb8be618f4c9bad98d3326f081423b5d14fae", size = 482609, upload-time = "2026-02-20T22:50:37.511Z" }, + { url = "https://files.pythonhosted.org/packages/54/6e/dcd3fa031320921a12ec7b4672dea3bd1dd90ddffa363a91831ba834d559/uuid_utils-0.14.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce6743ba194de3910b5feb1a62590cd2587e33a73ab6af8a01b642ceb5055862", size = 345699, upload-time = "2026-02-20T22:50:46.87Z" }, + { url = "https://files.pythonhosted.org/packages/04/28/e5220204b58b44ac0047226a9d016a113fde039280cc8732d9e6da43b39f/uuid_utils-0.14.1-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:043fb58fde6cf1620a6c066382f04f87a8e74feb0f95a585e4ed46f5d44af57b", size = 372205, upload-time = "2026-02-20T22:50:28.438Z" }, + { url = "https://files.pythonhosted.org/packages/c7/d9/3d2eb98af94b8dfffc82b6a33b4dfc87b0a5de2c68a28f6dde0db1f8681b/uuid_utils-0.14.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c915d53f22945e55fe0d3d3b0b87fd965a57f5fd15666fd92d6593a73b1dd297", size = 521836, upload-time = "2026-02-20T22:50:23.057Z" }, + { url = "https://files.pythonhosted.org/packages/a8/15/0eb106cc6fe182f7577bc0ab6e2f0a40be247f35c5e297dbf7bbc460bd02/uuid_utils-0.14.1-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:0972488e3f9b449e83f006ead5a0e0a33ad4a13e4462e865b7c286ab7d7566a3", size = 625260, upload-time = "2026-02-20T22:50:25.949Z" }, + { url = "https://files.pythonhosted.org/packages/3c/17/f539507091334b109e7496830af2f093d9fc8082411eafd3ece58af1f8ba/uuid_utils-0.14.1-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:1c238812ae0c8ffe77d8d447a32c6dfd058ea4631246b08b5a71df586ff08531", size = 587824, upload-time = "2026-02-20T22:50:35.225Z" }, + { url = "https://files.pythonhosted.org/packages/2e/c2/d37a7b2e41f153519367d4db01f0526e0d4b06f1a4a87f1c5dfca5d70a8b/uuid_utils-0.14.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:bec8f8ef627af86abf8298e7ec50926627e29b34fa907fcfbedb45aaa72bca43", size = 551407, upload-time = "2026-02-20T22:50:44.915Z" }, + { url = "https://files.pythonhosted.org/packages/65/36/2d24b2cbe78547c6532da33fb8613debd3126eccc33a6374ab788f5e46e9/uuid_utils-0.14.1-cp39-abi3-win32.whl", hash = "sha256:b54d6aa6252d96bac1fdbc80d26ba71bad9f220b2724d692ad2f2310c22ef523", size = 183476, upload-time = "2026-02-20T22:50:32.745Z" }, + { url = "https://files.pythonhosted.org/packages/83/92/2d7e90df8b1a69ec4cff33243ce02b7a62f926ef9e2f0eca5a026889cd73/uuid_utils-0.14.1-cp39-abi3-win_amd64.whl", hash = "sha256:fc27638c2ce267a0ce3e06828aff786f91367f093c80625ee21dad0208e0f5ba", size = 187147, upload-time = "2026-02-20T22:50:45.807Z" }, + { url = "https://files.pythonhosted.org/packages/d9/26/529f4beee17e5248e37e0bc17a2761d34c0fa3b1e5729c88adb2065bae6e/uuid_utils-0.14.1-cp39-abi3-win_arm64.whl", hash = "sha256:b04cb49b42afbc4ff8dbc60cf054930afc479d6f4dd7f1ec3bbe5dbfdde06b7a", size = 188132, upload-time = "2026-02-20T22:50:41.718Z" }, + { url = "https://files.pythonhosted.org/packages/91/f9/6c64bdbf71f58ccde7919e00491812556f446a5291573af92c49a5e9aaef/uuid_utils-0.14.1-pp311-pypy311_pp73-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:b197cd5424cf89fb019ca7f53641d05bfe34b1879614bed111c9c313b5574cd8", size = 591617, upload-time = "2026-02-20T22:50:24.532Z" }, + { url = "https://files.pythonhosted.org/packages/d0/f0/758c3b0fb0c4871c7704fef26a5bc861de4f8a68e4831669883bebe07b0f/uuid_utils-0.14.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:12c65020ba6cb6abe1d57fcbfc2d0ea0506c67049ee031714057f5caf0f9bc9c", size = 303702, upload-time = "2026-02-20T22:50:40.687Z" }, + { url = "https://files.pythonhosted.org/packages/85/89/d91862b544c695cd58855efe3201f83894ed82fffe34500774238ab8eba7/uuid_utils-0.14.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b5d2ad28063d422ccc2c28d46471d47b61a58de885d35113a8f18cb547e25bf", size = 337678, upload-time = "2026-02-20T22:50:39.768Z" }, + { url = "https://files.pythonhosted.org/packages/ee/6b/cf342ba8a898f1de024be0243fac67c025cad530c79ea7f89c4ce718891a/uuid_utils-0.14.1-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:da2234387b45fde40b0fedfee64a0ba591caeea9c48c7698ab6e2d85c7991533", size = 343711, upload-time = "2026-02-20T22:50:43.965Z" }, + { url = "https://files.pythonhosted.org/packages/b3/20/049418d094d396dfa6606b30af925cc68a6670c3b9103b23e6990f84b589/uuid_utils-0.14.1-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:50fffc2827348c1e48972eed3d1c698959e63f9d030aa5dd82ba451113158a62", size = 476731, upload-time = "2026-02-20T22:50:30.589Z" }, + { url = "https://files.pythonhosted.org/packages/77/a1/0857f64d53a90321e6a46a3d4cc394f50e1366132dcd2ae147f9326ca98b/uuid_utils-0.14.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c1dbe718765f70f5b7f9b7f66b6a937802941b1cc56bcf642ce0274169741e01", size = 338902, upload-time = "2026-02-20T22:50:33.927Z" }, + { url = "https://files.pythonhosted.org/packages/ed/d0/5bf7cbf1ac138c92b9ac21066d18faf4d7e7f651047b700eb192ca4b9fdb/uuid_utils-0.14.1-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:258186964039a8e36db10810c1ece879d229b01331e09e9030bc5dcabe231bd2", size = 364700, upload-time = "2026-02-20T22:50:21.732Z" }, +] + +[[package]] +name = "uvicorn" +version = "0.52.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f2/0f/3f86e61397dd33bf2ccf28188c40db6a740658aeebbbf6e7dbc101a1f487/uvicorn-0.52.4.tar.gz", hash = "sha256:73acfee47a0b133c5de13d219492d62d8a31e935f4fe6e41a232451a15379f86", size = 100627, upload-time = "2026-08-19T06:27:41.821Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/79/4a20b54ab0491485ccd8c077db2d39187c7f12b3e15485d38a7be37c81b4/uvicorn-0.52.4-py3-none-any.whl", hash = "sha256:f86e41a149d7d05a9969337e3946a9c171c06a5d42680896daaba624aeac8da1", size = 79871, upload-time = "2026-08-19T06:27:40.36Z" }, +] + +[[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, upload-time = "2025-05-01T05:42:06.7Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/6e/3e955517e22cbdd565f2f8b2e73d52528b14b8bcfdb04f62466b071de847/validators-0.35.0-py3-none-any.whl", hash = "sha256:e8c947097eae7892cb3d26868d637f79f47b4a0554bc6b80065dfe5aac3705dd", size = 44712, upload-time = "2025-05-01T05:42:04.203Z" }, +] + +[[package]] +name = "vine" +version = "5.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/e4/d07b5f29d283596b9727dd5275ccbceb63c44a1a82aa9e4bfd20426762ac/vine-5.1.0.tar.gz", hash = "sha256:8b62e981d35c41049211cf62a0a1242d8c1ee9bd15bb196ce38aefd6799e61e0", size = 48980, upload-time = "2023-11-05T08:46:53.857Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/ff/7c0c86c43b3cbb927e0ccc0255cb4057ceba4799cd44ae95174ce8e8b5b2/vine-5.1.0-py3-none-any.whl", hash = "sha256:40fdf3c48b2cfe1c38a49e9ae2da6fda88e4794c810050a728bd7413811fb1dc", size = 9636, upload-time = "2023-11-05T08:46:51.205Z" }, +] + +[[package]] +name = "wasabi" +version = "1.1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/f9/054e6e2f1071e963b5e746b48d1e3727470b2a490834d18ad92364929db3/wasabi-1.1.3.tar.gz", hash = "sha256:4bb3008f003809db0c3e28b4daf20906ea871a2bb43f9914197d540f4f2e0878", size = 30391, upload-time = "2024-05-31T16:56:18.99Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/7c/34330a89da55610daa5f245ddce5aab81244321101614751e7537f125133/wasabi-1.1.3-py3-none-any.whl", hash = "sha256:f76e16e8f7e79f8c4c8be49b4024ac725713ab10cd7f19350ad18a8e3f71728c", size = 27880, upload-time = "2024-05-31T16:56:16.699Z" }, +] + +[[package]] +name = "watchfiles" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cd/41/5e1a4bb12aac5f1493fa1bdc11154eca3b258ca4eba65d39c473fe19d8e9/watchfiles-1.2.0.tar.gz", hash = "sha256:c995fba777f1ea992f090f9236e9284cf7a5d1a0130dd5a3d82c598cacd76838", size = 108252, upload-time = "2026-05-18T04:32:04.251Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/3d/8024c801df84d1587740d0359e7fdd80afeae3d159011f3d5376dd82f18e/watchfiles-1.2.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:704fd259e332e01f9b9c178f4bce9e49027e5587cc2600eeeaf8e76e1c846201", size = 400242, upload-time = "2026-05-18T04:31:19.014Z" }, + { url = "https://files.pythonhosted.org/packages/87/5b/f4dfd45323e949984a3a7f9dc31d1cbb049921e7d98253488dda72ccdaa9/watchfiles-1.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6543cf55d170003296d185c0af981f3e1311564907e1f4e08671fc7693a890a5", size = 394562, upload-time = "2026-05-18T04:30:08.46Z" }, + { url = "https://files.pythonhosted.org/packages/98/d8/19483ef075d601c409bce8bcbb5c0f81a10876fff870400568f08ce484a1/watchfiles-1.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89d8c2394a065ca86f5d2910ff263ae67c127e1376ccc4f9fc35c71db879f80a", size = 456611, upload-time = "2026-05-18T04:30:45.723Z" }, + { url = "https://files.pythonhosted.org/packages/b1/6a/cc81fbe7ee42f2f22e661a6e12def7807e01b14b2f39e0ff83fd373fd307/watchfiles-1.2.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:772b80df316480d894a0e3165fdd19cf77f5d17f9a787f94029465ad0e3529d1", size = 461379, upload-time = "2026-05-18T04:31:29.292Z" }, + { url = "https://files.pythonhosted.org/packages/b1/57/7e669002082c0a0f4fb5113bb70125f7110124b846b0a11bc5ae8e90eac1/watchfiles-1.2.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d158cd89df6053823533e06fb1d73c549133bff5f0396170c0e53d9559340717", size = 493556, upload-time = "2026-05-18T04:30:05.44Z" }, + { url = "https://files.pythonhosted.org/packages/45/7d/f60a2b19807b21fe8281f3a8da4f59eef0d5f96825ac4680ba2d4f2ebf91/watchfiles-1.2.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d516b3283a758e087841aedb8031549fb41ced08f3db10aa6d2bf32dc042525b", size = 575255, upload-time = "2026-05-18T04:30:40.568Z" }, + { url = "https://files.pythonhosted.org/packages/bd/49/77f5b5e6efbcd57482f74948ebb1b97e5c0046d6b61475042d830c84b3ff/watchfiles-1.2.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:53b2290c92e0506d102cd448fbc610d87079553f86caa39d67440856a8b8bba5", size = 467052, upload-time = "2026-05-18T04:31:17.942Z" }, + { url = "https://files.pythonhosted.org/packages/ee/5a/73e2959af1b97fd5d556f9a8bdba017be23ceeef731869d5eaa0a753d5a3/watchfiles-1.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a711b51aec4370d0dcda5b6c09463206f133a5759341d7744b953a7b62e1100e", size = 456858, upload-time = "2026-05-18T04:30:30.182Z" }, + { url = "https://files.pythonhosted.org/packages/50/57/1bc8c27fad7e6c19bddee15d276dbb6ab72480ec01c127afff1673aee417/watchfiles-1.2.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:e2ca07fa7d89195ec0865d3d285666286740bfa83d83e5cee204043a31ecc165", size = 467579, upload-time = "2026-05-18T04:32:15.897Z" }, + { url = "https://files.pythonhosted.org/packages/09/6c/3c2e44edba3553c5e3c3b8c8a2a6dee6b9e12ae2cf4bd2378bebf9dc3038/watchfiles-1.2.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e0618518f282c4ebff60f5e5b1247b6d91bb8b9f4476947563a1e74acc66f3c6", size = 633253, upload-time = "2026-05-18T04:31:37.123Z" }, + { url = "https://files.pythonhosted.org/packages/30/c2/d8c84a882ab39bbefcc4915ab3e91830b7a7e990c5570b0b69075aba3faf/watchfiles-1.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0d191c054d0715c3c95c99df9b8dbf6fd096d8c1e021e8f212e1bd8bc444ccb5", size = 660713, upload-time = "2026-05-18T04:31:24.62Z" }, + { url = "https://files.pythonhosted.org/packages/a9/07/f97736a5fc605364fe67b25e9fa4a6965dfd4840d50c406ada507e9d735f/watchfiles-1.2.0-cp311-cp311-win32.whl", hash = "sha256:9342472aff9b093c5acd4f6d8f70ae0937964ab56542502bcf5579782da69ae8", size = 277222, upload-time = "2026-05-18T04:31:21.131Z" }, + { url = "https://files.pythonhosted.org/packages/cf/99/2b04981977fc2608afd60360d928c6aecf6b950292ca221d98f4005f6694/watchfiles-1.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:dbd6c97045dad81227c8d040173da044c1de08de64a5ea8b555da4aee1d5fa22", size = 290274, upload-time = "2026-05-18T04:31:45.966Z" }, + { url = "https://files.pythonhosted.org/packages/3c/74/f7f58a7075ee9cf612b0cfcddb78b8cd8234f0742d6f0075cf0da2dde1c6/watchfiles-1.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:57a2d9fa4fb4c2ecae57b13dfff2c7ab53e21a2ba674fe9f05506680fcdcc0d7", size = 283460, upload-time = "2026-05-18T04:31:39.126Z" }, + { url = "https://files.pythonhosted.org/packages/b8/2f/e42c992d2afda3108ea1c02acecc991b9f31d05c14adc2a7cee9ee211fc4/watchfiles-1.2.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:bc13eb17538be00c874699dc0abe4ee2bc8d50bb1166a6b9e175ef3fd7eb8f26", size = 400115, upload-time = "2026-05-18T04:32:02.06Z" }, + { url = "https://files.pythonhosted.org/packages/5f/8f/6af2ea19065c91d8b0ea3516fdfc8c0d349f407e8e9fbf4e5a17360de8ad/watchfiles-1.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2d95ddc1eb6914154253d239089900813f6a767e174b8e6a50e7fdacb7e4236c", size = 393659, upload-time = "2026-05-18T04:30:50.951Z" }, + { url = "https://files.pythonhosted.org/packages/13/01/b32a967c56fb3e3e5be3db52c3d3b87fa4513aa367d8ed1ad96d42952e5f/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f70d8b291ef6e88d19b1f297a6905ddb978888d9272b0d05e6f53309856bcfc", size = 453207, upload-time = "2026-05-18T04:31:04.231Z" }, + { url = "https://files.pythonhosted.org/packages/04/98/97557a812180338cb1abd32e1cffcc4588f59b5f23e0cb006b2ba95ba64a/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:56d8641cf834c2836922899105bd3ce3d0dfc69291d52edf0b4d0436829b34c0", size = 459273, upload-time = "2026-05-18T04:31:50.377Z" }, + { url = "https://files.pythonhosted.org/packages/e8/a8/b4b08dcb7653b8087c6586f7ce649505900e866bbcfe40dc9587af02e686/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2581a94056e55d7d0a31a823ea92bf73749c489ca2285bfdc0fbe6b2bb49d50c", size = 489927, upload-time = "2026-05-18T04:31:42.485Z" }, + { url = "https://files.pythonhosted.org/packages/50/94/3dceea03545d2e5ddfd839f0ddd5e1cecbf1697b5a428d5ba11cef6af95d/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:41bc1199f7523b3f82843c88cbb979180c949caef0342cf90968f178e5d49b01", size = 570476, upload-time = "2026-05-18T04:31:03.071Z" }, + { url = "https://files.pythonhosted.org/packages/cc/f2/d39a5450c3532092b91f81d274360e613c2371bc874a89c7a1a3c5e8d138/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7571e4464cb6e434958f867f7f730b8ab0b75e3f8e5eac0499168486ab3c33a8", size = 465650, upload-time = "2026-05-18T04:30:12.701Z" }, + { url = "https://files.pythonhosted.org/packages/22/24/ed72f68cbc1333ca9b9f2200aa048bb6658ae41709bc1caad4310f4bdffd/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e53a384f76b631c3ae5334ce6a52f0baa3a911eb94a4eac7f160079868b716d5", size = 456398, upload-time = "2026-05-18T04:30:13.784Z" }, + { url = "https://files.pythonhosted.org/packages/0d/64/982ef4a4e5bab5b6e5b6becc8cd5e732f6130a78b855f0abec6439a9a135/watchfiles-1.2.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:d20029a60a71a052a24c4db7673bc4de39ab89adbaccbfb5d67987c5d73f424d", size = 465140, upload-time = "2026-05-18T04:31:52.111Z" }, + { url = "https://files.pythonhosted.org/packages/a0/0c/95282abf4ed680b6096010bcfc30c5fa7a041fc5aa5a2ad17a2cc6c75bba/watchfiles-1.2.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:2cb93af48550faf1cea04c303107c8b75833de7013e57ce27d3b8d21d8d0f58c", size = 630259, upload-time = "2026-05-18T04:31:25.676Z" }, + { url = "https://files.pythonhosted.org/packages/30/45/607c1de1530c4bdcf2cf1d1ecc2505ddba5d96bd43ba9f2b0e79876f850f/watchfiles-1.2.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2995c176de7692b86a2e4c58d9ec718f753150a979cb4a754e2b4ffa38e70906", size = 659859, upload-time = "2026-05-18T04:30:24.333Z" }, + { url = "https://files.pythonhosted.org/packages/fa/08/d9e2e0f9e8e6791d33aefc694ad7eefa7f901f63caff84a81ded38692f9c/watchfiles-1.2.0-cp312-cp312-win32.whl", hash = "sha256:7a2cffd17d27d2ecbb310c2b1d8174f222a5495b1a721894afa88ec11e25b898", size = 275480, upload-time = "2026-05-18T04:30:31.307Z" }, + { url = "https://files.pythonhosted.org/packages/1c/e6/9d42569c0102645cc8cea5d8c7d8a1e9d4ada2cb7f05f75e554b8aa2202a/watchfiles-1.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:f155b3a1b2a5fc89cdc70d47ee5d54e3b75e88efa34982028a35daef9ba00379", size = 288718, upload-time = "2026-05-18T04:32:10.745Z" }, + { url = "https://files.pythonhosted.org/packages/0a/26/88e0dc6ee3898169d7fa22bb6a69cabf2502d2ee25cb8c876d1262d204f8/watchfiles-1.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:8fa585ede612ee9f9e91b18bebf9ba11b9ae29a4e3a0d0cf6fca3e382133f0d5", size = 281026, upload-time = "2026-05-18T04:30:22.23Z" }, + { url = "https://files.pythonhosted.org/packages/d1/4d/70a7feced9f87e2ff26dba42667290f41694fc64646c67261fbb8cab5d5c/watchfiles-1.2.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:01ea8d66f0693b9b60a6541c8d10263091ca9a9060d242f3c1f3143f9aad2c98", size = 399730, upload-time = "2026-05-18T04:31:38.162Z" }, + { url = "https://files.pythonhosted.org/packages/31/3a/0da302f2307aee316922806ebd5726c542cbd787c938271cf14a074c7daf/watchfiles-1.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7ba0480b9a74af058f43b337e937a451e109295c420916d68ad24e3dc02f5e44", size = 392842, upload-time = "2026-05-18T04:30:27.051Z" }, + { url = "https://files.pythonhosted.org/packages/db/ef/d5bdb705c224dbc256aa0c1ec47bf4e61ec52558f2afb44a71a1fe4d7015/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f34e26a19f91f710c08e0183429f0d1d15df734e6bc78c31e77b9ea9c433658", size = 452989, upload-time = "2026-05-18T04:31:11.945Z" }, + { url = "https://files.pythonhosted.org/packages/71/29/5495f2c1661949ef7a35e4d71111d129cfe7606414a26887a919d0a55406/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b4e77f6a55f858504069abd35d336a637555c09bca453dde1ee1e5ada8a6a1fb", size = 458978, upload-time = "2026-05-18T04:30:52.606Z" }, + { url = "https://files.pythonhosted.org/packages/d5/8c/7f9c07c433811c2fffd93e13fdfb7135de9aab5f2ae41be08960fa0047dc/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0cb4d80e212f116474a545c21c912b445f16bb0cef9e6a73a498164223e14e2f", size = 490248, upload-time = "2026-05-18T04:31:36.003Z" }, + { url = "https://files.pythonhosted.org/packages/3c/11/d93632febc52fbc21be90231bb7c17fd5387f46c9076fd40a5f9c2ae6910/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b974946a10af379d425e2eef5b62f5c6ebeaccf91d45eaad6f5b27ecd4f91aa0", size = 571847, upload-time = "2026-05-18T04:31:10.862Z" }, + { url = "https://files.pythonhosted.org/packages/55/b4/383173e73aabb07ad1d9c7aa859d95437ac46a6d6a1e11005facda0c9d19/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:86bc13c25a8d1fcd70b51d0ce7c9b65e90de5666fcbfd3e34957cc73ee19aeb5", size = 465974, upload-time = "2026-05-18T04:30:17.006Z" }, + { url = "https://files.pythonhosted.org/packages/a7/6c/89b1a230a78f57c52dd8893adb1f92f94411721b6ec12596c56d98c74356/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca148d73dea36c9763aaa351e4d7a51780ec1584217c45276f4fe8239c768b71", size = 454782, upload-time = "2026-05-18T04:30:35.656Z" }, + { url = "https://files.pythonhosted.org/packages/24/62/1732118367cfff0a9fce3bf62ff4bfded09ef5df21d9d446b858b3f70a96/watchfiles-1.2.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:c525543d91961c6955b2636b308569e84a1d1c5f5f2932041ab9ef46422f43e3", size = 465182, upload-time = "2026-05-18T04:30:20.846Z" }, + { url = "https://files.pythonhosted.org/packages/28/96/716f7e5f51339bf22963f3345f9f27d7f3b30e2eadc597e257c881dd3c53/watchfiles-1.2.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:a204794696ffb8f9b10fba6f7cb5216d42f3b2b71860ccac6b6e42f5f10973b0", size = 629841, upload-time = "2026-05-18T04:31:05.397Z" }, + { url = "https://files.pythonhosted.org/packages/4c/fe/c40783950fd771ccf66ab3ec2722d188a9af1c7f96c6e811f36e40c6e03f/watchfiles-1.2.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:10d86db20695afe7997ac9e1717637d6714a8d0220458c33f3d2061f54cec427", size = 658028, upload-time = "2026-05-18T04:31:48.22Z" }, + { url = "https://files.pythonhosted.org/packages/71/72/4508db1856d1d87fcbb3b63f4839bab1b5682cb0e8d224d122263c09654a/watchfiles-1.2.0-cp313-cp313-win32.whl", hash = "sha256:eb283ee99e21ad6443c8cdb06ac5b34b1308c329cbdf03fa02b445363714c799", size = 275183, upload-time = "2026-05-18T04:30:59.57Z" }, + { url = "https://files.pythonhosted.org/packages/f9/36/14b76ca57652e5cc5fd1c11f32a261292c08a0d19a00351013c2549cbfb2/watchfiles-1.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:a0f27f01bee51861392bb6b7c4fdb290b27d1eb194e9e28788d68102a0e898d9", size = 288059, upload-time = "2026-05-18T04:32:07.937Z" }, + { url = "https://files.pythonhosted.org/packages/1b/8d/0a85e395398d8d20fadfe5c5d32c726eee17a519e78fb356f2cf7531bffe/watchfiles-1.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:3651aa7058595e9cfb75d35dd5ada2bf9f48a5b8a0f3562821d3e210c507e077", size = 280186, upload-time = "2026-05-18T04:31:54.484Z" }, + { url = "https://files.pythonhosted.org/packages/37/68/36db056f1fdcc5f07302f56e631774d6835bcd6fa3ace402304621d5f9e5/watchfiles-1.2.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:faea288b6f0ab1902ef08f4ca6de005dccf856c4e0c4f21b8c5fce02d90a1b08", size = 399031, upload-time = "2026-05-18T04:30:44.576Z" }, + { url = "https://files.pythonhosted.org/packages/c1/64/01a9d6f66a82a5c101ce939274106cc72759d62427e153f01edd2b9f87c2/watchfiles-1.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:01859b11fd9fbca670f4d5da00fbac282cfea9bd67a2125d8b2833a3b5617ea9", size = 391205, upload-time = "2026-05-18T04:30:25.413Z" }, + { url = "https://files.pythonhosted.org/packages/84/2c/0a44fe058cb4bb7b8ede6b6670698bbb7c0400740e378d00022189b7b31d/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fff610d7bb2256a317bb1e96f0d7862c7aa8076733ee5df0fd41bbe76a24a4f4", size = 451892, upload-time = "2026-05-18T04:32:14.005Z" }, + { url = "https://files.pythonhosted.org/packages/67/a1/351e0d56cd35e6488b5c8b4fb11a809a5bc923e8fe8fed9faf8920be0c89/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b141a4891c995a039cd89e9a49e62df1dc8a559a5d1a6e4c7106d16c12777a55", size = 458867, upload-time = "2026-05-18T04:31:22.279Z" }, + { url = "https://files.pythonhosted.org/packages/d5/7d/9d09605187f1b838998624049fcf8bf47b73c1a3b76901fcac1782f62277/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f22943b7770483f6ea0721c6b11d022947a98eb0acae14694de034f4d0d38925", size = 490217, upload-time = "2026-05-18T04:31:43.657Z" }, + { url = "https://files.pythonhosted.org/packages/60/5d/a17a16eccb182f04188cd308ec24b1a71a9b5c4e7098269cf35d9fa56d02/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1bc6195825b7dcd217968bb1f801a60fd4c16e8eeab5bedc7fe917d7d5995ab4", size = 571458, upload-time = "2026-05-18T04:32:11.875Z" }, + { url = "https://files.pythonhosted.org/packages/d3/3d/4dd457062083ab1938e5dfd45032eb425cee2ac817287ca8ff4356183e5d/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d4a4b147f5dca2a5d325a06a832fb43f345751adfbc63204aec30e0d9ca965a2", size = 464707, upload-time = "2026-05-18T04:30:43.492Z" }, + { url = "https://files.pythonhosted.org/packages/c6/71/ea8c57b128f5383de74d0c7d2d9c57ad7c9a65a930c451bd25d524b295b7/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4543579a9bdb0c9560039b4ffddbdb39545707659fbc430ce4c10f3f68d557f9", size = 454663, upload-time = "2026-05-18T04:30:16.061Z" }, + { url = "https://files.pythonhosted.org/packages/53/fd/2e812bf938406d7db351f0703ddd3fc6c061cf30d96153a77bc79a943a44/watchfiles-1.2.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:20aa0e708b920bde876a4aa82dc7dd6ebea228a63a67cda6632c2fc87b787efa", size = 463537, upload-time = "2026-05-18T04:31:44.9Z" }, + { url = "https://files.pythonhosted.org/packages/86/56/d17a7f1dd1bc3035f1072694a551301272f1739c2d8e319c927cb9e29b38/watchfiles-1.2.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:d413349d565dab74297f2a63e84a097936be69bf8f3b3801f27f380e32040f44", size = 629194, upload-time = "2026-05-18T04:31:14.141Z" }, + { url = "https://files.pythonhosted.org/packages/be/06/f1ff66bf5cae50aa4062779a0ecd0bbaf15e466195719074078947d9a17d/watchfiles-1.2.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:f28b2725eb8cce327b9b3ab02415c853011dc55c95832fe90de6bc56f5315f72", size = 656194, upload-time = "2026-05-18T04:31:47.14Z" }, + { url = "https://files.pythonhosted.org/packages/e7/54/a9c7ea9a82a4ac65e7004c0a03920b5cdd2f9c3b678757d9cd425aa51d53/watchfiles-1.2.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:b8c8358484d5fa12ef34f05b7f4168eaf1932f408725ff6d023c33ec17bd79d4", size = 400205, upload-time = "2026-05-18T04:32:05.153Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5d/c9ab3534374a4a67450696905d6ef16a04405448b8dc52bd752ae50423d4/watchfiles-1.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f04b092229ad2c50126dd3c922c8822e51e605993764a33058d4a791ab42281", size = 392508, upload-time = "2026-05-18T04:30:54.849Z" }, + { url = "https://files.pythonhosted.org/packages/26/ca/1ad30103535cf0cecd7b993e8d50edc5351b1820e38f2d22e3df58962feb/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7a7ce236284f002a156f70add88efe5c70879cccbb658be0822c54b1306fc09d", size = 452448, upload-time = "2026-05-18T04:30:53.727Z" }, + { url = "https://files.pythonhosted.org/packages/37/a1/ceee2cdf2afbd715fa07758d39c9859513eae411b23196f7fd039e5feedd/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b9909cc2b48468b575eefa944919e1fe8a36c5849d5c7c168f80a8c1db69398e", size = 459605, upload-time = "2026-05-18T04:30:23.312Z" }, + { url = "https://files.pythonhosted.org/packages/e8/f6/421e30fd1cb3907a84ed92ab3f1983e37ba2dca015e9a894a048418417a2/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a37faaed405c67e28e6be45a1fa4f206ef5a2860f27c237db9fa30704c38242", size = 490757, upload-time = "2026-05-18T04:30:47.358Z" }, + { url = "https://files.pythonhosted.org/packages/41/b0/55ed1b97ed08be7bba6f9a541cac15f2a858e1d74d2b07b6da70a82aab00/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9649193aa27bd9ff2e80ff29bfaa93085496c7a3a377592823cc58b77ee88add", size = 568672, upload-time = "2026-05-18T04:30:38.915Z" }, + { url = "https://files.pythonhosted.org/packages/d1/cf/d8ae8a80dd7bafab395ea7681c10237311bbf34d37704a8c744e7cf31fc7/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e4ff8e37f99cf1da89e255e07c9c4b37c214038c4283707bdec308cb1b0ea1f", size = 464197, upload-time = "2026-05-18T04:30:09.914Z" }, + { url = "https://files.pythonhosted.org/packages/7c/8a/3076c496ca8dafe0e8cd03fcebdfc47be4b1174b4e5b24ff6e396e6b3af2/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:054dc20fd2e3132b4c3883b4a00d72fd6e1f56fdaf89fccd12e8057d74cd74d7", size = 453181, upload-time = "2026-05-18T04:30:14.829Z" }, + { url = "https://files.pythonhosted.org/packages/e5/10/9745e17c98e7b8a86454df0a3c7b5686bd650383f1e9f26e4ebcbd6cc0c0/watchfiles-1.2.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:e140ed30ebde76796b686e67c182cff10ea2fbab186fafd1560f74bb5a473a6e", size = 465109, upload-time = "2026-05-18T04:30:28.123Z" }, + { url = "https://files.pythonhosted.org/packages/8f/95/8ef4a95481d3e0cb52d62a06fa6e972e81424be2d9698b91a2fecca9904c/watchfiles-1.2.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:bb7e52ecf68ba46d22df23467b87cffeb2146908aa523ebfe803019618cfda06", size = 630653, upload-time = "2026-05-18T04:31:49.304Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e4/3b3bf36b0f829b50c6ebcb8d031583863c59f923d6a6af3d485e470d0fac/watchfiles-1.2.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:23282a321c8baf9b3a3c4afff673f9fe65eb7fdc2338d765ccad9d3d1916a5ba", size = 657838, upload-time = "2026-05-18T04:31:06.497Z" }, + { url = "https://files.pythonhosted.org/packages/21/b1/6cbbb50c1f3002ab568777d44aa21206dfb8807a840990c4037523b51812/watchfiles-1.2.0-cp314-cp314-win32.whl", hash = "sha256:c0db965c5f79aa49fe672d297cf1febc5ad149b658594944f49a54a2b96270a7", size = 275108, upload-time = "2026-05-18T04:30:06.891Z" }, + { url = "https://files.pythonhosted.org/packages/92/45/190ce6db8dcb4536682cf75d3889ff1a27182a58cb519d343cb6d9ea63d8/watchfiles-1.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:71283b39fd17e5408eb123bd37aeecfd9d54c81fc184421943208aadb879d103", size = 288441, upload-time = "2026-05-18T04:32:12.901Z" }, + { url = "https://files.pythonhosted.org/packages/74/0d/3eae1c2313ab08378431d907c3f8095ecca00f3eda33111cf4f0f2591799/watchfiles-1.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:c5c19526f4e54a00f2666a6c0e9e40d582c09e865055ea7378bf0009aab857b3", size = 280684, upload-time = "2026-05-18T04:31:26.902Z" }, + { url = "https://files.pythonhosted.org/packages/b1/75/fb64e6c25d6b5ca636d03df34ffb1c6e9873303e76d27967e045f8df088f/watchfiles-1.2.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:d73a585accffa5ae39c17264c36ec3166d2fad7000c780f5ef83b2722afb9dd2", size = 398857, upload-time = "2026-05-18T04:32:17.108Z" }, + { url = "https://files.pythonhosted.org/packages/73/4e/9f7adf01754cbf81843722ccfec169d8f26c69778281a302855cecd2ee08/watchfiles-1.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ae99b14c5f21e026e0e9d96f40e07d8570ebee6cafd9d8fc318354606daa7a28", size = 392413, upload-time = "2026-05-18T04:31:07.911Z" }, + { url = "https://files.pythonhosted.org/packages/47/c8/bec626bcc2d69f44b9acb24ce7d60ed7b16b73628eea747fcbd169d8edda/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4429f3b105524a10b72c3a819b091c495d2811d419c1e1e8df773a5a5974f831", size = 452409, upload-time = "2026-05-18T04:31:20.142Z" }, + { url = "https://files.pythonhosted.org/packages/00/b7/b6362068e81e7c556d155a34c35d40ac3ef42d747b06d7f6e5bf58e359c2/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:43d818978d06062d9b22c4fab2ebe44cf5213d42dc8e62bda8c2760cfa2eeb33", size = 458827, upload-time = "2026-05-18T04:32:06.219Z" }, + { url = "https://files.pythonhosted.org/packages/67/f8/9a813fa42afb1e0b4625e75f0479826644d3ee8dc287e093799bc01f390c/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b9f732dc58b2dbe69e464ccf8fff7a03b0dd0be439da4c0720d3558527d3d6b4", size = 490104, upload-time = "2026-05-18T04:31:56.034Z" }, + { url = "https://files.pythonhosted.org/packages/2f/bf/27dfb6094ca4c9aad21298b5525b6c53cb36121ee454331d05161e58d130/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8f200104103feb097de4cab8fe4f5dd18a2026934c7dea98c55a2f5fd6d5a33b", size = 571360, upload-time = "2026-05-18T04:31:57.133Z" }, + { url = "https://files.pythonhosted.org/packages/fb/39/44a096d67270ea93df91d33877dbe91fbda3aa4f8ec2edf799d93eda8736/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:63ac26eefbf4af1741247d6fb68b11c49a25b2f7413fbd318a83a12aaa9cf666", size = 464644, upload-time = "2026-05-18T04:30:57.33Z" }, + { url = "https://files.pythonhosted.org/packages/0e/80/c7472203bad6268e3ef1ad260739704847898938ad7ea8b63a5131f46b50/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c4997d4e4a55f0d02b6cde327322daf3a0400e5df6c6b15948994bf72497925", size = 454771, upload-time = "2026-05-18T04:30:48.736Z" }, + { url = "https://files.pythonhosted.org/packages/51/cf/3b10b268b4b7f0fc26e9debb5eef1998b515887840f444cd3ec80c688755/watchfiles-1.2.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:4c887eba18b7945ac73067a8b4a66f21cd46c2539b2bc68588f7be6c7eb6d26b", size = 463494, upload-time = "2026-05-18T04:31:33.826Z" }, + { url = "https://files.pythonhosted.org/packages/3d/3e/a4302545cd589262a0dc7d140e86f7688eba3f9c72776c27f7e23b8864c4/watchfiles-1.2.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:3416ff151bb6b5a8d8d11664974fbef4d9305b9b2957839ab5a270468fd8df30", size = 629383, upload-time = "2026-05-18T04:31:15.596Z" }, + { url = "https://files.pythonhosted.org/packages/db/99/d5649df0a9a410d45b7c882304d0b790903ac9b6e8f2cfd12114e0c6b9f2/watchfiles-1.2.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:0e831a271c035d89789cffc386b6aa1375f39f1cd25eb7ca0997e4970d152fc5", size = 656093, upload-time = "2026-05-18T04:31:58.707Z" }, + { url = "https://files.pythonhosted.org/packages/92/b9/362702539275019a54dd2e94511b31a9b89c5f9e6a21966de7eb692549fc/watchfiles-1.2.0-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:37a6721cdf3f65dbb13aa9503510ccb4451603ac837e44d265d7992a597e1374", size = 400109, upload-time = "2026-05-18T04:31:16.879Z" }, + { url = "https://files.pythonhosted.org/packages/8f/75/71d5ba62db781e5587bded1d944c675374bc4aa37ff33d5018d98e8b6538/watchfiles-1.2.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2b37d10b5a63bd4d87e18472d80fa525bd670586fae62e5dd580452764879b65", size = 392167, upload-time = "2026-05-18T04:31:28.058Z" }, + { url = "https://files.pythonhosted.org/packages/3c/01/c66dd95d0423fe30d31820e2d1d5bda773764131bbb6ac0cb1cf303ac328/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a105bc2283f67e8fbec74253ec2d94925de92ed72c0393f1206bf326b7b7b69", size = 452372, upload-time = "2026-05-18T04:31:00.836Z" }, + { url = "https://files.pythonhosted.org/packages/91/15/2fe99557e72f85627c6a8eed50d889e8d101623e060a22ad75b875cb932d/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5327989a465505f05cfe06f04fa9d0c2fd5432bb243e10e6f012b1bdca3c8579", size = 459596, upload-time = "2026-05-18T04:31:34.96Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/d4acfa0023367428ed48351b3b9b267893037b6cadae55620c61c24bcfd4/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ecb47f183a8025b2aa18b546725c3657e542112ae9c0613a2af79b4fa8d04ad7", size = 490869, upload-time = "2026-05-18T04:31:59.923Z" }, + { url = "https://files.pythonhosted.org/packages/a4/5f/3164cbdce06c9fb95c4f7b9e2f9760b5e2797af43a9ecc317ef42a23a278/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8520a4ab0e37f770afc34459c4f8f7019e153f9124dc101c15538365875d1ab2", size = 571641, upload-time = "2026-05-18T04:32:00.948Z" }, + { url = "https://files.pythonhosted.org/packages/41/e6/85d3731c55e65cd7690f3f803d24c139588aaf863e4bf2148fe7a7fa1a19/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:71cd71740ed2c15211ebb237ced4e39a1cdf6f80566e5fe95428da1626f4fde6", size = 464444, upload-time = "2026-05-18T04:30:34.298Z" }, + { url = "https://files.pythonhosted.org/packages/f4/7d/562641012b8b09872742c3b8adf9629ec479fd78f8d68ae4a0c13da8add6/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f88af53d6ddaf72179ef613ddc905e6f4785f712b49b80b3bef9f3525e6194b4", size = 453593, upload-time = "2026-05-18T04:31:23.464Z" }, + { url = "https://files.pythonhosted.org/packages/56/fe/cb8ef3d6f929d14158fdaaad9925985b7310abc9384dcd4d82dd0016fb59/watchfiles-1.2.0-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:cee9d5efd929efdac5f7e58f72b3376f676b64050a91c5b99a7094c5b2317488", size = 465096, upload-time = "2026-05-18T04:31:30.384Z" }, + { url = "https://files.pythonhosted.org/packages/25/91/80908e835e100527a9267147b08c0eee1fa6ab0ffec15edc04d1d44885f7/watchfiles-1.2.0-cp315-cp315-musllinux_1_1_aarch64.whl", hash = "sha256:b718bf356bbc15e559bd8ef41782b573b8ae0e3f177ab244b440568d7ea02cfb", size = 630638, upload-time = "2026-05-18T04:30:49.89Z" }, + { url = "https://files.pythonhosted.org/packages/46/4b/95ab2f256bb4af3cb2eb23b9317bda984ee6e0f11733a5c004a6c95b06e3/watchfiles-1.2.0-cp315-cp315-musllinux_1_1_x86_64.whl", hash = "sha256:922c0e019fe68b3ae392965a766b02a71ba1168c932cebc3733cd52c5fe5b377", size = 657684, upload-time = "2026-05-18T04:31:32.027Z" }, + { url = "https://files.pythonhosted.org/packages/23/f4/7513ef1e85fc4c6331b59479d6d72661fc391fbe543678052ac72c8b6c19/watchfiles-1.2.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:4674d49eb94706dfe666c069fc0a1b646ffcf920473492e209f6d5f60d3f0cc2", size = 403050, upload-time = "2026-05-18T04:30:36.753Z" }, + { url = "https://files.pythonhosted.org/packages/27/0b/a54103cfd732bb703c7a749222011a0483ef3705948dae3b203158601119/watchfiles-1.2.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:094b9b70103d4e963499bdea001ee3c2697b144cd9ae6218a62c0f89ec9e31db", size = 396629, upload-time = "2026-05-18T04:32:03.268Z" }, + { url = "https://files.pythonhosted.org/packages/5e/2c/73f31a3b893886206c3f54d73e8ad8dee58cdb2f69ad2622e0a8a9e07f4e/watchfiles-1.2.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b0ef001f8c25ad0fa9529f914c1600647ecd0f542d11c19b7894768c67b6acb7", size = 457318, upload-time = "2026-05-18T04:31:01.932Z" }, + { url = "https://files.pythonhosted.org/packages/e9/f9/45d021e4a5cc7b9dd567f7cbb06d3b75f751a690063fb6cc7ec60f4e46b7/watchfiles-1.2.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a88fc94e647bc4eec523f1caa540258eb71d14278b9daf72fa1e2658a98df0f0", size = 457771, upload-time = "2026-05-18T04:30:56.331Z" }, +] + +[[package]] +name = "wcwidth" +version = "0.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/35/a2/8e3becb46433538a38726c948d3399905a4c7cabd0df578ede5dc51f0ec2/wcwidth-0.6.0.tar.gz", hash = "sha256:cdc4e4262d6ef9a1a57e018384cbeb1208d8abbc64176027e2c2455c81313159", size = 159684, upload-time = "2026-02-06T19:19:40.919Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/5a/199c59e0a824a3db2b89c5d2dade7ab5f9624dbf6448dc291b46d5ec94d3/wcwidth-0.6.0-py3-none-any.whl", hash = "sha256:1a3a1e510b553315f8e146c54764f4fb6264ffad731b3d78088cdb1478ffbdad", size = 94189, upload-time = "2026-02-06T19:19:39.646Z" }, +] + +[[package]] +name = "weasel" +version = "0.4.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cloudpathlib" }, + { name = "confection" }, + { name = "packaging" }, + { name = "pydantic" }, + { name = "requests" }, + { name = "smart-open" }, + { name = "srsly" }, + { name = "typer-slim" }, + { name = "wasabi" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/09/d7/edd9c24e60cf8e5de130aa2e8af3b01521f4d0216c371d01212f580d0d8e/weasel-0.4.3.tar.gz", hash = "sha256:f293d6174398e8f478c78481e00c503ee4b82ea7a3e6d0d6a01e46a6b1396845", size = 38733, upload-time = "2025-11-13T23:52:28.193Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/74/a148b41572656904a39dfcfed3f84dd1066014eed94e209223ae8e9d088d/weasel-0.4.3-py3-none-any.whl", hash = "sha256:08f65b5d0dbded4879e08a64882de9b9514753d9eaa4c4e2a576e33666ac12cf", size = 50757, upload-time = "2025-11-13T23:52:26.982Z" }, +] + +[[package]] +name = "websockets" +version = "16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/04/24/4b2031d72e840ce4c1ccb255f693b15c334757fc50023e4db9537080b8c4/websockets-16.0.tar.gz", hash = "sha256:5f6261a5e56e8d5c42a4497b364ea24d94d9563e8fbd44e78ac40879c60179b5", size = 179346, upload-time = "2026-01-10T09:23:47.181Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f2/db/de907251b4ff46ae804ad0409809504153b3f30984daf82a1d84a9875830/websockets-16.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:31a52addea25187bde0797a97d6fc3d2f92b6f72a9370792d65a6e84615ac8a8", size = 177340, upload-time = "2026-01-10T09:22:34.539Z" }, + { url = "https://files.pythonhosted.org/packages/f3/fa/abe89019d8d8815c8781e90d697dec52523fb8ebe308bf11664e8de1877e/websockets-16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:417b28978cdccab24f46400586d128366313e8a96312e4b9362a4af504f3bbad", size = 175022, upload-time = "2026-01-10T09:22:36.332Z" }, + { url = "https://files.pythonhosted.org/packages/58/5d/88ea17ed1ded2079358b40d31d48abe90a73c9e5819dbcde1606e991e2ad/websockets-16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:af80d74d4edfa3cb9ed973a0a5ba2b2a549371f8a741e0800cb07becdd20f23d", size = 175319, upload-time = "2026-01-10T09:22:37.602Z" }, + { url = "https://files.pythonhosted.org/packages/d2/ae/0ee92b33087a33632f37a635e11e1d99d429d3d323329675a6022312aac2/websockets-16.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:08d7af67b64d29823fed316505a89b86705f2b7981c07848fb5e3ea3020c1abe", size = 184631, upload-time = "2026-01-10T09:22:38.789Z" }, + { url = "https://files.pythonhosted.org/packages/c8/c5/27178df583b6c5b31b29f526ba2da5e2f864ecc79c99dae630a85d68c304/websockets-16.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7be95cfb0a4dae143eaed2bcba8ac23f4892d8971311f1b06f3c6b78952ee70b", size = 185870, upload-time = "2026-01-10T09:22:39.893Z" }, + { url = "https://files.pythonhosted.org/packages/87/05/536652aa84ddc1c018dbb7e2c4cbcd0db884580bf8e95aece7593fde526f/websockets-16.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d6297ce39ce5c2e6feb13c1a996a2ded3b6832155fcfc920265c76f24c7cceb5", size = 185361, upload-time = "2026-01-10T09:22:41.016Z" }, + { url = "https://files.pythonhosted.org/packages/6d/e2/d5332c90da12b1e01f06fb1b85c50cfc489783076547415bf9f0a659ec19/websockets-16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1c1b30e4f497b0b354057f3467f56244c603a79c0d1dafce1d16c283c25f6e64", size = 184615, upload-time = "2026-01-10T09:22:42.442Z" }, + { url = "https://files.pythonhosted.org/packages/77/fb/d3f9576691cae9253b51555f841bc6600bf0a983a461c79500ace5a5b364/websockets-16.0-cp311-cp311-win32.whl", hash = "sha256:5f451484aeb5cafee1ccf789b1b66f535409d038c56966d6101740c1614b86c6", size = 178246, upload-time = "2026-01-10T09:22:43.654Z" }, + { url = "https://files.pythonhosted.org/packages/54/67/eaff76b3dbaf18dcddabc3b8c1dba50b483761cccff67793897945b37408/websockets-16.0-cp311-cp311-win_amd64.whl", hash = "sha256:8d7f0659570eefb578dacde98e24fb60af35350193e4f56e11190787bee77dac", size = 178684, upload-time = "2026-01-10T09:22:44.941Z" }, + { url = "https://files.pythonhosted.org/packages/84/7b/bac442e6b96c9d25092695578dda82403c77936104b5682307bd4deb1ad4/websockets-16.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:71c989cbf3254fbd5e84d3bff31e4da39c43f884e64f2551d14bb3c186230f00", size = 177365, upload-time = "2026-01-10T09:22:46.787Z" }, + { url = "https://files.pythonhosted.org/packages/b0/fe/136ccece61bd690d9c1f715baaeefd953bb2360134de73519d5df19d29ca/websockets-16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8b6e209ffee39ff1b6d0fa7bfef6de950c60dfb91b8fcead17da4ee539121a79", size = 175038, upload-time = "2026-01-10T09:22:47.999Z" }, + { url = "https://files.pythonhosted.org/packages/40/1e/9771421ac2286eaab95b8575b0cb701ae3663abf8b5e1f64f1fd90d0a673/websockets-16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:86890e837d61574c92a97496d590968b23c2ef0aeb8a9bc9421d174cd378ae39", size = 175328, upload-time = "2026-01-10T09:22:49.809Z" }, + { url = "https://files.pythonhosted.org/packages/18/29/71729b4671f21e1eaa5d6573031ab810ad2936c8175f03f97f3ff164c802/websockets-16.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9b5aca38b67492ef518a8ab76851862488a478602229112c4b0d58d63a7a4d5c", size = 184915, upload-time = "2026-01-10T09:22:51.071Z" }, + { url = "https://files.pythonhosted.org/packages/97/bb/21c36b7dbbafc85d2d480cd65df02a1dc93bf76d97147605a8e27ff9409d/websockets-16.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e0334872c0a37b606418ac52f6ab9cfd17317ac26365f7f65e203e2d0d0d359f", size = 186152, upload-time = "2026-01-10T09:22:52.224Z" }, + { url = "https://files.pythonhosted.org/packages/4a/34/9bf8df0c0cf88fa7bfe36678dc7b02970c9a7d5e065a3099292db87b1be2/websockets-16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a0b31e0b424cc6b5a04b8838bbaec1688834b2383256688cf47eb97412531da1", size = 185583, upload-time = "2026-01-10T09:22:53.443Z" }, + { url = "https://files.pythonhosted.org/packages/47/88/4dd516068e1a3d6ab3c7c183288404cd424a9a02d585efbac226cb61ff2d/websockets-16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:485c49116d0af10ac698623c513c1cc01c9446c058a4e61e3bf6c19dff7335a2", size = 184880, upload-time = "2026-01-10T09:22:55.033Z" }, + { url = "https://files.pythonhosted.org/packages/91/d6/7d4553ad4bf1c0421e1ebd4b18de5d9098383b5caa1d937b63df8d04b565/websockets-16.0-cp312-cp312-win32.whl", hash = "sha256:eaded469f5e5b7294e2bdca0ab06becb6756ea86894a47806456089298813c89", size = 178261, upload-time = "2026-01-10T09:22:56.251Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f0/f3a17365441ed1c27f850a80b2bc680a0fa9505d733fe152fdf5e98c1c0b/websockets-16.0-cp312-cp312-win_amd64.whl", hash = "sha256:5569417dc80977fc8c2d43a86f78e0a5a22fee17565d78621b6bb264a115d4ea", size = 178693, upload-time = "2026-01-10T09:22:57.478Z" }, + { url = "https://files.pythonhosted.org/packages/cc/9c/baa8456050d1c1b08dd0ec7346026668cbc6f145ab4e314d707bb845bf0d/websockets-16.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:878b336ac47938b474c8f982ac2f7266a540adc3fa4ad74ae96fea9823a02cc9", size = 177364, upload-time = "2026-01-10T09:22:59.333Z" }, + { url = "https://files.pythonhosted.org/packages/7e/0c/8811fc53e9bcff68fe7de2bcbe75116a8d959ac699a3200f4847a8925210/websockets-16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:52a0fec0e6c8d9a784c2c78276a48a2bdf099e4ccc2a4cad53b27718dbfd0230", size = 175039, upload-time = "2026-01-10T09:23:01.171Z" }, + { url = "https://files.pythonhosted.org/packages/aa/82/39a5f910cb99ec0b59e482971238c845af9220d3ab9fa76dd9162cda9d62/websockets-16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e6578ed5b6981005df1860a56e3617f14a6c307e6a71b4fff8c48fdc50f3ed2c", size = 175323, upload-time = "2026-01-10T09:23:02.341Z" }, + { url = "https://files.pythonhosted.org/packages/bd/28/0a25ee5342eb5d5f297d992a77e56892ecb65e7854c7898fb7d35e9b33bd/websockets-16.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:95724e638f0f9c350bb1c2b0a7ad0e83d9cc0c9259f3ea94e40d7b02a2179ae5", size = 184975, upload-time = "2026-01-10T09:23:03.756Z" }, + { url = "https://files.pythonhosted.org/packages/f9/66/27ea52741752f5107c2e41fda05e8395a682a1e11c4e592a809a90c6a506/websockets-16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0204dc62a89dc9d50d682412c10b3542d748260d743500a85c13cd1ee4bde82", size = 186203, upload-time = "2026-01-10T09:23:05.01Z" }, + { url = "https://files.pythonhosted.org/packages/37/e5/8e32857371406a757816a2b471939d51c463509be73fa538216ea52b792a/websockets-16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:52ac480f44d32970d66763115edea932f1c5b1312de36df06d6b219f6741eed8", size = 185653, upload-time = "2026-01-10T09:23:06.301Z" }, + { url = "https://files.pythonhosted.org/packages/9b/67/f926bac29882894669368dc73f4da900fcdf47955d0a0185d60103df5737/websockets-16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6e5a82b677f8f6f59e8dfc34ec06ca6b5b48bc4fcda346acd093694cc2c24d8f", size = 184920, upload-time = "2026-01-10T09:23:07.492Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a1/3d6ccdcd125b0a42a311bcd15a7f705d688f73b2a22d8cf1c0875d35d34a/websockets-16.0-cp313-cp313-win32.whl", hash = "sha256:abf050a199613f64c886ea10f38b47770a65154dc37181bfaff70c160f45315a", size = 178255, upload-time = "2026-01-10T09:23:09.245Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ae/90366304d7c2ce80f9b826096a9e9048b4bb760e44d3b873bb272cba696b/websockets-16.0-cp313-cp313-win_amd64.whl", hash = "sha256:3425ac5cf448801335d6fdc7ae1eb22072055417a96cc6b31b3861f455fbc156", size = 178689, upload-time = "2026-01-10T09:23:10.483Z" }, + { url = "https://files.pythonhosted.org/packages/f3/1d/e88022630271f5bd349ed82417136281931e558d628dd52c4d8621b4a0b2/websockets-16.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8cc451a50f2aee53042ac52d2d053d08bf89bcb31ae799cb4487587661c038a0", size = 177406, upload-time = "2026-01-10T09:23:12.178Z" }, + { url = "https://files.pythonhosted.org/packages/f2/78/e63be1bf0724eeb4616efb1ae1c9044f7c3953b7957799abb5915bffd38e/websockets-16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:daa3b6ff70a9241cf6c7fc9e949d41232d9d7d26fd3522b1ad2b4d62487e9904", size = 175085, upload-time = "2026-01-10T09:23:13.511Z" }, + { url = "https://files.pythonhosted.org/packages/bb/f4/d3c9220d818ee955ae390cf319a7c7a467beceb24f05ee7aaaa2414345ba/websockets-16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fd3cb4adb94a2a6e2b7c0d8d05cb94e6f1c81a0cf9dc2694fb65c7e8d94c42e4", size = 175328, upload-time = "2026-01-10T09:23:14.727Z" }, + { url = "https://files.pythonhosted.org/packages/63/bc/d3e208028de777087e6fb2b122051a6ff7bbcca0d6df9d9c2bf1dd869ae9/websockets-16.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:781caf5e8eee67f663126490c2f96f40906594cb86b408a703630f95550a8c3e", size = 185044, upload-time = "2026-01-10T09:23:15.939Z" }, + { url = "https://files.pythonhosted.org/packages/ad/6e/9a0927ac24bd33a0a9af834d89e0abc7cfd8e13bed17a86407a66773cc0e/websockets-16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:caab51a72c51973ca21fa8a18bd8165e1a0183f1ac7066a182ff27107b71e1a4", size = 186279, upload-time = "2026-01-10T09:23:17.148Z" }, + { url = "https://files.pythonhosted.org/packages/b9/ca/bf1c68440d7a868180e11be653c85959502efd3a709323230314fda6e0b3/websockets-16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19c4dc84098e523fd63711e563077d39e90ec6702aff4b5d9e344a60cb3c0cb1", size = 185711, upload-time = "2026-01-10T09:23:18.372Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f8/fdc34643a989561f217bb477cbc47a3a07212cbda91c0e4389c43c296ebf/websockets-16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a5e18a238a2b2249c9a9235466b90e96ae4795672598a58772dd806edc7ac6d3", size = 184982, upload-time = "2026-01-10T09:23:19.652Z" }, + { url = "https://files.pythonhosted.org/packages/dd/d1/574fa27e233764dbac9c52730d63fcf2823b16f0856b3329fc6268d6ae4f/websockets-16.0-cp314-cp314-win32.whl", hash = "sha256:a069d734c4a043182729edd3e9f247c3b2a4035415a9172fd0f1b71658a320a8", size = 177915, upload-time = "2026-01-10T09:23:21.458Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f1/ae6b937bf3126b5134ce1f482365fde31a357c784ac51852978768b5eff4/websockets-16.0-cp314-cp314-win_amd64.whl", hash = "sha256:c0ee0e63f23914732c6d7e0cce24915c48f3f1512ec1d079ed01fc629dab269d", size = 178381, upload-time = "2026-01-10T09:23:22.715Z" }, + { url = "https://files.pythonhosted.org/packages/06/9b/f791d1db48403e1f0a27577a6beb37afae94254a8c6f08be4a23e4930bc0/websockets-16.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:a35539cacc3febb22b8f4d4a99cc79b104226a756aa7400adc722e83b0d03244", size = 177737, upload-time = "2026-01-10T09:23:24.523Z" }, + { url = "https://files.pythonhosted.org/packages/bd/40/53ad02341fa33b3ce489023f635367a4ac98b73570102ad2cdd770dacc9a/websockets-16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b784ca5de850f4ce93ec85d3269d24d4c82f22b7212023c974c401d4980ebc5e", size = 175268, upload-time = "2026-01-10T09:23:25.781Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/6158d4e459b984f949dcbbb0c5d270154c7618e11c01029b9bbd1bb4c4f9/websockets-16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:569d01a4e7fba956c5ae4fc988f0d4e187900f5497ce46339c996dbf24f17641", size = 175486, upload-time = "2026-01-10T09:23:27.033Z" }, + { url = "https://files.pythonhosted.org/packages/e5/2d/7583b30208b639c8090206f95073646c2c9ffd66f44df967981a64f849ad/websockets-16.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:50f23cdd8343b984957e4077839841146f67a3d31ab0d00e6b824e74c5b2f6e8", size = 185331, upload-time = "2026-01-10T09:23:28.259Z" }, + { url = "https://files.pythonhosted.org/packages/45/b0/cce3784eb519b7b5ad680d14b9673a31ab8dcb7aad8b64d81709d2430aa8/websockets-16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:152284a83a00c59b759697b7f9e9cddf4e3c7861dd0d964b472b70f78f89e80e", size = 186501, upload-time = "2026-01-10T09:23:29.449Z" }, + { url = "https://files.pythonhosted.org/packages/19/60/b8ebe4c7e89fb5f6cdf080623c9d92789a53636950f7abacfc33fe2b3135/websockets-16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bc59589ab64b0022385f429b94697348a6a234e8ce22544e3681b2e9331b5944", size = 186062, upload-time = "2026-01-10T09:23:31.368Z" }, + { url = "https://files.pythonhosted.org/packages/88/a8/a080593f89b0138b6cba1b28f8df5673b5506f72879322288b031337c0b8/websockets-16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32da954ffa2814258030e5a57bc73a3635463238e797c7375dc8091327434206", size = 185356, upload-time = "2026-01-10T09:23:32.627Z" }, + { url = "https://files.pythonhosted.org/packages/c2/b6/b9afed2afadddaf5ebb2afa801abf4b0868f42f8539bfe4b071b5266c9fe/websockets-16.0-cp314-cp314t-win32.whl", hash = "sha256:5a4b4cc550cb665dd8a47f868c8d04c8230f857363ad3c9caf7a0c3bf8c61ca6", size = 178085, upload-time = "2026-01-10T09:23:33.816Z" }, + { url = "https://files.pythonhosted.org/packages/9f/3e/28135a24e384493fa804216b79a6a6759a38cc4ff59118787b9fb693df93/websockets-16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b14dc141ed6d2dde437cddb216004bcac6a1df0935d79656387bd41632ba0bbd", size = 178531, upload-time = "2026-01-10T09:23:35.016Z" }, + { url = "https://files.pythonhosted.org/packages/72/07/c98a68571dcf256e74f1f816b8cc5eae6eb2d3d5cfa44d37f801619d9166/websockets-16.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:349f83cd6c9a415428ee1005cadb5c2c56f4389bc06a9af16103c3bc3dcc8b7d", size = 174947, upload-time = "2026-01-10T09:23:36.166Z" }, + { url = "https://files.pythonhosted.org/packages/7e/52/93e166a81e0305b33fe416338be92ae863563fe7bce446b0f687b9df5aea/websockets-16.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:4a1aba3340a8dca8db6eb5a7986157f52eb9e436b74813764241981ca4888f03", size = 175260, upload-time = "2026-01-10T09:23:37.409Z" }, + { url = "https://files.pythonhosted.org/packages/56/0c/2dbf513bafd24889d33de2ff0368190a0e69f37bcfa19009ef819fe4d507/websockets-16.0-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f4a32d1bd841d4bcbffdcb3d2ce50c09c3909fbead375ab28d0181af89fd04da", size = 176071, upload-time = "2026-01-10T09:23:39.158Z" }, + { url = "https://files.pythonhosted.org/packages/a5/8f/aea9c71cc92bf9b6cc0f7f70df8f0b420636b6c96ef4feee1e16f80f75dd/websockets-16.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0298d07ee155e2e9fda5be8a9042200dd2e3bb0b8a38482156576f863a9d457c", size = 176968, upload-time = "2026-01-10T09:23:41.031Z" }, + { url = "https://files.pythonhosted.org/packages/9a/3f/f70e03f40ffc9a30d817eef7da1be72ee4956ba8d7255c399a01b135902a/websockets-16.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:a653aea902e0324b52f1613332ddf50b00c06fdaf7e92624fbf8c77c78fa5767", size = 178735, upload-time = "2026-01-10T09:23:42.259Z" }, + { url = "https://files.pythonhosted.org/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598, upload-time = "2026-01-10T09:23:45.395Z" }, +] + +[[package]] +name = "wrapt" +version = "2.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2e/64/925f213fdcbb9baeb1530449ac71a4d57fc361c053d06bf78d0c5c7cd80c/wrapt-2.1.2.tar.gz", hash = "sha256:3996a67eecc2c68fd47b4e3c564405a5777367adfd9b8abb58387b63ee83b21e", size = 81678, upload-time = "2026-03-06T02:53:25.134Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/81/60c4471fce95afa5922ca09b88a25f03c93343f759aae0f31fb4412a85c7/wrapt-2.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:96159a0ee2b0277d44201c3b5be479a9979cf154e8c82fa5df49586a8e7679bb", size = 60666, upload-time = "2026-03-06T02:52:58.934Z" }, + { url = "https://files.pythonhosted.org/packages/6b/be/80e80e39e7cb90b006a0eaf11c73ac3a62bbfb3068469aec15cc0bc795de/wrapt-2.1.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:98ba61833a77b747901e9012072f038795de7fc77849f1faa965464f3f87ff2d", size = 61601, upload-time = "2026-03-06T02:53:00.487Z" }, + { url = "https://files.pythonhosted.org/packages/b0/be/d7c88cd9293c859fc74b232abdc65a229bb953997995d6912fc85af18323/wrapt-2.1.2-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:767c0dbbe76cae2a60dd2b235ac0c87c9cccf4898aef8062e57bead46b5f6894", size = 114057, upload-time = "2026-03-06T02:52:44.08Z" }, + { url = "https://files.pythonhosted.org/packages/ea/25/36c04602831a4d685d45a93b3abea61eca7fe35dab6c842d6f5d570ef94a/wrapt-2.1.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c691a6bc752c0cc4711cc0c00896fcd0f116abc253609ef64ef930032821842", size = 116099, upload-time = "2026-03-06T02:54:56.74Z" }, + { url = "https://files.pythonhosted.org/packages/5c/4e/98a6eb417ef551dc277bec1253d5246b25003cf36fdf3913b65cb7657a56/wrapt-2.1.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f3b7d73012ea75aee5844de58c88f44cf62d0d62711e39da5a82824a7c4626a8", size = 112457, upload-time = "2026-03-06T02:53:52.842Z" }, + { url = "https://files.pythonhosted.org/packages/cb/a6/a6f7186a5297cad8ec53fd7578533b28f795fdf5372368c74bd7e6e9841c/wrapt-2.1.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:577dff354e7acd9d411eaf4bfe76b724c89c89c8fc9b7e127ee28c5f7bcb25b6", size = 115351, upload-time = "2026-03-06T02:53:32.684Z" }, + { url = "https://files.pythonhosted.org/packages/97/6f/06e66189e721dbebd5cf20e138acc4d1150288ce118462f2fcbff92d38db/wrapt-2.1.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:3d7b6fd105f8b24e5bd23ccf41cb1d1099796524bcc6f7fbb8fe576c44befbc9", size = 111748, upload-time = "2026-03-06T02:53:08.455Z" }, + { url = "https://files.pythonhosted.org/packages/ef/43/4808b86f499a51370fbdbdfa6cb91e9b9169e762716456471b619fca7a70/wrapt-2.1.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:866abdbf4612e0b34764922ef8b1c5668867610a718d3053d59e24a5e5fcfc15", size = 113783, upload-time = "2026-03-06T02:53:02.02Z" }, + { url = "https://files.pythonhosted.org/packages/91/2c/a3f28b8fa7ac2cefa01cfcaca3471f9b0460608d012b693998cd61ef43df/wrapt-2.1.2-cp311-cp311-win32.whl", hash = "sha256:5a0a0a3a882393095573344075189eb2d566e0fd205a2b6414e9997b1b800a8b", size = 57977, upload-time = "2026-03-06T02:53:27.844Z" }, + { url = "https://files.pythonhosted.org/packages/3f/c3/2b1c7bd07a27b1db885a2fab469b707bdd35bddf30a113b4917a7e2139d2/wrapt-2.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:64a07a71d2730ba56f11d1a4b91f7817dc79bc134c11516b75d1921a7c6fcda1", size = 60336, upload-time = "2026-03-06T02:54:28.104Z" }, + { url = "https://files.pythonhosted.org/packages/ec/5c/76ece7b401b088daa6503d6264dd80f9a727df3e6042802de9a223084ea2/wrapt-2.1.2-cp311-cp311-win_arm64.whl", hash = "sha256:b89f095fe98bc12107f82a9f7d570dc83a0870291aeb6b1d7a7d35575f55d98a", size = 58756, upload-time = "2026-03-06T02:53:16.319Z" }, + { url = "https://files.pythonhosted.org/packages/4c/b6/1db817582c49c7fcbb7df6809d0f515af29d7c2fbf57eb44c36e98fb1492/wrapt-2.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ff2aad9c4cda28a8f0653fc2d487596458c2a3f475e56ba02909e950a9efa6a9", size = 61255, upload-time = "2026-03-06T02:52:45.663Z" }, + { url = "https://files.pythonhosted.org/packages/a2/16/9b02a6b99c09227c93cd4b73acc3678114154ec38da53043c0ddc1fba0dc/wrapt-2.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6433ea84e1cfacf32021d2a4ee909554ade7fd392caa6f7c13f1f4bf7b8e8748", size = 61848, upload-time = "2026-03-06T02:53:48.728Z" }, + { url = "https://files.pythonhosted.org/packages/af/aa/ead46a88f9ec3a432a4832dfedb84092fc35af2d0ba40cd04aea3889f247/wrapt-2.1.2-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c20b757c268d30d6215916a5fa8461048d023865d888e437fab451139cad6c8e", size = 121433, upload-time = "2026-03-06T02:54:40.328Z" }, + { url = "https://files.pythonhosted.org/packages/3a/9f/742c7c7cdf58b59085a1ee4b6c37b013f66ac33673a7ef4aaed5e992bc33/wrapt-2.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79847b83eb38e70d93dc392c7c5b587efe65b3e7afcc167aa8abd5d60e8761c8", size = 123013, upload-time = "2026-03-06T02:53:26.58Z" }, + { url = "https://files.pythonhosted.org/packages/e8/44/2c3dd45d53236b7ed7c646fcf212251dc19e48e599debd3926b52310fafb/wrapt-2.1.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f8fba1bae256186a83d1875b2b1f4e2d1242e8fac0f58ec0d7e41b26967b965c", size = 117326, upload-time = "2026-03-06T02:53:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/74/e2/b17d66abc26bd96f89dec0ecd0ef03da4a1286e6ff793839ec431b9fae57/wrapt-2.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e3d3b35eedcf5f7d022291ecd7533321c4775f7b9cd0050a31a68499ba45757c", size = 121444, upload-time = "2026-03-06T02:54:09.5Z" }, + { url = "https://files.pythonhosted.org/packages/3c/62/e2977843fdf9f03daf1586a0ff49060b1b2fc7ff85a7ea82b6217c1ae36e/wrapt-2.1.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:6f2c5390460de57fa9582bc8a1b7a6c86e1a41dfad74c5225fc07044c15cc8d1", size = 116237, upload-time = "2026-03-06T02:54:03.884Z" }, + { url = "https://files.pythonhosted.org/packages/88/dd/27fc67914e68d740bce512f11734aec08696e6b17641fef8867c00c949fc/wrapt-2.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7dfa9f2cf65d027b951d05c662cc99ee3bd01f6e4691ed39848a7a5fffc902b2", size = 120563, upload-time = "2026-03-06T02:53:20.412Z" }, + { url = "https://files.pythonhosted.org/packages/ec/9f/b750b3692ed2ef4705cb305bd68858e73010492b80e43d2a4faa5573cbe7/wrapt-2.1.2-cp312-cp312-win32.whl", hash = "sha256:eba8155747eb2cae4a0b913d9ebd12a1db4d860fc4c829d7578c7b989bd3f2f0", size = 58198, upload-time = "2026-03-06T02:53:37.732Z" }, + { url = "https://files.pythonhosted.org/packages/8e/b2/feecfe29f28483d888d76a48f03c4c4d8afea944dbee2b0cd3380f9df032/wrapt-2.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:1c51c738d7d9faa0b3601708e7e2eda9bf779e1b601dce6c77411f2a1b324a63", size = 60441, upload-time = "2026-03-06T02:52:47.138Z" }, + { url = "https://files.pythonhosted.org/packages/44/e1/e328f605d6e208547ea9fd120804fcdec68536ac748987a68c47c606eea8/wrapt-2.1.2-cp312-cp312-win_arm64.whl", hash = "sha256:c8e46ae8e4032792eb2f677dbd0d557170a8e5524d22acc55199f43efedd39bf", size = 58836, upload-time = "2026-03-06T02:53:22.053Z" }, + { url = "https://files.pythonhosted.org/packages/4c/7a/d936840735c828b38d26a854e85d5338894cda544cb7a85a9d5b8b9c4df7/wrapt-2.1.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:787fd6f4d67befa6fe2abdffcbd3de2d82dfc6fb8a6d850407c53332709d030b", size = 61259, upload-time = "2026-03-06T02:53:41.922Z" }, + { url = "https://files.pythonhosted.org/packages/5e/88/9a9b9a90ac8ca11c2fdb6a286cb3a1fc7dd774c00ed70929a6434f6bc634/wrapt-2.1.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4bdf26e03e6d0da3f0e9422fd36bcebf7bc0eeb55fdf9c727a09abc6b9fe472e", size = 61851, upload-time = "2026-03-06T02:52:48.672Z" }, + { url = "https://files.pythonhosted.org/packages/03/a9/5b7d6a16fd6533fed2756900fc8fc923f678179aea62ada6d65c92718c00/wrapt-2.1.2-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bbac24d879aa22998e87f6b3f481a5216311e7d53c7db87f189a7a0266dafffb", size = 121446, upload-time = "2026-03-06T02:54:14.013Z" }, + { url = "https://files.pythonhosted.org/packages/45/bb/34c443690c847835cfe9f892be78c533d4f32366ad2888972c094a897e39/wrapt-2.1.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:16997dfb9d67addc2e3f41b62a104341e80cac52f91110dece393923c0ebd5ca", size = 123056, upload-time = "2026-03-06T02:54:10.829Z" }, + { url = "https://files.pythonhosted.org/packages/93/b9/ff205f391cb708f67f41ea148545f2b53ff543a7ac293b30d178af4d2271/wrapt-2.1.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:162e4e2ba7542da9027821cb6e7c5e068d64f9a10b5f15512ea28e954893a267", size = 117359, upload-time = "2026-03-06T02:53:03.623Z" }, + { url = "https://files.pythonhosted.org/packages/1f/3d/1ea04d7747825119c3c9a5e0874a40b33594ada92e5649347c457d982805/wrapt-2.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f29c827a8d9936ac320746747a016c4bc66ef639f5cd0d32df24f5eacbf9c69f", size = 121479, upload-time = "2026-03-06T02:53:45.844Z" }, + { url = "https://files.pythonhosted.org/packages/78/cc/ee3a011920c7a023b25e8df26f306b2484a531ab84ca5c96260a73de76c0/wrapt-2.1.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:a9dd9813825f7ecb018c17fd147a01845eb330254dff86d3b5816f20f4d6aaf8", size = 116271, upload-time = "2026-03-06T02:54:46.356Z" }, + { url = "https://files.pythonhosted.org/packages/98/fd/e5ff7ded41b76d802cf1191288473e850d24ba2e39a6ec540f21ae3b57cb/wrapt-2.1.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6f8dbdd3719e534860d6a78526aafc220e0241f981367018c2875178cf83a413", size = 120573, upload-time = "2026-03-06T02:52:50.163Z" }, + { url = "https://files.pythonhosted.org/packages/47/c5/242cae3b5b080cd09bacef0591691ba1879739050cc7c801ff35c8886b66/wrapt-2.1.2-cp313-cp313-win32.whl", hash = "sha256:5c35b5d82b16a3bc6e0a04349b606a0582bc29f573786aebe98e0c159bc48db6", size = 58205, upload-time = "2026-03-06T02:53:47.494Z" }, + { url = "https://files.pythonhosted.org/packages/12/69/c358c61e7a50f290958809b3c61ebe8b3838ea3e070d7aac9814f95a0528/wrapt-2.1.2-cp313-cp313-win_amd64.whl", hash = "sha256:f8bc1c264d8d1cf5b3560a87bbdd31131573eb25f9f9447bb6252b8d4c44a3a1", size = 60452, upload-time = "2026-03-06T02:53:30.038Z" }, + { url = "https://files.pythonhosted.org/packages/8e/66/c8a6fcfe321295fd8c0ab1bd685b5a01462a9b3aa2f597254462fc2bc975/wrapt-2.1.2-cp313-cp313-win_arm64.whl", hash = "sha256:3beb22f674550d5634642c645aba4c72a2c66fb185ae1aebe1e955fae5a13baf", size = 58842, upload-time = "2026-03-06T02:52:52.114Z" }, + { url = "https://files.pythonhosted.org/packages/da/55/9c7052c349106e0b3f17ae8db4b23a691a963c334de7f9dbd60f8f74a831/wrapt-2.1.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0fc04bc8664a8bc4c8e00b37b5355cffca2535209fba1abb09ae2b7c76ddf82b", size = 63075, upload-time = "2026-03-06T02:53:19.108Z" }, + { url = "https://files.pythonhosted.org/packages/09/a8/ce7b4006f7218248dd71b7b2b732d0710845a0e49213b18faef64811ffef/wrapt-2.1.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a9b9d50c9af998875a1482a038eb05755dfd6fe303a313f6a940bb53a83c3f18", size = 63719, upload-time = "2026-03-06T02:54:33.452Z" }, + { url = "https://files.pythonhosted.org/packages/e4/e5/2ca472e80b9e2b7a17f106bb8f9df1db11e62101652ce210f66935c6af67/wrapt-2.1.2-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2d3ff4f0024dd224290c0eabf0240f1bfc1f26363431505fb1b0283d3b08f11d", size = 152643, upload-time = "2026-03-06T02:52:42.721Z" }, + { url = "https://files.pythonhosted.org/packages/36/42/30f0f2cefca9d9cbf6835f544d825064570203c3e70aa873d8ae12e23791/wrapt-2.1.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3278c471f4468ad544a691b31bb856374fbdefb7fee1a152153e64019379f015", size = 158805, upload-time = "2026-03-06T02:54:25.441Z" }, + { url = "https://files.pythonhosted.org/packages/bb/67/d08672f801f604889dcf58f1a0b424fe3808860ede9e03affc1876b295af/wrapt-2.1.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a8914c754d3134a3032601c6984db1c576e6abaf3fc68094bb8ab1379d75ff92", size = 145990, upload-time = "2026-03-06T02:53:57.456Z" }, + { url = "https://files.pythonhosted.org/packages/68/a7/fd371b02e73babec1de6ade596e8cd9691051058cfdadbfd62a5898f3295/wrapt-2.1.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:ff95d4264e55839be37bafe1536db2ab2de19da6b65f9244f01f332b5286cfbf", size = 155670, upload-time = "2026-03-06T02:54:55.309Z" }, + { url = "https://files.pythonhosted.org/packages/86/2d/9fe0095dfdb621009f40117dcebf41d7396c2c22dca6eac779f4c007b86c/wrapt-2.1.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:76405518ca4e1b76fbb1b9f686cff93aebae03920cc55ceeec48ff9f719c5f67", size = 144357, upload-time = "2026-03-06T02:54:24.092Z" }, + { url = "https://files.pythonhosted.org/packages/0e/b6/ec7b4a254abbe4cde9fa15c5d2cca4518f6b07d0f1b77d4ee9655e30280e/wrapt-2.1.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c0be8b5a74c5824e9359b53e7e58bef71a729bacc82e16587db1c4ebc91f7c5a", size = 150269, upload-time = "2026-03-06T02:53:31.268Z" }, + { url = "https://files.pythonhosted.org/packages/6e/6b/2fabe8ebf148f4ee3c782aae86a795cc68ffe7d432ef550f234025ce0cfa/wrapt-2.1.2-cp313-cp313t-win32.whl", hash = "sha256:f01277d9a5fc1862f26f7626da9cf443bebc0abd2f303f41c5e995b15887dabd", size = 59894, upload-time = "2026-03-06T02:54:15.391Z" }, + { url = "https://files.pythonhosted.org/packages/ca/fb/9ba66fc2dedc936de5f8073c0217b5d4484e966d87723415cc8262c5d9c2/wrapt-2.1.2-cp313-cp313t-win_amd64.whl", hash = "sha256:84ce8f1c2104d2f6daa912b1b5b039f331febfeee74f8042ad4e04992bd95c8f", size = 63197, upload-time = "2026-03-06T02:54:41.943Z" }, + { url = "https://files.pythonhosted.org/packages/c0/1c/012d7423c95d0e337117723eb8ecf73c622ce15a97847e84cf3f8f26cd7e/wrapt-2.1.2-cp313-cp313t-win_arm64.whl", hash = "sha256:a93cd767e37faeddbe07d8fc4212d5cba660af59bdb0f6372c93faaa13e6e679", size = 60363, upload-time = "2026-03-06T02:54:48.093Z" }, + { url = "https://files.pythonhosted.org/packages/39/25/e7ea0b417db02bb796182a5316398a75792cd9a22528783d868755e1f669/wrapt-2.1.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:1370e516598854e5b4366e09ce81e08bfe94d42b0fd569b88ec46cc56d9164a9", size = 61418, upload-time = "2026-03-06T02:53:55.706Z" }, + { url = "https://files.pythonhosted.org/packages/ec/0f/fa539e2f6a770249907757eaeb9a5ff4deb41c026f8466c1c6d799088a9b/wrapt-2.1.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6de1a3851c27e0bd6a04ca993ea6f80fc53e6c742ee1601f486c08e9f9b900a9", size = 61914, upload-time = "2026-03-06T02:52:53.37Z" }, + { url = "https://files.pythonhosted.org/packages/53/37/02af1867f5b1441aaeda9c82deed061b7cd1372572ddcd717f6df90b5e93/wrapt-2.1.2-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:de9f1a2bbc5ac7f6012ec24525bdd444765a2ff64b5985ac6e0692144838542e", size = 120417, upload-time = "2026-03-06T02:54:30.74Z" }, + { url = "https://files.pythonhosted.org/packages/c3/b7/0138a6238c8ba7476c77cf786a807f871672b37f37a422970342308276e7/wrapt-2.1.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:970d57ed83fa040d8b20c52fe74a6ae7e3775ae8cff5efd6a81e06b19078484c", size = 122797, upload-time = "2026-03-06T02:54:51.539Z" }, + { url = "https://files.pythonhosted.org/packages/e1/ad/819ae558036d6a15b7ed290d5b14e209ca795dd4da9c58e50c067d5927b0/wrapt-2.1.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3969c56e4563c375861c8df14fa55146e81ac11c8db49ea6fb7f2ba58bc1ff9a", size = 117350, upload-time = "2026-03-06T02:54:37.651Z" }, + { url = "https://files.pythonhosted.org/packages/8b/2d/afc18dc57a4600a6e594f77a9ae09db54f55ba455440a54886694a84c71b/wrapt-2.1.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:57d7c0c980abdc5f1d98b11a2aa3bb159790add80258c717fa49a99921456d90", size = 121223, upload-time = "2026-03-06T02:54:35.221Z" }, + { url = "https://files.pythonhosted.org/packages/b9/5b/5ec189b22205697bc56eb3b62aed87a1e0423e9c8285d0781c7a83170d15/wrapt-2.1.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:776867878e83130c7a04237010463372e877c1c994d449ca6aaafeab6aab2586", size = 116287, upload-time = "2026-03-06T02:54:19.654Z" }, + { url = "https://files.pythonhosted.org/packages/f7/2d/f84939a7c9b5e6cdd8a8d0f6a26cabf36a0f7e468b967720e8b0cd2bdf69/wrapt-2.1.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:fab036efe5464ec3291411fabb80a7a39e2dd80bae9bcbeeca5087fdfa891e19", size = 119593, upload-time = "2026-03-06T02:54:16.697Z" }, + { url = "https://files.pythonhosted.org/packages/0b/fe/ccd22a1263159c4ac811ab9374c061bcb4a702773f6e06e38de5f81a1bdc/wrapt-2.1.2-cp314-cp314-win32.whl", hash = "sha256:e6ed62c82ddf58d001096ae84ce7f833db97ae2263bff31c9b336ba8cfe3f508", size = 58631, upload-time = "2026-03-06T02:53:06.498Z" }, + { url = "https://files.pythonhosted.org/packages/65/0a/6bd83be7bff2e7efaac7b4ac9748da9d75a34634bbbbc8ad077d527146df/wrapt-2.1.2-cp314-cp314-win_amd64.whl", hash = "sha256:467e7c76315390331c67073073d00662015bb730c566820c9ca9b54e4d67fd04", size = 60875, upload-time = "2026-03-06T02:53:50.252Z" }, + { url = "https://files.pythonhosted.org/packages/6c/c0/0b3056397fe02ff80e5a5d72d627c11eb885d1ca78e71b1a5c1e8c7d45de/wrapt-2.1.2-cp314-cp314-win_arm64.whl", hash = "sha256:da1f00a557c66225d53b095a97eace0fc5349e3bfda28fa34ffae238978ee575", size = 59164, upload-time = "2026-03-06T02:53:59.128Z" }, + { url = "https://files.pythonhosted.org/packages/71/ed/5d89c798741993b2371396eb9d4634f009ff1ad8a6c78d366fe2883ea7a6/wrapt-2.1.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:62503ffbc2d3a69891cf29beeaccdb4d5e0a126e2b6a851688d4777e01428dbb", size = 63163, upload-time = "2026-03-06T02:52:54.873Z" }, + { url = "https://files.pythonhosted.org/packages/c6/8c/05d277d182bf36b0a13d6bd393ed1dec3468a25b59d01fba2dd70fe4d6ae/wrapt-2.1.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c7e6cd120ef837d5b6f860a6ea3745f8763805c418bb2f12eeb1fa6e25f22d22", size = 63723, upload-time = "2026-03-06T02:52:56.374Z" }, + { url = "https://files.pythonhosted.org/packages/f4/27/6c51ec1eff4413c57e72d6106bb8dec6f0c7cdba6503d78f0fa98767bcc9/wrapt-2.1.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3769a77df8e756d65fbc050333f423c01ae012b4f6731aaf70cf2bef61b34596", size = 152652, upload-time = "2026-03-06T02:53:23.79Z" }, + { url = "https://files.pythonhosted.org/packages/db/4c/d7dd662d6963fc7335bfe29d512b02b71cdfa23eeca7ab3ac74a67505deb/wrapt-2.1.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a76d61a2e851996150ba0f80582dd92a870643fa481f3b3846f229de88caf044", size = 158807, upload-time = "2026-03-06T02:53:35.742Z" }, + { url = "https://files.pythonhosted.org/packages/b4/4d/1e5eea1a78d539d346765727422976676615814029522c76b87a95f6bcdd/wrapt-2.1.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6f97edc9842cf215312b75fe737ee7c8adda75a89979f8e11558dfff6343cc4b", size = 146061, upload-time = "2026-03-06T02:52:57.574Z" }, + { url = "https://files.pythonhosted.org/packages/89/bc/62cabea7695cd12a288023251eeefdcb8465056ddaab6227cb78a2de005b/wrapt-2.1.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4006c351de6d5007aa33a551f600404ba44228a89e833d2fadc5caa5de8edfbf", size = 155667, upload-time = "2026-03-06T02:53:39.422Z" }, + { url = "https://files.pythonhosted.org/packages/e9/99/6f2888cd68588f24df3a76572c69c2de28287acb9e1972bf0c83ce97dbc1/wrapt-2.1.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:a9372fc3639a878c8e7d87e1556fa209091b0a66e912c611e3f833e2c4202be2", size = 144392, upload-time = "2026-03-06T02:54:22.41Z" }, + { url = "https://files.pythonhosted.org/packages/40/51/1dfc783a6c57971614c48e361a82ca3b6da9055879952587bc99fe1a7171/wrapt-2.1.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3144b027ff30cbd2fca07c0a87e67011adb717eb5f5bd8496325c17e454257a3", size = 150296, upload-time = "2026-03-06T02:54:07.848Z" }, + { url = "https://files.pythonhosted.org/packages/6c/38/cbb8b933a0201076c1f64fc42883b0023002bdc14a4964219154e6ff3350/wrapt-2.1.2-cp314-cp314t-win32.whl", hash = "sha256:3b8d15e52e195813efe5db8cec156eebe339aaf84222f4f4f051a6c01f237ed7", size = 60539, upload-time = "2026-03-06T02:54:00.594Z" }, + { url = "https://files.pythonhosted.org/packages/82/dd/e5176e4b241c9f528402cebb238a36785a628179d7d8b71091154b3e4c9e/wrapt-2.1.2-cp314-cp314t-win_amd64.whl", hash = "sha256:08ffa54146a7559f5b8df4b289b46d963a8e74ed16ba3687f99896101a3990c5", size = 63969, upload-time = "2026-03-06T02:54:39Z" }, + { url = "https://files.pythonhosted.org/packages/5c/99/79f17046cf67e4a95b9987ea129632ba8bcec0bc81f3fb3d19bdb0bd60cd/wrapt-2.1.2-cp314-cp314t-win_arm64.whl", hash = "sha256:72aaa9d0d8e4ed0e2e98019cea47a21f823c9dd4b43c7b77bba6679ffcca6a00", size = 60554, upload-time = "2026-03-06T02:53:14.132Z" }, + { url = "https://files.pythonhosted.org/packages/1a/c7/8528ac2dfa2c1e6708f647df7ae144ead13f0a31146f43c7264b4942bf12/wrapt-2.1.2-py3-none-any.whl", hash = "sha256:b8fd6fa2b2c4e7621808f8c62e8317f4aae56e59721ad933bac5239d913cf0e8", size = 43993, upload-time = "2026-03-06T02:53:12.905Z" }, +] + +[[package]] +name = "xlsxwriter" +version = "3.2.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/46/2c/c06ef49dc36e7954e55b802a8b231770d286a9758b3d936bd1e04ce5ba88/xlsxwriter-3.2.9.tar.gz", hash = "sha256:254b1c37a368c444eac6e2f867405cc9e461b0ed97a3233b2ac1e574efb4140c", size = 215940, upload-time = "2025-09-16T00:16:21.63Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/0c/3662f4a66880196a590b202f0db82d919dd2f89e99a27fadef91c4a33d41/xlsxwriter-3.2.9-py3-none-any.whl", hash = "sha256:9a5db42bc5dff014806c58a20b9eae7322a134abb6fce3c92c181bfb275ec5b3", size = 175315, upload-time = "2025-09-16T00:16:20.108Z" }, +] + +[[package]] +name = "xxhash" +version = "3.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/02/84/30869e01909fb37a6cc7e18688ee8bf1e42d57e7e0777636bd47524c43c7/xxhash-3.6.0.tar.gz", hash = "sha256:f0162a78b13a0d7617b2845b90c763339d1f1d82bb04a4b07f4ab535cc5e05d6", size = 85160, upload-time = "2025-10-02T14:37:08.097Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/d4/cc2f0400e9154df4b9964249da78ebd72f318e35ccc425e9f403c392f22a/xxhash-3.6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b47bbd8cf2d72797f3c2772eaaac0ded3d3af26481a26d7d7d41dc2d3c46b04a", size = 32844, upload-time = "2025-10-02T14:34:14.037Z" }, + { url = "https://files.pythonhosted.org/packages/5e/ec/1cc11cd13e26ea8bc3cb4af4eaadd8d46d5014aebb67be3f71fb0b68802a/xxhash-3.6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2b6821e94346f96db75abaa6e255706fb06ebd530899ed76d32cd99f20dc52fa", size = 30809, upload-time = "2025-10-02T14:34:15.484Z" }, + { url = "https://files.pythonhosted.org/packages/04/5f/19fe357ea348d98ca22f456f75a30ac0916b51c753e1f8b2e0e6fb884cce/xxhash-3.6.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d0a9751f71a1a65ce3584e9cae4467651c7e70c9d31017fa57574583a4540248", size = 194665, upload-time = "2025-10-02T14:34:16.541Z" }, + { url = "https://files.pythonhosted.org/packages/90/3b/d1f1a8f5442a5fd8beedae110c5af7604dc37349a8e16519c13c19a9a2de/xxhash-3.6.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b29ee68625ab37b04c0b40c3fafdf24d2f75ccd778333cfb698f65f6c463f62", size = 213550, upload-time = "2025-10-02T14:34:17.878Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ef/3a9b05eb527457d5db13a135a2ae1a26c80fecd624d20f3e8dcc4cb170f3/xxhash-3.6.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6812c25fe0d6c36a46ccb002f40f27ac903bf18af9f6dd8f9669cb4d176ab18f", size = 212384, upload-time = "2025-10-02T14:34:19.182Z" }, + { url = "https://files.pythonhosted.org/packages/0f/18/ccc194ee698c6c623acbf0f8c2969811a8a4b6185af5e824cd27b9e4fd3e/xxhash-3.6.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4ccbff013972390b51a18ef1255ef5ac125c92dc9143b2d1909f59abc765540e", size = 445749, upload-time = "2025-10-02T14:34:20.659Z" }, + { url = "https://files.pythonhosted.org/packages/a5/86/cf2c0321dc3940a7aa73076f4fd677a0fb3e405cb297ead7d864fd90847e/xxhash-3.6.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:297b7fbf86c82c550e12e8fb71968b3f033d27b874276ba3624ea868c11165a8", size = 193880, upload-time = "2025-10-02T14:34:22.431Z" }, + { url = "https://files.pythonhosted.org/packages/82/fb/96213c8560e6f948a1ecc9a7613f8032b19ee45f747f4fca4eb31bb6d6ed/xxhash-3.6.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:dea26ae1eb293db089798d3973a5fc928a18fdd97cc8801226fae705b02b14b0", size = 210912, upload-time = "2025-10-02T14:34:23.937Z" }, + { url = "https://files.pythonhosted.org/packages/40/aa/4395e669b0606a096d6788f40dbdf2b819d6773aa290c19e6e83cbfc312f/xxhash-3.6.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7a0b169aafb98f4284f73635a8e93f0735f9cbde17bd5ec332480484241aaa77", size = 198654, upload-time = "2025-10-02T14:34:25.644Z" }, + { url = "https://files.pythonhosted.org/packages/67/74/b044fcd6b3d89e9b1b665924d85d3f400636c23590226feb1eb09e1176ce/xxhash-3.6.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:08d45aef063a4531b785cd72de4887766d01dc8f362a515693df349fdb825e0c", size = 210867, upload-time = "2025-10-02T14:34:27.203Z" }, + { url = "https://files.pythonhosted.org/packages/bc/fd/3ce73bf753b08cb19daee1eb14aa0d7fe331f8da9c02dd95316ddfe5275e/xxhash-3.6.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:929142361a48ee07f09121fe9e96a84950e8d4df3bb298ca5d88061969f34d7b", size = 414012, upload-time = "2025-10-02T14:34:28.409Z" }, + { url = "https://files.pythonhosted.org/packages/ba/b3/5a4241309217c5c876f156b10778f3ab3af7ba7e3259e6d5f5c7d0129eb2/xxhash-3.6.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:51312c768403d8540487dbbfb557454cfc55589bbde6424456951f7fcd4facb3", size = 191409, upload-time = "2025-10-02T14:34:29.696Z" }, + { url = "https://files.pythonhosted.org/packages/c0/01/99bfbc15fb9abb9a72b088c1d95219fc4782b7d01fc835bd5744d66dd0b8/xxhash-3.6.0-cp311-cp311-win32.whl", hash = "sha256:d1927a69feddc24c987b337ce81ac15c4720955b667fe9b588e02254b80446fd", size = 30574, upload-time = "2025-10-02T14:34:31.028Z" }, + { url = "https://files.pythonhosted.org/packages/65/79/9d24d7f53819fe301b231044ea362ce64e86c74f6e8c8e51320de248b3e5/xxhash-3.6.0-cp311-cp311-win_amd64.whl", hash = "sha256:26734cdc2d4ffe449b41d186bbeac416f704a482ed835d375a5c0cb02bc63fef", size = 31481, upload-time = "2025-10-02T14:34:32.062Z" }, + { url = "https://files.pythonhosted.org/packages/30/4e/15cd0e3e8772071344eab2961ce83f6e485111fed8beb491a3f1ce100270/xxhash-3.6.0-cp311-cp311-win_arm64.whl", hash = "sha256:d72f67ef8bf36e05f5b6c65e8524f265bd61071471cd4cf1d36743ebeeeb06b7", size = 27861, upload-time = "2025-10-02T14:34:33.555Z" }, + { url = "https://files.pythonhosted.org/packages/9a/07/d9412f3d7d462347e4511181dea65e47e0d0e16e26fbee2ea86a2aefb657/xxhash-3.6.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:01362c4331775398e7bb34e3ab403bc9ee9f7c497bc7dee6272114055277dd3c", size = 32744, upload-time = "2025-10-02T14:34:34.622Z" }, + { url = "https://files.pythonhosted.org/packages/79/35/0429ee11d035fc33abe32dca1b2b69e8c18d236547b9a9b72c1929189b9a/xxhash-3.6.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b7b2df81a23f8cb99656378e72501b2cb41b1827c0f5a86f87d6b06b69f9f204", size = 30816, upload-time = "2025-10-02T14:34:36.043Z" }, + { url = "https://files.pythonhosted.org/packages/b7/f2/57eb99aa0f7d98624c0932c5b9a170e1806406cdbcdb510546634a1359e0/xxhash-3.6.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:dc94790144e66b14f67b10ac8ed75b39ca47536bf8800eb7c24b50271ea0c490", size = 194035, upload-time = "2025-10-02T14:34:37.354Z" }, + { url = "https://files.pythonhosted.org/packages/4c/ed/6224ba353690d73af7a3f1c7cdb1fc1b002e38f783cb991ae338e1eb3d79/xxhash-3.6.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:93f107c673bccf0d592cdba077dedaf52fe7f42dcd7676eba1f6d6f0c3efffd2", size = 212914, upload-time = "2025-10-02T14:34:38.6Z" }, + { url = "https://files.pythonhosted.org/packages/38/86/fb6b6130d8dd6b8942cc17ab4d90e223653a89aa32ad2776f8af7064ed13/xxhash-3.6.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2aa5ee3444c25b69813663c9f8067dcfaa2e126dc55e8dddf40f4d1c25d7effa", size = 212163, upload-time = "2025-10-02T14:34:39.872Z" }, + { url = "https://files.pythonhosted.org/packages/ee/dc/e84875682b0593e884ad73b2d40767b5790d417bde603cceb6878901d647/xxhash-3.6.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f7f99123f0e1194fa59cc69ad46dbae2e07becec5df50a0509a808f90a0f03f0", size = 445411, upload-time = "2025-10-02T14:34:41.569Z" }, + { url = "https://files.pythonhosted.org/packages/11/4f/426f91b96701ec2f37bb2b8cec664eff4f658a11f3fa9d94f0a887ea6d2b/xxhash-3.6.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:49e03e6fe2cac4a1bc64952dd250cf0dbc5ef4ebb7b8d96bce82e2de163c82a2", size = 193883, upload-time = "2025-10-02T14:34:43.249Z" }, + { url = "https://files.pythonhosted.org/packages/53/5a/ddbb83eee8e28b778eacfc5a85c969673e4023cdeedcfcef61f36731610b/xxhash-3.6.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bd17fede52a17a4f9a7bc4472a5867cb0b160deeb431795c0e4abe158bc784e9", size = 210392, upload-time = "2025-10-02T14:34:45.042Z" }, + { url = "https://files.pythonhosted.org/packages/1e/c2/ff69efd07c8c074ccdf0a4f36fcdd3d27363665bcdf4ba399abebe643465/xxhash-3.6.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:6fb5f5476bef678f69db04f2bd1efbed3030d2aba305b0fc1773645f187d6a4e", size = 197898, upload-time = "2025-10-02T14:34:46.302Z" }, + { url = "https://files.pythonhosted.org/packages/58/ca/faa05ac19b3b622c7c9317ac3e23954187516298a091eb02c976d0d3dd45/xxhash-3.6.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:843b52f6d88071f87eba1631b684fcb4b2068cd2180a0224122fe4ef011a9374", size = 210655, upload-time = "2025-10-02T14:34:47.571Z" }, + { url = "https://files.pythonhosted.org/packages/d4/7a/06aa7482345480cc0cb597f5c875b11a82c3953f534394f620b0be2f700c/xxhash-3.6.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:7d14a6cfaf03b1b6f5f9790f76880601ccc7896aff7ab9cd8978a939c1eb7e0d", size = 414001, upload-time = "2025-10-02T14:34:49.273Z" }, + { url = "https://files.pythonhosted.org/packages/23/07/63ffb386cd47029aa2916b3d2f454e6cc5b9f5c5ada3790377d5430084e7/xxhash-3.6.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:418daf3db71e1413cfe211c2f9a528456936645c17f46b5204705581a45390ae", size = 191431, upload-time = "2025-10-02T14:34:50.798Z" }, + { url = "https://files.pythonhosted.org/packages/0f/93/14fde614cadb4ddf5e7cebf8918b7e8fac5ae7861c1875964f17e678205c/xxhash-3.6.0-cp312-cp312-win32.whl", hash = "sha256:50fc255f39428a27299c20e280d6193d8b63b8ef8028995323bf834a026b4fbb", size = 30617, upload-time = "2025-10-02T14:34:51.954Z" }, + { url = "https://files.pythonhosted.org/packages/13/5d/0d125536cbe7565a83d06e43783389ecae0c0f2ed037b48ede185de477c0/xxhash-3.6.0-cp312-cp312-win_amd64.whl", hash = "sha256:c0f2ab8c715630565ab8991b536ecded9416d615538be8ecddce43ccf26cbc7c", size = 31534, upload-time = "2025-10-02T14:34:53.276Z" }, + { url = "https://files.pythonhosted.org/packages/54/85/6ec269b0952ec7e36ba019125982cf11d91256a778c7c3f98a4c5043d283/xxhash-3.6.0-cp312-cp312-win_arm64.whl", hash = "sha256:eae5c13f3bc455a3bbb68bdc513912dc7356de7e2280363ea235f71f54064829", size = 27876, upload-time = "2025-10-02T14:34:54.371Z" }, + { url = "https://files.pythonhosted.org/packages/33/76/35d05267ac82f53ae9b0e554da7c5e281ee61f3cad44c743f0fcd354f211/xxhash-3.6.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:599e64ba7f67472481ceb6ee80fa3bd828fd61ba59fb11475572cc5ee52b89ec", size = 32738, upload-time = "2025-10-02T14:34:55.839Z" }, + { url = "https://files.pythonhosted.org/packages/31/a8/3fbce1cd96534a95e35d5120637bf29b0d7f5d8fa2f6374e31b4156dd419/xxhash-3.6.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7d8b8aaa30fca4f16f0c84a5c8d7ddee0e25250ec2796c973775373257dde8f1", size = 30821, upload-time = "2025-10-02T14:34:57.219Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ea/d387530ca7ecfa183cb358027f1833297c6ac6098223fd14f9782cd0015c/xxhash-3.6.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d597acf8506d6e7101a4a44a5e428977a51c0fadbbfd3c39650cca9253f6e5a6", size = 194127, upload-time = "2025-10-02T14:34:59.21Z" }, + { url = "https://files.pythonhosted.org/packages/ba/0c/71435dcb99874b09a43b8d7c54071e600a7481e42b3e3ce1eb5226a5711a/xxhash-3.6.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:858dc935963a33bc33490128edc1c12b0c14d9c7ebaa4e387a7869ecc4f3e263", size = 212975, upload-time = "2025-10-02T14:35:00.816Z" }, + { url = "https://files.pythonhosted.org/packages/84/7a/c2b3d071e4bb4a90b7057228a99b10d51744878f4a8a6dd643c8bd897620/xxhash-3.6.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ba284920194615cb8edf73bf52236ce2e1664ccd4a38fdb543506413529cc546", size = 212241, upload-time = "2025-10-02T14:35:02.207Z" }, + { url = "https://files.pythonhosted.org/packages/81/5f/640b6eac0128e215f177df99eadcd0f1b7c42c274ab6a394a05059694c5a/xxhash-3.6.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4b54219177f6c6674d5378bd862c6aedf64725f70dd29c472eaae154df1a2e89", size = 445471, upload-time = "2025-10-02T14:35:03.61Z" }, + { url = "https://files.pythonhosted.org/packages/5e/1e/3c3d3ef071b051cc3abbe3721ffb8365033a172613c04af2da89d5548a87/xxhash-3.6.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:42c36dd7dbad2f5238950c377fcbf6811b1cdb1c444fab447960030cea60504d", size = 193936, upload-time = "2025-10-02T14:35:05.013Z" }, + { url = "https://files.pythonhosted.org/packages/2c/bd/4a5f68381939219abfe1c22a9e3a5854a4f6f6f3c4983a87d255f21f2e5d/xxhash-3.6.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f22927652cba98c44639ffdc7aaf35828dccf679b10b31c4ad72a5b530a18eb7", size = 210440, upload-time = "2025-10-02T14:35:06.239Z" }, + { url = "https://files.pythonhosted.org/packages/eb/37/b80fe3d5cfb9faff01a02121a0f4d565eb7237e9e5fc66e73017e74dcd36/xxhash-3.6.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b45fad44d9c5c119e9c6fbf2e1c656a46dc68e280275007bbfd3d572b21426db", size = 197990, upload-time = "2025-10-02T14:35:07.735Z" }, + { url = "https://files.pythonhosted.org/packages/d7/fd/2c0a00c97b9e18f72e1f240ad4e8f8a90fd9d408289ba9c7c495ed7dc05c/xxhash-3.6.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:6f2580ffab1a8b68ef2b901cde7e55fa8da5e4be0977c68f78fc80f3c143de42", size = 210689, upload-time = "2025-10-02T14:35:09.438Z" }, + { url = "https://files.pythonhosted.org/packages/93/86/5dd8076a926b9a95db3206aba20d89a7fc14dd5aac16e5c4de4b56033140/xxhash-3.6.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:40c391dd3cd041ebc3ffe6f2c862f402e306eb571422e0aa918d8070ba31da11", size = 414068, upload-time = "2025-10-02T14:35:11.162Z" }, + { url = "https://files.pythonhosted.org/packages/af/3c/0bb129170ee8f3650f08e993baee550a09593462a5cddd8e44d0011102b1/xxhash-3.6.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f205badabde7aafd1a31e8ca2a3e5a763107a71c397c4481d6a804eb5063d8bd", size = 191495, upload-time = "2025-10-02T14:35:12.971Z" }, + { url = "https://files.pythonhosted.org/packages/e9/3a/6797e0114c21d1725e2577508e24006fd7ff1d8c0c502d3b52e45c1771d8/xxhash-3.6.0-cp313-cp313-win32.whl", hash = "sha256:2577b276e060b73b73a53042ea5bd5203d3e6347ce0d09f98500f418a9fcf799", size = 30620, upload-time = "2025-10-02T14:35:14.129Z" }, + { url = "https://files.pythonhosted.org/packages/86/15/9bc32671e9a38b413a76d24722a2bf8784a132c043063a8f5152d390b0f9/xxhash-3.6.0-cp313-cp313-win_amd64.whl", hash = "sha256:757320d45d2fbcce8f30c42a6b2f47862967aea7bf458b9625b4bbe7ee390392", size = 31542, upload-time = "2025-10-02T14:35:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/39/c5/cc01e4f6188656e56112d6a8e0dfe298a16934b8c47a247236549a3f7695/xxhash-3.6.0-cp313-cp313-win_arm64.whl", hash = "sha256:457b8f85dec5825eed7b69c11ae86834a018b8e3df5e77783c999663da2f96d6", size = 27880, upload-time = "2025-10-02T14:35:16.315Z" }, + { url = "https://files.pythonhosted.org/packages/f3/30/25e5321c8732759e930c555176d37e24ab84365482d257c3b16362235212/xxhash-3.6.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a42e633d75cdad6d625434e3468126c73f13f7584545a9cf34e883aa1710e702", size = 32956, upload-time = "2025-10-02T14:35:17.413Z" }, + { url = "https://files.pythonhosted.org/packages/9f/3c/0573299560d7d9f8ab1838f1efc021a280b5ae5ae2e849034ef3dee18810/xxhash-3.6.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:568a6d743219e717b07b4e03b0a828ce593833e498c3b64752e0f5df6bfe84db", size = 31072, upload-time = "2025-10-02T14:35:18.844Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1c/52d83a06e417cd9d4137722693424885cc9878249beb3a7c829e74bf7ce9/xxhash-3.6.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bec91b562d8012dae276af8025a55811b875baace6af510412a5e58e3121bc54", size = 196409, upload-time = "2025-10-02T14:35:20.31Z" }, + { url = "https://files.pythonhosted.org/packages/e3/8e/c6d158d12a79bbd0b878f8355432075fc82759e356ab5a111463422a239b/xxhash-3.6.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78e7f2f4c521c30ad5e786fdd6bae89d47a32672a80195467b5de0480aa97b1f", size = 215736, upload-time = "2025-10-02T14:35:21.616Z" }, + { url = "https://files.pythonhosted.org/packages/bc/68/c4c80614716345d55071a396cf03d06e34b5f4917a467faf43083c995155/xxhash-3.6.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3ed0df1b11a79856df5ffcab572cbd6b9627034c1c748c5566fa79df9048a7c5", size = 214833, upload-time = "2025-10-02T14:35:23.32Z" }, + { url = "https://files.pythonhosted.org/packages/7e/e9/ae27c8ffec8b953efa84c7c4a6c6802c263d587b9fc0d6e7cea64e08c3af/xxhash-3.6.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0e4edbfc7d420925b0dd5e792478ed393d6e75ff8fc219a6546fb446b6a417b1", size = 448348, upload-time = "2025-10-02T14:35:25.111Z" }, + { url = "https://files.pythonhosted.org/packages/d7/6b/33e21afb1b5b3f46b74b6bd1913639066af218d704cc0941404ca717fc57/xxhash-3.6.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fba27a198363a7ef87f8c0f6b171ec36b674fe9053742c58dd7e3201c1ab30ee", size = 196070, upload-time = "2025-10-02T14:35:26.586Z" }, + { url = "https://files.pythonhosted.org/packages/96/b6/fcabd337bc5fa624e7203aa0fa7d0c49eed22f72e93229431752bddc83d9/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:794fe9145fe60191c6532fa95063765529770edcdd67b3d537793e8004cabbfd", size = 212907, upload-time = "2025-10-02T14:35:28.087Z" }, + { url = "https://files.pythonhosted.org/packages/4b/d3/9ee6160e644d660fcf176c5825e61411c7f62648728f69c79ba237250143/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:6105ef7e62b5ac73a837778efc331a591d8442f8ef5c7e102376506cb4ae2729", size = 200839, upload-time = "2025-10-02T14:35:29.857Z" }, + { url = "https://files.pythonhosted.org/packages/0d/98/e8de5baa5109394baf5118f5e72ab21a86387c4f89b0e77ef3e2f6b0327b/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:f01375c0e55395b814a679b3eea205db7919ac2af213f4a6682e01220e5fe292", size = 213304, upload-time = "2025-10-02T14:35:31.222Z" }, + { url = "https://files.pythonhosted.org/packages/7b/1d/71056535dec5c3177eeb53e38e3d367dd1d16e024e63b1cee208d572a033/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:d706dca2d24d834a4661619dcacf51a75c16d65985718d6a7d73c1eeeb903ddf", size = 416930, upload-time = "2025-10-02T14:35:32.517Z" }, + { url = "https://files.pythonhosted.org/packages/dc/6c/5cbde9de2cd967c322e651c65c543700b19e7ae3e0aae8ece3469bf9683d/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5f059d9faeacd49c0215d66f4056e1326c80503f51a1532ca336a385edadd033", size = 193787, upload-time = "2025-10-02T14:35:33.827Z" }, + { url = "https://files.pythonhosted.org/packages/19/fa/0172e350361d61febcea941b0cc541d6e6c8d65d153e85f850a7b256ff8a/xxhash-3.6.0-cp313-cp313t-win32.whl", hash = "sha256:1244460adc3a9be84731d72b8e80625788e5815b68da3da8b83f78115a40a7ec", size = 30916, upload-time = "2025-10-02T14:35:35.107Z" }, + { url = "https://files.pythonhosted.org/packages/ad/e6/e8cf858a2b19d6d45820f072eff1bea413910592ff17157cabc5f1227a16/xxhash-3.6.0-cp313-cp313t-win_amd64.whl", hash = "sha256:b1e420ef35c503869c4064f4a2f2b08ad6431ab7b229a05cce39d74268bca6b8", size = 31799, upload-time = "2025-10-02T14:35:36.165Z" }, + { url = "https://files.pythonhosted.org/packages/56/15/064b197e855bfb7b343210e82490ae672f8bc7cdf3ddb02e92f64304ee8a/xxhash-3.6.0-cp313-cp313t-win_arm64.whl", hash = "sha256:ec44b73a4220623235f67a996c862049f375df3b1052d9899f40a6382c32d746", size = 28044, upload-time = "2025-10-02T14:35:37.195Z" }, + { url = "https://files.pythonhosted.org/packages/7e/5e/0138bc4484ea9b897864d59fce9be9086030825bc778b76cb5a33a906d37/xxhash-3.6.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:a40a3d35b204b7cc7643cbcf8c9976d818cb47befcfac8bbefec8038ac363f3e", size = 32754, upload-time = "2025-10-02T14:35:38.245Z" }, + { url = "https://files.pythonhosted.org/packages/18/d7/5dac2eb2ec75fd771957a13e5dda560efb2176d5203f39502a5fc571f899/xxhash-3.6.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a54844be970d3fc22630b32d515e79a90d0a3ddb2644d8d7402e3c4c8da61405", size = 30846, upload-time = "2025-10-02T14:35:39.6Z" }, + { url = "https://files.pythonhosted.org/packages/fe/71/8bc5be2bb00deb5682e92e8da955ebe5fa982da13a69da5a40a4c8db12fb/xxhash-3.6.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:016e9190af8f0a4e3741343777710e3d5717427f175adfdc3e72508f59e2a7f3", size = 194343, upload-time = "2025-10-02T14:35:40.69Z" }, + { url = "https://files.pythonhosted.org/packages/e7/3b/52badfb2aecec2c377ddf1ae75f55db3ba2d321c5e164f14461c90837ef3/xxhash-3.6.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4f6f72232f849eb9d0141e2ebe2677ece15adfd0fa599bc058aad83c714bb2c6", size = 213074, upload-time = "2025-10-02T14:35:42.29Z" }, + { url = "https://files.pythonhosted.org/packages/a2/2b/ae46b4e9b92e537fa30d03dbc19cdae57ed407e9c26d163895e968e3de85/xxhash-3.6.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:63275a8aba7865e44b1813d2177e0f5ea7eadad3dd063a21f7cf9afdc7054063", size = 212388, upload-time = "2025-10-02T14:35:43.929Z" }, + { url = "https://files.pythonhosted.org/packages/f5/80/49f88d3afc724b4ac7fbd664c8452d6db51b49915be48c6982659e0e7942/xxhash-3.6.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cd01fa2aa00d8b017c97eb46b9a794fbdca53fc14f845f5a328c71254b0abb7", size = 445614, upload-time = "2025-10-02T14:35:45.216Z" }, + { url = "https://files.pythonhosted.org/packages/ed/ba/603ce3961e339413543d8cd44f21f2c80e2a7c5cfe692a7b1f2cccf58f3c/xxhash-3.6.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0226aa89035b62b6a86d3c68df4d7c1f47a342b8683da2b60cedcddb46c4d95b", size = 194024, upload-time = "2025-10-02T14:35:46.959Z" }, + { url = "https://files.pythonhosted.org/packages/78/d1/8e225ff7113bf81545cfdcd79eef124a7b7064a0bba53605ff39590b95c2/xxhash-3.6.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c6e193e9f56e4ca4923c61238cdaced324f0feac782544eb4c6d55ad5cc99ddd", size = 210541, upload-time = "2025-10-02T14:35:48.301Z" }, + { url = "https://files.pythonhosted.org/packages/6f/58/0f89d149f0bad89def1a8dd38feb50ccdeb643d9797ec84707091d4cb494/xxhash-3.6.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9176dcaddf4ca963d4deb93866d739a343c01c969231dbe21680e13a5d1a5bf0", size = 198305, upload-time = "2025-10-02T14:35:49.584Z" }, + { url = "https://files.pythonhosted.org/packages/11/38/5eab81580703c4df93feb5f32ff8fa7fe1e2c51c1f183ee4e48d4bb9d3d7/xxhash-3.6.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c1ce4009c97a752e682b897aa99aef84191077a9433eb237774689f14f8ec152", size = 210848, upload-time = "2025-10-02T14:35:50.877Z" }, + { url = "https://files.pythonhosted.org/packages/5e/6b/953dc4b05c3ce678abca756416e4c130d2382f877a9c30a20d08ee6a77c0/xxhash-3.6.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:8cb2f4f679b01513b7adbb9b1b2f0f9cdc31b70007eaf9d59d0878809f385b11", size = 414142, upload-time = "2025-10-02T14:35:52.15Z" }, + { url = "https://files.pythonhosted.org/packages/08/a9/238ec0d4e81a10eb5026d4a6972677cbc898ba6c8b9dbaec12ae001b1b35/xxhash-3.6.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:653a91d7c2ab54a92c19ccf43508b6a555440b9be1bc8be553376778be7f20b5", size = 191547, upload-time = "2025-10-02T14:35:53.547Z" }, + { url = "https://files.pythonhosted.org/packages/f1/ee/3cf8589e06c2164ac77c3bf0aa127012801128f1feebf2a079272da5737c/xxhash-3.6.0-cp314-cp314-win32.whl", hash = "sha256:a756fe893389483ee8c394d06b5ab765d96e68fbbfe6fde7aa17e11f5720559f", size = 31214, upload-time = "2025-10-02T14:35:54.746Z" }, + { url = "https://files.pythonhosted.org/packages/02/5d/a19552fbc6ad4cb54ff953c3908bbc095f4a921bc569433d791f755186f1/xxhash-3.6.0-cp314-cp314-win_amd64.whl", hash = "sha256:39be8e4e142550ef69629c9cd71b88c90e9a5db703fecbcf265546d9536ca4ad", size = 32290, upload-time = "2025-10-02T14:35:55.791Z" }, + { url = "https://files.pythonhosted.org/packages/b1/11/dafa0643bc30442c887b55baf8e73353a344ee89c1901b5a5c54a6c17d39/xxhash-3.6.0-cp314-cp314-win_arm64.whl", hash = "sha256:25915e6000338999236f1eb68a02a32c3275ac338628a7eaa5a269c401995679", size = 28795, upload-time = "2025-10-02T14:35:57.162Z" }, + { url = "https://files.pythonhosted.org/packages/2c/db/0e99732ed7f64182aef4a6fb145e1a295558deec2a746265dcdec12d191e/xxhash-3.6.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c5294f596a9017ca5a3e3f8884c00b91ab2ad2933cf288f4923c3fd4346cf3d4", size = 32955, upload-time = "2025-10-02T14:35:58.267Z" }, + { url = "https://files.pythonhosted.org/packages/55/f4/2a7c3c68e564a099becfa44bb3d398810cc0ff6749b0d3cb8ccb93f23c14/xxhash-3.6.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1cf9dcc4ab9cff01dfbba78544297a3a01dafd60f3bde4e2bfd016cf7e4ddc67", size = 31072, upload-time = "2025-10-02T14:35:59.382Z" }, + { url = "https://files.pythonhosted.org/packages/c6/d9/72a29cddc7250e8a5819dad5d466facb5dc4c802ce120645630149127e73/xxhash-3.6.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:01262da8798422d0685f7cef03b2bd3f4f46511b02830861df548d7def4402ad", size = 196579, upload-time = "2025-10-02T14:36:00.838Z" }, + { url = "https://files.pythonhosted.org/packages/63/93/b21590e1e381040e2ca305a884d89e1c345b347404f7780f07f2cdd47ef4/xxhash-3.6.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51a73fb7cb3a3ead9f7a8b583ffd9b8038e277cdb8cb87cf890e88b3456afa0b", size = 215854, upload-time = "2025-10-02T14:36:02.207Z" }, + { url = "https://files.pythonhosted.org/packages/ce/b8/edab8a7d4fa14e924b29be877d54155dcbd8b80be85ea00d2be3413a9ed4/xxhash-3.6.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b9c6df83594f7df8f7f708ce5ebeacfc69f72c9fbaaababf6cf4758eaada0c9b", size = 214965, upload-time = "2025-10-02T14:36:03.507Z" }, + { url = "https://files.pythonhosted.org/packages/27/67/dfa980ac7f0d509d54ea0d5a486d2bb4b80c3f1bb22b66e6a05d3efaf6c0/xxhash-3.6.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:627f0af069b0ea56f312fd5189001c24578868643203bca1abbc2c52d3a6f3ca", size = 448484, upload-time = "2025-10-02T14:36:04.828Z" }, + { url = "https://files.pythonhosted.org/packages/8c/63/8ffc2cc97e811c0ca5d00ab36604b3ea6f4254f20b7bc658ca825ce6c954/xxhash-3.6.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aa912c62f842dfd013c5f21a642c9c10cd9f4c4e943e0af83618b4a404d9091a", size = 196162, upload-time = "2025-10-02T14:36:06.182Z" }, + { url = "https://files.pythonhosted.org/packages/4b/77/07f0e7a3edd11a6097e990f6e5b815b6592459cb16dae990d967693e6ea9/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:b465afd7909db30168ab62afe40b2fcf79eedc0b89a6c0ab3123515dc0df8b99", size = 213007, upload-time = "2025-10-02T14:36:07.733Z" }, + { url = "https://files.pythonhosted.org/packages/ae/d8/bc5fa0d152837117eb0bef6f83f956c509332ce133c91c63ce07ee7c4873/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:a881851cf38b0a70e7c4d3ce81fc7afd86fbc2a024f4cfb2a97cf49ce04b75d3", size = 200956, upload-time = "2025-10-02T14:36:09.106Z" }, + { url = "https://files.pythonhosted.org/packages/26/a5/d749334130de9411783873e9b98ecc46688dad5db64ca6e04b02acc8b473/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:9b3222c686a919a0f3253cfc12bb118b8b103506612253b5baeaac10d8027cf6", size = 213401, upload-time = "2025-10-02T14:36:10.585Z" }, + { url = "https://files.pythonhosted.org/packages/89/72/abed959c956a4bfc72b58c0384bb7940663c678127538634d896b1195c10/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:c5aa639bc113e9286137cec8fadc20e9cd732b2cc385c0b7fa673b84fc1f2a93", size = 417083, upload-time = "2025-10-02T14:36:12.276Z" }, + { url = "https://files.pythonhosted.org/packages/0c/b3/62fd2b586283b7d7d665fb98e266decadf31f058f1cf6c478741f68af0cb/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5c1343d49ac102799905e115aee590183c3921d475356cb24b4de29a4bc56518", size = 193913, upload-time = "2025-10-02T14:36:14.025Z" }, + { url = "https://files.pythonhosted.org/packages/9a/9a/c19c42c5b3f5a4aad748a6d5b4f23df3bed7ee5445accc65a0fb3ff03953/xxhash-3.6.0-cp314-cp314t-win32.whl", hash = "sha256:5851f033c3030dd95c086b4a36a2683c2ff4a799b23af60977188b057e467119", size = 31586, upload-time = "2025-10-02T14:36:15.603Z" }, + { url = "https://files.pythonhosted.org/packages/03/d6/4cc450345be9924fd5dc8c590ceda1db5b43a0a889587b0ae81a95511360/xxhash-3.6.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0444e7967dac37569052d2409b00a8860c2135cff05502df4da80267d384849f", size = 32526, upload-time = "2025-10-02T14:36:16.708Z" }, + { url = "https://files.pythonhosted.org/packages/0f/c9/7243eb3f9eaabd1a88a5a5acadf06df2d83b100c62684b7425c6a11bcaa8/xxhash-3.6.0-cp314-cp314t-win_arm64.whl", hash = "sha256:bb79b1e63f6fd84ec778a4b1916dfe0a7c3fdb986c06addd5db3a0d413819d95", size = 28898, upload-time = "2025-10-02T14:36:17.843Z" }, + { url = "https://files.pythonhosted.org/packages/93/1e/8aec23647a34a249f62e2398c42955acd9b4c6ed5cf08cbea94dc46f78d2/xxhash-3.6.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0f7b7e2ec26c1666ad5fc9dbfa426a6a3367ceaf79db5dd76264659d509d73b0", size = 30662, upload-time = "2025-10-02T14:37:01.743Z" }, + { url = "https://files.pythonhosted.org/packages/b8/0b/b14510b38ba91caf43006209db846a696ceea6a847a0c9ba0a5b1adc53d6/xxhash-3.6.0-pp311-pypy311_pp73-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5dc1e14d14fa0f5789ec29a7062004b5933964bb9b02aae6622b8f530dc40296", size = 41056, upload-time = "2025-10-02T14:37:02.879Z" }, + { url = "https://files.pythonhosted.org/packages/50/55/15a7b8a56590e66ccd374bbfa3f9ffc45b810886c8c3b614e3f90bd2367c/xxhash-3.6.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:881b47fc47e051b37d94d13e7455131054b56749b91b508b0907eb07900d1c13", size = 36251, upload-time = "2025-10-02T14:37:04.44Z" }, + { url = "https://files.pythonhosted.org/packages/62/b2/5ac99a041a29e58e95f907876b04f7067a0242cb85b5f39e726153981503/xxhash-3.6.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c6dc31591899f5e5666f04cc2e529e69b4072827085c1ef15294d91a004bc1bd", size = 32481, upload-time = "2025-10-02T14:37:05.869Z" }, + { url = "https://files.pythonhosted.org/packages/7b/d9/8d95e906764a386a3d3b596f3c68bb63687dfca806373509f51ce8eea81f/xxhash-3.6.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:15e0dac10eb9309508bfc41f7f9deaa7755c69e35af835db9cb10751adebc35d", size = 31565, upload-time = "2025-10-02T14:37:06.966Z" }, +] + +[[package]] +name = "zstandard" +version = "0.25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fd/aa/3e0508d5a5dd96529cdc5a97011299056e14c6505b678fd58938792794b1/zstandard-0.25.0.tar.gz", hash = "sha256:7713e1179d162cf5c7906da876ec2ccb9c3a9dcbdffef0cc7f70c3667a205f0b", size = 711513, upload-time = "2025-09-14T22:15:54.002Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/83/c3ca27c363d104980f1c9cee1101cc8ba724ac8c28a033ede6aab89585b1/zstandard-0.25.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:933b65d7680ea337180733cf9e87293cc5500cc0eb3fc8769f4d3c88d724ec5c", size = 795254, upload-time = "2025-09-14T22:16:26.137Z" }, + { url = "https://files.pythonhosted.org/packages/ac/4d/e66465c5411a7cf4866aeadc7d108081d8ceba9bc7abe6b14aa21c671ec3/zstandard-0.25.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3f79487c687b1fc69f19e487cd949bf3aae653d181dfb5fde3bf6d18894706f", size = 640559, upload-time = "2025-09-14T22:16:27.973Z" }, + { url = "https://files.pythonhosted.org/packages/12/56/354fe655905f290d3b147b33fe946b0f27e791e4b50a5f004c802cb3eb7b/zstandard-0.25.0-cp311-cp311-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:0bbc9a0c65ce0eea3c34a691e3c4b6889f5f3909ba4822ab385fab9057099431", size = 5348020, upload-time = "2025-09-14T22:16:29.523Z" }, + { url = "https://files.pythonhosted.org/packages/3b/13/2b7ed68bd85e69a2069bcc72141d378f22cae5a0f3b353a2c8f50ef30c1b/zstandard-0.25.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:01582723b3ccd6939ab7b3a78622c573799d5d8737b534b86d0e06ac18dbde4a", size = 5058126, upload-time = "2025-09-14T22:16:31.811Z" }, + { url = "https://files.pythonhosted.org/packages/c9/dd/fdaf0674f4b10d92cb120ccff58bbb6626bf8368f00ebfd2a41ba4a0dc99/zstandard-0.25.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5f1ad7bf88535edcf30038f6919abe087f606f62c00a87d7e33e7fc57cb69fcc", size = 5405390, upload-time = "2025-09-14T22:16:33.486Z" }, + { url = "https://files.pythonhosted.org/packages/0f/67/354d1555575bc2490435f90d67ca4dd65238ff2f119f30f72d5cde09c2ad/zstandard-0.25.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:06acb75eebeedb77b69048031282737717a63e71e4ae3f77cc0c3b9508320df6", size = 5452914, upload-time = "2025-09-14T22:16:35.277Z" }, + { url = "https://files.pythonhosted.org/packages/bb/1f/e9cfd801a3f9190bf3e759c422bbfd2247db9d7f3d54a56ecde70137791a/zstandard-0.25.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9300d02ea7c6506f00e627e287e0492a5eb0371ec1670ae852fefffa6164b072", size = 5559635, upload-time = "2025-09-14T22:16:37.141Z" }, + { url = "https://files.pythonhosted.org/packages/21/88/5ba550f797ca953a52d708c8e4f380959e7e3280af029e38fbf47b55916e/zstandard-0.25.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:bfd06b1c5584b657a2892a6014c2f4c20e0db0208c159148fa78c65f7e0b0277", size = 5048277, upload-time = "2025-09-14T22:16:38.807Z" }, + { url = "https://files.pythonhosted.org/packages/46/c0/ca3e533b4fa03112facbe7fbe7779cb1ebec215688e5df576fe5429172e0/zstandard-0.25.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f373da2c1757bb7f1acaf09369cdc1d51d84131e50d5fa9863982fd626466313", size = 5574377, upload-time = "2025-09-14T22:16:40.523Z" }, + { url = "https://files.pythonhosted.org/packages/12/9b/3fb626390113f272abd0799fd677ea33d5fc3ec185e62e6be534493c4b60/zstandard-0.25.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6c0e5a65158a7946e7a7affa6418878ef97ab66636f13353b8502d7ea03c8097", size = 4961493, upload-time = "2025-09-14T22:16:43.3Z" }, + { url = "https://files.pythonhosted.org/packages/cb/d3/23094a6b6a4b1343b27ae68249daa17ae0651fcfec9ed4de09d14b940285/zstandard-0.25.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c8e167d5adf59476fa3e37bee730890e389410c354771a62e3c076c86f9f7778", size = 5269018, upload-time = "2025-09-14T22:16:45.292Z" }, + { url = "https://files.pythonhosted.org/packages/8c/a7/bb5a0c1c0f3f4b5e9d5b55198e39de91e04ba7c205cc46fcb0f95f0383c1/zstandard-0.25.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:98750a309eb2f020da61e727de7d7ba3c57c97cf6213f6f6277bb7fb42a8e065", size = 5443672, upload-time = "2025-09-14T22:16:47.076Z" }, + { url = "https://files.pythonhosted.org/packages/27/22/503347aa08d073993f25109c36c8d9f029c7d5949198050962cb568dfa5e/zstandard-0.25.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:22a086cff1b6ceca18a8dd6096ec631e430e93a8e70a9ca5efa7561a00f826fa", size = 5822753, upload-time = "2025-09-14T22:16:49.316Z" }, + { url = "https://files.pythonhosted.org/packages/e2/be/94267dc6ee64f0f8ba2b2ae7c7a2df934a816baaa7291db9e1aa77394c3c/zstandard-0.25.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:72d35d7aa0bba323965da807a462b0966c91608ef3a48ba761678cb20ce5d8b7", size = 5366047, upload-time = "2025-09-14T22:16:51.328Z" }, + { url = "https://files.pythonhosted.org/packages/7b/a3/732893eab0a3a7aecff8b99052fecf9f605cf0fb5fb6d0290e36beee47a4/zstandard-0.25.0-cp311-cp311-win32.whl", hash = "sha256:f5aeea11ded7320a84dcdd62a3d95b5186834224a9e55b92ccae35d21a8b63d4", size = 436484, upload-time = "2025-09-14T22:16:55.005Z" }, + { url = "https://files.pythonhosted.org/packages/43/a3/c6155f5c1cce691cb80dfd38627046e50af3ee9ddc5d0b45b9b063bfb8c9/zstandard-0.25.0-cp311-cp311-win_amd64.whl", hash = "sha256:daab68faadb847063d0c56f361a289c4f268706b598afbf9ad113cbe5c38b6b2", size = 506183, upload-time = "2025-09-14T22:16:52.753Z" }, + { url = "https://files.pythonhosted.org/packages/8c/3e/8945ab86a0820cc0e0cdbf38086a92868a9172020fdab8a03ac19662b0e5/zstandard-0.25.0-cp311-cp311-win_arm64.whl", hash = "sha256:22a06c5df3751bb7dc67406f5374734ccee8ed37fc5981bf1ad7041831fa1137", size = 462533, upload-time = "2025-09-14T22:16:53.878Z" }, + { url = "https://files.pythonhosted.org/packages/82/fc/f26eb6ef91ae723a03e16eddb198abcfce2bc5a42e224d44cc8b6765e57e/zstandard-0.25.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7b3c3a3ab9daa3eed242d6ecceead93aebbb8f5f84318d82cee643e019c4b73b", size = 795738, upload-time = "2025-09-14T22:16:56.237Z" }, + { url = "https://files.pythonhosted.org/packages/aa/1c/d920d64b22f8dd028a8b90e2d756e431a5d86194caa78e3819c7bf53b4b3/zstandard-0.25.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:913cbd31a400febff93b564a23e17c3ed2d56c064006f54efec210d586171c00", size = 640436, upload-time = "2025-09-14T22:16:57.774Z" }, + { url = "https://files.pythonhosted.org/packages/53/6c/288c3f0bd9fcfe9ca41e2c2fbfd17b2097f6af57b62a81161941f09afa76/zstandard-0.25.0-cp312-cp312-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:011d388c76b11a0c165374ce660ce2c8efa8e5d87f34996aa80f9c0816698b64", size = 5343019, upload-time = "2025-09-14T22:16:59.302Z" }, + { url = "https://files.pythonhosted.org/packages/1e/15/efef5a2f204a64bdb5571e6161d49f7ef0fffdbca953a615efbec045f60f/zstandard-0.25.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6dffecc361d079bb48d7caef5d673c88c8988d3d33fb74ab95b7ee6da42652ea", size = 5063012, upload-time = "2025-09-14T22:17:01.156Z" }, + { url = "https://files.pythonhosted.org/packages/b7/37/a6ce629ffdb43959e92e87ebdaeebb5ac81c944b6a75c9c47e300f85abdf/zstandard-0.25.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:7149623bba7fdf7e7f24312953bcf73cae103db8cae49f8154dd1eadc8a29ecb", size = 5394148, upload-time = "2025-09-14T22:17:03.091Z" }, + { url = "https://files.pythonhosted.org/packages/e3/79/2bf870b3abeb5c070fe2d670a5a8d1057a8270f125ef7676d29ea900f496/zstandard-0.25.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:6a573a35693e03cf1d67799fd01b50ff578515a8aeadd4595d2a7fa9f3ec002a", size = 5451652, upload-time = "2025-09-14T22:17:04.979Z" }, + { url = "https://files.pythonhosted.org/packages/53/60/7be26e610767316c028a2cbedb9a3beabdbe33e2182c373f71a1c0b88f36/zstandard-0.25.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5a56ba0db2d244117ed744dfa8f6f5b366e14148e00de44723413b2f3938a902", size = 5546993, upload-time = "2025-09-14T22:17:06.781Z" }, + { url = "https://files.pythonhosted.org/packages/85/c7/3483ad9ff0662623f3648479b0380d2de5510abf00990468c286c6b04017/zstandard-0.25.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:10ef2a79ab8e2974e2075fb984e5b9806c64134810fac21576f0668e7ea19f8f", size = 5046806, upload-time = "2025-09-14T22:17:08.415Z" }, + { url = "https://files.pythonhosted.org/packages/08/b3/206883dd25b8d1591a1caa44b54c2aad84badccf2f1de9e2d60a446f9a25/zstandard-0.25.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:aaf21ba8fb76d102b696781bddaa0954b782536446083ae3fdaa6f16b25a1c4b", size = 5576659, upload-time = "2025-09-14T22:17:10.164Z" }, + { url = "https://files.pythonhosted.org/packages/9d/31/76c0779101453e6c117b0ff22565865c54f48f8bd807df2b00c2c404b8e0/zstandard-0.25.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1869da9571d5e94a85a5e8d57e4e8807b175c9e4a6294e3b66fa4efb074d90f6", size = 4953933, upload-time = "2025-09-14T22:17:11.857Z" }, + { url = "https://files.pythonhosted.org/packages/18/e1/97680c664a1bf9a247a280a053d98e251424af51f1b196c6d52f117c9720/zstandard-0.25.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:809c5bcb2c67cd0ed81e9229d227d4ca28f82d0f778fc5fea624a9def3963f91", size = 5268008, upload-time = "2025-09-14T22:17:13.627Z" }, + { url = "https://files.pythonhosted.org/packages/1e/73/316e4010de585ac798e154e88fd81bb16afc5c5cb1a72eeb16dd37e8024a/zstandard-0.25.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f27662e4f7dbf9f9c12391cb37b4c4c3cb90ffbd3b1fb9284dadbbb8935fa708", size = 5433517, upload-time = "2025-09-14T22:17:16.103Z" }, + { url = "https://files.pythonhosted.org/packages/5b/60/dd0f8cfa8129c5a0ce3ea6b7f70be5b33d2618013a161e1ff26c2b39787c/zstandard-0.25.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:99c0c846e6e61718715a3c9437ccc625de26593fea60189567f0118dc9db7512", size = 5814292, upload-time = "2025-09-14T22:17:17.827Z" }, + { url = "https://files.pythonhosted.org/packages/fc/5f/75aafd4b9d11b5407b641b8e41a57864097663699f23e9ad4dbb91dc6bfe/zstandard-0.25.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:474d2596a2dbc241a556e965fb76002c1ce655445e4e3bf38e5477d413165ffa", size = 5360237, upload-time = "2025-09-14T22:17:19.954Z" }, + { url = "https://files.pythonhosted.org/packages/ff/8d/0309daffea4fcac7981021dbf21cdb2e3427a9e76bafbcdbdf5392ff99a4/zstandard-0.25.0-cp312-cp312-win32.whl", hash = "sha256:23ebc8f17a03133b4426bcc04aabd68f8236eb78c3760f12783385171b0fd8bd", size = 436922, upload-time = "2025-09-14T22:17:24.398Z" }, + { url = "https://files.pythonhosted.org/packages/79/3b/fa54d9015f945330510cb5d0b0501e8253c127cca7ebe8ba46a965df18c5/zstandard-0.25.0-cp312-cp312-win_amd64.whl", hash = "sha256:ffef5a74088f1e09947aecf91011136665152e0b4b359c42be3373897fb39b01", size = 506276, upload-time = "2025-09-14T22:17:21.429Z" }, + { url = "https://files.pythonhosted.org/packages/ea/6b/8b51697e5319b1f9ac71087b0af9a40d8a6288ff8025c36486e0c12abcc4/zstandard-0.25.0-cp312-cp312-win_arm64.whl", hash = "sha256:181eb40e0b6a29b3cd2849f825e0fa34397f649170673d385f3598ae17cca2e9", size = 462679, upload-time = "2025-09-14T22:17:23.147Z" }, + { url = "https://files.pythonhosted.org/packages/35/0b/8df9c4ad06af91d39e94fa96cc010a24ac4ef1378d3efab9223cc8593d40/zstandard-0.25.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ec996f12524f88e151c339688c3897194821d7f03081ab35d31d1e12ec975e94", size = 795735, upload-time = "2025-09-14T22:17:26.042Z" }, + { url = "https://files.pythonhosted.org/packages/3f/06/9ae96a3e5dcfd119377ba33d4c42a7d89da1efabd5cb3e366b156c45ff4d/zstandard-0.25.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a1a4ae2dec3993a32247995bdfe367fc3266da832d82f8438c8570f989753de1", size = 640440, upload-time = "2025-09-14T22:17:27.366Z" }, + { url = "https://files.pythonhosted.org/packages/d9/14/933d27204c2bd404229c69f445862454dcc101cd69ef8c6068f15aaec12c/zstandard-0.25.0-cp313-cp313-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:e96594a5537722fdfb79951672a2a63aec5ebfb823e7560586f7484819f2a08f", size = 5343070, upload-time = "2025-09-14T22:17:28.896Z" }, + { url = "https://files.pythonhosted.org/packages/6d/db/ddb11011826ed7db9d0e485d13df79b58586bfdec56e5c84a928a9a78c1c/zstandard-0.25.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bfc4e20784722098822e3eee42b8e576b379ed72cca4a7cb856ae733e62192ea", size = 5063001, upload-time = "2025-09-14T22:17:31.044Z" }, + { url = "https://files.pythonhosted.org/packages/db/00/87466ea3f99599d02a5238498b87bf84a6348290c19571051839ca943777/zstandard-0.25.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:457ed498fc58cdc12fc48f7950e02740d4f7ae9493dd4ab2168a47c93c31298e", size = 5394120, upload-time = "2025-09-14T22:17:32.711Z" }, + { url = "https://files.pythonhosted.org/packages/2b/95/fc5531d9c618a679a20ff6c29e2b3ef1d1f4ad66c5e161ae6ff847d102a9/zstandard-0.25.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:fd7a5004eb1980d3cefe26b2685bcb0b17989901a70a1040d1ac86f1d898c551", size = 5451230, upload-time = "2025-09-14T22:17:34.41Z" }, + { url = "https://files.pythonhosted.org/packages/63/4b/e3678b4e776db00f9f7b2fe58e547e8928ef32727d7a1ff01dea010f3f13/zstandard-0.25.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8e735494da3db08694d26480f1493ad2cf86e99bdd53e8e9771b2752a5c0246a", size = 5547173, upload-time = "2025-09-14T22:17:36.084Z" }, + { url = "https://files.pythonhosted.org/packages/4e/d5/ba05ed95c6b8ec30bd468dfeab20589f2cf709b5c940483e31d991f2ca58/zstandard-0.25.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3a39c94ad7866160a4a46d772e43311a743c316942037671beb264e395bdd611", size = 5046736, upload-time = "2025-09-14T22:17:37.891Z" }, + { url = "https://files.pythonhosted.org/packages/50/d5/870aa06b3a76c73eced65c044b92286a3c4e00554005ff51962deef28e28/zstandard-0.25.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:172de1f06947577d3a3005416977cce6168f2261284c02080e7ad0185faeced3", size = 5576368, upload-time = "2025-09-14T22:17:40.206Z" }, + { url = "https://files.pythonhosted.org/packages/5d/35/398dc2ffc89d304d59bc12f0fdd931b4ce455bddf7038a0a67733a25f550/zstandard-0.25.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3c83b0188c852a47cd13ef3bf9209fb0a77fa5374958b8c53aaa699398c6bd7b", size = 4954022, upload-time = "2025-09-14T22:17:41.879Z" }, + { url = "https://files.pythonhosted.org/packages/9a/5c/36ba1e5507d56d2213202ec2b05e8541734af5f2ce378c5d1ceaf4d88dc4/zstandard-0.25.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1673b7199bbe763365b81a4f3252b8e80f44c9e323fc42940dc8843bfeaf9851", size = 5267889, upload-time = "2025-09-14T22:17:43.577Z" }, + { url = "https://files.pythonhosted.org/packages/70/e8/2ec6b6fb7358b2ec0113ae202647ca7c0e9d15b61c005ae5225ad0995df5/zstandard-0.25.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:0be7622c37c183406f3dbf0cba104118eb16a4ea7359eeb5752f0794882fc250", size = 5433952, upload-time = "2025-09-14T22:17:45.271Z" }, + { url = "https://files.pythonhosted.org/packages/7b/01/b5f4d4dbc59ef193e870495c6f1275f5b2928e01ff5a81fecb22a06e22fb/zstandard-0.25.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:5f5e4c2a23ca271c218ac025bd7d635597048b366d6f31f420aaeb715239fc98", size = 5814054, upload-time = "2025-09-14T22:17:47.08Z" }, + { url = "https://files.pythonhosted.org/packages/b2/e5/fbd822d5c6f427cf158316d012c5a12f233473c2f9c5fe5ab1ae5d21f3d8/zstandard-0.25.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f187a0bb61b35119d1926aee039524d1f93aaf38a9916b8c4b78ac8514a0aaf", size = 5360113, upload-time = "2025-09-14T22:17:48.893Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e0/69a553d2047f9a2c7347caa225bb3a63b6d7704ad74610cb7823baa08ed7/zstandard-0.25.0-cp313-cp313-win32.whl", hash = "sha256:7030defa83eef3e51ff26f0b7bfb229f0204b66fe18e04359ce3474ac33cbc09", size = 436936, upload-time = "2025-09-14T22:17:52.658Z" }, + { url = "https://files.pythonhosted.org/packages/d9/82/b9c06c870f3bd8767c201f1edbdf9e8dc34be5b0fbc5682c4f80fe948475/zstandard-0.25.0-cp313-cp313-win_amd64.whl", hash = "sha256:1f830a0dac88719af0ae43b8b2d6aef487d437036468ef3c2ea59c51f9d55fd5", size = 506232, upload-time = "2025-09-14T22:17:50.402Z" }, + { url = "https://files.pythonhosted.org/packages/d4/57/60c3c01243bb81d381c9916e2a6d9e149ab8627c0c7d7abb2d73384b3c0c/zstandard-0.25.0-cp313-cp313-win_arm64.whl", hash = "sha256:85304a43f4d513f5464ceb938aa02c1e78c2943b29f44a750b48b25ac999a049", size = 462671, upload-time = "2025-09-14T22:17:51.533Z" }, + { url = "https://files.pythonhosted.org/packages/3d/5c/f8923b595b55fe49e30612987ad8bf053aef555c14f05bb659dd5dbe3e8a/zstandard-0.25.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e29f0cf06974c899b2c188ef7f783607dbef36da4c242eb6c82dcd8b512855e3", size = 795887, upload-time = "2025-09-14T22:17:54.198Z" }, + { url = "https://files.pythonhosted.org/packages/8d/09/d0a2a14fc3439c5f874042dca72a79c70a532090b7ba0003be73fee37ae2/zstandard-0.25.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:05df5136bc5a011f33cd25bc9f506e7426c0c9b3f9954f056831ce68f3b6689f", size = 640658, upload-time = "2025-09-14T22:17:55.423Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7c/8b6b71b1ddd517f68ffb55e10834388d4f793c49c6b83effaaa05785b0b4/zstandard-0.25.0-cp314-cp314-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:f604efd28f239cc21b3adb53eb061e2a205dc164be408e553b41ba2ffe0ca15c", size = 5379849, upload-time = "2025-09-14T22:17:57.372Z" }, + { url = "https://files.pythonhosted.org/packages/a4/86/a48e56320d0a17189ab7a42645387334fba2200e904ee47fc5a26c1fd8ca/zstandard-0.25.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223415140608d0f0da010499eaa8ccdb9af210a543fac54bce15babbcfc78439", size = 5058095, upload-time = "2025-09-14T22:17:59.498Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ad/eb659984ee2c0a779f9d06dbfe45e2dc39d99ff40a319895df2d3d9a48e5/zstandard-0.25.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2e54296a283f3ab5a26fc9b8b5d4978ea0532f37b231644f367aa588930aa043", size = 5551751, upload-time = "2025-09-14T22:18:01.618Z" }, + { url = "https://files.pythonhosted.org/packages/61/b3/b637faea43677eb7bd42ab204dfb7053bd5c4582bfe6b1baefa80ac0c47b/zstandard-0.25.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ca54090275939dc8ec5dea2d2afb400e0f83444b2fc24e07df7fdef677110859", size = 6364818, upload-time = "2025-09-14T22:18:03.769Z" }, + { url = "https://files.pythonhosted.org/packages/31/dc/cc50210e11e465c975462439a492516a73300ab8caa8f5e0902544fd748b/zstandard-0.25.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e09bb6252b6476d8d56100e8147b803befa9a12cea144bbe629dd508800d1ad0", size = 5560402, upload-time = "2025-09-14T22:18:05.954Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ae/56523ae9c142f0c08efd5e868a6da613ae76614eca1305259c3bf6a0ed43/zstandard-0.25.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a9ec8c642d1ec73287ae3e726792dd86c96f5681eb8df274a757bf62b750eae7", size = 4955108, upload-time = "2025-09-14T22:18:07.68Z" }, + { url = "https://files.pythonhosted.org/packages/98/cf/c899f2d6df0840d5e384cf4c4121458c72802e8bda19691f3b16619f51e9/zstandard-0.25.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a4089a10e598eae6393756b036e0f419e8c1d60f44a831520f9af41c14216cf2", size = 5269248, upload-time = "2025-09-14T22:18:09.753Z" }, + { url = "https://files.pythonhosted.org/packages/1b/c0/59e912a531d91e1c192d3085fc0f6fb2852753c301a812d856d857ea03c6/zstandard-0.25.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f67e8f1a324a900e75b5e28ffb152bcac9fbed1cc7b43f99cd90f395c4375344", size = 5430330, upload-time = "2025-09-14T22:18:11.966Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/7e31db1240de2df22a58e2ea9a93fc6e38cc29353e660c0272b6735d6669/zstandard-0.25.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:9654dbc012d8b06fc3d19cc825af3f7bf8ae242226df5f83936cb39f5fdc846c", size = 5811123, upload-time = "2025-09-14T22:18:13.907Z" }, + { url = "https://files.pythonhosted.org/packages/f6/49/fac46df5ad353d50535e118d6983069df68ca5908d4d65b8c466150a4ff1/zstandard-0.25.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4203ce3b31aec23012d3a4cf4a2ed64d12fea5269c49aed5e4c3611b938e4088", size = 5359591, upload-time = "2025-09-14T22:18:16.465Z" }, + { url = "https://files.pythonhosted.org/packages/c2/38/f249a2050ad1eea0bb364046153942e34abba95dd5520af199aed86fbb49/zstandard-0.25.0-cp314-cp314-win32.whl", hash = "sha256:da469dc041701583e34de852d8634703550348d5822e66a0c827d39b05365b12", size = 444513, upload-time = "2025-09-14T22:18:20.61Z" }, + { url = "https://files.pythonhosted.org/packages/3a/43/241f9615bcf8ba8903b3f0432da069e857fc4fd1783bd26183db53c4804b/zstandard-0.25.0-cp314-cp314-win_amd64.whl", hash = "sha256:c19bcdd826e95671065f8692b5a4aa95c52dc7a02a4c5a0cac46deb879a017a2", size = 516118, upload-time = "2025-09-14T22:18:17.849Z" }, + { url = "https://files.pythonhosted.org/packages/f0/ef/da163ce2450ed4febf6467d77ccb4cd52c4c30ab45624bad26ca0a27260c/zstandard-0.25.0-cp314-cp314-win_arm64.whl", hash = "sha256:d7541afd73985c630bafcd6338d2518ae96060075f9463d7dc14cfb33514383d", size = 476940, upload-time = "2025-09-14T22:18:19.088Z" }, +]